@specship/specship-win32-arm64 0.16.0 → 0.18.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 (52) hide show
  1. package/lib/dist/activation/steering.d.ts +11 -2
  2. package/lib/dist/activation/steering.d.ts.map +1 -1
  3. package/lib/dist/activation/steering.js +26 -9
  4. package/lib/dist/activation/steering.js.map +1 -1
  5. package/lib/dist/bin/specship.js +34 -2
  6. package/lib/dist/bin/specship.js.map +1 -1
  7. package/lib/dist/commands/specship/learn.md +45 -0
  8. package/lib/dist/config/runtime-settings.d.ts +25 -0
  9. package/lib/dist/config/runtime-settings.d.ts.map +1 -0
  10. package/lib/dist/config/runtime-settings.js +99 -0
  11. package/lib/dist/config/runtime-settings.js.map +1 -0
  12. package/lib/dist/index.d.ts +9 -0
  13. package/lib/dist/index.d.ts.map +1 -1
  14. package/lib/dist/index.js +8 -0
  15. package/lib/dist/index.js.map +1 -1
  16. package/lib/dist/installer/index.js +1 -1
  17. package/lib/dist/installer/index.js.map +1 -1
  18. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  19. package/lib/dist/installer/targets/claude.js +7 -6
  20. package/lib/dist/installer/targets/claude.js.map +1 -1
  21. package/lib/dist/mcp/model-context.d.ts +5 -3
  22. package/lib/dist/mcp/model-context.d.ts.map +1 -1
  23. package/lib/dist/mcp/model-context.js +17 -8
  24. package/lib/dist/mcp/model-context.js.map +1 -1
  25. package/lib/dist/mcp/session.d.ts +1 -0
  26. package/lib/dist/mcp/session.d.ts.map +1 -1
  27. package/lib/dist/mcp/session.js +13 -0
  28. package/lib/dist/mcp/session.js.map +1 -1
  29. package/lib/dist/mcp/tools.d.ts +30 -0
  30. package/lib/dist/mcp/tools.d.ts.map +1 -1
  31. package/lib/dist/mcp/tools.js +151 -11
  32. package/lib/dist/mcp/tools.js.map +1 -1
  33. package/lib/dist/reflect/index.d.ts +3 -1
  34. package/lib/dist/reflect/index.d.ts.map +1 -1
  35. package/lib/dist/reflect/index.js +5 -1
  36. package/lib/dist/reflect/index.js.map +1 -1
  37. package/lib/dist/reflect/miner.d.ts.map +1 -1
  38. package/lib/dist/reflect/miner.js +130 -5
  39. package/lib/dist/reflect/miner.js.map +1 -1
  40. package/lib/dist/reflect/session-outcomes.d.ts +50 -0
  41. package/lib/dist/reflect/session-outcomes.d.ts.map +1 -0
  42. package/lib/dist/reflect/session-outcomes.js +110 -0
  43. package/lib/dist/reflect/session-outcomes.js.map +1 -0
  44. package/lib/dist/reflect/sweep.d.ts +11 -0
  45. package/lib/dist/reflect/sweep.d.ts.map +1 -1
  46. package/lib/dist/reflect/sweep.js +29 -0
  47. package/lib/dist/reflect/sweep.js.map +1 -1
  48. package/lib/dist/server/routes/claude.js +27 -0
  49. package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +197 -0
  50. package/lib/node_modules/.package-lock.json +1 -1
  51. package/lib/package.json +1 -1
  52. package/package.json +1 -1
@@ -17,6 +17,8 @@ exports.getStaticTools = getStaticTools;
17
17
  const directory_1 = require("../directory");
18
18
  const statusline_1 = require("../statusline");
19
19
  const model_context_1 = require("./model-context");
20
+ const miner_1 = require("../reflect/miner");
21
+ const session_outcomes_1 = require("../reflect/session-outcomes");
20
22
  // Lazy-load the heavy SpecShip chain off the MCP startup path — see the same
