@cspeach/cli 1.1.30 → 1.1.32

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 (128) hide show
  1. package/README.md +11 -1
  2. package/dist/agent/loop.js +132 -9
  3. package/dist/cli.js +49 -18
  4. package/dist/commands/config-set.js +1 -1
  5. package/dist/commands/help.js +16 -1
  6. package/dist/commands/plan-chain.js +3 -0
  7. package/dist/commands/plan-continue.js +1 -1
  8. package/dist/commands/plan-gate.js +17 -1
  9. package/dist/commands/plan-resume.js +28 -18
  10. package/dist/commands/plan-startup.js +49 -6
  11. package/dist/commands/team-discover.js +195 -0
  12. package/dist/commands/team-plan.js +585 -0
  13. package/dist/commands/team-rules.js +187 -0
  14. package/dist/commands/team.js +971 -95
  15. package/dist/journey/chain.js +978 -0
  16. package/dist/journey/clean-core.js +235 -0
  17. package/dist/journey/flow.js +216 -0
  18. package/dist/journey/rebuild.js +464 -0
  19. package/dist/journey/run.js +628 -0
  20. package/dist/journey/session-deps.js +81 -0
  21. package/dist/journey/trigger.js +153 -0
  22. package/dist/lane/app-chain.js +21 -9
  23. package/dist/lane/chain-token.js +9 -5
  24. package/dist/lane/roots.js +97 -41
  25. package/dist/one-shot.js +28 -2
  26. package/dist/projects/clean-core-confirm.js +192 -3
  27. package/dist/projects/detail-path.js +9 -4
  28. package/dist/projects/extract-clean-core.js +3 -0
  29. package/dist/projects/migration.js +4 -105
  30. package/dist/projects/plan-lane.js +67 -17
  31. package/dist/projects/plan-sources.js +13 -7
  32. package/dist/projects/project-folder.js +131 -0
  33. package/dist/projects/promote-command.js +39 -0
  34. package/dist/projects/save-command.js +100 -12
  35. package/dist/projects/saved-lines.js +17 -3
  36. package/dist/projects/sort-command.js +499 -0
  37. package/dist/projects/sort-core.js +248 -0
  38. package/dist/projects/workspace-retire.js +48 -0
  39. package/dist/projects/workspace.js +182 -121
  40. package/dist/register/baseline.js +39 -2
  41. package/dist/register/decision-rules.js +35 -0
  42. package/dist/register/discover.js +931 -0
  43. package/dist/register/export-files.js +58 -0
  44. package/dist/register/findings.js +5 -2
  45. package/dist/register/measure.js +191 -9
  46. package/dist/register/offer.js +100 -8
  47. package/dist/register/picture.js +6 -2
  48. package/dist/register/plan-import.js +328 -0
  49. package/dist/register/records.js +40 -3
  50. package/dist/register/rule-proposal.js +4 -0
  51. package/dist/register/rules-edit.js +257 -0
  52. package/dist/register/status-file/estate-parts.js +168 -0
  53. package/dist/register/status-file/plan-parts.js +68 -0
  54. package/dist/register/status-file/progress-chart.js +110 -0
  55. package/dist/register/status-file/register-map.js +11 -3
  56. package/dist/register/status-file/register-model.js +40 -6
  57. package/dist/register/status-file/register-parts.js +57 -11
  58. package/dist/register/status-file/register-status-file.js +106 -90
  59. package/dist/register/status-file/report-model.js +569 -0
  60. package/dist/register/status-file/team-css.js +1 -1
  61. package/dist/register/status-file-sync.js +4 -0
  62. package/dist/register/status-html.js +27 -6
  63. package/dist/register/store.js +44 -5
  64. package/dist/register/variant.js +65 -0
  65. package/dist/register/workbooks.js +28 -42
  66. package/dist/register/write-whole.js +30 -2
  67. package/dist/register/xlsx.js +13 -3
  68. package/dist/register/xref.js +117 -0
  69. package/dist/renderer/startup-lines.js +11 -0
  70. package/dist/renderer/tool-labels.js +3 -1
  71. package/dist/renderer/tool-widget.js +15 -1
  72. package/dist/repl/at-completer.js +6 -2
  73. package/dist/repl/at-picker.js +1 -1
  74. package/dist/repl/builtin-commands.js +4 -0
  75. package/dist/repl/journey-entry.js +27 -0
  76. package/dist/repl/plan-hop.js +2 -9
  77. package/dist/repl/rules-menu.js +177 -0
  78. package/dist/repl/slash-completer.js +1 -0
  79. package/dist/repl/slash-picker.js +5 -4
  80. package/dist/repl.js +236 -59
  81. package/dist/router/classifier.js +8 -4
  82. package/dist/router/router-core.js +43 -7
  83. package/dist/router/routing-decision.js +1 -1
  84. package/dist/runs/bundled-runs.js +100 -0
  85. package/dist/runs/front-door.js +19 -0
  86. package/dist/runs/manifest-runs.js +50 -0
  87. package/dist/runs/run-command.js +24 -0
  88. package/dist/runs/runs.js +27 -0
  89. package/dist/runs/shadow.js +100 -0
  90. package/dist/runs/types.js +1 -0
  91. package/dist/session/backfill.js +342 -0
  92. package/dist/session/project-match.js +63 -0
  93. package/dist/session/queued-step.js +45 -0
  94. package/dist/session/resume.js +72 -24
  95. package/dist/session/schema.js +3 -1
  96. package/dist/session/store.js +96 -51
  97. package/dist/skill-catalog.js +2 -2
  98. package/dist/skills/bundled-skills.js +53 -53
  99. package/dist/tools/_filesystem-shared.js +174 -12
  100. package/dist/tools/_team-work-path.js +133 -0
  101. package/dist/tools/approval.js +7 -1
  102. package/dist/tools/dispatch-skill.js +8 -4
  103. package/dist/tools/filesystem/extract-xlsx.js +27 -3
  104. package/dist/tools/filesystem/file-edit.js +12 -4
  105. package/dist/tools/filesystem/file-read.js +8 -4
  106. package/dist/tools/filesystem/file-write.js +12 -4
  107. package/dist/tools/filesystem/glob.js +33 -8
  108. package/dist/tools/filesystem/grep.js +9 -4
  109. package/dist/tools/filesystem/read-document.js +6 -3
  110. package/dist/tools/lane/app-lane-resolve.js +1 -1
  111. package/dist/tools/project/cca_merge.js +8 -2
  112. package/dist/tools/project/rules_propose.js +59 -0
  113. package/dist/tools/project/team_estate.js +91 -0
  114. package/dist/tools/project/team_findings.js +89 -5
  115. package/dist/tools/project/upgrade_merge_progress.js +8 -2
  116. package/dist/tools/sap-read.js +21 -4
  117. package/dist/tools/sap-write.js +200 -10
  118. package/dist/tools/subagent/read_agent.js +3 -0
  119. package/dist/tools/tool-loading.js +1 -1
  120. package/dist/tools/transport-fit-check.js +117 -0
  121. package/dist/tools/transport-fit.js +15 -7
  122. package/dist/tools/transport.js +57 -2
  123. package/dist/ui/app.js +2 -2
  124. package/dist/ui/approval-modal.js +74 -6
  125. package/dist/ui/widgets/confirm-card.js +68 -5
  126. package/dist/upgrade-check.js +49 -4
  127. package/dist/util/open-in-browser.js +143 -0
  128. package/package.json +3 -3
