@cspeach/cli 1.1.28 → 1.1.30

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 (41) hide show
  1. package/dist/agent/loop.js +6 -3
  2. package/dist/agent/turn-error-ux.js +40 -0
  3. package/dist/auth/auth-file.js +2 -2
  4. package/dist/cli.js +4 -0
  5. package/dist/commands/login.js +1 -1
  6. package/dist/commands/logout.js +1 -1
  7. package/dist/commands/team.js +298 -26
  8. package/dist/commands/whoami.js +1 -1
  9. package/dist/config/paths.js +15 -1
  10. package/dist/one-shot.js +1 -0
  11. package/dist/projects/clean-core-confirm.js +36 -0
  12. package/dist/projects/extract-clean-core.js +79 -0
  13. package/dist/projects/save-command.js +38 -9
  14. package/dist/projects/validate.js +2 -1
  15. package/dist/projects/workspace.js +2 -1
  16. package/dist/register/atc-file.js +9 -1
  17. package/dist/register/findings.js +79 -0
  18. package/dist/register/measure.js +60 -1
  19. package/dist/register/offer.js +33 -0
  20. package/dist/register/picture.js +21 -0
  21. package/dist/register/records.js +32 -0
  22. package/dist/register/status-file/register-map.js +213 -0
  23. package/dist/register/status-file/register-model.js +307 -0
  24. package/dist/register/status-file/register-parts.js +509 -0
  25. package/dist/register/status-file/register-status-file.js +171 -0
  26. package/dist/register/status-file/team-css.js +5 -0
  27. package/dist/register/status-file-sync.js +19 -0
  28. package/dist/register/status-html.js +17 -0
  29. package/dist/register/workbooks.js +104 -0
  30. package/dist/register/write-whole.js +22 -0
  31. package/dist/register/xlsx.js +117 -0
  32. package/dist/renderer/tool-labels.js +2 -1
  33. package/dist/repl.js +1 -0
  34. package/dist/skills/bundled-skills.js +83 -83
  35. package/dist/tools/_filesystem-shared.js +16 -9
  36. package/dist/tools/project/team_findings.js +72 -0
  37. package/dist/tools/sap-write.js +8 -0
  38. package/dist/tools/tool-loading.js +1 -1
  39. package/dist/ui/app.js +4 -0
  40. package/dist/ui/widgets/hold-raw-mode.js +26 -0
  41. package/package.json +2 -2
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import * as path from 'node:path';
11
11
  import { promises as fsPromises, realpathSync, existsSync } from 'node:fs';
12
- import { cspeachRoot } from '../config/paths.js';
12
+ import { cspeachRoot, defaultCspeachRoot } from '../config/paths.js';
13
13
  /**
14
14
  * Phase 3 sensitive in-root directory prefixes — refused by all filesystem
15
15
  * tools. Matched as `rel === prefix` or `rel` starts with `prefix + '/'`.
@@ -229,18 +229,25 @@ function relIsInside(p, rel) {
229
229
  return rel === '' || (!rel.startsWith('..') && !p.isAbsolute(rel));
230
230
  }
231
231
  /**
232
- * True if `absPath` is the CLI home (`cspeachRoot()`, `~/.cspeach`) or below
233
- * it — auth tokens, config, sessions, snapshots, logs. Checked against the
232
+ * True if `absPath` is the CLI home (`cspeachRoot()`) or `~/.cspeach`, or below
233
+ * either — auth tokens, config, sessions, snapshots, logs. Checked against the
234
234
  * spelled and the real home, and through NTFS spellings of the `.cspeach`
235
235
  * segment (`.cspeach.`, `.cspeach `), case-insensitively.
236
236
  */
