@cspeach/cli 1.1.48 → 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 (101) 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/help.js +1 -1
  11. package/dist/commands/project.js +301 -0
  12. package/dist/commands/team-plan.js +16 -6
  13. package/dist/commands/team.js +295 -68
  14. package/dist/doctor/checks/zcspeach.js +96 -0
  15. package/dist/journey/chain.js +489 -61
  16. package/dist/journey/clean-core.js +61 -36
  17. package/dist/journey/flow.js +75 -2
  18. package/dist/journey/people.js +37 -0
  19. package/dist/journey/rebuild.js +7 -6
  20. package/dist/journey/run.js +129 -42
  21. package/dist/journey/trigger.js +16 -10
  22. package/dist/one-shot.js +1 -1
  23. package/dist/project-context/render.js +7 -0
  24. package/dist/projects/clean-core-confirm.js +166 -44
  25. package/dist/projects/email-template.js +56 -0
  26. package/dist/projects/extract-cca.js +8 -7
  27. package/dist/projects/index.js +0 -1
  28. package/dist/projects/merge-cca.js +0 -19
  29. package/dist/projects/merge-upgrade.js +2 -2
  30. package/dist/projects/project-folder.js +2 -1
  31. package/dist/projects/save-command.js +51 -14
  32. package/dist/register/decision-rules.js +2 -1
  33. package/dist/register/export-files.js +18 -0
  34. package/dist/register/picture.js +3 -3
  35. package/dist/register/project-transport.js +125 -0
  36. package/dist/register/records.js +25 -13
  37. package/dist/register/rules-edit.js +5 -0
  38. package/dist/register/status-file/progress-chart.js +17 -6
  39. package/dist/register/status-file/register-map.js +8 -5
  40. package/dist/register/status-file/register-model.js +55 -36
  41. package/dist/register/status-file/register-parts.js +97 -82
  42. package/dist/register/status-file/register-status-file.js +16 -7
  43. package/dist/register/status-file/report-model.js +293 -205
  44. package/dist/register/status-file/team-css.js +1 -1
  45. package/dist/register/status-html.js +11 -4
  46. package/dist/register/store.js +2 -2
  47. package/dist/register/workbooks.js +24 -13
  48. package/dist/renderer/footer-line.js +10 -2
  49. package/dist/renderer/tool-labels.js +2 -2
  50. package/dist/repl/builtin-commands.js +1 -1
  51. package/dist/repl/continue-skill.js +5 -0
  52. package/dist/repl/level-command.js +6 -15
  53. package/dist/repl.js +21 -8
  54. package/dist/router/router-core.js +5 -3
  55. package/dist/skill-catalog.js +4 -7
  56. package/dist/tools/_team-work-path.js +53 -0
  57. package/dist/tools/approval.js +97 -19
  58. package/dist/tools/filesystem/file-edit.js +8 -1
  59. package/dist/tools/filesystem/file-write.js +8 -1
  60. package/dist/tools/local-files.js +31 -0
  61. package/dist/tools/project/_merge-shared.js +2 -2
  62. package/dist/tools/project/_team-tool-shared.js +5 -0
  63. package/dist/tools/project/cca_merge.js +1 -11
  64. package/dist/tools/project/rules_propose.js +2 -1
  65. package/dist/tools/project/team_findings.js +81 -28
  66. package/dist/tools/project/team_lane_move.js +127 -0
  67. package/dist/tools/project/upgrade_merge_progress.js +8 -4
  68. package/dist/tools/subagent/schedule_create.js +131 -0
  69. package/dist/tools/text-elements/tools.js +7 -1
  70. package/dist/tools/tool-loading.js +1 -1
  71. package/dist/tools/update-method-intercept.js +18 -1
  72. package/dist/tools/write-pipeline.js +19 -5
  73. package/dist/ui/approval-modal.js +6 -3
  74. package/dist/ui/error-emitter.js +21 -0
  75. package/dist/ui/footer-line.js +19 -1
  76. package/dist/ui/header.js +41 -0
  77. package/dist/ui/ink-classifier-route.js +19 -0
  78. package/dist/ui/rich-error-box.js +9 -0
  79. package/dist/ui/sidebar.js +12 -0
  80. package/dist/ui/status-emitter.js +19 -0
  81. package/dist/ui/status-line.js +62 -0
  82. package/dist/ui/status-row.js +38 -0
  83. package/dist/ui/todo-panel.js +85 -0
  84. package/dist/ui/widgets/ask-form.js +250 -0
  85. package/dist/ui/widgets/ask-question-modal.js +178 -0
  86. package/fiori/zlas-inspection-lr/package.json +26 -0
  87. package/fiori/zlas-inspection-lr/ui5-local.yaml +45 -0
  88. package/fiori/zlas-inspection-lr/ui5.yaml +41 -0
  89. package/fiori/zlas-inspection-lr/webapp/Component.js +12 -0
  90. package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +9 -0
  91. package/fiori/zlas-inspection-lr/webapp/index.html +35 -0
  92. package/fiori/zlas-inspection-lr/webapp/manifest.json +145 -0
  93. package/live-check-2026-10-17-1343.txt +41 -0
  94. package/package.json +121 -121
  95. package/test-harness/fe-draft/app.ts +0 -188
  96. package/test-harness/fe-draft/fe-draft.e2e.test.ts +0 -461
  97. package/test-harness/fe-draft/manifest-enhancer.unit.test.ts +0 -51
  98. package/test-harness/fe-draft/routes.ts +0 -224
  99. package/test-harness/fe-draft/routes.unit.test.ts +0 -45
  100. package/test-harness/fe-draft/server.ts +0 -225
  101. package/test-harness/fe-draft/variants.ts +0 -196
@@ -13,27 +13,41 @@
13
13
  * Every queued command is built by code (run.ts triageCommand / refactorCommand).
14
14
  * Headless runs nothing and queues nothing: runJourneyStage says the next step.
15
15
  */
16
- import { estateSummaryOf } from '@cspeach/register-core';
17
- import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, runTeamCommand, whoAmI } from '../commands/team.js';
18
- import { activeProject, readEstate, readProjectFile, VARIANT_NAME } from '../register/store.js';
16
+ import { estateSummaryOf, viewWords } from '@cspeach/register-core';
17
+ import { DEFAULT_ATC_VARIANT, FIRST_CHECK_PER_PACKAGE_LINE, FIRST_CHECK_WHOLE_MAX, isLead, ruleObject, runTeamCommand, whoAmI } from '../commands/team.js';
18
+ import { activeProject, readBaseline, readEstate, readProjectFile, VARIANT_NAME } from '../register/store.js';
19
19
  import { findingsByObject } from '../register/findings.js';
