@kaelio/ktx 0.1.0-rc.5 → 0.1.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 (180) hide show
  1. package/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl +0 -0
  2. package/assets/python/manifest.json +2 -2
  3. package/dist/clack.d.ts +6 -0
  4. package/dist/clack.js +23 -0
  5. package/dist/cli-program.js +5 -2
  6. package/dist/cli-program.test.js +7 -1
  7. package/dist/cli-runtime.d.ts +4 -0
  8. package/dist/cli-runtime.js +8 -1
  9. package/dist/command-schemas.d.ts +1 -1
  10. package/dist/commands/ingest-commands.js +1 -0
  11. package/dist/commands/knowledge-commands.js +5 -0
  12. package/dist/commands/mcp-commands.js +11 -3
  13. package/dist/commands/mcp-commands.test.js +30 -1
  14. package/dist/commands/sql-commands.d.ts +3 -0
  15. package/dist/commands/sql-commands.js +43 -0
  16. package/dist/commands/sql-commands.test.d.ts +1 -0
  17. package/dist/commands/sql-commands.test.js +68 -0
  18. package/dist/context-build-view.js +5 -1
  19. package/dist/dev.test.js +27 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.test.js +56 -21
  23. package/dist/ingest.js +123 -18
  24. package/dist/ingest.test.js +206 -0
  25. package/dist/io/print-list.d.ts +2 -1
  26. package/dist/io/print-list.js +7 -0
  27. package/dist/io/print-list.test.js +13 -11
  28. package/dist/io/symbols.d.ts +2 -2
  29. package/dist/knowledge.d.ts +1 -0
  30. package/dist/knowledge.js +34 -16
  31. package/dist/knowledge.test.js +27 -0
  32. package/dist/managed-python-command.d.ts +2 -0
  33. package/dist/managed-python-command.js +17 -9
  34. package/dist/managed-python-command.test.js +59 -4
  35. package/dist/next-steps.js +1 -1
  36. package/dist/next-steps.test.js +2 -0
  37. package/dist/print-command-tree.js +7 -1
  38. package/dist/public-ingest.d.ts +9 -1
  39. package/dist/public-ingest.js +50 -7
  40. package/dist/public-ingest.test.js +69 -2
  41. package/dist/release-version.d.ts +5 -0
  42. package/dist/release-version.js +44 -0
  43. package/dist/runtime-requirements.d.ts +23 -0
  44. package/dist/runtime-requirements.js +99 -0
  45. package/dist/runtime-requirements.test.d.ts +1 -0
  46. package/dist/runtime-requirements.test.js +63 -0
  47. package/dist/setup-agents.d.ts +11 -3
  48. package/dist/setup-agents.js +397 -134
  49. package/dist/setup-agents.test.js +359 -61
  50. package/dist/setup-embeddings.js +3 -6
  51. package/dist/setup-embeddings.test.js +18 -2
  52. package/dist/setup-models.js +2 -2
  53. package/dist/setup-models.test.js +5 -3
  54. package/dist/setup-ready-menu.d.ts +1 -1
  55. package/dist/setup-ready-menu.js +2 -0
  56. package/dist/setup-ready-menu.test.js +3 -0
  57. package/dist/setup-runtime.d.ts +45 -0
  58. package/dist/setup-runtime.js +47 -0
  59. package/dist/setup-runtime.test.d.ts +1 -0
  60. package/dist/setup-runtime.test.js +110 -0
  61. package/dist/setup-sources-notion.test.d.ts +1 -0
  62. package/dist/setup-sources-notion.test.js +107 -0
  63. package/dist/setup-sources.js +5 -2
  64. package/dist/setup.d.ts +19 -1
  65. package/dist/setup.js +104 -29
  66. package/dist/setup.test.js +221 -57
  67. package/dist/sl.js +2 -2
  68. package/dist/sl.test.js +10 -0
  69. package/dist/source-mapping.js +9 -1
  70. package/dist/source-mapping.test.d.ts +1 -0
  71. package/dist/source-mapping.test.js +65 -0
  72. package/dist/sql.d.ts +22 -0
  73. package/dist/sql.js +125 -0
  74. package/dist/sql.test.d.ts +1 -0
  75. package/dist/sql.test.js +226 -0
  76. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  77. package/node_modules/@ktx/context/dist/connections/connection-type.d.ts +4 -4
  78. package/node_modules/@ktx/context/dist/core/git.service.d.ts +3 -0
  79. package/node_modules/@ktx/context/dist/core/git.service.js +47 -1
  80. package/node_modules/@ktx/context/dist/core/git.service.patch.test.d.ts +1 -0
  81. package/node_modules/@ktx/context/dist/core/git.service.patch.test.js +40 -0
  82. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/types.d.ts +5 -5
  83. package/node_modules/@ktx/context/dist/ingest/adapters/looker/looker.adapter.d.ts +2 -2
  84. package/node_modules/@ktx/context/dist/ingest/adapters/looker/tools/looker-query-to-sl.tool.d.ts +2 -2
  85. package/node_modules/@ktx/context/dist/ingest/adapters/looker/types.d.ts +16 -16
  86. package/node_modules/@ktx/context/dist/ingest/adapters/lookml/pull-config.d.ts +1 -1
  87. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.js +16 -0
  88. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.test.js +41 -0
  89. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.d.ts +2 -1
  90. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.js +40 -0
  91. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.test.js +116 -1
  92. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.d.ts +29 -0
  93. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.js +40 -0
  94. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/pull-config.d.ts +1 -1
  95. package/node_modules/@ktx/context/dist/ingest/artifact-gates.d.ts +25 -0
  96. package/node_modules/@ktx/context/dist/ingest/artifact-gates.js +149 -0
  97. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.d.ts +1 -0
  98. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.js +167 -0
  99. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.d.ts +29 -0
  100. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.js +178 -0
  101. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.d.ts +1 -0
  102. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.js +109 -0
  103. package/node_modules/@ktx/context/dist/ingest/index.d.ts +8 -1
  104. package/node_modules/@ktx/context/dist/ingest/index.js +7 -0
  105. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +18 -2
  106. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.d.ts +1 -0
  107. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +1761 -0
  108. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +1695 -901
  109. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +135 -118
  110. package/node_modules/@ktx/context/dist/ingest/ingest-trace.d.ts +50 -0
  111. package/node_modules/@ktx/context/dist/ingest/ingest-trace.js +88 -0
  112. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.d.ts +1 -0
  113. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.js +76 -0
  114. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.d.ts +16 -0
  115. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.js +78 -0
  116. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.d.ts +1 -0
  117. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.js +76 -0
  118. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.d.ts +58 -0
  119. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.js +223 -0
  120. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.d.ts +1 -0
  121. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.js +369 -0
  122. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.d.ts +23 -0
  123. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.js +190 -0
  124. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.d.ts +1 -0
  125. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.js +101 -0
  126. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.d.ts +15 -0
  127. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.js +61 -0
  128. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.d.ts +1 -0
  129. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.js +137 -0
  130. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +7 -0
  131. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +54 -10
  132. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +65 -0
  133. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +23 -5
  134. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +17 -0
  135. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.test.js +1 -0
  136. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +6 -0
  137. package/node_modules/@ktx/context/dist/ingest/parsed-target-table.d.ts +1 -1
  138. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +3 -0
  139. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +32 -7
  140. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +25 -0
  141. package/node_modules/@ktx/context/dist/ingest/report-snapshot.test.js +124 -0
  142. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +23 -0
  143. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.d.ts +11 -0
  144. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.js +26 -0
  145. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.d.ts +1 -0
  146. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.js +25 -0
  147. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.d.ts +4 -0
  148. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.js +4 -0
  149. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.test.js +29 -0
  150. package/node_modules/@ktx/context/dist/ingest/tools/emit-unmapped-fallback.tool.d.ts +1 -1
  151. package/node_modules/@ktx/context/dist/ingest/types.d.ts +24 -0
  152. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.d.ts +24 -0
  153. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.js +111 -0
  154. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.d.ts +1 -0
  155. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.js +138 -0
  156. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.js +19 -2
  157. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.test.js +33 -0
  158. package/node_modules/@ktx/context/dist/project/setup-config.d.ts +1 -1
  159. package/node_modules/@ktx/context/dist/project/setup-config.js +10 -1
  160. package/node_modules/@ktx/context/dist/project/setup-config.test.js +3 -2
  161. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.js +5 -1
  162. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.test.js +15 -0
  163. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.js +5 -1
  164. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.test.js +22 -0
  165. package/node_modules/@ktx/context/dist/tools/action-target-connection.d.ts +9 -0
  166. package/node_modules/@ktx/context/dist/tools/action-target-connection.js +14 -0
  167. package/node_modules/@ktx/context/dist/tools/context-candidate-write.tool.d.ts +4 -4
  168. package/node_modules/@ktx/context/dist/tools/index.d.ts +1 -0
  169. package/node_modules/@ktx/context/dist/tools/index.js +1 -0
  170. package/node_modules/@ktx/context/dist/wiki/local-knowledge.js +4 -1
  171. package/node_modules/@ktx/context/dist/wiki/local-knowledge.test.js +44 -0
  172. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.js +3 -48
  173. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.test.js +28 -0
  174. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.d.ts +17 -0
  175. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.js +79 -0
  176. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.d.ts +1 -0
  177. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.js +64 -0
  178. package/node_modules/@ktx/context/prompts/memory_agent_bundle_ingest_work_unit.md +23 -4
  179. package/node_modules/@ktx/context/skills/ingest_triage/SKILL.md +7 -3
  180. package/package.json +4 -4
