@contentful/experience-design-system-cli 2.12.0 → 2.12.1-dev-build-bae15c7.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 (144) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +260 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +820 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/merge-ai-decisions.d.ts +5 -0
  62. package/dist/src/import/tui/merge-ai-decisions.js +22 -0
  63. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  64. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  65. package/dist/src/import/tui/push-progress.d.ts +23 -0
  66. package/dist/src/import/tui/push-progress.js +14 -0
  67. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  68. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  69. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  70. package/dist/src/import/tui/run-teaser.js +9 -0
  71. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  72. package/dist/src/import/tui/runLivePreview.js +73 -0
  73. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  74. package/dist/src/import/tui/runScopeGate.js +28 -0
  75. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  76. package/dist/src/import/tui/scope-gate-host.js +20 -0
  77. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  78. package/dist/src/import/tui/spawn-generate.js +53 -0
  79. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  80. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  81. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  82. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  83. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  84. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  85. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  86. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  87. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  88. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  89. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  90. package/dist/src/import/tui/steps/ScopeGateStep.js +250 -0
  91. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  92. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  93. package/dist/src/import/tui/useLivePreview.js +120 -0
  94. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  95. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  96. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  97. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  98. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  99. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  100. package/dist/src/lib/contentful-urls.d.ts +34 -0
  101. package/dist/src/lib/contentful-urls.js +41 -0
  102. package/dist/src/program.js +2 -0
  103. package/dist/src/runs/export-helpers.d.ts +30 -0
  104. package/dist/src/runs/export-helpers.js +59 -0
  105. package/dist/src/runs/fingerprint.d.ts +47 -0
  106. package/dist/src/runs/fingerprint.js +75 -0
  107. package/dist/src/runs/ls-command.d.ts +12 -0
  108. package/dist/src/runs/ls-command.js +137 -0
  109. package/dist/src/runs/modify-launcher.d.ts +24 -0
  110. package/dist/src/runs/modify-launcher.js +36 -0
  111. package/dist/src/runs/path-prompt.d.ts +8 -0
  112. package/dist/src/runs/path-prompt.js +72 -0
  113. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  114. package/dist/src/runs/push-creds-prompt.js +39 -0
  115. package/dist/src/runs/push-helpers.d.ts +29 -0
  116. package/dist/src/runs/push-helpers.js +69 -0
  117. package/dist/src/runs/replay-helpers.d.ts +67 -0
  118. package/dist/src/runs/replay-helpers.js +148 -0
  119. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  120. package/dist/src/runs/resolve-run-target.js +45 -0
  121. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  122. package/dist/src/runs/run-picker-mount.js +72 -0
  123. package/dist/src/runs/run-picker.d.ts +17 -0
  124. package/dist/src/runs/run-picker.js +145 -0
  125. package/dist/src/runs/save-conflict.d.ts +8 -0
  126. package/dist/src/runs/save-conflict.js +56 -0
  127. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  128. package/dist/src/runs/save-path-resolver.js +71 -0
  129. package/dist/src/runs/staleness.d.ts +38 -0
  130. package/dist/src/runs/staleness.js +140 -0
  131. package/dist/src/runs/store.d.ts +78 -0
  132. package/dist/src/runs/store.js +136 -0
  133. package/dist/src/session/cache-keys.d.ts +15 -0
  134. package/dist/src/session/cache-keys.js +38 -0
  135. package/dist/src/session/db.d.ts +89 -3
  136. package/dist/src/session/db.js +447 -104
  137. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  138. package/dist/src/setup/auto-filter-prompt.js +24 -0
  139. package/dist/src/setup/command.d.ts +7 -0
  140. package/dist/src/setup/command.js +58 -2
  141. package/dist/src/types.d.ts +6 -0
  142. package/package.json +2 -2
  143. package/skills/generate-components.md +54 -6
  144. package/skills/select-components.md +13 -4
@@ -16,6 +16,9 @@ export async function readExperiencesCredentials() {
16
16
  ...(host ? { host } : {}),
17
17
  ...(parsed.agent ? { agent: parsed.agent } : {}),
18
18
  ...(parsed.agentModel ? { agentModel: parsed.agentModel } : {}),
19
+ ...(parsed.selectPromptPath ? { selectPromptPath: parsed.selectPromptPath } : {}),
20
+ ...(parsed.generatePromptPath ? { generatePromptPath: parsed.generatePromptPath } : {}),
21
+ ...(typeof parsed.autoFilter === 'boolean' ? { autoFilter: parsed.autoFilter } : {}),
19
22
  };
