@specship/specship-win32-arm64 0.21.2 → 0.23.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 (122) hide show
  1. package/lib/dist/activation/search-intercept.d.ts +59 -0
  2. package/lib/dist/activation/search-intercept.d.ts.map +1 -0
  3. package/lib/dist/activation/search-intercept.js +195 -0
  4. package/lib/dist/activation/search-intercept.js.map +1 -0
  5. package/lib/dist/bin/specship.js +372 -11
  6. package/lib/dist/bin/specship.js.map +1 -1
  7. package/lib/dist/bin/uninstall.d.ts +4 -3
  8. package/lib/dist/bin/uninstall.d.ts.map +1 -1
  9. package/lib/dist/bin/uninstall.js +4 -3
  10. package/lib/dist/bin/uninstall.js.map +1 -1
  11. package/lib/dist/commands/specship/jira.md +74 -0
  12. package/lib/dist/commands/specship/spec.md +61 -1
  13. package/lib/dist/extraction/index.d.ts +6 -0
  14. package/lib/dist/extraction/index.d.ts.map +1 -1
  15. package/lib/dist/extraction/index.js.map +1 -1
  16. package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
  17. package/lib/dist/extraction/specs/markdown-spec-extractor.js +8 -1
  18. package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
  19. package/lib/dist/index.d.ts +7 -0
  20. package/lib/dist/index.d.ts.map +1 -1
  21. package/lib/dist/index.js +45 -18
  22. package/lib/dist/index.js.map +1 -1
  23. package/lib/dist/installer/index.d.ts +20 -6
  24. package/lib/dist/installer/index.d.ts.map +1 -1
  25. package/lib/dist/installer/index.js +123 -63
  26. package/lib/dist/installer/index.js.map +1 -1
  27. package/lib/dist/installer/targets/claude.d.ts +9 -0
  28. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  29. package/lib/dist/installer/targets/claude.js +46 -0
  30. package/lib/dist/installer/targets/claude.js.map +1 -1
  31. package/lib/dist/installer/targets/gemini.d.ts +78 -0
  32. package/lib/dist/installer/targets/gemini.d.ts.map +1 -0
  33. package/lib/dist/installer/targets/gemini.js +200 -0
  34. package/lib/dist/installer/targets/gemini.js.map +1 -0
  35. package/lib/dist/installer/targets/registry.d.ts +5 -2
  36. package/lib/dist/installer/targets/registry.d.ts.map +1 -1
  37. package/lib/dist/installer/targets/registry.js +7 -3
  38. package/lib/dist/installer/targets/registry.js.map +1 -1
  39. package/lib/dist/installer/targets/types.d.ts +6 -2
  40. package/lib/dist/installer/targets/types.d.ts.map +1 -1
  41. package/lib/dist/jira/auto-publish.d.ts +67 -0
  42. package/lib/dist/jira/auto-publish.d.ts.map +1 -0
  43. package/lib/dist/jira/auto-publish.js +256 -0
  44. package/lib/dist/jira/auto-publish.js.map +1 -0
  45. package/lib/dist/jira/board-first.d.ts +64 -0
  46. package/lib/dist/jira/board-first.d.ts.map +1 -0
  47. package/lib/dist/jira/board-first.js +91 -0
  48. package/lib/dist/jira/board-first.js.map +1 -0
  49. package/lib/dist/jira/client.d.ts +87 -1
  50. package/lib/dist/jira/client.d.ts.map +1 -1
  51. package/lib/dist/jira/client.js +249 -2
  52. package/lib/dist/jira/client.js.map +1 -1
  53. package/lib/dist/jira/config.d.ts +1 -1
  54. package/lib/dist/jira/config.d.ts.map +1 -1
  55. package/lib/dist/jira/config.js +14 -3
  56. package/lib/dist/jira/config.js.map +1 -1
  57. package/lib/dist/jira/coverage.d.ts +105 -0
  58. package/lib/dist/jira/coverage.d.ts.map +1 -0
  59. package/lib/dist/jira/coverage.js +170 -0
  60. package/lib/dist/jira/coverage.js.map +1 -0
  61. package/lib/dist/jira/index.d.ts +1 -0
  62. package/lib/dist/jira/index.d.ts.map +1 -1
  63. package/lib/dist/jira/index.js +1 -0
  64. package/lib/dist/jira/index.js.map +1 -1
  65. package/lib/dist/jira/milestone-comment.d.ts +95 -0
  66. package/lib/dist/jira/milestone-comment.d.ts.map +1 -0
  67. package/lib/dist/jira/milestone-comment.js +271 -0
  68. package/lib/dist/jira/milestone-comment.js.map +1 -0
  69. package/lib/dist/jira/publish.d.ts +86 -2
  70. package/lib/dist/jira/publish.d.ts.map +1 -1
  71. package/lib/dist/jira/publish.js +0 -0
  72. package/lib/dist/jira/publish.js.map +1 -1
  73. package/lib/dist/jira/published-specs.d.ts +29 -0
  74. package/lib/dist/jira/published-specs.d.ts.map +1 -0
  75. package/lib/dist/jira/published-specs.js +147 -0
  76. package/lib/dist/jira/published-specs.js.map +1 -0
  77. package/lib/dist/jira/reconcile.d.ts +81 -0
  78. package/lib/dist/jira/reconcile.d.ts.map +1 -0
  79. package/lib/dist/jira/reconcile.js +111 -0
  80. package/lib/dist/jira/reconcile.js.map +1 -0
  81. package/lib/dist/jira/regression-pack.d.ts +547 -0
  82. package/lib/dist/jira/regression-pack.d.ts.map +1 -0
  83. package/lib/dist/jira/regression-pack.js +986 -0
  84. package/lib/dist/jira/regression-pack.js.map +1 -0
  85. package/lib/dist/jira/repo-config.d.ts +73 -0
  86. package/lib/dist/jira/repo-config.d.ts.map +1 -0
  87. package/lib/dist/jira/repo-config.js +249 -0
  88. package/lib/dist/jira/repo-config.js.map +1 -0
  89. package/lib/dist/jira/spec-amend.d.ts +46 -0
  90. package/lib/dist/jira/spec-amend.d.ts.map +1 -0
  91. package/lib/dist/jira/spec-amend.js +232 -0
  92. package/lib/dist/jira/spec-amend.js.map +1 -0
  93. package/lib/dist/jira/spec-writer.d.ts +22 -0
  94. package/lib/dist/jira/spec-writer.d.ts.map +1 -1
  95. package/lib/dist/jira/spec-writer.js +113 -0
  96. package/lib/dist/jira/spec-writer.js.map +1 -1
  97. package/lib/dist/jira/types.d.ts +62 -0
  98. package/lib/dist/jira/types.d.ts.map +1 -1
  99. package/lib/dist/jira/types.js +15 -1
  100. package/lib/dist/jira/types.js.map +1 -1
  101. package/lib/dist/mcp/jira-tools.d.ts +102 -0
  102. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  103. package/lib/dist/mcp/jira-tools.js +826 -53
  104. package/lib/dist/mcp/jira-tools.js.map +1 -1
  105. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  106. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  107. package/lib/dist/mcp/server-instructions.js +4 -0
  108. package/lib/dist/mcp/server-instructions.js.map +1 -1
  109. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  110. package/lib/dist/mcp/spec-tools.js +7 -1
  111. package/lib/dist/mcp/spec-tools.js.map +1 -1
  112. package/lib/dist/mcp/tools.d.ts +53 -0
  113. package/lib/dist/mcp/tools.d.ts.map +1 -1
  114. package/lib/dist/mcp/tools.js +255 -8
  115. package/lib/dist/mcp/tools.js.map +1 -1
  116. package/lib/dist/resolution/spec-link-resolver.d.ts +2 -0
  117. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  118. package/lib/dist/resolution/spec-link-resolver.js +11 -3
  119. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  120. package/lib/node_modules/.package-lock.json +1 -1
  121. package/lib/package.json +1 -1
  122. package/package.json +1 -1
