sigmap 8.24.0 → 8.26.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/CHANGELOG.md CHANGED
@@ -10,6 +10,30 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.26.0] — 2026-08-18
14
+
15
+ Minor release — **"Agent Economy III" (v8.26, F3+F4)**: the optimal SigMap usage loop ships as installable agent skills in every client's native format — completing the Agent Economy pillar (F1 budget → F2 tune → F3+F4 skills).
16
+
17
+ ### Added
18
+ - **`sigmap skills` (#517, PR #518):** new `src/skills/skills.js` — two canonical, deterministic skill documents: **sigmap-usage-maximizer** (F3 — the spend-minimizing loop: `ask` before any file read · `get_lines` for anchored ranges instead of whole files · `verify_suggestion` before trusting generated code · `squeeze` any big log/trace/JSON · checkpoint via `create_checkpoint`/`note` · check `get_budget` and, near budget, summarize-then-drop; token accounting reads the F1 ledger — no LLM calls) and **sigmap-config-optimizer** (the F2 playbook: `tune` → review the evidence-naming reasons → `tune --apply` → `validate`). Multi-client installer mirroring the `mcp/install.js` CLIENTS pattern: Claude Code (`.claude/skills/<skill>/SKILL.md`), Cursor (`.cursor/rules/*.mdc`), Windsurf (`.windsurf/rules/*.md`), GitHub Copilot (`.github/instructions/*.instructions.md`), and Codex — a marker-delimited block injected into `AGENTS.md` **above** the `## Auto-generated signatures` marker, so the codex adapter's regeneration preserves it (proven by an end-to-end regeneration test). Human content is never touched; every install is idempotent (`installed`/`updated`/`already`). CLI: `sigmap skills list` and `sigmap skills install [--client <name> | --all] [--json]` — plain `install` wires only clients whose parent artifact exists (the `--setup` only-touch-existing precedent); `--client`/`--all` create. Zero new dependencies.
19
+
20
+ ### Changed
21
+ - 8 new integration tests (132 test files); bundle rebuilt (149 modules).
22
+
23
+ ---
24
+
25
+ ## [8.25.0] — 2026-08-18
26
+
27
+ Minor release — **"Agent Economy II" (v8.25, F2)**: the discovery stack becomes a config optimizer — one command that recommends (and can apply) the config a repo actually needs.
28
+
29
+ ### Added
30
+ - **`sigmap tune` (#514, PR #515):** new `src/config/tune.js` — `buildTuneProposal(cwd)` packages the existing discovery stack (`resolveSourceRoots`, workspace markers, client-artifact probes) into a deterministic recommended-config diff with one evidence-naming reason per change. Five rules, explicit user choices never overridden: **srcDirs pin** (resolver roots, confidence-gated, never proposed against user-pinned dirs — pinned srcDirs are stable across runs and protected from token-budget drops), **monorepo** (fires when a workspace marker exists and the mode is off; reason names the marker — `pnpm-workspace.yaml`/`turbo.json`/`nx.json`/`lerna.json`/package.json `workspaces`), **adapters** (additive from client artifacts present: `CLAUDE.md`→claude, `.cursorrules`/`.cursor/`→cursor, `.windsurfrules`→windsurf, `AGENTS.md`→codex), **exclude** (curated vendored/generated dirs present at root — `third_party`, `external(s)`, `generated`, `testdata`, `tmp`, `.cache` … — appended with defaults preserved), **autoMaxTokens** (fires only when a pinned budget is below a labeled ~25-tokens/file heuristic estimate). `applyTuneProposal` merges into `gen-context.config.json` preserving every user key; re-proposal after apply is empty (idempotent). CLI: `sigmap tune` is read-only by default (`--dry-run` alias), `--apply` writes and points to `sigmap validate`, `--json` for agents. Zero new dependencies.
31
+
32
+ ### Changed
33
+ - 10 new integration tests (131 test files); bundle rebuilt (148 modules).
34
+
35
+ ---
36
+
13
37
  ## [8.24.0] — 2026-07-28
14
38
 
15
39
  Minor release — **"Trust Quick Wins I" (v8.24, G3a)**: the secret-redaction engine that already protects signatures, `get_lines`, and evidence packs becomes a standalone command for arbitrary text.
package/README.md CHANGED
@@ -59,7 +59,7 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
59
59
 
60
60
  **Proof it pays off** (full benchmark below):
61
61
  <!--SM:whyMetrics-->
62
- - **82.2% hit@5** — right file in top 5 results (vs 44.8% single-shot grep baseline — 1.59× lift)
62
+ - **82.2% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.87× lift)
63
63
  - **96.8% token reduction** — average across 21 real repos
64
64
  - **64.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
65
  - **1.53 prompts per task** — down from 2.84 (46.1% fewer retries, modeled)
@@ -122,10 +122,10 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.24-main (21 repositories, including R language)
126
- Date : 2026-07-28
125
+ Benchmark : sigmap-v8.26-main (21 repositories, including R language)
126
+ Date : 2026-08-17
127
127
 
128
- Hit@5 : 82.2% (grep-agent baseline 44.8% — 1.59× lift)
128
+ Hit@5 : 82.2% (grep-agent baseline 44.0% — 1.87× lift)
129
129
  Token reduction: 96.8% (across 21 repos)
130
130
  Prompt reduction : 46.1% (2.84 → 1.53 prompts per task, modeled)
131
131
  Task success : 64.8% (proxy — modeled from retrieval tiers)
package/gen-context.js CHANGED
@@ -1877,6 +1877,220 @@ __factories["./src/config/loader"] = function(module, exports) {
1877
1877
 
1878
1878
  };
1879
1879
 
1880
+ // ── ./src/config/tune ──
1881
+ __factories["./src/config/tune"] = function(module, exports) {
1882
+
1883
+ /**
1884
+ * sigmap tune — deterministic config optimizer (F2, #514).
1885
+ *
1886
+ * Packages the existing discovery stack (source-root-resolver, monorepo
1887
+ * markers, client-artifact probes) into a recommended config diff with a
1888
+ * one-line reason per change. Read-only by default; `applyTuneProposal`
1889
+ * merges accepted changes into gen-context.config.json, preserving every
1890
+ * user key. Explicit user choices are never proposed against.
1891
+ */
1892
+
1893
+ const fs = require('fs');
1894
+ const path = require('path');
1895
+ const { loadConfig } = __require('./src/config/loader');
1896
+ const { resolveSourceRoots } = __require('./src/discovery/source-root-resolver');
1897
+
1898
+ // Workspace markers, in probe order (reason names the first one found).
1899
+ const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
1900
+
1901
+ // Client artifacts → adapter names (additive only).
1902
+ const ADAPTER_MARKERS = [
1903
+ { adapter: 'claude', files: ['CLAUDE.md'] },
1904
+ { adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
1905
+ { adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
1906
+ { adapter: 'codex', files: ['AGENTS.md'] },
1907
+ ];
1908
+
1909
+ // Root-level dirs that are typically vendored/generated when present.
1910
+ const JUNK_DIRS = [
1911
+ 'third_party', 'thirdparty', 'external', 'externals',
1912
+ 'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
1913
+ ];
1914
+
1915
+ // Rough signature cost per source file (chars/4 world) for the budget check.
1916
+ const TOKENS_PER_FILE = 25;
1917
+
1918
+ const SOURCE_EXTS = new Set([
1919
+ '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
1920
+ '.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
1921
+ '.scala', '.php', '.gd', '.r', '.R',
1922
+ ]);
1923
+
1924
+ /** Raw user config file content, or null when absent/unparsable. */
1925
+ function _readUserConfig(cwd) {
1926
+ try {
1927
+ return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
1928
+ } catch (_) {
1929
+ return null;
1930
+ }
1931
+ }
1932
+
1933
+ /** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
1934
+ function _countSourceFiles(cwd, roots, exclude, depth = 5) {
1935
+ const excSet = new Set(exclude || []);
1936
+ let count = 0;
1937
+ const walk = (dir, d) => {
1938
+ if (d <= 0 || count > 20000) return;
1939
+ let entries;
1940
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
1941
+ for (const e of entries) {
1942
+ if (excSet.has(e.name)) continue;
1943
+ if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
1944
+ else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
1945
+ }
1946
+ };
1947
+ for (const r of roots) {
1948
+ const full = path.join(cwd, r);
1949
+ if (fs.existsSync(full)) walk(full, depth);
1950
+ }
1951
+ return count;
1952
+ }
1953
+
1954
+ /** The workspace marker present at cwd, or null. */
1955
+ function _monorepoMarker(cwd) {
1956
+ for (const m of MONOREPO_MARKERS) {
1957
+ if (fs.existsSync(path.join(cwd, m))) return m;
1958
+ }
1959
+ try {
1960
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
1961
+ if (pkg.workspaces) return 'package.json workspaces';
1962
+ } catch (_) {}
1963
+ return null;
1964
+ }
1965
+
1966
+ /**
1967
+ * Build the recommended config diff for a repo.
1968
+ *
1969
+ * @param {string} cwd
1970
+ * @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
1971
+ * detection: { roots:string[], confidence:string, isMonorepo:boolean },
1972
+ * configExists: boolean }}
1973
+ */
1974
+ function buildTuneProposal(cwd) {
1975
+ const userConfig = _readUserConfig(cwd);
1976
+ const config = loadConfig(cwd);
1977
+ const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
1978
+ const changes = [];
1979
+
1980
+ // 1. srcDirs — recommend pinning the detected roots when the user hasn't.
1981
+ // Pinned srcDirs make generation explicit/stable and are protected from
1982
+ // token-budget drops; user-pinned srcDirs are never proposed against.
1983
+ const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
1984
+ if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
1985
+ changes.push({
1986
+ key: 'srcDirs',
1987
+ current: null,
1988
+ recommended: detection.roots,
1989
+ reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
1990
+ });
1991
+ }
1992
+
1993
+ // 2. monorepo — a workspace marker exists but the mode is off.
1994
+ const marker = _monorepoMarker(cwd);
1995
+ if (marker && config.monorepo !== true) {
1996
+ changes.push({
1997
+ key: 'monorepo',
1998
+ current: config.monorepo,
1999
+ recommended: true,
2000
+ reason: `workspace marker found: ${marker}`,
2001
+ });
2002
+ }
2003
+
2004
+ // 3. adapters — client artifacts present that the adapter list doesn't cover.
2005
+ const currentAdapters = Array.isArray(config.adapters) ? config.adapters
2006
+ : (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
2007
+ const found = [];
2008
+ for (const { adapter, files } of ADAPTER_MARKERS) {
2009
+ if (currentAdapters.includes(adapter)) continue;
2010
+ const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
2011
+ if (hit) found.push({ adapter, hit });
2012
+ }
2013
+ if (found.length > 0) {
2014
+ changes.push({
2015
+ key: 'adapters',
2016
+ current: currentAdapters,
2017
+ recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
2018
+ reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
2019
+ });
2020
+ }
2021
+
2022
+ // 4. exclude — root-level vendored/generated dirs not excluded yet.
2023
+ const junkFound = JUNK_DIRS.filter((d) => {
2024
+ if ((config.exclude || []).includes(d)) return false;
2025
+ try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
2026
+ });
2027
+ if (junkFound.length > 0) {
2028
+ changes.push({
2029
+ key: 'exclude',
2030
+ current: userConfig && userConfig.exclude ? userConfig.exclude : null,
2031
+ recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
2032
+ reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
2033
+ });
2034
+ }
2035
+
2036
+ // 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
2037
+ if (config.autoMaxTokens === false) {
2038
+ const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
2039
+ const files = _countSourceFiles(cwd, roots, config.exclude);
2040
+ const estTokens = files * TOKENS_PER_FILE;
2041
+ if (estTokens > config.maxTokens) {
2042
+ changes.push({
2043
+ key: 'autoMaxTokens',
2044
+ current: false,
2045
+ recommended: true,
2046
+ reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
2047
+ });
2048
+ }
2049
+ }
2050
+
2051
+ return {
2052
+ changes,
2053
+ detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
2054
+ configExists: userConfig !== null,
2055
+ };
2056
+ }
2057
+
2058
+ /**
2059
+ * Merge a proposal's changes into gen-context.config.json (create if absent).
2060
+ * Preserves every existing user key; only the proposed keys are written.
2061
+ *
2062
+ * @returns {{ path: string, applied: string[] }}
2063
+ */
2064
+ function applyTuneProposal(cwd, proposal) {
2065
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
2066
+ const existing = _readUserConfig(cwd) || {};
2067
+ for (const c of proposal.changes) existing[c.key] = c.recommended;
2068
+ fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
2069
+ return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
2070
+ }
2071
+
2072
+ /** Human rendering of a proposal (one block per change, reason indented). */
2073
+ function formatTuneProposal(proposal) {
2074
+ const lines = [];
2075
+ if (proposal.changes.length === 0) {
2076
+ lines.push('[sigmap] tune: config already matches detection — no changes recommended');
2077
+ } else {
2078
+ lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
2079
+ for (const c of proposal.changes) {
2080
+ lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
2081
+ lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
2082
+ }
2083
+ lines.push('');
2084
+ lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
2085
+ }
2086
+ lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
2087
+ return lines.join('\n');
2088
+ }
2089
+
2090
+ module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
2091
+
2092
+ };
2093
+
1880
2094
  // ── ./src/conventions/ci ──
1881
2095
  __factories["./src/conventions/ci"] = function(module, exports) {
1882
2096
 
@@ -14981,7 +15195,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
14981
15195
 
14982
15196
  const SERVER_INFO = {
14983
15197
  name: 'sigmap',
14984
- version: '8.24.0',
15198
+ version: '8.26.0',
14985
15199
  description: 'SigMap MCP server — code signatures on demand',
14986
15200
  };
14987
15201
 
@@ -17875,6 +18089,192 @@ __factories["./src/session/notes"] = function(module, exports) {
17875
18089
 
17876
18090
  };
17877
18091
 
18092
+ // ── ./src/skills/skills ──
18093
+ __factories["./src/skills/skills"] = function(module, exports) {
18094
+
18095
+ /**
18096
+ * sigmap skills — canonical agent playbooks + multi-client installer (F3+F4, #517).
18097
+ *
18098
+ * Two deterministic skill documents (the spend-minimizing usage loop and the
18099
+ * config-optimizer playbook) emitted in each client's native skill/rules
18100
+ * format — the `mcp/install.js` CLIENTS pattern applied to skills. File-based
18101
+ * clients get sigmap-namespaced files we own outright; AGENTS.md (codex) gets
18102
+ * a marker-delimited block inserted ABOVE the `## Auto-generated signatures`
18103
+ * marker, since the codex adapter preserves content above it and replaces
18104
+ * everything below. Idempotent; human content is never touched.
18105
+ */
18106
+
18107
+ const fs = require('fs');
18108
+ const path = require('path');
18109
+
18110
+ const START = '<!-- sigmap-skills:start -->';
18111
+ const END = '<!-- sigmap-skills:end -->';
18112
+ const SIGNATURES_MARKER = '## Auto-generated signatures';
18113
+
18114
+ const SKILLS = {
18115
+ 'sigmap-usage-maximizer': {
18116
+ title: 'SigMap usage maximizer',
18117
+ description: 'Spend-minimizing loop for agents in a SigMap-indexed repo: ask before reading, read ranges, verify before trusting, squeeze big pastes, checkpoint, watch the budget.',
18118
+ body: [
18119
+ 'Follow this loop before any file exploration in a repo with SigMap installed.',
18120
+ '',
18121
+ '1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
18122
+ '2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
18123
+ '3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
18124
+ '4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
18125
+ '5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
18126
+ '6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
18127
+ ].join('\n'),
18128
+ },
18129
+ 'sigmap-config-optimizer': {
18130
+ title: 'SigMap config optimizer',
18131
+ description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
18132
+ body: [
18133
+ 'Playbook for configuring SigMap on a new or misconfigured repo.',
18134
+ '',
18135
+ '1. **Detect.** `sigmap tune` prints a recommended config diff — srcDirs pin, monorepo mode, adapters, excludes, budget — with one evidence-naming reason per change. Read-only.',
18136
+ '2. **Review.** Every reason names its evidence (workspace marker, client artifact, vendored dir, file-count estimate). Explicit user config is never proposed against.',
18137
+ '3. **Apply.** `sigmap tune --apply` merges the recommendations into `gen-context.config.json`, preserving every existing key. Idempotent — a second `tune` proposes nothing.',
18138
+ '4. **Validate.** `sigmap validate` confirms the generated context matches the config; `sigmap doctor` diagnoses anything left.',
18139
+ ].join('\n'),
18140
+ },
18141
+ };
18142
+
18143
+ // Client registry — parent = artifact whose presence means "user uses this
18144
+ // client" (the `--setup` only-touch-existing rule for plain `skills install`).
18145
+ const SKILL_CLIENTS = {
18146
+ claude: { label: 'Claude Code', parent: ['.claude'],
18147
+ target: (cwd, skill) => path.join(cwd, '.claude', 'skills', skill, 'SKILL.md') },
18148
+ cursor: { label: 'Cursor', parent: ['.cursor'],
18149
+ target: (cwd, skill) => path.join(cwd, '.cursor', 'rules', `${skill}.mdc`) },
18150
+ windsurf: { label: 'Windsurf', parent: ['.windsurf'],
18151
+ target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
18152
+ copilot: { label: 'GitHub Copilot', parent: ['.github'],
18153
+ target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
18154
+ codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
18155
+ target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
18156
+ };
18157
+
18158
+ function _footer(version) {
18159
+ const ver = version ? ` v${version}` : '';
18160
+ return `<sub>Generated by SigMap${ver} · run \`sigmap skills install\` to refresh.</sub>`;
18161
+ }
18162
+
18163
+ /** Render one skill's client-specific file content. */
18164
+ function renderSkill(client, skillName, version) {
18165
+ const skill = SKILLS[skillName];
18166
+ const body = `# ${skill.title}\n\n${skill.body}\n\n${_footer(version)}\n`;
18167
+ if (client === 'claude') {
18168
+ return `---\nname: ${skillName}\ndescription: ${skill.description}\n---\n\n${body}`;
18169
+ }
18170
+ if (client === 'cursor') {
18171
+ return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
18172
+ }
18173
+ if (client === 'copilot') {
18174
+ return `---\napplyTo: "**"\n---\n\n${body}`;
18175
+ }
18176
+ return body; // windsurf: plain markdown
18177
+ }
18178
+
18179
+ /** Render the combined AGENTS.md block (both skills, marker-delimited). */
18180
+ function renderAgentsBlock(version) {
18181
+ const parts = [START, '## SigMap agent skills', ''];
18182
+ for (const name of Object.keys(SKILLS)) {
18183
+ parts.push(`### ${SKILLS[name].title}`, '', SKILLS[name].body, '');
18184
+ }
18185
+ parts.push(_footer(version), END);
18186
+ return parts.join('\n');
18187
+ }
18188
+
18189
+ /**
18190
+ * Inject (or replace) the skills block in AGENTS.md content.
18191
+ * A new block is inserted ABOVE the `## Auto-generated signatures` marker —
18192
+ * the codex adapter's write() preserves content above that marker and
18193
+ * replaces everything below it. Never touches content outside the markers.
18194
+ */
18195
+ function injectSkillsBlock(existing, block) {
18196
+ const src = String(existing || '');
18197
+ const startIdx = src.indexOf(START);
18198
+ if (startIdx !== -1) {
18199
+ const endIdx = src.indexOf(END, startIdx);
18200
+ if (endIdx !== -1) {
18201
+ return src.slice(0, startIdx) + block + src.slice(endIdx + END.length);
18202
+ }
18203
+ }
18204
+ const sigIdx = src.indexOf(SIGNATURES_MARKER);
18205
+ if (sigIdx !== -1) {
18206
+ return src.slice(0, sigIdx) + block + '\n\n' + src.slice(sigIdx);
18207
+ }
18208
+ if (src.trim() === '') return block + '\n';
18209
+ return src + (src.endsWith('\n') ? '\n' : '\n\n') + block + '\n';
18210
+ }
18211
+
18212
+ function _writeIfChanged(filePath, content) {
18213
+ if (fs.existsSync(filePath)) {
18214
+ const existing = fs.readFileSync(filePath, 'utf8');
18215
+ if (existing === content) return 'already';
18216
+ fs.writeFileSync(filePath, content);
18217
+ return 'updated';
18218
+ }
18219
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
18220
+ fs.writeFileSync(filePath, content);
18221
+ return 'installed';
18222
+ }
18223
+
18224
+ /**
18225
+ * Install both skills for one client.
18226
+ * @returns {{ client, label, results: Array<{skill, path, status}> }}
18227
+ * status: 'installed' | 'updated' | 'already'; or { status:'unknown', valid } for a bad client.
18228
+ */
18229
+ function installSkills(client, opts = {}) {
18230
+ const spec = SKILL_CLIENTS[client];
18231
+ if (!spec) return { client, status: 'unknown', valid: Object.keys(SKILL_CLIENTS) };
18232
+ const cwd = opts.cwd || process.cwd();
18233
+ const version = opts.version || null;
18234
+ const results = [];
18235
+
18236
+ if (spec.inject) {
18237
+ const filePath = spec.target(cwd);
18238
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
18239
+ const next = injectSkillsBlock(existing, renderAgentsBlock(version));
18240
+ let status;
18241
+ if (next === existing) status = 'already';
18242
+ else { fs.writeFileSync(filePath, next); status = existing ? 'updated' : 'installed'; }
18243
+ results.push({ skill: Object.keys(SKILLS).join(' + '), path: filePath, status });
18244
+ } else {
18245
+ for (const skillName of Object.keys(SKILLS)) {
18246
+ const filePath = spec.target(cwd, skillName);
18247
+ results.push({ skill: skillName, path: filePath, status: _writeIfChanged(filePath, renderSkill(client, skillName, version)) });
18248
+ }
18249
+ }
18250
+ return { client, label: spec.label, results };
18251
+ }
18252
+
18253
+ /** True when the client's parent artifact exists (plain-install eligibility). */
18254
+ function clientPresent(client, cwd) {
18255
+ const spec = SKILL_CLIENTS[client];
18256
+ return !!spec && fs.existsSync(path.join(cwd, ...spec.parent));
18257
+ }
18258
+
18259
+ /** List clients with target paths, presence, and installed state. */
18260
+ function listSkillClients(opts = {}) {
18261
+ const cwd = opts.cwd || process.cwd();
18262
+ return Object.keys(SKILL_CLIENTS).map((client) => {
18263
+ const spec = SKILL_CLIENTS[client];
18264
+ const targets = spec.inject
18265
+ ? [spec.target(cwd)]
18266
+ : Object.keys(SKILLS).map((s) => spec.target(cwd, s));
18267
+ const installed = spec.inject
18268
+ ? (fs.existsSync(targets[0]) && fs.readFileSync(targets[0], 'utf8').includes(START))
18269
+ : targets.every((t) => fs.existsSync(t));
18270
+ return { client, label: spec.label, present: clientPresent(client, cwd), installed, targets };
18271
+ });
18272
+ }
18273
+
18274
+ module.exports = { SKILLS, SKILL_CLIENTS, renderSkill, renderAgentsBlock, injectSkillsBlock, installSkills, listSkillClients, clientPresent, START, END };
18275
+
18276
+ };
18277
+
17878
18278
  // ── ./src/squeeze/cilog ──
17879
18279
  __factories["./src/squeeze/cilog"] = function(module, exports) {
17880
18280
 
@@ -20407,7 +20807,7 @@ function __tryGit(args, opts = {}) {
20407
20807
  catch (_) { return ''; }
20408
20808
  }
20409
20809
 
20410
- const VERSION = '8.24.0';
20810
+ const VERSION = '8.26.0';
20411
20811
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
20412
20812
 
20413
20813
  function requireSourceOrBundled(key) {
@@ -22295,6 +22695,10 @@ Usage:
22295
22695
  ${cmd} budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
22296
22696
  ${cmd} budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
22297
22697
  ${cmd} redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
22698
+ ${cmd} tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
22699
+ ${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
22700
+ ${cmd} skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
22701
+ ${cmd} skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
22298
22702
  ${cmd} note "<text>" Append a note to the cross-session decision log
22299
22703
  ${cmd} note List recent notes (also: note --list <N>)
22300
22704
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -23694,6 +24098,94 @@ function main() {
23694
24098
  process.exit(0);
23695
24099
  }
23696
24100
 
24101
+ // `sigmap tune [--apply|--dry-run] [--json]` — deterministic config optimizer:
24102
+ // recommend srcDirs/monorepo/adapters/exclude/autoMaxTokens from the existing
24103
+ // discovery stack, one reason per change. Read-only unless --apply.
24104
+ if (args[0] === 'tune') {
24105
+ const { buildTuneProposal, applyTuneProposal, formatTuneProposal } = requireSourceOrBundled('./src/config/tune');
24106
+ let proposal;
24107
+ try {
24108
+ proposal = buildTuneProposal(cwd);
24109
+ } catch (err) {
24110
+ console.error(`[sigmap] tune failed: ${err.message}`);
24111
+ process.exit(1);
24112
+ }
24113
+ if (args.includes('--apply') && proposal.changes.length > 0) {
24114
+ const res = applyTuneProposal(cwd, proposal);
24115
+ if (args.includes('--json')) {
24116
+ process.stdout.write(JSON.stringify({ ...proposal, applied: res.applied, path: res.path }) + '\n');
24117
+ } else {
24118
+ console.log(formatTuneProposal(proposal));
24119
+ console.log(`[sigmap] tune: wrote ${res.applied.join(', ')} → ${path.relative(cwd, res.path)} — run: sigmap validate`);
24120
+ }
24121
+ process.exit(0);
24122
+ }
24123
+ if (args.includes('--json')) {
24124
+ process.stdout.write(JSON.stringify(proposal) + '\n');
24125
+ } else {
24126
+ console.log(formatTuneProposal(proposal));
24127
+ }
24128
+ process.exit(0);
24129
+ }
24130
+
24131
+ // `sigmap skills list|install [--client <name>|--all] [--json]` — install the
24132
+ // agent playbooks (usage-maximizer, config-optimizer) in each client's native
24133
+ // skill/rules format. Plain `install` touches only clients whose parent
24134
+ // artifact exists; --client/--all create. Idempotent.
24135
+ if (args[0] === 'skills') {
24136
+ const { installSkills, listSkillClients, SKILL_CLIENTS } = requireSourceOrBundled('./src/skills/skills');
24137
+ const sub = args[1];
24138
+ const jsonOut = args.includes('--json');
24139
+
24140
+ if (sub === 'list' || sub === undefined) {
24141
+ const clients = listSkillClients({ cwd });
24142
+ if (jsonOut) {
24143
+ process.stdout.write(JSON.stringify(clients, null, 2) + '\n');
24144
+ process.exit(0);
24145
+ }
24146
+ console.log('Supported skill clients:\n');
24147
+ for (const c of clients) {
24148
+ const state = c.installed ? 'installed' : (c.present ? 'client present, skills not installed' : 'client not detected');
24149
+ console.log(` ${c.client.padEnd(10)} ${c.label.padEnd(24)} ${state}`);
24150
+ for (const t of c.targets) console.log(` ${path.relative(cwd, t)}`);
24151
+ }
24152
+ console.log('\nInstall with: sigmap skills install [--client <name> | --all]');
24153
+ process.exit(0);
24154
+ }
24155
+
24156
+ if (sub === 'install') {
24157
+ const cIdx = args.indexOf('--client');
24158
+ const one = cIdx !== -1 ? args[cIdx + 1] : null;
24159
+ let targets;
24160
+ if (one) targets = [one];
24161
+ else if (args.includes('--all')) targets = Object.keys(SKILL_CLIENTS);
24162
+ else targets = listSkillClients({ cwd }).filter((c) => c.present).map((c) => c.client);
24163
+ if (targets.length === 0) {
24164
+ console.error('[sigmap] skills: no clients detected — use --client <name> or --all (see: sigmap skills list)');
24165
+ process.exit(1);
24166
+ }
24167
+ const out = targets.map((c) => installSkills(c, { cwd, version: VERSION }));
24168
+ const bad = out.find((r) => r.status === 'unknown');
24169
+ if (bad) {
24170
+ console.error(`[sigmap] skills: unknown client "${bad.client}" — valid: ${bad.valid.join(', ')}`);
24171
+ process.exit(1);
24172
+ }
24173
+ if (jsonOut) {
24174
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
24175
+ process.exit(0);
24176
+ }
24177
+ for (const r of out) {
24178
+ for (const res of r.results) {
24179
+ console.log(` ${r.client.padEnd(10)} ${res.status.padEnd(10)} ${path.relative(cwd, res.path)}`);
24180
+ }
24181
+ }
24182
+ process.exit(0);
24183
+ }
24184
+
24185
+ console.error('[sigmap] usage: sigmap skills [list | install [--client <name> | --all]] [--json]');
24186
+ process.exit(1);
24187
+ }
24188
+
23697
24189
  // `sigmap note "<text>"` — append to the cross-session decision log.
23698
24190
  // With no text, lists recent notes (also `note --list [N]`).
23699
24191
  // `sigmap memory` — one view over the cross-session stores in .context/.
package/llms-full.txt CHANGED
@@ -11,17 +11,17 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.24.0 | Benchmark: sigmap-v8.24-main (2026-07-28)
14
+ # Version: 8.26.0 | Benchmark: sigmap-v8.26-main (2026-08-17)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v8.24-main, 2026-07-28)
20
+ ## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-17)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 44.8% (single-shot grep) | 82.2% (1.59× lift) |
24
+ | Retrieval hit@5 | 44.0% (single-shot grep) | 82.2% (1.87× lift) |
25
25
  | Token reduction | — | 96.8% average |
26
26
  | Task-success proxy (modeled) | — | 64.8% |
27
27
  | Prompts per task | 2.84 | 1.53 (46.1% fewer) |
@@ -128,6 +128,10 @@ sigmap memory --clear <store> Clear one store: session|notes|weights|
128
128
  sigmap budget Session spend ledger — estimated SigMap-emitted tokens, budget, context age (--json)
129
129
  sigmap budget --budget <tokens> One-off budget override (config: sessionBudgetTokens, contextTtlDays)
130
130
  sigmap redact [file] Mask secrets in a file or stdin (10-pattern bank); redacted text to stdout (--json)
131
+ sigmap tune Recommend config from repo detection — srcDirs, monorepo, adapters, exclude, budget (--json)
132
+ sigmap tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
133
+ sigmap skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
134
+ sigmap skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
131
135
  sigmap note "<text>" Append a note to the cross-session decision log
132
136
  sigmap note List recent notes (also: note --list <N>)
133
137
  sigmap status Show repo state — branch, dirty files, index freshness, notes
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.24.0 | Benchmark: sigmap-v8.24-main (2026-07-28)
14
+ # Version: 8.26.0 | Benchmark: sigmap-v8.26-main (2026-08-17)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,9 +23,9 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v8.24-main, 2026-07-28)
26
+ ## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-17)
27
27
 
28
- - hit@5 retrieval: 82.2% vs 44.8% single-shot grep baseline (1.59× lift)
28
+ - hit@5 retrieval: 82.2% vs 44.0% single-shot grep baseline (1.87× lift)
29
29
  - Token reduction: 96.8% average across benchmark repos
30
30
  - Task-success proxy: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
31
31
  - Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.24.0",
3
+ "version": "8.26.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.24.0",
3
+ "version": "8.26.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.24.0",
3
+ "version": "8.26.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sigmap tune — deterministic config optimizer (F2, #514).
5
+ *
6
+ * Packages the existing discovery stack (source-root-resolver, monorepo
7
+ * markers, client-artifact probes) into a recommended config diff with a
8
+ * one-line reason per change. Read-only by default; `applyTuneProposal`
9
+ * merges accepted changes into gen-context.config.json, preserving every
10
+ * user key. Explicit user choices are never proposed against.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { loadConfig } = require('./loader');
16
+ const { resolveSourceRoots } = require('../discovery/source-root-resolver');
17
+
18
+ // Workspace markers, in probe order (reason names the first one found).
19
+ const MONOREPO_MARKERS = ['pnpm-workspace.yaml', 'turbo.json', 'nx.json', 'lerna.json'];
20
+
21
+ // Client artifacts → adapter names (additive only).
22
+ const ADAPTER_MARKERS = [
23
+ { adapter: 'claude', files: ['CLAUDE.md'] },
24
+ { adapter: 'cursor', files: ['.cursorrules', '.cursor'] },
25
+ { adapter: 'windsurf', files: ['.windsurfrules', '.windsurf'] },
26
+ { adapter: 'codex', files: ['AGENTS.md'] },
27
+ ];
28
+
29
+ // Root-level dirs that are typically vendored/generated when present.
30
+ const JUNK_DIRS = [
31
+ 'third_party', 'thirdparty', 'external', 'externals',
32
+ 'generated', 'testdata', 'snapshots', 'tmp', 'temp', '.cache',
33
+ ];
34
+
35
+ // Rough signature cost per source file (chars/4 world) for the budget check.
36
+ const TOKENS_PER_FILE = 25;
37
+
38
+ const SOURCE_EXTS = new Set([
39
+ '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
40
+ '.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
41
+ '.scala', '.php', '.gd', '.r', '.R',
42
+ ]);
43
+
44
+ /** Raw user config file content, or null when absent/unparsable. */
45
+ function _readUserConfig(cwd) {
46
+ try {
47
+ return JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
48
+ } catch (_) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /** Count source files under `roots` (relative to cwd), depth-capped, deterministic. */
54
+ function _countSourceFiles(cwd, roots, exclude, depth = 5) {
55
+ const excSet = new Set(exclude || []);
56
+ let count = 0;
57
+ const walk = (dir, d) => {
58
+ if (d <= 0 || count > 20000) return;
59
+ let entries;
60
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
61
+ for (const e of entries) {
62
+ if (excSet.has(e.name)) continue;
63
+ if (e.isDirectory()) walk(path.join(dir, e.name), d - 1);
64
+ else if (e.isFile() && SOURCE_EXTS.has(path.extname(e.name))) count++;
65
+ }
66
+ };
67
+ for (const r of roots) {
68
+ const full = path.join(cwd, r);
69
+ if (fs.existsSync(full)) walk(full, depth);
70
+ }
71
+ return count;
72
+ }
73
+
74
+ /** The workspace marker present at cwd, or null. */
75
+ function _monorepoMarker(cwd) {
76
+ for (const m of MONOREPO_MARKERS) {
77
+ if (fs.existsSync(path.join(cwd, m))) return m;
78
+ }
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
81
+ if (pkg.workspaces) return 'package.json workspaces';
82
+ } catch (_) {}
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Build the recommended config diff for a repo.
88
+ *
89
+ * @param {string} cwd
90
+ * @returns {{ changes: Array<{key:string, current:*, recommended:*, reason:string}>,
91
+ * detection: { roots:string[], confidence:string, isMonorepo:boolean },
92
+ * configExists: boolean }}
93
+ */
94
+ function buildTuneProposal(cwd) {
95
+ const userConfig = _readUserConfig(cwd);
96
+ const config = loadConfig(cwd);
97
+ const detection = resolveSourceRoots(cwd, { exclude: config.exclude });
98
+ const changes = [];
99
+
100
+ // 1. srcDirs — recommend pinning the detected roots when the user hasn't.
101
+ // Pinned srcDirs make generation explicit/stable and are protected from
102
+ // token-budget drops; user-pinned srcDirs are never proposed against.
103
+ const userPinnedSrcDirs = !!(userConfig && Array.isArray(userConfig.srcDirs));
104
+ if (!userPinnedSrcDirs && detection.roots.length > 0 && detection.confidence !== 'low') {
105
+ changes.push({
106
+ key: 'srcDirs',
107
+ current: null,
108
+ recommended: detection.roots,
109
+ reason: `pin the ${detection.roots.length} detected source root(s) [confidence ${detection.confidence}] — explicit srcDirs are stable across runs and protected from budget drops`,
110
+ });
111
+ }
112
+
113
+ // 2. monorepo — a workspace marker exists but the mode is off.
114
+ const marker = _monorepoMarker(cwd);
115
+ if (marker && config.monorepo !== true) {
116
+ changes.push({
117
+ key: 'monorepo',
118
+ current: config.monorepo,
119
+ recommended: true,
120
+ reason: `workspace marker found: ${marker}`,
121
+ });
122
+ }
123
+
124
+ // 3. adapters — client artifacts present that the adapter list doesn't cover.
125
+ const currentAdapters = Array.isArray(config.adapters) ? config.adapters
126
+ : (Array.isArray(config.outputs) ? config.outputs : ['copilot']);
127
+ const found = [];
128
+ for (const { adapter, files } of ADAPTER_MARKERS) {
129
+ if (currentAdapters.includes(adapter)) continue;
130
+ const hit = files.find((f) => fs.existsSync(path.join(cwd, f)));
131
+ if (hit) found.push({ adapter, hit });
132
+ }
133
+ if (found.length > 0) {
134
+ changes.push({
135
+ key: 'adapters',
136
+ current: currentAdapters,
137
+ recommended: [...currentAdapters, ...found.map((f) => f.adapter)],
138
+ reason: `client files present: ${found.map((f) => f.hit).join(', ')}`,
139
+ });
140
+ }
141
+
142
+ // 4. exclude — root-level vendored/generated dirs not excluded yet.
143
+ const junkFound = JUNK_DIRS.filter((d) => {
144
+ if ((config.exclude || []).includes(d)) return false;
145
+ try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
146
+ });
147
+ if (junkFound.length > 0) {
148
+ changes.push({
149
+ key: 'exclude',
150
+ current: userConfig && userConfig.exclude ? userConfig.exclude : null,
151
+ recommended: [...((userConfig && userConfig.exclude) || config.exclude), ...junkFound],
152
+ reason: `present at root and typically vendored/generated: ${junkFound.join(', ')}`,
153
+ });
154
+ }
155
+
156
+ // 5. autoMaxTokens — a pinned budget that the repo's size will overflow.
157
+ if (config.autoMaxTokens === false) {
158
+ const roots = detection.roots.length > 0 ? detection.roots : config.srcDirs;
159
+ const files = _countSourceFiles(cwd, roots, config.exclude);
160
+ const estTokens = files * TOKENS_PER_FILE;
161
+ if (estTokens > config.maxTokens) {
162
+ changes.push({
163
+ key: 'autoMaxTokens',
164
+ current: false,
165
+ recommended: true,
166
+ reason: `~${files} source files need ~${estTokens} tokens (heuristic) but maxTokens is pinned at ${config.maxTokens} — auto-scaling targets ${Math.round((config.coverageTarget || 0.8) * 100)}% coverage`,
167
+ });
168
+ }
169
+ }
170
+
171
+ return {
172
+ changes,
173
+ detection: { roots: detection.roots, confidence: detection.confidence, isMonorepo: detection.isMonorepo },
174
+ configExists: userConfig !== null,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Merge a proposal's changes into gen-context.config.json (create if absent).
180
+ * Preserves every existing user key; only the proposed keys are written.
181
+ *
182
+ * @returns {{ path: string, applied: string[] }}
183
+ */
184
+ function applyTuneProposal(cwd, proposal) {
185
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
186
+ const existing = _readUserConfig(cwd) || {};
187
+ for (const c of proposal.changes) existing[c.key] = c.recommended;
188
+ fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2) + '\n');
189
+ return { path: cfgPath, applied: proposal.changes.map((c) => c.key) };
190
+ }
191
+
192
+ /** Human rendering of a proposal (one block per change, reason indented). */
193
+ function formatTuneProposal(proposal) {
194
+ const lines = [];
195
+ if (proposal.changes.length === 0) {
196
+ lines.push('[sigmap] tune: config already matches detection — no changes recommended');
197
+ } else {
198
+ lines.push(`[sigmap] tune: ${proposal.changes.length} recommended change(s)`);
199
+ for (const c of proposal.changes) {
200
+ lines.push(` ${c.key.padEnd(14)} ${JSON.stringify(c.current)} → ${JSON.stringify(c.recommended)}`);
201
+ lines.push(` ${''.padEnd(14)} reason: ${c.reason}`);
202
+ }
203
+ lines.push('');
204
+ lines.push(' apply with: sigmap tune --apply (then: sigmap validate)');
205
+ }
206
+ lines.push(` detection: roots [${proposal.detection.roots.join(', ')}] · confidence ${proposal.detection.confidence} · monorepo ${proposal.detection.isMonorepo ? 'yes' : 'no'}`);
207
+ return lines.join('\n');
208
+ }
209
+
210
+ module.exports = { buildTuneProposal, applyTuneProposal, formatTuneProposal, JUNK_DIRS, TOKENS_PER_FILE };
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.24.0',
21
+ version: '8.26.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * sigmap skills — canonical agent playbooks + multi-client installer (F3+F4, #517).
5
+ *
6
+ * Two deterministic skill documents (the spend-minimizing usage loop and the
7
+ * config-optimizer playbook) emitted in each client's native skill/rules
8
+ * format — the `mcp/install.js` CLIENTS pattern applied to skills. File-based
9
+ * clients get sigmap-namespaced files we own outright; AGENTS.md (codex) gets
10
+ * a marker-delimited block inserted ABOVE the `## Auto-generated signatures`
11
+ * marker, since the codex adapter preserves content above it and replaces
12
+ * everything below. Idempotent; human content is never touched.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const START = '<!-- sigmap-skills:start -->';
19
+ const END = '<!-- sigmap-skills:end -->';
20
+ const SIGNATURES_MARKER = '## Auto-generated signatures';
21
+
22
+ const SKILLS = {
23
+ 'sigmap-usage-maximizer': {
24
+ title: 'SigMap usage maximizer',
25
+ description: 'Spend-minimizing loop for agents in a SigMap-indexed repo: ask before reading, read ranges, verify before trusting, squeeze big pastes, checkpoint, watch the budget.',
26
+ body: [
27
+ 'Follow this loop before any file exploration in a repo with SigMap installed.',
28
+ '',
29
+ '1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
30
+ '2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
31
+ '3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
32
+ '4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
33
+ '5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
34
+ '6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
35
+ ].join('\n'),
36
+ },
37
+ 'sigmap-config-optimizer': {
38
+ title: 'SigMap config optimizer',
39
+ description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
40
+ body: [
41
+ 'Playbook for configuring SigMap on a new or misconfigured repo.',
42
+ '',
43
+ '1. **Detect.** `sigmap tune` prints a recommended config diff — srcDirs pin, monorepo mode, adapters, excludes, budget — with one evidence-naming reason per change. Read-only.',
44
+ '2. **Review.** Every reason names its evidence (workspace marker, client artifact, vendored dir, file-count estimate). Explicit user config is never proposed against.',
45
+ '3. **Apply.** `sigmap tune --apply` merges the recommendations into `gen-context.config.json`, preserving every existing key. Idempotent — a second `tune` proposes nothing.',
46
+ '4. **Validate.** `sigmap validate` confirms the generated context matches the config; `sigmap doctor` diagnoses anything left.',
47
+ ].join('\n'),
48
+ },
49
+ };
50
+
51
+ // Client registry — parent = artifact whose presence means "user uses this
52
+ // client" (the `--setup` only-touch-existing rule for plain `skills install`).
53
+ const SKILL_CLIENTS = {
54
+ claude: { label: 'Claude Code', parent: ['.claude'],
55
+ target: (cwd, skill) => path.join(cwd, '.claude', 'skills', skill, 'SKILL.md') },
56
+ cursor: { label: 'Cursor', parent: ['.cursor'],
57
+ target: (cwd, skill) => path.join(cwd, '.cursor', 'rules', `${skill}.mdc`) },
58
+ windsurf: { label: 'Windsurf', parent: ['.windsurf'],
59
+ target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
60
+ copilot: { label: 'GitHub Copilot', parent: ['.github'],
61
+ target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
62
+ codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
63
+ target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
64
+ };
65
+
66
+ function _footer(version) {
67
+ const ver = version ? ` v${version}` : '';
68
+ return `<sub>Generated by SigMap${ver} · run \`sigmap skills install\` to refresh.</sub>`;
69
+ }
70
+
71
+ /** Render one skill's client-specific file content. */
72
+ function renderSkill(client, skillName, version) {
73
+ const skill = SKILLS[skillName];
74
+ const body = `# ${skill.title}\n\n${skill.body}\n\n${_footer(version)}\n`;
75
+ if (client === 'claude') {
76
+ return `---\nname: ${skillName}\ndescription: ${skill.description}\n---\n\n${body}`;
77
+ }
78
+ if (client === 'cursor') {
79
+ return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
80
+ }
81
+ if (client === 'copilot') {
82
+ return `---\napplyTo: "**"\n---\n\n${body}`;
83
+ }
84
+ return body; // windsurf: plain markdown
85
+ }
86
+
87
+ /** Render the combined AGENTS.md block (both skills, marker-delimited). */
88
+ function renderAgentsBlock(version) {
89
+ const parts = [START, '## SigMap agent skills', ''];
90
+ for (const name of Object.keys(SKILLS)) {
91
+ parts.push(`### ${SKILLS[name].title}`, '', SKILLS[name].body, '');
92
+ }
93
+ parts.push(_footer(version), END);
94
+ return parts.join('\n');
95
+ }
96
+
97
+ /**
98
+ * Inject (or replace) the skills block in AGENTS.md content.
99
+ * A new block is inserted ABOVE the `## Auto-generated signatures` marker —
100
+ * the codex adapter's write() preserves content above that marker and
101
+ * replaces everything below it. Never touches content outside the markers.
102
+ */
103
+ function injectSkillsBlock(existing, block) {
104
+ const src = String(existing || '');
105
+ const startIdx = src.indexOf(START);
106
+ if (startIdx !== -1) {
107
+ const endIdx = src.indexOf(END, startIdx);
108
+ if (endIdx !== -1) {
109
+ return src.slice(0, startIdx) + block + src.slice(endIdx + END.length);
110
+ }
111
+ }
112
+ const sigIdx = src.indexOf(SIGNATURES_MARKER);
113
+ if (sigIdx !== -1) {
114
+ return src.slice(0, sigIdx) + block + '\n\n' + src.slice(sigIdx);
115
+ }
116
+ if (src.trim() === '') return block + '\n';
117
+ return src + (src.endsWith('\n') ? '\n' : '\n\n') + block + '\n';
118
+ }
119
+
120
+ function _writeIfChanged(filePath, content) {
121
+ if (fs.existsSync(filePath)) {
122
+ const existing = fs.readFileSync(filePath, 'utf8');
123
+ if (existing === content) return 'already';
124
+ fs.writeFileSync(filePath, content);
125
+ return 'updated';
126
+ }
127
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
128
+ fs.writeFileSync(filePath, content);
129
+ return 'installed';
130
+ }
131
+
132
+ /**
133
+ * Install both skills for one client.
134
+ * @returns {{ client, label, results: Array<{skill, path, status}> }}
135
+ * status: 'installed' | 'updated' | 'already'; or { status:'unknown', valid } for a bad client.
136
+ */
137
+ function installSkills(client, opts = {}) {
138
+ const spec = SKILL_CLIENTS[client];
139
+ if (!spec) return { client, status: 'unknown', valid: Object.keys(SKILL_CLIENTS) };
140
+ const cwd = opts.cwd || process.cwd();
141
+ const version = opts.version || null;
142
+ const results = [];
143
+
144
+ if (spec.inject) {
145
+ const filePath = spec.target(cwd);
146
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
147
+ const next = injectSkillsBlock(existing, renderAgentsBlock(version));
148
+ let status;
149
+ if (next === existing) status = 'already';
150
+ else { fs.writeFileSync(filePath, next); status = existing ? 'updated' : 'installed'; }
151
+ results.push({ skill: Object.keys(SKILLS).join(' + '), path: filePath, status });
152
+ } else {
153
+ for (const skillName of Object.keys(SKILLS)) {
154
+ const filePath = spec.target(cwd, skillName);
155
+ results.push({ skill: skillName, path: filePath, status: _writeIfChanged(filePath, renderSkill(client, skillName, version)) });
156
+ }
157
+ }
158
+ return { client, label: spec.label, results };
159
+ }
160
+
161
+ /** True when the client's parent artifact exists (plain-install eligibility). */
162
+ function clientPresent(client, cwd) {
163
+ const spec = SKILL_CLIENTS[client];
164
+ return !!spec && fs.existsSync(path.join(cwd, ...spec.parent));
165
+ }
166
+
167
+ /** List clients with target paths, presence, and installed state. */
168
+ function listSkillClients(opts = {}) {
169
+ const cwd = opts.cwd || process.cwd();
170
+ return Object.keys(SKILL_CLIENTS).map((client) => {
171
+ const spec = SKILL_CLIENTS[client];
172
+ const targets = spec.inject
173
+ ? [spec.target(cwd)]
174
+ : Object.keys(SKILLS).map((s) => spec.target(cwd, s));
175
+ const installed = spec.inject
176
+ ? (fs.existsSync(targets[0]) && fs.readFileSync(targets[0], 'utf8').includes(START))
177
+ : targets.every((t) => fs.existsSync(t));
178
+ return { client, label: spec.label, present: clientPresent(client, cwd), installed, targets };
179
+ });
180
+ }
181
+
182
+ module.exports = { SKILLS, SKILL_CLIENTS, renderSkill, renderAgentsBlock, injectSkillsBlock, installSkills, listSkillClients, clientPresent, START, END };