@event4u/agent-config 14.6.0 → 14.7.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 (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +260 -7
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  5. package/dist/agent-src/commands/analyze.md +1 -1
  6. package/dist/agent-src/commands/roadmap/process-full.md +116 -30
  7. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  8. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +135 -104
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/decision-revisit-gate.md +3 -0
  11. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  12. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  13. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  14. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  15. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  16. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  17. package/dist/agent-src/rules/skill-quality.md +0 -2
  18. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  19. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  20. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  21. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  22. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  23. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  24. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  25. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  26. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  27. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  28. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  29. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  30. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  31. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  32. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  33. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  34. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  35. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  36. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  37. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  38. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  39. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  40. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  41. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  42. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  43. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  44. package/dist/agent-src/templates/roadmaps.md +45 -2
  45. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  46. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  47. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  48. package/dist/cli/registry.js +1 -1
  49. package/dist/cli/registry.js.map +1 -1
  50. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  51. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  52. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  53. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  54. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  55. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  57. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  58. package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-JZ4NR2TW.js} +338 -0
  59. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  60. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  61. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  62. package/dist/cli-delegate/cmd_conformance.js +459 -709
  63. package/dist/cli-delegate/cmd_doctor.js +6 -6
  64. package/dist/cli-delegate/cmd_explain.js +1 -1
  65. package/dist/cli-delegate/cmd_export.js +7 -6
  66. package/dist/cli-delegate/cmd_handoff.js +6 -6
  67. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  68. package/dist/cli-delegate/cmd_preflight.js +2 -2
  69. package/dist/cli-delegate/cmd_prune.js +7 -6
  70. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  71. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  72. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  73. package/dist/cli-delegate/cmd_sync.js +7 -6
  74. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  75. package/dist/cli-delegate/cmd_update.js +1 -1
  76. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  77. package/dist/cli-delegate/cmd_validate.js +7 -6
  78. package/dist/discovery/deprecation-report.md +1 -1
  79. package/dist/discovery/discovery-manifest.json +68 -25
  80. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  81. package/dist/discovery/discovery-manifest.summary.md +4 -4
  82. package/dist/discovery/dormancy-report.md +1 -1
  83. package/dist/discovery/orphan-report.md +1 -1
  84. package/dist/discovery/packs.json +10 -8
  85. package/dist/discovery/trust-report.md +3 -3
  86. package/dist/discovery/workspaces.json +8 -6
  87. package/dist/hooks/dispatch.js +2901 -2053
  88. package/dist/install/claudePathsPlan.js +187 -0
  89. package/dist/install/claudePathsPlan.js.map +1 -0
  90. package/dist/install/hostLayerFingerprint.js +136 -0
  91. package/dist/install/hostLayerFingerprint.js.map +1 -0
  92. package/dist/install/install.mjs +553 -425
  93. package/dist/install/partitionEligibility.js +276 -0
  94. package/dist/install/partitionEligibility.js.map +1 -0
  95. package/dist/mcp/registry-manifest.json +3 -3
  96. package/dist/mcp/server.json +2 -2
  97. package/dist/mcp/server.mjs +1744 -1191
  98. package/dist/router.json +1 -1
  99. package/dist/scripts/_lib/agent_src.js +41 -0
  100. package/dist/scripts/_lib/agent_src.js.map +1 -1
  101. package/dist/scripts/_lib/install_layout.js +83 -0
  102. package/dist/scripts/_lib/install_layout.js.map +1 -0
  103. package/dist/scripts/_lib/installed_lock.js +444 -0
  104. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  105. package/dist/scripts/condense.js +13 -164
  106. package/dist/scripts/condense.js.map +1 -1
  107. package/dist/server/schemas/settings.js +4 -0
  108. package/dist/server/schemas/settings.js.map +1 -1
  109. package/docs/contracts/settings-classes.md +12 -6
  110. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  111. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  112. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  113. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  114. package/package.json +1 -1
  115. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  116. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  117. package/src/agent-src/scripts/gate_budget.ts +282 -0
  118. package/src/agent-src/scripts/gate_execute.ts +72 -31
  119. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  120. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  121. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  122. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  123. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  124. package/src/config/agent-settings.template.yml +27 -2
  125. package/src/config/ci-local-parity.yml +18 -0
  126. package/src/config/estate-count-budget.json +339 -3
  127. package/src/config/gate-violation-baselines.json +41 -41
  128. package/src/config/rule-activation-census.json +14 -6
  129. package/src/config/rule-stub-ceilings.json +19 -6
  130. package/src/install/claudePathsPlan.ts +196 -0
  131. package/src/install/hostLayerFingerprint.ts +145 -0
  132. package/src/install/partitionEligibility.ts +325 -0
  133. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  134. package/src/scripts/_dispatch.bash +6 -1
  135. package/src/scripts/_lib/ac_heading.ts +30 -0
  136. package/src/scripts/_lib/agent_src.ts +42 -0
  137. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  138. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  139. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  140. package/src/scripts/_lib/installed_lock.ts +50 -5
  141. package/src/scripts/_lib/md_section.ts +129 -0
  142. package/src/scripts/_lib/scope_guard.sh +26 -3
  143. package/src/scripts/_lib/self_repair.ts +19 -0
  144. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  145. package/src/scripts/ai_council/cli_help.ts +55 -0
  146. package/src/scripts/before_complete_hook.ts +153 -28
  147. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  148. package/src/scripts/check_references.ts +60 -0
  149. package/src/scripts/check_release_published.ts +32 -11
  150. package/src/scripts/check_single_delivery.ts +59 -15
  151. package/src/scripts/ci_settle.ts +170 -0
  152. package/src/scripts/condense.ts +20 -170
  153. package/src/scripts/council_cli.ts +3 -3
  154. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  155. package/src/scripts/hook_manifest.json +1 -1
  156. package/src/scripts/hook_manifest.yaml +77 -14
  157. package/src/scripts/hooks/block_no_verify.ts +61 -10
  158. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  159. package/src/scripts/hooks/concern_registry.ts +4 -0
  160. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  161. package/src/scripts/hooks/run_continuation_hook.ts +8 -5
  162. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  163. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  164. package/src/scripts/hooks/state_io.ts +782 -20
  165. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  166. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  167. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  168. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  169. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  170. package/src/scripts/install.ts +16 -11
  171. package/src/scripts/language_mirror_hook.ts +561 -25
  172. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  173. package/src/scripts/lint_plan_risk_register.ts +4 -3
  174. package/src/scripts/lint_skill_router_head.ts +4 -4
  175. package/src/scripts/mcp_server/tools.ts +2 -2
  176. package/src/scripts/new_roadmap.ts +136 -0
  177. package/src/scripts/pack_mcp_content.ts +20 -5
  178. package/src/scripts/release.ts +37 -38
  179. package/src/scripts/report_layer_overlap.ts +6 -0
  180. package/src/scripts/self_repair_hook.ts +37 -5
  181. package/src/scripts/session_register_hook.ts +11 -0
  182. package/src/scripts/skill_usage_collect.ts +11 -1
  183. package/src/scripts/skill_usage_report.ts +278 -8
  184. package/src/scripts/sync_pr_branch.ts +7 -0
  185. package/src/server/schemas/settings.ts +8 -0
  186. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  187. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  188. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Aggregate `agents/runtime/metrics/skill-usage.jsonl` into a per-skill report.