@@ -5,8 +5,10 @@
5
5
  * Defines the tools exposed by the SpecShip MCP server.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.ToolHandler = exports.tools = void 0;
8
+ exports.ToolHandler = exports.tools = exports.MAX_PINNED_FILES = exports.SEEDABLE_KINDS = void 0;
9
9
  exports.getExploreBudget = getExploreBudget;
10
+ exports.extractNamedTokens = extractNamedTokens;
11
+ exports.resolveNamedFilePaths = resolveNamedFilePaths;
10
12
  exports.getExploreOutputBudget = getExploreOutputBudget;
11
13
  exports.formatStaleBanner = formatStaleBanner;
12
14
  exports.formatStaleFooter = formatStaleFooter;
@@ -87,6 +89,129 @@ function getExploreBudget(fileCount) {
87
89
  return 4;
88
90
  return 5;
89
91
  }
92
+ /**
93
+ * File extensions stripped from a named token to recover its basename
94
+ * (`server-instructions.ts` → `server-instructions`). Symbols are indexed by
95
+ * basename, so both forms are tried — see `extractNamedTokens`.
96
+ */
97
+ const FILE_EXT_RE = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
98
+ /**
99
+ * A query token that could name a symbol or a file.
100
+ *
101
+ * `-` is admitted deliberately (REQ-EXPLORE-PIN-002): kebab-case is the
102
+ * dominant filename convention here and in most TS projects, and the previous
103
+ * `[\w$]`-only pattern silently discarded every hyphenated token — so naming
104
+ * `server-instructions.ts`, the most natural way to ask for that file, seeded
105
+ * nothing at all. Admitting `-` cannot pull in unrelated files on its own: a
106
+ * token only seeds when it RESOLVES against an indexed path or symbol name, so
107
+ * prose like "point-of-use" matches nothing and falls away.
108
+ */
109
+ const NAMED_TOKEN_RE = /^[A-Za-z_$][\w$-]*(?:(?:::|[./])[\w$-]+)*$/;
110
+ /**
111
+ * NodeKinds a named token may seed the subgraph with (REQ-EXPLORE-PIN-003).
112
+ *
113
+ * Callables plus the type-ish kinds an agent routinely asks about by name — a
114
+ * configuration constant or an interface is as likely to be the subject of a
115
+ * question as a function is, and restricting to callables meant naming one had
116
+ * no effect on ranking whatsoever. Deliberately EXCLUDES `variable`,
117
+ * `property`, `field`, `parameter`, `import`, and `export`: they outnumber real
118
+ * answers by orders of magnitude and carry no standalone meaning, so seeding
119
+ * them would spend the output budget on noise. Overload floods stay bounded by
120
+ * the existing `cands.length <= 3` cap, which is kind-independent.
121
+ */
122
+ exports.SEEDABLE_KINDS = new Set([
123
+ 'method', 'function', 'component', 'constructor',
124
+ 'class', 'interface', 'type_alias', 'constant', 'enum', 'struct',
125
+ 'trait', 'protocol',
126
+ ]);
127
+ /**
128
+ * Pull the tokens from a query that could name a symbol or file, in order of
129
+ * appearance, deduplicated and capped.
130
+ *
131
+ * A token carrying a file extension yields BOTH forms — the extension-bearing
132
+ * one (the strongest signal that the agent means a file, so it must survive to
133
+ * be matched against paths) and the stripped basename (how symbols are
134
+ * indexed). Stripping is therefore additive, never a precondition.
135
+ */
136
+ function extractNamedTokens(query, limit = 16) {
137
+ const out = [];
138
+ const seen = new Set();
139
+ const push = (t) => {
140
+ if (t.length >= 3 && !seen.has(t)) {
141
+ seen.add(t);
142
+ out.push(t);
143
+ }
144
+ };
145
+ for (const raw of query.split(/[\s,()[\]]+/)) {
146
+ const t = raw.trim();
147
+ if (!t || !NAMED_TOKEN_RE.test(t))
148
+ continue;
149
+ push(t);
150
+ const stripped = t.replace(FILE_EXT_RE, '');
151
+ if (stripped !== t)
152
+ push(stripped);
153
+ }
154
+ return out.slice(0, limit);
155
+ }
156
+ /**
157
+ * Cap on files pinned by name in a single explore call (REQ-EXPLORE-PIN-001).
158
+ * Pinned files take first claim on the output budget, so an unbounded count
159
+ * would let a name-heavy query consume the whole response and crowd out the
160
+ * ranked context that answers the rest of the question.
161
+ */
162
+ exports.MAX_PINNED_FILES = 4;
163
+ /**
164
+ * Resolve query tokens that name an indexed FILE to their project-relative
165
+ * paths (REQ-EXPLORE-PIN-001).
166
+ *
167
+ * Symbol lookup alone cannot do this: `getNodesByName` / `findAllSymbols` match
168
+ * symbols, so naming a file by path found nothing and the file was left to the
169
+ * ranker — which drops it whenever it has low graph connectivity. Naming a file
170
+ * is the strongest possible relevance signal, so it gets resolved explicitly.
171
+ *
172
+ * Match order per token, first hit wins:
173
+ * 1. exact project-relative path
174
+ * 2. trailing path fragment (`mcp/server-instructions.ts`)
175
+ * 3. unique basename, with or without extension
176
+ *
177
+ * A bare basename matching MORE than one file resolves to nothing: guessing one
178
+ * of several `claude.ts` files would be worse than letting the ranker decide,
179
+ * and the agent can disambiguate by adding a path segment. Results preserve
180
+ * query order (earliest-named pins first) and are capped.
181
+ */
182
+ function resolveNamedFilePaths(tokens, filePaths, cap = exports.MAX_PINNED_FILES) {
183
+ if (tokens.length === 0 || filePaths.length === 0)
184
+ return [];
185
+ const basename = (p) => p.slice(p.lastIndexOf('/') + 1);
186
+ const stripExt = (s) => s.replace(/\.[^./]+$/, '');
187
+ const out = [];
188
+ const seen = new Set();
189
+ for (const raw of tokens) {
190
+ if (out.length >= cap)
191
+ break;
192
+ const t = raw.toLowerCase();
193
+ if (!t)
194
+ continue;
195
+ let hit;
196
+ // 1. exact path, then 2. trailing fragment — both unambiguous by shape.
197
+ hit = filePaths.find((p) => p.toLowerCase() === t)
198
+ ?? filePaths.find((p) => p.toLowerCase().endsWith(`/${t}`));
199
+ // 3. basename, with or without extension — only when it names ONE file.
200
+ if (!hit) {
201
+ const matches = filePaths.filter((p) => {
202
+ const b = basename(p).toLowerCase();
203
+ return b === t || stripExt(b) === stripExt(t);
204
+ });
205
+ if (matches.length === 1)
206
+ hit = matches[0];
207
+ }
208
+ if (hit && !seen.has(hit)) {
209
+ seen.add(hit);
210
+ out.push(hit);
211
+ }
212
+ }
213
+ return out;
214
+ }
90
215
  function getExploreOutputBudget(fileCount) {
91
216
  // Tiered budget, scaled to project size. The budget is a CEILING (relevance
92
217
  // still gates WHAT is included), and it MUST stay under the agent's INLINE
@@ -1136,6 +1261,16 @@ class ToolHandler {
1136
1261
  projectRoot: cg.getProjectRoot(),
1137
1262
  });
