@systemdox/mcp-server 0.1.51 → 0.1.53

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/dist/index.js CHANGED
@@ -870,6 +870,17 @@ ${text3}`
870
870
 
871
871
  // src/tools/get-specs.ts
872
872
  import { z as z2 } from "zod";
873
+
874
+ // src/lib/spec-label.ts
875
+ var DECISION_PREFIX = "ADR: ";
876
+ function isDecisionDraft(name) {
877
+ return name.startsWith(DECISION_PREFIX);
878
+ }
879
+ function specLabel(spec) {
880
+ return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
881
+ }
882
+
883
+ // src/tools/get-specs.ts
873
884
  function registerGetSpecs(server, apiClient2) {
874
885
  server.registerTool(
875
886
  "get_specs",
@@ -934,7 +945,7 @@ ${spec.taskContent}`);
934
945
  };
935
946
  }
936
947
  const text3 = filtered.map(
937
- (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
948
+ (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
938
949
  ).join("\n");
939
950
  return {
940
951
  content: [
@@ -952,6 +963,11 @@ ${text3}`
952
963
 
953
964
  // src/tools/get-docs.ts
954
965
  import { z as z3 } from "zod";
966
+ var PATH_FILTER_LIMIT = "1000";
967
+ function literalPrefix(pattern) {
968
+ const wildcard = pattern.indexOf("*");
969
+ return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
970
+ }
955
971
  function matchGlob(path, pattern) {
956
972
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
957
973
  return new RegExp(`^${regex}$`).test(path);
@@ -1012,11 +1028,17 @@ ${doc.content}`;
1012
1028
  const params = {};
1013
1029
  if (repo) params.repo = repo;
1014
1030
  if (search) params.q = search;
1031
+ if (path) {
1032
+ params.limit = PATH_FILTER_LIMIT;
1033
+ const prefix = literalPrefix(path);
1034
+ if (prefix) params.pathPrefix = prefix;
1035
+ }
1015
1036
  const response = await apiClient2.get(
1016
1037
  "/v1/docs",
1017
1038
  params
1018
1039
  );
1019
1040
  let items = response.items;
1041
+ const unread = path ? response.total - response.items.length : 0;
1020
1042
  if (path) {
1021
1043
  items = items.filter((item) => matchGlob(item.path, path));
1022
1044
  }
@@ -1046,7 +1068,9 @@ ${doc.content}`;
1046
1068
 
1047
1069
  ${text3}
1048
1070
 
1049
- Use doc_id for full content, or doc_id + section to fetch one section.`
1071
+ ` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
1072
+
1073
+ ` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
1050
1074
  }
1051
1075
  ]
1052
1076
  };
@@ -1147,7 +1171,9 @@ ${deps}`);
1147
1171
  }
1148
1172
  if (ctx.adrs.length > 0) {
1149
1173
  const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
1150
- sections.push(`## Architecture Decision Records (${ctx.adrs.length})
1174
+ const total = ctx.adrTotal ?? ctx.adrs.length;
1175
+ const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
1176
+ sections.push(`## Architecture Decision Records (${count})
1151
1177
 
1152
1178
  ${adrs}`);
1153
1179
  }
@@ -1519,6 +1545,9 @@ var SEVERITY_SCORES = {
1519
1545
  medium: 0.5,
1520
1546
  low: 0.25
1521
1547
  };
1548
+ var MIN_TITLE_SHARE = 1 / 3;
1549
+ var RECORD_PATH = /(^|\/)changelog\//;
1550
+ var RECORD_FACTOR = 0.5;
1522
1551
  var CHARS_PER_TOKEN = 4;