20
20
  import { shown, shownUpTo } from '../register/atc-file.js';
21
21
  import { cspeachWritesThisSession } from '../register/offer.js';
22
22
  import { cardSlotEmitter } from '../ui/card-slot.js';
23
- import { claimedSinceCheck, decidePage, fixFailedCheck, folderKind, provenForTarget, readJourneyFacts, stageOf, stepOf, STEPS, toDecide, toFix, toRedesign } from './clean-core.js';
23
+ import { decidePage, folderKind, journeyView, readJourneyFacts, stageOf, stepOf, STEPS, toDecide, toFix, toRedesign } from './clean-core.js';
24
24
  import { handOffRebuild, rebuildCard, rebuildGroups } from './rebuild.js';
25
- import { addJourneyLeftOut, addJourneySkips, clearJourneyFlow, forgetNotNow, getJourneyFlow, journeyHopLine, journeySkips, journeyStepKey, noteJourneyStop, pauseJourneyFlow, setJourneyAt, setJourneyPage, setJourneyQueued, startJourneyFlow, } from './flow.js';
25
+ import { addJourneyLeftOut, addJourneySkips, clearJourneyFlow, forgetDoneFingerprint, forgetNotNow, forgetSkippedRows, getJourneyFlow, journeyHopLine, journeySkips, journeyStepKey, folderReadThisSession, markFolderRead, markJourneyEnded, markSkippedAsked, noteJourneyStop, pauseJourneyFlow, setJourneyAt, setJourneyDevelopers, setJourneyPage, setJourneyQueued, startJourneyFlow, } from './flow.js';
26
+ import { parsePeople } from './people.js';
26
27
  import { recordFullNotice } from '../renderer/notice-log.js';
27
- import { firstCheckPerPackage, needsFirstCheck, notRecordedLine, passedOverLine, refactorCommand, runJourneyStage, shownReason } from './run.js';
28
- import { cleanKeepReason, FACT_ROWS, KEEP_QUESTION } from '../projects/clean-core-confirm.js';
28
+ import { RISK_NOTE } from '../approvals/risk-floor.js';
29
+ import { firstCheckPerPackage, needsFirstCheck, notInViewLine, notRecordedLine, passedOverLine, refactorCommand, runJourneyStage, shownReason } from './run.js';
30
+ import { cleanKeepReason, FACT_ROWS, KEEP_QUESTION, RETIRE_QUESTION, retireNeedsLine } from '../projects/clean-core-confirm.js';
29
31
  import { sessionTeamDeps } from './session-deps.js';
30
32
  import { lookupVariant, variantLookupLine, variantWhat, VARIANT_TITLE } from '../register/variant.js';
33
+ import { isModifiable } from '@cspeach/sap-client';
34
+ import { notARequestLine, TRANSPORT_REQUEST } from '../register/project-transport.js';
31
35
  /* ── words ──────────────────────────────────────────────────────────────── */
32
36
  export const PAUSED_LINE = (step) => step ? `Paused at step ${step}. Say carry on to continue.` : 'Paused. Say carry on to continue.';
33
37
  export const LOOK_TITLE = 'Look at the system?';
34
38
  export const START_TITLE = 'Start the project here?';
35
39
  export const NOTHING_READ_LINE = 'Nothing was read from the system.';
36
40
  export const NOT_STARTED_LINE = 'No project was started.';
41
+ /** B1: asked at every project start, before "Start the project here?". */
42
+ export const PEOPLE_TITLE = 'Anyone else working on this?';
43
+ export const PEOPLE_QUESTION = 'Who else? Full names, comma separated — for example: James Miller, Emily Parker';
44
+ /** R8: said before a read that can take a while (a project folder read cold, or a big one). */
45
+ export const READING_LINE = 'Reading the folder…';
46
+ /** R8: a baseline above this many objects is read slowly enough to say so on every run. */
47
+ const READING_BIG = 200;
48
+ export const JUST_YOU_LINE = 'Carrying on with just you.';
49
+ /** One person's line on the people cards and the start card. */
50
+ const shareFact = (name) => `${shown(name)}: a share of the work, set at the start.`;
37
51
  /** I15: the start above 500 objects measured nothing; this card offers the first check per package. */
38
52
  export const FIRST_CHECK_TITLE = 'Run the first check per package now?';