1138
1263
  }
1264
+ case 'specship_jira_coverage': {
1265
+ // coverage joins the bound project's active sprint to spec truth —
1266
+ // read-only by default; posts only when the caller opts in
1267
+ // (REQ-JIRATEAM-004).
1268
+ const cg = this.getSpecShip(args.projectPath);
1269
+ return await (0, jira_tools_1.handleSpecshipJiraCoverage)(args, {
1270
+ specQueries: cg.getSpecQueries(),
1271
+ projectRoot: cg.getProjectRoot(),
1272
+ });
1273
+ }
1139
1274
  case 'specship_jira_publish': {
1140
1275
  // publish writes a Story + Sub-tasks from an authored spec and
1141
1276
  // records the key in the spec file (REQ-JIRAPUB-001/-002).
@@ -1145,10 +1280,28 @@ class ToolHandler {
1145
1280
  projectRoot: cg.getProjectRoot(),
1146
1281
  });
1147
1282
  }
1283
+ case 'specship_jira_reconcile': {
1284
+ // reconcile detects JIRA-side edits to published specs and (only
1285
+ // after preview + explicit user confirmation) folds them back into
1286
+ // the spec (REQ-JIRATEAM-005).
1287
+ const cg = this.getSpecShip(args.projectPath);
1288
+ return await (0, jira_tools_1.handleSpecshipJiraReconcile)(args, {
1289
+ specQueries: cg.getSpecQueries(),
1290
+ projectRoot: cg.getProjectRoot(),
1291
+ });
1292
+ }
1148
1293
  case 'specship_jira_transition':
