@shrkcrft/cli 0.1.0-alpha.13 → 0.1.0-alpha.15

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.
@@ -1 +1 @@
1
- {"version":3,"file":"context.command.d.ts","sourceRoot":"","sources":["../../src/commands/context.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,cAAc,EAAE,eAiI5B,CAAC"}
1
+ {"version":3,"file":"context.command.d.ts","sourceRoot":"","sources":["../../src/commands/context.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAgChC,eAAO,MAAM,cAAc,EAAE,eAyI5B,CAAC"}
@@ -6,10 +6,33 @@ import * as nodePath from 'node:path';
6
6
  import { buildUniversalSearch, explainTaskRouting, recommendCommands, renderOverviewText, buildProjectOverview, } from '@shrkcrft/inspector';
7
7
  import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
8
8
  import { asJson, header } from "../output/format-output.js";
9
+ /**
10
+ * Minimal JSON shape for agent / skill consumption — the context-side mirror
11
+ * of `shrk task --compact`. Drops the heavy `body` and `request` echo and
12
+ * carries the section map + structured action hints (so the agent reads
13
+ * forbiddenActions / verificationCommands / preferredFlow directly instead of
14
+ * regexing the markdown body). The schema marker is distinct so consumers can
15
+ * tell the shapes apart at a glance.
16
+ */
17
+ function minimalContext(task, result, commands) {
18
+ return {
19
+ schema: 'sharkcraft.context/v1-compact',
20
+ task,
21
+ tokens: { used: result.totalTokens, max: result.maxTokens },
22
+ sections: result.sections.map((s) => ({
23
+ title: s.title,
24
+ tokens: s.tokens,
25
+ ...(s.truncated ? { truncated: true } : {}),
26
+ })),
27
+ omittedSections: result.omittedSections,
28
+ actionHints: result.actionHints,
29
+ topCommands: (commands?.recommendations ?? []).slice(0, 5).map((r) => r.command),
30
+ };
31
+ }
9
32
  export const contextCommand = {
10
33
  name: 'context',
11
34
  description: 'Build relevant AI-ready context for a task (token-budgeted). Subcommands: build / refresh / status.',
12
- usage: 'shrk context [build|refresh|status] --task "<task>" [--max-tokens 3000] [--framework x] [--area y] [--json]',
35
+ usage: 'shrk context [build|refresh|status] --task "<task>" [--max-tokens 3000] [--framework x] [--area y] [--json] [--compact] [--full]',
13
36
  async run(args) {
14
37
  // Dispatch subcommands (build / refresh / status) based on first positional.
15
38
  const sub = args.positional[0];
@@ -79,6 +102,14 @@ export const contextCommand = {
79
102
  // ignore — fall back to legacy context only.
80
103
  }
81
104
  if (flagBool(args, 'json') || flagBool(args, 'machine-json')) {
105
+ // `--compact` emits a minimal, structured agent shape (no long body /
106
+ // request echo) — the context-side mirror of `shrk task --compact`.
107
+ // Carries the load-bearing action hints as structured data so the agent
108
+ // never has to parse the markdown body. Full shape stays the default.
109
+ if (flagBool(args, 'compact')) {
110
+ process.stdout.write(asJson(minimalContext(task, result, commandRecommendations)) + '\n');
111
+ return 0;
112
+ }
82
113
  process.stdout.write(asJson({
83
114
  ...result,
84
115
  commands: commandRecommendations,
@@ -1 +1 @@
1
- {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AAuBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAyDhC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,eA+JjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAmF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAiMF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC"}
1
+ {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AAyBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAyDhC;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,eA6LjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAmF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import * as nodePath from 'node:path';
4
- import { AiMessageRole, buildPromptMessages, EnhancementPipeline, EnhancementStageKind, OllamaProvider, buildDefaultEnhancementStages, selectAiProvider, } from '@shrkcrft/ai';
4
+ import * as os from 'node:os';
5
+ import { AiMessageRole, buildPromptMessages, EnhancementPipeline, EnhancementStageKind, OllamaProvider, buildDefaultEnhancementStages, buildFastEnhancementStages, selectAiProvider, } from '@shrkcrft/ai';
5
6
  import { buildContext } from '@shrkcrft/context';
6
7
  import { EdgeKind, GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
7
8
  import { buildProjectOverview, buildTaskPacket, inspectSharkcraft, renderOverviewText, } from '@shrkcrft/inspector';
@@ -42,13 +43,28 @@ const SMART_CONTEXT_DIR = nodePath.join('.sharkcraft', 'smart-context');
42
43
  export const smartContextCommand = {
43
44
  name: 'smart-context',
44
45
  description: 'Build deterministic context and ask an AI provider to synthesise an enriched brief (default), structured plan (--plan), or two-stage development plan (--ai-plan).',
45
- usage: 'shrk smart-context "<task>" [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
46
+ usage: 'shrk smart-context "<task>" [--plus] [--budget <seconds>] [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
46
47
  async run(args) {
47
48
  const task = args.positional.join(' ').trim();
48
49
  if (!task) {
49
50
  process.stderr.write('Usage: shrk smart-context "<task>" [--plan] [--ai-plan] [--save]\n');
50
51
  return 2;
51
52
  }
53
+ // Isolate the LLM / native-runtime work in a child process. On macOS the
54
+ // node-llama-cpp (ggml/Metal) and ONNX static destructors abort during
55
+ // `exit()` — surfacing a GGML backtrace + `libc++abi … mutex lock failed`
56
+ // (and a shell `abort`) AFTER a perfectly good result. There is no JS hook
57
+ // in this Node build to skip libc++ finalizers, so instead the child
58
+ // self-contains that noise (fd 2 → log on exit) and hands its real exit
59
+ // code back through a sentinel file; the parent never loads a native
60
+ // runtime, so it exits cleanly with the correct code. Dry-run does no
61
+ // native work, so it stays in-process. Gated on SHRK_CLI so a unit test
62
+ // calling `run()` in-process never spawns a subprocess.
63
+ if (process.env.SHRK_CLI === '1' &&
64
+ process.env.SHRK_SMART_CONTEXT_WORKER !== '1' &&
65
+ !flagBool(args, 'dry-run')) {
66
+ return runSmartContextInChild();
67
+ }
52
68
  const cwd = resolveCwd(args);
53
69
  const opts = readCommonOptions(args);
54
70
  const inspection = await inspectSharkcraft({ cwd });
@@ -125,6 +141,15 @@ export const smartContextCommand = {
125
141
  process.stderr.write(`[smart-context] conversation saved → ${path}\n`);
126
142
  }
127
143
  }
144
+ const enh = enhanced.value.enhancement;
145
+ if (!opts.json && !enh.deterministicFallback) {
146
+ if (enh.budgetExhausted) {
147
+ process.stderr.write(`[smart-context] budget reached before all ${enh.plannedPasses} passes finished — output is the best so far. Try a smaller --model or raise --budget.\n`);
148
+ }
149
+ if (!enh.plus) {
150
+ process.stderr.write(`[smart-context] fast ${enh.plannedPasses}-pass enhancement. Pass --plus for the full draft→critique→refine→polish (denser, slower).\n`);
151
+ }
152
+ }
128
153
  const envelope = buildEnvelope({
129
154
  task,
130
155
  seed,
@@ -1040,6 +1065,63 @@ function isEmbeddingsCleanupNoise(line) {
1040
1065
  * already set the kernel-visible exit code before the abort. We
1041
1066
  * surface that code verbatim.
1042
1067
  */
1068
+ /**
1069
+ * Run a smart-context brief/plan in an isolated child and return its real exit
1070
+ * code. stdio is inherited so progress + result flow straight to the user's
1071
+ * terminal; the child redirects fd 2 to a log file before its native teardown
1072
+ * abort, so no backtrace reaches the console. The child writes its true exit
1073
+ * code to a sentinel file (read back here) because the SIGABRT during teardown
1074
+ * would otherwise clobber it with 134. The parent loads no native runtime, so
1075
+ * it exits cleanly — no `zsh: abort`, correct code.
1076
+ */
1077
+ function runSmartContextInChild() {
1078
+ return new Promise((resolve) => {
1079
+ const exitFile = nodePath.join(os.tmpdir(), `shrk-sc-exit-${process.pid}-${Date.now()}.code`);
1080
+ const child = spawn(process.execPath, process.argv.slice(1), {
1081
+ env: {
1082
+ ...process.env,
1083
+ SHRK_SMART_CONTEXT_WORKER: '1',
1084
+ SHRK_WORKER_EXITCODE_FILE: exitFile,
1085
+ },
1086
+ stdio: 'inherit',
1087
+ });
1088
+ child.on('error', (err) => {
1089
+ process.stderr.write(`Failed to spawn smart-context worker: ${err.message}\n`);
1090
+ resolve(1);
1091
+ });
1092
+ child.on('close', (code, signal) => {
1093
+ // Prefer the sentinel — the worker writes its true exit code before the
1094
+ // native teardown can abort the process.
1095
+ let real = null;
1096
+ try {
1097
+ if (existsSync(exitFile)) {
1098
+ const raw = readFileSync(exitFile, 'utf8').trim();
1099
+ if (raw.length > 0 && Number.isFinite(Number(raw)))
1100
+ real = Number(raw);
1101
+ try {
1102
+ unlinkSync(exitFile);
1103
+ }
1104
+ catch {
1105
+ // best-effort cleanup
1106
+ }
1107
+ }
1108
+ }
1109
+ catch {
1110
+ // fall through to the signal/code-based result below
1111
+ }
1112
+ if (real !== null) {
1113
+ resolve(real);
1114
+ return;
1115
+ }
1116
+ // No sentinel (worker crashed mid-run, not during teardown) → surface a
1117
+ // failure rather than masking it. SIGABRT with no sentinel ⇒ non-zero.
1118
+ if (typeof code === 'number')
1119
+ resolve(code);
1120
+ else
1121
+ resolve(signal ? 1 : 0);
1122
+ });
1123
+ });
1124
+ }
1043
1125
  function runEmbeddingsBuildInChild() {
1044
1126
  return new Promise((resolve) => {
1045
1127
  const child = spawn(process.execPath, process.argv.slice(1), {
@@ -1269,6 +1351,10 @@ function readCommonOptions(args) {
1269
1351
  stream: flagBool(args, 'stream'),
1270
1352
  enhance: resolveEnhanceFlag(args),
1271
1353
  enhancePasses: flagNumber(args, 'enhance-passes') ?? readEnhancePassesEnv(),
1354
+ plus: flagBool(args, 'plus'),
1355
+ ...(flagNumber(args, 'budget') !== undefined
1356
+ ? { budgetMs: Math.max(1, flagNumber(args, 'budget')) * 1000 }
1357
+ : {}),
1272
1358
  logPrompt: flagBool(args, 'log-prompt'),
1273
1359
  saveConversation: flagBool(args, 'save-conversation') || flagString(args, 'save-conversation') !== undefined,
1274
1360
  ...(flagString(args, 'save-conversation')
@@ -2514,13 +2600,29 @@ function truncateLine(text, max) {
2514
2600
  * system body verbatim across stages so the model never loses
2515
2601
  * grounding; only the user turn changes per stage.
2516
2602
  */
2603
+ /**
2604
+ * Wall-clock ceilings for the enhancement pipeline. These are anti-hang
2605
+ * guards, not target runtimes — the speed win comes from running fewer passes
2606
+ * by default and from picking a smaller `--model`. A slow model that overruns
2607
+ * degrades to the best output so far (or the deterministic seed). Override per
2608
+ * invocation with `--budget <seconds>`.
2609
+ */
2610
+ const PER_STAGE_TIMEOUT_MS = 90_000;
2611
+ const FAST_ENHANCE_BUDGET_MS = 150_000;
2612
+ const PLUS_ENHANCE_BUDGET_MS = 360_000;
2517
2613
  async function runEnhancementPipeline(input) {
2518
2614
  const provider = input.provider;
2519
2615
  const systemMsg = input.messages.find((m) => m.role === AiMessageRole.System);
2520
2616
  const userMsg = input.messages.find((m) => m.role === AiMessageRole.User);
2521
2617
  const originalContext = systemMsg?.content ?? '';
2522
2618
  const taskBody = userMsg?.content ?? input.seed.task;
2523
- const pipeline = new EnhancementPipeline(buildDefaultEnhancementStages());
2619
+ // Default is the fast 2-pass draft→polish; `--plus` opts into the full
2620
+ // draft→critique→refine→polish for denser output. Both are wall-clock
2621
+ // bounded so a slow local model degrades gracefully instead of hanging.
2622
+ const plus = input.options.plus;
2623
+ const stages = plus ? buildDefaultEnhancementStages() : buildFastEnhancementStages();
2624
+ const budgetMs = input.options.budgetMs ?? (plus ? PLUS_ENHANCE_BUDGET_MS : FAST_ENHANCE_BUDGET_MS);
2625
+ const pipeline = new EnhancementPipeline(stages);
2524
2626
  const stageInputs = [];
2525
2627
  const stageResponses = [];
2526
2628
  // Tee per-stage prompts/responses so we can rebuild the conversation
@@ -2540,6 +2642,8 @@ async function runEnhancementPipeline(input) {
2540
2642
  const piRun = await pipeline.run({ task: taskBody, originalContext }, recordingProvider, {
2541
2643
  ...(input.options.enhancePasses ? { maxPasses: input.options.enhancePasses } : {}),
2542
2644
  maxTokensPerStage: input.options.maxTokens,
2645
+ budgetMs,
2646
+ perStageTimeoutMs: PER_STAGE_TIMEOUT_MS,
2543
2647
  ...(input.options.model ? { model: input.options.model } : {}),
2544
2648
  onStage: (e) => {
2545
2649
  if (!input.options.json) {
@@ -2607,6 +2711,9 @@ async function runEnhancementPipeline(input) {
2607
2711
  })),
2608
2712
  totalUsage: piRun.value.totalUsage,
2609
2713
  deterministicFallback: piRun.value.deterministicFallback,
2714
+ budgetExhausted: piRun.value.budgetExhausted,
2715
+ plannedPasses: stages.length,
2716
+ plus,
2610
2717
  },
2611
2718
  turns,
2612
2719
  },
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-api-server.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-api-server.ts"],"names":[],"mappings":"AA4DA,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2BD,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC1F;AA2jBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"dashboard-api-server.d.ts","sourceRoot":"","sources":["../../src/dashboard/dashboard-api-server.ts"],"names":[],"mappings":"AAiEA,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,UAAU,aAAa;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2BD,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAqC1F;AAqlBD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
@@ -15,9 +15,10 @@ import * as http from 'node:http';
15
15
  import * as fs from 'node:fs';
16
16
  import { existsSync, readFileSync, statSync } from 'node:fs';
17
17
  import * as nodePath from 'node:path';
18
- import { buildDashboardAdoption, buildDashboardArchitecture, buildDashboardBoundaries, buildDashboardCapabilities, buildDashboardCommands, buildDashboardCoverage, buildDashboardDoctor, buildDashboardDrift, buildDashboardGraph, buildDashboardGraphNode, buildDashboardGraphPath, buildDashboardHealth, buildDashboardMcpSummary, buildDashboardOnboarding, buildDashboardOverview, buildDashboardPacks, buildDashboardPipelines, buildDashboardPresets, buildDashboardQuality, buildDashboardReports, buildDashboardReview, buildDashboardSafety, buildDashboardScaffolds, buildDashboardSchemas, buildDashboardSessionDetail, buildDashboardSessions, buildDashboardStats, inspectSharkcraft, renderDevSessionHtml, scanDevSession, } from '@shrkcrft/inspector';
18
+ import { buildDashboardAdoption, buildDashboardArchitecture, buildDashboardBoundaries, buildDashboardCapabilities, buildDashboardCommands, buildDashboardCoverage, buildDashboardDoctor, buildDashboardDrift, buildDashboardGraph, buildDashboardGraphNode, buildDashboardGraphPath, buildDashboardHealth, buildDashboardKnowledgeList, buildDashboardKnowledgeEntry, buildDashboardKnowledgeGraph, buildDashboardKnowledgeSimilar, buildDashboardMcpSummary, buildDashboardOnboarding, buildDashboardOverview, buildDashboardPacks, buildDashboardPipelines, buildDashboardPresets, buildDashboardQuality, buildDashboardReports, buildDashboardReview, buildDashboardSafety, buildDashboardScaffolds, buildDashboardSchemas, buildDashboardSessionDetail, buildDashboardSessions, buildDashboardStats, inspectSharkcraft, renderDevSessionHtml, scanDevSession, } from '@shrkcrft/inspector';
19
19
  import { COMMAND_CATALOG } from "../commands/command-catalog.js";
20
20
  import { buildDashboardCodeIntelligence, buildDashboardMigrations, buildDashboardQualityGates, buildDashboardRoutes, } from "./code-intelligence-data.js";
21
+ import { buildKnowledgeAsk } from "./knowledge-ask.js";
21
22
  const SCHEMA_ID = 'sharkcraft.dashboard-api/v1';
22
23
  export async function startDashboardApiServer(opts) {
23
24
  const host = opts.host ?? '127.0.0.1';
@@ -324,6 +325,7 @@ async function handle(req, res, ctx) {
324
325
  path.startsWith('/api/pipelines') ||
325
326
  path.startsWith('/api/architecture') ||
326
327
  path.startsWith('/api/graph') ||
328
+ path.startsWith('/api/knowledge') ||
327
329
  path.startsWith('/api/onboarding') ||
328
330
  path.startsWith('/api/review') ||
329
331
  path.startsWith('/api/scaffolds');
@@ -377,6 +379,32 @@ async function handle(req, res, ctx) {
377
379
  return respondError(res, 400, 'bad-request', 'from and to query params required');
378
380
  return respond(res, buildEnvelope(projectRoot, buildDashboardGraphPath(inspection, from, to)));
379
381
  }
382
+ // Knowledge explorer. Specific sub-paths (ask / graph / entry) are matched
383
+ // before the bare list route. The ask route synthesizes a grounded answer
384
+ // with the local LLM — still a read: it never writes, and is wall-clock
385
+ // bounded so it cannot hang the server.
386
+ if (path === '/api/knowledge/ask') {
387
+ const q = url.searchParams.get('q');
388
+ if (!q || !q.trim())
389
+ return respondError(res, 400, 'bad-request', 'q query param required');
390
+ return respond(res, buildEnvelope(projectRoot, await buildKnowledgeAsk(inspection, q)));
391
+ }
392
+ if (path === '/api/knowledge/graph') {
393
+ return respond(res, buildEnvelope(projectRoot, buildDashboardKnowledgeGraph(inspection)));
394
+ }
395
+ const knowledgeSimilarMatch = path.match(/^\/api\/knowledge\/similar\/(.+)$/);
396
+ if (knowledgeSimilarMatch) {
397
+ const id = decodeURIComponent(knowledgeSimilarMatch[1]);
398
+ return respond(res, buildEnvelope(projectRoot, buildDashboardKnowledgeSimilar(inspection, id)));
399
+ }
400
+ const knowledgeEntryMatch = path.match(/^\/api\/knowledge\/entry\/(.+)$/);
401
+ if (knowledgeEntryMatch) {
402
+ const id = decodeURIComponent(knowledgeEntryMatch[1]);
403
+ return respond(res, buildEnvelope(projectRoot, buildDashboardKnowledgeEntry(inspection, id)));
404
+ }
405
+ if (path === '/api/knowledge') {
406
+ return respond(res, buildEnvelope(projectRoot, buildDashboardKnowledgeList(inspection)));
407
+ }
380
408
  if (path === '/api/onboarding') {
381
409
  return respond(res, buildEnvelope(projectRoot, buildDashboardOnboarding(inspection)));
382
410
  }
@@ -0,0 +1,4 @@
1
+ import type { ISharkcraftInspection } from '@shrkcrft/inspector';
2
+ import type { IDashboardKnowledgeAskResponse } from '@shrkcrft/dashboard-api';
3
+ export declare function buildKnowledgeAsk(inspection: ISharkcraftInspection, question: string): Promise<IDashboardKnowledgeAskResponse>;
4
+ //# sourceMappingURL=knowledge-ask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-ask.d.ts","sourceRoot":"","sources":["../../src/dashboard/knowledge-ask.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,8BAA8B,EAE/B,MAAM,yBAAyB,CAAC;AAwCjC,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,qBAAqB,EACjC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,8BAA8B,CAAC,CA0EzC"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Read-only "ask a question over the knowledge base" for the dashboard.
3
+ *
4
+ * Deterministic retrieval (searchKnowledge) is the source of truth and ALWAYS
5
+ * populates `sources`. The local LLM only synthesizes a prose `answer` grounded
6
+ * in those entries. When no LLM is reachable, or it times out / errors, the
7
+ * response degrades gracefully to the retrieved entries — the GET handler never
8
+ * hangs and never writes anything.
9
+ */
10
+ import { ERROR_CODES } from '@shrkcrft/core';
11
+ import { AiMessageRole, selectAiProvider } from '@shrkcrft/ai';
12
+ import { searchKnowledge } from '@shrkcrft/knowledge';
13
+ /** Hard wall-clock bound so a slow local model can't hang a dashboard GET. */
14
+ const ASK_TIMEOUT_MS = 15_000;
15
+ /** How many entries to retrieve and feed as grounding. */
16
+ const RETRIEVE_LIMIT = 8;
17
+ /** Per-entry grounding budget (chars) — keeps the prompt small for local models. */
18
+ const ENTRY_CHARS = 1100;
19
+ const SYSTEM_PROMPT = [
20
+ 'You are the SharkCraft knowledge assistant for a single repository.',
21
+ 'Answer the question USING ONLY the knowledge entries provided below as context.',
22
+ 'Every entry is delimited and prefixed with its id in [brackets].',
23
+ 'Cite the entries you used by writing their id in [brackets] inline.',
24
+ 'If the answer is not present in the provided entries, say so plainly — do not invent.',
25
+ 'Be concise: a few short paragraphs or bullets. No preamble, no sign-off.',
26
+ ].join('\n');
27
+ function roundScore(n) {
28
+ return Math.round(n * 100) / 100;
29
+ }
30
+ function buildMessages(question, grounding) {
31
+ return [
32
+ { role: AiMessageRole.System, content: SYSTEM_PROMPT },
33
+ {
34
+ role: AiMessageRole.User,
35
+ content: [
36
+ '# Knowledge entries',
37
+ grounding,
38
+ '',
39
+ '# Question',
40
+ question.trim(),
41
+ '',
42
+ 'Answer now, citing entry ids in [brackets].',
43
+ ].join('\n'),
44
+ },
45
+ ];
46
+ }
47
+ export async function buildKnowledgeAsk(inspection, question) {
48
+ const startedAt = Date.now();
49
+ const trimmed = question.trim();
50
+ // 1. Deterministic retrieval — always available, the engine's ground truth.
51
+ const results = searchKnowledge(inspection.knowledgeEntries, {
52
+ query: trimmed,
53
+ limit: RETRIEVE_LIMIT,
54
+ });
55
+ const sources = results.map((r) => ({
56
+ id: r.entry.id,
57
+ title: r.entry.title,
58
+ type: String(r.entry.type),
59
+ score: roundScore(r.score),
60
+ }));
61
+ const degrade = (note, provider) => ({
62
+ question: trimmed,
63
+ llmAvailable: false,
64
+ ...(provider ? { provider } : {}),
65
+ answer: null,
66
+ degraded: true,
67
+ note,
68
+ sources,
69
+ citedEntryIds: [],
70
+ durationMs: Date.now() - startedAt,
71
+ });
72
+ // 2. Provider selection — local-only, may be absent.
73
+ const selection = selectAiProvider(process.env.AI_PROVIDER);
74
+ if (!selection.provider) {
75
+ return degrade('No local LLM is reachable, so this is the deterministic top-matches view. Start an Ollama daemon or set LLAMACPP_MODEL_PATH to enable synthesized answers.');
76
+ }
77
+ if (results.length === 0) {
78
+ return degrade('No knowledge entries matched the question.', selection.provider.id);
79
+ }
80
+ // 3. Grounded prompt from the retrieved entries.
81
+ const grounding = results
82
+ .map((r) => {
83
+ const body = (r.entry.summary ?? r.entry.content).slice(0, ENTRY_CHARS);
84
+ return `### [${r.entry.id}] ${r.entry.title}\n${body}`;
85
+ })
86
+ .join('\n\n');
87
+ // 4. Bounded LLM call; any failure degrades to retrieval-only.
88
+ const res = await selection.provider.send({
89
+ messages: buildMessages(trimmed, grounding),
90
+ maxTokens: 1024,
91
+ temperature: 0.2,
92
+ timeoutMs: ASK_TIMEOUT_MS,
93
+ });
94
+ if (!res.ok) {
95
+ const note = res.error.code === ERROR_CODES.TIMEOUT
96
+ ? 'The local LLM timed out — showing the deterministic top matches instead.'
97
+ : `The local LLM could not answer (${res.error.message}) — showing the deterministic top matches instead.`;
98
+ return degrade(note, selection.provider.id);
99
+ }
100
+ const answer = res.value.content.trim();
101
+ const citedEntryIds = sources.map((s) => s.id).filter((id) => answer.includes(id));
102
+ return {
103
+ question: trimmed,
104
+ llmAvailable: true,
105
+ provider: selection.provider.id,
106
+ answer,
107
+ degraded: false,
108
+ sources,
109
+ citedEntryIds,
110
+ durationMs: Date.now() - startedAt,
111
+ };
112
+ }
package/dist/main.js CHANGED
@@ -816,6 +816,39 @@ function printDidYouMean(attempted) {
816
816
  if (footer)
817
817
  process.stderr.write(renderErrorFooter(footer));
818
818
  }
819
+ /**
820
+ * Point fd 2 (stderr) at a log file so native-runtime teardown noise written
821
+ * during process exit lands in a file instead of the user's terminal. Returns
822
+ * silently on any failure (the worst case is the pre-existing noisy stderr).
823
+ *
824
+ * The log path can be overridden with `SHRK_NATIVE_TEARDOWN_LOG`; default is
825
+ * `<tmpdir>/shrk-native-teardown.log`. We append, with a timestamped header,
826
+ * so the trace is recoverable for debugging without ever touching the console.
827
+ */
828
+ async function redirectStderrToTeardownLog() {
829
+ try {
830
+ const fs = await import('node:fs');
831
+ const os = await import('node:os');
832
+ const path = await import('node:path');
833
+ const logPath = process.env.SHRK_NATIVE_TEARDOWN_LOG?.trim() ||
834
+ path.join(os.tmpdir(), 'shrk-native-teardown.log');
835
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
836
+ // Close fd 2; the next open() reclaims the lowest free descriptor (2),
837
+ // so all subsequent stderr — including native C++ writes during
838
+ // `__cxa_finalize` — flows to the log file.
839
+ fs.closeSync(2);
840
+ const fd = fs.openSync(logPath, 'a');
841
+ if (fd !== 2) {
842
+ // Couldn't reclaim fd 2 — leave things as they are rather than risk
843
+ // writing the result to the wrong descriptor.
844
+ return;
845
+ }
846
+ fs.writeSync(2, `\n--- shrk native-runtime teardown @ ${new Date().toISOString()} ---\n`);
847
+ }
848
+ catch {
849
+ // Best-effort containment; never let log redirection break the exit.
850
+ }
851
+ }
819
852
  // Entry point when invoked directly.
820
853
  //
821
854
  // Bun exposes `import.meta.main`; Node does not. When Node runs the
@@ -830,6 +863,10 @@ if (isMain ||
830
863
  entryPath.endsWith('shrk') ||
831
864
  entryPath.endsWith('shrk.js') ||
832
865
  entryPath.endsWith('shrk.cmd')) {
866
+ // Marks a real CLI invocation (vs. a command handler imported directly by a
867
+ // test). Commands that re-exec themselves in an isolated child gate on this
868
+ // so unit tests calling `run()` in-process never spawn a subprocess.
869
+ process.env.SHRK_CLI = '1';
833
870
  loadDotenv(process.cwd());
834
871
  const argv = process.argv.slice(2);
835
872
  const cleanShutdown = async (code) => {
@@ -839,10 +876,16 @@ if (isMain ||
839
876
  // work completed — the user sees their result then `zsh: abort`.
840
877
  // Dynamic imports keep these off the hot path for commands that
841
878
  // never touched them.
879
+ // Track whether any native runtime (ONNX via embeddings, Metal/ggml via
880
+ // node-llama-cpp) was actually loaded this run. If so, its static
881
+ // destructors can still abort with a backtrace during `exit()` below —
882
+ // and there is no JS hook in this Node version to skip libc++ finalizers.
883
+ // We contain that by redirecting fd 2 to a log file just before exit.
884
+ let nativeRuntimeLoaded = false;
842
885
  try {
843
886
  const mod = (await import('@shrkcrft/embeddings'));
844
887
  if (typeof mod.disposeSemanticIndexPipeline === 'function') {
845
- await mod.disposeSemanticIndexPipeline();
888
+ nativeRuntimeLoaded = (await mod.disposeSemanticIndexPipeline()) || nativeRuntimeLoaded;
846
889
  }
847
890
  }
848
891
  catch {
@@ -851,7 +894,7 @@ if (isMain ||
851
894
  try {
852
895
  const mod = (await import('@shrkcrft/ai'));
853
896
  if (typeof mod.disposeLlamaCppRuntime === 'function') {
854
- await mod.disposeLlamaCppRuntime();
897
+ nativeRuntimeLoaded = (await mod.disposeLlamaCppRuntime()) || nativeRuntimeLoaded;
855
898
  }
856
899
  }
857
900
  catch {
@@ -869,6 +912,29 @@ if (isMain ||
869
912
  catch {
870
913
  // ignore flush failures
871
914
  }
915
+ // When running as an isolated worker (e.g. the smart-context child), hand
916
+ // the real exit code back to the parent via a sentinel file. The native
917
+ // teardown abort below would otherwise clobber it with SIGABRT (134).
918
+ const exitCodeFile = process.env.SHRK_WORKER_EXITCODE_FILE;
919
+ if (exitCodeFile) {
920
+ try {
921
+ const fs = await import('node:fs');
922
+ fs.writeFileSync(exitCodeFile, String(code), 'utf8');
923
+ }
924
+ catch {
925
+ // best-effort; parent falls back to the child's signal/code.
926
+ }
927
+ }
928
+ // Contain native-runtime teardown noise. The ggml/ONNX destructors write a
929
+ // backtrace + `libc++abi: terminating … mutex lock failed` straight to fd 2
930
+ // during `exit()`, AFTER our real output is already on screen. That bypasses
931
+ // any JS stream wrapper, so the only reliable way to keep it off the user's
932
+ // terminal is to point fd 2 at a log file first: close(2) frees the lowest
933
+ // fd, and the next open() reclaims it. Gated on `nativeRuntimeLoaded` so
934
+ // ordinary commands keep their stderr untouched.
935
+ if (nativeRuntimeLoaded) {
936
+ await redirectStderrToTeardownLog();
937
+ }
872
938
  // Prefer a low-level exit over `process.exit` on Node. Without
873
939
  // this, libc++ static destructors run during `process.exit`, and
874
940
  // native bindings still resident in memory abort with libc++abi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/cli",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.15",
4
4
  "description": "SharkCraft CLI (`shrk`): structured project intelligence for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -47,37 +47,37 @@
47
47
  "typecheck": "tsc --noEmit -p tsconfig.json"
48
48
  },
49
49
  "dependencies": {
50
- "@shrkcrft/core": "^0.1.0-alpha.13",
51
- "@shrkcrft/config": "^0.1.0-alpha.13",
52
- "@shrkcrft/workspace": "^0.1.0-alpha.13",
53
- "@shrkcrft/knowledge": "^0.1.0-alpha.13",
54
- "@shrkcrft/context": "^0.1.0-alpha.13",
55
- "@shrkcrft/rules": "^0.1.0-alpha.13",
56
- "@shrkcrft/paths": "^0.1.0-alpha.13",
57
- "@shrkcrft/templates": "^0.1.0-alpha.13",
58
- "@shrkcrft/plugin-api": "^0.1.0-alpha.13",
59
- "@shrkcrft/dashboard": "^0.1.0-alpha.13",
60
- "@shrkcrft/dashboard-api": "^0.1.0-alpha.13",
61
- "@shrkcrft/pipelines": "^0.1.0-alpha.13",
62
- "@shrkcrft/presets": "^0.1.0-alpha.13",
63
- "@shrkcrft/boundaries": "^0.1.0-alpha.13",
64
- "@shrkcrft/graph": "^0.1.0-alpha.13",
65
- "@shrkcrft/rule-graph": "^0.1.0-alpha.13",
66
- "@shrkcrft/structural-search": "^0.1.0-alpha.13",
67
- "@shrkcrft/impact-engine": "^0.1.0-alpha.13",
68
- "@shrkcrft/context-planner": "^0.1.0-alpha.13",
69
- "@shrkcrft/architecture-guard": "^0.1.0-alpha.13",
70
- "@shrkcrft/framework-scanners": "^0.1.0-alpha.13",
71
- "@shrkcrft/api-surface-diff": "^0.1.0-alpha.13",
72
- "@shrkcrft/quality-gates": "^0.1.0-alpha.13",
73
- "@shrkcrft/migrate": "^0.1.0-alpha.13",
74
- "@shrkcrft/generator": "^0.1.0-alpha.13",
75
- "@shrkcrft/importer": "^0.1.0-alpha.13",
76
- "@shrkcrft/inspector": "^0.1.0-alpha.13",
77
- "@shrkcrft/ai": "^0.1.0-alpha.13",
78
- "@shrkcrft/embeddings": "^0.1.0-alpha.13",
79
- "@shrkcrft/shared": "^0.1.0-alpha.13",
80
- "@shrkcrft/mcp-server": "^0.1.0-alpha.13",
50
+ "@shrkcrft/core": "^0.1.0-alpha.15",
51
+ "@shrkcrft/config": "^0.1.0-alpha.15",
52
+ "@shrkcrft/workspace": "^0.1.0-alpha.15",
53
+ "@shrkcrft/knowledge": "^0.1.0-alpha.15",
54
+ "@shrkcrft/context": "^0.1.0-alpha.15",
55
+ "@shrkcrft/rules": "^0.1.0-alpha.15",
56
+ "@shrkcrft/paths": "^0.1.0-alpha.15",
57
+ "@shrkcrft/templates": "^0.1.0-alpha.15",
58
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.15",
59
+ "@shrkcrft/dashboard": "^0.1.0-alpha.15",
60
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.15",
61
+ "@shrkcrft/pipelines": "^0.1.0-alpha.15",
62
+ "@shrkcrft/presets": "^0.1.0-alpha.15",
63
+ "@shrkcrft/boundaries": "^0.1.0-alpha.15",
64
+ "@shrkcrft/graph": "^0.1.0-alpha.15",
65
+ "@shrkcrft/rule-graph": "^0.1.0-alpha.15",
66
+ "@shrkcrft/structural-search": "^0.1.0-alpha.15",
67
+ "@shrkcrft/impact-engine": "^0.1.0-alpha.15",
68
+ "@shrkcrft/context-planner": "^0.1.0-alpha.15",
69
+ "@shrkcrft/architecture-guard": "^0.1.0-alpha.15",
70
+ "@shrkcrft/framework-scanners": "^0.1.0-alpha.15",
71
+ "@shrkcrft/api-surface-diff": "^0.1.0-alpha.15",
72
+ "@shrkcrft/quality-gates": "^0.1.0-alpha.15",
73
+ "@shrkcrft/migrate": "^0.1.0-alpha.15",
74
+ "@shrkcrft/generator": "^0.1.0-alpha.15",
75
+ "@shrkcrft/importer": "^0.1.0-alpha.15",
76
+ "@shrkcrft/inspector": "^0.1.0-alpha.15",
77
+ "@shrkcrft/ai": "^0.1.0-alpha.15",
78
+ "@shrkcrft/embeddings": "^0.1.0-alpha.15",
79
+ "@shrkcrft/shared": "^0.1.0-alpha.15",
80
+ "@shrkcrft/mcp-server": "^0.1.0-alpha.15",
81
81
  "@huggingface/transformers": "^3.7.5"
82
82
  },
83
83
  "publishConfig": {