@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
@@ -34,7 +34,10 @@ 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, cspeachWritesThisSession } from '../register/offer.js';
38
+ import { isJourneyActive } from '../journey/flow.js';
37
39
  import { appendPending, finalizeToolCall } from '../session/pending.js';
40
+ import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
38
41
  import { auditLog } from '@cspeach/sap-client';
39
42
  import { ERR } from '../errors/codes.js';
40
43
  import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
@@ -42,7 +45,8 @@ import { resolveWriteTransport } from './transport-resolution.js';
42
45
  // F-L2-1: statusline-only wire — records the transport a write ACTUALLY
43
46
  // stamped so the `tr` segment stops reading `none` for plan-supplied
44
47
  // transports. Never touches the session transport pointer.
45
- import { noteWriteTransport } from '../repl/current-transport.js';
48
+ import { noteWriteTransport, setCurrentTransport } from '../repl/current-transport.js';
49
+ import { markTransportCreatedThisSession } from './transport.js';
46
50
  import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
47
51
  import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
48
52
  // ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
@@ -118,6 +122,21 @@ function chainActivation(w) {
118
122
  return { state: 'active' };
119
123
  return w.stillInactive ? { state: 'still_inactive' } : { state: 'unverified' };
120
124
  }
125
+ /**
126
+ * 1.1.31 — the write itself activated and verified an object of the joined team
127
+ * project: CSPeach wrote it, so the fix is recorded for the developer (rehearsal
128
+ * 2026-10-07: the refactor's write activated the class; no separate sap_activate
129
+ * ran, so the hook in sap_activate alone never fired).
130
+ */
131
+ /** Guided clean-core journey on: the fix lines in solo words (register/offer.ts). */
132
+ const journeyWording = (ctx) => (isJourneyActive(ctx.session) ? { wording: 'journey' } : {});
133
+ function recordOwnFixAfterWrite(ctx, type, name, transport, w) {
134
+ if (!w?.success || !w.verified)
135
+ return;
136
+ const written = new Map([[String(name).trim().toUpperCase(), String(transport ?? '').trim().toUpperCase()]]);
137
+ for (const line of teamFixOffers([{ type, name }], { written, ...journeyWording(ctx) }))
138
+ ctx.chunkEmitter?.emit('info', line);
139
+ }
121
140
  function activatedByWrite(w) {
122
141
  if (!w)
123
142
  return {};
@@ -316,6 +335,7 @@ registerTool({
316
335
  newSource: oldSource !== undefined ? args.source : undefined,
317
336
  },
318
337
  });
