@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.
- package/lib/dist/activation/steering.d.ts +11 -2
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +26 -9
- package/lib/dist/activation/steering.js.map +1 -1
- package/lib/dist/bin/specship.js +34 -2
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/learn.md +45 -0
- package/lib/dist/config/runtime-settings.d.ts +25 -0
- package/lib/dist/config/runtime-settings.d.ts.map +1 -0
- package/lib/dist/config/runtime-settings.js +99 -0
- package/lib/dist/config/runtime-settings.js.map +1 -0
- package/lib/dist/index.d.ts +9 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +8 -0
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.js +1 -1
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +7 -6
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/mcp/model-context.d.ts +5 -3
- package/lib/dist/mcp/model-context.d.ts.map +1 -1
- package/lib/dist/mcp/model-context.js +17 -8
- package/lib/dist/mcp/model-context.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +1 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +13 -0
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +30 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +151 -11
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/reflect/index.d.ts +3 -1
- package/lib/dist/reflect/index.d.ts.map +1 -1
- package/lib/dist/reflect/index.js +5 -1
- package/lib/dist/reflect/index.js.map +1 -1
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +130 -5
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/reflect/session-outcomes.d.ts +50 -0
- package/lib/dist/reflect/session-outcomes.d.ts.map +1 -0
- package/lib/dist/reflect/session-outcomes.js +110 -0
- package/lib/dist/reflect/session-outcomes.js.map +1 -0
- package/lib/dist/reflect/sweep.d.ts +11 -0
- package/lib/dist/reflect/sweep.d.ts.map +1 -1
- package/lib/dist/reflect/sweep.js +29 -0
- package/lib/dist/reflect/sweep.js.map +1 -1
- package/lib/dist/server/routes/claude.js +27 -0
- package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +197 -0
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
package/lib/dist/mcp/tools.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
1472
|
-
|
|
1473
|
-
|
|
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.
|
|
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) —
|