@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
@@ -0,0 +1,127 @@
1
+ /**
2
+ * A minimal .xlsx writer (spec §6.1): an .xlsx is a zip of a few XML parts.
3
+ *
4
+ * Text cells are inline strings (`t="inlineStr"`), which Excel never evaluates
5
+ * as formulas, so `=SUM(` in a reason stays text; a text cell that starts with
6
+ * = + - or @ also carries the quote-prefix style, so it stays text after an edit. Every cell goes through XML
7
+ * escaping, and characters XML 1.0 cannot hold are dropped. One bold header
8
+ * style, column widths, a frozen header row and an autofilter. Entry times are
9
+ * pinned, so the same sheets give the same bytes.
10
+ *
11
+ * Quote characters inside regex literals are written \x22 / \x27 so the
12
+ * terminal words guard (team-words.test.ts) keeps its place in this file.
13
+ */
14
+ import AdmZip from 'adm-zip';
15
+ import { writeWhole } from './write-whole.js';
16
+ const PINNED = new Date(Date.UTC(2026, 0, 1, 0, 0, 0));
17
+ const MAX_CELL = 32767;
18
+ /** XML 1.0 forbids C0 controls other than tab, LF and CR, and U+FFFE / U+FFFF. */
19
+ const NOT_XML = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
20
+ const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
21
+ const NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
22
+ const REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
23
+ const HEAD = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
24
+ /** Cells Excel could read as a formula: =, +, - or @ first. */
25
+ const FORMULA_LEAD = /^[=+\-@]/;
26
+ /** The cell style with `quotePrefix` (the third xf in STYLES). */
27
+ const QUOTED = 2;
28
+ export function xmlText(v) {
29
+ // Strip lone surrogates again after the cut: the cut can split a pair.
30
+ return v.replace(NOT_XML, '').replace(LONE_SURROGATE, '').slice(0, MAX_CELL).replace(LONE_SURROGATE, '')
31
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\x22/g, '&quot;');
32
+ }
33
+ export function colName(i) {
34
+ let s = '';
35
+ for (let n = i + 1; n > 0; n = Math.floor((n - 1) / 26))
36
+ s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
37
+ return s;
38
+ }
39
+ function cellXml(v, ref, style) {
40
+ if (v === null || v === undefined || v === '')
41
+ return '';
42
+ const s = style ? ` s="${style}"` : '';
43
+ if (typeof v === 'number')
44
+ return Number.isFinite(v) ? `<c r="${ref}"${s}><v>${v}</v></c>` : '';
45
+ const text = String(v);
46
+ // Text that starts like a formula gets Excel's own quote-prefix style: shown as
47
+ // typed, and kept text even when someone edits the cell in Excel and presses Enter.
48
+ const t = !style && FORMULA_LEAD.test(text) ? ` s="${QUOTED}"` : s;
49
+ return `<c r="${ref}"${t} t="inlineStr"><is><t xml:space="preserve">${xmlText(text)}</t></is></c>`;
50
+ }
51
+ function sheetXml(sheet) {
52
+ const width = Math.max(1, sheet.header.length, ...sheet.rows.map((r) => r.length));
53
+ const row = (cells, r, style) => `<row r="${r}">${cells.map((v, c) => cellXml(v, `${colName(c)}${r}`, style)).join('')}</row>`;
54
+ const cols = (sheet.widths ?? []).map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join('');
55
+ const filtered = sheet.filter !== false && sheet.rows.length > 0;
56
+ return `${HEAD}<worksheet xmlns="${NS}">`
57
+ + '<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>'
58
+ + (cols ? `<cols>${cols}</cols>` : '')
59
+ + `<sheetData>${row(sheet.header, 1, 1)}${sheet.rows.map((r, i) => row(r, i + 2, 0)).join('')}</sheetData>`
60
+ + (filtered ? `<autoFilter ref="A1:${colName(width - 1)}${sheet.rows.length + 1}"/>` : '')
61
+ + '</worksheet>';
62
+ }
63
+ const STYLES = `${HEAD}<styleSheet xmlns="${NS}">`
64
+ + '<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>'
65
+ + '<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
66
+ + '<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
67
+ + '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
68
+ + '<cellXfs count="3"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>'
69
+ + '<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" quotePrefix="1"/></cellXfs>'
70
+ + '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>';
71
+ export function buildXlsx(sheets) {
72
+ const taken = new Set();
73
+ const named = sheets.map((s) => ({ ...s, name: safeSheetName(s.name, taken) }));
74
+ const zip = new AdmZip();
75
+ const add = (p, text) => zip.addFile(p, Buffer.from(text, 'utf-8'));
76
+ add('[Content_Types].xml', `${HEAD}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">`
77
+ + '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
78
+ + '<Default Extension="xml" ContentType="application/xml"/>'
79
+ + '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
80
+ + '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
81
+ + named.map((_, i) => `<Override PartName="/xl/worksheets/sheet${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('')
82
+ + '</Types>');
83
+ add('_rels/.rels', `${HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
84
+ + `<Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/></Relationships>`);
85
+ const defined = named.map((s, i) => {
86
+ if (s.filter === false || !s.rows.length)
87
+ return '';
88
+ const width = Math.max(1, s.header.length, ...s.rows.map((r) => r.length));
89
+ return `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">'${xmlText(s.name.replace(/\x27/g, "''"))}'!$A$1:$${colName(width - 1)}$${s.rows.length + 1}</definedName>`;
90
+ }).join('');
91
+ add('xl/workbook.xml', `${HEAD}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>`
92
+ + named.map((s, i) => `<sheet name="${xmlText(s.name)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join('')
93
+ + `</sheets>${defined ? `<definedNames>${defined}</definedNames>` : ''}</workbook>`);
94
+ add('xl/_rels/workbook.xml.rels', `${HEAD}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
95
+ + named.map((_, i) => `<Relationship Id="rId${i + 1}" Type="${REL_NS}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`).join('')
96
+ + `<Relationship Id="rId${named.length + 1}" Type="${REL_NS}/styles" Target="styles.xml"/></Relationships>`);
97
+ add('xl/styles.xml', STYLES);
98
+ named.forEach((s, i) => add(`xl/worksheets/sheet${i + 1}.xml`, sheetXml(s)));
99
+ for (const e of zip.getEntries())
100
+ e.header.time = PINNED;
101
+ return zip.toBuffer();
102
+ }
103
+ export function writeXlsxFile(file, sheets) {
104
+ writeWhole(file, buildXlsx(sheets));
105
+ }
106
+ /** Excel's rules: no []:*?/\ , no leading or trailing apostrophe, 1–31 characters, unique ignoring case. */
107
+ export function safeSheetName(name, taken) {
108
+ const cleaned = String(name ?? '').replace(/[[\]:*?/\\]/g, ' ').replace(/\s+/g, ' ').trim().replace(/^\x27+|\x27+$/g, '').trim();
109
+ // Cut, then clean the ends again: a cut can leave an apostrophe (or a space) last.
110
+ const base = (cleaned || 'Sheet').slice(0, 31).replace(/^[\s\x27]+|[\s\x27]+$/g, '') || 'Sheet';
111
+ let candidate = base;
112
+ for (let n = 2; taken.has(candidate.toLowerCase()); n += 1) {
113
+ const tail = ` (${n})`;
114
+ candidate = `${base.slice(0, 31 - tail.length).trim()}${tail}`;
115
+ }
116
+ taken.add(candidate.toLowerCase());
117
+ return candidate;
118
+ }
119
+ const RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
120
+ /** A name Windows (and every sync tool) accepts as a file name stem. */
121
+ export function safeFileStem(name) {
122
+ const cleaned = String(name ?? '').replace(/[<>:\x22/\\|?*\u0000-\u001F]/g, ' ').replace(/\s+/g, ' ').trim()
123
+ .replace(/[. ]+$/, '').slice(0, 80).trim();
124
+ if (!cleaned)
125
+ return 'team project';
126
+ return RESERVED.test(cleaned) ? `_${cleaned}` : cleaned;
127
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The where-used index, turned into calling objects (spec §5.3).
3
+ *
4
+ * WBCROSSGT and CROSS name the INCLUDE that uses an object. A caller is the
5
+ * object that include belongs to: a class (its pool includes follow a fixed
6
+ * naming scheme), an interface pool, a function group (SAPL<fg>, or an L<fg>…
7
+ * include through its D010INC master), or a report (its master, or itself).
8
+ * Anything that resolves to nothing is counted as unmapped — never guessed.
9
+ *
10
+ * Namespaced names (`/ACME/CL_THING======CP`, `/ACME/SAPLZFG`) follow the
11
+ * same rules; the namespace stays part of the object name.
12
+ */
13
+ /** Class-pool includes: name padded with '=' then CP/CU/CI/CO/CT/CS, CMnnn, CCDEF/CCIMP/CCMAC/CCAU. */
14
+ const CLASS_INCLUDE = /^([^=]{1,30})=+(C[A-Z]{1,2}|CM[0-9A-Z]{3}|CCDEF|CCIMP|CCMAC|CCAU)$/;
15
+ /** Interface-pool includes. */
16
+ const INTF_INCLUDE = /^([^=]{1,30})=+I[PU]$/;
17
+ /**
18
+ * The same pool includes read at fixed width: the object name is the first 30
19
+ * characters, padded with '='. A name of exactly 30 characters has no '=' at
20
+ * all, which the patterns above cannot see.
21
+ */
22
+ const CLASS_SUFFIX = /^(C[A-Z]{1,2}|CM[0-9A-Z]{3}|CCDEF|CCIMP|CCMAC|CCAU)$/;
23
+ const INTF_SUFFIX = /^I[PU]$/;
24
+ /** Function-group main program, optionally namespaced: SAPLZFG, /ACME/SAPLZFG. */
25
+ const FUGR_MAIN = /^(\/[A-Z0-9_]+\/)?SAPL(.+)$/;
26
+ /** Pattern-only mapping: what an include or main program name says by itself. */
27
+ function byPattern(include) {
28
+ let m = CLASS_INCLUDE.exec(include);
29
+ if (m)
30
+ return { type: 'CLAS', name: m[1] };
31
+ m = INTF_INCLUDE.exec(include);
32
+ if (m)
33
+ return { type: 'INTF', name: m[1] };
34
+ if (include.length > 30) {
35
+ const name = include.slice(0, 30).replace(/=+$/, '');
36
+ const suffix = include.slice(30);
37
+ if (name && !name.includes('=')) {
38
+ if (CLASS_SUFFIX.test(suffix))
39
+ return { type: 'CLAS', name };
40
+ if (INTF_SUFFIX.test(suffix))
41
+ return { type: 'INTF', name };
42
+ }
43
+ }
44
+ m = FUGR_MAIN.exec(include);
45
+ if (m)
46
+ return { type: 'FUGR', name: `${m[1] ?? ''}${m[2]}` };
47
+ return null;
48
+ }
49
+ /**
50
+ * The object an include belongs to. `masters` maps include → main program
51
+ * (from D010INC); a report known from TADIR is entered as mapping to itself.
52
+ */
53
+ export function objectOfInclude(include, masters) {
54
+ const inc = (include ?? '').trim().toUpperCase();
55
+ if (!inc)
56
+ return null;
57
+ const direct = byPattern(inc);
58
+ if (direct)
59
+ return direct;
60
+ const master = masters.get(inc);
61
+ if (!master)
62
+ return null;
63
+ return byPattern(master) ?? { type: 'PROG', name: master };
64
+ }
65
+ /**
66
+ * Callers per referenced object name. Index names can carry a component
67
+ * (`ZCL_X\ME:RUN`); the part before the backslash is the object. `selfOf(name)`
68
+ * gives that object's own key (`TYPE|NAME`) so a self-reference is dropped.
69
+ * `includesUnmapped` counts distinct includes CSPeach could not map.
70
+ */
71
+ export function callersFromIndex(rows, masters, selfOf) {
72
+ const out = new Map();
73
+ const unmapped = new Map();
74
+ for (const row of rows) {
75
+ const name = (row.name ?? '').trim().toUpperCase().split('\\')[0];
76
+ if (!name)
77
+ continue;
78
+ const include = (row.include ?? '').trim().toUpperCase();
79
+ let entry = out.get(name);
80
+ if (!entry) {
81
+ entry = { objects: new Set(), includesUnmapped: 0 };
82
+ out.set(name, entry);
83
+ }
84
+ const ref = objectOfInclude(include, masters);
85
+ if (!ref) {
86
+ let seen = unmapped.get(name);
87
+ if (!seen) {
88
+ seen = new Set();
89
+ unmapped.set(name, seen);
90
+ }
91
+ if (include)
92
+ seen.add(include);
93
+ entry.includesUnmapped = seen.size;
94
+ continue;
95
+ }
96
+ const key = `${ref.type}|${ref.name}`;
97
+ if (key === selfOf(name))
98
+ continue;
99
+ entry.objects.add(key);
100
+ }
101
+ return out;
102
+ }
103
+ /** The REPOSRC program name that carries an object's change date, or null for other types. */
104
+ export function reposrcNameFor(type, name) {
105
+ const t = type.toUpperCase();
106
+ const n = name.toUpperCase();
107
+ switch (t) {
108
+ case 'PROG': return n;
109
+ case 'CLAS': return `${n.padEnd(30, '=')}CP`;
110
+ case 'INTF': return `${n.padEnd(30, '=')}IP`;
111
+ case 'FUGR': {
112
+ const m = /^(\/[A-Z0-9_]+\/)(.+)$/.exec(n);
113
+ return m ? `${m[1]}SAPL${m[2]}` : `SAPL${n}`;
114
+ }
115
+ default: return null;
116
+ }
117
+ }
@@ -12,6 +12,7 @@ import { formatAttention } from './notice-shape.js';
12
12
  import { truncateToWidth, uiWidth, frameWidth } from './ui-width.js';
13
13
  import { sanitizeControlChars } from './sanitize.js';
14
14
  import { holdBrand } from './brand-settled.js';
15
+ import { formatTimeAgo } from '../session/time-ago.js';
15
16
  export { brandSettled } from './brand-settled.js';
16
17
  /** Folder budget on the panels status line (controller ruling R2). */
17
18
  const STATUS_PATH_MAX = 20;
@@ -131,6 +132,16 @@ export function panelsResumeLine(last, priorTurns) {
131
132
  const fact = last ?? `${priorTurns} prior turn${priorTurns === 1 ? '' : 's'}`;
132
133
  return ' ' + paint('dim', truncateToWidth(sanitizeControlChars(`resumed · ${fact}`), Math.max(20, uiWidth() - 3)));
133
134
  }
135
+ /** 2026-10-08 (sessions/folders) — the dim start line pointing at this folder's
136
+ * latest session. Quotes the user's prompt, so control characters are neutralised. */
137
+ export function lastSessionHereLine(s, now = new Date()) {
138
+ const when = formatTimeAgo(s.lastTurnAt, now);
139
+ const what = [
140
+ s.skill ? `/${s.skill}` : null,
141
+ s.promptSnippet ? `"${truncateToWidth(sanitizeControlChars(s.promptSnippet), 40)}"` : null,
142
+ ].filter(Boolean).join(', ');
143
+ return ' ' + paint('dim', `Last session here: ${when}${what ? ` (${what})` : ''} — cspeach --continue picks it up.`);
144
+ }
134
145
  /** A "!" line for the start screen (no Next: line → exactly one line). */
135
146
  export function panelsInterruptedNote(n) {
136
147
  return formatAttention({ text: `${n} unfinished session${n === 1 ? '' : 's'}`, fact: 'cspeach --resume' }).trimEnd();
@@ -160,6 +160,8 @@ const RULES = {
160
160
  sap_link: (a) => ({ verb: 'SAP link', object: str(a['action']) ?? '' }),
161
161
  cap_link_smoke: (a) => ({ verb: 'S/4 link check', object: str(a['s4']?.['entitySet']) ?? '' }),
162
162
  app_lane_resolve: (a) => ({ verb: 'Chose lane', object: shortLeft(str(a['specPath']) ?? '') }),
163
+ team_findings: (a) => ({ verb: 'Read team findings', object: str(a['lane']) ?? str(a['packages']) ?? 'all objects' }),
164
+ team_estate: (a) => ({ verb: 'Read estate inventory', object: str(a['packages']) ?? 'all packages' }),
163
165
  sap_badi_list: (a) => ({ verb: 'Searched BAdIs', object: shortRight(`"${str(a['query']) ?? ''}"`) }),
164
166
  sap_snapshot_list: (a) => ({ verb: 'Listed snapshots', object: shortRight(str(a['name']) ?? '') }),
165
167
  sap_snapshot_read: sapObject('Read snapshot'),
@@ -265,7 +267,7 @@ const PRESENT = {
265
267
  Preview: 'Preview', 'Searched samples': 'Searching samples', 'Read sample': 'Reading sample',
266
268
  'Audited metadata': 'Auditing metadata', 'Extended model': 'Extending model',
267
269
  'Checked capability': 'Checking capability',
268
- 'Read texts of': 'Reading texts of', 'Wrote texts': 'Writing texts',
270
+ 'Read texts of': 'Reading texts of', 'Read team findings': 'Reading team findings', 'Read estate inventory': 'Reading estate inventory', 'Wrote texts': 'Writing texts',
269
271
  };
270
272
  export function toolActivityLabel(name, args) {
271
273
  const l = toolLabel(name, args);
@@ -402,6 +402,18 @@ function approvalDeclined(content) {
402
402
  return undefined;
403
403
  }
404
404
  }
405
+ /** 1.1.31 — true when request_approval approved by the user's auto_approve setting, not by a person. */
406
+ function approvalAutoApproved(content) {
407
+ if (!content || !content.trim().startsWith('{'))
408
+ return false;
409
+ try {
410
+ const r = JSON.parse(content);
411
+ return r.approved === true && r.auto_approved === true;
412
+ }
413
+ catch {
414
+ return false;
415
+ }
416
+ }
405
417
  function panelsRow(p) {
406
418
  const width = Math.max(20, getEffectiveBodyWidth());
407
419
  const label = toolLabel(p.name, p.args, p.isError ? undefined : p.resultContent);
@@ -410,7 +422,9 @@ function panelsRow(p) {
410
422
  const declined = !p.isError && p.name === 'request_approval' ? approvalDeclined(p.resultContent) : undefined;
411
423
  const icon = p.isError ? paint('bad', '✗') : declined ? paint('warn', '·') : paint('ok', '✓');
412
424
  const timing = p.durationMs >= 1000 ? formatDuration(p.durationMs) : '';
413
- const tail = [declined ?? label.detail, timing].filter(Boolean).join(' · ');
425
+ // 1.1.31: a setting said yes, not a person — the row says so.
426
+ const auto = !p.isError && !declined && p.name === 'request_approval' && approvalAutoApproved(p.resultContent);
427
+ const tail = [declined ?? (auto ? 'auto-approved' : label.detail), timing].filter(Boolean).join(' · ');
414
428
  const verb = label.verb.padEnd(VERB_COL);
415
429
  // Budget: "✓ " + verb + " " + object + " " + tail must fit the width.
416
430
  const budget = width - 2 - verb.length - 1 - (tail ? tail.length + 2 : 0);
@@ -17,7 +17,7 @@
17
17
  * The cache TTL is short enough that newly-saved files appear within a
18
18
  * few seconds of their save without manual refresh.
19
19
  */
20
- import { ensureWorkspaceSync, listProjectFilesSync } from '../projects/workspace.js';
20
+ import { ensureWorkspaceSync, listLegacyFilesSync, listProjectFilesSync } from '../projects/workspace.js';
21
21
  const CACHE_TTL_MS = 3000;
22
22
  let cache = null;
23
23
  function getCachedFiles() {
@@ -25,7 +25,11 @@ function getCachedFiles() {
25
25
  if (!cache || now - cache.at > CACHE_TTL_MS) {
26
26
  try {
27
27
  const ws = ensureWorkspaceSync();
28
- cache = { files: listProjectFilesSync(ws), at: now };
28
+ const files = listProjectFilesSync(ws);
29
+ // Task 11 — old save locations stay readable for one release; their
30
+ // files follow the new folder's, same names skipped.
31
+ const legacy = listLegacyFilesSync(process.cwd(), files.map((f) => f.filename));
32
+ cache = { files: [...files, ...legacy], at: now };
29
33
  }
30
34
  catch {
31
35
  cache = { files: [], at: now };
@@ -48,7 +48,7 @@ export function listWorkspaceFileChoices(opts) {
48
48
  const seen = new Set();
49
49
  // Workspace half (takes precedence on name collisions).
50
50
  try {
51
- const ws = opts?.workspace ?? ensureWorkspaceSync();
51
+ const ws = opts?.workspace ?? ensureWorkspaceSync(opts?.cwd);
52
52
  for (const f of listProjectFilesSync(ws)) {
53
53
  if (seen.has(f.filename))
54
54
  continue;
@@ -0,0 +1,25 @@
1
+ import { enterJourney } from '../journey/chain.js';
2
+ import { openStatusPage } from '../util/open-in-browser.js';
3
+ export async function runJourneyEntry(trigger, a) {
4
+ const hooks = {
5
+ session: a.session,
6
+ cwd: a.cwd,
7
+ headless: a.headless,
8
+ emit: a.emit,
9
+ ...(a.queueNext && !a.headless ? { queueNext: a.queueNext } : {}),
10
+ ...(!a.standalone && a.adt ? { adt: a.adt } : {}),
11
+ sapAlias: a.sapAlias,
12
+ ...(a.standalone ? { noSap: true } : {}),
13
+ signal: a.signal,
14
+ // Task 6 — the status page opens in the browser after Yes on its card.
15
+ openFile: (file) => openStatusPage(file),
16
+ };
17
+ try {
18
+ await enterJourney(trigger, hooks);
19
+ }
20
+ catch (err) {
21
+ if (process.env['CSPEACH_DEBUG'] === '1')
22
+ throw err;
23
+ a.emit('The clean-core steps stopped here. Say carry on to continue.');
24
+ }
25
+ }