@event4u/agent-config 14.22.0 → 14.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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +159 -380
- package/README.md +1 -1
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
- package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/archive_completed_roadmaps.js +52 -4
- package/dist/cli-delegate/{chunk-TGIPZBCW.js → chunk-2KKXJW25.js} +22 -8
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/{chunk-BIXEDQPZ.js → chunk-KCNTUSPI.js} +1 -1
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +20 -12
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +3 -13
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +880 -289
- package/dist/install/install.mjs +15 -3
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +5458 -2453
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +20 -20
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +56 -0
- package/docs/contracts/settings-classes.md +39 -7
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +106 -4
- package/src/config/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +7 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/hook-token-budget.json +7 -6
- package/src/config/packed-binary-manifest.json +1 -1
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- package/src/scripts/_lib/continuity_slot.ts +295 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/hook_settings.ts +85 -0
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +37 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- package/src/scripts/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- package/src/scripts/check_host_tree_parity.ts +291 -0
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +79 -0
- package/src/scripts/code_graph/cli.ts +258 -7
- package/src/scripts/code_graph/detect.ts +38 -0
- package/src/scripts/code_graph/query.ts +25 -2
- package/src/scripts/code_graph/sqlite_store.ts +16 -1
- package/src/scripts/code_graph/types.ts +52 -3
- package/src/scripts/code_graph/validate.ts +3 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +20 -20
- package/src/scripts/handoff_context_hook.ts +39 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +24 -1
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/code_graph_context_hook.ts +7 -18
- package/src/scripts/hooks/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +107 -20
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hot_context_hook.ts +62 -7
- package/src/scripts/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +3 -13
- package/src/scripts/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- package/src/scripts/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- package/src/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +27 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"description": "Source-of-truth catalog of consumer-relevant MCP tools. Read by the stdio server (scripts/mcp_server/) and packed into the Cloud Worker bundle (internal/workers/mcp/). The 'implemented_on' field lists transports where the real handler is wired. Entries marked '[stub — implemented on demand]' are DOCUMENTATION of the on-demand backlog: per ADR-132 they are NOT registered on the stdio tools/list (the wire carries implemented tools only); the 'not_implemented' envelope contract (docs/contracts/mcp-tool-stub-envelope.md) remains for Worker parity. Generated by `build_mcp_catalog` — never hand-edit this file.",
|
|
4
|
-
"install_hint_stdio": "
|
|
3
|
+
"description": "Source-of-truth catalog of consumer-relevant MCP tools. Read by the stdio server (scripts/mcp_server/) and packed into the Cloud Worker bundle (internal/workers/mcp/). The 'implemented_on' field lists transports where the real handler is wired. Entries marked '[stub — implemented on demand]' are DOCUMENTATION of the on-demand backlog: per ADR-132 they are NOT registered on the stdio tools/list (the wire carries implemented tools only); the 'not_implemented' envelope contract (docs/contracts/mcp-tool-stub-envelope.md) remains for Worker parity. The 'install_hint_stdio' field is the INSTALLED-BINARY form and therefore assumes `agent-config` is on PATH (a global install, or an npx-resolved one the client already has); it deliberately carries no `npx` invocation, because the un-prompted form of that command is the one string this file's own gate forbids and the prompted form hangs a non-interactive client start. Generated by `build_mcp_catalog` — never hand-edit this file.",
|
|
4
|
+
"install_hint_stdio": "agent-config mcp-server",
|
|
5
5
|
"tools": [
|
|
6
6
|
{
|
|
7
7
|
"name": "capabilities_index",
|
|
@@ -222,6 +222,156 @@
|
|
|
222
222
|
"readOnlyHint": true
|
|
223
223
|
}
|
|
224
224
|
},
|
|
225
|
+
{
|
|
226
|
+
"name": "graph_dead",
|
|
227
|
+
"description": "Symbols with no accepted reference that no declared entry point names. REFUSES (status `refused`) while a source is unreadable: with no exportedness recorded, an exported-but-unimported symbol is indistinguishable from a dead one. Never a deletion recommendation.",
|
|
228
|
+
"side_effect": "ro",
|
|
229
|
+
"implemented_on": [
|
|
230
|
+
"stdio"
|
|
231
|
+
],
|
|
232
|
+
"input_schema": {
|
|
233
|
+
"type": "object",
|
|
234
|
+
"properties": {
|
|
235
|
+
"entry_points": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Path under this root to a newline-separated id/label list; `#` comments ok."
|
|
238
|
+
},
|
|
239
|
+
"accept_missing_exports": {
|
|
240
|
+
"type": "boolean",
|
|
241
|
+
"default": false,
|
|
242
|
+
"description": "Answer anyway, accepting the gap. Never a default."
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"additionalProperties": false
|
|
246
|
+
},
|
|
247
|
+
"annotations": {
|
|
248
|
+
"readOnlyHint": true
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "graph_impact",
|
|
253
|
+
"description": "What a diff can break: callers, dependents and test files reaching the changed symbols. Walks only stated-fact edges, never a same-name guess, and reports the guesses refused. Runs `git diff` for the rev.",
|
|
254
|
+
"side_effect": "shell",
|
|
255
|
+
"implemented_on": [
|
|
256
|
+
"stdio"
|
|
257
|
+
],
|
|
258
|
+
"input_schema": {
|
|
259
|
+
"type": "object",
|
|
260
|
+
"required": [
|
|
261
|
+
"diff"
|
|
262
|
+
],
|
|
263
|
+
"properties": {
|
|
264
|
+
"diff": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "Git rev to diff HEAD against, e.g. `HEAD~1`."
|
|
267
|
+
},
|
|
268
|
+
"depth": {
|
|
269
|
+
"type": "integer",
|
|
270
|
+
"minimum": 1,
|
|
271
|
+
"default": 2,
|
|
272
|
+
"description": "Hop limit."
|
|
273
|
+
},
|
|
274
|
+
"untested_only": {
|
|
275
|
+
"type": "boolean",
|
|
276
|
+
"default": false,
|
|
277
|
+
"description": "Instead return changed symbols no test imports."
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"additionalProperties": false
|
|
281
|
+
},
|
|
282
|
+
"annotations": {
|
|
283
|
+
"readOnlyHint": false
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "graph_path",
|
|
288
|
+
"description": "The shortest relation chain between two symbols, direction preserved. Answers \"how does A reach B\" without reading the files between. `(no path found)` when unconnected is an answer.",
|
|
289
|
+
"side_effect": "ro",
|
|
290
|
+
"implemented_on": [
|
|
291
|
+
"stdio"
|
|
292
|
+
],
|
|
293
|
+
"input_schema": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"required": [
|
|
296
|
+
"from",
|
|
297
|
+
"to"
|
|
298
|
+
],
|
|
299
|
+
"properties": {
|
|
300
|
+
"from": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"description": "Start node id, label, or free text."
|
|
303
|
+
},
|
|
304
|
+
"to": {
|
|
305
|
+
"type": "string",
|
|
306
|
+
"description": "Goal node id, label, or free text."
|
|
307
|
+
},
|
|
308
|
+
"budget": {
|
|
309
|
+
"type": "integer",
|
|
310
|
+
"minimum": 1,
|
|
311
|
+
"default": 1500,
|
|
312
|
+
"description": "Token budget for lines."
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
"additionalProperties": false
|
|
316
|
+
},
|
|
317
|
+
"annotations": {
|
|
318
|
+
"readOnlyHint": true
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "graph_query",
|
|
323
|
+
"description": "A symbol's direct outgoing relations — calls, imports, uses, inherits — each line carrying its confidence and resolution mechanism. For \"what does this touch\" before reading a file.",
|
|
324
|
+
"side_effect": "ro",
|
|
325
|
+
"implemented_on": [
|
|
326
|
+
"stdio"
|
|
327
|
+
],
|
|
328
|
+
"input_schema": {
|
|
329
|
+
"type": "object",
|
|
330
|
+
"required": [
|
|
331
|
+
"symbol"
|
|
332
|
+
],
|
|
333
|
+
"properties": {
|
|
334
|
+
"symbol": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"description": "Node id, label, or free text."
|
|
337
|
+
},
|
|
338
|
+
"budget": {
|
|
339
|
+
"type": "integer",
|
|
340
|
+
"minimum": 1,
|
|
341
|
+
"default": 1500,
|
|
342
|
+
"description": "Token budget for lines."
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
"additionalProperties": false
|
|
346
|
+
},
|
|
347
|
+
"annotations": {
|
|
348
|
+
"readOnlyHint": true
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"name": "graph_tests_for",
|
|
353
|
+
"description": "Which test files exercise a symbol, derived from a test file importing it or its module. Empty means no test imports it — a finding, not an error.",
|
|
354
|
+
"side_effect": "ro",
|
|
355
|
+
"implemented_on": [
|
|
356
|
+
"stdio"
|
|
357
|
+
],
|
|
358
|
+
"input_schema": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"required": [
|
|
361
|
+
"symbol"
|
|
362
|
+
],
|
|
363
|
+
"properties": {
|
|
364
|
+
"symbol": {
|
|
365
|
+
"type": "string",
|
|
366
|
+
"description": "Node id `<relpath>#<sym>`, label, or free text."
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
"additionalProperties": false
|
|
370
|
+
},
|
|
371
|
+
"annotations": {
|
|
372
|
+
"readOnlyHint": true
|
|
373
|
+
}
|
|
374
|
+
},
|
|
225
375
|
{
|
|
226
376
|
"name": "lint_skills",
|
|
227
377
|
"description": "Lint skill, rule, command, guideline, and persona markdown files for frontmatter and structural errors. Use before committing or opening a PR that adds or edits any of those artifacts, to catch schema violations early. Read-only — never writes files or spawns git. Returns the `scripts/skill_linter.py --format json` payload: a `summary` object (pass / pass_with_warnings / fail / total counts) and a per-file `results` array with severity-tagged findings. Pass `paths` to lint a subset; omit for a full tree scan.",
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five code-graph MCP tools (`road-to-a-graph-that-is-shipped` 4.1).
|
|
3
|
+
*
|
|
4
|
+
* `graph_impact`, `graph_tests_for`, `graph_dead`, `graph_query`, `graph_path`.
|
|
5
|
+
* They are the agent-facing half of Phase 3: the verbs exist on the CLI and an
|
|
6
|
+
* agent that has to shell out to reach them will not, so the engine's answers
|
|
7
|
+
* were unreachable from the surface the consumer actually talks to. Risk-Register
|
|
8
|
+
* rank 1 is precisely "the install grows and the graph is still unread"; these
|
|
9
|
+
* are the readers.
|
|
10
|
+
*
|
|
11
|
+
* REGISTERED HERE, NOT IN `tools.ts`.
|
|
12
|
+
*
|
|
13
|
+
* `tools.ts` sits ~500 lines past the 1500-line ceiling
|
|
14
|
+
* `check_source_size_budget` enforces as a shrink-only ratchet, so five tool
|
|
15
|
+
* records with their schemas would cost ~150 units of excess there and nothing
|
|
16
|
+
* here. `tools.ts` spreads {@link GRAPH_TOOLS} into `ALLOWLIST`, which is two
|
|
17
|
+
* lines — paid for by moving `_strip` / `_resolvePath` out to `path_util.ts` in
|
|
18
|
+
* the same change.
|
|
19
|
+
*
|
|
20
|
+
* TELEMETRY NEEDS NO PER-TOOL WORK.
|
|
21
|
+
*
|
|
22
|
+
* `ToolCache.dispatch` records every `tools/call` centrally, so these five emit
|
|
23
|
+
* the identical line the other 31 do by being in `ALLOWLIST` at all — which is
|
|
24
|
+
* what the step's "same telemetry line as the other 31" asks for, and is
|
|
25
|
+
* stronger than a per-tool emit because it cannot be forgotten.
|
|
26
|
+
*
|
|
27
|
+
* READ-ONLY, WITH ONE HONEST EXCEPTION.
|
|
28
|
+
*
|
|
29
|
+
* Four are `side_effect: 'ro'`. `graph_impact` is `'shell'`, because it resolves
|
|
30
|
+
* its `diff` argument by running `git diff --name-only`. That subprocess is
|
|
31
|
+
* read-only in effect, and the enum has no value that says so — between
|
|
32
|
+
* understating the mechanism as `'ro'` and naming it, naming it is the only
|
|
33
|
+
* choice a capability enum exists to support.
|
|
34
|
+
*/
|
|
35
|
+
import * as fs from 'node:fs';
|
|
36
|
+
import * as path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import { changedFiles } from '../code_graph/cli.js';
|
|
39
|
+
import { detectSources, graphState, NATIVE_CACHE_REL, pickSource } from '../code_graph/detect.js';
|
|
40
|
+
import { resolvePath } from './path_util.js';
|
|
41
|
+
import { isTestFile } from '../code_graph/build.js';
|
|
42
|
+
import { loadGraph, type LoadedGraph, path as graphPathVerb, query as graphQueryVerb } from '../code_graph/query.js';
|
|
43
|
+
import {
|
|
44
|
+
dead,
|
|
45
|
+
entryPointsFromFile,
|
|
46
|
+
impact,
|
|
47
|
+
repoEntryPoints,
|
|
48
|
+
testsFor,
|
|
49
|
+
untested,
|
|
50
|
+
} from '../code_graph/verbs.js';
|
|
51
|
+
import type { BuiltinTool } from './tools.js';
|
|
52
|
+
|
|
53
|
+
/** The `no graph here` answer, in one place so all five agree on its shape. */
|
|
54
|
+
function noGraph(root: string): Record<string, unknown> {
|
|
55
|
+
return {
|
|
56
|
+
status: 'unavailable',
|
|
57
|
+
staleness: 'absent',
|
|
58
|
+
error:
|
|
59
|
+
'no code-graph source found under this root — run `agent-config code-graph build` ' +
|
|
60
|
+
'to build one, or ship a graph.json',
|
|
61
|
+
root,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the graph, run `fn`, release the handle.
|
|
67
|
+
*
|
|
68
|
+
* The handle owns an open SQLite database on the indexed path, so the `finally`
|
|
69
|
+
* is not hygiene — a long-lived MCP server that leaked one per call would hold
|
|
70
|
+
* a descriptor for the life of the process.
|
|
71
|
+
*/
|
|
72
|
+
function withGraph<T extends Record<string, unknown>>(
|
|
73
|
+
root: string,
|
|
74
|
+
fn: (g: LoadedGraph) => T,
|
|
75
|
+
): Record<string, unknown> {
|
|
76
|
+
const picked = pickSource(detectSources(root, path.join(root, NATIVE_CACHE_REL)));
|
|
77
|
+
if (!picked || picked.kind === 'scip') return noGraph(root);
|
|
78
|
+
const g = loadGraph(picked.path, `${picked.kind}:${path.relative(root, picked.path)}`);
|
|
79
|
+
try {
|
|
80
|
+
const out = fn(g);
|
|
81
|
+
// `status` is DERIVED from the answer, never prefixed onto it. An
|
|
82
|
+
// independent review found `graph_dead`'s refusal returning
|
|
83
|
+
// `{status:'ok', refusal:'…', dead:[]}` — the same status a real answer
|
|
84
|
+
// carries, so a caller branching on `status` (which every fixture here
|
|
85
|
+
// trains it to do) read the refusal as "nothing is dead". That is the
|
|
86
|
+
// false negative the CLI's exit 1 exists to prevent, arriving over the
|
|
87
|
+
// wire instead. A refusal is its own status.
|
|
88
|
+
const refused = typeof out['refusal'] === 'string' && out['refusal'] !== '';
|
|
89
|
+
return { status: refused ? 'refused' : 'ok', ...out };
|
|
90
|
+
} finally {
|
|
91
|
+
g.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function str(args: Record<string, unknown>, key: string): string {
|
|
96
|
+
const v = args[key];
|
|
97
|
+
return typeof v === 'string' ? v.trim() : '';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function num(args: Record<string, unknown>, key: string, dflt: number): number {
|
|
101
|
+
const v = args[key];
|
|
102
|
+
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : dflt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const GRAPH_TOOLS: Record<string, BuiltinTool> = {
|
|
106
|
+
graph_impact: {
|
|
107
|
+
name: 'graph_impact',
|
|
108
|
+
side_effect: 'shell',
|
|
109
|
+
description:
|
|
110
|
+
'What a diff can break: callers, dependents and test files reaching the ' +
|
|
111
|
+
'changed symbols. Walks only stated-fact edges, never a same-name guess, ' +
|
|
112
|
+
'and reports the guesses refused. Runs `git diff` for the rev.',
|
|
113
|
+
input_schema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
required: ['diff'],
|
|
116
|
+
properties: {
|
|
117
|
+
diff: { type: 'string', description: 'Git rev to diff HEAD against, e.g. `HEAD~1`.' },
|
|
118
|
+
depth: { type: 'integer', minimum: 1, default: 2, description: 'Hop limit.' },
|
|
119
|
+
untested_only: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
default: false,
|
|
122
|
+
description: 'Instead return changed symbols no test imports.',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
},
|
|
127
|
+
handler: async (args, root) => {
|
|
128
|
+
const rev = str(args, 'diff');
|
|
129
|
+
if (rev === '') return { status: 'error', error: 'diff must be a non-empty git rev' };
|
|
130
|
+
const files = changedFiles(root, rev);
|
|
131
|
+
if (files === null) return { status: 'error', error: `cannot resolve rev '${rev}' in ${root}` };
|
|
132
|
+
const state = graphState(root);
|
|
133
|
+
return withGraph(root, (g) =>
|
|
134
|
+
args['untested_only'] === true
|
|
135
|
+
? { ...untested(g, files, state), changed_files: files.length }
|
|
136
|
+
: { ...impact(g, files, state, num(args, 'depth', 2), isTestFile), changed_files: files.length },
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
graph_tests_for: {
|
|
142
|
+
name: 'graph_tests_for',
|
|
143
|
+
side_effect: 'ro',
|
|
144
|
+
description:
|
|
145
|
+
'Which test files exercise a symbol, derived from a test file importing ' +
|
|
146
|
+
'it or its module. Empty means no test imports it — a finding, not an error.',
|
|
147
|
+
input_schema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
required: ['symbol'],
|
|
150
|
+
properties: {
|
|
151
|
+
symbol: { type: 'string', description: 'Node id `<relpath>#<sym>`, label, or free text.' },
|
|
152
|
+
},
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
},
|
|
155
|
+
handler: async (args, root) => {
|
|
156
|
+
const symbol = str(args, 'symbol');
|
|
157
|
+
if (symbol === '') return { status: 'error', error: 'symbol must be a non-empty string' };
|
|
158
|
+
const state = graphState(root);
|
|
159
|
+
return withGraph(root, (g) => ({ ...testsFor(g, symbol, state) }));
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
graph_dead: {
|
|
164
|
+
name: 'graph_dead',
|
|
165
|
+
side_effect: 'ro',
|
|
166
|
+
description:
|
|
167
|
+
'Symbols with no accepted reference that no declared entry point names. ' +
|
|
168
|
+
'REFUSES (status `refused`) while a source is unreadable: with no ' +
|
|
169
|
+
'exportedness recorded, an exported-but-unimported symbol is ' +
|
|
170
|
+
'indistinguishable from a dead one. Never a deletion recommendation.',
|
|
171
|
+
input_schema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
entry_points: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'Path under this root to a newline-separated id/label list; `#` comments ok.',
|
|
177
|
+
},
|
|
178
|
+
accept_missing_exports: {
|
|
179
|
+
type: 'boolean',
|
|
180
|
+
default: false,
|
|
181
|
+
description: 'Answer anyway, accepting the gap. Never a default.',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
},
|
|
186
|
+
handler: async (args, root) => {
|
|
187
|
+
const state = graphState(root);
|
|
188
|
+
const listArg = str(args, 'entry_points');
|
|
189
|
+
let sources = repoEntryPoints(root);
|
|
190
|
+
if (listArg !== '') {
|
|
191
|
+
// REALPATH containment, matching the sibling pattern in
|
|
192
|
+
// `tools.ts` rather than reimplementing a weaker one — an
|
|
193
|
+
// independent review found this check using `path.resolve` +
|
|
194
|
+
// `startsWith`, which a symlink inside the root walks straight
|
|
195
|
+
// through, in the same change that extracted `resolvePath` for
|
|
196
|
+
// reuse. `fs.readFileSync` would then follow the link, and
|
|
197
|
+
// because the file's lines used to be echoed back in the
|
|
198
|
+
// response (see `entry_count` below) that was an
|
|
199
|
+
// arbitrary-file-read path.
|
|
200
|
+
const abs = resolvePath(path.resolve(root, listArg));
|
|
201
|
+
const rootReal = resolvePath(path.resolve(root));
|
|
202
|
+
if (abs !== rootReal && !abs.startsWith(rootReal + path.sep)) {
|
|
203
|
+
return { status: 'error', error: `path escapes consumer_root: ${abs}` };
|
|
204
|
+
}
|
|
205
|
+
if (!fs.existsSync(abs)) return { status: 'error', error: `entry-point list not found: ${listArg}` };
|
|
206
|
+
sources = [
|
|
207
|
+
...sources.map((s) =>
|
|
208
|
+
s.name === 'exports'
|
|
209
|
+
? { ...s, status: 'read' as const, detail: `supplied by ${listArg}`, entries: [] }
|
|
210
|
+
: s,
|
|
211
|
+
),
|
|
212
|
+
entryPointsFromFile(abs),
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
return withGraph(root, (g) => {
|
|
216
|
+
const r = dead(g, sources, state, {
|
|
217
|
+
acceptMissingExports: args['accept_missing_exports'] === true,
|
|
218
|
+
});
|
|
219
|
+
// The `entries` arrays are NOT returned. On this repository they
|
|
220
|
+
// are ~2,595 identifier strings — an unbounded per-call payload
|
|
221
|
+
// beside the standing-cost row this change itself adds — and
|
|
222
|
+
// returning a file the caller named is what made the
|
|
223
|
+
// containment check above a read primitive. A count answers the
|
|
224
|
+
// question a caller actually has ("was this source read, and did
|
|
225
|
+
// it have anything in it") without shipping the contents.
|
|
226
|
+
return {
|
|
227
|
+
...r,
|
|
228
|
+
sources: r.sources.map((srcRow) => ({
|
|
229
|
+
name: srcRow.name,
|
|
230
|
+
status: srcRow.status,
|
|
231
|
+
...(srcRow.detail === undefined ? {} : { detail: srcRow.detail }),
|
|
232
|
+
entry_count: srcRow.entries.length,
|
|
233
|
+
})),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
graph_query: {
|
|
240
|
+
name: 'graph_query',
|
|
241
|
+
side_effect: 'ro',
|
|
242
|
+
description:
|
|
243
|
+
"A symbol's direct outgoing relations — calls, imports, uses, inherits — " +
|
|
244
|
+
'each line carrying its confidence and resolution mechanism. For "what ' +
|
|
245
|
+
'does this touch" before reading a file.',
|
|
246
|
+
input_schema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
required: ['symbol'],
|
|
249
|
+
properties: {
|
|
250
|
+
symbol: { type: 'string', description: 'Node id, label, or free text.' },
|
|
251
|
+
budget: { type: 'integer', minimum: 1, default: 1500, description: 'Token budget for lines.' },
|
|
252
|
+
},
|
|
253
|
+
additionalProperties: false,
|
|
254
|
+
},
|
|
255
|
+
handler: async (args, root) => {
|
|
256
|
+
const symbol = str(args, 'symbol');
|
|
257
|
+
if (symbol === '') return { status: 'error', error: 'symbol must be a non-empty string' };
|
|
258
|
+
const staleness = graphState(root);
|
|
259
|
+
return withGraph(root, (g) => ({
|
|
260
|
+
staleness,
|
|
261
|
+
...graphQueryVerb(g, symbol, num(args, 'budget', 1500)),
|
|
262
|
+
}));
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
graph_path: {
|
|
267
|
+
name: 'graph_path',
|
|
268
|
+
side_effect: 'ro',
|
|
269
|
+
description:
|
|
270
|
+
'The shortest relation chain between two symbols, direction preserved. ' +
|
|
271
|
+
'Answers "how does A reach B" without reading the files between. ' +
|
|
272
|
+
'`(no path found)` when unconnected is an answer.',
|
|
273
|
+
input_schema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
required: ['from', 'to'],
|
|
276
|
+
properties: {
|
|
277
|
+
from: { type: 'string', description: 'Start node id, label, or free text.' },
|
|
278
|
+
to: { type: 'string', description: 'Goal node id, label, or free text.' },
|
|
279
|
+
budget: { type: 'integer', minimum: 1, default: 1500, description: 'Token budget for lines.' },
|
|
280
|
+
},
|
|
281
|
+
additionalProperties: false,
|
|
282
|
+
},
|
|
283
|
+
handler: async (args, root) => {
|
|
284
|
+
const from = str(args, 'from');
|
|
285
|
+
const to = str(args, 'to');
|
|
286
|
+
if (from === '' || to === '') return { status: 'error', error: 'from and to must be non-empty strings' };
|
|
287
|
+
const staleness = graphState(root);
|
|
288
|
+
return withGraph(root, (g) => ({
|
|
289
|
+
staleness,
|
|
290
|
+
...graphPathVerb(g, from, to, num(args, 'budget', 1500)),
|
|
291
|
+
}));
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two string/path helpers extracted from `tools.ts`.
|
|
3
|
+
*
|
|
4
|
+
* They are generic — a whitespace trim and a tolerant realpath — and `tools.ts`
|
|
5
|
+
* sits ~525 lines past the 1500-line ceiling `check_source_size_budget`
|
|
6
|
+
* enforces as a shrink-only ratchet, so every line there costs one unit of
|
|
7
|
+
* excess and the gate names re-pinning the baseline a defect rather than a fix.
|
|
8
|
+
* Moving them here pays for the graph-tool registration that arrives with
|
|
9
|
+
* `road-to-a-graph-that-is-shipped` 4.1, and puts them in a file where their
|
|
10
|
+
* prose is free.
|
|
11
|
+
*
|
|
12
|
+
* They are imported under their original underscore-prefixed names, so the
|
|
13
|
+
* 28 call sites in `tools.ts` are unchanged — a rename would have made a
|
|
14
|
+
* two-function move read as a sweep.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
|
|
19
|
+
/** Mirror Python `str.strip()` (also strips the same Unicode whitespace set we care about here). */
|
|
20
|
+
export function strip(s: string): string {
|
|
21
|
+
return s.replace(/^\s+/, '').replace(/\s+$/, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Mirror Python `Path(...).resolve()` — absolutize + realpath, tolerating a missing tail. */
|
|
25
|
+
export function resolvePath(p: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return fs.realpathSync(p);
|
|
28
|
+
} catch {
|
|
29
|
+
return path.resolve(p);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -33,7 +33,13 @@ export const CATALOG_DESCRIPTION =
|
|
|
33
33
|
'backlog: per ADR-132 they are NOT registered on the stdio tools/list ' +
|
|
34
34
|
"(the wire carries implemented tools only); the 'not_implemented' " +
|
|
35
35
|
'envelope contract (docs/contracts/mcp-tool-stub-envelope.md) remains ' +
|
|
36
|
-
'for Worker parity.
|
|
36
|
+
'for Worker parity. ' +
|
|
37
|
+
"The 'install_hint_stdio' field is the INSTALLED-BINARY form and " +
|
|
38
|
+
'therefore assumes `agent-config` is on PATH (a global install, or an ' +
|
|
39
|
+
'npx-resolved one the client already has); it deliberately carries no ' +
|
|
40
|
+
'`npx` invocation, because the un-prompted form of that command is the ' +
|
|
41
|
+
"one string this file's own gate forbids and the prompted form hangs a " +
|
|
42
|
+
'non-interactive client start. Generated by `build_mcp_catalog` — ' +
|
|
37
43
|
'never hand-edit this file.';
|
|
38
44
|
|
|
39
45
|
export interface StubToolSource {
|
|
@@ -54,6 +54,8 @@ import {
|
|
|
54
54
|
load_catalog,
|
|
55
55
|
not_implemented_envelope,
|
|
56
56
|
} from './catalog.js';
|
|
57
|
+
import { GRAPH_TOOLS } from './graph_tools.js';
|
|
58
|
+
import { resolvePath as _resolvePath, strip as _strip } from './path_util.js';
|
|
57
59
|
import { type Outcome, record_call } from './telemetry.js';
|
|
58
60
|
import type { PromptCache } from './prompts.js';
|
|
59
61
|
import type { ResourceCache } from './resources.js';
|
|
@@ -111,19 +113,6 @@ export interface BuiltinTool {
|
|
|
111
113
|
readonly handler: ToolHandler;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
/** Mirror Python `str.strip()` (also strips the same Unicode whitespace set we care about here). */
|
|
115
|
-
function _strip(s: string): string {
|
|
116
|
-
return s.replace(/^\s+/, '').replace(/\s+$/, '');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Mirror Python `Path(...).resolve()` — absolutize + realpath, tolerating a missing tail. */
|
|
120
|
-
function _resolvePath(p: string): string {
|
|
121
|
-
try {
|
|
122
|
-
return fs.realpathSync(p);
|
|
123
|
-
} catch {
|
|
124
|
-
return path.resolve(p);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
116
|
|
|
128
117
|
/**
|
|
129
118
|
* Pick the consumer-project root.
|
|
@@ -1212,6 +1201,7 @@ async function _readResourceBodyHandler(
|
|
|
1212
1201
|
// ---------------------------------------------------------------------
|
|
1213
1202
|
|
|
1214
1203
|
export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
1204
|
+
...GRAPH_TOOLS,
|
|
1215
1205
|
suggest_skill_for_task: {
|
|
1216
1206
|
name: 'suggest_skill_for_task',
|
|
1217
1207
|
side_effect: 'ro',
|
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
loadRuleBody,
|
|
61
61
|
matchTierRules,
|
|
62
62
|
pathCapableRuleIds,
|
|
63
|
+
pathOnlyRuleIds,
|
|
63
64
|
selectForInjection,
|
|
64
65
|
triggerlessRuleIds,
|
|
65
66
|
type Router,
|
|
@@ -101,6 +102,18 @@ export interface CorpusCase {
|
|
|
101
102
|
rule: string;
|
|
102
103
|
prompt: string;
|
|
103
104
|
openFiles: string[] | null;
|
|
105
|
+
/**
|
|
106
|
+
* The slash command the case declares, or `null` (R2 finding 1).
|
|
107
|
+
*
|
|
108
|
+
* The loader DROPPED this field, so every `command:` trigger scored as an
|
|
109
|
+
* unmatched plain prompt. Two entries in the frozen corpus carry one —
|
|
110
|
+
* `roadmap-progress-sync` and `user-interrupt-priority` — and the shipped
|
|
111
|
+
* concern DOES reach command triggers: `hooks/rule_inject_hook.ts` extracts
|
|
112
|
+
* a leading `/command` from the prompt text and passes it to
|
|
113
|
+
* `matchTierRules`. Dropping the field made the shipped-reach reading
|
|
114
|
+
* falsely low in exactly the two places it mattered.
|
|
115
|
+
*/
|
|
116
|
+
command: string | null;
|
|
104
117
|
label: 'positive' | 'near_miss';
|
|
105
118
|
file: string;
|
|
106
119
|
}
|
|
@@ -119,10 +132,12 @@ function _asCases(
|
|
|
119
132
|
const prompt = typeof o['prompt'] === 'string' ? o['prompt'] : null;
|
|
120
133
|
if (prompt === null) continue;
|
|
121
134
|
const of = o['open_files'];
|
|
135
|
+
const cmd = o['command'];
|
|
122
136
|
out.push({
|
|
123
137
|
rule,
|
|
124
138
|
prompt,
|
|
125
139
|
openFiles: Array.isArray(of) ? of.map((x) => String(x)) : null,
|
|
140
|
+
command: typeof cmd === 'string' && cmd !== '' ? cmd : null,
|
|
126
141
|
label,
|
|
127
142
|
file,
|
|
128
143
|
});
|
|
@@ -618,17 +633,54 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
|
|
|
618
633
|
}
|
|
619
634
|
|
|
620
635
|
// (b) No labelled rule may end with ZERO matched positives.
|
|
621
|
-
|
|
636
|
+
//
|
|
637
|
+
// TWO READINGS SINCE 2026-09-08 (R2 finding 1), and the second one is the
|
|
638
|
+
// honest half. The pre-registered bar is scored with `open_files` HONOURED,
|
|
639
|
+
// which is the reach of the MECHANISM. It is not the reach of the SHIPPED
|
|
640
|
+
// BINDING: `rule-inject` is bound on `user_prompt_submit` + `pre_compact`
|
|
641
|
+
// only (the `pre_tool_use` binding was removed under owner ruling E2), and
|
|
642
|
+
// `user_prompt_submit` never populates `openFiles` — the payload carries no
|
|
643
|
+
// open-file list. So a rule whose only matching positives carry
|
|
644
|
+
// `open_files` is credited as reachable while no shipped slot can deliver
|
|
645
|
+
// it on that route.
|
|
646
|
+
//
|
|
647
|
+
// The bar is UNCHANGED (it is pre-registered; see
|
|
648
|
+
// `internal/bench/thin-inject-PREREG.md`). What is added is the column: the
|
|
649
|
+
// same recall computed with `open_files` ignored, and the names of the rules
|
|
650
|
+
// that differ between the two. A rule in that difference is either kept
|
|
651
|
+
// full-bodied by the always-eager residue — in which case it is reachable
|
|
652
|
+
// by projection rather than by delivery — or it is thinned, in which case it
|
|
653
|
+
// reaches a Claude Code session on the prompt route only.
|
|
654
|
+
const reach = (c: CorpusCase, honourOpenFiles: boolean): boolean =>
|
|
655
|
+
matchTierRules(
|
|
656
|
+
router,
|
|
657
|
+
c.prompt,
|
|
658
|
+
honourOpenFiles ? c.openFiles : null,
|
|
659
|
+
c.command,
|
|
660
|
+
).some((m) => m.id === c.rule);
|
|
661
|
+
|
|
662
|
+
const byRule = new Map<string, { hit: number; total: number; shipped: number }>();
|
|
622
663
|
for (const c of positives) {
|
|
623
|
-
const e = byRule.get(c.rule) ?? { hit: 0, total: 0 };
|
|
664
|
+
const e = byRule.get(c.rule) ?? { hit: 0, total: 0, shipped: 0 };
|
|
624
665
|
e.total += 1;
|
|
625
|
-
if (
|
|
666
|
+
if (reach(c, true)) e.hit += 1;
|
|
667
|
+
if (reach(c, false)) e.shipped += 1;
|
|
626
668
|
byRule.set(c.rule, e);
|
|
627
669
|
}
|
|
628
670
|
const unreachable = [...byRule.entries()].filter(([, v]) => v.hit === 0).map(([k]) => k);
|
|
629
671
|
const partial = [...byRule.entries()]
|
|
630
672
|
.filter(([, v]) => v.hit > 0 && v.hit < v.total)
|
|
631
673
|
.map(([k, v]) => `${k} ${v.hit}/${v.total}`);
|
|
674
|
+
const pathOnlyReach = [...byRule.entries()]
|
|
675
|
+
.filter(([, v]) => v.hit > 0 && v.shipped === 0)
|
|
676
|
+
.map(([k]) => k)
|
|
677
|
+
.sort();
|
|
678
|
+
const alwaysEager = new Set([
|
|
679
|
+
...triggerlessRuleIds(router),
|
|
680
|
+
...pathOnlyRuleIds(router),
|
|
681
|
+
...kernelIds(router),
|
|
682
|
+
]);
|
|
683
|
+
const thinnedAndPathOnly = pathOnlyReach.filter((id) => !alwaysEager.has(id));
|
|
632
684
|
|
|
633
685
|
// (c) A near-miss must never deliver its labelled rule.
|
|
634
686
|
const exact = scoreExact(router, cases, true);
|
|
@@ -659,7 +711,12 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
|
|
|
659
711
|
reading:
|
|
660
712
|
`${byRule.size - unreachable.length}/${byRule.size} rules reachable; ` +
|
|
661
713
|
`unreachable: ${unreachable.length === 0 ? 'none' : unreachable.join(', ')}; ` +
|
|
662
|
-
`partial: ${partial.length === 0 ? 'none' : partial.join(', ')}
|
|
714
|
+
`partial: ${partial.length === 0 ? 'none' : partial.join(', ')}` +
|
|
715
|
+
` | shipped user_prompt_submit reach (open_files IGNORED): ` +
|
|
716
|
+
`${byRule.size - pathOnlyReach.length}/${byRule.size}; reachable only via a ` +
|
|
717
|
+
`path trigger: ${pathOnlyReach.length === 0 ? 'none' : pathOnlyReach.join(', ')}` +
|
|
718
|
+
` (of which thinned, i.e. at no scope on that route: ` +
|
|
719
|
+
`${thinnedAndPathOnly.length === 0 ? 'none' : thinnedAndPathOnly.join(', ')})`,
|
|
663
720
|
bar: 'unreachable == 0 (a rule with zero matched positives is a rule the mode removed)',
|
|
664
721
|
passed: unreachable.length === 0,
|
|
665
722
|
},
|