237
237
  function isUnderCliHome(absPath, p, native) {
238
- const home = cspeachRoot();
239
- const homes = [p.resolve(home)];
240
- if (native) {
241
- const real = realDirOrNull(home);
242
- if (real && real !== homes[0])
243
- homes.push(real);
238
+ // Both homes: the one CSPEACH_HOME chose AND ~/.cspeach, which still holds
239
+ // history, logs, streams — and maybe an auth.json from before the move.
240
+ const chosen = [cspeachRoot(), defaultCspeachRoot()];
241
+ const homes = [];
242
+ for (const home of chosen) {
243
+ const spelled = p.resolve(home);
244
+ if (!homes.includes(spelled))
245
+ homes.push(spelled);
246
+ if (native) {
247
+ const real = realDirOrNull(home);
248
+ if (real && !homes.includes(real))
249
+ homes.push(real);
250
+ }
244
251
  }
245
252
  for (const h of homes) {
246
253
  const fromParent = p.relative(p.dirname(h), absPath).replace(/\\/g, '/');
@@ -0,0 +1,72 @@
1
+ /**
2
+ * team_findings — read-only (spec 2026-10-06 §4.2, triage).
3
+ *
4
+ * What a /abap-clean-core triage proposes from: the objects of a lane (or of
5
+ * package patterns) with the findings measurement zero recorded for them, read
6
+ * from the team project this laptop joined. No SAP call, no second ATC run, no
7
+ * write: every decision is still recorded by program code, one Yes per row.
8
+ */
9
+ import { dispositionOf, DISPOSITION_WORD, inLane } from '@cspeach/register-core';
10
+ import { registerTool } from '../index.js';
11
+ import { findingsByObject, shownUpTo } from '../../register/findings.js';
12
+ import { registerFor } from '../../register/picture.js';
13
+ import { activeProject, readProjectFile } from '../../register/store.js';
14
+ const CAP = 200;
15
+ /** A decision's reason was written on another laptop and goes to the model: cleaned like a finding title, kept whole up to 300. */
16
+ const REASON_CAP = 300;
17
+ const match = (pattern, value) => new RegExp(`^${pattern.trim().replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i').test(value);
18
+ export async function teamFindingsHandler(args, _ctx) {
19
+ const joined = activeProject();
20
+ if (!joined)
21
+ return { content: 'This laptop has not joined a team project. The developer runs: cspeach team join <folder>', is_error: true };
22
+ try {
23
+ const project = readProjectFile(joined.projectDir);
24
+ const { register } = registerFor(joined.projectDir, joined.outboxDir);
25
+ const laneId = args.lane?.trim();
26
+ const patterns = (args.packages ?? '').split(',').map((s) => s.trim()).filter(Boolean);
27
+ let objects = register.objects;
28
+ let scope = 'all objects';
29
+ if (laneId) {
30
+ const lane = register.lanes.find((l) => l.id.toLowerCase() === laneId.toLowerCase());
31
+ if (!lane)
32
+ return { content: `There is no lane "${laneId}". Lanes: ${register.lanes.map((l) => l.id).join(', ') || 'none yet'}.`, is_error: true };
33
+ objects = objects.filter((o) => inLane(o, lane.id));
34
+ scope = `lane ${lane.id}`;
35
+ }
36
+ else if (patterns.length) {
37
+ objects = objects.filter((o) => patterns.some((p) => match(p, o.package)));
38
+ scope = `packages ${patterns.join(', ')}`;
39
+ }
40
+ const findings = findingsByObject(joined.projectDir, joined.outboxDir);
41
+ const shown = objects.slice(0, CAP).map((o) => {
42
+ const f = findings.get(o.key);
43
+ const d = dispositionOf(o.decision);
44
+ return {
45
+ type: o.type, name: o.name, package: o.package,
46
+ owner: register.lanes.find((l) => l.id === o.lane)?.owner ?? null,
47
+ decided: d ? { disposition: DISPOSITION_WORD[d], level: o.decision.level ?? null, targetLevel: o.decision.targetLevel ?? null, reason: typeof o.decision.reason === 'string' ? shownUpTo(o.decision.reason, REASON_CAP) : null } : null,
48
+ baselineAt: f?.baselineAt ?? null, baseline: f?.baseline ?? [], openNow: o.openFindings,
49
+ };
50
+ });
51
+ return { content: JSON.stringify({ teamProject: project.name, scope, objects: shown, ...(objects.length > CAP ? { more: objects.length - CAP } : {}) }) };
52
+ }
53
+ catch (e) {
54
+ return { content: `The team project could not be read: ${e instanceof Error ? e.message : String(e)}`, is_error: true };
55
+ }
56
+ }
57
+ registerTool({
58
+ name: 'team_findings',
59
+ description: 'Read-only. The objects of a lane (or package patterns) in the team project this laptop joined, with the findings '
60
+ + 'measurement zero recorded for each and any decision already recorded. Use it for a /abap-clean-core triage. No SAP call.',
61
+ isMutating: false,
62
+ category: 'project',
63
+ requiresSap: false,
64
+ input_schema: {
65
+ type: 'object',
66
+ properties: {
67
+ lane: { type: 'string', description: 'Lane id, e.g. "fin".' },
68
+ packages: { type: 'string', description: 'Package patterns, comma-separated, e.g. "ZCC_DEMO_*". Ignored when lane is given.' },
69
+ },
70
+ },
71
+ handler: (args, ctx) => teamFindingsHandler(args, ctx),
72
+ });
@@ -34,6 +34,7 @@ import { getLook } from '../renderer/look.js';
34
34
  import { getEffectiveBodyWidth } from '../renderer/tty.js';
35
35
  import { verifySyntax, verifyActive } from './verify.js';
36
36
  import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from './syntax-state.js';
37
+ import { teamFixOffers } from '../register/offer.js';
37
38
  import { appendPending, finalizeToolCall } from '../session/pending.js';
38
39
  import { auditLog } from '@cspeach/sap-client';
39
40
  import { ERR } from '../errors/codes.js';
@@ -1314,6 +1315,7 @@ registerTool({
1314
1315
  // still inactive with syntax errors" incident.
1315
1316
  const stillInactive = [];
1316
1317
  const unverifiable = [];
1318
+ const verified = [];
1317
1319
  for (const obj of args.objects) {
1318
1320
  const v = await verifyActive(ctx.adt, obj.name, obj.type);
1319
1321
  if (v.verifyError) {
@@ -1325,6 +1327,7 @@ registerTool({
1325
1327
  else if (activationResults.find((r) => r.name === obj.name && r.type === obj.type)?.success) {
1326
1328
  // Final review A M-2 — activated AND verified: the source compiles.
1327
1329
  clearSyntaxErrorsAfterActivation(obj.name);
1330
+ verified.push({ name: obj.name, type: obj.type });
1328
1331
  }
1329
1332
  }
1330
1333
  const overallSuccess = !anyError && stillInactive.length === 0 && unverifiable.length === 0;
@@ -1342,6 +1345,11 @@ registerTool({
1342
1345
  // successful write op. Without this, Rule 8 counts a failed activate
1343
1346
  // as if it had run, inflating the "batch in progress" count and
1344
1347
  // confusing the user about how many real writes have happened.
1348
+ // 2026-10-06 — an object of the team project this laptop joined: offer the
1349
+ // claim in one line. Nothing is recorded; only the developer claims a fix.
1350
+ if (overallSuccess)
1351
+ for (const line of teamFixOffers(verified))
1352
+ ctx.chunkEmitter?.emit('info', line);
1345
1353
  return {
1346
1354
  content: JSON.stringify({
1347
1355
  success: overallSuccess,
@@ -57,7 +57,7 @@ export const SKILL_STARTERS = Object.freeze({
57
57
  'abap-app': ['app_lane_resolve', 'read_document'],
58
58
  'abap-atc-fix': ['sap_atc_run'],
59
59
  'abap-cca': ['sap_atc_run', 'sap_usage_references', 'sap_api_state'],
60
- 'abap-clean-core': ['sap_api_state', 'sap_atc_run', 'sap_usage_references'],
60
+ 'abap-clean-core': ['sap_api_state', 'sap_atc_run', 'sap_usage_references', 'team_findings'],
61
61
  'abap-data-model': ['sap_create_domain', 'sap_create_data_element'],
62
62
  'abap-document': ['sap_usage_references'],
63
63
  'abap-handover': ['sap_usage_references'],
package/dist/ui/app.js CHANGED
@@ -24,6 +24,7 @@ import { RewindPanel } from './rewind-panel.js';
24
24
  import { rewindEmitter } from './rewind-emitter.js';
25
25
  import { todoEmitter } from './todo-emitter.js';
26
26
  import { approvalEmitter } from './approval-emitter.js';
27
+ import { useHoldRawMode } from './widgets/hold-raw-mode.js';
27
28
  import { SapStateContext, globalStore } from './sap-state-store.js';
28
29
  import { coachingPickerEmitter } from './coaching-picker-emitter.js';
29
30
  import { CoachingPicker } from './widgets/coaching-picker.js';
@@ -66,6 +67,9 @@ export function isAskingOccupant(occupant) {
66
67
  * The terminal's native scrollback handles everything above the live area.
67
68
  */
68
69
  export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger, getToolCalls, getTodos, getRewindCandidates, onRewindRestore, sapRole, sapAlias, onInterrupt, onExitRequest }) {
70
+ // 1.1.29 — keep raw mode held across card hand-overs (Windows froze the
71
+ // approval card when Ink released it between cards). See hold-raw-mode.ts.
72
+ useHoldRawMode();
69
73
  const [pendingApproval, setPendingApproval] = useState(null);
70
74
  // Task 3 review I1(b): synchronous mirror + monotonic remount key — the same
71
75
  // I2 treatment ask-question already has. A second approval request arriving
@@ -0,0 +1,26 @@
1
+ import { useEffect } from 'react';
2
+ import { useStdin } from 'ink';
3
+ /**
4
+ * Hold one Ink raw-mode reference for the life of the component (1.1.29).
5
+ *
6
+ * Ink ref-counts raw mode: every active useInput takes a reference, and when
7
+ * the count falls to 0 Ink calls setRawMode(false) + unref on stdin. The main
8
+ * input boxes go inactive while a card is open, so at every card hand-over
9
+ * (plan card closes → per-change approval card opens) the count touched 0 and
10
+ * back. On Windows that release leaves a cooked console read pending and
11
+ * stdin delivers no more keys — the approval card sat frozen (live, 1.1.28;
12
+ * reproduced in a ConPTY replay, and fixed there by suppressing the release).
13
+ *
14
+ * Mounted once at the <App> root, this reference keeps the count above 0
15
+ * until the app unmounts, so hand-overs never release raw mode. It reads no
16
+ * keys itself: key routing is unchanged.
17
+ */
18
+ export function useHoldRawMode() {
19
+ const { setRawMode, isRawModeSupported } = useStdin();
20
+ useEffect(() => {
21
+ if (!isRawModeSupported)
22
+ return;
23
+ setRawMode(true);
24
+ return () => { setRawMode(false); };
25
+ }, [isRawModeSupported, setRawMode]);
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cspeach/cli",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
5
5
  "keywords": [
6
6
  "abap",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@anthropic-ai/sdk": "^0.90.0",
55
- "@cspeach/register-core": "0.1.0",
55
+ "@cspeach/register-core": "0.2.0",
56
56
  "@cspeach/sap-client": "0.1.16",
57
57
  "@iarna/toml": "^2.2.5",
58
58
  "@inkjs/ui": "^2.0.0",