@@ -7,6 +7,7 @@ import { sessionsDir, pinnedSessionsDir } from '../config/paths.js';
7
7
  import { SCHEMA_VERSION } from './schema.js';
8
8
  import { userPromptText, isUserPrompt, promptPreviewText } from './user-prompt.js';
9
9
  import { stripUnsignedThinking, dropTrailingThinkingOnly } from '../agent/repair-partial.js';
10
+ import { normDir, sessionMatchesFolder } from './project-match.js';
10
11
  export function makeSessionId() {
11
12
  return crypto.randomBytes(4).toString('hex');
12
13
  }
@@ -71,6 +72,9 @@ function validateSchema(parsed) {
71
72
  // Phase 4 chunk 4A: agentMode defaults to 'skill' for sessions written
72
73
  // before this chunk landed. Mirrors the lastUserPrompt back-compat above.
73
74
  parsed.agentMode ??= 'skill';
75
+ // 2026-10-08: sessions are listed per folder. Older files have no folder.
76
+ parsed.project_dir ??= null;
77
+ parsed.project_id ??= null;
74
78
  // Phase 1 cost tracking (2026-05-16): cache_creation_input_tokens added
75
79
  // to usage. Pre-2026-05-16 sessions don't have it; default to 0 so the
76
80
  // cost calculator can safely treat usage as a TokenCounts shape.
@@ -130,11 +134,17 @@ export const RESUME_PICKER_LIMIT = 20;
130
134
  async function recentSessionFiles(dir, limit) {
131
135
  // Merge note — Task 18 reader-* child sessions are skipped by NAME here, so a
132
136
  // burst of reader files can never crowd real sessions out of the window.
133
- const files = (await fs.readdir(dir).catch(() => []))
134
- .filter((f) => f.endsWith('.json') && !f.includes('-reader-'));
137
+ const files = await sessionFileNames(dir);
135
138
  const cap = Math.max(limit * 3, 50);
136
139
  if (files.length <= cap)
137
140
  return files;
141
+ return (await newestFirst(dir, files)).slice(0, cap);
142
+ }
143
+ async function sessionFileNames(dir) {
144
+ return (await fs.readdir(dir).catch(() => []))
145
+ .filter((f) => f.endsWith('.json') && !f.includes('-reader-'));
146
+ }
147
+ async function newestFirst(dir, files) {
138
148
  const stamped = await Promise.all(files.map(async (f) => {
139
149
  try {
140
150
  return { f, t: (await fs.stat(path.join(dir, f))).mtimeMs };
@@ -144,78 +154,113 @@ async function recentSessionFiles(dir, limit) {
144
154
  }
145
155
  }));
146
156
  stamped.sort((a, b) => b.t - a.t);
147
- return stamped.slice(0, cap).map((x) => x.f);
157
+ return stamped.map((x) => x.f);
158
+ }
159
+ /** 2026-10-08 — every session file, newest first by mtime (no cap): `--continue` / `--resume` scans. */
160
+ async function allSessionFilesNewestFirst(dir) {
161
+ return newestFirst(dir, await sessionFileNames(dir));
162
+ }
163
+ /** One picker row from a session file; null for a reader child or a corrupt file. */
164
+ async function summaryOf(file, filename) {
165
+ try {
166
+ const raw = await fs.readFile(file, 'utf-8');
167
+ const parsed = JSON.parse(raw);
168
+ if (isReaderSessionId(parsed.id))
169
+ return null; // Task 18 — internal child, never resumed
170
+ const messages = parsed.messages ?? [];
171
+ // First user message that's a plain string (not a tool_result array) — the
172
+ // most recent prompt text that initiated a turn. We pick the LAST such
173
+ // message because the picker wants the freshest turn's prompt, not the
174
+ // first one of the session.
175
+ let promptSnippet = null;
176
+ for (let i = messages.length - 1; i >= 0; i--) {
177
+ const m = messages[i];
178
+ const text = m ? userPromptText(m) : null;
179
+ if (text !== null) {
180
+ // Strip <session_context>...</session_context> wrapper that the
181
+ // first turn injects, leaving only the user-typed prompt visible
182
+ // in the picker.
183
+ // (fix round 3: every injected block, e.g. <project_context>, and
184
+ // newlines — they broke the picker's table row.)
185
+ const stripped = promptPreviewText(text);
186
+ promptSnippet = (stripped || '<empty>').slice(0, 80);
187
+ break;
188
+ }
189
+ }
190
+ const userTurnCount = messages.filter((m) => m && isUserPrompt(m)).length;
191
+ return {
192
+ id: parsed.id,
193
+ startedAt: parsed.started_at,
194
+ lastTurnAt: parsed.last_turn_at,
195
+ skill: parsed.skill,
196
+ sapSystem: parsed.sap_system,
197
+ filename,
198
+ interrupted: parsed.turnInterrupted === true,
199
+ interruptedReason: parsed.turnInterruptedReason ?? null,
200
+ promptSnippet,
201
+ userTurnCount,
202
+ projectDir: parsed.project_dir ?? null,
203
+ projectId: parsed.project_id ?? null,
204
+ };
205
+ }
206
+ catch {
207
+ return null; // skip corrupt
208
+ }
148
209
  }
210
+ /**
211
+ * The most recent sessions, newest first by last_turn_at.
212
+ * - `here`: only the sessions of that folder (sessionMatchesFolder).
213
+ * - `scanAll` (with `here`): walk every file newest-first instead of the
214
+ * startup window, stopping once `limit` matches are found
215
+ * (`--continue`, `--resume`). Without `here` the window is always used.
216
+ */
149
217
  export async function listSessions(limit = 10, opts = {}) {
150
218
  // NOTE: bypasses validateSchema() for performance — reads JSON directly via
151
219
  // JSON.parse + cast. Any new SessionState field accessed here must be
152
220
  // defaulted at the read site (e.g. `?? 'skill'`) because pre-chunk-4A
153
221
  // serialised sessions on disk lack the field.
154
222
  const dir = sessionsDir();
155
- const files = await recentSessionFiles(dir, limit);
223
+ const scanAll = !!opts.here && opts.scanAll === true;
224
+ const files = scanAll ? await allSessionFilesNewestFirst(dir) : await recentSessionFiles(dir, limit);
225
+ const hereDirNorm = opts.here ? normDir(opts.here.dir) : undefined; // once per call, not per file
156
226
  const results = [];
157
227
  for (const f of files) {
158
- try {
159
- const raw = await fs.readFile(path.join(dir, f), 'utf-8');
160
- const parsed = JSON.parse(raw);
161
- if (isReaderSessionId(parsed.id))
162
- continue; // Task 18 — internal child, never resumed
163
- const messages = parsed.messages ?? [];
164
- // First user message that's a plain string (not a tool_result array) — the
165
- // most recent prompt text that initiated a turn. We pick the LAST such
166
- // message because the picker wants the freshest turn's prompt, not the
167
- // first one of the session.
168
- let promptSnippet = null;
169
- for (let i = messages.length - 1; i >= 0; i--) {
170
- const m = messages[i];
171
- const text = m ? userPromptText(m) : null;
172
- if (text !== null) {
173
- // Strip <session_context>...</session_context> wrapper that the
174
- // first turn injects, leaving only the user-typed prompt visible
175
- // in the picker.
176
- // (fix round 3: every injected block, e.g. <project_context>, and
177
- // newlines — they broke the picker's table row.)
178
- const stripped = promptPreviewText(text);
179
- promptSnippet = (stripped || '<empty>').slice(0, 80);
180
- break;
181
- }
182
- }
183
- const userTurnCount = messages.filter((m) => m && isUserPrompt(m)).length;
184
- results.push({
185
- id: parsed.id,
186
- startedAt: parsed.started_at,
187
- lastTurnAt: parsed.last_turn_at,
188
- skill: parsed.skill,
189
- sapSystem: parsed.sap_system,
190
- filename: f,
191
- interrupted: parsed.turnInterrupted === true,
192
- interruptedReason: parsed.turnInterruptedReason ?? null,
193
- promptSnippet,
194
- userTurnCount,
195
- });
196
- }
197
- catch { /* skip corrupt */ }
228
+ const s = await summaryOf(path.join(dir, f), f);
229
+ if (!s)
230
+ continue;
231
+ if (opts.here && !sessionMatchesFolder(s, opts.here, hereDirNorm))
232
+ continue;
233
+ // Round 6 review M1 — the picker drops 0-turn sessions (unless interrupted)
234
+ // BEFORE the limit, so a pile of start-then-quit files cannot crowd out
235
+ // the sessions worth resuming.
236
+ if (opts.skipEmpty && !(s.userTurnCount > 0 || s.interrupted))
237
+ continue;
238
+ results.push(s);
239
+ if (scanAll && results.length >= limit)
240
+ break;
198
241
  }
199
242
  results.sort((a, b) => b.lastTurnAt.localeCompare(a.lastTurnAt));
200
- // Round 6 review M1 — the picker drops 0-turn sessions (unless interrupted)
201
- // BEFORE the limit, so a pile of start-then-quit files cannot crowd out
202
- // the sessions worth resuming.
203
- const kept = opts.skipEmpty ? results.filter((r) => r.userTurnCount > 0 || r.interrupted) : results;
204
- return kept.slice(0, limit);
243
+ return results.slice(0, limit);
244
+ }
245
+ /** 2026-10-08 — the latest non-empty session of this folder (`--continue`), or null. */
246
+ export async function latestSessionHere(here, opts = {}) {
247
+ const [s] = await listSessions(1, { here, skipEmpty: true, scanAll: opts.scanAll ?? false });
248
+ return s ?? null;
205
249
  }
206
250
  /**
207
251
  * Count interrupted sessions for the startup banner — among the sessions the
208
252
  * `--resume` picker can show (RESUME_PICKER_LIMIT most recent), so the
209
253
  * "! N unfinished sessions" notice never points at sessions the picker does
210
254
  * not list (fix round 3, item 11d). Returns 0 on any IO failure.
255
+ * 2026-10-08 — with `here`, only that folder's sessions count.
211
256
  */
212
- export async function countInterruptedSessions() {
257
+ export async function countInterruptedSessions(here) {
213
258
  try {
214
259
  // Round 8 item 7 — a turn the USER stopped (Ctrl+C, /exit mid-turn) is
215
260
  // not unfinished work: counting it kept "! 1 unfinished session" on every
216
261
  // start until that session happened to finish a turn. The picker still
217
262
  // lists it as "Stopped by you".
218
- return (await listSessions(RESUME_PICKER_LIMIT))
263
+ return (await listSessions(RESUME_PICKER_LIMIT, here ? { here } : {}))
219
264
  .filter((s) => s.interrupted && !isUserStopReason(s.interruptedReason)).length;
220
265
  }
221
266
  catch {
@@ -168,8 +168,8 @@ export const SKILL_CATALOG = [
168
168
  {
169
169
  name: 'abap-cca',
170
170
  category: 'Upgrade / Migrate',
171
- description: 'Custom code analysis pipeline for S/4 upgrade — whole-estate inventory + tier classification + ATC findings + report. Multi-session project with state file.',
172
- whenToUse: `pick this to discover + classify the whole custom estate (not /abap-migrate — that's per object)`,
171
+ description: 'Reads the estate inventory the lead pulled; proposes scope, areas, retire candidates and next commands; read-only.',
172
+ whenToUse: `pick this after cspeach team discover, to choose packages, lanes and a first focus (not /abap-migrate — that's per object)`,
173
173
  asks: 'which package or namespace? e.g. "Z*" or "package ZTEST_LAS"',
174
174
  },
175
175
  {