21
23
  // helper in engine.ts. ToolHandler must load to answer tools/list (static
22
24
  // schemas), but it must NOT drag in sqlite/query layers before the daemon binds;
@@ -641,6 +643,22 @@ class ToolHandler {
641
643
  // economics that justify trimming don't apply, so they always survive.
642
644
  visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name) || t.name.startsWith('designer_'));
643
645
  }
646
+ // Haiku-tier menu trim (LOWMODEL-DOC, REQ-LOWMODEL-004): small-model
647
+ // tool choice degrades with menu size, so the code-graph group trims
648
+ // to the core three; spec/link tools and enabled integrations are
649
+ // untouched. Composes with the tiny-repo gate as an intersection.
650
+ // Schemas never vary — only the list; `execute()` still answers a
651
+ // trimmed-away tool for clients that cached the full list (A2).
652
+ if ((0, model_context_1.detectModelTier)(this.cg.getProjectRoot()) === 'haiku') {
653
+ const HAIKU_CORE = new Set(['specship_explore', 'specship_search', 'specship_node']);
654
+ const CODE_GRAPH_GROUP = new Set([
655
+ 'specship_explore', 'specship_search', 'specship_node',
656
+ 'specship_callers', 'specship_callees', 'specship_impact',
657
+ 'specship_files', 'specship_status',
658
+ 'specship_maintainability', 'specship_fitness',
659
+ ]);
660
+ visible = visible.filter(t => !CODE_GRAPH_GROUP.has(t.name) || HAIKU_CORE.has(t.name));
661
+ }
644
662
  return visible.map(tool => {
645
663
  if (tool.name === 'specship_explore') {
646
664
  return {
@@ -831,6 +849,30 @@ class ToolHandler {
831
849
  * disables) and, on haiku/sonnet, compresses the response's prose
832
850
  * scaffolding — fenced code stays byte-verbatim. Full tier is identity.
833
851
  */
852
+ /**
853
+ * Tier-change listeners (LOWMODEL-DOC, REQ-LOWMODEL-004.A3): the haiku
854
+ * menu trim changes `tools/list`, so a mid-session /model switch must ride
855
+ * an MCP `notifications/tools/list_changed`. Sessions subscribe here; the
856
+ * change is detected on the next code-graph call (the same funnel that
857
+ * reads the tier for compaction).
858
+ */
859
+ tierListeners = new Set();
860
+ lastTier = null;
861
+ addTierChangeListener(cb) {
862
+ this.tierListeners.add(cb);
863
+ return () => this.tierListeners.delete(cb);
864
+ }
865
+ noteTier(tier) {
866
+ if (this.lastTier !== null && tier !== this.lastTier) {
867
+ for (const cb of this.tierListeners) {
868
+ try {
869
+ cb(tier);
870
+ }
871
+ catch { /* a listener must never break a tool call */ }
872
+ }
873
+ }
874
+ this.lastTier = tier;
875
+ }
834
876
  withModelCompaction(result) {
835
877
  if (result.isError)
836
878
  return result;
@@ -842,6 +884,7 @@ class ToolHandler {
842
884
  root = null;
843
885
  }
844
886
  const tier = (0, model_context_1.detectModelTier)(root);
887
+ this.noteTier(tier);
845
888
  if (tier === 'full')
846
889
  return result;
847
890
  const first = result.content?.[0];
@@ -1106,7 +1149,7 @@ class ToolHandler {
1106
1149
  kinds: kind ? [kind] : undefined,
1107
1150
  });
1108
1151
  if (results.length === 0) {
1109
- return this.textResult(`No results found for "${query}"`);
1152
+ return this.missWithSuggestions(cg, 'Query', query);
1110
1153
  }
1111
1154
  // Down-rank generated files within the FTS-returned set so a search
1112
1155
  // for "Send" surfaces the hand-written keeper before .pb.go stubs
@@ -1119,6 +1162,47 @@ class ToolHandler {
1119
1162
  const formatted = this.formatSearchResults(ranked);
1120
1163
  return this.textResult(this.truncateOutput(formatted));
1121
1164
  }
1165
+ /**
1166
+ * Anti-flounder miss template (LOWMODEL-DOC, REQ-LOWMODEL-001): a "not
1167
+ * found" that just says "not found" leaves the agent — small models
1168
+ * especially — to guess its next move, and the guess is usually grep/Read.
1169
+ * Instead: name the nearest REAL symbols (token-level lookup, since the
1170
+ * full query already failed FTS+LIKE) and end with a copy-pasteable next
1171
+ * call. Applies at every tier.
1172
+ */
1173
+ missWithSuggestions(cg, kindLabel, query) {
1174
+ // Split on separators AND camelCase so "AuthServic.logn" still yields
1175
+ // useful probes ("Auth", "Servic", "logn" → LIKE finds AuthService/login).
1176
+ const tokens = [...new Set(query
1177
+ .split(/[\s._:\->#/\\]+/)
1178
+ .flatMap((t) => t.split(/(?=[A-Z])/))
1179
+ .map((t) => t.trim())
1180
+ .filter((t) => t.length >= 3))].slice(0, 6);
1181
+ const seen = new Set();
1182
+ const suggestions = [];
1183
+ for (const token of tokens) {
1184
+ for (const r of cg.searchNodes(token, { limit: 3 })) {
1185
+ const key = r.node.qualifiedName;
1186
+ if (seen.has(key))
1187
+ continue;
1188
+ seen.add(key);
1189
+ suggestions.push({ name: key, where: `${r.node.filePath}:${r.node.startLine}` });
1190
+ if (suggestions.length >= 4)
1191
+ break;
1192
+ }
1193
+ if (suggestions.length >= 4)
1194
+ break;
1195
+ }
1196
+ const lines = [`${kindLabel} "${query}" not found in the index.`];
1197
+ if (suggestions.length > 0) {
1198
+ lines.push(`Closest indexed symbols: ${suggestions.map((s) => `${s.name} (${s.where})`).join(' · ')}`);
1199
+ lines.push(`Next: specship_explore "${suggestions.map((s) => s.name).join(' ')}"`);
1200
+ }
1201
+ else {
1202
+ lines.push('Nothing close in the index either. Next: specship_explore with broader words from the question — it also matches file names and doc text.');
1203
+ }
1204
+ return this.textResult(lines.join('\n'));
1205
+ }
1122
1206
  /**
1123
1207
  * Handle specship_callers
1124
1208
  */
@@ -1130,7 +1214,7 @@ class ToolHandler {
1130
1214
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1131
1215
  const allMatches = this.findAllSymbols(cg, symbol);
1132
1216
  if (allMatches.nodes.length === 0) {
1133
- return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1217
+ return this.missWithSuggestions(cg, 'Symbol', symbol);
1134
1218
  }
1135
1219
  // Aggregate callers across all matching symbols
1136
1220
  const seen = new Set();
@@ -1160,7 +1244,7 @@ class ToolHandler {
1160
1244
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1161
1245
  const allMatches = this.findAllSymbols(cg, symbol);
1162
1246
  if (allMatches.nodes.length === 0) {
1163
- return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1247
+ return this.missWithSuggestions(cg, 'Symbol', symbol);
1164
1248
  }
1165
1249
  // Aggregate callees across all matching symbols
1166
1250
  const seen = new Set();
@@ -1190,7 +1274,7 @@ class ToolHandler {
1190
1274
  const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
1191
1275
  const allMatches = this.findAllSymbols(cg, symbol);
1192
1276
  if (allMatches.nodes.length === 0) {
1193
- return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1277
+ return this.missWithSuggestions(cg, 'Symbol', symbol);
1194
1278
  }
1195
1279
  // Aggregate impact across all matching symbols
1196
1280
  const mergedNodes = new Map();
@@ -1333,7 +1417,7 @@ class ToolHandler {
1333
1417
  * whose qualifiedName contains another named token (`PmsProductServiceImpl::list`),
1334
1418
  * dropping unrelated `OmsOrderService::list`.
1335
1419
  */
1336
- buildFlowFromNamedSymbols(cg, query) {
1420
+ buildFlowFromNamedSymbols(cg, query, tier = 'full') {
1337
1421
  const EMPTY = { text: '', pathNodeIds: new Set(), namedNodeIds: new Set(), uniqueNamedNodeIds: new Set() };
1338
1422
  try {
1339
1423
  const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
@@ -1468,11 +1552,23 @@ class ToolHandler {
1468
1552
  out.push('## Flow (call path among the symbols you queried)', '');
1469
1553
  for (let i = 0; i < best.length; i++) {
1470
1554
  const step = best[i];
1471
- if (step.edge) {
1472
- const sy = this.synthEdgeNote(step.edge);
1473
- out.push(` ↓ ${sy ? sy.compact : step.edge.kind}`);
1555
+ if (tier === 'haiku') {
1556
+ // LOWMODEL-DOC REQ-LOWMODEL-003: small models don't synthesize
1557
+ // the flow from evidence — render each hop as ONE explicit line
1558
+ // with its mechanism inline. Additive formatting only; the code
1559
+ // bodies below are identical to the full tier's (A2).
1560
+ const mech = step.edge
1561
+ ? ` — via ${(this.synthEdgeNote(step.edge)?.compact ?? step.edge.kind)}`
1562
+ : '';
1563
+ out.push(`${i + 1}. ${i > 0 ? '→ ' : ''}${step.node.name} (${step.node.filePath}:${step.node.startLine})${mech}`);
1564
+ }
1565
+ else {
1566
+ if (step.edge) {
1567
+ const sy = this.synthEdgeNote(step.edge);
1568
+ out.push(` ↓ ${sy ? sy.compact : step.edge.kind}`);
1569
+ }
1570
+ out.push(`${i + 1}. ${step.node.name} (${step.node.filePath}:${step.node.startLine})`);
1474
1571
  }
1475
- out.push(`${i + 1}. ${step.node.name} (${step.node.filePath}:${step.node.startLine})`);
1476
1572
  }
1477
1573
  out.push('');
1478
1574
  }
@@ -1511,6 +1607,43 @@ class ToolHandler {
1511
1607
  * CLAUDE.md). Facts are few and human-authored, so a linear scan + token
1512
1608
  * match is cheap and precise enough.
1513
1609
  */
1610
+ /**
1611
+ * Prior-work recall (LEARN-DOC, REQ-LEARN-003): sessions from this
1612
+ * project's ingested task history that EDITED the files this exploration
1613
+ * touches — date, opening prompt, files, overlapping workflow runs.
1614
+ * Deterministic join over `claude_*` + `workflow_runs`; scoped to this
1615
+ * project in both path forms; silent when the tables are absent (ingest
1616
+ * never ran) or nothing matches. Capped tiny so the hot path stays lean.
1617
+ */
1618
+ buildPriorWorkSection(cg, subgraph) {
1619
+ try {
1620
+ const db = cg.getSpecQueries()['db'];
1621
+ const files = [...new Set([...subgraph.nodes.values()].map((n) => n.filePath))].slice(0, 12);
1622
+ const forms = (0, miner_1.projectPathForms)(cg.getProjectRoot());
1623
+ const sessions = (0, session_outcomes_1.sessionsTouchingFiles)(db, forms, files);
1624
+ if (sessions.length === 0)
1625
+ return '';
1626
+ const linesOut = ['## Prior work', '_Past sessions that edited these files (local task history — deterministic, from ingested transcripts)._', ''];
1627
+ for (const id of sessions) {
1628
+ const o = (0, session_outcomes_1.sessionOutcome)(db, id);
1629
+ if (!o)
1630
+ continue;
1631
+ const date = o.startedAt ? new Date(o.startedAt).toISOString().slice(0, 10) : '—';
1632
+ const runs = o.workflowRuns.length
1633
+ ? ` · runs in window: ${o.workflowRuns.map((r) => `${r.name} (${r.status})`).join(', ')}`
1634
+ : '';
1635
+ const links = o.linksAsserted > 0 ? ` · ${o.linksAsserted} spec link(s) asserted` : '';
1636
+ linesOut.push(`- ${date} · session ${o.sessionId.slice(0, 8)} · "${o.firstPrompt || '(no prompt text)'}" · edited: ${o.filesEdited.slice(0, 5).map((f) => f.split('/').pop()).join(', ')}${o.filesEdited.length > 5 ? ` +${o.filesEdited.length - 5}` : ''}${runs}${links}`);
1637
+ }
1638
+ if (linesOut.length <= 3)
1639
+ return '';
1640
+ linesOut.push('');
1641
+ return linesOut.join('\n');
1642
+ }
1643
+ catch {
1644
+ return ''; // recall must never break an exploration
1645
+ }
1646
+ }
1514
1647
  buildDomainFactsSection(cg, query) {
1515
1648
  let facts;
1516
1649
  try {
@@ -2042,6 +2175,13 @@ class ToolHandler {
2042
2175
  const domainFacts = this.buildDomainFactsSection(cg, query);
2043
2176
  if (domainFacts)
2044
2177
  lines.push(domainFacts);
2178
+ // Prior work (LEARN-DOC, REQ-LEARN-003): if past sessions edited the
2179
+ // files this exploration resolves to, surface them inline — the tier-3
2180
+ // task-history recall, riding the same additive/silent pattern as domain
2181
+ // facts (no new tool; one indexed query; nothing on no-match).
2182
+ const priorWork = this.buildPriorWorkSection(cg, subgraph);
2183
+ if (priorWork)
2184
+ lines.push(priorWork);
2045
2185
  // Blast radius (always-on, compact): for the entry symbols, who depends on
2046
2186
  // them + which tests cover them — locations only, no source — so the agent
2047
2187
  // knows what to update/verify before editing without a separate call.
@@ -2082,7 +2222,7 @@ class ToolHandler {
2082
2222
  // Compute the flow spine once — used both to prepend the Flow section (below)
2083
2223
  // and to gate adaptive source sizing: files on the spine get full source,
2084
2224
  // off-spine peers skeletonize.
2085
- const flow = this.buildFlowFromNamedSymbols(cg, query);
2225
+ const flow = this.buildFlowFromNamedSymbols(cg, query, (0, model_context_1.detectModelTier)(cg.getProjectRoot()));
2086
2226
  // Polymorphic-sibling detector for adaptive sizing. A class that implements/
2087
2227
  // extends a supertype shared by >= MIN_SIBLINGS classes is one of many
2088
2228
  // INTERCHANGEABLE implementations (OkHttp's 14 `: Interceptor` classes —
@@ -2663,7 +2803,7 @@ class ToolHandler {
2663
2803
  const lineHint = typeof args.line === 'number' && args.line > 0 ? args.line : undefined;
2664
2804
  let matches = this.findSymbolMatches(cg, symbol);
2665
2805
  if (matches.length === 0) {
2666
- return this.textResult(`Symbol "${symbol}" not found in the codebase`);
2806
+ return this.missWithSuggestions(cg, 'Symbol', symbol);
2667
2807
  }
2668
2808
  // Disambiguate a heavily-overloaded name to a specific definition the caller
2669
2809
  // pinned by file/line (the `file:line` a trail or another tool showed it) —