1523
1552
  function estimateTokens(text3) {
1524
1553
  return Math.ceil(text3.length / CHARS_PER_TOKEN);
@@ -1571,6 +1600,32 @@ function scoreSeverity(item) {
1571
1600
  if (!item.severity) return 0.5;
1572
1601
  return SEVERITY_SCORES[item.severity] ?? 0.5;
1573
1602
  }
1603
+ function stem(word) {
1604
+ return word.length > 3 ? word.replace(/s$/, "") : word;
1605
+ }
1606
+ function stems(text3) {
1607
+ return new Set(
1608
+ text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
1609
+ );
1610
+ }
1611
+ function titleShare(title, keywords) {
1612
+ const wanted = stems(keywords.join(" "));
1613
+ if (wanted.size === 0) return 0;
1614
+ const held = stems(title);
1615
+ let matches = 0;
1616
+ for (const word of wanted) {
1617
+ if (held.has(word)) matches++;
1618
+ }
1619
+ return matches / wanted.size;
1620
+ }
1621
+ function scoreReference(item, keywords) {
1622
+ const share = titleShare(item.title, keywords);
1623
+ const score = Math.max(
1624
+ item.relevance ?? 0,
1625
+ share >= MIN_TITLE_SHARE ? share : 0
1626
+ );
1627
+ return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
1628
+ }
1574
1629
  function extractKeywords(text3) {
1575
1630
  const stopWords = /* @__PURE__ */ new Set([
1576
1631
  "the",
@@ -1667,7 +1722,8 @@ function scoreItems(items, taskDescription, filePaths) {
1667
1722
  const keywordScore = scoreKeywordMatch(item, keywords);
1668
1723
  const recencyScore = scoreRecency(item);
1669
1724
  const severityScore = scoreSeverity(item);
1670
- const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
1725
+ const isReference = item.type === "spec" || item.type === "doc";
1726
+ const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
1671
1727
  return {
1672
1728
  type: item.type,
1673
1729
  id: item.id,
@@ -1676,7 +1732,7 @@ function scoreItems(items, taskDescription, filePaths) {
1676
1732
  score,
1677
1733
  tokenEstimate: estimateTokens(item.content)
1678
1734
  };
1679
- }).sort((a, b) => b.score - a.score);
1735
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
1680
1736
  }
1681
1737
  function selectWithinBudget(items, maxTokens) {
1682
1738
  const selected = [];
@@ -1698,6 +1754,10 @@ var MAX_HYDRATED = 8;
1698
1754
  var MAX_TAIL = 10;
1699
1755
  var TAIL_SHARE = 0.15;
1700
1756
  var TRUNCATION_NOTICE_TOKENS = 30;
1757
+ var STACK_SHARE = 0.1;
1758
+ var MIN_REFERENCES = 3;
1759
+ var SEARCH_RESULTS = 30;
1760
+ var MAX_QUERY_CHARS = 300;
1701
1761
  function formatGuardrail(g) {
1702
1762
  let text3 = `## ${g.title} [${g.severity}]
1703
1763
 
@@ -1716,7 +1776,7 @@ ${g.fix}
1716
1776
  }
1717
1777
  function formatSpecHeadline(s) {
1718
1778
  return `### ${s.name}
1719
- \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
1779
+ \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
1720
1780
  }
1721
1781
  function formatDocHeadline(d) {
1722
1782
  return `### ${d.title}
@@ -1727,6 +1787,21 @@ function formatStack(entries2) {
1727
1787
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
1728
1788
  ).join("\n");
1729
1789
  }
1790
+ function selectStack(entries2, words, maxTokens) {
1791
+ const named = (e) => {
1792
+ const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
1793
+ return words.some((w) => entry.includes(w));
1794
+ };
1795
+ const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
1796
+ const shown = [];
1797
+ for (const entry of ordered) {
1798
+ if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
1799
+ break;
1800
+ }
1801
+ shown.push(entry);
1802
+ }
1803
+ return { text: formatStack(shown), hidden: entries2.length - shown.length };
1804
+ }
1730
1805
  function formatRules(rules) {
1731
1806
  const byCategory = /* @__PURE__ */ new Map();
1732
1807
  for (const r of rules) {
@@ -1748,6 +1823,21 @@ function formatRules(rules) {
1748
1823
 
1749
1824
  ${text3.trim()}`;
1750
1825
  }
1826
+ async function findDocs(api, taskDescription) {
1827
+ const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
1828
+ if (query) {
1829
+ try {
1830
+ const found = await api.get("/v1/docs", {
1831
+ q: query,
1832
+ limit: String(SEARCH_RESULTS)
1833
+ });
1834
+ if (found.items.length > 0) return { ...found, searched: true };
1835
+ } catch {
1836
+ }
1837
+ }
1838
+ const listed = await api.get("/v1/docs");
1839
+ return { ...listed, searched: false };
1840
+ }
1751
1841
  async function fetchSpecBody(api, draftId) {
1752
1842
  const spec = await api.get(
1753
1843
  `/v1/specs/${encodeURIComponent(draftId)}`
@@ -1848,7 +1938,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1848
1938
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
1849
1939
  api.get("/v1/guardrails").catch(orEmpty([])),
1850
1940
  api.get("/v1/specs").catch(orEmpty([])),
1851
- api.get("/v1/docs").catch(orEmpty({ items: [] })),
1941
+ findDocs(api, taskDescription).catch(
1942
+ orEmpty({ items: [], searched: false })
1943
+ ),
1852
1944
  api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1853
1945
  api.get("/v1/standards/rules").catch(orEmpty([])),
1854
1946
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
@@ -1861,6 +1953,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1861
1953
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
1862
1954
  const refs = /* @__PURE__ */ new Map();
1863
1955
  const key = (type, id) => `${type}:${id}`;
1956
+ const docTitles = new Set(docs.items.map((d) => d.title));
1957
+ const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
1864
1958
  const scorable = [
1865
1959
  ...guardrails.map((g) => ({
1866
1960
  type: "guardrail",
@@ -1871,12 +1965,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1871
1965
  severity: g.severity,
1872
1966
  updatedAt: g.updatedAt
1873
1967
  })),
1874
- ...specs.filter((s) => s.status !== "archived").map((s) => {
1968
+ ...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
1875
1969
  refs.set(key("spec", s.draftId), {
1876
1970
  type: "spec",
1877
1971
  id: s.draftId,
1878
1972
  title: s.name,
1879
- listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s.requirementCount} requirements)`,
1973
+ listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
1880
1974
  fetchHint: `get_specs(draft_id="${s.draftId}")`
1881
1975
  });
1882
1976
  return {
@@ -1887,7 +1981,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1887
1981
  updatedAt: s.updatedAt
1888
1982
  };
1889
1983
  }),
1890
- ...docs.items.map((d) => {
1984
+ ...docs.items.map((d, position) => {
1891
1985
  const section = selectSection(d.outline, taskDescription);
1892
1986
  const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
1893
1987
  refs.set(key("doc", d.id), {
@@ -1906,7 +2000,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1906
2000
  title: d.title,
1907
2001
  content: formatDocHeadline(d),
1908
2002
  category: d.doc_type === "adr" ? "architecture" : void 0,
1909
- updatedAt: d.synced_at
2003
+ updatedAt: d.synced_at,
2004
+ path: d.path,
2005
+ relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
1910
2006
  };
1911
2007
  })
1912
2008
  ];
@@ -1914,9 +2010,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1914
2010
  const sections = [];
1915
2011
  let overhead = 0;
1916
2012
  if (stack.length > 0) {
2013
+ const { text: text3, hidden } = selectStack(
2014
+ stack,
2015
+ [...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
2016
+ Math.floor(budget * STACK_SHARE)
2017
+ );
1917
2018
  const section = `## Tech Stack
1918
2019
 
1919
- ${formatStack(stack)}`;
2020
+ ${text3}` + (hidden > 0 ? `
2021
+
2022
+ _${hidden} more with \`get_tech_stack\`._` : "");
1920
2023
  sections.push(section);
1921
2024
  overhead += estimateTokens(section);
1922
2025
  }
@@ -2035,9 +2138,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
2035
2138
  const tailLines = [];
2036
2139
  for (const meta of tailCandidates) {
2037
2140
  const cost = estimateTokens(meta.listLine);
2038
- if (cost > remaining) break;
2141
+ const named = blocks.length + tailLines.length;
2142
+ if (cost > remaining && named >= MIN_REFERENCES) break;
2039
2143
  tailLines.push(meta.listLine);
2040
- remaining -= cost;
2144
+ remaining = Math.max(0, remaining - cost);
2041
2145
  }
2042
2146
  if (tailLines.length > 0) {
2043
2147
  sections.push(
@@ -3020,9 +3124,9 @@ function localDate(now) {
3020
3124
  return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
3021
3125
  }
3022
3126
  function conflictCopyName(path, date, taken) {
3023
- const stem = path.replace(/\.md$/, "");
3127
+ const stem2 = path.replace(/\.md$/, "");
3024
3128
  for (let n = 1; ; n++) {
3025
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
3129
+ const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
3026
3130
  if (!taken.has(name.toLowerCase())) return name;
3027
3131
  }
3028
3132
  }
@@ -3909,7 +4013,7 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
3909
4013
  }
3910
4014
 
3911
4015
  // src/server.ts
3912
- var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
4016
+ var SERVER_VERSION = true ? "0.1.53" : "0.0.0-dev";
3913
4017
  function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
3914
4018
  const server = new McpServer(
3915
4019
  {
@@ -82,6 +82,9 @@ var SEVERITY_SCORES = {
82
82
  medium: 0.5,
83
83
  low: 0.25
84
84
  };
85
+ var MIN_TITLE_SHARE = 1 / 3;
86
+ var RECORD_PATH = /(^|\/)changelog\//;
87
+ var RECORD_FACTOR = 0.5;
85
88
  var CHARS_PER_TOKEN = 4;
86
89
  function estimateTokens(text) {
87
90
  return Math.ceil(text.length / CHARS_PER_TOKEN);
@@ -134,6 +137,32 @@ function scoreSeverity(item) {
134
137
  if (!item.severity) return 0.5;
135
138
  return SEVERITY_SCORES[item.severity] ?? 0.5;
136
139
  }
140
+ function stem(word) {
141
+ return word.length > 3 ? word.replace(/s$/, "") : word;
142
+ }
143
+ function stems(text) {
144
+ return new Set(
145
+ text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
146
+ );
147
+ }
148
+ function titleShare(title, keywords) {
149
+ const wanted = stems(keywords.join(" "));
150
+ if (wanted.size === 0) return 0;
151
+ const held = stems(title);
152
+ let matches = 0;
153
+ for (const word of wanted) {
154
+ if (held.has(word)) matches++;
155
+ }
156
+ return matches / wanted.size;
157
+ }
158
+ function scoreReference(item, keywords) {
159
+ const share = titleShare(item.title, keywords);
160
+ const score = Math.max(
161
+ item.relevance ?? 0,
162
+ share >= MIN_TITLE_SHARE ? share : 0
163
+ );
164
+ return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
165
+ }
137
166
  function extractKeywords(text) {
138
167
  const stopWords = /* @__PURE__ */ new Set([
139
168
  "the",
@@ -230,7 +259,8 @@ function scoreItems(items, taskDescription, filePaths) {
230
259
  const keywordScore = scoreKeywordMatch(item, keywords);
231
260
  const recencyScore = scoreRecency(item);
232
261
  const severityScore = scoreSeverity(item);
233
- const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
262
+ const isReference = item.type === "spec" || item.type === "doc";
263
+ const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
234
264
  return {
235
265
  type: item.type,
236
266
  id: item.id,
@@ -239,7 +269,7 @@ function scoreItems(items, taskDescription, filePaths) {
239
269
  score,
240
270
  tokenEstimate: estimateTokens(item.content)
241
271
  };
242
- }).sort((a, b) => b.score - a.score);
272
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
243
273
  }
244
274
  function selectWithinBudget(items, maxTokens) {
245
275
  const selected = [];
@@ -252,6 +282,15 @@ function selectWithinBudget(items, maxTokens) {
252
282
  return { selected, tokensUsed, totalItems: items.length };
253
283
  }
254
284
 
285
+ // src/lib/spec-label.ts
286
+ var DECISION_PREFIX = "ADR: ";
287
+ function isDecisionDraft(name) {
288
+ return name.startsWith(DECISION_PREFIX);
289
+ }
290
+ function specLabel(spec) {
291
+ return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
292
+ }
293
+
255
294
  // src/lib/context-builder.ts
256
295
  var DEFAULT_BUDGET = 4e3;
257
296
  var RESERVE_TOKENS = 200;
@@ -261,6 +300,10 @@ var MAX_HYDRATED = 8;
261
300
  var MAX_TAIL = 10;
262
301
  var TAIL_SHARE = 0.15;
263
302
  var TRUNCATION_NOTICE_TOKENS = 30;
303
+ var STACK_SHARE = 0.1;
304
+ var MIN_REFERENCES = 3;
305
+ var SEARCH_RESULTS = 30;
306
+ var MAX_QUERY_CHARS = 300;
264
307
  function formatGuardrail(g) {
265
308
  let text = `## ${g.title} [${g.severity}]
266
309
 
@@ -279,7 +322,7 @@ ${g.fix}
279
322
  }
280
323
  function formatSpecHeadline(s) {
281
324
  return `### ${s.name}
282
- \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
325
+ \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
283
326
  }
284
327
  function formatDocHeadline(d) {
285
328
  return `### ${d.title}
@@ -290,6 +333,21 @@ function formatStack(entries) {
290
333
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
291
334
  ).join("\n");
292
335
  }
336
+ function selectStack(entries, words, maxTokens) {
337
+ const named = (e) => {
338
+ const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
339
+ return words.some((w) => entry.includes(w));
340
+ };
341
+ const ordered = [...entries.filter(named), ...entries.filter((e) => !named(e))];
342
+ const shown = [];
343
+ for (const entry of ordered) {
344
+ if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
345
+ break;
346
+ }
347
+ shown.push(entry);
348
+ }
349
+ return { text: formatStack(shown), hidden: entries.length - shown.length };
350
+ }
293
351
  function formatRules(rules) {
294
352
  const byCategory = /* @__PURE__ */ new Map();
295
353
  for (const r of rules) {
@@ -311,6 +369,21 @@ function formatRules(rules) {
311
369
 
312
370
  ${text.trim()}`;
313
371
  }
372
+ async function findDocs(api, taskDescription) {
373
+ const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
374
+ if (query) {
375
+ try {
376
+ const found = await api.get("/v1/docs", {
377
+ q: query,
378
+ limit: String(SEARCH_RESULTS)
379
+ });
380
+ if (found.items.length > 0) return { ...found, searched: true };
381
+ } catch {
382
+ }
383
+ }
384
+ const listed = await api.get("/v1/docs");
385
+ return { ...listed, searched: false };
386
+ }
314
387
  async function fetchSpecBody(api, draftId) {
315
388
  const spec = await api.get(
316
389
  `/v1/specs/${encodeURIComponent(draftId)}`
@@ -411,7 +484,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
411
484
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
412
485
  api.get("/v1/guardrails").catch(orEmpty([])),
413
486
  api.get("/v1/specs").catch(orEmpty([])),
414
- api.get("/v1/docs").catch(orEmpty({ items: [] })),
487
+ findDocs(api, taskDescription).catch(
488
+ orEmpty({ items: [], searched: false })
489
+ ),
415
490
  api.get("/v1/ai/settings/stack").catch(orEmpty([])),
416
491
  api.get("/v1/standards/rules").catch(orEmpty([])),
417
492
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
@@ -424,6 +499,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
424
499
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
425
500
  const refs = /* @__PURE__ */ new Map();
426
501
  const key = (type, id) => `${type}:${id}`;
502
+ const docTitles = new Set(docs.items.map((d) => d.title));
503
+ const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
427
504
  const scorable = [
428
505
  ...guardrails.map((g) => ({
429
506
  type: "guardrail",
@@ -434,12 +511,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
434
511
  severity: g.severity,
435
512
  updatedAt: g.updatedAt
436
513
  })),
437
- ...specs.filter((s) => s.status !== "archived").map((s) => {
514
+ ...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
438
515
  refs.set(key("spec", s.draftId), {
439
516
  type: "spec",
440
517
  id: s.draftId,
441
518
  title: s.name,
442
- listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s.requirementCount} requirements)`,
519
+ listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
443
520
  fetchHint: `get_specs(draft_id="${s.draftId}")`
444
521
  });
445
522
  return {
@@ -450,7 +527,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
450
527
  updatedAt: s.updatedAt
451
528
  };
452
529
  }),
453
- ...docs.items.map((d) => {
530
+ ...docs.items.map((d, position) => {
454
531
  const section = selectSection(d.outline, taskDescription);
455
532
  const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
456
533
  refs.set(key("doc", d.id), {
@@ -469,7 +546,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
469
546
  title: d.title,
470
547
  content: formatDocHeadline(d),
471
548
  category: d.doc_type === "adr" ? "architecture" : void 0,
472
- updatedAt: d.synced_at
549
+ updatedAt: d.synced_at,
550
+ path: d.path,
551
+ relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
473
552
  };
474
553
  })
475
554
  ];
@@ -477,9 +556,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
477
556
  const sections = [];
478
557
  let overhead = 0;
479
558
  if (stack.length > 0) {
559
+ const { text, hidden } = selectStack(
560
+ stack,
561
+ [...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
562
+ Math.floor(budget * STACK_SHARE)
563
+ );
480
564
  const section = `## Tech Stack
481
565
 
482
- ${formatStack(stack)}`;
566
+ ${text}` + (hidden > 0 ? `
567
+
568
+ _${hidden} more with \`get_tech_stack\`._` : "");
483
569
  sections.push(section);
484
570
  overhead += estimateTokens(section);
485
571
  }
@@ -598,9 +684,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
598
684
  const tailLines = [];
599
685
  for (const meta of tailCandidates) {
600
686
  const cost = estimateTokens(meta.listLine);
601
- if (cost > remaining) break;
687
+ const named = blocks.length + tailLines.length;
688
+ if (cost > remaining && named >= MIN_REFERENCES) break;
602
689
  tailLines.push(meta.listLine);
603
- remaining -= cost;
690
+ remaining = Math.max(0, remaining - cost);
604
691
  }
605
692
  if (tailLines.length > 0) {
606
693
  sections.push(
@@ -15,6 +15,10 @@ interface ScorableItem {
15
15
  category?: string;
16
16
  severity?: string;
17
17
  updatedAt?: string;
18
+ /** Where a document's repository file lives. */
19
+ path?: string;
20
+ /** 0 to 1, from the order the document search returned it in. */
21
+ relevance?: number;
18
22
  }
19
23
  export declare function estimateTokens(text: string): number;
20
24
  /**
@@ -26,6 +30,7 @@ export declare function truncateToTokens(text: string, maxTokens: number): {
26
30
  text: string;
27
31
  truncated: boolean;
28
32
  };
33
+ export declare function extractKeywords(text: string): string[];
29
34
  export declare function scoreItems(items: ScorableItem[], taskDescription: string, filePaths: string[]): ScoredItem[];
30
35
  export declare function selectWithinBudget(items: ScoredItem[], maxTokens: number): {
31
36
  selected: ScoredItem[];
@@ -0,0 +1,5 @@
1
+ export declare function isDecisionDraft(name: string): boolean;
2
+ export declare function specLabel(spec: {
3
+ name: string;
4
+ requirementCount: number;
5
+ }): string;
@@ -93,7 +93,7 @@ export interface TeamSetup {
93
93
  /** Absent on an API older than ADR-223. */
94
94
  sessionBrief?: SessionBrief;
95
95
  }
96
- /** What every session is told (ADR-223, specs/changes/session-brief/contract.md). */
96
+ /** What every session is told (ADR-223, specs/archive/session-brief/contract.md). */
97
97
  export interface SessionBrief {
98
98
  text: string;
99
99
  /** What a session may install by itself: in the default space, and unable to run anything by itself. */