3
+ * Aggregate `agents/runtime/metrics/skill-usage.jsonl` into a per-skill report,
4
+ * plus a second section over the Class-A sink export when one is present.
4
5
  *
5
6
  * Ported from the retired Python `src/scripts/skill_usage_report.py` (ADR-200,
6
7
  * Phase 8 / Wave 8b). The public surface, CLI contract, exit code,
@@ -19,13 +20,47 @@ import * as path from 'node:path';
19
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
20
21
 
21
22
  import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
23
+ // The "existing threshold constant" the step names — the distinct-user bar the
24
+ // tier-usage re-tier already ships. Imported rather than restated so the sink
25
+ // section and the re-tier decision cannot drift to two different numbers.
26
+ import { DEFAULT_TIER_USAGE_RETIER } from '../agent-src/templates/scripts/telemetry/settings.js';
22
27
 
23
28
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
24
29
  // parent.parent.parent of the .py file (src/scripts/skill_usage_report.py)
25
30
  // is the repo root — two dirs up from src/scripts.
26
31
  const REPO = path.resolve(_HERE, '..', '..');
27
- const IN = path.join(REPO, 'agents', 'metrics', 'skill-usage.jsonl');
28
- const OUT = path.join(REPO, 'agents', 'metrics', 'skill-usage-report.md');
32
+ // Both of these pointed into `agents/metrics/`, a directory the agents/
33
+ // taxonomy consolidation retired — so the report read a file that does not
34
+ // exist and wrote beside it, while the real record set sat at
35
+ // `agents/runtime/metrics/` and the tracked report at
36
+ // `agents/evidence/metrics/`. road-to-org-telemetry Phase 0 named this repair
37
+ // a Phase 4 prerequisite: adding the sink as a SECOND source lands beside a
38
+ // first source that reads nothing. Destinations follow the agents/ layout
39
+ // contract, and both match what `taskfiles/ci-fast.yml`'s own task
40
+ // descriptions already claimed this script does.
41
+ //
42
+ // OUT IS THE RUNTIME COPY, NOT THE TRACKED ONE, AND THAT IS DELIBERATE. The
43
+ // tracked `agents/evidence/metrics/skill-usage-report.md` is a promoted
44
+ // baseline snapshot the roadmap cites by its numbers. Pointing OUT at it makes
45
+ // every run in a checkout WITHOUT the gitignored record set overwrite that
46
+ // baseline with zeros — measured while writing this change: 297 insertions /
47
+ // 339 deletions, every row reclassified `dead`, from a worktree where the
48
+ // JSONL simply does not exist. A measurement that corrupts its own baseline is
49
+ // worse than no measurement. Promotion to `evidence/` stays a deliberate copy.
50
+ const IN = path.join(REPO, 'agents', 'runtime', 'metrics', 'skill-usage.jsonl');
51
+ const OUT = path.join(REPO, 'agents', 'runtime', 'metrics', 'skill-usage-report.md');
52
+
53
+ /**
54
+ * The sink export — the SECOND source (Phase 4, step 4.1).
55
+ *
56
+ * Class-A records as JSONL, either an export of the org sink or one install's
57
+ * own local `.agent-telemetry.jsonl`. Default-absent is the normal state and
58
+ * is reported as such: `sink-choice` is transferred, so no sink exists for
59
+ * this repository to read, and a section that silently rendered nothing would
60
+ * be indistinguishable from a sink that answered zero — which is the exact
61
+ * blind-zero confusion this roadmap exists to remove.
62
+ */
63
+ const SINK_IN = path.join(REPO, 'agents', 'runtime', 'metrics', 'telemetry-class-a.jsonl');
29
64
 
