@cspeach/cli 1.1.30 → 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.
- package/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +121 -8
- package/dist/cli.js +36 -14
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team.js +375 -41
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +20 -1
- package/dist/projects/clean-core-confirm.js +156 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +83 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +25 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +24 -6
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +3 -0
- package/dist/register/status-html.js +3 -1
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +27 -7
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/journey-entry.js +25 -0
- package/dist/repl.js +157 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +77 -4
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -32,6 +32,7 @@ import { registerTool } from '../index.js';
|
|
|
32
32
|
import { mergeUpgradeProgress, renderUpgradeMergeManifest, } from '../../projects/merge-upgrade.js';
|
|
33
33
|
import { loadEnvelope, MANIFEST_PASTE_INSTRUCTIONS } from './_merge-shared.js';
|
|
34
34
|
import { assertWritable } from '../_filesystem-shared.js';
|
|
35
|
+
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamWorkTarget } from '../_team-work-path.js';
|
|
35
36
|
import { resolveDetailFile } from '../../projects/detail-path.js';
|
|
36
37
|
function sha256OfFile(path) {
|
|
37
38
|
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
@@ -149,11 +150,16 @@ export async function upgradeMergeProgressHandler(args, ctx) {
|
|
|
149
150
|
// mergedFrom: the input envelopes [{ file, label }]
|
|
150
151
|
// }
|
|
151
152
|
let detailWritten = true;
|
|
152
|
-
|
|
153
|
+
// Task 15 — in a team folder the detail file is a working file: the seat's.
|
|
154
|
+
const t = teamWorkTarget(ctx.cwd, merged.detailPath);
|
|
155
|
+
const mergedDetailAbs = isAbsolute(t.path) ? t.path : join(t.root, t.path);
|
|
153
156
|
try {
|
|
154
157
|
// SEC (review round 2): the real landing path must stay in the project and
|
|
155
158
|
// not be protected (a junctioned .cspeach/<domain> -> .git is refused).
|
|
156
|
-
await assertWritable(mergedDetailAbs,
|
|
159
|
+
const realDetail = await assertWritable(mergedDetailAbs, t.root);
|
|
160
|
+
// Final fix wave, item 3 — never into the shared .cspeach of a team folder.
|
|
161
|
+
if (pointsIntoSharedCspeach(ctx.cwd, realDetail))
|
|
162
|
+
throw new Error(SHARED_CSPEACH_REFUSAL);
|
|
157
163
|
mkdirSync(dirname(mergedDetailAbs), { recursive: true });
|
|
158
164
|
writeFileSync(mergedDetailAbs, JSON.stringify({
|
|
159
165
|
generatedAt: new Date().toISOString(),
|
package/dist/tools/sap-read.js
CHANGED
|
@@ -400,9 +400,12 @@ registerTool({
|
|
|
400
400
|
// -----------------------------------------------------------------------
|
|
401
401
|
// 8. sap_usage_references
|
|
402
402
|
// -----------------------------------------------------------------------
|
|
403
|
+
/** T001 alone has ~20,000 callers on an S/4 box; list the first 200, count the rest. */
|
|
404
|
+
const USAGE_REFERENCES_MAX_LINES = 200;
|
|
403
405
|
registerTool({
|
|
404
406
|
name: 'sap_usage_references',
|
|
405
|
-
description: 'Find all places where a given ABAP object is referenced (where-used list).'
|
|
407
|
+
description: 'Find all places where a given ABAP object is referenced (where-used list). '
|
|
408
|
+
+ 'Widely used SAP objects (T001, BUKRS …) can take minutes; prefer custom objects.',
|
|
406
409
|
isMutating: false,
|
|
407
410
|
requiresSap: true,
|
|
408
411
|
input_schema: {
|
|
@@ -414,9 +417,23 @@ registerTool({
|
|
|
414
417
|
required: ['name', 'type'],
|
|
415
418
|
},
|
|
416
419
|
handler: async (args, ctx) => {
|
|
417
|
-
// usageReferences(objectType, objectName) — type first
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
// usageReferences(objectType, objectName) — type first. `unreadable` is
|
|
421
|
+
// never "no callers" (spec 2026-10-06 §5.3) — and never a bare `[]`.
|
|
422
|
+
const result = await ctx.adt.usageReferences(args.type, args.name);
|
|
423
|
+
if (result.status !== 'ok') {
|
|
424
|
+
return { content: `Where-used could not be read: ${result.note || 'no reason given'}` };
|
|
425
|
+
}
|
|
426
|
+
if (result.refs.length === 0)
|
|
427
|
+
return { content: 'No callers found (where-used read OK)' };
|
|
428
|
+
const shown = result.refs.slice(0, USAGE_REFERENCES_MAX_LINES).map((r) => `${r.type} ${r.name}`);
|
|
429
|
+
const hidden = result.refs.length - shown.length;
|
|
430
|
+
return {
|
|
431
|
+
content: [
|
|
432
|
+
`Callers found: ${result.refs.length} (where-used read OK)`,
|
|
433
|
+
...shown,
|
|
434
|
+
...(hidden > 0 ? [`… and ${hidden} more not shown`] : []),
|
|
435
|
+
].join('\n'),
|
|
436
|
+
};
|
|
420
437
|
},
|
|
421
438
|
});
|
|
422
439
|
// -----------------------------------------------------------------------
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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] :
|
|
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
|
|
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
|