338
+ recordOwnFixAfterWrite(ctx, args.type, args.name, resolved.owning ?? resolved.transport, writeActivation);
319
339
  return {
320
340
  content: JSON.stringify({
321
341
  written: true,
@@ -525,6 +545,7 @@ registerTool({
525
545
  activation: chainActivation(writeActivation),
526
546
  card: { object: args.class_name, what: `method ${args.method_name}`, transport: resolved.transport },
527
547
  });
548
+ recordOwnFixAfterWrite(ctx, 'CLAS', args.class_name, resolved.owning ?? resolved.transport, writeActivation);
528
549
  return {
529
550
  content: JSON.stringify({
530
551
  updated: true,
@@ -672,7 +693,14 @@ registerTool({
672
693
  name: 'sap_create_object',
673
694
  description: 'Create a new ABAP object shell. '
674
695
  + 'Requires approval_id. No snapshot needed (new object). '
675
- + 'Write source separately via sap_set_source, then call sap_activate.',
696
+ + 'Write source separately via sap_set_source, then call sap_activate. '
697
+ + 'NEW PACKAGE (type DEVC): do NOT call sap_transport_create first. Without `transport`, '
698
+ + 'this tool asks SAP what the new package needs and creates its first transport itself, '
699
+ + 'then the package — ONE approval card covers both; the card text reads '
700
+ + "\"Create package NAME and its transport '<text>'\" (<text> = the description argument, ≤60 ASCII chars) and the approval object is the package name. "
701
+ + 'Optional DEVC params: description, softwareComponent (default HOME), transportLayer '
702
+ + '(default: the system\'s layer), applicationComponent, superPackage, responsible '
703
+ + '(default: you, upper case). A $-local package needs no transport.',
676
704
  isMutating: true,
677
705
  requiresSap: true,
678
706
  input_schema: {
@@ -689,6 +717,12 @@ registerTool({
689
717
  serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
690
718
  bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
691
719
  bindingVersion: { type: 'string', description: 'SRVB ONLY (optional): binding version. Default V4 (OData V4 UI). Use V2 for OData V2.' },
720
+ // DEVC-only (§10a): package attributes; ignored for other types.
721
+ softwareComponent: { type: 'string', description: 'DEVC ONLY (optional): software component. Default HOME.' },
722
+ transportLayer: { type: 'string', description: 'DEVC ONLY (optional): transport layer. When omitted: used if the SAP transport check names one; otherwise the create is refused and the layer must be named.' },
723
+ applicationComponent: { type: 'string', description: 'DEVC ONLY (optional): application component, e.g. FI-AR.' },
724
+ superPackage: { type: 'string', description: 'DEVC ONLY (optional): parent package.' },
725
+ responsible: { type: 'string', description: 'DEVC ONLY (optional): responsible user. Default: the logged-on user (upper case).' },
692
726
  },
693
727
  required: ['name', 'type', 'package', 'description', 'approval_id'],
694
728
  },
@@ -721,7 +755,17 @@ registerTool({
721
755
  }
722
756
  }
723
757
  // 3-tier fallback: explicit arg → session current-transport → undefined
724
- const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
758
+ let effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
759
+ // ── 1.1.31: does the chosen request fit the package? (see transport-fit-check.ts)
760
+ // A package create decides its own transport below (§10a), so only objects.
761
+ const isDevc = String(args.type ?? '').toUpperCase().split('/')[0].trim() === 'DEVC';
762
+ if (!isDevc) {
763
+ const fit = await checkTransportFit(ctx.adt, {
764
+ type: args.type, name: args.name, pkg: args.package, transport: effectiveTransport,
765
+ });
766
+ if (!fit.ok)
767
+ return { content: JSON.stringify(fit.refusal), is_error: true };
768
+ }
725
769
  // ── WAL: record pending entry ────────────────────────────────────────────
726
770
  await appendPending(ctx.session, {
727
771
  tool_use_id: toolUseId,
@@ -729,6 +773,48 @@ registerTool({
729
773
  args: { name: args.name, type: args.type, package: args.package },
730
774
  started_at: new Date().toISOString(),
731
775
  });
776
+ // ── §10a: a new package and its first transport, under this ONE approval ─
777
+ let devcExtras = {};
778
+ let devcTransportCreated;
779
+ if (isDevc) {
780
+ let flow;
781
+ try {
782
+ flow = await prepareNewPackage(args, ctx);
783
+ }
784
+ catch (err) {
785
+ // A throw (network, auth, …) must still close the pending entry and
786
+ // reach the person in plain words, never as a raw exception.
787
+ const msg = err instanceof Error ? err.message : String(err);
788
+ const answered = typeof err?.status === 'number';
789
+ const message = answered
790
+ ? `SAP refused to create the transport: ${msg}`
791
+ : `Could not reach SAP to create the transport: ${msg}`;
792
+ await finalizeToolCall(ctx.session, toolUseId, message, true, Date.now() - startedAt);
793
+ return {
794
+ content: JSON.stringify({ error: 'transport_create_failed', message, next: 'Next: check the connection to SAP and try again.' }),
795
+ is_error: true,
796
+ };
797
+ }
798
+ if ('refused' in flow) {
799
+ await finalizeToolCall(ctx.session, toolUseId, flow.refused.message, true, Date.now() - startedAt);
800
+ return { content: JSON.stringify(flow.refused), is_error: true };
801
+ }
802
+ effectiveTransport = flow.corrNr;
803
+ devcTransportCreated = flow.created;
804
+ if (flow.created) {
805
+ // Same auto-attach as sap_transport_create: later creates in the new
806
+ // package pick this request up through the 3-tier fallback.
807
+ setCurrentTransport(flow.created);
808
+ markTransportCreatedThisSession(flow.created);
809
+ }
810
+ devcExtras = {
811
+ softwareComponent: args.softwareComponent,
812
+ transportLayer: flow.layer,
813
+ applicationComponent: args.applicationComponent,
814
+ superPackage: args.superPackage ? String(args.superPackage).toUpperCase() : undefined,
815
+ responsible: args.responsible,
816
+ };
817
+ }
732
818
  // ── Create ───────────────────────────────────────────────────────────────
733
819
  // createObject(objtype, name, packageName, description, transport?, extras?)
734
820
  let result;
@@ -737,6 +823,7 @@ registerTool({
737
823
  serviceDefinition: args.serviceDefinition,
738
824
  bindingType: args.bindingType,
739
825
  bindingVersion: args.bindingVersion,
826
+ ...devcExtras,
740
827
  });
741
828
  await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
742
829
  }
@@ -744,14 +831,14 @@ registerTool({
744
831
  const errStr = String(err);
745
832
  await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
746
833
  return {
747
- content: JSON.stringify({ error: 'create_failed', detail: errStr }),
834
+ content: JSON.stringify({ error: 'create_failed', detail: errStr, ...orphanRequest(devcTransportCreated) }),
748
835
  is_error: true,
749
836
  };
750
837
  }
751
838
  if (result.status !== 'created') {
752
839
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
753
840
  return {
754
- content: JSON.stringify({ error: 'create_failed', detail: result.error }),
841
+ content: JSON.stringify({ error: 'create_failed', detail: result.error, ...orphanRequest(devcTransportCreated) }),
755
842
  is_error: true,
756
843
  };
757
844
  }
@@ -759,6 +846,22 @@ registerTool({
759
846
  // the 3-tier effectiveTransport above is what SAP stamped.
760
847
  noteWriteTransport(effectiveTransport);
761
848
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
849
+ if (isDevc) {
850
+ const pkgName = result.objectName || String(args.name).toUpperCase();
851
+ return {
852
+ content: JSON.stringify({
853
+ created: true,
854
+ object: pkgName,
855
+ type: 'DEVC',
856
+ ...(effectiveTransport ? { transport: effectiveTransport } : {}),
857
+ ...(devcTransportCreated ? { transport_created: true } : {}),
858
+ message: effectiveTransport
859
+ ? `Created package ${pkgName} on request ${effectiveTransport}.`
860
+ : `Created package ${pkgName} (no transport needed).`,
861
+ next_step: 'create_objects_in_the_package_on_this_transport',
862
+ }),
863
+ };
864
+ }
762
865
  return {
763
866
  content: JSON.stringify({
764
867
  created: true,
@@ -770,6 +873,90 @@ registerTool({
770
873
  };
771
874
  },
772
875
  });
876
+ /**
877
+ * §10a — when the package create fails after the flow already made a request,
878
+ * say so: the request exists, empty, and can be used on the next try.
879
+ */
880
+ function orphanRequest(created) {
881
+ if (!created)
882
+ return {};
883
+ return {
884
+ transport_created: created,
885
+ message: `Request ${created} was created first and is still there, empty. `
886
+ + 'It is now the session transport, so the next try uses it; or ask your Basis team to delete it.',
887
+ };
888
+ }
889
+ /**
890
+ * §10a — decide the transport for a brand-new package the way ADT does:
891
+ * 1. `$`-local package → no transport;
892
+ * 2. explicit `transport` → that one;
893
+ * 3. otherwise POST transportchecks with the package's own entry (a layer only
894
+ * when the caller named one); a refusal stops here. The layer is then the
895
+ * caller's ?? the one CTS resolved (PDEVCLASS); neither → refused in plain
896
+ * words, nothing created (never a value-help guess). No recording needed → no transport;
897
+ * else create the request from the package entry (REF = package URI,
898
+ * layer on the URL).
899
+ * Runs under the approval the caller already verified — nothing is asked twice.
900
+ */
901
+ async function prepareNewPackage(args, ctx) {
902
+ const name = String(args.name ?? '').trim().toUpperCase();
903
+ const explicitLayer = args.transportLayer ? String(args.transportLayer) : undefined;
904
+ if (name.startsWith('$'))
905
+ return { layer: explicitLayer };
906
+ if (args.transport)
907
+ return { corrNr: String(args.transport), layer: explicitLayer };
908
+ let layer = explicitLayer;
909
+ const uri = '/sap/bc/adt/packages/' + encodeURIComponent(name.toLowerCase());
910
+ const check = await ctx.adt.transportCheck({
911
+ pgmid: 'R3TR',
912
+ object: 'DEVC',
913
+ objectName: name,
914
+ devclass: name,
915
+ superPackage: args.superPackage ? String(args.superPackage).toUpperCase() : undefined,
916
+ operation: 'I',
917
+ uri,
918
+ transportLayer: layer,
919
+ });
920
+ if (!check.ok) {
921
+ return {
922
+ refused: {
923
+ error: 'transport_check_refused',
924
+ message: check.error ?? 'SAP refused the transport check with no message.',
925
+ },
926
+ };
927
+ }
928
+ // The layer is the caller's, or the one CTS resolved for this package
929
+ // (PDEVCLASS in the check's answer). Never a guess from the value help: it
930
+ // lists every layer on the box, and its order says nothing about this package.
931
+ layer = layer ?? (check.transportLayer || undefined);
932
+ if (!layer) {
933
+ return {
934
+ refused: {
935
+ error: 'transport_layer_unknown',
936
+ message: `SAP's transport check did not name a transport layer for package ${name}, so CSPeach did not create it.`,
937
+ next: 'Next: ask which transport layer the package belongs on (your Basis team knows), then ask again naming that transport layer.',
938
+ },
939
+ };
940
+ }
941
+ if (!check.requestRequired)
942
+ return { layer };
943
+ const text = args.description ? String(args.description) : `Clean core: ${name}`;
944
+ const tr = await ctx.adt.transportCreate(text, name, { ref: uri, transportLayer: layer });
945
+ if (tr.status === 'error' || !tr.transportNumber) {
946
+ return {
947
+ refused: {
948
+ error: 'transport_create_failed',
949
+ message: `SAP refused to create the transport: ${tr.error ?? 'no transport number came back'}`,
950
+ next: 'Next: check the package and its transport layer, or ask your Basis team.',
951
+ },
952
+ };
953
+ }
954
+ // 1.1.31 — SAP may hand back the user's newest open request instead of a
955
+ // new one; that request may be other work, so it is never used silently.
956
+ if (tr.status !== 'created')
957
+ return { refused: reusedRequestRefusal(tr) };
958
+ return { corrNr: tr.transportNumber, layer, created: tr.transportNumber };
959
+ }
773
960
  // ── sap_set_class_include ─────────────────────────────────────────────────────
774
961
  //
775
962
  // Signature: setClassInclude(className, includeType, source, transport?)
@@ -1314,6 +1501,7 @@ registerTool({
1314
1501
  // still inactive with syntax errors" incident.
1315
1502
  const stillInactive = [];
1316
1503
  const unverifiable = [];
1504
+ const verified = [];
1317
1505
  for (const obj of args.objects) {
1318
1506
  const v = await verifyActive(ctx.adt, obj.name, obj.type);
1319
1507
  if (v.verifyError) {
@@ -1325,6 +1513,7 @@ registerTool({
1325
1513
  else if (activationResults.find((r) => r.name === obj.name && r.type === obj.type)?.success) {
1326
1514
  // Final review A M-2 — activated AND verified: the source compiles.
1327
1515
  clearSyntaxErrorsAfterActivation(obj.name);
1516
+ verified.push({ name: obj.name, type: obj.type });
1328
1517
  }
1329
1518
  }
1330
1519
  const overallSuccess = !anyError && stillInactive.length === 0 && unverifiable.length === 0;
@@ -1342,6 +1531,15 @@ registerTool({
1342
1531
  // successful write op. Without this, Rule 8 counts a failed activate
1343
1532
  // as if it had run, inflating the "batch in progress" count and
1344
1533
  // confusing the user about how many real writes have happened.
1534
+ // 2026-10-06 — an object of the team project this laptop joined. 1.1.31:
1535
+ // when CSPeach itself wrote its source in this session, the fix is recorded
1536
+ // for the developer (only the next check proves it); otherwise one line
1537
+ // offers the claim.
1538
+ if (overallSuccess) {
1539
+ const written = cspeachWritesThisSession(ctx.session?.toolCalls);
1540
+ for (const line of teamFixOffers(verified, { written, ...journeyWording(ctx) }))
1541
+ ctx.chunkEmitter?.emit('info', line);
1542
+ }
1345
1543
  return {
1346
1544
  content: JSON.stringify({
1347
1545
  success: overallSuccess,
@@ -78,6 +78,9 @@ export async function readAgentHandler(args, ctx) {
78
78
  // The session model, never a cheaper one (owner decision 11); effort low.
79
79
  const model = ctx.session.model;
80
80
  const childSession = newSession(`reader-${Date.now()}-${Math.floor(Math.random() * 1e9).toString(16)}`, ctx.session.sap_system ?? null, READER_SKILL, model);
81
+ // Sessions/folders (Task 6) — a child keeps the parent's folder and id.
82
+ childSession.project_dir = ctx.session.project_dir ?? null;
83
+ childSession.project_id = ctx.session.project_id ?? null;
81
84
  // Only what a read needs. No previewHook (no writes), no chunkEmitter /
82
85
  // todoEmitter (the child never paints the parent UI), no pendingDispatch /
83
86
  // currentTransport (the child never changes parent REPL state).
@@ -56,8 +56,8 @@ export const SKILL_STARTERS = Object.freeze({
56
56
  // Both flag-gated (LOCAL_BUILD_TOOLS / STANDALONE_FILE_TOOLS): skipped when off.
57
57
  'abap-app': ['app_lane_resolve', 'read_document'],
58
58
  'abap-atc-fix': ['sap_atc_run'],
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'],
59
+ 'abap-cca': ['team_estate', 'sap_atc_run', 'sap_usage_references', 'sap_api_state'],
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'],
@@ -0,0 +1,117 @@
1
+ /**
2
+ * 1.1.31 — does the chosen request fit the package a new object goes into?
3
+ *
4
+ * Live 2026-10-07 (S/4HANA): a package with no transport layer is local. A
5
+ * create in it on a transportable request (target CL5) failed with HTTP 403
6
+ * CTS_WBO_API 047 "Request S4HK903753 is not a local request". Eclipse avoids
7
+ * this by asking POST /sap/bc/adt/cts/transportchecks first: the answer says
8
+ * whether a request is needed and lists the open requests that fit.
9
+ *
10
+ * This asks the same check before sap_create_object (non-package) sends the
11
+ * create:
12
+ * - no request needed (local package) and one was chosen → refused. The
13
+ * transport is never dropped silently, and nothing falls back to a local
14
+ * request: the person decides.
15
+ * - a request is needed and SAP lists fitting ones, but the chosen request
16
+ * (or task) is not among them → refused, naming the ones SAP listed.
17
+ * Live 2026-10-07: a Z package with NO layer still answers "request
18
+ * required", and lists only LOCAL requests (S4HK903755 was listed for
19
+ * ZLOCAL, the transportable S4HK903753 was not). No layer in the answer +
20
+ * the chosen request not listed → the local-package wording, then the list.
21
+ * - SAP lists none → no verdict (it may list only the user's own requests);
22
+ * the create goes ahead and SAP decides.
23
+ * - the check itself fails (endpoint missing on an older system, an error
24
+ * answer, a throw) → no verdict, the create goes ahead as before. This keeps
25
+ * older systems working; the reason goes to a CSPEACH_DEBUG line only.
26
+ */
27
+ import { toObjectUrl, localPackageMessage } from '@cspeach/sap-client';
28
+ /** At most this many fitting requests are named in a refusal. */
29
+ export const MAX_FITTING_SHOWN = 5;
30
+ function debug(msg) {
31
+ if (process.env['CSPEACH_DEBUG'] !== '1')
32
+ return;
33
+ process.stderr.write(`[transport-fit-check] ${msg}\n`);
34
+ }
35
+ export async function checkTransportFit(adt, o) {
36
+ const pkg = String(o.pkg ?? '').trim().toUpperCase();
37
+ const transport = String(o.transport ?? '').trim().toUpperCase();
38
+ // No request chosen: nothing to fit. A $ package is local by name.
39
+ if (!transport || !pkg || pkg.startsWith('$'))
40
+ return { ok: true };
41
+ if (typeof adt.transportCheck !== 'function')
42
+ return { ok: true };
43
+ const object = String(o.type ?? '').toUpperCase().split('/')[0].trim();
44
+ const objectName = String(o.name ?? '').trim().toUpperCase();
45
+ let uri;
46
+ try {
47
+ uri = toObjectUrl(object, objectName).toLowerCase();
48
+ }
49
+ catch {
50
+ debug(`no object URI for ${object} ${objectName}, no fit check`);
51
+ return { ok: true };
52
+ }
53
+ let check;
54
+ try {
55
+ check = await adt.transportCheck({ pgmid: 'R3TR', object, objectName, devclass: pkg, operation: 'I', uri });
56
+ }
57
+ catch (err) {
58
+ debug(`check failed, create goes ahead: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
59
+ return { ok: true };
60
+ }
61
+ if (!check.ok) {
62
+ debug(`check gave no answer, create goes ahead: ${(check.error ?? '').slice(0, 200)}`);
63
+ return { ok: true };
64
+ }
65
+ if (!check.requestRequired) {
66
+ // Local only when SAP also names no layer for the package (PDEVCLASS):
67
+ // a "no recording" answer next to a layer is not understood, so no verdict.
68
+ if (check.transportLayer) {
69
+ debug(`no recording needed but package ${pkg} has layer ${check.transportLayer}, no verdict`);
70
+ return { ok: true };
71
+ }
72
+ return { ok: false, refusal: { error: 'package_local', package_local: true, message: localPackageMessage(pkg) } };
73
+ }
74
+ const requests = check.requests ?? [];
75
+ if (requests.length === 0) {
76
+ debug(`SAP listed no fitting requests for ${pkg}, no verdict`);
77
+ return { ok: true };
78
+ }
79
+ const fits = requests.some((r) => r.number.toUpperCase() === transport || (r.tasks ?? []).some((t) => t.toUpperCase() === transport));
80
+ if (fits)
81
+ return { ok: true };
82
+ const shown = requests.slice(0, MAX_FITTING_SHOWN).map((r) => ({ number: r.number, description: r.text ?? '' }));
83
+ const list = shown.map((r) => (r.description ? `${r.number} ('${r.description}')` : r.number)).join(', ');
84
+ if (!check.transportLayer) {
85
+ return {
86
+ ok: false,
87
+ refusal: {
88
+ error: 'package_local',
89
+ package_local: true,
90
+ message: `${localPackageMessage(pkg)} Request ${transport} is not a local request. SAP lists these local requests for it: ${list}.`,
91
+ fitting_requests: shown,
92
+ },
93
+ };
94
+ }
95
+ return {
96
+ ok: false,
97
+ refusal: {
98
+ error: 'transport_does_not_fit',
99
+ message: `Request ${transport} does not fit package ${pkg}. SAP lists these open requests for it: ${list}. `
100
+ + 'Name one of them as the transport for this create, or create a new request for the package.',
101
+ fitting_requests: shown,
102
+ },
103
+ };
104
+ }
105
+ /**
106
+ * 1.1.31 — SAP answered a create with an existing request (the user's newest
107
+ * open one, which may belong to other work). It is never used silently, and
108
+ * never advised to be released.
109
+ */
110
+ export function reusedRequestRefusal(tr) {
111
+ return {
112
+ error: 'transport_reused',
113
+ message: `SAP handed back an existing request ${tr.transportNumber ?? '(no number)'} ('${tr.existingDescription ?? ''}') `
114
+ + 'instead of creating a new one; CSPeach did not use it.',
115
+ next: 'Next: create the request for the package in SE09/SE10, or name an existing request that fits.',
116
+ };
117
+ }
@@ -28,6 +28,7 @@
28
28
  * back empty). Strictly best-effort: a failed lookup leaves those fields as
29
29
  * SAP returned them, gives no verdict, and writes a CSPEACH_DEBUG line.
30
30
  */
31
+ import { localPackageMessage } from '@cspeach/sap-client';
31
32
  const TRKORR = /^[A-Z0-9]{1,20}$/;
32
33
  const PACKAGE = /^[A-Z0-9_/$]{1,30}$/;
33
34
  const REQUEST_SID = /^([A-Z0-9]{3})K\d+$/;
@@ -263,29 +264,33 @@ async function requestPackages(adt, requests) {
263
264
  }
264
265
  return out;
265
266
  }
266
- /** The package's consolidation target, or undefined when unknown / ambiguous. */
267
+ /**
268
+ * The package's consolidation target, or undefined when unknown / ambiguous.
269
+ * 1.1.31 — `local`: the package row was read and has no transport layer, so
270
+ * SAP treats it as local (said to the person, not only in a debug line).
271
+ */
267
272
  async function packageTarget(adt, pkg, sid) {
268
273
  const name = pkg.toUpperCase().trim();
269
274
  if (!PACKAGE.test(name))
270
- return undefined;
275
+ return {};
271
276
  const dev = await rows(adt, `SELECT DEVCLASS, PDEVCLASS FROM TDEVC WHERE DEVCLASS = '${name}'`, 1, 'TDEVC');
272
277
  const layer = (dev?.[0]?.['PDEVCLASS'] ?? '').trim();
273
278
  if (!layer || !PACKAGE.test(layer)) {
274
279
  if (dev)
275
280
  debug(`package ${name} has no transport layer, no ranking`);
276
- return undefined;
281
+ return dev && dev.length > 0 && !layer ? { local: true } : {};
277
282
  }
278
283
  let routes = await rows(adt, `SELECT VERSION, INTSYS, CONSYS FROM TCERELE WHERE TRANSLAYER = '${layer}'`, 50, 'TCERELE');
279
284
  if (!routes || routes.length === 0) {
280
285
  if (routes)
281
286
  debug(`layer ${layer} has no consolidation route, no ranking`);
282
- return undefined;
287
+ return {};
283
288
  }
284
289
  const active = await rows(adt, `SELECT VERSION FROM TCEVERS WHERE ACTIVE = 'A'`, 5, 'TCEVERS');
285
290
  // Review M6 — the read failed: a route of an inactive version could pass as
286
291
  // the target, so give no verdict (an empty answer still means "unfiltered").
287
292
  if (active === null)
288
- return undefined;
293
+ return {};
289
294
  const versions = new Set((active ?? []).map((r) => (r['VERSION'] ?? '').trim()).filter(Boolean));
290
295
  if (versions.size > 0)
291
296
  routes = routes.filter((r) => versions.has((r['VERSION'] ?? '').trim()));
@@ -297,7 +302,7 @@ async function packageTarget(adt, pkg, sid) {
297
302
  const targets = [...new Set(routes.map((r) => (r['CONSYS'] ?? '').trim()).filter((t) => t.length > 0))];
298
303
  if (targets.length !== 1)
299
304
  debug(`layer ${layer} routes to ${targets.length} targets, no ranking`);
300
- return targets.length === 1 ? targets[0] : undefined;
305
+ return targets.length === 1 ? { target: targets[0] } : {};
301
306
  }
302
307
  /**
303
308
  * E070-TRFUNCTION request codes (re-review 4). The list endpoint's
@@ -399,7 +404,10 @@ export async function enrichTransportList(rawAdt, transports, pkg) {
399
404
  }
400
405
  if (!pkg)
401
406
  return { transports: out };
402
- const target = await packageTarget(adt, pkg, systemId(numbers));
407
+ const resolved = await packageTarget(adt, pkg, systemId(numbers));
408
+ if (resolved.local)
409
+ return { transports: out, packageLocal: true, packageNote: localPackageMessage(pkg) };
410
+ const target = resolved.target;
403
411
  if (!target)
404
412
  return { transports: out };
405
413
  // Review I4 — only a workbench request (TRFUNCTION 'K') can take a
@@ -19,6 +19,7 @@ import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
19
19
  import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
20
20
  import { setCurrentTransport, noteConfirmedTransport } from '../repl/current-transport.js';
21
21
  import { enrichTransportList } from './transport-fit.js';
22
+ import { reusedRequestRefusal } from './transport-fit-check.js';
22
23
  // ── Session ledger: transports CREATED by sap_transport_create this session ──
23
24
  //
24
25
  // Orphan-hint support (third junk transport this month, 2026-06-12 — D15/D20
@@ -100,7 +101,9 @@ registerTool({
100
101
  type: 'string',
101
102
  description: 'Target package (DEVCLASS) for the transport — typically the same package the '
102
103
  + 'objects will live in. Required by SAP\'s CreateCorrectionRequest endpoint. '
103
- + 'Empty / missing target_package on this endpoint commonly causes HTTP 400.',
104
+ + 'Empty / missing target_package on this endpoint commonly causes HTTP 400. '
105
+ + 'The package must already exist: for a NEW package use sap_create_object type DEVC, '
106
+ + 'which creates the package and its first transport together.',
104
107
  },
105
108
  approval_id: {
106
109
  type: 'string',
@@ -193,6 +196,38 @@ registerTool({
193
196
  + `(${err instanceof Error ? err.message : String(err)})`);
194
197
  }
195
198
  }
199
+ // ── Gate 0b: the target package must exist (§10a) ───────────────────────
200
+ // A request for a package that is not in the system has no transport
201
+ // target — SAP answers HTTP 500. A new package gets its first transport
202
+ // from sap_create_object DEVC (check → create from the package's own
203
+ // entry). Read-only lookup, before the approval gate like Gate 0.
204
+ // Best-effort: a failed lookup never blocks the create.
205
+ if (args.target_package) {
206
+ const pkg = String(args.target_package).trim().toUpperCase();
207
+ let missing = false;
208
+ try {
209
+ const isPkg = (hits) => hits.some((h) => (h.name ?? '').toUpperCase() === pkg && (!h.type || h.type.toUpperCase().startsWith('DEVC')));
210
+ let hits = await ctx.adt.searchObject(pkg, 'DEVC/K', 50);
211
+ // A type filter the system reads differently must never invent a
212
+ // "missing" package: an empty filtered answer is asked again unfiltered.
213
+ if (Array.isArray(hits) && hits.length === 0)
214
+ hits = await ctx.adt.searchObject(pkg, undefined, 50);
215
+ missing = Array.isArray(hits) && !isPkg(hits);
216
+ }
217
+ catch {
218
+ missing = false;
219
+ }
220
+ if (missing) {
221
+ return {
222
+ content: JSON.stringify({
223
+ error: 'package_missing',
224
+ message: `Package ${pkg} does not exist yet. Create it with sap_create_object DEVC — `
225
+ + 'CSPeach creates the package and its transport together.',
226
+ }),
227
+ is_error: true,
228
+ };
229
+ }
230
+ }
196
231
  // ── Gate 1: Approval ────────────────────────────────────────────────────
197
232
  const verify = await verifyAndSpendApprovalId(args.approval_id, args.description, 'create');
198
233
  if (!verify.ok) {
@@ -215,7 +250,24 @@ registerTool({
215
250
  // transportCreate(description, targetPackage?)
216
251
  try {
217
252
  const result = await ctx.adt.transportCreate(args.description, args.target_package);
253
+ if (result.status === 'error') {
254
+ // §10a: SAP's own sentence in plain words, audited as the error it is.
255
+ await auditLog('transportCreate', 'CTS', result.transportNumber ?? 'UNKNOWN', 'error', Date.now() - startedAt);
256
+ return {
257
+ content: JSON.stringify({
258
+ error: 'transport_create_failed',
259
+ message: `SAP refused to create the transport: ${result.error ?? 'no message'}`,
260
+ next: 'Next: check the package and its transport layer, or ask your Basis team.',
261
+ }),
262
+ is_error: true,
263
+ };
264
+ }
218
265
  await auditLog('transportCreate', 'CTS', result.transportNumber ?? 'UNKNOWN', 'success', Date.now() - startedAt);
266
+ // 1.1.31 — a request SAP handed back is refused in plain words; the
267
+ // advice is to create one for the package or name one that fits.
268
+ if (result.status === 'reused') {
269
+ return { content: JSON.stringify(reusedRequestRefusal(result)), is_error: true };
270
+ }
219
271
  if (result.status !== 'created' || !result.transportNumber) {
220
272
  return {
221
273
  content: JSON.stringify({ error: 'transport_create_failed', detail: result.error }),
@@ -371,7 +423,8 @@ registerTool({
371
423
  + 'recommend a request with contents "other_work" or "not_checked" for reuse; list a '
372
424
  + '"not_checked" one as "not checked", never as a clean fit. A description that seems to name '
373
425
  + 'other work may only lower a request in your answer; it never overrides `suggested: true`. '
374
- + 'When no request is suggested, offer to create a new transport.',
426
+ + 'When no request is suggested, offer to create a new transport. '
427
+ + '`package_local: true` means the package has no transport layer: show `package_note` to the person and offer no request.',
375
428
  isMutating: false,
376
429
  requiresSap: true,
377
430
  input_schema: {
@@ -405,6 +458,8 @@ registerTool({
405
458
  filter_status: result.filterStatus,
406
459
  count: enriched.transports.length,
407
460
  ...(enriched.packageTarget ? { package: String(args.package).toUpperCase(), package_target: enriched.packageTarget } : {}),
461
+ // 1.1.31 — a package with no transport layer is local: say so plainly.
462
+ ...(enriched.packageLocal ? { package: String(args.package).toUpperCase(), package_local: true, package_note: enriched.packageNote } : {}),
408
463
  transports: enriched.transports,
409
464
  // Round 5 item 3 — sap-client says when the list may be incomplete.
410
465
  ...(result.note ? { note: result.note } : {}),
package/dist/ui/app.js CHANGED
@@ -581,7 +581,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
581
581
  cardRows = estimateAskCardRows(askQuestionPending, true, uiWidth(columns));
582
582
  // C1 — the confirm card's real height (facts + choices) for the spec §2 cap.
583
583
  if (occupant === 'confirm' && confirmSlot.pending)
584
- cardRows = estimateConfirmCardRows(confirmSlot.pending);
584
+ cardRows = estimateConfirmCardRows(confirmSlot.pending, uiWidth(columns), rows - 6);
585
585
  const paletteRows = lineResolution ? PALETTE_ROWS : 0;
586
586
  // P2 — the checklist's WANTED height (header + rows); liveBudget folds it.
587
587
  const showChecklist = todos.length > 0 && !planClosed;
@@ -610,7 +610,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
610
610
  }
611
611
  else if (occupant === 'confirm' && confirmSlot.pending) {
612
612
  const req = confirmSlot.pending;
613
- slot = (_jsx(ConfirmCard, { request: req, width: uiWidth(columns), onResolve: (r) => confirmSlot.finish(req, r), onInterruptTurn: () => { onInterrupt?.(); } }, confirmSlot.seq));
613
+ slot = (_jsx(ConfirmCard, { request: req, width: uiWidth(columns), maxRows: rows - 6, onResolve: (r) => confirmSlot.finish(req, r), onInterruptTurn: () => { onInterrupt?.(); } }, confirmSlot.seq));
614
614
  }
615
615
  else if (occupant === 'ask' && askQuestionPending) {
616
616
  const req = askQuestionPending;