@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
@@ -34,8 +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 } from '../register/offer.js';
37
+ import { teamFixOffers, cspeachWritesThisSession } from '../register/offer.js';
38
+ import { isJourneyActive } from '../journey/flow.js';
38
39
  import { appendPending, finalizeToolCall } from '../session/pending.js';
40
+ import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
39
41
  import { auditLog } from '@cspeach/sap-client';
40
42
  import { ERR } from '../errors/codes.js';
41
43
  import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
@@ -43,7 +45,8 @@ import { resolveWriteTransport } from './transport-resolution.js';
43
45
  // F-L2-1: statusline-only wire — records the transport a write ACTUALLY
44
46
  // stamped so the `tr` segment stops reading `none` for plan-supplied
45
47
  // transports. Never touches the session transport pointer.
46
- import { noteWriteTransport } from '../repl/current-transport.js';
48
+ import { noteWriteTransport, setCurrentTransport } from '../repl/current-transport.js';
49
+ import { markTransportCreatedThisSession } from './transport.js';
47
50
  import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
48
51
  import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
49
52
  // ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
@@ -119,6 +122,21 @@ function chainActivation(w) {
119
122
  return { state: 'active' };
120
123
  return w.stillInactive ? { state: 'still_inactive' } : { state: 'unverified' };
121
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
+ }
122
140
  function activatedByWrite(w) {
123
141
  if (!w)
124
142
  return {};
@@ -317,6 +335,7 @@ registerTool({
317
335
  newSource: oldSource !== undefined ? args.source : undefined,
318
336
  },
319
337
  });
338
+ recordOwnFixAfterWrite(ctx, args.type, args.name, resolved.owning ?? resolved.transport, writeActivation);
320
339
  return {
321
340
  content: JSON.stringify({
322
341
  written: true,
@@ -526,6 +545,7 @@ registerTool({
526
545
  activation: chainActivation(writeActivation),
527
546
  card: { object: args.class_name, what: `method ${args.method_name}`, transport: resolved.transport },
528
547
  });
548
+ recordOwnFixAfterWrite(ctx, 'CLAS', args.class_name, resolved.owning ?? resolved.transport, writeActivation);
529
549
  return {
530
550
  content: JSON.stringify({
531
551
  updated: true,
@@ -673,7 +693,14 @@ registerTool({
673
693
  name: 'sap_create_object',
674
694
  description: 'Create a new ABAP object shell. '
675
695
  + 'Requires approval_id. No snapshot needed (new object). '
676
- + '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.',
677
704
  isMutating: true,
678
705
  requiresSap: true,
679
706
  input_schema: {
@@ -690,6 +717,12 @@ registerTool({
690
717
  serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
691
718
  bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
692
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).' },
693
726
  },
694
727
  required: ['name', 'type', 'package', 'description', 'approval_id'],
695
728
  },
@@ -722,7 +755,17 @@ registerTool({
722
755
  }
723
756
  }
724
757
  // 3-tier fallback: explicit arg → session current-transport → undefined
725
- 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
+ }
726
769
  // ── WAL: record pending entry ────────────────────────────────────────────
727
770
  await appendPending(ctx.session, {
728
771
  tool_use_id: toolUseId,
@@ -730,6 +773,48 @@ registerTool({
730
773
  args: { name: args.name, type: args.type, package: args.package },
731
774
  started_at: new Date().toISOString(),
732
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
+ }
733
818
  // ── Create ───────────────────────────────────────────────────────────────
734
819
  // createObject(objtype, name, packageName, description, transport?, extras?)
735
820
  let result;
@@ -738,6 +823,7 @@ registerTool({
738
823
  serviceDefinition: args.serviceDefinition,
739
824
  bindingType: args.bindingType,
740
825
  bindingVersion: args.bindingVersion,
826
+ ...devcExtras,
741
827
  });
742
828
  await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
743
829
  }
@@ -745,14 +831,14 @@ registerTool({
745
831
  const errStr = String(err);
746
832
  await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
747
833
  return {
748
- content: JSON.stringify({ error: 'create_failed', detail: errStr }),
834
+ content: JSON.stringify({ error: 'create_failed', detail: errStr, ...orphanRequest(devcTransportCreated) }),
749
835
  is_error: true,
750
836
  };
751
837
  }
752
838
  if (result.status !== 'created') {
753
839
  await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
754
840
  return {
755
- content: JSON.stringify({ error: 'create_failed', detail: result.error }),
841
+ content: JSON.stringify({ error: 'create_failed', detail: result.error, ...orphanRequest(devcTransportCreated) }),
756
842
  is_error: true,
757
843
  };
758
844
  }
@@ -760,6 +846,22 @@ registerTool({
760
846
  // the 3-tier effectiveTransport above is what SAP stamped.
761
847
  noteWriteTransport(effectiveTransport);
762
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
+ }
763
865
  return {
764
866
  content: JSON.stringify({
765
867
  created: true,
@@ -771,6 +873,90 @@ registerTool({
771
873
  };
772
874
  },
773
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
+ }
774
960
  // ── sap_set_class_include ─────────────────────────────────────────────────────
775
961
  //
776
962
  // Signature: setClassInclude(className, includeType, source, transport?)
@@ -1345,11 +1531,15 @@ registerTool({
1345
1531
  // successful write op. Without this, Rule 8 counts a failed activate
1346
1532
  // as if it had run, inflating the "batch in progress" count and
1347
1533
  // 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))
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) }))
1352
1541
  ctx.chunkEmitter?.emit('info', line);
1542
+ }
1353
1543
  return {
1354
1544
  content: JSON.stringify({
1355
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,7 +56,7 @@ 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'],
59
+ 'abap-cca': ['team_estate', 'sap_atc_run', 'sap_usage_references', 'sap_api_state'],
60
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'],
@@ -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;