@cspeach/cli 0.6.0

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 (210) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +108 -0
  3. package/dist/agent/anthropic-provider.js +59 -0
  4. package/dist/agent/llm-provider.js +1 -0
  5. package/dist/agent/loop.js +709 -0
  6. package/dist/agent/maybe-build-project-context.js +126 -0
  7. package/dist/agent/providers/ai-hub-provider.js +58 -0
  8. package/dist/agent/providers/byok-provider.js +53 -0
  9. package/dist/agent/providers/factory.js +13 -0
  10. package/dist/agent/providers/local-provider.js +125 -0
  11. package/dist/agent/repair-partial.js +31 -0
  12. package/dist/agent/retry-key.js +58 -0
  13. package/dist/agent/retry.js +17 -0
  14. package/dist/agent/sap-connection-adapter.js +82 -0
  15. package/dist/agent/skill-checkpoint.js +119 -0
  16. package/dist/agent/tool-dispatch.js +47 -0
  17. package/dist/agent/turn-assistant-text.js +49 -0
  18. package/dist/agent/turn-error-ux.js +126 -0
  19. package/dist/agent/turn-stream.js +79 -0
  20. package/dist/agent/turn-watchdog.js +71 -0
  21. package/dist/approvals/advisory-prompt.js +40 -0
  22. package/dist/approvals/advisory-render.js +38 -0
  23. package/dist/approvals/approval-prompt.js +100 -0
  24. package/dist/approvals/jwt.js +33 -0
  25. package/dist/approvals/render.js +211 -0
  26. package/dist/approvals/risk-floor.js +26 -0
  27. package/dist/auth/api-key.js +40 -0
  28. package/dist/auth/auth-file.js +59 -0
  29. package/dist/auth/device.js +8 -0
  30. package/dist/auth/me.js +19 -0
  31. package/dist/classifier/client.js +58 -0
  32. package/dist/cli-args.js +38 -0
  33. package/dist/cli.js +148 -0
  34. package/dist/commands/config-set.js +245 -0
  35. package/dist/commands/config-show.js +159 -0
  36. package/dist/commands/help.js +93 -0
  37. package/dist/commands/login.js +122 -0
  38. package/dist/commands/logout.js +17 -0
  39. package/dist/commands/project-context-impact.js +215 -0
  40. package/dist/commands/reroute.js +60 -0
  41. package/dist/commands/spec-gap-status.js +52 -0
  42. package/dist/commands/whoami.js +35 -0
  43. package/dist/config/loader.js +67 -0
  44. package/dist/config/paths.js +20 -0
  45. package/dist/doctor/checks/_http-probe.js +56 -0
  46. package/dist/doctor/checks/auth.js +15 -0
  47. package/dist/doctor/checks/cert.js +24 -0
  48. package/dist/doctor/checks/forge-rules.js +102 -0
  49. package/dist/doctor/checks/keychain-fallback.js +14 -0
  50. package/dist/doctor/checks/keychain.js +23 -0
  51. package/dist/doctor/checks/llm-mode.js +27 -0
  52. package/dist/doctor/checks/proxy.js +13 -0
  53. package/dist/doctor/checks/sap.js +33 -0
  54. package/dist/doctor/checks/skill.js +24 -0
  55. package/dist/doctor/checks/write-mode.js +34 -0
  56. package/dist/doctor/checks/zcspeach.js +76 -0
  57. package/dist/doctor/run.js +46 -0
  58. package/dist/errors/codes.js +12 -0
  59. package/dist/index.js +6 -0
  60. package/dist/lock-contention.js +22 -0
  61. package/dist/one-shot.js +104 -0
  62. package/dist/project-context/conventions.js +309 -0
  63. package/dist/project-context/detect.js +250 -0
  64. package/dist/project-context/domain/abap-cloud.js +26 -0
  65. package/dist/project-context/domain/abapgit.js +177 -0
  66. package/dist/project-context/domain/cap.js +164 -0
  67. package/dist/project-context/domain/fiori.js +326 -0
  68. package/dist/project-context/git.js +115 -0
  69. package/dist/project-context/index-files.js +235 -0
  70. package/dist/project-context/index.js +117 -0
  71. package/dist/project-context/render.js +308 -0
  72. package/dist/project-context/types.js +14 -0
  73. package/dist/projects/build.js +20 -0
  74. package/dist/projects/canonicalize.js +39 -0
  75. package/dist/projects/email-template.js +54 -0
  76. package/dist/projects/extract-cca.js +139 -0
  77. package/dist/projects/extract-design.js +107 -0
  78. package/dist/projects/extract-estimate.js +93 -0
  79. package/dist/projects/extract-modernize.js +130 -0
  80. package/dist/projects/extract-spec-gap.js +101 -0
  81. package/dist/projects/extract-test-coverage.js +137 -0
  82. package/dist/projects/extract-upgrade.js +230 -0
  83. package/dist/projects/filename.js +18 -0
  84. package/dist/projects/index.js +8 -0
  85. package/dist/projects/migration.js +111 -0
  86. package/dist/projects/promote-command.js +96 -0
  87. package/dist/projects/promote.js +107 -0
  88. package/dist/projects/save-command.js +124 -0
  89. package/dist/projects/save.js +21 -0
  90. package/dist/projects/status.js +170 -0
  91. package/dist/projects/types.js +1 -0
  92. package/dist/projects/validate.js +146 -0
  93. package/dist/projects/workspace.js +478 -0
  94. package/dist/renderer/abap-inline.js +121 -0
  95. package/dist/renderer/banners.js +39 -0
  96. package/dist/renderer/highlighters/abap.js +126 -0
  97. package/dist/renderer/highlighters/bdef.js +81 -0
  98. package/dist/renderer/highlighters/cds.js +91 -0
  99. package/dist/renderer/markdown.js +291 -0
  100. package/dist/renderer/pipeline.js +201 -0
  101. package/dist/renderer/progress-chatter.js +237 -0
  102. package/dist/renderer/question-normalizer.js +306 -0
  103. package/dist/renderer/severity.js +61 -0
  104. package/dist/renderer/status-footer.js +50 -0
  105. package/dist/renderer/syntax.js +58 -0
  106. package/dist/renderer/tables.js +55 -0
  107. package/dist/renderer/thinking-heartbeat.js +70 -0
  108. package/dist/renderer/tool-widget.js +199 -0
  109. package/dist/renderer/tty.js +66 -0
  110. package/dist/renderer/widget-extractor.js +87 -0
  111. package/dist/renderer/widget-fallback.js +78 -0
  112. package/dist/renderer/widget-schemas.js +43 -0
  113. package/dist/repl/at-completer.js +64 -0
  114. package/dist/repl/at-picker.js +122 -0
  115. package/dist/repl/bracketed-paste.js +284 -0
  116. package/dist/repl/current-transport.js +46 -0
  117. package/dist/repl/diff-display.js +41 -0
  118. package/dist/repl/file-picker.js +219 -0
  119. package/dist/repl/inquirer-guard.js +130 -0
  120. package/dist/repl/inquirer-theme.js +41 -0
  121. package/dist/repl/rule8-detector.js +99 -0
  122. package/dist/repl/safety-confirm.js +106 -0
  123. package/dist/repl/safety-mode-state.js +36 -0
  124. package/dist/repl/slash-completer.js +59 -0
  125. package/dist/repl/slash-picker.js +124 -0
  126. package/dist/repl/update-method-preview-hook.js +45 -0
  127. package/dist/repl.js +1383 -0
  128. package/dist/router/classifier.js +38 -0
  129. package/dist/router/intent-extractor.js +140 -0
  130. package/dist/router/routing-decision.js +19 -0
  131. package/dist/sap/connection-manager.js +52 -0
  132. package/dist/sap/onboarding.js +178 -0
  133. package/dist/sap/system-info.js +515 -0
  134. package/dist/session/awaiting-answer.js +73 -0
  135. package/dist/session/gc.js +28 -0
  136. package/dist/session/pending.js +37 -0
  137. package/dist/session/resume.js +77 -0
  138. package/dist/session/schema.js +20 -0
  139. package/dist/session/store.js +147 -0
  140. package/dist/session/time-ago.js +41 -0
  141. package/dist/skill-catalog.js +222 -0
  142. package/dist/skills/bundled-skills.js +1 -0
  143. package/dist/skills/canonical.js +12 -0
  144. package/dist/skills/manifest-client.js +93 -0
  145. package/dist/skills/promotion-dispatch.js +24 -0
  146. package/dist/skills/signing-public-key.js +4 -0
  147. package/dist/skills/source-bundled.js +20 -0
  148. package/dist/skills/source-managed.js +26 -0
  149. package/dist/skills/source-manifest.js +26 -0
  150. package/dist/tools/_command-shared.js +110 -0
  151. package/dist/tools/_filesystem-shared.js +81 -0
  152. package/dist/tools/_flag.js +39 -0
  153. package/dist/tools/approval.js +228 -0
  154. package/dist/tools/ask-question.js +205 -0
  155. package/dist/tools/dispatch-skill.js +81 -0
  156. package/dist/tools/filesystem/file-edit.js +140 -0
  157. package/dist/tools/filesystem/file-read.js +89 -0
  158. package/dist/tools/filesystem/file-write.js +128 -0
  159. package/dist/tools/filesystem/glob.js +177 -0
  160. package/dist/tools/filesystem/grep.js +163 -0
  161. package/dist/tools/index.js +32 -0
  162. package/dist/tools/project/convention_get.js +91 -0
  163. package/dist/tools/project/playbook_get.js +132 -0
  164. package/dist/tools/project/project_context_get.js +101 -0
  165. package/dist/tools/sap-read.js +454 -0
  166. package/dist/tools/sap-write.js +746 -0
  167. package/dist/tools/shell/shell_exec.js +209 -0
  168. package/dist/tools/snapshot.js +107 -0
  169. package/dist/tools/subagent/_background-shared.js +133 -0
  170. package/dist/tools/subagent/agent_run.js +186 -0
  171. package/dist/tools/subagent/background_run.js +143 -0
  172. package/dist/tools/subagent/monitor_emit.js +65 -0
  173. package/dist/tools/subagent/schedule_create.js +131 -0
  174. package/dist/tools/transport.js +233 -0
  175. package/dist/tools/update-method-intercept.js +119 -0
  176. package/dist/tools/verify.js +39 -0
  177. package/dist/tools/web/_web-shared.js +251 -0
  178. package/dist/tools/web/web_fetch.js +257 -0
  179. package/dist/tools/web/web_search.js +195 -0
  180. package/dist/tools/write-mode.js +22 -0
  181. package/dist/ui/app.js +95 -0
  182. package/dist/ui/approval-emitter.js +10 -0
  183. package/dist/ui/approval-modal.js +53 -0
  184. package/dist/ui/ascii-chars.js +6 -0
  185. package/dist/ui/body.js +102 -0
  186. package/dist/ui/coaching-picker-classic.js +36 -0
  187. package/dist/ui/coaching-picker-emitter.js +27 -0
  188. package/dist/ui/command-palette.js +34 -0
  189. package/dist/ui/error-emitter.js +21 -0
  190. package/dist/ui/footer.js +103 -0
  191. package/dist/ui/header.js +17 -0
  192. package/dist/ui/ink-classifier-route.js +19 -0
  193. package/dist/ui/login-banner.js +72 -0
  194. package/dist/ui/rich-error-box.js +9 -0
  195. package/dist/ui/sap-state-store.js +65 -0
  196. package/dist/ui/session-timeline.js +31 -0
  197. package/dist/ui/sidebar.js +10 -0
  198. package/dist/ui/skill-picker.js +50 -0
  199. package/dist/ui/status-row.js +12 -0
  200. package/dist/ui/widget-control.js +4 -0
  201. package/dist/ui/widgets/bar-chart.js +15 -0
  202. package/dist/ui/widgets/coaching-picker.js +41 -0
  203. package/dist/ui/widgets/component-registry.js +12 -0
  204. package/dist/ui/widgets/dep-graph.js +9 -0
  205. package/dist/ui/widgets/diff-viewer.js +11 -0
  206. package/dist/ui/widgets/question-card.js +11 -0
  207. package/dist/ui/widgets/stack-frames.js +5 -0
  208. package/dist/upgrade-check.js +28 -0
  209. package/dist/upgrade.js +13 -0
  210. package/package.json +83 -0
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Token-budgeted prompt rendering for ProjectContext.
3
+ *
4
+ * Phase 2's hook (`maybeBuildProjectContext` in agent/loop.ts) will
5
+ * inject this string verbatim into the system prompt when the
6
+ * CSPEACH_PROJECT_CONTEXT flag is on. We must therefore stay within
7
+ * a tight budget — too much context displaces the actual skill body.
8
+ *
9
+ * Targets:
10
+ * ~3,000 tokens TOTAL on a medium-sized project
11
+ * header / conventions: ~200 tokens
12
+ * sap connection: ~200 tokens
13
+ * git: ~150 tokens
14
+ * file index: ~600 tokens (capped sample of paths)
15
+ * domain objects: ~1,800 tokens (per-category caps)
16
+ *
17
+ * We don't have a tokenizer at hand and don't want to ship one for this
18
+ * render alone. We approximate via character budgets calibrated against
19
+ * a 4-chars-per-token rough rule. Anything that overshoots gets
20
+ * truncation markers like `... (47 more — search via glob)` so the
21
+ * model knows there's more it can pull on demand.
22
+ */
23
+ const CAP_CONVENTION_CHARS = 1500;
24
+ const CAP_PATH_LIST = 60;
25
+ const CAP_FIORI_CONTROLLERS = 25;
26
+ const CAP_FIORI_VIEWS = 25;
27
+ const CAP_FIORI_ROUTES = 40;
28
+ const CAP_FIORI_I18N_KEYS = 40;
29
+ const CAP_CAP_SERVICES = 30;
30
+ const CAP_CAP_ENTITIES = 60;
31
+ const CAP_ABAP_OBJECTS = 80;
32
+ const CAP_GIT_COMMITS = 5;
33
+ const CAP_RECENT_OBJECTS = 10;
34
+ /**
35
+ * Render a ProjectContext as prompt text. The result is a self-contained
36
+ * <project_context> XML-ish block ready to splice into a system prompt.
37
+ * Empty / null contexts render to an empty string — callers can compose
38
+ * unconditionally.
39
+ */
40
+ export function renderProjectContext(ctx) {
41
+ if (!ctx)
42
+ return '';
43
+ const parts = [];
44
+ parts.push(`<project_context type="${ctx.type}">`);
45
+ parts.push(` <root>${escapeXml(ctx.root)}</root>`);
46
+ parts.push(` <detected_at>${ctx.detectedAt}</detected_at>`);
47
+ parts.push(renderConventions(ctx.conventions));
48
+ if (ctx.sapConnection)
49
+ parts.push(renderSapConnection(ctx.sapConnection));
50
+ parts.push(renderGit(ctx.git));
51
+ parts.push(renderFileIndex(ctx));
52
+ parts.push(renderDomainObjects(ctx.domainObjects));
53
+ parts.push(`</project_context>`);
54
+ return parts.filter((p) => p.length > 0).join('\n');
55
+ }
56
+ /* ── Section renderers ─────────────────────────────────────────────── */
57
+ function renderConventions(c) {
58
+ const lines = [' <conventions>'];
59
+ // Human-readable form: "2 spaces", "1 tab", etc. The earlier "2sp" /
60
+ // "4tab" packed-token form was ambiguous to the model — observed
61
+ // during 2026-05-10 dogfooding: "2sp" was parsed as "1-space indent"
62
+ // because there's no separator between count and unit.
63
+ const indentUnit = c.style.indentChar === 'tab' ? 'tab' : 'space';
64
+ const indentDesc = `${c.style.indentSize} ${indentUnit}${c.style.indentSize === 1 ? '' : 's'}`;
65
+ lines.push(` <style indent="${indentDesc}" eol="${c.style.eol.toUpperCase()}" />`);
66
+ if (c.namingPatterns.classes.length || c.namingPatterns.interfaces.length || c.namingPatterns.cdsViews.length) {
67
+ lines.push(' <naming>');
68
+ if (c.namingPatterns.classes.length) {
69
+ lines.push(` <classes>${escapeXml(c.namingPatterns.classes.join(', '))}</classes>`);
70
+ }
71
+ if (c.namingPatterns.interfaces.length) {
72
+ lines.push(` <interfaces>${escapeXml(c.namingPatterns.interfaces.join(', '))}</interfaces>`);
73
+ }
74
+ if (c.namingPatterns.cdsViews.length) {
75
+ lines.push(` <cds_views>${escapeXml(c.namingPatterns.cdsViews.join(', '))}</cds_views>`);
76
+ }
77
+ lines.push(' </naming>');
78
+ }
79
+ if (c.freeText) {
80
+ const truncated = truncateText(c.freeText, CAP_CONVENTION_CHARS);
81
+ lines.push(' <freeText>');
82
+ // CLAUDE.md / CSPEACH.md frequently contains literal XML snippets
83
+ // (this very repo's CLAUDE.md has <asx:values>, <atc:run>, etc).
84
+ // Escape unconditionally so the system-prompt block stays well-formed.
85
+ lines.push(indent(escapeXml(truncated.text), ' '));
86
+ if (truncated.truncated) {
87
+ lines.push(' ... (truncated — read CLAUDE.md / CSPEACH.md for full text)');
88
+ }
89
+ lines.push(' </freeText>');
90
+ }
91
+ lines.push(' </conventions>');
92
+ return lines.join('\n');
93
+ }
94
+ function renderSapConnection(s) {
95
+ const lines = [` <sap_connection alias="${escapeAttr(s.alias)}">`];
96
+ lines.push(` <system sid="${escapeAttr(s.system.sid ?? '?')}" release="${escapeAttr(s.system.release ?? '?')}" abap_version="${escapeAttr(s.system.abapVersion ?? '?')}" platform="${s.system.platform}" />`);
97
+ if (s.currentTransport) {
98
+ lines.push(` <current_transport>${escapeXml(s.currentTransport)}</current_transport>`);
99
+ }
100
+ if (s.activeScope) {
101
+ if (s.activeScope.package) {
102
+ lines.push(` <active_package>${escapeXml(s.activeScope.package)}</active_package>`);
103
+ }
104
+ if (s.activeScope.object) {
105
+ lines.push(` <active_object name="${escapeAttr(s.activeScope.object.name)}" type="${escapeAttr(s.activeScope.object.type)}" />`);
106
+ }
107
+ }
108
+ if (s.recentObjects.length) {
109
+ lines.push(' <recent_objects>');
110
+ for (const ref of s.recentObjects.slice(0, CAP_RECENT_OBJECTS)) {
111
+ lines.push(` <object name="${escapeAttr(ref.name)}" type="${escapeAttr(ref.type)}" action="${escapeAttr(ref.lastAction)}" />`);
112
+ }
113
+ if (s.recentObjects.length > CAP_RECENT_OBJECTS) {
114
+ lines.push(` <!-- ... ${s.recentObjects.length - CAP_RECENT_OBJECTS} more -->`);
115
+ }
116
+ lines.push(' </recent_objects>');
117
+ }
118
+ lines.push(' </sap_connection>');
119
+ return lines.join('\n');
120
+ }
121
+ function renderGit(g) {
122
+ if (!g.isRepo) {
123
+ return ' <git is_repo="false" />';
124
+ }
125
+ const lines = [' <git is_repo="true">'];
126
+ lines.push(` <branch>${escapeXml(g.branch ?? '')}</branch>`);
127
+ lines.push(` <head_sha>${escapeXml(g.headSha ?? '')}</head_sha>`);
128
+ lines.push(` <dirty_files>${g.dirtyFileCount}</dirty_files>`);
129
+ if (g.recentCommits.length) {
130
+ lines.push(' <recent_commits>');
131
+ for (const line of g.recentCommits.slice(0, CAP_GIT_COMMITS)) {
132
+ lines.push(` ${escapeXml(line)}`);
133
+ }
134
+ lines.push(' </recent_commits>');
135
+ }
136
+ lines.push(' </git>');
137
+ return lines.join('\n');
138
+ }
139
+ function renderFileIndex(ctx) {
140
+ const idx = ctx.index;
141
+ const lines = [
142
+ ` <file_index total="${idx.totalFiles}"${idx.truncated ? ' truncated="true"' : ''}>`,
143
+ ];
144
+ if (idx.topLevelDirs.length) {
145
+ lines.push(` <top_level_dirs>${escapeXml(idx.topLevelDirs.join(', '))}</top_level_dirs>`);
146
+ }
147
+ const extEntries = Object.entries(idx.byExtension).sort((a, b) => b[1] - a[1]);
148
+ if (extEntries.length) {
149
+ const summary = extEntries
150
+ .slice(0, 12)
151
+ .map(([ext, n]) => `${ext}=${n}`)
152
+ .join(', ');
153
+ lines.push(` <by_extension>${escapeXml(summary)}</by_extension>`);
154
+ }
155
+ if (idx.paths.length) {
156
+ const sample = idx.paths.slice(0, CAP_PATH_LIST);
157
+ lines.push(' <paths>');
158
+ for (const p of sample)
159
+ lines.push(` ${escapeXml(p)}`);
160
+ if (idx.paths.length > CAP_PATH_LIST) {
161
+ lines.push(` ... (${idx.paths.length - CAP_PATH_LIST} more — search via glob)`);
162
+ }
163
+ lines.push(' </paths>');
164
+ }
165
+ lines.push(' </file_index>');
166
+ return lines.join('\n');
167
+ }
168
+ function renderDomainObjects(d) {
169
+ switch (d.type) {
170
+ case 'fiori-ui5':
171
+ return renderFiori(d);
172
+ case 'cap-node':
173
+ return renderCap(d);
174
+ case 'abapgit':
175
+ return renderAbapList('abapgit', d.objects);
176
+ case 'abap-cloud':
177
+ return renderAbapCloud(d.objects, d.behaviorDefinitions);
178
+ case 'mixed':
179
+ return ` <domain_objects type="mixed" modules="${d.modules.length}" />`;
180
+ case 'unknown':
181
+ default:
182
+ return ' <domain_objects type="unknown" />';
183
+ }
184
+ }
185
+ function renderFiori(d) {
186
+ const lines = [' <domain_objects type="fiori-ui5">'];
187
+ if (d.routes.length) {
188
+ lines.push(' <routes>');
189
+ for (const r of d.routes.slice(0, CAP_FIORI_ROUTES)) {
190
+ lines.push(` <route name="${escapeAttr(r.name)}" pattern="${escapeAttr(r.pattern)}" targets="${escapeAttr(r.targets.join(','))}" />`);
191
+ }
192
+ if (d.routes.length > CAP_FIORI_ROUTES) {
193
+ lines.push(` <!-- ... ${d.routes.length - CAP_FIORI_ROUTES} more routes -->`);
194
+ }
195
+ lines.push(' </routes>');
196
+ }
197
+ if (d.controllers.length) {
198
+ lines.push(' <controllers>');
199
+ for (const c of d.controllers.slice(0, CAP_FIORI_CONTROLLERS)) {
200
+ const view = c.viewPath ? ` view="${escapeAttr(c.viewPath)}"` : '';
201
+ const i18n = c.i18nKeys.length ? ` i18n_keys="${c.i18nKeys.length}"` : '';
202
+ lines.push(` <controller name="${escapeAttr(c.name)}" path="${escapeAttr(c.path)}"${view}${i18n} />`);
203
+ }
204
+ if (d.controllers.length > CAP_FIORI_CONTROLLERS) {
205
+ lines.push(` <!-- ... ${d.controllers.length - CAP_FIORI_CONTROLLERS} more controllers -->`);
206
+ }
207
+ lines.push(' </controllers>');
208
+ }
209
+ if (d.views.length) {
210
+ lines.push(' <views>');
211
+ for (const v of d.views.slice(0, CAP_FIORI_VIEWS)) {
212
+ const ctl = v.controllerPath ? ` controller="${escapeAttr(v.controllerPath)}"` : '';
213
+ lines.push(` <view name="${escapeAttr(v.name)}" path="${escapeAttr(v.path)}"${ctl} />`);
214
+ }
215
+ if (d.views.length > CAP_FIORI_VIEWS) {
216
+ lines.push(` <!-- ... ${d.views.length - CAP_FIORI_VIEWS} more views -->`);
217
+ }
218
+ lines.push(' </views>');
219
+ }
220
+ if (d.i18nKeys.length) {
221
+ const sample = d.i18nKeys.slice(0, CAP_FIORI_I18N_KEYS).join(', ');
222
+ const more = d.i18nKeys.length > CAP_FIORI_I18N_KEYS
223
+ ? ` ... (+${d.i18nKeys.length - CAP_FIORI_I18N_KEYS} more)`
224
+ : '';
225
+ lines.push(` <i18n_keys>${escapeXml(sample)}${more}</i18n_keys>`);
226
+ }
227
+ lines.push(' </domain_objects>');
228
+ return lines.join('\n');
229
+ }
230
+ function renderCap(d) {
231
+ const lines = [' <domain_objects type="cap-node">'];
232
+ if (d.services.length) {
233
+ lines.push(' <services>');
234
+ for (const s of d.services.slice(0, CAP_CAP_SERVICES)) {
235
+ const entities = s.entities.length ? ` entities="${escapeAttr(s.entities.join(','))}"` : '';
236
+ lines.push(` <service name="${escapeAttr(s.name)}" path="${escapeAttr(s.path)}"${entities} />`);
237
+ }
238
+ if (d.services.length > CAP_CAP_SERVICES) {
239
+ lines.push(` <!-- ... ${d.services.length - CAP_CAP_SERVICES} more services -->`);
240
+ }
241
+ lines.push(' </services>');
242
+ }
243
+ if (d.entities.length) {
244
+ lines.push(' <entities>');
245
+ for (const e of d.entities.slice(0, CAP_CAP_ENTITIES)) {
246
+ const src = e.source ? ` source="${escapeAttr(e.source)}"` : '';
247
+ lines.push(` <entity name="${escapeAttr(e.name)}" path="${escapeAttr(e.path)}"${src} />`);
248
+ }
249
+ if (d.entities.length > CAP_CAP_ENTITIES) {
250
+ lines.push(` <!-- ... ${d.entities.length - CAP_CAP_ENTITIES} more entities -->`);
251
+ }
252
+ lines.push(' </entities>');
253
+ }
254
+ lines.push(' </domain_objects>');
255
+ return lines.join('\n');
256
+ }
257
+ function renderAbapList(type, objects) {
258
+ const lines = [` <domain_objects type="${type}" total="${objects.length}">`];
259
+ for (const o of objects.slice(0, CAP_ABAP_OBJECTS)) {
260
+ const pkg = o.package ? ` package="${escapeAttr(o.package)}"` : '';
261
+ lines.push(` <object name="${escapeAttr(o.name)}" type="${escapeAttr(o.type)}"${pkg} />`);
262
+ }
263
+ if (objects.length > CAP_ABAP_OBJECTS) {
264
+ lines.push(` <!-- ... ${objects.length - CAP_ABAP_OBJECTS} more objects — search via glob -->`);
265
+ }
266
+ lines.push(' </domain_objects>');
267
+ return lines.join('\n');
268
+ }
269
+ function renderAbapCloud(objects, bdefs) {
270
+ const lines = [
271
+ ` <domain_objects type="abap-cloud" total="${objects.length}" behavior_definitions="${bdefs.length}">`,
272
+ ];
273
+ if (bdefs.length) {
274
+ const sample = bdefs.slice(0, 20).join(', ');
275
+ const more = bdefs.length > 20 ? ` ... (+${bdefs.length - 20} more)` : '';
276
+ lines.push(` <bdefs>${escapeXml(sample)}${more}</bdefs>`);
277
+ }
278
+ for (const o of objects.slice(0, CAP_ABAP_OBJECTS)) {
279
+ const pkg = o.package ? ` package="${escapeAttr(o.package)}"` : '';
280
+ lines.push(` <object name="${escapeAttr(o.name)}" type="${escapeAttr(o.type)}"${pkg} />`);
281
+ }
282
+ if (objects.length > CAP_ABAP_OBJECTS) {
283
+ lines.push(` <!-- ... ${objects.length - CAP_ABAP_OBJECTS} more objects — search via glob -->`);
284
+ }
285
+ lines.push(' </domain_objects>');
286
+ return lines.join('\n');
287
+ }
288
+ /* ── Helpers ───────────────────────────────────────────────────────── */
289
+ function escapeXml(text) {
290
+ return text
291
+ .replace(/&/g, '&amp;')
292
+ .replace(/</g, '&lt;')
293
+ .replace(/>/g, '&gt;');
294
+ }
295
+ function escapeAttr(text) {
296
+ return escapeXml(text).replace(/"/g, '&quot;');
297
+ }
298
+ function truncateText(text, charBudget) {
299
+ if (text.length <= charBudget)
300
+ return { text, truncated: false };
301
+ return { text: text.slice(0, charBudget), truncated: true };
302
+ }
303
+ function indent(text, prefix) {
304
+ return text
305
+ .split('\n')
306
+ .map((line) => prefix + line)
307
+ .join('\n');
308
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Project context types — the structured model of whatever project the
3
+ * user is in. Phase 1 of Track B (the agentic CSPeach rebuild).
4
+ *
5
+ * The full ProjectContext is computed at session start (lazily, on
6
+ * demand, via getProjectContext from ./index.ts) and ideally cached for
7
+ * the session. Phase 2 wires it into the agent's system prompt behind
8
+ * a feature flag; Phase 4's intent-driven loop relies on it heavily.
9
+ *
10
+ * Today (Phase 1), nothing in the existing CLI imports this module.
11
+ * It exists in code, has its own tests, but doesn't affect runtime
12
+ * behavior. Risk to existing CSPeach: zero by construction.
13
+ */
14
+ export {};
@@ -0,0 +1,20 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export function buildEnvelope(args) {
3
+ const now = new Date().toISOString();
4
+ return {
5
+ schemaVersion: '1.0',
6
+ artefactType: args.artefactType,
7
+ id: randomUUID(),
8
+ title: args.extract.title,
9
+ createdAt: now,
10
+ createdBy: args.author,
11
+ lastEditedAt: now,
12
+ lastEditedBy: args.author,
13
+ version: 1,
14
+ source: args.source,
15
+ promotedFrom: args.promotedFrom ?? null,
16
+ content: args.extract.content,
17
+ interaction: { items: args.extract.items },
18
+ history: [{ at: now, by: args.author, action: 'created' }],
19
+ };
20
+ }
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Canonical SHA-256 of an envelope.
4
+ * Strips fields that change on every save without changing semantic content.
5
+ * Keys are lex-sorted recursively.
6
+ */
7
+ export async function canonicalSha256(env) {
8
+ const stripped = stripVolatileFields(env);
9
+ const canonical = canonicalJson(stripped);
10
+ return createHash('sha256').update(canonical, 'utf8').digest('hex');
11
+ }
12
+ function stripVolatileFields(env) {
13
+ const copy = JSON.parse(JSON.stringify(env));
14
+ delete copy.lastEditedAt;
15
+ if (Array.isArray(copy.history)) {
16
+ copy.history = copy.history.map((h) => {
17
+ const { at: _omit, ...rest } = h;
18
+ return rest;
19
+ });
20
+ }
21
+ return copy;
22
+ }
23
+ function canonicalJson(value) {
24
+ if (value === null)
25
+ return 'null';
26
+ if (typeof value === 'boolean' || typeof value === 'number')
27
+ return JSON.stringify(value);
28
+ if (typeof value === 'string')
29
+ return JSON.stringify(value);
30
+ if (Array.isArray(value)) {
31
+ return '[' + value.map(canonicalJson).join(',') + ']';
32
+ }
33
+ if (typeof value === 'object') {
34
+ const keys = Object.keys(value).sort();
35
+ const parts = keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k]));
36
+ return '{' + parts.join(',') + '}';
37
+ }
38
+ throw new Error(`canonicalJson: unsupported value type ${typeof value}`);
39
+ }
@@ -0,0 +1,54 @@
1
+ const EDITOR_URL = process.env.CSPEACH_EDITOR_URL ?? 'https://cspeach.dev/edit';
2
+ const SUBJECT_BY_TYPE = {
3
+ 'spec-gap': (t) => `Gap analysis — ${t}`,
4
+ 'design': (t) => `Design — ${t}`,
5
+ 'estimate': (t) => `Estimate — ${t}`,
6
+ 'cca': (t) => `Custom code analysis — ${t}`,
7
+ 'incident': (t) => `Incident report — ${t}`,
8
+ 'upgrade-scan': (t) => `S/4HANA upgrade scan — ${t}`,
9
+ 'upgrade-progress': (t) => `Upgrade remediation progress — ${t}`,
10
+ 'upgrade-report': (t) => `Upgrade remediation report — ${t}`,
11
+ 'cca-assessment': (t) => `Custom code assessment — ${t}`,
12
+ 'modernize-result': (t) => `Modernization result — ${t}`,
13
+ 'test-coverage': (t) => `Test coverage report — ${t}`,
14
+ };
15
+ const BODY_BY_TYPE = {
16
+ 'spec-gap': (a) => `Attached is the gap analysis for the ${a.title}.\n${a.itemCount} ${a.itemNoun} across business / data / authorisation / edge cases.`,
17
+ 'design': (a) => `Attached is the architecture design for the ${a.title}.\n${a.itemCount} ${a.itemNoun} to review (keep / redesign / defer).`,
18
+ 'estimate': (a) => `Attached is the effort estimate for the ${a.title}.\n${a.itemCount} ${a.itemNoun} to review (confirm / dispute / defer).`,
19
+ 'cca': (a) => `Attached is the custom code analysis for the ${a.title}.\n${a.itemCount} ${a.itemNoun} classified.`,
20
+ 'incident': (a) => `Attached is the incident report for the ${a.title}.`,
21
+ 'upgrade-scan': (a) => `Attached is the S/4HANA upgrade scan baseline for ${a.title}.\n${a.itemCount} ${a.itemNoun} to remediate.`,
22
+ 'upgrade-progress': (a) => `Attached is the upgrade remediation progress for ${a.title}.\n${a.itemCount} ${a.itemNoun} attempted.`,
23
+ 'upgrade-report': (a) => `Attached is the upgrade remediation report for ${a.title}.\n${a.itemCount} ${a.itemNoun} listed (regressions to address before sign-off).`,
24
+ 'cca-assessment': (a) => `Attached is the custom code assessment for ${a.title}.\n${a.itemCount} ${a.itemNoun} classified — review keep / fix / retire / redesign per object.`,
25
+ 'modernize-result': (a) => `Attached is the modernization result for ${a.title}.\n${a.itemCount} ${a.itemNoun} attempted — review applied / skipped / failed per object before sign-off.`,
26
+ 'test-coverage': (a) => `Attached is the test coverage report for ${a.title}.\n${a.itemCount} test ${a.itemNoun} generated — review the targets + method counts before merging.`,
27
+ };
28
+ export function renderEmailTemplate(a) {
29
+ const subject = SUBJECT_BY_TYPE[a.artefactType](a.title);
30
+ const body = BODY_BY_TYPE[a.artefactType](a);
31
+ return [
32
+ '─────────────────────────────────────────────────────────────────',
33
+ 'Suggested email — copy from below the line, edit, send',
34
+ '─────────────────────────────────────────────────────────────────',
35
+ `Subject: ${subject}`,
36
+ '',
37
+ 'Hi,',
38
+ '',
39
+ body,
40
+ '',
41
+ 'To review:',
42
+ ` 1. Open ${EDITOR_URL}`,
43
+ ` 2. Drag the attached ${a.filename} onto the page`,
44
+ ' 3. Click each item, mark its status, add comments',
45
+ ' 4. Click "Download updated file"',
46
+ ' 5. Email me the downloaded file',
47
+ '',
48
+ 'If your email blocks .json, rename it to .cspeach.json.txt before sending —',
49
+ 'the editor accepts both.',
50
+ '',
51
+ 'Thanks,',
52
+ a.author,
53
+ ].join('\n');
54
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Parser for the `<!-- csforge:cca-manifest ... -->` block that
3
+ * /abap-cca and /abap-cca-merge emit at the end of their markdown
4
+ * output. Hidden from human renderers, structured enough for a
5
+ * deterministic extractor.
6
+ *
7
+ * Format mirrors the upgrade-manifest pattern (see extract-upgrade.ts):
8
+ *
9
+ * <!-- csforge:cca-manifest
10
+ * artefact: cca-assessment
11
+ * title: ZFI / ZSD — S/4HANA 2023 estate
12
+ * detail_path: .abapforge/cca/projects/zfi-zsd-2026-04/project.json
13
+ * project_id: zfi-zsd-2026-04
14
+ * scope: ZFI* + ZSD*
15
+ * atc_variant: S4HANA_READINESS_2023
16
+ * total_objects: 1842
17
+ * keep: 421
18
+ * fix: 738
19
+ * retire: 124
20
+ * redesign: 312
21
+ * uncategorized: 247
22
+ * classifications:
23
+ * item-001 | ZFI_BP_SYNC | PROG | fix | high
24
+ * item-002 | ZCL_FI_DOC | CLAS | redesign | medium
25
+ * -->
26
+ *
27
+ * The classifications table is capped at 50 rows in the manifest; the
28
+ * full per-object detail lives in the project.json at `detail_path`.
29
+ *
30
+ * Parsing is permissive — missing optional fields produce defaults; a
31
+ * missing manifest block throws so callers know to surface the error.
32
+ */
33
+ const MANIFEST_RE = /<!--\s*csforge:cca-manifest\s*\n([\s\S]*?)\n\s*-->/;
34
+ function parseManifestBlock(markdown) {
35
+ const m = MANIFEST_RE.exec(markdown);
36
+ if (!m) {
37
+ throw new Error('No <!-- csforge:cca-manifest --> block found in skill output');
38
+ }
39
+ const body = m[1] ?? '';
40
+ const kv = {};
41
+ const tables = {};
42
+ let currentTable = null;
43
+ for (const rawLine of body.split('\n')) {
44
+ const line = rawLine.replace(/\r$/, '');
45
+ if (line.trim().length === 0) {
46
+ currentTable = null;
47
+ continue;
48
+ }
49
+ if (currentTable !== null && /^\s+/.test(line)) {
50
+ const cells = line.trim().split('|').map((c) => c.trim());
51
+ if (cells.length > 0) {
52
+ if (!tables[currentTable])
53
+ tables[currentTable] = [];
54
+ tables[currentTable].push(cells);
55
+ }
56
+ continue;
57
+ }
58
+ const colon = line.indexOf(':');
59
+ if (colon === -1)
60
+ continue;
61
+ const key = line.slice(0, colon).trim();
62
+ const value = line.slice(colon + 1).trim();
63
+ if (value.length === 0) {
64
+ currentTable = key;
65
+ tables[key] = [];
66
+ }
67
+ else {
68
+ kv[key] = value;
69
+ currentTable = null;
70
+ }
71
+ }
72
+ return { kv, tables };
73
+ }
74
+ function intOr(kv, key, fallback) {
75
+ const v = kv[key];
76
+ if (v === undefined)
77
+ return fallback;
78
+ const n = Number.parseInt(v, 10);
79
+ return Number.isFinite(n) ? n : fallback;
80
+ }
81
+ const CLASSIFICATIONS = ['keep', 'fix', 'retire', 'redesign'];
82
+ const CONFIDENCES = ['high', 'medium', 'low'];
83
+ function asClassification(raw) {
84
+ const v = raw.toLowerCase();
85
+ return CLASSIFICATIONS.includes(v)
86
+ ? v
87
+ : 'keep';
88
+ }
89
+ function asConfidence(raw) {
90
+ const v = raw.toLowerCase();
91
+ return CONFIDENCES.includes(v)
92
+ ? v
93
+ : 'medium';
94
+ }
95
+ export function extractCcaAssessment(markdown) {
96
+ const { kv, tables } = parseManifestBlock(markdown);
97
+ if (kv.artefact !== 'cca-assessment') {
98
+ throw new Error(`Manifest artefact mismatch: expected 'cca-assessment', got '${kv.artefact}'`);
99
+ }
100
+ if (!kv.detail_path) {
101
+ throw new Error("cca-assessment manifest missing required 'detail_path'");
102
+ }
103
+ if (!kv.project_id) {
104
+ throw new Error("cca-assessment manifest missing required 'project_id'");
105
+ }
106
+ const classifications = (tables.classifications ?? []).map((row, i) => ({
107
+ id: row[0] ?? `item-${String(i + 1).padStart(3, '0')}`,
108
+ objectName: row[1] ?? '',
109
+ objectType: row[2] ?? '',
110
+ classification: asClassification(row[3] ?? 'keep'),
111
+ confidence: asConfidence(row[4] ?? 'medium'),
112
+ }));
113
+ const content = {
114
+ detailPath: kv.detail_path,
115
+ projectId: kv.project_id,
116
+ scope: kv.scope ?? '',
117
+ atcVariant: kv.atc_variant ?? '',
118
+ summary: {
119
+ totalObjects: intOr(kv, 'total_objects', classifications.length),
120
+ keep: intOr(kv, 'keep', 0),
121
+ fix: intOr(kv, 'fix', 0),
122
+ retire: intOr(kv, 'retire', 0),
123
+ redesign: intOr(kv, 'redesign', 0),
124
+ uncategorized: intOr(kv, 'uncategorized', 0),
125
+ },
126
+ classifications,
127
+ };
128
+ // One InteractionItem per classification entry, default status 'open'
129
+ // (auto-classified by the AI, awaiting human review).
130
+ const items = classifications.map((c) => ({
131
+ id: c.id,
132
+ status: 'open',
133
+ answer: null,
134
+ answeredBy: null,
135
+ answeredAt: null,
136
+ comments: [],
137
+ }));
138
+ return { title: kv.title ?? 'CCA Assessment', content, items };
139
+ }