1149
1294
  // transition moves a tracked issue to a target state (or lists the
1150
1295
  // available ones) — independent of the code graph (REQ-JIRATRANS-001).
1151
1296
  return await (0, jira_tools_1.handleSpecshipJiraTransition)(args);
1297
+ case 'specship_jira_anchor':
1298
+ // Board-first anchor gate (REQ-JIRATEAM-007). Independent of the
1299
+ // code graph — reads the repo binding + optionally calls JIRA.
1300
+ return await (0, jira_tools_1.handleSpecshipJiraAnchor)(args);
1301
+ case 'specship_jira_epics':
1302
+ // Epic picker for the `/specship:jira` menu (REQ-JIRATEAM-008).
1303
+ // Independent of the code graph — reads the repo binding + JIRA.
1304
+ return await (0, jira_tools_1.handleSpecshipJiraEpics)(args);
1152
1305
  case 'specship_jira_add_task': {
1153
1306
  // add_task creates a discovered task under its epic/story, routed by
1154
1307
  // taskship availability (TASKSHIP-BRIDGE-DOC, REQ-TASKSHIP-003). Needs
@@ -1156,6 +1309,28 @@ class ToolHandler {
1156
1309
  const cg = this.getSpecShip(args.projectPath);
1157
1310
  return await (0, jira_tools_1.handleSpecshipJiraAddTask)(args, { projectRoot: cg.getProjectRoot() });
1158
1311
  }
1312
+ case 'specship_jira_regression_record': {
1313
+ // Recorder for a single pack case result (REQ-JIRAREG-005). Needs
1314
+ // the spec queries so it can upsert the validates-kind evidence
1315
+ // link back onto the source criterion.
1316
+ const cg = this.getSpecShip(args.projectPath);
1317
+ return await (0, jira_tools_1.handleSpecshipJiraRegressionRecord)(args, {
1318
+ specQueries: cg.getSpecQueries(),
1319
+ projectRoot: cg.getProjectRoot(),
1320
+ });
1321
+ }
1322
+ case 'specship_jira_regression_pack': {
1323
+ // Regression-pack generator (REQ-JIRAREG-001). Reads the loaded spec
1324
+ // set, plans an epic → domain-area → case hierarchy, and upserts it
1325
+ // idempotently.
1326
+ const cg = this.getSpecShip(args.projectPath);
1327
+ return await (0, jira_tools_1.handleSpecshipJiraRegressionPack)(args, {
1328
+ specQueries: cg.getSpecQueries(),
1329
+ projectRoot: cg.getProjectRoot(),
1330
+ getLinkedNodesForReq: (reqId) => cg.getLinkedNodesForReq(reqId),
1331
+ getNeighbourNodes: (nodeIds) => cg.getNeighbourNodes(nodeIds),
1332
+ });
1333
+ }
1159
1334
  default:
1160
1335
  return this.errorResult(`Unknown tool: ${toolName}`);
1161
1336
  }