39
53
  /**
@@ -158,9 +172,14 @@ function queueStep(h, command) {
158
172
  const line = journeyHopLine(h.session, command);
159
173
  h.queueNext(command, { by: 'journey', ...(line ? { line } : {}) });
160
174
  }
161
- /** End the chain: optional last line, the guarded run off. */
162
- function end(h, line) {
175
+ /**
176
+ * End the chain: optional last line, the guarded run off. `done`: it ended at
177
+ * DONE, whose line says "say carry on" — so bare "carry on" picks it up (B8).
178
+ */
179
+ function end(h, line, done = false) {
163
180
  clearJourneyFlow(h.session);
181
+ if (done)
182
+ markJourneyEnded(h.session);
164
183
  if (line)
165
184
  h.emit(line);
166
185
  }
@@ -227,7 +246,13 @@ export async function enterJourney(trigger, h) {
227
246
  return;
228
247
  }
229
248
  startJourneyFlow(h.session, trigger.kind === 'start' ? trigger.packages : []);
249
+ // B8: a start sentence is a new run: its DONE never reads as "nothing left" against an earlier one.
250
+ if (trigger.kind === 'start')
251
+ forgetDoneFingerprint(h.session);
230
252
  forgetNotNow(h.session);
253
+ // G3: "carry on" asks the rows skipped again (a start sentence keeps them put off).
254
+ if (trigger.kind === 'resume')
255
+ forgetSkippedRows(h.session);
231
256
  try {
232
257
  await drive(h);
233
258
  }
@@ -269,36 +294,38 @@ function startScope(cwd, packages) {
269
294
  /** The card shows this much of a recorded reason, wrapped over up to FACT_ROWS rows. */
270
295
  export const CARD_REASON = 300;
271
296
  const LEVEL = /^[A-D]$/;
297
+ /** The view's words with a capital first letter, for a fact line. */
298
+ const sentence = (words) => words.charAt(0).toUpperCase() + words.slice(1);
272
299
  /**
273
- * Every fix or rebuild is recorded with target A when none was given
274
- * (register/records.ts, `cspeach team decide`); register-core keeps what it is
275
- * handed, so a recorded A cannot be told from a default. Only another level
276
- * was set on purpose, and only that one is said.
277
- */
278
- const DEFAULT_TARGET = 'A';
279
- /**
280
- * The facts of the fix card, from the register only (no SAP call): the open
281
- * findings, a failed check, why (the decision's reason as written, cleaned
282
- * for the screen, at most 300 characters, wrapped on the card), the target level when one was set
283
- * on purpose, the approval promise, and where this object stands in the list.
300
+ * The facts of the fix card, from the project view only (no SAP call): the
301
+ * open findings, a failed check, the decision's reason in the view's card
302
+ * words (`viewWords.cardReason`: "Proposed reason:", "Why:" or "Reason
303
+ * recorded:"; cleaned for the screen, at most 300 characters, wrapped on the
304
+ * card), the target only when a person or the project set one
305
+ * (`viewWords.target`, never a level the model wrote), the approval promise,
306
+ * and where this object stands in the list ("12 objects marked fix; this is
307
+ * the first.", or "1 object marked fix.").
284
308
  */
285
309
  export function fixFacts(o, at, of, open = []) {
286
- const reason = shownReason(o.decision?.reason, CARD_REASON);
287
- const set = o.decision?.targetLevel;
288
- // Step 2: a target a person or the project set (proof.target) is said, A included.
289
- // Without one, the legacy reading stands: only a recorded level other than A.
290
- const named = o.proof?.target?.level;
291
- const target = named && LEVEL.test(named) ? named : set && LEVEL.test(set) && set !== DEFAULT_TARGET ? set : undefined;
292
- const now = o.decision?.level && LEVEL.test(o.decision.level) ? o.decision.level : undefined;
310
+ const r = o.decision.reason;
311
+ const reason = viewWords.cardReason(r ? { ...r, text: shownReason(r.text, CARD_REASON) } : null);
312
+ const t = o.target;
313
+ const now = o.level.now ?? o.level.decided;
314
+ const target = t.from !== 'none' && t.level
315
+ ? [`${sentence(viewWords.target({ ...t, by: t.by ? { ...t.by, name: shown(t.by.name) } : null }))}${now && now !== t.level ? `, now ${now}` : ''}.`]
316
+ : [];
293
317
  return [
294
- o.openFindings === null ? 'Not checked yet.' : `${plural(o.openFindings, 'open finding', 'open findings')}.`,
295
- ...(fixFailedCheck(o) ? ['Its last fix failed the check.', ...open] : []),
296
- ...(reason ? [`Why: ${reason}`] : []),
297
- ...(target ? [`Target: level ${target}${now && now !== target ? `, now ${now}` : ''}.`] : []),
318
+ o.proof.open === null ? 'Not checked yet.' : `${plural(o.proof.open, 'open finding', 'open findings')}.`,
319
+ ...(o.part === 'failed-check' ? ['Its last fix failed the check.', ...open] : []),
320
+ ...(reason ? [reason] : []),
321
+ ...target,
298
322
  'CSPeach asks before it writes to SAP.',
299
- `${at} of ${plural(of, 'object marked fix', 'objects marked fix')}.`,
323
+ of === 1 ? '1 object marked fix.' : `${plural(of, 'object marked fix', 'objects marked fix')}; this is ${whichOne(at)}.`,
300
324
  ];
301
325
  }
326
+ const ORDINALS = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth'];
327
+ /** Live re-run L4: "the first" … "the tenth", then "number 11" ("1 of 12" read as "one of twelve"). */
328
+ const whichOne = (at) => (ORDINALS[at - 1] ? `the ${ORDINALS[at - 1]}` : `number ${at.toLocaleString('en-US')}`);
302
329
  /** "Still open:" shows at most this many lines on a card. */
303
330
  const STILL_OPEN_ROWS = 3;
304
331
  /** A fact wraps onto at most this many rows on a card that also lists what is still open. */
@@ -340,20 +367,28 @@ export function stillOpenLines(key) {
340
367
  * only the chosen object first. Objects whose names are not ABAP names never
341
368
  * reach this card: the chain passes them over first.
342
369
  */
343
- export async function fixCard(confirm, objects, skips, openOf = () => []) {
344
- const list = objects.filter((o) => refactorCommand({ type: o.type, name: o.name }) !== null);
370
+ export async function fixCard(confirm, objects, skips, view, passOver, openOf = () => []) {
371
+ // Fix round 1: an object the card cannot show is passed over with its line, never dropped in silence.
372
+ const list = objects.filter((o) => {
373
+ const line = fixPassOverLine(o, view);
374
+ if (line)
375
+ passOver(o, line);
376
+ return !line;
377
+ });
345
378
  if (!list.length)
346
379
  return { kind: 'none' };
347
380
  const many = list.length > 1;
348
381
  for (let i = 0; i < list.length * 20; i++) {
349
382
  const o = list[i % list.length];
350
383
  const name = o.name.toUpperCase();
351
- const open = fixFailedCheck(o) ? openOf(o) : [];
384
+ const v = view.object(o.key);
385
+ const failed = v.part === 'failed-check';
386
+ const open = failed ? openOf(o) : [];
352
387
  const res = await confirm({
353
388
  id: 'journey-fix',
354
389
  tag: 'QUESTION',
355
- title: fixFailedCheck(o) ? `${name}'s fix failed the check — try again?` : `Fix ${name} now?`,
356
- facts: fixFacts(o, (i % list.length) + 1, list.length, open),
390
+ title: failed ? `${name}'s fix failed the check — try again?` : `Fix ${name} now?`,
391
+ facts: fixFacts(v, (i % list.length) + 1, list.length, open),
357
392
  choices: [{ value: 'yes', label: 'Yes' }, ...(many ? [{ value: 'another', label: 'Pick another' }] : []), { value: 'later', label: 'Later' }],
358
393
  initial: 0,
359
394
  aliases: { y: 'yes', ...(many ? { p: 'another' } : {}), l: 'later' },
@@ -372,9 +407,15 @@ export async function fixCard(confirm, objects, skips, openOf = () => []) {
372
407
  }
373
408
  return { kind: 'cancel' };
374
409
  }
375
- /** The object's target for the fix turn's prompt, when a person or the project set one. */
410
+ /** Why the fix card cannot show an object: a name that is not an ABAP name, or an object the project view does not hold. Null: it can. */
411
+ export function fixPassOverLine(o, view) {
412
+ if (refactorCommand({ type: o.type, name: o.name }) === null)
413
+ return passedOverLine(o.name);
414
+ return view.object(o.key) ? null : notInViewLine(o.name);
415
+ }
416
+ /** The object's target for the fix turn's prompt, from the project view: only when a person or the project set one. */
376
417
  function targetFor(o) {
377
- const level = o?.proof?.target?.level;
418
+ const level = o && o.target.from !== 'none' ? o.target.level : null;
378
419
  return level && LEVEL.test(level) ? { target: level } : {};
379
420
  }
380
421
  export const VARIANT_QUESTION = 'Which check variant? Type its name.';
@@ -442,6 +483,166 @@ async function variantCard(h, team, confirm) {
442
483
  h.emit(VARIANT_TRIES_LINE);
443
484
  return null;
444
485
  }
486
+ /** Task 8 (B11): asked at the start, after the check variant. */
487
+ export const TRANSPORT_TITLE = 'Which transport request will hold the fixes?';
488
+ export const TRANSPORT_QUESTION = 'Which transport request? Type its number.';
489
+ /** Said when five answers gave no request that can take the fixes; the project starts without one. */
490
+ export const TRANSPORT_TRIES_LINE = 'Too many tries to choose a transport request. The project starts without one.';
491
+ /** The longest request description the card shows. */
492
+ const TRANSPORT_DESCRIPTION_MAX = 60;
493
+ /** The session can read a transport request from SAP (the card needs it). Standalone and no-SAP sessions cannot. */
494
+ function readsTransports(h, team) {
495
+ return !h.noSap && !team.deps.noSap && typeof h.adt?.transportRead === 'function';
496
+ }
497
+ /**
498
+ * Task 8 (B11), step 2, after the check variant: "Which transport request
499
+ * will hold the fixes?" [Type it] [None yet]. A typed number is checked for
500
+ * shape, read from SAP (as the release card reads it: request_approval
501
+ * attachReleaseFacts) and shown back — "Record fixes in <REQ> "<text>"?"
502
+ * [Yes] [Use another] [None yet] with its owner and status. A task, a
503
+ * request SAP cannot read, or one that can no longer take changes is said in
504
+ * one line and the question comes again; five answers, then none yet.
505
+ * Returns the request, '' for none yet, or null when a card or the question
506
+ * was put away (no project, as on every start card).
507
+ */
508
+ async function transportCard(h, confirm) {
509
+ const first = await confirm({
510
+ id: 'journey-transport',
511
+ tag: 'QUESTION',
512
+ title: TRANSPORT_TITLE,
513
+ choices: [{ value: 'type', label: 'Type it' }, { value: 'none', label: 'None yet' }],
514
+ initial: 0,
515
+ aliases: { t: 'type', n: 'none' },
516
+ footnote: footnote('START'),
517
+ });
518
+ if (first.cancelled || first.value === null)
519
+ return null;
520
+ if (first.value !== 'type')
521
+ return '';
522
+ const ask = h.ask ?? journeyAsk();
523
+ for (let tries = 0; tries < 5; tries++) {
524
+ let said;
525
+ try {
526
+ said = await ask(TRANSPORT_QUESTION);
527
+ }
528
+ catch {
529
+ return null;
530
+ }
531
+ const asked = said.trim().toUpperCase();
532
+ if (!TRANSPORT_REQUEST.test(asked)) {
533
+ h.emit(notARequestLine(shownUpTo(said.trim(), 60)));
534
+ continue;
535
+ }
536
+ let doc;
537
+ try {
538
+ doc = await h.adt.transportRead(asked);
539
+ }
540
+ catch {
541
+ h.emit(`CSPeach could not read ${asked}.`);
542
+ continue;
543
+ }
544
+ const number = shown(String(doc.number ?? '')).trim().toUpperCase();
545
+ if (doc.kind === 'task') {
546
+ // A task document without its request's number says so without one.
547
+ h.emit(TRANSPORT_REQUEST.test(number) && number !== asked
548
+ ? `${asked} is a task of ${number}. Type the request number.`
549
+ : `${asked} is a task, not a request. Type the request number.`);
550
+ continue;
551
+ }
552
+ const status = shown(String(doc.statusText || doc.status || '')).trim() || 'of unknown status';
553
+ if (!isModifiable(doc.status)) {
554
+ h.emit(`${asked} is ${status}; it cannot take changes.`);
555
+ continue;
556
+ }
557
+ const request = TRANSPORT_REQUEST.test(number) ? number : asked;
558
+ const description = shownUpTo(String(doc.description ?? '').trim(), TRANSPORT_DESCRIPTION_MAX);
559
+ const owner = shown(String(doc.owner ?? '')).trim();
560
+ const res = await confirm({
561
+ id: 'journey-transport-confirm',
562
+ tag: 'QUESTION',
563
+ title: `Record fixes in ${request}${description ? ` "${description}"` : ''}?`,
564
+ facts: [`${owner ? `Owner: ${owner}. ` : ''}Status: ${status}.`],
565
+ choices: [{ value: 'yes', label: 'Yes' }, { value: 'another', label: 'Use another' }, { value: 'none', label: 'None yet' }],
566
+ initial: 0,
567
+ aliases: { y: 'yes', u: 'another', n: 'none' },
568
+ footnote: footnote('START'),
569
+ });
570
+ if (res.cancelled || res.value === null)
571
+ return null;
572
+ if (res.value === 'yes')
573
+ return request;
574
+ if (res.value === 'none')
575
+ return '';
576
+ }
577
+ h.emit(TRANSPORT_TRIES_LINE);
578
+ return '';
579
+ }
580
+ /** "James Miller", "James Miller and Emily Parker", "A, B and C". */
581
+ const andList = (names) => names.length < 2 ? names.join('') : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
582
+ /**
583
+ * B1, step 2 — "Anyone else working on this?" [Just me] [Add people], before
584
+ * "Start the project here?". Add people asks for full names (free text,
585
+ * people.ts parsePeople: three tries, then just you) and shows them back —
586
+ * "Add James Miller and Emily Parker?" [Yes] [Retype] [Just me]. Returns the
587
+ * names (none: Just me), or null when a card or the question was put away.
588
+ */
589
+ async function peopleCard(h, confirm, me, packages) {
590
+ const res = await confirm({
591
+ id: 'journey-people',
592
+ tag: 'QUESTION',
593
+ title: PEOPLE_TITLE,
594
+ facts: ['Each person gets a share of the packages, balanced by the work.'],
595
+ choices: [{ value: 'me', label: 'Just me' }, { value: 'add', label: 'Add people' }],
596
+ initial: 0,
597
+ aliases: { j: 'me', a: 'add' },
598
+ footnote: footnote('START'),
599
+ });
600
+ if (res.cancelled || res.value === null)
601
+ return null;
602
+ if (res.value !== 'add')
603
+ return [];
604
+ const ask = h.ask ?? journeyAsk();
605
+ for (let round = 0; round < 5; round++) {
606
+ let names = null;
607
+ for (let tries = 0; tries < 3 && !names; tries++) {
608
+ let said;
609
+ try {
610
+ said = await ask(PEOPLE_QUESTION);
611
+ }
612
+ catch {
613
+ return null;
614
+ }
615
+ const parsed = parsePeople(said, packages);
616
+ // The lead is "me" already: typed again under their own name, it is not someone else.
617
+ if (parsed.ok)
618
+ names = parsed.names.filter((x) => x.toLowerCase() !== me.trim().toLowerCase());
619
+ else
620
+ h.emit(parsed.line);
621
+ }
622
+ if (!names?.length) {
623
+ h.emit(JUST_YOU_LINE);
624
+ return [];
625
+ }
626
+ const back = await confirm({
627
+ id: 'journey-people-names',
628
+ tag: 'QUESTION',
629
+ title: `Add ${andList(names.map((x) => shown(x)))}?`,
630
+ facts: [`You: ${shown(me)} (lead).`, ...names.map(shareFact)],
631
+ choices: [{ value: 'yes', label: 'Yes' }, { value: 'retype', label: 'Retype' }, { value: 'me', label: 'Just me' }],
632
+ initial: 0,
633
+ aliases: { y: 'yes', r: 'retype', j: 'me' },
634
+ footnote: footnote('START'),
635
+ });
636
+ if (back.cancelled || back.value === null)
637
+ return null;
638
+ if (back.value === 'yes')
639
+ return names;
640
+ if (back.value === 'me')
641
+ return [];
642
+ }
643
+ h.emit(JUST_YOU_LINE);
644
+ return [];
645
+ }
445
646
  /**
446
647
  * I15: "Run the first check per package now? It resumes if stopped." Yes runs
447
648
  * `team refresh --all --per-package`; Later (or the card put away) ends the
@@ -472,8 +673,14 @@ async function firstCheckCard(h, team, skips, packages) {
472
673
  return true;
473
674
  }
474
675
  /* ── the chain ──────────────────────────────────────────────────────────── */
676
+ /** G3: how many of my objects the session skipped and are still undecided (the DONE count's "skipped this time": the view's part). */
677
+ function skippedRows(view, me, skips) {
678
+ if (!skips.decide?.size)
679
+ return 0;
680
+ return view.me(me).parts.skipped;
681
+ }
475
682
  /** Where the chain is: the stage, and a print of what the session put off (no progress → stop). */
476
- const fingerprint = (stage, s) => `${stage}|${s.decide?.size ?? 0}|${s.fix?.size ?? 0}|${s.prove ? 1 : 0}|${s.report ? 1 : 0}|${s.rebuild?.size ?? 0}`;
683
+ const fingerprint = (stage, s) => `${stage}|${s.decide?.size ?? 0}|${s.fix?.size ?? 0}|${s.prove ? 1 : 0}|${s.report ? 1 : 0}|${s.rebuild?.size ?? 0}|${s.retire?.size ?? 0}`;
477
684
  /** The newest check's rule ids per object (the kind proposal's ALV fact, F6); none when they cannot be read. */
478
685
  function rulesReader() {
479
686
  let byKey = null;
@@ -487,6 +694,135 @@ function rulesReader() {
487
694
  }
488
695
  return (key) => byKey?.get(key)?.latest.map((r) => r.rule) ?? [];
489
696
  }
697
+ /** This seat is the project's lead (the one test, commands/team.ts). Unreadable → not the lead. */
698
+ function leadHere(team) {
699
+ try {
700
+ const joined = activeProject();
701
+ return !!joined && isLead(readProjectFile(joined.projectDir), team.deps);
702
+ }
703
+ catch {
704
+ return false;
705
+ }
706
+ }
707
+ /** Where-used, in the words CSPeach prints for it (the words a retire reason quotes). */
708
+ function callersLine(c) {
709
+ if (!c)
710
+ return 'Where-used could not be read: no inventory for this object.';
711
+ if (!c.read)
712
+ return `Where-used could not be read: ${shownUpTo(c.why, 120).replace(/[\s.]+$/, '')}.`;
713
+ return c.objects === 0 ? 'No callers found (where-used read OK).' : `Callers found: ${plural(c.objects, 'object', 'objects')} (where-used read OK).`;
714
+ }
715
+ /** Usage with its source and period, as team_findings gives it; "unknown" says why. */
716
+ function usageLine(u) {
717
+ if (!u)
718
+ return 'Usage: unknown.';
719
+ const source = u.source ? `, from ${shownUpTo(u.source, 60)}` : '';
720
+ const period = u.period ? ` (${shownUpTo(u.period.from, 30)} to ${shownUpTo(u.period.to, 30)})` : '';
721
+ const last = u.lastUsed ? `, last used ${shownUpTo(u.lastUsed, 30)}` : '';
722
+ const why = !u.source && u.why ? ` — ${shownUpTo(u.why, 160).replace(/[\s.]+$/, '')}` : '';
723
+ return `Usage: ${u.state}${source}${period}${last}${why}.`;
724
+ }
725
+ /**
726
+ * The facts of the lead's retire card: who proposed it and why (the project
727
+ * view's decision: a person's retire candidate), the evidence, where it stands.
728
+ * When the decision that stands is another one (a conflict), the person whose
729
+ * retire still stands is named, with no words that are not theirs.
730
+ */
731
+ export function retireFacts(o, v, at, of) {
732
+ const d = v?.decision;
733
+ const reason = shownReason(d?.reason?.text, CARD_REASON).replace(/[\s.]+$/, '');
734
+ const proposer = d && d.from === 'person' && d.what === 'retire-candidate' && d.by ? shown(d.by.name).trim() : '';
735
+ const standing = proposer ? '' : shown(d?.standing?.find((x) => x.what === 'retire-candidate')?.name ?? '').trim();
736
+ return [
737
+ ...(proposer ? [`Proposed by ${proposer}${reason ? `: ${reason}` : ''}.`] : standing ? [`Proposed by ${standing}.`] : []),
738
+ callersLine(o.evidence?.callers),
739
+ usageLine(o.evidence?.usage),
740
+ `Row ${at} of ${of}.`,
741
+ ];
742
+ }
743
+ /**
744
+ * Task 6 (B4): "Retire <NAME>?" [Yes] [Not yet], one card per candidate. Yes
745
+ * asks "What did you check?" and writes the lead's ruling with those words
746
+ * (`team rule <T> <N> retire --reason "…"`, run in process); nothing typed
747
+ * says so and shows the card again. Not yet puts it off for the session.
748
+ */
749
+ async function retireCards(h, team, list, at, view) {
750
+ const ask = h.ask ?? journeyAsk();
751
+ // Round 1: the step it runs in (the report is step 6); DONE has no step, so no footnote.
752
+ const foot = stepOf(at) ? { footnote: footnote(at) } : {};
753
+ for (let i = 0; i < list.length; i++) {
754
+ const o = list[i];
755
+ const type = shown(o.type.toUpperCase());
756
+ const name = shown(o.name.toUpperCase());
757
+ const later = () => addJourneySkips(h.session, { retire: new Set([o.key]) });
758
+ let settled = false;
759
+ for (let tries = 0; tries < 5 && !settled; tries++) {
760
+ const res = await team.confirm({
761
+ id: 'journey-retire',
762
+ tag: 'QUESTION',
763
+ title: `Retire ${name}?`,
764
+ facts: retireFacts(o, view?.object(o.key), i + 1, list.length),
765
+ choices: [{ value: 'yes', label: 'Yes' }, { value: 'later', label: 'Not yet' }],
766
+ initial: 0,
767
+ aliases: { y: 'yes', n: 'later' },
768
+ ...foot,
769
+ wrapFacts: FACT_ROWS,
770
+ });
771
+ if (res.cancelled || res.value === null)
772
+ return 'cancel';
773
+ if (res.value === 'later') {
774
+ later();
775
+ settled = true;
776
+ continue;
777
+ }
778
+ let said;
779
+ try {
780
+ said = await ask(RETIRE_QUESTION);
781
+ }
782
+ catch {
783
+ return 'cancel';
784
+ }
785
+ const reason = cleanKeepReason(said);
786
+ if (!reason) {
787
+ h.emit(retireNeedsLine(type, name));
788
+ continue;
789
+ }
790
+ team.said.length = 0;
791
+ try {
792
+ ruleObject({ type: o.type, name: o.name, classification: 'retire', reason }, team.deps);
793
+ h.emit(`✓ Recorded: ${type} ${name} — retired, reason recorded.`);
794
+ }
795
+ catch (e) {
796
+ h.emit(notRecordedLine(e));
797
+ later();
798
+ }
799
+ settled = true;
800
+ }
801
+ if (!settled) {
802
+ later();
803
+ h.emit(`${name}: left for later.`);
804
+ }
805
+ }
806
+ return 'done';
807
+ }
808
+ /**
809
+ * R8: a project folder this laptop records into, read for the first time in
810
+ * the session or holding a baseline of more than 200 objects. Any other folder
811
+ * (no project, or someone else's) reads no register, so says nothing. Headless
812
+ * prints results only, as before.
813
+ */
814
+ function slowRead(h, me) {
815
+ if (h.headless || folderKind(h.cwd, me).kind !== 'joined')
816
+ return false;
817
+ if (!folderReadThisSession(h.session, h.cwd))
818
+ return true;
819
+ try {
820
+ return readBaseline(h.cwd).length > READING_BIG;
821
+ }
822
+ catch {
823
+ return false;
824
+ }
825
+ }
490
826
  async function drive(h) {
491
827
  const flow = getJourneyFlow(h.session);
492
828
  if (!flow)
@@ -497,10 +833,19 @@ async function drive(h) {
497
833
  let ranInventory = false;
498
834
  let saidInventory = false;
499
835
  let previous = '';
836
+ // G3: Yes on the skipped rows goes straight to the triage, without "Next 40?" first.
837
+ let reask = false;
838
+ let saidReading = false;
500
839
  for (let round = 0; round < 24; round++) {
501
840
  const me = whoAmI(team.deps);
502
841
  const skips = journeySkips(h.session);
842
+ // R8: the read below is where a cold or big project folder goes quiet. Said once a run.
843
+ if (!saidReading && slowRead(h, me)) {
844
+ h.emit(READING_LINE);
845
+ saidReading = true;
846
+ }
503
847
  const facts = readJourneyFacts(h.cwd, me, skips);
848
+ markFolderRead(h.session, h.cwd);
504
849
  const stage = stageOf(facts);
505
850
  setJourneyAt(h.session, stage);
506
851
  // Ctrl-C: what was measured is kept; "carry on" picks up from the folder.
@@ -518,6 +863,9 @@ async function drive(h) {
518
863
  let runAs = stage;
519
864
  let runSkips = skips;
520
865
  let startVariant;
866
+ let startTransport;
867
+ // Task 8 (B11): the start asked for its transport (the card, or headless: the line).
868
+ let transportAsked = false;
521
869
  let bigStart = false;
522
870
  if (!h.headless) {
523
871
  // Step 1 — "Look at the system?": no inventory yet, or one older than a week.
@@ -555,6 +903,13 @@ async function drive(h) {
555
903
  if (runAs === 'START') {
556
904
  const scope = startScope(h.cwd, flow.packages);
557
905
  if (scope) {
906
+ // B1: who else works on it, asked at every start; Just me leaves the start card as it was.
907
+ const people = await peopleCard(h, confirm, me, scope.names);
908
+ if (!people) {
909
+ end(h, NOT_STARTED_LINE);
910
+ return;
911
+ }
912
+ setJourneyDevelopers(h.session, people.length ? [...people, 'me'] : undefined);
558
913
  const n = scope.objects;
559
914
  bigStart = n > FIRST_CHECK_WHOLE_MAX;
560
915
  const yes = await yesNo(confirm, {
@@ -567,7 +922,7 @@ async function drive(h) {
567
922
  'Saved in this folder.',
568
923
  // Recording fix 5: the name every record is written under, seen before anything is written.
569
924
  `You: ${shown(me)}.`,
570
- `You own all ${n.toLocaleString('en-US')}.`,
925
+ ...(people.length ? people.map(shareFact) : [`You own all ${n.toLocaleString('en-US')}.`]),
571
926
  'Writes nothing to SAP.',
572
927
  ],
573
928
  footnote: footnote('START'),
@@ -584,6 +939,17 @@ async function drive(h) {
584
939
  return;
585
940
  }
586
941
  startVariant = chosen;
942
+ // B11: which request holds the fixes — only where the session can read one from SAP.
943
+ if (readsTransports(h, team)) {
944
+ const request = await transportCard(h, confirm);
945
+ if (request === null) {
946
+ end(h, NOT_STARTED_LINE);
947
+ return;
948
+ }
949
+ if (request)
950
+ startTransport = request;
951
+ transportAsked = true;
952
+ }
587
953
  }
588
954
  }
589
955
  // T7 review 1: nothing measured yet, and the scope is too big for one check — never
@@ -598,14 +964,19 @@ async function drive(h) {
598
964
  // Step 4 — "Fix <NAME> now?".
599
965
  if (runAs === 'FIX' && facts.register) {
600
966
  // A name that is not an ABAP name never reaches a card or a command: one line, passed over.
601
- const bad = toFix(facts.register, me, skips).filter((o) => refactorCommand({ type: o.type, name: o.name }) === null);
967
+ const view = facts.view ?? journeyView(facts.register);
968
+ // A name that is not an ABAP name, or an object the view does not hold, never reaches a card: one line, passed over.
969
+ const bad = toFix(facts.register, me, skips, view).map((o) => ({ o, line: fixPassOverLine(o, view) })).filter((x) => x.line !== null);
602
970
  if (bad.length) {
603
- for (const o of bad)
604
- h.emit(passedOverLine(o.name));
605
- addJourneySkips(h.session, { fix: new Set(bad.map((o) => o.key)) });
971
+ for (const x of bad)
972
+ h.emit(x.line);
973
+ addJourneySkips(h.session, { fix: new Set(bad.map((x) => x.o.key)) });
606
974
  continue;
607
975
  }
608
- const pick = await fixCard(confirm, toFix(facts.register, me, skips), skips, (o) => stillOpenLines(o.key));
976
+ const pick = await fixCard(confirm, toFix(facts.register, me, skips, view), skips, view, (o, line) => {
977
+ h.emit(line);
978
+ addJourneySkips(h.session, { fix: new Set([o.key]) });
979
+ }, (o) => stillOpenLines(o.key));
609
980
  if (pick.kind === 'cancel') {
610
981
  pause(h, stepOf('FIX'));
611
982
  return;
@@ -618,10 +989,53 @@ async function drive(h) {
618
989
  if (pick.kind === 'yes')
619
990
  runSkips = pick.skips;
620
991
  }
992
+ // Task 6 (B4) — at the report and at the end, the lead rules on each
993
+ // retire candidate: one card each. Then the stage derives again, so the
994
+ // report that follows shows the ruling. A developer never sees it.
995
+ if ((runAs === 'REPORT' || runAs === 'DONE') && facts.register && leadHere(team)) {
996
+ // Task 9 fix round 1: the view's candidates (active only, as every count says), never the register's flag.
997
+ const byKey = new Map(facts.register.objects.map((o) => [o.key, o]));
998
+ const view = facts.view ?? journeyView(facts.register);
999
+ const candidates = view.all.candidates.filter((v) => !skips.retire?.has(v.key)).map((v) => byKey.get(v.key)).filter((o) => !!o);
1000
+ if (candidates.length) {
1001
+ if ((await retireCards(h, team, candidates, runAs, view)) === 'cancel') {
1002
+ pause(h, stepOf(runAs));
1003
+ return;
1004
+ }
1005
+ previous = '';
1006
+ continue;
1007
+ }
1008
+ }
1009
+ // G3 — before DONE: the rows skipped this session, still undecided, asked about once.
1010
+ // Once a run (the flag lives on the flow): "Later" on "Next 40?" counts as asked.
1011
+ if (runAs === 'DONE' && facts.view && !getJourneyFlow(h.session)?.skippedAsked) {
1012
+ const skipped = skippedRows(facts.view, me, skips);
1013
+ if (skipped) {
1014
+ markSkippedAsked(h.session);
1015
+ const res = await confirm({
1016
+ id: 'journey-skipped',
1017
+ tag: 'QUESTION',
1018
+ title: `Look at the ${plural(skipped, 'row', 'rows')} you skipped?`,
1019
+ choices: [{ value: 'yes', label: 'Yes' }, { value: 'later', label: 'Later' }],
1020
+ initial: 0,
1021
+ aliases: { y: 'yes', l: 'later' },
1022
+ footnote: footnote('DECIDE'),
1023
+ });
1024
+ if (res.cancelled || res.value === null) {
1025
+ pause(h, stepOf('DECIDE'));
1026
+ return;
1027
+ }
1028
+ if (res.value === 'yes') {
1029
+ forgetSkippedRows(h.session);
1030
+ reask = true;
1031
+ continue;
1032
+ }
1033
+ }
1034
+ }
621
1035
  // Step 2, Task 5 (D3, F1) — at the end, the rebuild hand-off: one card
622
1036
  // per object or app label. Yes ends the journey, then queues the rebuild.
623
1037
  if (runAs === 'DONE' && facts.register) {
624
- const groups = rebuildGroups(toRedesign(facts.register, me), skips.rebuild, rulesReader());
1038
+ const groups = rebuildGroups(toRedesign(facts.register, me, facts.view), skips.rebuild, rulesReader(), (key) => facts.view?.object(key)?.target);
625
1039
  if (groups.passed.length) {
626
1040
  // A rebuild CSPeach cannot hand off is said once, with why, and not asked again.
627
1041
  for (const p of groups.passed)
@@ -649,9 +1063,11 @@ async function drive(h) {
649
1063
  }
650
1064
  }
651
1065
  // Step 3, from the second page on — "Next 40? [Yes] [Later]".
652
- if (runAs === 'DECIDE' && facts.register && (getJourneyFlow(h.session)?.pages ?? 0) > 0) {
653
- const left = toDecide(facts.register, me, skips);
654
- const next = decidePage(facts.register, me, skips).length;
1066
+ if (runAs === 'DECIDE' && reask)
1067
+ reask = false;
1068
+ else if (runAs === 'DECIDE' && facts.register && (getJourneyFlow(h.session)?.pages ?? 0) > 0) {
1069
+ const left = toDecide(facts.register, me, skips, facts.view);
1070
+ const next = decidePage(facts.register, me, skips, facts.view).length;
655
1071
  const res = await confirm({
656
1072
  id: 'journey-next-page',
657
1073
  tag: 'QUESTION',
@@ -669,15 +1085,23 @@ async function drive(h) {
669
1085
  if (res.value === 'later') {
670
1086
  // Left out, not skipped: "carry on" brings them back to deciding.
671
1087
  addJourneyLeftOut(h.session, left.map((o) => o.key));
1088
+ markSkippedAsked(h.session);
672
1089
  h.emit('Left for later.');
673
1090
  continue;
674
1091
  }
675
1092
  }
676
1093
  }
1094
+ const developers = runAs === 'START' ? getJourneyFlow(h.session)?.developers : undefined;
677
1095
  const r = await runJourneyStage(runAs, {
678
1096
  cwd: h.cwd, team, headless: h.headless, skips: runSkips, packages: flow.packages,
679
1097
  ...(h.openFile ? { openFile: h.openFile } : {}),
680
1098
  ...(startVariant ? { variant: startVariant } : {}),
1099
+ ...(developers ? { developers: [...developers] } : {}),
1100
+ ...(startTransport ? { transport: startTransport } : {}),
1101
+ // Headless shows no card: where the card would be asked, the start says the transport as a line.
1102
+ ...(transportAsked || (h.headless && runAs === 'START' && readsTransports(h, team)) ? { transportAsked: true } : {}),
1103
+ // B8: the DONE summary knows what the last DONE of this session summed up (headless: as before).
1104
+ ...(h.headless ? {} : { session: h.session }),
681
1105
  });
682
1106
  if (runAs === 'INVENTORY')
683
1107
  ranInventory = true;
@@ -705,18 +1129,18 @@ async function drive(h) {
705
1129
  }
706
1130
  const skill = /^\/(\S+)/.exec(command)[1];
707
1131
  if (runAs === 'DECIDE') {
708
- setJourneyQueued(h.session, { command, skill, stage: 'DECIDE', left: facts.register ? toDecide(facts.register, me, skips).length : 0 });
1132
+ setJourneyQueued(h.session, { command, skill, stage: 'DECIDE', left: facts.register ? toDecide(facts.register, me, skips, facts.view).length : 0 });
709
1133
  // The page the triage is shown (team_findings reads it): at most 40, in the queued area.
710
- setJourneyPage(h.session, facts.register ? decidePage(facts.register, me, skips).map((o) => o.key) : []);
1134
+ setJourneyPage(h.session, facts.register ? decidePage(facts.register, me, skips, facts.view).map((o) => o.key) : []);
711
1135
  }
712
1136
  else {
713
1137
  const key = r.next.key;
714
- setJourneyQueued(h.session, { command, skill, stage: 'FIX', ...(key ? { key } : {}), callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd), ...targetFor(facts.register?.objects.find((o) => o.key === key)) });
1138
+ setJourneyQueued(h.session, { command, skill, stage: 'FIX', ...(key ? { key } : {}), callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd), ...targetFor(key ? facts.view?.object(key) : undefined) });
715
1139
  }
716
1140
  queueStep(h, command);
717
1141
  return;
718
1142
  }
719
- end(h); // stop or done: the runner said its last words
1143
+ end(h, undefined, r.next.kind === 'done'); // stop or done: the runner said its last words
720
1144
  return;
721
1145
  }
722
1146
  pause(h);
@@ -741,9 +1165,9 @@ export async function afterJourneyTurn(h, t) {
741
1165
  const stage = stageOf(facts);
742
1166
  let moved = stage !== q.stage;
743
1167
  if (!moved && facts.register && q.stage === 'DECIDE')
744
- moved = toDecide(facts.register, me, skips).length < (q.left ?? Number.MAX_SAFE_INTEGER);
1168
+ moved = toDecide(facts.register, me, skips, facts.view).length < (q.left ?? Number.MAX_SAFE_INTEGER);
745
1169
  if (!moved && facts.register && q.stage === 'FIX')
746
- moved = !toFix(facts.register, me).some((o) => o.key === q.key);
1170
+ moved = !toFix(facts.register, me, {}, facts.view).some((o) => o.key === q.key);
747
1171
  if (!moved) {
748
1172
  if (/\?\s*$/.test(t.assistantText.trim()))
749
1173
  return;
@@ -755,10 +1179,10 @@ export async function afterJourneyTurn(h, t) {
755
1179
  // nothing recorded. "Left for later" alone hid what was still open: one
756
1180
  // card says it and asks what to do.
757
1181
  const o = facts.register?.objects.find((x) => x.key === q.key);
758
- // Proof first: a fix with nothing open for its own target is not offered again.
759
- if (o && fixFailedCheck(o) && !claimedSinceCheck(o) && !provenForTarget(o)) {
1182
+ // Proof first: the view's part — a fix proven for its own target, or claimed again since, is not offered again.
1183
+ if (o && facts.view?.object(o.key)?.part === 'failed-check') {
760
1184
  setJourneyQueued(h.session, undefined);
761
- const goOn = await retryCard(h, team, o);
1185
+ const goOn = await retryCard(h, team, o, facts.view?.object(o.key));
762
1186
  if (!goOn)
763
1187
  return;
764
1188
  try {
@@ -796,7 +1220,7 @@ export async function afterJourneyTurn(h, t) {
796
1220
  * Returns true when the chain goes on from the folder (drive), false when it
797
1221
  * stopped or a command was queued.
798
1222
  */
799
- async function retryCard(h, team, o) {
1223
+ async function retryCard(h, team, o, v) {
800
1224
  const name = o.name.toUpperCase();
801
1225
  const type = o.type.toUpperCase();
802
1226
  const level = o.decision?.level && LEVEL.test(o.decision.level) ? o.decision.level : undefined;
@@ -839,7 +1263,7 @@ async function retryCard(h, team, o) {
839
1263
  return false;
840
1264
  }
841
1265
  h.emit(`Step ${stepOf('FIX')} of ${STEPS} — fix ${name} again.`);
842
- setJourneyQueued(h.session, { command, skill: 'abap-refactor', stage: 'FIX', key: o.key, callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd), ...targetFor(o) });
1266
+ setJourneyQueued(h.session, { command, skill: 'abap-refactor', stage: 'FIX', key: o.key, callsAt: toolCallsOf(h.session).length, variant: projectVariant(h.cwd), ...targetFor(v) });
843
1267
  queueStep(h, command);
844
1268
  return false;
845
1269
  }
@@ -929,6 +1353,10 @@ export function journeyFixBlock(variant, target) {
929
1353
  "This fix is one step of CSPeach's guided clean-core run.",
930
1354
  `The project's check variant is ${v}.`,
931
1355
  ...(t ? [`This object's target is clean-core level ${t}. CSPeach proves the fix against that target with this check.`] : []),
1356
+ // Task 7 — the approval card shows what callers or users will see differently.
1357
+ "On the approval request, fill changes[].behaviour for every change: what callers or users will see differently, or 'none'.",
1358
+ // Task 7 (R2) — the card's risk line is CSPeach's reading; the model relays it.
1359
+ RISK_NOTE,
932
1360
  `After you activate the object, run ATC on it with the variant ${v}, not the default one.`,
933
1361
  // Final review M4: with a target other than A, findings that only matter for A may stay.
934
1362
  t && t !== 'A'