@event4u/agent-config 14.5.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +384 -3
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/decision.md +7 -0
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/optimize/project.md +10 -0
- package/dist/agent-src/commands/roadmap/process-full.md +132 -4
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +45 -2
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
- package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
- package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_conformance.js +459 -709
- package/dist/cli-delegate/cmd_doctor.js +6 -6
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +7 -6
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +7 -6
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +7 -6
- package/dist/cli-delegate/cmd_uninstall.js +9 -7
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +7 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -29
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +3127 -2022
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +276 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1744 -1191
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/condense.js +13 -164
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +18 -0
- package/src/config/estate-count-budget.json +367 -3
- package/src/config/gate-violation-baselines.json +41 -41
- package/src/config/pack-size-budget.json +3 -2
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +19 -6
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +325 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +6 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +38 -8
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/scope_guard.sh +86 -4
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/_lib/subagent_capsule.ts +81 -1
- package/src/scripts/adr/regenerate_index.ts +13 -2
- package/src/scripts/adr_cite_check.ts +424 -0
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/check_adr_frontmatter.ts +187 -1
- package/src/scripts/check_references.ts +60 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_single_delivery.ts +517 -0
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +20 -170
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +887 -68
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +782 -20
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/install.sh +12 -2
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/memory_consolidation.ts +189 -0
- package/src/scripts/memory_eviction.ts +384 -0
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +78 -0
- package/src/scripts/report_memory_pointers.ts +622 -0
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +115 -15
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +7 -0
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
|
@@ -354,8 +354,17 @@ export function claim_is_stale(
|
|
|
354
354
|
// roadmaps directory" — and it is the same check for a traversal, an absolute
|
|
355
355
|
// path, and an encoding nobody enumerated. A slug that fails it is reported
|
|
356
356
|
// stale rather than resolved, so no read happens at all.
|
|
357
|
+
// Structural, not textual. Round 6 finding 3: the previous form ANDed a
|
|
358
|
+
// `!rel.includes('..')` substring test onto the prefix check, and round 5
|
|
359
|
+
// finding 10 removed exactly that test from `resolveRoadmap` — one-sided. The
|
|
360
|
+
// two functions then disagreed about one claim string in the opposite
|
|
361
|
+
// direction: for a legal slug like `road-to-a..b` the register rendered a live
|
|
362
|
+
// claim as stale (dropping it from the collision set and disabling the
|
|
363
|
+
// duplicate-work warning) while the hook resolved it and engaged. `path.dirname`
|
|
364
|
+
// asks the only question that matters — is the result a file directly inside the
|
|
365
|
+
// roadmaps directory — and is the same check both sides now make.
|
|
357
366
|
const rel = path.normalize(path.join('agents', 'roadmaps', `${base}.md`));
|
|
358
|
-
const inside =
|
|
367
|
+
const inside = path.dirname(rel) === path.join('agents', 'roadmaps');
|
|
359
368
|
if (!inside || path.isAbsolute(base)) {
|
|
360
369
|
return true; // not a slug this repo can hold; never render it as live work
|
|
361
370
|
}
|
|
@@ -409,29 +418,98 @@ export function claim_is_stale(
|
|
|
409
418
|
* conditions, because a wrong anchor is worse than the fallback:
|
|
410
419
|
*
|
|
411
420
|
* 1. it is an existing directory;
|
|
412
|
-
* 2. it
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* another repo would otherwise write this repo's register with a foreign
|
|
421
|
+
* 2. it resolves to the NEAREST enclosing checkout root, walking up from the
|
|
422
|
+
* reported directory;
|
|
423
|
+
* 3. that root belongs to the SAME repository — identical git common dir. A cwd
|
|
424
|
+
* in another repo would otherwise write this repo's register with a foreign
|
|
417
425
|
* branch, and the register is shared by every worktree here.
|
|
426
|
+
*
|
|
427
|
+
* ## Condition 2 used to reject a subdirectory, and that was wrong here
|
|
428
|
+
*
|
|
429
|
+
* It required the reported directory to BE a checkout root, and defended the
|
|
430
|
+
* rejection as "a session whose cwd is a subdirectory degrades to
|
|
431
|
+
* `workspace_root` — today's behaviour, never something worse". R2 round 3
|
|
432
|
+
* finding 2 measured that claim false in this repository's own layout, where
|
|
433
|
+
* worktrees live at `<parent>/.claude/worktrees/<name>` — NESTED under the
|
|
434
|
+
* parent. For a session standing at `<parent>/.claude/worktrees/wt/src`:
|
|
435
|
+
*
|
|
436
|
+
* - the degraded root is `<parent>`, i.e. equal to `workspace_root`;
|
|
437
|
+
* - so `git_dir` and `git_common_dir` taken from it are both `<parent>/.git`;
|
|
438
|
+
* - and the reported cwd is under `<parent>` as well.
|
|
439
|
+
*
|
|
440
|
+
* Every available signal then reports a healthy same-tree run for a genuine
|
|
441
|
+
* two-tree one. That is not a loss of precision, it is a confident wrong answer,
|
|
442
|
+
* which is strictly worse than the fallback the docblock was defending.
|
|
443
|
+
*
|
|
444
|
+
* The walk is bounded twice over, per the AI council's condition (2026-08-19,
|
|
445
|
+
* 2/2 convergent on this shape): it stops at the FIRST enclosing checkout root,
|
|
446
|
+
* so a nested worktree is found before the parent that contains it, and the
|
|
447
|
+
* same-repository check is retained on whatever it finds — a cwd inside an
|
|
448
|
+
* unrelated repo nested in this tree resolves to that repo's root, fails the
|
|
449
|
+
* identity check, and falls back. Paths are canonicalised before the walk so a
|
|
450
|
+
* symlinked cwd cannot walk a different chain than the one it names.
|
|
418
451
|
*/
|
|
419
452
|
export function session_checkout(
|
|
420
453
|
workspace_root: string,
|
|
421
454
|
payload_cwd: string | null | undefined,
|
|
422
455
|
): string {
|
|
456
|
+
// EVERY branch returns a canonical path, resolve and fallback alike.
|
|
457
|
+
//
|
|
458
|
+
// Round 4 finding 5: the resolve branch realpath-normalised and the four
|
|
459
|
+
// fallback branches returned `workspace_root` verbatim, so under a repo
|
|
460
|
+
// reached through a symlinked ancestor two sessions in the SAME working tree
|
|
461
|
+
// stored different strings for it — `/private/var/…/repo` from the resolver
|
|
462
|
+
// and `/var/…/repo` from the envelope. `foreign_sessions_block` compares the
|
|
463
|
+
// stored forms with `path.resolve`, which does not follow symlinks, so it read
|
|
464
|
+
// them as different worktrees and printed the benign "separate trees" note
|
|
465
|
+
// where the same-worktree COLLISION prompt belongs. The walk-up widened the
|
|
466
|
+
// exposure by moving subdirectory sessions onto the normalising branch.
|
|
467
|
+
const fallback = canonical(workspace_root);
|
|
423
468
|
const cwd = String(payload_cwd ?? '').trim();
|
|
424
|
-
if (cwd === '') return
|
|
469
|
+
if (cwd === '') return fallback;
|
|
470
|
+
let start: string;
|
|
425
471
|
try {
|
|
426
|
-
if (!fs.statSync(cwd).isDirectory()) return
|
|
472
|
+
if (!fs.statSync(cwd).isDirectory()) return fallback;
|
|
473
|
+
start = fs.realpathSync(cwd);
|
|
427
474
|
} catch {
|
|
428
|
-
return
|
|
475
|
+
return fallback;
|
|
429
476
|
}
|
|
430
|
-
|
|
431
|
-
|
|
477
|
+
const root = nearest_checkout_root(start);
|
|
478
|
+
if (root === null) return fallback;
|
|
479
|
+
const mine = git_common_dir(root);
|
|
432
480
|
const theirs = git_common_dir(workspace_root);
|
|
433
|
-
if (mine === null || theirs === null || mine !== theirs) return
|
|
434
|
-
return
|
|
481
|
+
if (mine === null || theirs === null || mine !== theirs) return fallback;
|
|
482
|
+
return root;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** `realpath` that falls back to an absolute form when the path is unreadable. */
|
|
486
|
+
function canonical(p: string): string {
|
|
487
|
+
const abs = path.resolve(p);
|
|
488
|
+
try {
|
|
489
|
+
return fs.realpathSync(abs);
|
|
490
|
+
} catch {
|
|
491
|
+
return abs;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The nearest enclosing directory that is a checkout root, or `null`.
|
|
497
|
+
*
|
|
498
|
+
* The iteration cap is a loop guard, not a policy: `path.dirname` is a fixed
|
|
499
|
+
* point at the filesystem root, so the terminating condition is the unchanged
|
|
500
|
+
* path and the counter only bounds a pathological filesystem. The FIRST hit
|
|
501
|
+
* wins, which is what makes a nested worktree resolve to itself rather than to
|
|
502
|
+
* the checkout it sits inside.
|
|
503
|
+
*/
|
|
504
|
+
function nearest_checkout_root(start: string): string | null {
|
|
505
|
+
let dir = start;
|
|
506
|
+
for (let i = 0; i < 64; i++) {
|
|
507
|
+
if (git_dir(dir) !== null) return dir;
|
|
508
|
+
const up = path.dirname(dir);
|
|
509
|
+
if (up === dir) return null;
|
|
510
|
+
dir = up;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
435
513
|
}
|
|
436
514
|
|
|
437
515
|
/**
|
|
@@ -619,13 +697,35 @@ export function foreign_sessions_block(
|
|
|
619
697
|
// `session_checkout`. Before that every record claimed the main checkout, so
|
|
620
698
|
// every collision looked like the dangerous one.
|
|
621
699
|
if (branch_hit !== null) {
|
|
622
|
-
|
|
623
|
-
|
|
700
|
+
// Compared through `canonical`, not `path.resolve`, because resolve does not
|
|
701
|
+
// follow symlinks — round 5 finding 11. Round 4 finding 5 made every
|
|
702
|
+
// `session_checkout` branch canonical, which fixes records written from now
|
|
703
|
+
// on; it does nothing for a record already in the register from before the
|
|
704
|
+
// upgrade, and those live for the whole TTL window. Under a symlinked
|
|
705
|
+
// ancestor such a peer holds `/var/…/repo` while this session holds
|
|
706
|
+
// `/private/var/…/repo`, and comparing the raw strings reads two sessions
|
|
707
|
+
// in ONE working tree as separate trees — printing the benign note where
|
|
708
|
+
// the collision prompt belongs. Canonicalising at the comparison closes the
|
|
709
|
+
// upgrade boundary the writer side cannot reach.
|
|
710
|
+
const peer_tree = canonical(branch_hit.record.worktree ?? '');
|
|
711
|
+
const same_tree =
|
|
712
|
+
(branch_hit.record.worktree ?? '') !== '' && peer_tree === canonical(workspace_root);
|
|
624
713
|
if (same_tree) {
|
|
625
714
|
parts.push(
|
|
626
715
|
'',
|
|
627
716
|
`COLLISION: another live session is on branch \`${here}\` in THIS SAME worktree`,
|
|
628
717
|
`(${branch_hit.record.worktree}) — the same files and the same git index.`,
|
|
718
|
+
'What "join anyway" actually costs, because option 1 is chosen routinely and',
|
|
719
|
+
'the price was not in this text until 2026-08-20 (road-to-session-closeout 6.3):',
|
|
720
|
+
' · SHARED INDEX — `git add -A` stages the peer\'s work too. Measured: a peer',
|
|
721
|
+
' session\'s untracked roadmap was swept into an unrelated merge commit and',
|
|
722
|
+
' surfaced three commits later at an estate gate. Use pathspecs, never -A.',
|
|
723
|
+
' · SHARED STASH STACK — `git stash` is repo-wide, so your stash is visible and',
|
|
724
|
+
' poppable by the peer, and `stash@{0}` means a different thing to each of you.',
|
|
725
|
+
' · PRE-PUSH runs over the shared tree — it sees the peer\'s files, not only',
|
|
726
|
+
' yours. `task consistency` is now scoped to the paths it regenerates, so a',
|
|
727
|
+
' peer\'s unrelated edit no longer fails it; anything else in that hook still',
|
|
728
|
+
' reads one tree written by two sessions.',
|
|
629
729
|
'Ask the user ONCE PER SESSION, as numbered options, before writing anything:',
|
|
630
730
|
' 1. Join anyway and coordinate manually (this register is advisory, not a lock)',
|
|
631
731
|
' 2. Spawn a separate worktree for this session',
|
|
@@ -31,7 +31,17 @@ const _HERE = fileURLToPath(import.meta.url);
|
|
|
31
31
|
|
|
32
32
|
// src/scripts/skill_usage_collect.ts → parents[2] of the .py file is repo root.
|
|
33
33
|
export const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
34
|
-
|
|
34
|
+
// `agents/metrics/` was retired by the agents/ taxonomy consolidation (the
|
|
35
|
+
// same finding `skill_overlap.ts` records), so this collector was appending to
|
|
36
|
+
// a directory that does not exist while the real record set sat untouched at
|
|
37
|
+
// `agents/runtime/metrics/skill-usage.jsonl` — last written 2026-05-16. The
|
|
38
|
+
// report script read the same dead path, so the pair agreed with each other
|
|
39
|
+
// and with nothing else. road-to-org-telemetry Phase 0 measured the
|
|
40
|
+
// consequence (spike 3: 0 of 89 invocations detected on the set the collector
|
|
41
|
+
// reads) and named the repair a Phase 4 prerequisite. `runtime/` is the typed,
|
|
42
|
+
// gitignored home for a record set nothing tracks, per the agents/ layout
|
|
43
|
+
// contract.
|
|
44
|
+
const OUT = path.join(REPO, 'agents', 'runtime', 'metrics', 'skill-usage.jsonl');
|
|
35
45
|
|
|
36
46
|
// LISTING_LINE_RE = re.compile(r"^-\s+([a-z0-9][a-z0-9_-]+):\s", re.MULTILINE)
|
|
37
47
|
const LISTING_LINE_RE = /^-\s+([a-z0-9][a-z0-9_-]+):\s/gm;
|
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|