30
65
  interface Bucket {
31
66
  exposures_total: number;
@@ -197,7 +232,214 @@ function _dateIso(d: Date): string {
197
232
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
198
233
  }
199
234
 
200
- export function render(per: Map<string, Bucket>, known: Set<string>): string {
235
+ // ── The sink as a second source (Phase 4, step 4.1) ────────────────────────
236
+ //
237
+ // WHY THIS IS A SEPARATE SECTION AND NOT EXTRA COLUMNS. The two sources do not
238
+ // measure the same thing and cannot share a row. The first source is a regex
239
+ // scan of the maintainer's own transcripts, so it counts *exposure* and
240
+ // *mention* on one machine; Phase 0's third spike measured it detecting **0 of
241
+ // 89** real invocations on the set it reads. The sink counts *invocations* with
242
+ // a pseudonymous user per record, across every consenting install. Merging them
243
+ // into one table would produce a row whose numbers come from different
244
+ // populations, which is exactly the kind of arithmetic the zero-activation
245
+ // reading already taught this repository not to trust.
246
+ //
247
+ // DISTINCT USERS ARE THE POINT. A per-skill invocation count from one heavy
248
+ // user is not adoption. `user_hash` is a salted digest of host + login, so it
249
+ // distinguishes people without naming them, and the threshold below is the bar
250
+ // the tier-usage re-tier already uses.
251
+
252
+ /** One Class-A usage record, as the emitter writes it. */
253
+ interface ClassALine {
254
+ skill: string;
255
+ user_hash: string;
256
+ session_hash: string | null;
257
+ host: string | null;
258
+ ts_bucket: string;
259
+ record_class: string;
260
+ }
261
+
262
+ export interface SinkBucket {
263
+ invocations_total: number;
264
+ invocations_window: number;
265
+ users_window: Set<string>;
266
+ sessions_window: Set<string>;
267
+ hosts: Set<string>;
268
+ last_seen: Date | null;
269
+ }
270
+
271
+ /**
272
+ * Parse an hour bucket (`2026-08-20T10:00Z`).
273
+ *
274
+ * Its own parser rather than `parse_ts`: the first source carries a full ISO
275
+ * timestamp and the sink deliberately carries hour resolution only, so one
276
+ * parser over both would quietly accept a precision neither guarantees.
277
+ */
278
+ export function parse_hour_bucket(raw: unknown): Date | null {
279
+ if (typeof raw !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:00Z$/u.test(raw)) {
280
+ return null;
281
+ }
282
+ const d = new Date(raw);
283
+ return Number.isNaN(d.getTime()) ? null : d;
284
+ }
285
+
286
+ /** Load Class-A records. A missing sink export is a normal state, not an error. */
287
+ export function load_class_a(p: string): ClassALine[] {
288
+ let text = '';
289
+ try {
290
+ text = fs.readFileSync(p, 'utf-8');
291
+ } catch {
292
+ return [];
293
+ }
294
+ const out: ClassALine[] = [];
295
+ for (const line of text.split('\n')) {
296
+ if (!line.trim()) continue;
297
+ try {
298
+ const rec = JSON.parse(line) as Record<string, unknown>;
299
+ const skill = rec['skill'];
300
+ const user_hash = rec['user_hash'];
301
+ const ts_bucket = rec['ts_bucket'];
302
+ // A record missing any of the three fields the aggregate is built
303
+ // from is dropped rather than counted with a substitute: a
304
+ // fabricated user or a fabricated hour both inflate the very
305
+ // numbers an estate decision would cite.
306
+ if (typeof skill !== 'string' || typeof user_hash !== 'string') continue;
307
+ if (typeof ts_bucket !== 'string') continue;
308
+ out.push({
309
+ skill,
310
+ user_hash,
311
+ session_hash: typeof rec['session_hash'] === 'string' ? rec['session_hash'] : null,
312
+ host: typeof rec['host'] === 'string' ? rec['host'] : null,
313
+ ts_bucket,
314
+ record_class: typeof rec['record_class'] === 'string' ? rec['record_class'] : '',
315
+ });
316
+ } catch {
317
+ // Same tolerance as the first source: a truncated tail line does
318
+ // not discard the file.
319
+ }
320
+ }
321
+ return out;
322
+ }
323
+
324
+ export function aggregate_sink(
325
+ records: readonly ClassALine[],
326
+ now: Date,
327
+ window_days: number,
328
+ ): Map<string, SinkBucket> {
329
+ const cutoff = now.getTime() - window_days * 86400_000;
330
+ const per = new Map<string, SinkBucket>();
331
+ for (const r of records) {
332
+ // Only usage records feed a usage report. Phase 5 adds a self-repair
333
+ // class to the same schema, and counting those as invocations would
334
+ // read defect reports as adoption.
335
+ if (r.record_class !== 'usage') continue;
336
+ const ts = parse_hour_bucket(r.ts_bucket);
337
+ if (ts === null) continue;
338
+ let b = per.get(r.skill);
339
+ if (b === undefined) {
340
+ b = {
341
+ invocations_total: 0,
342
+ invocations_window: 0,
343
+ users_window: new Set<string>(),
344
+ sessions_window: new Set<string>(),
345
+ hosts: new Set<string>(),
346
+ last_seen: null,
347
+ };
348
+ per.set(r.skill, b);
349
+ }
350
+ b.invocations_total += 1;
351
+ if (r.host !== null) b.hosts.add(r.host);
352
+ if (b.last_seen === null || ts.getTime() > b.last_seen.getTime()) b.last_seen = ts;
353
+ if (ts.getTime() >= cutoff) {
354
+ b.invocations_window += 1;
355
+ b.users_window.add(r.user_hash);
356
+ if (r.session_hash !== null) b.sessions_window.add(r.session_hash);
357
+ }
358
+ }
359
+ return per;
360
+ }
361
+
362
+ /** The distinct-user bar, from the constant the re-tier already ships. */
363
+ export const MIN_DISTINCT_USERS = DEFAULT_TIER_USAGE_RETIER.min_distinct_users;
364
+
365
+ /**
366
+ * Render the sink section.
367
+ *
368
+ * An ABSENT or EMPTY sink renders a paragraph saying so, naming the path and
369
+ * the reason. That is the load-bearing case today: `sink-choice` is
370
+ * transferred, so no sink exists for this repository to read. A section that
371
+ * rendered nothing would leave a reader unable to tell "no sink" from "a sink
372
+ * that answered zero" — the blind-zero confusion this whole roadmap exists to
373
+ * remove, reproduced one layer up.
374
+ */
375
+ export function render_sink_section(
376
+ per: Map<string, SinkBucket>,
377
+ window_days: number,
378
+ source_rel: string,
379
+ present: boolean,
380
+ ): string[] {
381
+ const lines: string[] = ['', '## Second source — org sink (Class-A)', ''];
382
+ if (!present || per.size === 0) {
383
+ lines.push(
384
+ `> **No sink data.** \`${source_rel}\` ` +
385
+ (present ? 'holds no usage records' : 'does not exist') +
386
+ '. This is the expected state while the sink itself is transferred ' +
387
+ '(`sink-choice`, see `agents/roadmaps/stubs/road-to-org-telemetry-sink.md`): ' +
388
+ 'nothing has been stood up for this report to read. Read it as ' +
389
+ '**no instrument**, never as no adoption — the distinction is the ' +
390
+ 'reason this roadmap exists.',
391
+ );
392
+ lines.push('');
393
+ return lines;
394
+ }
395
+
396
+ const rows = [...per.entries()].sort((a, b) => {
397
+ const du = b[1].users_window.size - a[1].users_window.size;
398
+ if (du !== 0) return du;
399
+ const iv = b[1].invocations_window - a[1].invocations_window;
400
+ if (iv !== 0) return iv;
401
+ return pyStrCmp(a[0], b[0]);
402
+ });
403
+ const meeting = rows.filter(([, b]) => b.users_window.size >= MIN_DISTINCT_USERS).length;
404
+ lines.push(
405
+ `**Window:** ${window_days}-day rolling · **Source:** \`${source_rel}\` · ` +
406
+ `**Skills with any invocation:** ${rows.length} · ` +
407
+ `**At or above the ${MIN_DISTINCT_USERS}-distinct-user bar:** ${meeting}`,
408
+ );
409
+ lines.push('');
410
+ lines.push(
411
+ `| # | slug | invocations_${window_days}d | distinct_users_${window_days}d | ` +
412
+ `distinct_sessions_${window_days}d | hosts | meets_bar | last_seen |`,
413
+ );
414
+ lines.push('|---|---|---|---|---|---|---|---|');
415
+ let i = 0;
416
+ for (const [slug, b] of rows) {
417
+ i += 1;
418
+ const hosts = [...b.hosts].sort(pyStrCmp).join(', ') || '—';
419
+ const meets = b.users_window.size >= MIN_DISTINCT_USERS ? 'yes' : 'no';
420
+ const last = b.last_seen ? _dateIso(b.last_seen) : '—';
421
+ lines.push(
422
+ `| ${i} | \`${slug}\` | ${b.invocations_window} | ${b.users_window.size} | ` +
423
+ `${b.sessions_window.size} | ${hosts} | ${meets} | ${last} |`,
424
+ );
425
+ }
426
+ lines.push('');
427
+ lines.push(
428
+ `**Read-out:** \`meets_bar\` is the ${MIN_DISTINCT_USERS}-distinct-user threshold ` +
429
+ 'the tier-usage re-tier already uses; below it, a count is one person\'s habit ' +
430
+ 'rather than adoption. A host with no `Skill` tool emits no records at all, so ' +
431
+ 'a missing host in this table is a missing instrument and not a missing user.',
432
+ );
433
+ lines.push('');
434
+ return lines;
435
+ }
436
+
437
+ export function render(
438
+ per: Map<string, Bucket>,
439
+ known: Set<string>,
440
+ sink: { per: Map<string, SinkBucket>; source_rel: string; present: boolean } | null = null,
441
+ window_days = 30,
442
+ ): string {
201
443
  // sorted(known | set(per))
202
444
  const allSlugs = pySortedUnique([...known, ...per.keys()]);
203
445
  const rows: ReportRow[] = [];
@@ -227,7 +469,7 @@ export function render(per: Map<string, Bucket>, known: Set<string>): string {
227
469
  const lines: string[] = [
228
470
  '# Skill Usage Report (baseline)',
229
471
  '',
230
- '> Generated by `scripts/skill_usage_report.py`. Source:',
472
+ '> Generated by `src/scripts/skill_usage_report.ts`. Source:',
231
473
  '> `agents/runtime/metrics/skill-usage.jsonl` (collector emits per-turn',
232
474
  '> exposure/mention records). See',
233
475
  '> [`step-2-skill-inventory-rationalization.md`](../roadmaps/step-2-skill-inventory-rationalization.md)',
@@ -257,6 +499,9 @@ export function render(per: Map<string, Bucket>, known: Set<string>): string {
257
499
  'structural overlap before any deletion.',
258
500
  );
259
501
  lines.push('');
502
+ if (sink !== null) {
503
+ lines.push(...render_sink_section(sink.per, window_days, sink.source_rel, sink.present));
504
+ }
260
505
  return lines.join('\n');
261
506
  }
262
507
 
@@ -286,12 +531,14 @@ function _relativeToRepo(p: string): string {
286
531
  interface ParsedArgs {
287
532
  inp: string;
288
533
  out: string;
534
+ /** The Class-A sink export — the second source. */
535
+ sink: string;
289
536
  window: number;
290
537
  quiet: boolean;
291
538
  }
292
539
 
293
540
  export function parse_args(argv: string[]): ParsedArgs {
294
- const out: ParsedArgs = { inp: IN, out: OUT, window: 30, quiet: false };
541
+ const out: ParsedArgs = { inp: IN, out: OUT, sink: SINK_IN, window: 30, quiet: false };
295
542
  const fail = (msg: string): never => {
296
543
  process.stderr.write(`skill_usage_report: error: ${msg}\n`);
297
544
  process.exit(2);
@@ -310,6 +557,12 @@ export function parse_args(argv: string[]): ParsedArgs {
310
557
  out.out = path.resolve(v as string);
311
558
  } else if (a.startsWith('--out=')) {
312
559
  out.out = path.resolve(a.slice('--out='.length));
560
+ } else if (a === '--sink') {
561
+ const v = argv[++i];
562
+ if (v === undefined) fail('argument --sink: expected one argument');
563
+ out.sink = path.resolve(v as string);
564
+ } else if (a.startsWith('--sink=')) {
565
+ out.sink = path.resolve(a.slice('--sink='.length));
313
566
  } else if (a === '--window') {
314
567
  const v = argv[++i];
315
568
  if (v === undefined) fail('argument --window: expected one argument');
@@ -319,7 +572,7 @@ export function parse_args(argv: string[]): ParsedArgs {
319
572
  } else if (a === '--quiet') {
320
573
  out.quiet = true;
321
574
  } else if (a === '-h' || a === '--help') {
322
- process.stdout.write('usage: skill_usage_report [-h] [--in INP] [--out OUT] [--window WINDOW] [--quiet]\n');
575
+ process.stdout.write('usage: skill_usage_report [-h] [--in INP] [--out OUT] [--sink SINK] [--window WINDOW] [--quiet]\n');
323
576
  process.exit(0);
324
577
  } else {
325
578
  fail(`unrecognized arguments: ${a}`);
@@ -353,8 +606,25 @@ export function main(argv?: string[]): number {
353
606
  }
354
607
  throw exc;
355
608
  }
609
+ // The second source. Absent is the normal state and is rendered as such
610
+ // rather than omitted — see `render_sink_section`.
611
+ const sink_present = fs.existsSync(args.sink);
612
+ const sink_per = aggregate_sink(load_class_a(args.sink), now, args.window);
613
+ let sink_rel: string;
614
+ try {
615
+ sink_rel = _relativeToRepo(args.sink);
616
+ } catch {
617
+ // A sink export outside the repo is legitimate (an org may mount one);
618
+ // the report then names it by basename rather than refusing to render.
619
+ sink_rel = path.basename(args.sink);
620
+ }
621
+
356
622
  fs.mkdirSync(path.dirname(args.out), { recursive: true });
357
- fs.writeFileSync(args.out, render(per, known), 'utf-8');
623
+ fs.writeFileSync(
624
+ args.out,
625
+ render(per, known, { per: sink_per, source_rel: sink_rel, present: sink_present }, args.window),
626
+ 'utf-8',
627
+ );
358
628
  if (!args.quiet) {
359
629
  const count = new Set([...known, ...per.keys()]).size;
360
630
  process.stdout.write(`✅ Wrote ${_relativeToRepo(args.out)} (${count} skill(s))\n`);
@@ -40,6 +40,13 @@ const GENERATED = [
40
40
  'docs/catalog.md',
41
41
  'dist/agent-src/',
42
42
  '.augment/',
43
+ // Compiled from `hook_manifest.yaml` by `task build-ts`, never hand-written.
44
+ // It was classified AUTHORED until 2026-08-20, so this tool told the reader
45
+ // to "read both sides" on a file where mixing hunks yields a concern table
46
+ // matching NEITHER branch. One session resolved this same conflict three
47
+ // times and named it structural: main adds a concern and recompiles, so
48
+ // every open branch collides here. Regenerate; never merge.
49
+ 'src/scripts/hook_manifest.json',
43
50
  ];
44
51
 
45
52
  function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string; err: string } {
@@ -200,6 +200,14 @@ export const settingsSchema = z.object({
200
200
  horizon_weeks: z.number().int().min(0).default(0).describe(
201
201
  'Optional planning horizon (weeks) the agent shows in roadmap framing ("next 4 weeks"). Set 0 to omit the horizon — most teams prefer to ship without a hardcoded window.',
202
202
  ),
203
+ gate_budget: z.object({
204
+ max_cost_per_run_usd: z.number().min(0).default(5).describe(
205
+ 'Per-run USD ceiling for a CLASS-1 roadmap blocker executed via `agent-config gates --execute`. A class-1 entry whose **Budget:** field states a larger figure renders its consent line instead of running. Bounds the size of an authorised spend; never supplies the authorisation — `--confirm` is still required on every class-1 run.',
206
+ ),
207
+ max_cost_per_rolling_7d_usd: z.number().min(0).default(25).describe(
208
+ 'Rolling 7-day USD ceiling for class-1 gate execution, summed from the append-only receipt ledger at agents/runtime/state/gate-budget-ledger.jsonl. A run whose estimate would cross it renders instead of running. A per-run cap alone bounds one mistake, not a week of them, which is why option (a) of b-gate-budget-preauth carries two numbers.',
209
+ ),
210
+ }).default({ max_cost_per_run_usd: 5, max_cost_per_rolling_7d_usd: 25 }),
203
211
  }),
204
212
  planning: z.object({
205
213
  challenge_on_create: z.boolean().default(true).describe(
@@ -1,172 +0,0 @@
1
- import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);
2
-
3
- // src/scripts/hooks/state_io.ts
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- var LOCK_BASENAME = ".dispatcher.lock";
7
- var REPLAY_ENV_VAR = "AGENT_CONFIG_REPLAY";
8
- function is_replay_mode() {
9
- return (process.env[REPLAY_ENV_VAR] ?? "").trim() === "1";
10
- }
11
- function _lock_path(state_dir) {
12
- return path.join(state_dir, LOCK_BASENAME);
13
- }
14
- function _py_json_dumps(value, indent) {
15
- const pad = " ".repeat(indent);
16
- const escapeString = (s) => {
17
- let out = '"';
18
- for (const ch of s) {
19
- const code = ch.codePointAt(0);
20
- switch (ch) {
21
- case '"':
22
- out += '\\"';
23
- break;
24
- case "\\":
25
- out += "\\\\";
26
- break;
27
- case "\n":
28
- out += "\\n";
29
- break;
30
- case "\r":
31
- out += "\\r";
32
- break;
33
- case " ":
34
- out += "\\t";
35
- break;
36
- case "\b":
37
- out += "\\b";
38
- break;
39
- case "\f":
40
- out += "\\f";
41
- break;
42
- default:
43
- if (code < 32 || code > 126) {
44
- if (code > 65535) {
45
- const c = code - 65536;
46
- const hi = 55296 + (c >> 10);
47
- const lo = 56320 + (c & 1023);
48
- out += "\\u" + hi.toString(16).padStart(4, "0") + "\\u" + lo.toString(16).padStart(4, "0");
49
- } else {
50
- out += "\\u" + code.toString(16).padStart(4, "0");
51
- }
52
- } else {
53
- out += ch;
54
- }
55
- }
56
- }
57
- return out + '"';
58
- };
59
- const render = (v, depth) => {
60
- if (v === null || v === void 0) {
61
- return "null";
62
- }
63
- if (typeof v === "boolean") {
64
- return v ? "true" : "false";
65
- }
66
- if (typeof v === "number") {
67
- if (!Number.isFinite(v)) {
68
- if (Number.isNaN(v)) return "NaN";
69
- return v > 0 ? "Infinity" : "-Infinity";
70
- }
71
- return String(v);
72
- }
73
- if (typeof v === "string") {
74
- return escapeString(v);
75
- }
76
- const curPad = pad.repeat(depth + 1);
77
- const closePad = pad.repeat(depth);
78
- if (Array.isArray(v)) {
79
- if (v.length === 0) {
80
- return "[]";
81
- }
82
- const items = v.map((item) => curPad + render(item, depth + 1));
83
- return "[\n" + items.join(",\n") + "\n" + closePad + "]";
84
- }
85
- if (typeof v === "object") {
86
- const obj = v;
87
- const keys = Object.keys(obj);
88
- if (keys.length === 0) {
89
- return "{}";
90
- }
91
- const items = keys.map(
92
- (k) => curPad + escapeString(k) + ": " + render(obj[k], depth + 1)
93
- );
94
- return "{\n" + items.join(",\n") + "\n" + closePad + "}";
95
- }
96
- throw new TypeError(`Object of type ${typeof v} is not JSON serializable`);
97
- };
98
- return render(value, 0);
99
- }
100
- function atomic_write_json(target, payload, options = {}) {
101
- if (is_replay_mode()) {
102
- return;
103
- }
104
- const indent = options.indent ?? 2;
105
- const targetPath = path.resolve(target);
106
- const state_dir = path.dirname(targetPath);
107
- fs.mkdirSync(state_dir, { recursive: true });
108
- const body = _py_json_dumps(payload, indent) + "\n";
109
- _atomic_write_text(targetPath, body);
110
- }
111
- function atomic_write_text(target, text) {
112
- if (is_replay_mode()) {
113
- return;
114
- }
115
- const targetPath = path.resolve(target);
116
- const state_dir = path.dirname(targetPath);
117
- fs.mkdirSync(state_dir, { recursive: true });
118
- _atomic_write_text(targetPath, text);
119
- }
120
- function _acquire_lock(lock_path) {
121
- const fd = fs.openSync(lock_path, "a", 420);
122
- const held = lock_path + ".held";
123
- const deadlineMs = 5e3;
124
- const start = Date.now();
125
- for (; ; ) {
126
- try {
127
- const heldFd = fs.openSync(held, "wx", 420);
128
- fs.closeSync(heldFd);
129
- return fd;
130
- } catch (err) {
131
- if (err.code !== "EEXIST") {
132
- return fd;
133
- }
134
- if (Date.now() - start > deadlineMs) {
135
- try {
136
- fs.rmSync(held, { force: true });
137
- } catch {
138
- }
139
- continue;
140
- }
141
- const sab = new Int32Array(new SharedArrayBuffer(4));
142
- Atomics.wait(sab, 0, 0, 2);
143
- }
144
- }
145
- }
146
- function _release_lock(fd, lock_path) {
147
- try {
148
- fs.closeSync(fd);
149
- } catch {
150
- }
151
- try {
152
- fs.rmSync(lock_path + ".held", { force: true });
153
- } catch {
154
- }
155
- }
156
- function _atomic_write_text(target, text) {
157
- const tmp = target + `.tmp.${process.pid}`;
158
- const lock_path = _lock_path(path.dirname(target));
159
- const fd = _acquire_lock(lock_path);
160
- try {
161
- fs.writeFileSync(tmp, text, { encoding: "utf-8" });
162
- fs.renameSync(tmp, target);
163
- } finally {
164
- _release_lock(fd, lock_path);
165
- }
166
- }
167
-
168
- export {
169
- is_replay_mode,
170
- atomic_write_json,
171
- atomic_write_text
172
- };
@@ -1,38 +0,0 @@
1
- import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);
2
-
3
- // src/scripts/_lib/git_common_dir.ts
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- function realpath_or_self(p) {
7
- try {
8
- return fs.realpathSync(p);
9
- } catch {
10
- return p;
11
- }
12
- }
13
- function git_common_dir(project_root) {
14
- const dot_git = path.join(project_root, ".git");
15
- try {
16
- const st = fs.statSync(dot_git);
17
- if (st.isDirectory()) {
18
- return realpath_or_self(dot_git);
19
- }
20
- const m = fs.readFileSync(dot_git, "utf-8").match(/^gitdir:\s*(.+)\s*$/m);
21
- if (m === null) {
22
- return null;
23
- }
24
- const gitdir = path.resolve(project_root, m[1].trim());
25
- const commondir_file = path.join(gitdir, "commondir");
26
- if (fs.existsSync(commondir_file)) {
27
- const common = path.resolve(gitdir, fs.readFileSync(commondir_file, "utf-8").trim());
28
- return realpath_or_self(common);
29
- }
30
- return realpath_or_self(gitdir);
31
- } catch {
32
- return null;
33
- }
34
- }
35
-
36
- export {
37
- git_common_dir
38
- };