20
23
  }
21
24
  catch {
@@ -29,7 +32,7 @@ export async function readExperiencesCredentials() {
29
32
  }
30
33
  }
31
34
  export async function writeExperiencesCredentials(creds) {
32
- const { host: _host, agent, agentModel, ...rest } = creds;
35
+ const { host: _host, agent, agentModel, selectPromptPath, generatePromptPath, autoFilter, ...rest } = creds;
33
36
  const host = toConfiguredHost(creds.host);
34
37
  await mkdir(CREDENTIALS_DIR, { recursive: true });
35
38
  await writeFile(CREDENTIALS_PATH, JSON.stringify({
@@ -37,6 +40,9 @@ export async function writeExperiencesCredentials(creds) {
37
40
  ...(host ? { host } : {}),
38
41
  ...(agent ? { agent } : {}),
39
42
  ...(agentModel ? { agentModel } : {}),
43
+ ...(selectPromptPath ? { selectPromptPath } : {}),
44
+ ...(generatePromptPath ? { generatePromptPath } : {}),
45
+ ...(typeof autoFilter === 'boolean' ? { autoFilter } : {}),
40
46
  }, null, 2) + '\n', { mode: 0o600 });
41
47
  }
42
48
  export function experiencesCredentialsPath() {
@@ -15,6 +15,8 @@ export interface ClassifyPropCall {
15
15
  required?: boolean;
16
16
  description?: string;
17
17
  default?: string | boolean;
18
+ /** Internal LLM rationale; not customer-facing. Persisted to raw_props.rationale. */
19
+ reason?: string;
18
20
  }
19
21
  export interface ExcludePropCall {
20
22
  tool: 'exclude_prop';
@@ -24,6 +26,16 @@ export interface ExcludePropCall {
24
26
  export interface ClassifyComponentCall {
25
27
  tool: 'classify_component';
26
28
  description?: string;
29
+ /**
30
+ * Component-level rationale strings. Surfaced by the `I` ComponentRationalePanel.
31
+ * Each field is optional; missing fields leave existing DB values untouched
32
+ * (sparse update semantics in applyToolCalls).
33
+ */
34
+ rationale?: {
35
+ description?: string;
36
+ props?: string;
37
+ slots?: string;
38
+ };
27
39
  }
28
40
  export interface ClassifySlotCall {
29
41
  tool: 'classify_slot';
@@ -31,6 +43,8 @@ export interface ClassifySlotCall {
31
43
  required?: boolean;
32
44
  allowed_components?: string[];
33
45
  description?: string;
46
+ /** Per-slot rationale; persisted to raw_slots.rationale. */
47
+ rationale?: string;
34
48
  }
35
49
  export type ToolCall = ClassifyPropCall | ExcludePropCall | ClassifyComponentCall | ClassifySlotCall;
36
50
  export interface SelectComponentCall {
@@ -99,6 +99,8 @@ export function parseToolCallLines(stdout) {
99
99
  call.description = rec.description;
100
100
  if (typeof rec.default === 'string' || typeof rec.default === 'boolean')
101
101
  call.default = rec.default;
102
+ if (typeof rec.reason === 'string')
103
+ call.reason = rec.reason;
102
104
  calls.push(call);
103
105
  }
104
106
  else if (tool === 'exclude_prop') {
@@ -116,6 +118,18 @@ export function parseToolCallLines(stdout) {
116
118
  const call = { tool: 'classify_component' };
117
119
  if (typeof rec.description === 'string')
118
120
  call.description = rec.description;
121
+ if (typeof rec.rationale === 'object' && rec.rationale !== null) {
122
+ const r = rec.rationale;
123
+ const rationale = {};
124
+ if (typeof r.description === 'string')
125
+ rationale.description = r.description;
126
+ if (typeof r.props === 'string')
127
+ rationale.props = r.props;
128
+ if (typeof r.slots === 'string')
129
+ rationale.slots = r.slots;
130
+ if (Object.keys(rationale).length > 0)
131
+ call.rationale = rationale;
132
+ }
119
133
  calls.push(call);
120
134
  }
121
135
  else if (tool === 'classify_slot') {
@@ -131,6 +145,8 @@ export function parseToolCallLines(stdout) {
131
145
  }
132
146
  if (typeof rec.description === 'string')
133
147
  call.description = rec.description;
148
+ if (typeof rec.rationale === 'string')
149
+ call.rationale = rec.rationale;
134
150
  calls.push(call);
135
151
  }
136
152
  }
@@ -1,2 +1,8 @@
1
1
  import type { Command } from 'commander';
2
+ /**
3
+ * Feature 8: render the warning banner shown when a custom skill prompt is
4
+ * active. Always cites the bundled invariants that the override bypasses so
5
+ * the operator cannot miss it.
6
+ */
7
+ export declare function formatCustomPromptBanner(skill: 'components' | 'select', path: string): string;
2
8
  export declare function registerGenerateCommand(program: Command): void;
@@ -6,10 +6,12 @@ import { execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import { parseToolCallLines, parseTokenToolCallLines, resolveBinary, runAgent, } from './agent-runner.js';
8
8
  import { OutputFormatter, c } from '../output/format.js';
9
+ import { formatGenerateProgressLine } from './progress.js';
9
10
  import { buildPrompt, resolveSkillPath } from './prompt-builder.js';
10
11
  import { GenerateView } from './tui/GenerateView.js';
11
12
  import { registerGenerateEditCommand } from './edit/command.js';
12
13
  import { openPipelineDb, loadRawComponents, applyToolCalls, applyTokenToolCalls, computeComponentInputHash, computeTokenInputHash, lookupCache, lookupCacheByEntity, storeCache, copyComponentFromCache, copyTokensFromCache, renameEmptySlots, } from '../session/db.js';
14
+ import { hashPromptForSkill } from '../session/cache-keys.js';
13
15
  import { getRefineArtifactsRoot, getRefineSessionPaths } from '../analyze/select/persistence.js';
14
16
  import { readExperiencesCredentials } from '../credentials-store.js';
15
17
  const execFileAsync = promisify(execFile);
@@ -17,6 +19,16 @@ const VALID_AGENTS = new Set(['claude', 'codex', 'opencode', 'cursor']);
17
19
  const DEFAULT_TIMEOUT_MS = Number(process.env.EDS_AGENT_TIMEOUT_MS ?? 3 * 60 * 1000);
18
20
  const DEFAULT_COMPONENT_CONCURRENCY = 10;
19
21
  const RETRY_BACKOFF_MS = Number(process.env.EDS_RETRY_BACKOFF_MS ?? 5_000);
22
+ /**
23
+ * Feature 8: render the warning banner shown when a custom skill prompt is
24
+ * active. Always cites the bundled invariants that the override bypasses so
25
+ * the operator cannot miss it.
26
+ */
27
+ export function formatCustomPromptBanner(skill, path) {
28
+ return (`WARNING: Custom prompt active for ${skill}: ${path}\n` +
29
+ ` Bundled invariants (utility-wrapper rejection, description content rules) do NOT apply.\n` +
30
+ ` You are responsible for the prompt's correctness.\n`);
31
+ }
20
32
  function die(message) {
21
33
  process.stderr.write(`${message}\n`);
22
34
  process.exit(1);
@@ -104,11 +116,11 @@ function printFallbackInstructions(options) {
104
116
  lines.push(` Re-run the generate command with the agent available, or use --dry-run to inspect the prompt.`);
105
117
  process.stderr.write(lines.join('\n') + '\n');
106
118
  }
107
- async function runOneComponent(agent, model, db, sessionId, component, tokensInline, tokenMapInline, index, total, verbose, noCache) {
119
+ async function runOneComponent(agent, model, db, sessionId, component, tokensInline, tokenMapInline, index, total, verbose, noCache, skillPathOverride, promptHash) {
108
120
  const pos = c.dim(`[${index + 1}/${total}]`);
109
121
  if (!noCache) {
110
122
  const inputHash = computeComponentInputHash(component);
111
- const cached = lookupCache(db, inputHash, 'component', component.component_id);
123
+ const cached = lookupCache(db, inputHash, 'component', component.component_id, promptHash);
112
124
  if (cached) {
113
125
  copyComponentFromCache(db, cached.sourceSessionId, sessionId, component.component_id);
114
126
  process.stderr.write(` ${pos} ${c.bold(component.name)} ${c.green('cached')}\n`);
@@ -168,6 +180,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
168
180
  tokenMapInline,
169
181
  outDir: process.cwd(),
170
182
  componentName: component.name,
183
+ skillPathOverride,
171
184
  });
172
185
  const maxAttempts = 2;
173
186
  let lastError = '';
@@ -215,7 +228,7 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
215
228
  const applied = applyToolCalls(db, sessionId, component.component_id, component.name, calls, warnings);
216
229
  if (!noCache) {
217
230
  const inputHash = computeComponentInputHash(component);
218
- storeCache(db, inputHash, 'component', component.component_id, sessionId, false);
231
+ storeCache(db, inputHash, 'component', component.component_id, sessionId, false, promptHash);
219
232
  }
220
233
  return {
221
234
  componentName: component.name,
@@ -238,17 +251,20 @@ async function runOneComponent(agent, model, db, sessionId, component, tokensInl
238
251
  renamedSlotsCount: renames.length,
239
252
  };
240
253
  }
241
- async function runAllComponents(agent, model, db, sessionId, components, tokensInline, tokenMapInline, verbose, noCache) {
254
+ async function runAllComponents(agent, model, db, sessionId, components, tokensInline, tokenMapInline, verbose, noCache, skillPathOverride, promptHash) {
242
255
  const concurrency = Number(process.env.EDS_GENERATE_CONCURRENCY ?? DEFAULT_COMPONENT_CONCURRENCY);
243
256
  process.stderr.write(`Categorizing ${c.bold(String(components.length))} component${components.length === 1 ? '' : 's'}` +
244
257
  c.dim(` (concurrency: ${concurrency})`) +
245
258
  '\n');
246
259
  const results = new Array(components.length);
247
260
  let next = 0;
261
+ let completed = 0;
248
262
  async function worker() {
249
263
  while (next < components.length) {
250
264
  const i = next++;
251
- results[i] = await runOneComponent(agent, model, db, sessionId, components[i], tokensInline, tokenMapInline, i, components.length, verbose, noCache);
265
+ results[i] = await runOneComponent(agent, model, db, sessionId, components[i], tokensInline, tokenMapInline, i, components.length, verbose, noCache, skillPathOverride, promptHash);
266
+ completed += 1;
267
+ process.stderr.write(`${formatGenerateProgressLine(completed, components.length, results[i].componentName)}\n`);
252
268
  }
253
269
  }
254
270
  await Promise.all(Array.from({ length: Math.min(concurrency, components.length) }, worker));
@@ -300,6 +316,19 @@ async function runGenerateSkill(skill, opts, verbose = false) {
300
316
  die(`Error: no agent configured. Pass --agent <name> or run experiences setup. Accepted values: claude, codex, opencode, cursor`);
301
317
  }
302
318
  const agent = agentName;
319
+ // Feature 8: resolve custom-prompt path for `components` (flag wins over
320
+ // saved credentials), validate, and emit the warning banner once at action
321
+ // entry.
322
+ const generatePromptPath = skill === 'components' ? (opts.generatePromptPath ?? savedCreds.generatePromptPath) : undefined;
323
+ if (generatePromptPath) {
324
+ if (!(await pathExists(resolve(generatePromptPath)))) {
325
+ die(`Error: custom prompt path not found: ${resolve(generatePromptPath)}`);
326
+ }
327
+ if (!generatePromptPath.toLowerCase().endsWith('.md')) {
328
+ process.stderr.write(`WARNING: custom prompt path does not end in .md (${generatePromptPath}) — proceeding anyway.\n`);
329
+ }
330
+ process.stderr.write(formatCustomPromptBanner('components', resolve(generatePromptPath)));
331
+ }
303
332
  if (skill === 'tokens' && !opts.rawTokens) {
304
333
  die('Error: --raw-tokens is required when using generate tokens');
305
334
  }
@@ -373,6 +402,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
373
402
  tokensInline,
374
403
  tokenMapInline,
375
404
  outDir: process.cwd(),
405
+ skillPathOverride: generatePromptPath,
376
406
  });
377
407
  process.stdout.write(prompt + '\n');
378
408
  process.exit(0);
@@ -390,7 +420,8 @@ async function runGenerateSkill(skill, opts, verbose = false) {
390
420
  const db = openPipelineDb();
391
421
  let componentResults;
392
422
  try {
393
- componentResults = await runAllComponents(agent, model, db, sessionId, allComponents, tokensInline, tokenMapInline, verbose, opts.cache === false || process.env.EDS_NO_CACHE === '1');
423
+ const promptHash = await hashPromptForSkill('components', generatePromptPath);
424
+ componentResults = await runAllComponents(agent, model, db, sessionId, allComponents, tokensInline, tokenMapInline, verbose, opts.cache === false || process.env.EDS_NO_CACHE === '1', generatePromptPath, promptHash);
394
425
  }
395
426
  finally {
396
427
  db.close();
@@ -449,9 +480,10 @@ async function runGenerateSkill(skill, opts, verbose = false) {
449
480
  resolvedSessionId = newId;
450
481
  }
451
482
  }
483
+ const tokenPromptHash = await hashPromptForSkill('tokens');
452
484
  // Check cache before invoking agent
453
485
  if (!noCache) {
454
- const tokenCached = lookupCache(db, tokenInputHash, 'token_set', '__tokens__');
486
+ const tokenCached = lookupCache(db, tokenInputHash, 'token_set', '__tokens__', tokenPromptHash);
455
487
  if (tokenCached) {
456
488
  copyTokensFromCache(db, tokenCached.sourceSessionId, resolvedSessionId);
457
489
  sessionId = resolvedSessionId;
@@ -508,7 +540,7 @@ async function runGenerateSkill(skill, opts, verbose = false) {
508
540
  }
509
541
  applyTokenToolCalls(db, resolvedSessionId, tokenCalls, []);
510
542
  if (!noCache) {
511
- storeCache(db, tokenInputHash, 'token_set', '__tokens__', resolvedSessionId, false);
543
+ storeCache(db, tokenInputHash, 'token_set', '__tokens__', resolvedSessionId, false, tokenPromptHash);
512
544
  }
513
545
  sessionId = resolvedSessionId;
514
546
  const groupCount = tokenCalls.filter((tc) => tc.tool === 'set_group').length;
@@ -541,7 +573,9 @@ function addAgentFlags(cmd) {
541
573
  .option('--model <name>', 'Model to use (defaults to a small/fast model per agent)')
542
574
  .option('--verbose', 'Show full agent output including reasoning text')
543
575
  .option('--dry-run', 'Print the prompt without invoking the agent')
544
- .option('--no-cache', 'Bypass generation cache and force AI re-generation');
576
+ .option('--no-cache', 'Bypass ALL fine-grained caches (extract, select, generate) and force AI re-run. ' +
577
+ 'Cache keys now factor in prompt content — changing the prompt file via --generate-prompt-path or ' +
578
+ '--select-prompt-path will already bust the corresponding stage. Use --no-cache to force a full re-run.');
545
579
  }
546
580
  export function registerGenerateCommand(program) {
547
581
  const generate = program.command('generate').description('Generate CDF/DTCG artifacts or correct generation output');
@@ -551,7 +585,8 @@ export function registerGenerateCommand(program) {
551
585
  .description('Invoke a coding agent to produce components.json from raw analysis output')
552
586
  .option('--session <id>', 'Session ID from analyze extract (defaults to most recent)')
553
587
  .option('--tokens <path>', 'Path to tokens.json for token-linked prop resolution')
554
- .option('--token-map <path>', 'Path to token-name-map.json sidecar');
588
+ .option('--token-map <path>', 'Path to token-name-map.json sidecar')
589
+ .option('--generate-prompt-path <path>', 'Path to a custom .md skill prompt for components generation (bypasses bundled prompt invariants)');
555
590
  addAgentFlags(componentsCmd).action(async (opts) => {
556
591
  await runGenerateSkill('components', opts, opts.verbose ?? false);
557
592
  });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Formats a structured generation-progress line for the wizard parser.
3
+ *
4
+ * Emitted at terminal completion of each component (success, cache hit,
5
+ * pinned hit, or final failure). `done` represents the count of completed
6
+ * components, which is monotonically non-decreasing — unlike the legacy
7
+ * `[index+1/total]` line, which reports input position.
8
+ */
9
+ export declare function formatGenerateProgressLine(done: number, total: number, name: string): string;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Formats a structured generation-progress line for the wizard parser.
3
+ *
4
+ * Emitted at terminal completion of each component (success, cache hit,
5
+ * pinned hit, or final failure). `done` represents the count of completed
6
+ * components, which is monotonically non-decreasing — unlike the legacy
7
+ * `[index+1/total]` line, which reports input position.
8
+ */
9
+ export function formatGenerateProgressLine(done, total, name) {
10
+ return `progress=generate:${done}/${total}:${name}`;
11
+ }
@@ -13,6 +13,14 @@ export interface PromptOptions {
13
13
  outDir: string;
14
14
  /** For components skill only: the single component's name (used in error messages). */
15
15
  componentName?: string;
16
+ /**
17
+ * Feature 8: custom prompt path override. When set, this absolute or relative
18
+ * `.md` path is read in place of the bundled skill file. The bundled-prompt
19
+ * invariants (utility-wrapper rejection, description content rules, etc.) do
20
+ * NOT apply under an override — callers are responsible for showing the
21
+ * appropriate warning banner.
22
+ */
23
+ skillPathOverride?: string;
16
24
  }
17
25
  export declare function buildPrompt(options: PromptOptions): Promise<string>;
18
26
  export declare function resolveSkillPath(skill: Skill): string;
@@ -13,7 +13,7 @@ const OUTPUT_FILES = {
13
13
  select: 'select.json',
14
14
  };
15
15
  export async function buildPrompt(options) {
16
- const skillContent = await readSkillFile(options.skill);
16
+ const skillContent = await readSkillFile(options.skill, options.skillPathOverride);
17
17
  const preamble = buildPreamble(options);
18
18
  return `${preamble}\n\nSkill instructions follow:\n---\n${skillContent}`;
19
19
  }
@@ -34,7 +34,16 @@ export function resolveSkillPath(skill) {
34
34
  dir = parent;
35
35
  }
36
36
  }
37
- async function readSkillFile(skill) {
37
+ async function readSkillFile(skill, override) {
38
+ if (override) {
39
+ const skillPath = resolve(override);
40
+ try {
41
+ return await readFile(skillPath, 'utf8');
42
+ }
43
+ catch {
44
+ throw new Error(`custom prompt file not found (skill: ${skill}, path: ${skillPath})`);
45
+ }
46
+ }
38
47
  const skillPath = resolveSkillPath(skill);
39
48
  try {
40
49
  return await readFile(skillPath, 'utf8');
@@ -123,13 +132,13 @@ Do NOT write any files or emit any JSON blobs. Instead, emit one JSON object per
123
132
  The four tool calls you may emit are:
124
133
 
125
134
  \`\`\`
126
- {"tool":"classify_component","description":"<optional component-level description>"}
135
+ {"tool":"classify_component","description":"<optional component-level description>","rationale":{"description":"<why this component is classified the way it is>","props":"<why these props were chosen>","slots":"<why these slots were chosen>"}}
127
136
 
128
- {"tool":"classify_prop","prop":"<propName>","cdf_type":"<type>","cdf_category":"<category>","required":<bool>,"description":"<reason>","values":["a","b"],"token_kind":"color","default":"<value>"}
137
+ {"tool":"classify_prop","prop":"<propName>","cdf_type":"<type>","cdf_category":"<category>","required":<bool>,"description":"<short customer-facing description>","reason":"<full internal rationale; not customer-facing>","values":["a","b"],"token_kind":"color","default":"<value>"}
129
138
 
130
139
  {"tool":"exclude_prop","prop":"<propName>","reason":"<why excluded>"}
131
140
 
132
- {"tool":"classify_slot","slot":"<slotName>","required":<bool>,"allowed_components":["ComponentName"],"description":"<reason>"}
141
+ {"tool":"classify_slot","slot":"<slotName>","required":<bool>,"allowed_components":["ComponentName"],"description":"<reason>","rationale":"<why this slot was kept in the catalog>"}
133
142
  \`\`\`
134
143
 
135
144
  Rules:
@@ -143,12 +152,14 @@ Rules:
143
152
  - href and URL props → cdf_type "string", cdf_category "content". Do NOT use cdf_type "link" — it is not valid.
144
153
  - Framework internals (ref, event handlers, test IDs) → exclude_prop.
145
154
  - CSS design props (className, style, styles, positional/geometric props: top, bottom, left, right, rotation, offset, etc.) → classify_prop, cdf_type: "string", cdf_category: "design".
155
+ - On classify_component, "rationale" fields are operator-facing (read-only) but may surface in customer-facing exports. The "rationale.description" field is subject to the description content rules in the skill prompt (no internal initiative names). "rationale.props" and "rationale.slots" describe your reasoning about scope; "classify_slot.rationale" explains why each slot was kept.
156
+ - On classify_prop, "reason" is REQUIRED and is the LLM's internal rationale — shown to the developer reviewing the import, never to end-users. "description" is the customer-facing copy and is subject to the description content rules in the skill prompt. Keep them distinct: "description" is short and customer-facing; "reason" explains your reasoning in detail.
146
157
  - You may emit prose lines (not starting with {) anywhere — they are ignored by the parser and serve as your reasoning log.`;
147
158
  }
148
159
  function buildSelectAutonomousPreamble(inputBlock) {
149
160
  return `You are running as part of the experience-design-system-cli import pipeline in AUTONOMOUS mode. The developer is not present to answer questions.
150
161
 
151
- Your task: review the single component provided below and decide whether it belongs in Contentful Experience Orchestration as a Component Type. Apply all judgment calls yourself — do not pause to ask for confirmation. Include a brief "reason" to document your reasoning.
162
+ Your task: review the components provided below and decide whether each belongs in Contentful Experience Orchestration as a Component Type. The input is a JSON array — you may receive 1–N components in a single message. Emit one tool call per input component, named after the component. Apply all judgment calls yourself — do not pause to ask for confirmation. Include a brief "reason" to document your reasoning for each decision.
152
163
 
153
164
  Key rule: accept any component that renders visible UI — atoms, molecules, and organisms are all valid Component Types in Contentful Experience Orchestration. Reject only components that produce zero visual output: React hooks, pure context providers, A/B testing or variant-routing wrappers, analytics trackers, and security utilities. Do NOT reject a component because it has few props, is low-level, or has some A/B testing or personalization-related props mixed in — those props are handled in the generate step.
154
165
 
@@ -156,9 +167,9 @@ All input data is provided inline below — do not read any additional files.${i
156
167
 
157
168
  ## Output protocol
158
169
 
159
- Do NOT write any files or emit any JSON blobs. Instead, emit one JSON object on a single line to stdout. The CLI reads your stdout line by line.
170
+ Do NOT write any files or emit any JSON blobs. Instead, emit JSON tool calls one per line to stdout. The CLI reads your stdout line by line.
160
171
 
161
- The two tool calls — emit exactly one:
172
+ The two tool calls — emit exactly one per input component:
162
173
 
163
174
  \`\`\`
164
175
  {"tool":"select_component","name":"<ComponentName>","reason":"<brief reason>"}
@@ -167,9 +178,9 @@ The two tool calls — emit exactly one:
167
178
  \`\`\`
168
179
 
169
180
  Rules:
170
- - Emit exactly one JSON object, on one line. No multi-line JSON. No markdown fences.
171
- - The name must match the component name in the input exactly.
172
- - You may emit prose lines (not starting with {) to reason before the final tool call — they are ignored by the parser.`;
181
+ - Emit exactly one JSON object per line. No multi-line JSON. No markdown fences.
182
+ - Emit exactly one tool call per input component. The "name" field must match a component name from the input array exactly. Tool calls may appear in any order.
183
+ - You may emit prose lines (not starting with {) to reason before each tool call — they are ignored by the parser.`;
173
184
  }
174
185
  function buildTokensAutonomousPreamble(inputBlock) {
175
186
  return `You are running as part of the experience-design-system-cli generate pipeline in AUTONOMOUS mode. The developer is not present to answer questions.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Resolution chain for `--agent` and `--model` overrides on `experiences import`.
3
+ *
4
+ * The wizard path previously hard-defaulted `--agent` to `'claude'` at the
5
+ * commander layer, which (a) shadowed the stored `credentials.json` value when
6
+ * the user passed no flag, and (b) made it impossible to distinguish "user
7
+ * explicitly passed --agent claude" from "user passed nothing". `--model` was
8
+ * registered but never threaded into the wizard subprocesses at all.
9
+ *
10
+ * These helpers produce the resolved values to feed into `WizardApp` /
11
+ * `runPipeline`:
12
+ *
13
+ * 1. CLI flag wins when provided.
14
+ * 2. Otherwise, the value persisted in `credentials.json` (written by
15
+ * `experiences setup`) is used.
16
+ * 3. Otherwise, the built-in default (currently `'claude'`) is used for
17
+ * the agent. Model has no default — agents pick a small/fast model
18
+ * themselves when `--model` is omitted.
19
+ */
20
+ /** Built-in fallback agent when neither a flag nor a stored preference is set. */
21
+ export declare const DEFAULT_AGENT = "claude";
22
+ export declare function resolveAgent(flagValue: string | undefined, storedValue: string | undefined): string;
23
+ export declare function resolveModel(flagValue: string | undefined, storedValue: string | undefined): string | undefined;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Resolution chain for `--agent` and `--model` overrides on `experiences import`.
3
+ *
4
+ * The wizard path previously hard-defaulted `--agent` to `'claude'` at the
5
+ * commander layer, which (a) shadowed the stored `credentials.json` value when
6
+ * the user passed no flag, and (b) made it impossible to distinguish "user
7
+ * explicitly passed --agent claude" from "user passed nothing". `--model` was
8
+ * registered but never threaded into the wizard subprocesses at all.
9
+ *
10
+ * These helpers produce the resolved values to feed into `WizardApp` /
11
+ * `runPipeline`:
12
+ *
13
+ * 1. CLI flag wins when provided.
14
+ * 2. Otherwise, the value persisted in `credentials.json` (written by
15
+ * `experiences setup`) is used.
16
+ * 3. Otherwise, the built-in default (currently `'claude'`) is used for
17
+ * the agent. Model has no default — agents pick a small/fast model
18
+ * themselves when `--model` is omitted.
19
+ */
20
+ /** Built-in fallback agent when neither a flag nor a stored preference is set. */
21
+ export const DEFAULT_AGENT = 'claude';
22
+ export function resolveAgent(flagValue, storedValue) {
23
+ if (flagValue && flagValue.length > 0)
24
+ return flagValue;
25
+ if (storedValue && storedValue.length > 0)
26
+ return storedValue;
27
+ return DEFAULT_AGENT;
28
+ }
29
+ export function resolveModel(flagValue, storedValue) {
30
+ if (flagValue && flagValue.length > 0)
31
+ return flagValue;
32
+ if (storedValue && storedValue.length > 0)
33
+ return storedValue;
34
+ return undefined;
35
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolve the effective auto-filter setting from a CLI flag plus the persisted
3
+ * config preference.
4
+ *
5
+ * Precedence (highest first):
6
+ * 1. CLI flag (`--auto-filter` or `--no-auto-filter`) — wins when set
7
+ * 2. Config (`credentials.json` `autoFilter` field) — used when flag absent
8
+ * 3. Default ON — when neither is set
9
+ */
10
+ export declare function resolveAutoFilter(opts: {
11
+ autoFilter?: boolean;
12
+ }, configAutoFilter?: boolean): boolean;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolve the effective auto-filter setting from a CLI flag plus the persisted
3
+ * config preference.
4
+ *
5
+ * Precedence (highest first):
6
+ * 1. CLI flag (`--auto-filter` or `--no-auto-filter`) — wins when set
7
+ * 2. Config (`credentials.json` `autoFilter` field) — used when flag absent
8
+ * 3. Default ON — when neither is set
9
+ */
10
+ export function resolveAutoFilter(opts, configAutoFilter) {
11
+ if (opts.autoFilter !== undefined)
12
+ return opts.autoFilter;
13
+ if (configAutoFilter !== undefined)
14
+ return configAutoFilter;
15
+ return true;
16
+ }