@event4u/agent-config 14.19.0 → 14.21.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 +351 -156
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/agent-handoff.md +42 -41
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +29 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
- package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_conformance.js +88 -110
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- 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 +62 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +3801 -2803
- package/dist/install/install.mjs +396 -168
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +809 -180
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/llms.txt +14 -14
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/gate-coverage.yml +55 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +76 -1
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/install.ts +4 -4
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +50 -95
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +40 -31
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/self_review_gate.ts +409 -15
- package/src/scripts/skill_scout.ts +69 -0
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -56,7 +56,7 @@ For each learning, apply ALL of these checks. ALL must be YES to promote:
|
|
|
56
56
|
|---|---|
|
|
57
57
|
| All YES | **Promote** — continue to Step 3 |
|
|
58
58
|
| One-off, never seen before | **Reject** — do nothing |
|
|
59
|
-
| Seen once, but generalizable | **
|
|
59
|
+
| Seen once, but generalizable | **Record** — write a `model-noticed` self-repair record naming the asset it is about; act on the second occurrence |
|
|
60
60
|
| Already covered | **Update existing** — skip to Step 4 with the existing skill/rule |
|
|
61
61
|
| Vague | **Reject** — not actionable |
|
|
62
62
|
|
|
@@ -66,9 +66,26 @@ Show the user:
|
|
|
66
66
|
> Category: {category-tag}
|
|
67
67
|
> Repeated: {yes/no} | Prevents failure: {yes/no} | Not covered: {yes/no} | Actionable: {yes/no}
|
|
68
68
|
>
|
|
69
|
-
> → {Promote / Reject /
|
|
69
|
+
> → {Promote / Reject / Record / Update existing}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
### Where a seen-once learning goes — and why not a host-native memory tool
|
|
73
|
+
|
|
74
|
+
The **Record** branch writes a `model-noticed` defect record through
|
|
75
|
+
`upsertFinding` in `src/scripts/_lib/self_repair_store.ts`, carrying a `target`
|
|
76
|
+
drawn from the closed `rule: | skill: | command: | hook:` vocabulary and
|
|
77
|
+
resolved against the tree at write time.
|
|
78
|
+
|
|
79
|
+
It used to route to a host-native memory tool, and that destination could not
|
|
80
|
+
work for this branch by construction: it is not target-addressed, nothing in
|
|
81
|
+
this repository can read it, it appears in no corroboration counter, and it is
|
|
82
|
+
simply absent on hosts that do not ship it. So the ≥2-origin counter at
|
|
83
|
+
`src/scripts/learning_sidecar.ts:39` restarted at one every time, and the second
|
|
84
|
+
occurrence could never be recognised AS the second — which is the whole
|
|
85
|
+
mechanism this branch exists to feed. A record is readable, joinable, and
|
|
86
|
+
counted per target, so a third manifestation against one asset is a fact the
|
|
87
|
+
tree can establish rather than one that depends on a human recalling it.
|
|
88
|
+
|
|
72
89
|
#### Cross-project promotion signal (surfacing only — never an auto-writer)
|
|
73
90
|
|
|
74
91
|
If the **same learning** has now been observed in **≥ 2 distinct projects**, it
|
|
@@ -82,10 +99,15 @@ to the user — do **not** auto-write it:
|
|
|
82
99
|
> → surface for promotion (human decides; no auto-write)
|
|
83
100
|
```
|
|
84
101
|
|
|
85
|
-
This is a *signal*, not a
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
This is a *signal*, not a promotion: there is **no auto-write and no
|
|
103
|
+
auto-promotion** — that half is the live boundary and is unchanged. The other
|
|
104
|
+
two clauses this sentence used to carry are retired, because the tree
|
|
105
|
+
contradicts them: `src/scripts/learning_sidecar.ts:37` sets
|
|
106
|
+
`HALF_LIFE_DAYS = 30` and decays a signal's weight over a per-project intake
|
|
107
|
+
store, and ADR-249 permits a **supervised** resident process in core,
|
|
108
|
+
superseding the blanket no-daemon clause this sentence inherited. Decay and
|
|
109
|
+
runtime are therefore **governed, not prohibited**. The human still decides
|
|
110
|
+
whether the cross-project recurrence justifies promotion.
|
|
89
111
|
|
|
90
112
|
### Step 3: Classify
|
|
91
113
|
|
|
@@ -79,6 +79,8 @@ For complex features or systems. Stored in `agents/features/` or module `agents/
|
|
|
79
79
|
|---|---|---|---|
|
|
80
80
|
|
|
81
81
|
## Open Questions
|
|
82
|
+
{Each line is a question still owed to the user — ask one at a time and move
|
|
83
|
+
the answer into the section it belongs to. Not a storage section.}
|
|
82
84
|
- [ ] {Unresolved question}
|
|
83
85
|
|
|
84
86
|
## References
|
|
@@ -123,6 +125,8 @@ technical spec, not here.}
|
|
|
123
125
|
- **{Module name}** — {one-sentence responsibility}
|
|
124
126
|
|
|
125
127
|
## Open Questions
|
|
128
|
+
{Each line is a question still owed to the user — ask one at a time and move
|
|
129
|
+
the answer into the section it belongs to. Not a storage section.}
|
|
126
130
|
- [ ] {Unresolved product question — pricing, permission, copy, edge case}
|
|
127
131
|
|
|
128
132
|
## References
|
|
@@ -120,9 +120,7 @@ RED.
|
|
|
120
120
|
|
|
121
121
|
### Mode inference on resume
|
|
122
122
|
|
|
123
|
-
Step 0 of any resume:
|
|
124
|
-
from its Mode + Contract-owed fields (see `/agent-handoff` file mode) instead
|
|
125
|
-
of re-deriving. Otherwise infer the mode from observable state — never
|
|
123
|
+
Step 0 of any resume: infer the mode from observable state — never
|
|
126
124
|
assume Design:
|
|
127
125
|
|
|
128
126
|
| Observed state | Resume in |
|
|
@@ -39,7 +39,7 @@ schema_version: 1
|
|
|
39
39
|
# CI guard: a release bump of `package.json` must update this value
|
|
40
40
|
# in lockstep — see scripts/check_template_pin_drift.ts (road-to-
|
|
41
41
|
# portable-runtime-and-update-check P3.3).
|
|
42
|
-
agent_config_version: "14.
|
|
42
|
+
agent_config_version: "14.21.0"
|
|
43
43
|
|
|
44
44
|
# --- Project identity ---
|
|
45
45
|
project:
|
|
@@ -90,6 +90,11 @@ Reference existing code where helpful.}
|
|
|
90
90
|
|
|
91
91
|
## Open Questions
|
|
92
92
|
|
|
93
|
+
> Every entry here is a question still **owed to the user** — not a permanent
|
|
94
|
+
> section and not a place to file a decision. Each one is put to the user in the
|
|
95
|
+
> chat, one at a time, and the answer moves out of this list into the section it
|
|
96
|
+
> belongs to. A question that stays here across a session was parked, not asked.
|
|
97
|
+
|
|
93
98
|
- [ ] {Unresolved question 1}
|
|
94
99
|
- [ ] {Unresolved question 2}
|
|
95
100
|
|
|
@@ -43,7 +43,20 @@ that was never a judgement call.
|
|
|
43
43
|
- `agents/roadmaps/` — active (in progress or planned **and workable now**)
|
|
44
44
|
- `agents/roadmaps/archive/` — work happened (fully or partially); no further work planned
|
|
45
45
|
- `agents/roadmaps/skipped/` — decision against pursuit; typically 0 items `[x]` (superseded, scope rejected)
|
|
46
|
-
- `agents/roadmaps/later/` — open work remains but is **blocked-for-later** (gated on an external
|
|
46
|
+
- `agents/roadmaps/later/` — open work remains but is **blocked-for-later** (gated on an external condition or a decision); **will resume**. Roadmaps with open tasks deferred for later are **always** moved here, never left active. Excluded from the dashboard and `/roadmap:process-*`; enforced by `lint_roadmap_later_disposition`. Two frontmatter obligations, both in force **regardless of `status`** — the status word alone stopped satisfying this gate on 2026-09-07, and so did the bare word `trigger`:
|
|
47
|
+
|
|
48
|
+
- **A wake condition.** Preferred form is a structured `entry_condition:` mapping naming all three parts:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
entry_condition:
|
|
52
|
+
what: what would change the decision
|
|
53
|
+
when: when that could arrive
|
|
54
|
+
who: who would have to act # `none` is a legal answer; blank is not
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A condition that cannot be observed is a deferral wearing a condition's clothes, which is why all three are required and why a **scalar** `entry_condition` or a blank part fails hard rather than being baselined. The legacy body-line form (`Blocked until` / `Resume when`) still passes and is what the shrink-only ratchet is walking down from.
|
|
58
|
+
|
|
59
|
+
- **`review_by:`** — a date. A park with no review date is indistinguishable from an abandonment. Ratcheted from the count measured when the obligation landed; a **newly** parked roadmap must carry it.
|
|
47
60
|
|
|
48
61
|
See the `roadmap-management` skill for the exact trigger matrix and user-confirmation flow.
|
|
49
62
|
13. **No tags, releases, or version numbers.** Roadmaps describe work, not shipping.
|
|
@@ -288,6 +301,21 @@ that was never a judgement call.
|
|
|
288
301
|
step gated by a specific blocker may cross-reference it inline:
|
|
289
302
|
`- [ ] … <!-- blocked-by: <blocker-id> -->`.
|
|
290
303
|
|
|
304
|
+
**A user-decision blocker's annotation records whether the question
|
|
305
|
+
was actually put.** Where the blocker's `Owner:` is the maintainer,
|
|
306
|
+
the user or the owner, the marker carries an `asked:` field —
|
|
307
|
+
`<!-- blocked-by: <id> | asked: yes -->` when the question reached
|
|
308
|
+
the user, `<!-- blocked-by: <id> | asked: no — <reason> -->` when it
|
|
309
|
+
did not. `lint_roadmap_blockers` fails on a missing field and on an
|
|
310
|
+
`asked: no` with no reason. Without it a decision only the user can
|
|
311
|
+
make can be filed, correctly labelled, in a file nobody is watching,
|
|
312
|
+
and a later reader cannot tell a declined decision from one that was
|
|
313
|
+
never offered. On an interactive host the question is put FIRST;
|
|
314
|
+
only a decline, a timeout, or a non-interactive context writes the
|
|
315
|
+
marker, and a timeout is never consent — see
|
|
316
|
+
[`roadmap-process-loop`](../contexts/execution/roadmap-process-loop.md)
|
|
317
|
+
§ Ask before park.
|
|
318
|
+
|
|
291
319
|
**A blocker is a decision the owner can make in one sitting, or it
|
|
292
320
|
is not finished being written.** The five original fields describe
|
|
293
321
|
the *situation*; they do not make it decidable, and a blocker that
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-lite call reading for `telemetry:report` (roadmap step 4.2).
|
|
3
|
+
*
|
|
4
|
+
* Reads the sink the stdio-lite MCP server writes — the same
|
|
5
|
+
* `agents/runtime/mcp-telemetry/calls.jsonl` the kernel MCP server already
|
|
6
|
+
* used, not a second store — and aggregates it per tool and per host.
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS IS A SEPARATE MODULE. The engagement aggregator models artefacts
|
|
9
|
+
* with a consulted/applied ratio; a `tools/call` has neither. Forcing MCP rows
|
|
10
|
+
* through it would have meant either widening the engagement schema or
|
|
11
|
+
* mislabelling a tool call as a "skill consulted", and both would have made the
|
|
12
|
+
* quartile buckets mean two different things at once.
|
|
13
|
+
*
|
|
14
|
+
* PUBLISH THE ZERO. `render_mcp_lite_markdown` emits the section even when the
|
|
15
|
+
* count is zero, and says so in words. The adjacent archived roadmaps in this
|
|
16
|
+
* family closed at `measured-null` precisely because nulls were published
|
|
17
|
+
* rather than buried; a section that disappears when empty would make "nobody
|
|
18
|
+
* used it" and "nobody looked" indistinguishable.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A ZERO DOES NOT MEAN. The emitter that fills this sink did not exist
|
|
21
|
+
* before this roadmap, and it is gated off by default. A zero is therefore a
|
|
22
|
+
* statement about the age and the default of the instrument first, and about
|
|
23
|
+
* consumer behaviour only once a machine has run with the gate on. The rendered
|
|
24
|
+
* section says this itself, so a reader cannot lift the number away from it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import { check_id_redaction } from './engagement.js';
|
|
29
|
+
|
|
30
|
+
/** Where the stdio-lite server writes, relative to the consumer root. */
|
|
31
|
+
export const MCP_LITE_LOG_REL = 'agents/runtime/mcp-telemetry/calls.jsonl';
|
|
32
|
+
|
|
33
|
+
/** The transport label the stdio-lite surface stamps on every row it writes. */
|
|
34
|
+
export const MCP_LITE_TRANSPORT = 'stdio-lite';
|
|
35
|
+
|
|
36
|
+
export interface McpLiteAggregate {
|
|
37
|
+
/** The file that was read, whether or not it existed. */
|
|
38
|
+
log_path: string;
|
|
39
|
+
/** Rows on this transport that parsed and fell inside the window. */
|
|
40
|
+
calls: number;
|
|
41
|
+
/** Lines that did not parse as JSON, or carried no usable tool name. */
|
|
42
|
+
skipped_lines: number;
|
|
43
|
+
/** Rows that parsed but belong to another transport (the kernel server). */
|
|
44
|
+
other_transport: number;
|
|
45
|
+
by_tool: Map<string, number>;
|
|
46
|
+
by_host: Map<string, number>;
|
|
47
|
+
by_outcome: Map<string, number>;
|
|
48
|
+
earliest_ts: string | null;
|
|
49
|
+
latest_ts: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _empty(log_path: string): McpLiteAggregate {
|
|
53
|
+
return {
|
|
54
|
+
log_path,
|
|
55
|
+
calls: 0,
|
|
56
|
+
skipped_lines: 0,
|
|
57
|
+
other_transport: 0,
|
|
58
|
+
by_tool: new Map(),
|
|
59
|
+
by_host: new Map(),
|
|
60
|
+
by_outcome: new Map(),
|
|
61
|
+
earliest_ts: null,
|
|
62
|
+
latest_ts: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _bump(m: Map<string, number>, k: string): void {
|
|
67
|
+
m.set(k, (m.get(k) ?? 0) + 1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Epoch ms for an ISO-8601 stamp, or `null` when it does not parse. */
|
|
71
|
+
function _ms(ts: unknown): number | null {
|
|
72
|
+
if (typeof ts !== 'string') return null;
|
|
73
|
+
const v = Date.parse(ts);
|
|
74
|
+
return Number.isNaN(v) ? null : v;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Aggregate the lite surface's calls.
|
|
79
|
+
*
|
|
80
|
+
* A missing file is an empty reading, never an error: the overwhelmingly common
|
|
81
|
+
* case is a machine that has simply never run the server, and that is a zero,
|
|
82
|
+
* not a failure. `since` is an epoch-ms lower bound, matching the engagement
|
|
83
|
+
* aggregator's own window semantics; a row with an unreadable timestamp is
|
|
84
|
+
* kept when there is no window and dropped when there is, because it cannot be
|
|
85
|
+
* shown to fall inside one.
|
|
86
|
+
*/
|
|
87
|
+
export function aggregate_mcp_lite(
|
|
88
|
+
log_path: string,
|
|
89
|
+
opts: { since?: number | null } = {},
|
|
90
|
+
): McpLiteAggregate {
|
|
91
|
+
const since = opts.since ?? null;
|
|
92
|
+
const out = _empty(log_path);
|
|
93
|
+
let text: string;
|
|
94
|
+
try {
|
|
95
|
+
text = fs.readFileSync(log_path, 'utf-8');
|
|
96
|
+
} catch {
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
for (const raw of text.split(/\r\n|\r|\n/)) {
|
|
100
|
+
if (raw.trim() === '') continue;
|
|
101
|
+
let rec: Record<string, unknown>;
|
|
102
|
+
try {
|
|
103
|
+
const parsed: unknown = JSON.parse(raw);
|
|
104
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
105
|
+
out.skipped_lines += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
rec = parsed as Record<string, unknown>;
|
|
109
|
+
} catch {
|
|
110
|
+
out.skipped_lines += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (rec.transport !== MCP_LITE_TRANSPORT) {
|
|
114
|
+
out.other_transport += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const tool = typeof rec.tool_name === 'string' ? rec.tool_name : '';
|
|
118
|
+
if (tool === '') {
|
|
119
|
+
out.skipped_lines += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const ms = _ms(rec.ts);
|
|
123
|
+
if (since !== null && (ms === null || ms < since)) continue;
|
|
124
|
+
|
|
125
|
+
out.calls += 1;
|
|
126
|
+
_bump(out.by_tool, tool);
|
|
127
|
+
_bump(out.by_host, typeof rec.host === 'string' && rec.host !== '' ? rec.host : 'unknown');
|
|
128
|
+
_bump(out.by_outcome, typeof rec.outcome === 'string' ? rec.outcome : 'unknown');
|
|
129
|
+
const ts = typeof rec.ts === 'string' ? rec.ts : null;
|
|
130
|
+
if (ts !== null) {
|
|
131
|
+
if (out.earliest_ts === null || ts < out.earliest_ts) out.earliest_ts = ts;
|
|
132
|
+
if (out.latest_ts === null || ts > out.latest_ts) out.latest_ts = ts;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Descending by count, then by name, so the rendering is deterministic. */
|
|
139
|
+
function _sorted(m: Map<string, number>): [string, number][] {
|
|
140
|
+
return [...m.entries()].sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ZERO_NOTE =
|
|
144
|
+
'_No calls recorded. The lite-surface emitter is default-off '
|
|
145
|
+
+ '(`telemetry.artifact_engagement.enabled`), so a zero here says the '
|
|
146
|
+
+ 'instrument was off or unused on this machine — it is not a measurement '
|
|
147
|
+
+ 'of what consumers do._';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The markdown section. Always renders, zero included.
|
|
151
|
+
*
|
|
152
|
+
* Ids are re-validated through the engagement redaction floor even though the
|
|
153
|
+
* emitter already clamps them: this renderer prints whatever is in the file,
|
|
154
|
+
* and a file is not a boundary the emitter controls.
|
|
155
|
+
*/
|
|
156
|
+
export function render_mcp_lite_markdown(agg: McpLiteAggregate): string {
|
|
157
|
+
const lines: string[] = [];
|
|
158
|
+
lines.push('## MCP lite surface');
|
|
159
|
+
lines.push('');
|
|
160
|
+
lines.push(`- calls recorded: **${agg.calls}**`);
|
|
161
|
+
if (agg.earliest_ts && agg.latest_ts) {
|
|
162
|
+
lines.push(`- ts range: \`${agg.earliest_ts}\` → \`${agg.latest_ts}\``);
|
|
163
|
+
}
|
|
164
|
+
lines.push('');
|
|
165
|
+
if (agg.calls === 0) {
|
|
166
|
+
lines.push(ZERO_NOTE);
|
|
167
|
+
lines.push('');
|
|
168
|
+
return `${lines.join('\n')}`;
|
|
169
|
+
}
|
|
170
|
+
lines.push('| tool | calls |');
|
|
171
|
+
lines.push('|---|---:|');
|
|
172
|
+
for (const [tool, n] of _sorted(agg.by_tool)) {
|
|
173
|
+
check_id_redaction('mcp_lite.by_tool.id', tool);
|
|
174
|
+
lines.push(`| ${tool} | ${n} |`);
|
|
175
|
+
}
|
|
176
|
+
lines.push('');
|
|
177
|
+
lines.push('| host | calls |');
|
|
178
|
+
lines.push('|---|---:|');
|
|
179
|
+
for (const [host, n] of _sorted(agg.by_host)) {
|
|
180
|
+
check_id_redaction('mcp_lite.by_host.id', host);
|
|
181
|
+
lines.push(`| ${host} | ${n} |`);
|
|
182
|
+
}
|
|
183
|
+
lines.push('');
|
|
184
|
+
return `${lines.join('\n')}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Plain object for the JSON format. Same validation as the markdown path. */
|
|
188
|
+
export function mcp_lite_json(agg: McpLiteAggregate): Record<string, unknown> {
|
|
189
|
+
const by_tool: Record<string, number> = {};
|
|
190
|
+
for (const [tool, n] of _sorted(agg.by_tool)) {
|
|
191
|
+
check_id_redaction('mcp_lite.by_tool.id', tool);
|
|
192
|
+
by_tool[tool] = n;
|
|
193
|
+
}
|
|
194
|
+
const by_host: Record<string, number> = {};
|
|
195
|
+
for (const [host, n] of _sorted(agg.by_host)) {
|
|
196
|
+
check_id_redaction('mcp_lite.by_host.id', host);
|
|
197
|
+
by_host[host] = n;
|
|
198
|
+
}
|
|
199
|
+
const by_outcome: Record<string, number> = {};
|
|
200
|
+
for (const [outcome, n] of _sorted(agg.by_outcome)) {
|
|
201
|
+
by_outcome[outcome] = n;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
log_path: agg.log_path,
|
|
205
|
+
calls: agg.calls,
|
|
206
|
+
skipped_lines: agg.skipped_lines,
|
|
207
|
+
by_tool,
|
|
208
|
+
by_host,
|
|
209
|
+
by_outcome,
|
|
210
|
+
earliest_ts: agg.earliest_ts,
|
|
211
|
+
latest_ts: agg.latest_ts,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
rank_artefacts,
|
|
13
13
|
} from './aggregator.js';
|
|
14
14
|
import { ALLOWED_OUTCOMES, check_id_redaction } from './engagement.js';
|
|
15
|
+
import { mcp_lite_json, render_mcp_lite_markdown, type McpLiteAggregate } from './mcp_lite.js';
|
|
15
16
|
|
|
16
17
|
export const QUARTILE_TOP_RATIO = 0.20;
|
|
17
18
|
export const QUARTILE_BOTTOM_RATIO = 0.20;
|
|
@@ -57,6 +58,17 @@ export function bucketise(stats: ArtefactStat[]): BucketedStat[] {
|
|
|
57
58
|
export interface RenderOptions {
|
|
58
59
|
top?: number | null;
|
|
59
60
|
since_label?: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* The MCP-lite reading, when the caller resolved one (step 4.2).
|
|
63
|
+
*
|
|
64
|
+
* Optional, and ABSENT is the default: a caller that does not supply it
|
|
65
|
+
* gets exactly the report this renderer produced before, byte for byte.
|
|
66
|
+
* That matters concretely — the Python→TS parity goldens for this script
|
|
67
|
+
* are frozen snapshots of a `.py` that no longer exists and can never be
|
|
68
|
+
* re-captured, so a section that rendered unconditionally would break a
|
|
69
|
+
* pin that cannot be repaired.
|
|
70
|
+
*/
|
|
71
|
+
mcp_lite?: McpLiteAggregate | null;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/** Render a markdown report. `top` truncates each bucket; null keeps all. */
|
|
@@ -137,6 +149,9 @@ export function render_markdown(aggregate: AggregateResult, opts: RenderOptions
|
|
|
137
149
|
}
|
|
138
150
|
lines.push('');
|
|
139
151
|
}
|
|
152
|
+
if (opts.mcp_lite) {
|
|
153
|
+
lines.push(render_mcp_lite_markdown(opts.mcp_lite));
|
|
154
|
+
}
|
|
140
155
|
return `${_pyRStrip(lines.join('\n'))}\n`;
|
|
141
156
|
}
|
|
142
157
|
|
|
@@ -181,6 +196,7 @@ export function render_json(aggregate: AggregateResult, opts: RenderOptions = {}
|
|
|
181
196
|
},
|
|
182
197
|
outcomes: outcomes_payload,
|
|
183
198
|
buckets: grouped,
|
|
199
|
+
...(opts.mcp_lite ? { mcp_lite: mcp_lite_json(opts.mcp_lite) } : {}),
|
|
184
200
|
};
|
|
185
201
|
return `${_py_json_dumps_indent2_sorted(payload)}\n`;
|
|
186
202
|
}
|
|
@@ -19,6 +19,7 @@ import { aggregate } from './telemetry/aggregator.js';
|
|
|
19
19
|
import { EngagementSchemaError } from './telemetry/engagement.js';
|
|
20
20
|
import { render_json, render_markdown } from './telemetry/report_renderer.js';
|
|
21
21
|
import { read_settings } from './telemetry/settings.js';
|
|
22
|
+
import { MCP_LITE_LOG_REL, aggregate_mcp_lite } from './telemetry/mcp_lite.js';
|
|
22
23
|
|
|
23
24
|
const _DURATION_RE = /^\s*(\d+)\s*([dhm])\s*$/u;
|
|
24
25
|
|
|
@@ -59,6 +60,7 @@ interface ReportArgs {
|
|
|
59
60
|
format: string;
|
|
60
61
|
log_path: string | null;
|
|
61
62
|
settings: string;
|
|
63
|
+
mcp_log_path: string | null;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
class ArgError extends Error {}
|
|
@@ -70,6 +72,7 @@ function _parseArgs(argv: string[]): ReportArgs {
|
|
|
70
72
|
format: 'markdown',
|
|
71
73
|
log_path: null,
|
|
72
74
|
settings: '.agent-settings.yml',
|
|
75
|
+
mcp_log_path: null,
|
|
73
76
|
};
|
|
74
77
|
const formatChoices = ['markdown', 'json'];
|
|
75
78
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -104,6 +107,7 @@ function _parseArgs(argv: string[]): ReportArgs {
|
|
|
104
107
|
}
|
|
105
108
|
a.format = v;
|
|
106
109
|
} else if (tok === '--log-path') a.log_path = next();
|
|
110
|
+
else if (tok === '--mcp-log-path') a.mcp_log_path = next();
|
|
107
111
|
else if (tok === '--settings') a.settings = next();
|
|
108
112
|
else throw new ArgError(`unrecognized arguments: ${argv[i]}`);
|
|
109
113
|
}
|
|
@@ -155,13 +159,29 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
155
159
|
return 2;
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// The MCP-lite section (step 4.2) renders in the DEFAULT, settings-driven
|
|
163
|
+
// invocation — which is what `agent-config telemetry:report` is — and when
|
|
164
|
+
// a caller names the sink explicitly. It does NOT render when the caller
|
|
165
|
+
// pointed `--log-path` at one specific engagement log and named no MCP
|
|
166
|
+
// sink: in that mode the report is scoped to the file it was handed and
|
|
167
|
+
// does not go looking for other stores on the machine. That rule also keeps
|
|
168
|
+
// the frozen Python-parity goldens intact, since every pinned invocation
|
|
169
|
+
// passes `--log-path`; the goldens cannot be re-captured, the `.py` having
|
|
170
|
+
// been retired, so an unconditional section would break a pin permanently.
|
|
171
|
+
const want_mcp = args.mcp_log_path !== null || args.log_path === null;
|
|
172
|
+
const mcp_lite = want_mcp
|
|
173
|
+
? aggregate_mcp_lite(args.mcp_log_path ?? path.join(process.cwd(), MCP_LITE_LOG_REL), {
|
|
174
|
+
since: cutoff,
|
|
175
|
+
})
|
|
176
|
+
: null;
|
|
177
|
+
|
|
158
178
|
const top = args.top <= 0 ? null : args.top;
|
|
159
179
|
let rendered: string;
|
|
160
180
|
try {
|
|
161
181
|
if (args.format === 'json') {
|
|
162
|
-
rendered = render_json(result, { top, since_label });
|
|
182
|
+
rendered = render_json(result, { top, since_label, mcp_lite });
|
|
163
183
|
} else {
|
|
164
|
-
rendered = render_markdown(result, { top, since_label });
|
|
184
|
+
rendered = render_markdown(result, { top, since_label, mcp_lite });
|
|
165
185
|
}
|
|
166
186
|
} catch (exc) {
|
|
167
187
|
if (exc instanceof EngagementSchemaError) {
|
package/dist/cli/mcp/dispatch.js
CHANGED
|
@@ -167,6 +167,16 @@ export const LITE_TOOLS = [
|
|
|
167
167
|
},
|
|
168
168
|
];
|
|
169
169
|
const LITE_TOOL_NAMES = new Set(LITE_TOOLS.map((t) => t.name));
|
|
170
|
+
/**
|
|
171
|
+
* Is this name one of the two tools this surface actually runs?
|
|
172
|
+
*
|
|
173
|
+
* Exported so the impure transport shell can classify a call's outcome without
|
|
174
|
+
* re-deriving the tool list — one reader of one fact, the same discipline
|
|
175
|
+
* `describeHostCapabilities` applies to the capability manifest.
|
|
176
|
+
*/
|
|
177
|
+
export function isLiteTool(name) {
|
|
178
|
+
return LITE_TOOL_NAMES.has(name);
|
|
179
|
+
}
|
|
170
180
|
/** Chars/4 over the served `tools/list` payload — the figure the cap is on. */
|
|
171
181
|
export function liteToolsTokenCost() {
|
|
172
182
|
return Math.round(JSON.stringify({ tools: LITE_TOOLS }).length / 4);
|
package/dist/cli/mcp/stdio.js
CHANGED
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
* a single stray stdout byte breaks the JSON-RPC stream for the client.
|
|
11
11
|
*
|
|
12
12
|
* Notifications (no `id`) receive no response, per JSON-RPC 2.0.
|
|
13
|
+
*
|
|
14
|
+
* This shell is also where call telemetry is emitted (roadmap step 4.1):
|
|
15
|
+
* `dispatch` is pure — no I/O, no clock — and stays that way, so the one row
|
|
16
|
+
* per `tools/call` is written here, from the impure side, after the response
|
|
17
|
+
* has been produced. The recorder is default-off and never throws; see
|
|
18
|
+
* `./telemetry.js` for the gate and the closed host vocabulary.
|
|
13
19
|
*/
|
|
14
20
|
import { createInterface } from 'node:readline';
|
|
15
|
-
import { dispatch, rpcError, RPC_PARSE_ERROR, RPC_INVALID_REQUEST, } from './dispatch.js';
|
|
21
|
+
import { dispatch, isLiteTool, rpcError, RPC_PARSE_ERROR, RPC_INVALID_REQUEST, } from './dispatch.js';
|
|
22
|
+
import { HOST_UNKNOWN, normalizeHost, recordLiteCall } from './telemetry.js';
|
|
16
23
|
function isNotification(value) {
|
|
17
24
|
return typeof value === 'object' && value !== null && !('id' in value);
|
|
18
25
|
}
|
|
@@ -24,6 +31,10 @@ function isNotification(value) {
|
|
|
24
31
|
export function runStdioServer(tree, identity, opts = {}) {
|
|
25
32
|
const input = opts.input ?? process.stdin;
|
|
26
33
|
const output = opts.output ?? process.stdout;
|
|
34
|
+
// The client's own name, learned at `initialize` and resolved onto the
|
|
35
|
+
// closed host vocabulary. A client that calls a tool before initializing
|
|
36
|
+
// records `unknown` — which is the truth, not a guess.
|
|
37
|
+
let host = HOST_UNKNOWN;
|
|
27
38
|
const write = (resp) => {
|
|
28
39
|
output.write(`${JSON.stringify(resp)}\n`);
|
|
29
40
|
};
|
|
@@ -51,7 +62,27 @@ export function runStdioServer(tree, identity, opts = {}) {
|
|
|
51
62
|
write(rpcError(id, RPC_INVALID_REQUEST, 'Invalid Request'));
|
|
52
63
|
return;
|
|
53
64
|
}
|
|
54
|
-
|
|
65
|
+
const req = parsed;
|
|
66
|
+
if (req.method === 'initialize') {
|
|
67
|
+
const p = (req.params ?? {});
|
|
68
|
+
const info = (p.clientInfo ?? {});
|
|
69
|
+
host = normalizeHost(info.name);
|
|
70
|
+
}
|
|
71
|
+
write(dispatch(tree, identity, req));
|
|
72
|
+
// Exactly one row per `tools/call` that named a tool. A call with
|
|
73
|
+
// no `name` is a malformed request, not a call to a tool, and has
|
|
74
|
+
// nothing to attribute a row to.
|
|
75
|
+
if (req.method === 'tools/call') {
|
|
76
|
+
const p = (req.params ?? {});
|
|
77
|
+
const name = typeof p.name === 'string' ? p.name : '';
|
|
78
|
+
recordLiteCall({
|
|
79
|
+
toolName: name,
|
|
80
|
+
isLiteTool: isLiteTool(name),
|
|
81
|
+
host,
|
|
82
|
+
consumerRoot: opts.consumerRoot,
|
|
83
|
+
settingsPath: opts.settingsPath,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
55
86
|
});
|
|
56
87
|
rl.on('close', () => {
|
|
57
88
|
// Drain any buffered stdout before resolving — on macOS a pipe write
|