@@ -1949,14 +2124,22 @@ class ToolHandler {
1949
2124
  // trace endpoint picker uses) and inject it as an entry, so every symbol the
1950
2125
  // agent explicitly named is in the subgraph and its file is scored.
1951
2126
  const namedSeedIds = new Set();
2127
+ // Named-file pinning accounting (REQ-EXPLORE-PIN-001): which files the
2128
+ // agent named that we committed to render, which were dropped by the pin
2129
+ // cap, and which file-shaped tokens matched nothing — the render stage
2130
+ // reports the last two so a miss is never silent.
2131
+ const pinnedFilePaths = [];
2132
+ const pinnedOverCap = [];
2133
+ const namedPathMisses = [];
1952
2134
  {
1953
- const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
1954
- const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
1955
2135
  const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
1956
2136
  const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
1957
- const tokens = [...new Set(query.split(/[\s,()[\]]+/)
1958
- .map((t) => t.replace(FILE_EXT, '').trim())
1959
- .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
2137
+ // Token shapes + seedable kinds are module-level and unit-tested
2138
+ // (REQ-EXPLORE-PIN-002 / -003): both filters used to silently discard
2139
+ // the most natural ways to name a target — kebab-case filenames and
2140
+ // non-callable kinds — so explore returned nothing for a symbol the
2141
+ // agent had named verbatim and it fell back to Grep.
2142
+ const tokens = extractNamedTokens(query);
1960
2143
  // PascalCase tokens in the query are type/file disambiguators — when the
1961
2144
  // agent writes "DataRequest task validate", the `task`/`validate` it wants
1962
2145
  // are DataRequest's, NOT the same-named overloads in Validation.swift /
@@ -1976,7 +2159,7 @@ class ToolHandler {
1976
2159
  const isQual = /[.\/]|::/.test(t);
1977
2160
  const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
1978
2161
  const cands = raw
1979
- .filter((n) => CALLABLE.has(n.kind) && !isTestPath(n.filePath))
2162
+ .filter((n) => exports.SEEDABLE_KINDS.has(n.kind) && !isTestPath(n.filePath))
1980
2163
  .sort((a, b) => (bodyLines(b) > 1 ? 1 : 0) - (bodyLines(a) > 1 ? 1 : 0) || bodyLines(b) - bodyLines(a));
1981
2164
  // A specific name (<=3 defs) injects all its defs. An overloaded name
1982
2165
  // (`validate` = 10, `request` = 44) would flood the subgraph, so inject
@@ -2003,6 +2186,44 @@ class ToolHandler {
2003
2186
  namedSeedIds.add(n.id);
2004
2187
  }
2005
2188
  }
2189
+ // Named FILES (REQ-EXPLORE-PIN-001). The symbol loop above resolves
2190
+ // tokens against symbol names, so a token naming a file by path matched
2191
+ // nothing and the file was left to the ranker — which drops it whenever
2192
+ // its graph connectivity is low, even though naming it is the strongest
2193
+ // relevance signal there is. Resolve those tokens against the file index
2194
+ // and seed the file's own symbols, so it inherits the same +50 score,
2195
+ // gate exemption, and first-place sort as a named symbol. Bounded by
2196
+ // MAX_PINNED_FILES so a name-heavy query can't consume the response.
2197
+ try {
2198
+ const allPaths = cg.getFiles().map((f) => f.path);
2199
+ const resolved = resolveNamedFilePaths(tokens, allPaths, Number.POSITIVE_INFINITY);
2200
+ pinnedFilePaths.push(...resolved.slice(0, exports.MAX_PINNED_FILES));
2201
+ pinnedOverCap.push(...resolved.slice(exports.MAX_PINNED_FILES));
2202
+ // A FILE-shaped token (path separator or extension — the agent named a
2203
+ // file, not a symbol) that resolves to nothing is reported, not
2204
+ // swallowed (REQ-EXPLORE-PIN-001.A5): a silent miss teaches the agent
2205
+ // that naming files does nothing, and the next lookup goes to Grep.
2206
+ for (const t of tokens) {
2207
+ if (!t.includes('/') && !FILE_EXT_RE.test(t))
2208
+ continue;
2209
+ if (resolveNamedFilePaths([t], allPaths, 1).length === 0)
2210
+ namedPathMisses.push(t);
2211
+ }
2212
+ for (const filePath of pinnedFilePaths) {
2213
+ for (const n of cg.getNodesInFile(filePath)) {
2214
+ // Same noise filter the file-grouping step applies below.
2215
+ if (n.kind === 'import' || n.kind === 'export')
2216
+ continue;
2217
+ if (!subgraph.nodes.has(n.id))
2218
+ subgraph.nodes.set(n.id, n);
2219
+ namedSeedIds.add(n.id);
2220
+ }
2221
+ }
2222
+ }
2223
+ catch {
2224
+ // A file-index hiccup must never fail the exploration — the ranked
2225
+ // path below still answers, just without the pin.
2226
+ }
2006
2227
  }
2007
2228
  // Step 2: Group nodes by file, score by relevance
2008
2229
  const fileGroups = new Map();
@@ -2074,7 +2295,11 @@ class ToolHandler {
2074
2295
  {
2075
2296
  const queryMentionsTests = /\b(test|tests|testing|spec|verify|verifies)\b/i.test(query);
2076
2297
  if (!queryMentionsTests) {
2077
- const nonLow = relevantFiles.filter(([p]) => !isLowValue(p));
2298
+ // A file the agent PINNED by name is exempt (REQ-EXPLORE-PIN-001.A2):
2299
+ // asking for a file by name outranks the low-value heuristic — the
2300
+ // agent asking for an icons/i18n/test file means it wants that file.
2301
+ const pinnedSet = new Set(pinnedFilePaths);
2302
+ const nonLow = relevantFiles.filter(([p]) => pinnedSet.has(p) || !isLowValue(p));
2078
2303
  if (nonLow.length >= 2) {
2079
2304
  relevantFiles = nonLow;
2080
2305
  }
@@ -2322,6 +2547,7 @@ class ToolHandler {
2322
2547
  let totalChars = lines.join('\n').length;
2323
2548
  let filesIncluded = 0;
2324
2549
  let anyFileTrimmed = false;
2550
+ const renderedFilePaths = new Set();
2325
2551
  for (const [filePath, group] of sortedFiles) {
2326
2552
  if (filesIncluded >= maxFiles)
2327
2553
  break;
@@ -2473,6 +2699,7 @@ class ToolHandler {
2473
2699
  lines.push(`#### ${filePath} — ${names} · ${tag}`, '', '```' + lang, skel.join('\n'), '```', '');
2474
2700
  totalChars += skel.join('\n').length + 120;
2475
2701
  filesIncluded++;
2702
+ renderedFilePaths.add(filePath);
2476
2703
  continue;
2477
2704
  }
2478
2705
  }
@@ -2519,6 +2746,7 @@ class ToolHandler {
2519
2746
  lines.push(wholeHeader, '', '```' + lang, wholeSection, '```', '');
2520
2747
  totalChars += wholeSection.length + 200;
2521
2748
  filesIncluded++;
2749
+ renderedFilePaths.add(filePath);
2522
2750
  continue;
2523
2751
  }
2524
2752
  // Cluster nearby symbols to avoid reading huge gaps between distant symbols.
@@ -2756,6 +2984,25 @@ class ToolHandler {
2756
2984
  lines.push('');
2757
2985
  totalChars += fileSection.length + 200;
2758
2986
  filesIncluded++;
2987
+ renderedFilePaths.add(filePath);
2988
+ }
2989
+ // Named-file accounting (REQ-EXPLORE-PIN-001.A3 / .A5). Emitted in EVERY
2990
+ // tier (not gated on includeCompletenessSignal): a named file that silently
2991
+ // fails to appear reads as "explore can't do targeted lookups" and sends
2992
+ // the agent to Grep — the exact decay this spec exists to stop.
2993
+ {
2994
+ const omittedNamed = [
2995
+ ...pinnedFilePaths.filter((p) => !renderedFilePaths.has(p)),
2996
+ ...pinnedOverCap,
2997
+ ];
2998
+ if (omittedNamed.length > 0) {
2999
+ lines.push(`> Named files omitted for budget: ${omittedNamed.join(', ')} — specship_explore them by name in a follow-up call for their source.`);
3000
+ lines.push('');
3001
+ }
3002
+ if (namedPathMisses.length > 0) {
3003
+ lines.push(`> Named paths not found in the index (or ambiguous): ${[...new Set(namedPathMisses)].join(', ')}. Check the spelling, or add a directory segment to disambiguate.`);
3004
+ lines.push('');
3005
+ }
2759
3006
  }
2760
3007
  // Add remaining files as references (from both relevant and peripheral files).
2761
3008
  // Small projects (per budget) skip this — the relevant story already fits