@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.
Files changed (161) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. 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 | **Note** — use `remember` tool, act on second occurrence |
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 / Note / Update existing}
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 store: there is no auto-write, no decay, no runtime
86
- (the writable per-project learning store stays rejected —
87
- [[council-agent-memory-sunset]]). The human decides whether the cross-project
88
- recurrence justifies promotion.
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: if `agents/runtime/state/HANDOFF.md` exists, 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.19.0"
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 trigger or a decision); **will resume**. Set `status: later` + a `Blocked until` / `Trigger` line. 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`.
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) {
@@ -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);
@@ -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
- write(dispatch(tree, identity, parsed));
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