@@ -22,7 +22,7 @@ export interface PrintListColumn<Row> {
22
22
  * - `'suffix'` — trailing em-dash optional value. Default: any column with `optional: true`.
23
23
  */
24
24
  role?: 'name' | 'metric' | 'badge' | 'suffix';
25
- /** Custom pretty-mode value formatter (e.g. score "87%"). Plain/JSON unaffected. */
25
+ /** Custom pretty-mode value formatter (for example, score -> "#1"). Plain/JSON unaffected. */
26
26
  prettyFormat?: (value: Row[keyof Row & string], row: Row) => string;
27
27
  }
28
28
  export interface PrintListArgs<Row> {
@@ -40,3 +40,4 @@ export interface PrintListArgs<Row> {
40
40
  io: KtxCliIo;
41
41
  }
42
42
  export declare function printList<Row extends object>(args: PrintListArgs<Row>): void;
43
+ export declare function createRankBadgeFormatter<Row extends object>(rows: ReadonlyArray<Row>): (_value: Row[keyof Row & string], row: Row) => string;
@@ -15,6 +15,13 @@ export function printList(args) {
15
15
  return;
16
16
  }
17
17
  }
18
+ export function createRankBadgeFormatter(rows) {
19
+ const ranks = new WeakMap();
20
+ rows.forEach((row, index) => {
21
+ ranks.set(row, index + 1);
22
+ });
23
+ return (_value, row) => `#${ranks.get(row) ?? rows.indexOf(row) + 1}`;
24
+ }
18
25
  function isEmpty(value) {
19
26
  return value === undefined || value === null || value === '';
20
27
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { printList } from './print-list.js';
2
+ import { createRankBadgeFormatter, printList } from './print-list.js';
3
3
  import { SYMBOLS } from './symbols.js';
4
4
  function recorder() {
5
5
  let stdout = '';
@@ -209,25 +209,25 @@ describe('printList — pretty mode', () => {
209
209
  expect(out).toContain('(2 pages)');
210
210
  expect(out).toContain('2 pages');
211
211
  });
212
- it('renders a leading badge column with prettyFormat in pretty mode', () => {
212
+ it('renders a leading rank badge column in pretty mode', () => {
213
213
  const r = recorder();
214
+ const rows = [
215
+ { score: 0.87, scope: 'GLOBAL', key: 'alpha', summary: 'first' },
216
+ { score: 0.04, scope: 'GLOBAL', key: 'beta', summary: 'second' },
217
+ ];
214
218
  const SEARCH_COLUMNS = [
215
219
  {
216
220
  key: 'score',
217
221
  label: 'SCORE',
218
222
  plain: 'score=',
219
223
  role: 'badge',
220
- prettyFormat: (v) => `${Math.round(Number(v) * 100)}%`,
224
+ prettyFormat: createRankBadgeFormatter(rows),
221
225
  dim: true,
222
226
  },
223
227
  { key: 'scope', label: 'SCOPE', plain: '' },
224
228
  { key: 'key', label: 'KEY', plain: '' },
225
229
  { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
226
230
  ];
227
- const rows = [
228
- { score: 0.87, scope: 'GLOBAL', key: 'alpha', summary: 'first' },
229
- { score: 0.04, scope: 'GLOBAL', key: 'beta', summary: 'second' },
230
- ];
231
231
  printList({
232
232
  rows,
233
233
  columns: SEARCH_COLUMNS,
@@ -239,18 +239,20 @@ describe('printList — pretty mode', () => {
239
239
  io: r.io,
240
240
  });
241
241
  const out = stripAnsi(r.out());
242
- expect(out).toMatch(/87%\s+alpha\s+/);
243
- expect(out).toMatch(/4%\s+beta\s+/);
242
+ expect(out).toMatch(/#1\s+alpha\s+/);
243
+ expect(out).toMatch(/#2\s+beta\s+/);
244
+ expect(out).not.toContain('%');
244
245
  });
245
246
  it('emits the badge column in plain mode using its plain prefix', () => {
246
247
  const r = recorder();
248
+ const rows = [{ score: 0.87, scope: 'GLOBAL', key: 'alpha', summary: 'first' }];
247
249
  const SEARCH_COLUMNS = [
248
250
  {
249
251
  key: 'score',
250
252
  label: 'SCORE',
251
253
  plain: 'score=',
252
254
  role: 'badge',
253
- prettyFormat: (v) => `${Math.round(Number(v) * 100)}%`,
255
+ prettyFormat: createRankBadgeFormatter(rows),
254
256
  dim: true,
255
257
  },
256
258
  { key: 'scope', label: 'SCOPE', plain: '' },
@@ -258,7 +260,7 @@ describe('printList — pretty mode', () => {
258
260
  { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
259
261
  ];
260
262
  printList({
261
- rows: [{ score: 0.87, scope: 'GLOBAL', key: 'alpha', summary: 'first' }],
263
+ rows,
262
264
  columns: SEARCH_COLUMNS,
263
265
  groupBy: 'scope',
264
266
  mode: 'plain',
@@ -1,6 +1,6 @@
1
1
  export declare const SYMBOLS: {
2
- readonly middot: "·" | "-";
3
- readonly emDash: "" | "--";
2
+ readonly middot: "-" | "·";
3
+ readonly emDash: "--" | "";
4
4
  };
5
5
  export declare function dim(text: string): string;
6
6
  export declare function bold(text: string): string;
@@ -13,6 +13,7 @@ export type KtxKnowledgeArgs = {
13
13
  output?: string;
14
14
  json?: boolean;
15
15
  limit?: number;
16
+ debug?: boolean;
16
17
  };
17
18
  type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
18
19
  interface KtxKnowledgeDeps {
package/dist/knowledge.js CHANGED
@@ -2,25 +2,27 @@ import { createLocalKtxEmbeddingProviderFromConfig, KtxIngestEmbeddingPortAdapte
2
2
  import { loadKtxProject } from '@ktx/context/project';
3
3
  import { listLocalKnowledgePages, searchLocalKnowledgePages, } from '@ktx/context/wiki';
4
4
  import { resolveOutputMode } from './io/mode.js';
5
- import { printList } from './io/print-list.js';
5
+ import { createRankBadgeFormatter, printList } from './io/print-list.js';
6
6
  const WIKI_LIST_COLUMNS = [
7
7
  { key: 'scope', label: 'SCOPE', plain: '' },
8
8
  { key: 'key', label: 'KEY', plain: '' },
9
9
  { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
10
10
  ];
11
- const WIKI_SEARCH_COLUMNS = [
12
- {
13
- key: 'score',
14
- label: 'SCORE',
15
- plain: 'score=',
16
- role: 'badge',
17
- prettyFormat: (value) => `${Math.round(Number(value) * 100)}%`,
18
- dim: true,
19
- },
20
- { key: 'scope', label: 'SCOPE', plain: '' },
21
- { key: 'key', label: 'KEY', plain: '' },
22
- { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
23
- ];
11
+ function wikiSearchColumns(rows) {
12
+ return [
13
+ {
14
+ key: 'score',
15
+ label: 'SCORE',
16
+ plain: 'score=',
17
+ role: 'badge',
18
+ prettyFormat: createRankBadgeFormatter(rows),
19
+ dim: true,
20
+ },
21
+ { key: 'scope', label: 'SCOPE', plain: '' },
22
+ { key: 'key', label: 'KEY', plain: '' },
23
+ { key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
24
+ ];
25
+ }
24
26
  function wikiSearchEmbeddingService(project, deps) {
25
27
  if ('embeddingService' in deps) {
26
28
  return deps.embeddingService ?? null;
@@ -28,6 +30,14 @@ function wikiSearchEmbeddingService(project, deps) {
28
30
  const provider = (deps.createEmbeddingProvider ?? createLocalKtxEmbeddingProviderFromConfig)(project.config.ingest.embeddings);
29
31
  return provider ? new KtxIngestEmbeddingPortAdapter(provider) : null;
30
32
  }
33
+ function writeWikiSearchDebug(io, input) {
34
+ io.stderr.write(`[debug] wiki search mode=${input.mode} embedding=${input.embeddingConfigured ? 'configured' : 'unconfigured'} results=${input.results.length}\n`);
35
+ const lanes = input.results[0]?.lanes ?? [];
36
+ for (const lane of lanes) {
37
+ const reason = lane.reason ? ` reason=${lane.reason}` : '';
38
+ io.stderr.write(`[debug] wiki search lane=${lane.lane} status=${lane.status} returned=${lane.returnedCandidateCount} weight=${lane.weight}${reason}\n`);
39
+ }
40
+ }
31
41
  export async function runKtxKnowledge(args, io = process, deps = {}) {
32
42
  try {
33
43
  const project = await loadKtxProject({ projectDir: args.projectDir });
@@ -48,12 +58,20 @@ export async function runKtxKnowledge(args, io = process, deps = {}) {
48
58
  return 0;
49
59
  }
50
60
  if (args.command === 'search') {
61
+ const embeddingService = wikiSearchEmbeddingService(project, deps);
51
62
  const results = await searchLocalKnowledgePages(project, {
52
63
  query: args.query,
53
64
  userId: args.userId,
54
- embeddingService: wikiSearchEmbeddingService(project, deps),
65
+ embeddingService,
55
66
  limit: args.limit,
56
67
  });
68
+ if (args.debug) {
69
+ writeWikiSearchDebug(io, {
70
+ mode: project.config.storage.search,
71
+ embeddingConfigured: embeddingService !== null,
72
+ results,
73
+ });
74
+ }
57
75
  const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
58
76
  let emptyMessage = `No local wiki pages matched "${args.query}"`;
59
77
  let emptyHint = 'Run `ktx wiki list` to inspect available pages.';
@@ -66,7 +84,7 @@ export async function runKtxKnowledge(args, io = process, deps = {}) {
66
84
  }
67
85
  printList({
68
86
  rows: results,
69
- columns: WIKI_SEARCH_COLUMNS,
87
+ columns: wikiSearchColumns(results),
70
88
  groupBy: 'scope',
71
89
  emptyMessage,
72
90
  emptyHint,
@@ -1,6 +1,7 @@
1
1
  import { mkdtemp, rm } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { stripVTControlCharacters } from 'node:util';
4
5
  import { initKtxProject, loadKtxProject } from '@ktx/context/project';
5
6
  import { writeLocalKnowledgePage } from '@ktx/context/wiki';
6
7
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -67,6 +68,16 @@ describe('runKtxKnowledge', () => {
67
68
  await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local' }, searchIo.io)).resolves.toBe(0);
68
69
  expect(searchIo.stdout()).toContain('metrics-revenue');
69
70
  });
71
+ it('prints wiki search rank badges in pretty output', async () => {
72
+ const projectDir = join(tempDir, 'rank-project');
73
+ await initKtxProject({ projectDir });
74
+ await seedWikiPage(projectDir);
75
+ const searchIo = makeIo();
76
+ await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', output: 'pretty' }, searchIo.io)).resolves.toBe(0);
77
+ const stdout = stripVTControlCharacters(searchIo.stdout());
78
+ expect(stdout).toMatch(/#1\s+metrics-revenue/);
79
+ expect(stdout).not.toContain('%');
80
+ });
70
81
  it('prints wiki list and search as public JSON envelopes', async () => {
71
82
  const projectDir = join(tempDir, 'project');
72
83
  await initKtxProject({ projectDir });
@@ -110,4 +121,20 @@ describe('runKtxKnowledge', () => {
110
121
  expect(searchIo.stdout()).toContain('active-contract-arr-open-tickets');
111
122
  expect(searchIo.stderr()).toBe('');
112
123
  });
124
+ it('writes wiki search lane diagnostics to stderr when debug is enabled', async () => {
125
+ const projectDir = join(tempDir, 'debug-project');
126
+ await initKtxProject({ projectDir });
127
+ await seedWikiPage(projectDir);
128
+ const searchIo = makeIo();
129
+ await expect(runKtxKnowledge({ command: 'search', projectDir, query: 'paid order', userId: 'local', json: true, debug: true }, searchIo.io, { embeddingService: new FakeEmbeddingPort() })).resolves.toBe(0);
130
+ expect(JSON.parse(searchIo.stdout())).toMatchObject({
131
+ kind: 'list',
132
+ data: { items: [expect.objectContaining({ key: 'metrics-revenue' })] },
133
+ meta: { command: 'wiki search' },
134
+ });
135
+ expect(searchIo.stderr()).toContain('[debug] wiki search mode=sqlite-fts5');
136
+ expect(searchIo.stderr()).toContain('embedding=configured');
137
+ expect(searchIo.stderr()).toContain('lane=lexical status=available');
138
+ expect(searchIo.stderr()).toContain('lane=semantic status=available');
139
+ });
113
140
  });
@@ -1,5 +1,6 @@
1
1
  import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
2
2
  import type { KtxCliIo } from './cli-runtime.js';
3
+ import { type KtxCliSpinner } from './clack.js';
3
4
  import { type InstalledKtxRuntimeManifest, type KtxRuntimeFeature, type ManagedPythonRuntimeInstallOptions, type ManagedPythonRuntimeInstallResult, type ManagedPythonRuntimeLayout, type ManagedPythonRuntimeLayoutOptions, type ManagedPythonRuntimeStatus } from './managed-python-runtime.js';
4
5
  export type KtxManagedPythonInstallPolicy = 'prompt' | 'auto' | 'never';
5
6
  export declare function runtimeInstallPolicyFromFlags(options: {
@@ -14,6 +15,7 @@ export interface ManagedPythonCommandDeps {
14
15
  readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
15
16
  installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
16
17
  confirmInstall?: (message: string, io: KtxCliIo) => Promise<boolean>;
18
+ spinner?: () => KtxCliSpinner;
17
19
  }
18
20
  export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps {
19
21
  cliVersion: string;
@@ -1,5 +1,5 @@
1
1
  import { createPythonSemanticLayerComputePort } from '@ktx/context/daemon';
2
- import { createClackPromptAdapter } from './clack.js';
2
+ import { createClackPromptAdapter, createStaticCliSpinner } from './clack.js';
3
3
  import { installManagedPythonRuntime, readManagedPythonRuntimeStatus, } from './managed-python-runtime.js';
4
4
  export function runtimeInstallPolicyFromFlags(options) {
5
5
  if (options.yes === true && options.input === false) {
@@ -50,14 +50,21 @@ export async function ensureManagedPythonCommandRuntime(options) {
50
50
  throw new Error(`KTX Python runtime installation was cancelled. Run: ${managedRuntimeInstallCommand(feature)}`);
51
51
  }
52
52
  }
53
- options.io.stderr.write(`Installing KTX Python runtime (${feature}) with uv...\n`);
54
- const installed = await installRuntime({
55
- cliVersion: options.cliVersion,
56
- features: [feature],
57
- force: false,
58
- });
59
- options.io.stderr.write(`KTX Python runtime ready: ${installed.layout.versionDir}\n`);
60
- return { layout: installed.layout, manifest: installed.manifest };
53
+ const progress = (options.spinner ?? (() => createStaticCliSpinner(options.io)))();
54
+ progress.start(`Installing KTX Python runtime (${feature}) with uv...`);
55
+ try {
56
+ const installed = await installRuntime({
57
+ cliVersion: options.cliVersion,
58
+ features: [feature],
59
+ force: false,
60
+ });
61
+ progress.stop(`KTX Python runtime ready: ${installed.layout.versionDir}`);
62
+ return { layout: installed.layout, manifest: installed.manifest };
63
+ }
64
+ catch (error) {
65
+ progress.error(`KTX Python runtime install failed: ${error instanceof Error ? error.message : String(error)}`);
66
+ throw error;
67
+ }
61
68
  }
62
69
  export async function createManagedPythonSemanticLayerComputePort(options) {
63
70
  const runtime = await ensureManagedPythonCommandRuntime({
@@ -68,6 +75,7 @@ export async function createManagedPythonSemanticLayerComputePort(options) {
68
75
  ...(options.readStatus ? { readStatus: options.readStatus } : {}),
69
76
  ...(options.installRuntime ? { installRuntime: options.installRuntime } : {}),
70
77
  ...(options.confirmInstall ? { confirmInstall: options.confirmInstall } : {}),
78
+ ...(options.spinner ? { spinner: options.spinner } : {}),
71
79
  });
72
80
  const createPythonCompute = options.createPythonCompute ?? createPythonSemanticLayerComputePort;
73
81
  return createPythonCompute({
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { createManagedPythonSemanticLayerComputePort, managedRuntimeInstallCommand, runtimeInstallPolicyFromFlags, } from './managed-python-command.js';
2
+ import { createManagedPythonSemanticLayerComputePort, ensureManagedPythonCommandRuntime, managedRuntimeInstallCommand, runtimeInstallPolicyFromFlags, } from './managed-python-command.js';
3
3
  function makeIo() {
4
4
  let stdout = '';
5
5
  let stderr = '';
@@ -85,6 +85,16 @@ function installResult(features = ['core']) {
85
85
  manifest: installedManifest,
86
86
  };
87
87
  }
88
+ function makeSpinnerEvents() {
89
+ const events = [];
90
+ const spinner = vi.fn(() => ({
91
+ start: (msg) => events.push(`start:${msg}`),
92
+ message: (msg) => events.push(`message:${msg}`),
93
+ stop: (msg) => events.push(`stop:${msg}`),
94
+ error: (msg) => events.push(`error:${msg}`),
95
+ }));
96
+ return { events, spinner };
97
+ }
88
98
  describe('managedRuntimeInstallCommand', () => {
89
99
  it('prints the exact command for each managed runtime feature', () => {
90
100
  expect(managedRuntimeInstallCommand('core')).toBe('ktx dev runtime install --yes');
@@ -103,6 +113,42 @@ describe('runtimeInstallPolicyFromFlags', () => {
103
113
  });
104
114
  });
105
115
  describe('createManagedPythonSemanticLayerComputePort', () => {
116
+ it('uses non-animated runtime setup status by default', async () => {
117
+ const io = makeIo();
118
+ await expect(ensureManagedPythonCommandRuntime({
119
+ cliVersion: '0.2.0',
120
+ installPolicy: 'auto',
121
+ io: io.io,
122
+ readStatus: vi.fn(async () => missingStatus()),
123
+ installRuntime: vi.fn(async () => installResult(['local-embeddings'])),
124
+ feature: 'local-embeddings',
125
+ })).resolves.toMatchObject({
126
+ layout: { versionDir: '/runtime/0.2.0' },
127
+ });
128
+ expect(io.stderr()).toContain('Installing KTX Python runtime (local-embeddings) with uv...');
129
+ expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
130
+ expect(io.stderr().match(/Installing KTX Python runtime/g)).toHaveLength(1);
131
+ });
132
+ it('shows runtime installation progress with the CLI spinner', async () => {
133
+ const io = makeIo();
134
+ const { events, spinner } = makeSpinnerEvents();
135
+ const options = {
136
+ cliVersion: '0.2.0',
137
+ installPolicy: 'auto',
138
+ io: io.io,
139
+ readStatus: vi.fn(async () => missingStatus()),
140
+ installRuntime: vi.fn(async () => installResult(['local-embeddings'])),
141
+ feature: 'local-embeddings',
142
+ spinner,
143
+ };
144
+ await expect(ensureManagedPythonCommandRuntime(options)).resolves.toMatchObject({
145
+ layout: { versionDir: '/runtime/0.2.0' },
146
+ });
147
+ expect(events).toEqual([
148
+ 'start:Installing KTX Python runtime (local-embeddings) with uv...',
149
+ 'stop:KTX Python runtime ready: /runtime/0.2.0',
150
+ ]);
151
+ });
106
152
  it('uses the managed ktx-daemon executable when the runtime is ready', async () => {
107
153
  const io = makeIo();
108
154
  const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
@@ -135,6 +181,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
135
181
  });
136
182
  it('installs the core runtime without prompting when policy is auto', async () => {
137
183
  const io = makeIo();
184
+ const { events, spinner } = makeSpinnerEvents();
138
185
  const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
139
186
  const createPythonCompute = vi.fn(() => compute);
140
187
  const installRuntime = vi.fn(async () => installResult());
@@ -145,17 +192,21 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
145
192
  readStatus: vi.fn(async () => missingStatus()),
146
193
  installRuntime,
147
194
  createPythonCompute,
195
+ spinner,
148
196
  })).resolves.toBe(compute);
149
197
  expect(installRuntime).toHaveBeenCalledWith({
150
198
  cliVersion: '0.2.0',
151
199
  features: ['core'],
152
200
  force: false,
153
201
  });
154
- expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv');
155
- expect(io.stderr()).toContain('KTX Python runtime ready: /runtime/0.2.0');
202
+ expect(events).toEqual([
203
+ 'start:Installing KTX Python runtime (core) with uv...',
204
+ 'stop:KTX Python runtime ready: /runtime/0.2.0',
205
+ ]);
156
206
  });
157
207
  it('prompts before installing when policy is prompt', async () => {
158
208
  const io = makeIo();
209
+ const { events, spinner } = makeSpinnerEvents();
159
210
  const confirmInstall = vi.fn(async () => true);
160
211
  const installRuntime = vi.fn(async () => installResult());
161
212
  await createManagedPythonSemanticLayerComputePort({
@@ -166,6 +217,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
166
217
  installRuntime,
167
218
  createPythonCompute: vi.fn(() => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() })),
168
219
  confirmInstall,
220
+ spinner,
169
221
  });
170
222
  expect(confirmInstall).toHaveBeenCalledWith('KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?', io.io);
171
223
  expect(installRuntime).toHaveBeenCalledWith({
@@ -173,9 +225,11 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
173
225
  features: ['core'],
174
226
  force: false,
175
227
  });
228
+ expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
176
229
  });
177
230
  it('uses injected runtime confirmation instead of reading process TTY directly', async () => {
178
231
  const io = makeIo();
232
+ const { events, spinner } = makeSpinnerEvents();
179
233
  const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
180
234
  const installRuntime = vi.fn(async () => installResult());
181
235
  const confirmInstall = vi.fn(async () => true);
@@ -187,9 +241,10 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
187
241
  installRuntime,
188
242
  confirmInstall,
189
243
  createPythonCompute: () => compute,
244
+ spinner,
190
245
  })).resolves.toBe(compute);
191
246
  expect(confirmInstall).toHaveBeenCalledWith('KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?', io.io);
192
- expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv...');
247
+ expect(events).toContainEqual('start:Installing KTX Python runtime (core) with uv...');
193
248
  });
194
249
  it('can decide default runtime prompting from injected io capabilities', async () => {
195
250
  const io = makeIo();
@@ -29,7 +29,7 @@ function commandLines(commands, indent) {
29
29
  }
30
30
  export function formatNextStepLines(indent = ' ') {
31
31
  return [
32
- `${indent}KTX context is ready for agents. Open your coding agent in this directory and ask a data question.`,
32
+ `${indent}KTX context is ready for agents. Open your coding agent from the KTX project directory and ask a data question.`,
33
33
  `${indent}Verify with:`,
34
34
  ...commandLines(KTX_NEXT_STEP_DIRECT_COMMANDS, indent),
35
35
  ];
@@ -38,8 +38,10 @@ describe('KTX demo next steps', () => {
38
38
  it('explains what the next-step commands are for', () => {
39
39
  const rendered = formatNextStepLines().join('\n');
40
40
  expect(rendered).toContain('KTX context is ready for agents.');
41
+ expect(rendered).toContain('KTX project directory');
41
42
  expect(rendered).toContain('ask a data question');
42
43
  expect(rendered).toContain('Verify with:');
44
+ expect(rendered).not.toContain('this directory');
43
45
  expect(rendered).not.toContain('Preferred route');
44
46
  expect(rendered).not.toContain('Optional MCP:');
45
47
  });
@@ -8,7 +8,13 @@ function silentIo() {
8
8
  };
9
9
  }
10
10
  function stubPackageInfo() {
11
- return { name: '@ktx/cli', version: '0.0.0-docs', contextPackageName: '@ktx/context' };
11
+ return {
12
+ name: '@ktx/cli',
13
+ version: '0.0.0-docs',
14
+ packageVersion: '0.0.0-private',
15
+ runtimeVersion: '0.0.0-docs',
16
+ contextPackageName: '@ktx/context',
17
+ };
12
18
  }
13
19
  export function renderKtxCommandTree() {
14
20
  const program = buildKtxProgram({
@@ -3,7 +3,8 @@ import type { KtxProgressPort } from '@ktx/context/scan';
3
3
  import type { KtxCliIo } from './index.js';
4
4
  import type { KtxIngestArgs, KtxIngestDeps, KtxIngestProgressUpdate } from './ingest.js';
5
5
  import { type KtxDatabaseContextDepth } from './ingest-depth.js';
6
- import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js';
6
+ import { type KtxManagedPythonInstallPolicy, type ManagedPythonCommandRuntime } from './managed-python-command.js';
7
+ import type { KtxRuntimeFeature } from './managed-python-runtime.js';
7
8
  import type { KtxScanArgs, KtxScanDeps } from './scan.js';
8
9
  type KtxPublicIngestStepName = 'database-schema' | 'query-history' | 'source-ingest' | 'memory-update';
9
10
  type KtxPublicIngestStepStatus = 'done' | 'skipped' | 'failed' | 'not-run';
@@ -75,6 +76,13 @@ export interface KtxPublicIngestDeps {
75
76
  }>;
76
77
  scanProgress?: KtxProgressPort;
77
78
  ingestProgress?: (update: KtxIngestProgressUpdate) => void;
79
+ ensureRuntime?: (options: {
80
+ cliVersion: string;
81
+ installPolicy: KtxManagedPythonInstallPolicy;
82
+ io: KtxCliIo;
83
+ feature: KtxRuntimeFeature;
84
+ }) => Promise<ManagedPythonCommandRuntime>;
85
+ env?: NodeJS.ProcessEnv;
78
86
  runtimeIo?: KtxCliIo;
79
87
  onPhaseStart?: (phaseKey: KtxPublicIngestPhaseKey) => void;
80
88
  onPhaseEnd?: (phaseKey: KtxPublicIngestPhaseKey, status: 'done' | 'failed' | 'skipped', summary?: string) => void;
@@ -1,6 +1,8 @@
1
1
  import { loadKtxProject } from '@ktx/context/project';
2
2
  import { databaseContextDepth, deepReadinessGaps, isDatabaseDriver, normalizeConnectionDriver, } from './ingest-depth.js';
3
+ import { ensureManagedPythonCommandRuntime, } from './managed-python-command.js';
3
4
  import { publicIngestOutputLine } from './public-ingest-copy.js';
5
+ import { resolvePublicIngestRuntimeRequirements } from './runtime-requirements.js';
4
6
  import { profileMark } from './startup-profile.js';
5
7
  profileMark('module:public-ingest');
6
8
  const sourceAdapterByDriver = new Map([
@@ -304,6 +306,7 @@ function failureDetailWithRetry(input) {
304
306
  }
305
307
  function markTargetResult(target, args, status, failedOperation, failureDetail) {
306
308
  const selectedFailedOperation = failedOperation ?? (target.operation === 'database-ingest' ? 'database-schema' : 'source-ingest');
309
+ const selectedFailedOperationIndex = target.steps.indexOf(selectedFailedOperation);
307
310
  return {
308
311
  connectionId: target.connectionId,
309
312
  driver: target.driver,
@@ -314,6 +317,10 @@ function markTargetResult(target, args, status, failedOperation, failureDetail)
314
317
  if (status === 'done') {
315
318
  return { ...step, status: 'done' };
316
319
  }
320
+ const stepIndex = target.steps.indexOf(step.operation);
321
+ if (selectedFailedOperationIndex >= 0 && stepIndex >= 0 && stepIndex < selectedFailedOperationIndex) {
322
+ return { ...step, status: 'done' };
323
+ }
317
324
  if (step.operation === selectedFailedOperation) {
318
325
  return {
319
326
  ...step,
@@ -386,15 +393,34 @@ function createCapturedPublicIngestIo() {
386
393
  };
387
394
  }
388
395
  const INTERNAL_STATUS_LINE_RE = /^(Report|Run|Job|Status|Adapter|Connection|Sync|Diff|Tasks|Work units|Failed tasks|Saved memory|Provenance rows):\s*/;
389
- function firstCapturedFailureLine(output) {
390
- return output
396
+ const ACTIONABLE_FAILURE_LINE_RE = /^(Missing bundled Python runtime manifest|KTX Python runtime is required|KTX managed daemon|Error:|Failed\b|Could not\b|Cannot\b)/;
397
+ const RUNTIME_BACKED_RETRY_LINE_RE = /^Then retry the runtime-backed KTX command\.?$/;
398
+ function trimErrorPrefix(line) {
399
+ return line.replace(/^Error:\s*/, '');
400
+ }
401
+ function capturedFailureMessage(output) {
402
+ const lines = output
391
403
  .split(/\r?\n/)
392
404
  .map((line) => line.trim())
393
405
  .filter((line) => line.length > 0)
394
406
  .filter((line) => !line.startsWith('KTX scan completed'))
395
407
  .filter((line) => !INTERNAL_STATUS_LINE_RE.test(line))
396
- .map(publicIngestOutputLine)
397
- .find((line) => line.length > 0);
408
+ .map(publicIngestOutputLine);
409
+ const actionableIndex = lines.findIndex((line) => ACTIONABLE_FAILURE_LINE_RE.test(line));
410
+ if (actionableIndex < 0) {
411
+ const line = lines.find((candidate) => candidate.length > 0);
412
+ return line ? trimErrorPrefix(line) : undefined;
413
+ }
414
+ const firstLine = lines[actionableIndex];
415
+ if (!firstLine?.startsWith('Missing bundled Python runtime manifest')) {
416
+ return trimErrorPrefix(firstLine);
417
+ }
418
+ const followupLines = lines
419
+ .slice(actionableIndex + 1)
420
+ .filter((line) => !RUNTIME_BACKED_RETRY_LINE_RE.test(line))
421
+ .filter((line) => !/\bRetry:\s/.test(line))
422
+ .filter((line) => line.startsWith('In a source checkout, build the local runtime assets with:'));
423
+ return [firstLine, ...followupLines].join('\n');
398
424
  }
399
425
  export async function executePublicIngestTarget(target, args, io, deps) {
400
426
  if (target.preflightFailure) {
@@ -445,7 +471,7 @@ export async function executePublicIngestTarget(target, args, io, deps) {
445
471
  if (target.queryHistory?.enabled === true) {
446
472
  deps.onPhaseEnd?.('query-history', 'skipped');
447
473
  }
448
- return markTargetResult(target, args, 'failed', 'database-schema', capturedScanIo ? firstCapturedFailureLine(capturedScanIo.capturedOutput()) : undefined);
474
+ return markTargetResult(target, args, 'failed', 'database-schema', capturedScanIo ? capturedFailureMessage(capturedScanIo.capturedOutput()) : undefined);
449
475
  }
450
476
  deps.onPhaseEnd?.('database-schema', 'done');
451
477
  if (target.queryHistory?.enabled === true) {
@@ -478,7 +504,7 @@ export async function executePublicIngestTarget(target, args, io, deps) {
478
504
  : await runIngest(ingestArgs, ingestIo);
479
505
  if (qhExitCode !== 0) {
480
506
  deps.onPhaseEnd?.('query-history', 'failed');
481
- return markTargetResult(target, args, 'failed', 'query-history', capturedIngestIo ? firstCapturedFailureLine(capturedIngestIo.capturedOutput()) : undefined);
507
+ return markTargetResult(target, args, 'failed', 'query-history', capturedIngestIo ? capturedFailureMessage(capturedIngestIo.capturedOutput()) : undefined);
482
508
  }
483
509
  deps.onPhaseEnd?.('query-history', 'done');
484
510
  }
@@ -509,12 +535,29 @@ export async function executePublicIngestTarget(target, args, io, deps) {
509
535
  ? await runIngest(ingestArgs, ingestIo, ingestDeps)
510
536
  : await runIngest(ingestArgs, ingestIo);
511
537
  deps.onPhaseEnd?.('source-ingest', exitCode === 0 ? 'done' : 'failed');
512
- return markTargetResult(target, args, exitCode === 0 ? 'done' : 'failed', 'source-ingest', capturedIngestIo ? firstCapturedFailureLine(capturedIngestIo.capturedOutput()) : undefined);
538
+ return markTargetResult(target, args, exitCode === 0 ? 'done' : 'failed', 'source-ingest', capturedIngestIo ? capturedFailureMessage(capturedIngestIo.capturedOutput()) : undefined);
513
539
  }
514
540
  export async function runKtxPublicIngest(args, io, deps = {}) {
515
541
  const loadProject = deps.loadProject ?? loadKtxProject;
516
542
  const project = await loadProject({ projectDir: args.projectDir });
517
543
  if (shouldUseForegroundContextBuildView(args, io)) {
544
+ const plan = buildPublicIngestPlan(project, args);
545
+ const requirements = resolvePublicIngestRuntimeRequirements(plan, { env: deps.env ?? process.env });
546
+ const ensureRuntime = deps.ensureRuntime ?? ensureManagedPythonCommandRuntime;
547
+ for (const feature of requirements.features) {
548
+ try {
549
+ await ensureRuntime({
550
+ cliVersion: args.cliVersion ?? '0.0.0-private',
551
+ installPolicy: args.runtimeInstallPolicy ?? 'prompt',
552
+ io,
553
+ feature,
554
+ });
555
+ }
556
+ catch (error) {
557
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
558
+ return 1;
559
+ }
560
+ }
518
561
  const { runContextBuild } = await import('./context-build-view.js');
519
562
  const contextBuild = deps.runContextBuild ?? runContextBuild;
520
563
  const result = await contextBuild(project, {