@event4u/agent-config 14.6.0 → 14.8.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 +255 -145
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +272 -32
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
- 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 +20 -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 +26 -7
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- 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 +296 -53
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- 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-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -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/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- 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 +65 -4
- 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 +2 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
- 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_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +466 -716
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -10
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +10 -9
- package/dist/cli-delegate/cmd_handoff.js +10 -9
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +11 -10
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +11 -10
- package/dist/cli-delegate/cmd_uninstall.js +13 -11
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +11 -10
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +111 -37
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +15 -12
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +11 -8
- package/dist/hooks/dispatch.js +3165 -2192
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.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 +432 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1863 -1234
- 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/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +63 -251
- 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/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- 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 +26 -7
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- 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 +296 -53
- 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 +39 -0
- package/src/config/estate-count-budget.json +612 -3
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +34 -7
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +502 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +17 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -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 +49 -9
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +78 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +194 -17
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +79 -259
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/external_sources_denylist.json +4 -1
- 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 +21 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- 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 +870 -21
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- 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/injection_scan_hook.ts +137 -5
- 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_hook_manifest.ts +49 -0
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- 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 +6 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- 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 +170 -20
- 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-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
|
@@ -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`);
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* do not hand-merge) versus authored (a human decision). Auto-resolving a
|
|
22
22
|
* content conflict is how a parallel session's work disappears.
|
|
23
23
|
*
|
|
24
|
+
* Classification changes the ADVICE, not the conflict. A path stays a hotspot by
|
|
25
|
+
* frequency while becoming mechanical to resolve, so a class added here is never
|
|
26
|
+
* conflict-count reduction and must not be banked as drawdown (AI council
|
|
27
|
+
* 2026-08-21, both seats; roadmap road-to-merge-hotspot-drawdown).
|
|
28
|
+
*
|
|
24
29
|
* Exit codes: 0 = already current, or merged cleanly · 1 = conflict, or the base
|
|
25
30
|
* could not be resolved · 2 = internal error. `scanned:` on every path.
|
|
26
31
|
*/
|
|
@@ -35,11 +40,78 @@ const NETWORK_TIMEOUT_MS = 8_000;
|
|
|
35
40
|
|
|
36
41
|
/** Paths that are GENERATED — a conflict here is regenerated, never hand-merged. */
|
|
37
42
|
const GENERATED = [
|
|
43
|
+
// Untracked in THIS repository since 2026-08-21 (it was the #1 conflict
|
|
44
|
+
// path: 830 commits/60d, in the conflict set of every open CONFLICTING PR).
|
|
45
|
+
// The entry STAYS anyway, and the retirement criterion is a declared window
|
|
46
|
+
// rather than "one release" — the AI council flagged the release count as
|
|
47
|
+
// arbitrary and unverifiable. Remove this entry once no open branch predates
|
|
48
|
+
// the untrack commit, which is checkable:
|
|
49
|
+
//
|
|
50
|
+
// git branch -r --contains <untrack-commit> # must list every open branch
|
|
51
|
+
//
|
|
52
|
+
// Until then a straggler branch created before the untrack still carries the
|
|
53
|
+
// tracked file, and a conflict on it is still resolved by regeneration.
|
|
38
54
|
'agents/roadmaps-progress.md',
|
|
39
55
|
'agents/index.md',
|
|
40
56
|
'docs/catalog.md',
|
|
41
57
|
'dist/agent-src/',
|
|
58
|
+
// Written by `src/scripts/adr/regenerate_index.ts`, which takes the decisions
|
|
59
|
+
// directory as an ARGUMENT — so the path literal appears nowhere in the
|
|
60
|
+
// generator and every grep-based audit of this list missed it. Found instead
|
|
61
|
+
// by measurement: 4 of the last 50 sessions resolved a conflict here, and
|
|
62
|
+
// its only correct resolution is `task regenerate-adr-index`, never a hunk
|
|
63
|
+
// merge of two append-shaped indexes.
|
|
64
|
+
'docs/decisions/INDEX.md',
|
|
65
|
+
// Compiled by `compile_router.ts`. The `dist/agent-src/` prefix above does
|
|
66
|
+
// NOT match a sibling one level up under `dist/`, so this file was routed to
|
|
67
|
+
// a human decision despite being pure build output.
|
|
68
|
+
'dist/router.json',
|
|
42
69
|
'.augment/',
|
|
70
|
+
// Compiled from `hook_manifest.yaml` by `task build-ts`, never hand-written.
|
|
71
|
+
// It was classified AUTHORED until 2026-08-20, so this tool told the reader
|
|
72
|
+
// to "read both sides" on a file where mixing hunks yields a concern table
|
|
73
|
+
// matching NEITHER branch. One session resolved this same conflict three
|
|
74
|
+
// times and named it structural: main adds a concern and recompiles, so
|
|
75
|
+
// every open branch collides here. Regenerate; never merge.
|
|
76
|
+
'src/scripts/hook_manifest.json',
|
|
77
|
+
// Written together by one `build_archive_index.ts` call (`:409-410`) from the
|
|
78
|
+
// archived roadmap tree, so neither is ever hand-authored. They were
|
|
79
|
+
// classified AUTHORED until 2026-08-21, which made this tool ask for a human
|
|
80
|
+
// decision about a file whose only correct resolution is regeneration --
|
|
81
|
+
// the identical defect the hook_manifest.json comment above records, on two
|
|
82
|
+
// more paths at 47 commits/60d each. They conflict in 2 of the 7 open PRs
|
|
83
|
+
// measured for road-to-merge-hotspot-drawdown. Regenerate; never merge.
|
|
84
|
+
'agents/roadmaps/archive/INDEX.md',
|
|
85
|
+
'agents/roadmaps/archive/index.json',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Paths that are MEASURED BASELINES -- a conflict here is neither regenerated
|
|
90
|
+
* nor hand-merged. It is RE-MEASURED on the merged tree, and the number that
|
|
91
|
+
* measurement produces is the resolution.
|
|
92
|
+
*
|
|
93
|
+
* Why this is a third class and not a variant of GENERATED: a generated file has
|
|
94
|
+
* one correct content given the tree, so "regenerate" is a complete instruction.
|
|
95
|
+
* A ratchet baseline records what a tree MEASURED at a point in time, and two
|
|
96
|
+
* branches legitimately measured two different trees -- so there is no side to
|
|
97
|
+
* take and no file to re-render. Picking a side on a ratchet number is how the
|
|
98
|
+
* ratchet silently loosens, and mixing hunks yields a baseline matching neither
|
|
99
|
+
* branch.
|
|
100
|
+
*
|
|
101
|
+
* Why this tool never performs the re-measurement itself (AI council 2026-08-21,
|
|
102
|
+
* both seats): "re-measure instead of merge" delegates conflict resolution to a
|
|
103
|
+
* script and its execution environment, and a wrong or environment-dependent
|
|
104
|
+
* measurement would overwrite a deliberate tightening with nothing objecting,
|
|
105
|
+
* because the result looks measured either way. So this class NAMES the
|
|
106
|
+
* resolution and stops. The human runs it, with the result in front of them.
|
|
107
|
+
*
|
|
108
|
+
* Both members conflict in 7 of the 7 open PRs measured for
|
|
109
|
+
* road-to-merge-hotspot-drawdown. Neither is gitignorable: an untracked baseline
|
|
110
|
+
* is a baseline no PR diff can be compared against, which deletes the ratchet.
|
|
111
|
+
*/
|
|
112
|
+
const REMEASURED = [
|
|
113
|
+
'src/config/estate-count-budget.json',
|
|
114
|
+
'src/config/gate-violation-baselines.json',
|
|
43
115
|
];
|
|
44
116
|
|
|
45
117
|
function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string; err: string } {
|
|
@@ -53,17 +125,59 @@ function sh(cmd: string, args: readonly string[], cwd: string): { ok: boolean; o
|
|
|
53
125
|
}
|
|
54
126
|
|
|
55
127
|
/** True when `rel` is a generated artefact rather than an authored one. */
|
|
128
|
+
/**
|
|
129
|
+
* The subset of GENERATED that this repository no longer commits at all
|
|
130
|
+
* (`road-to-generated-artifacts-out-of-index`, 2026-08-22). They are still
|
|
131
|
+
* generated, so they stay in `GENERATED` and keep being reported — a straggler
|
|
132
|
+
* branch created before the untrack still carries them, which is exactly when
|
|
133
|
+
* the reader needs to be told something.
|
|
134
|
+
*
|
|
135
|
+
* What changes is the INSTRUCTION. A branch that predates the cutover hits a
|
|
136
|
+
* `modify/delete` conflict here, not a content conflict, and `git checkout
|
|
137
|
+
* --ours` has no side to check out: the correct resolution is to take the
|
|
138
|
+
* deletion. Printing the generic advice would send the reader to re-add the
|
|
139
|
+
* file, which is precisely how PR #1505 put the dashboard back on `main` a day
|
|
140
|
+
* after it was first untracked.
|
|
141
|
+
*/
|
|
142
|
+
const UNTRACKED_BY_DESIGN: readonly string[] = [
|
|
143
|
+
'agents/roadmaps-progress.md',
|
|
144
|
+
'agents/roadmaps/archive/INDEX.md',
|
|
145
|
+
'agents/roadmaps/archive/index.json',
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/** Is this a generated path this repository deliberately does not commit? */
|
|
149
|
+
export function isUntrackedByDesign(rel: string): boolean {
|
|
150
|
+
return UNTRACKED_BY_DESIGN.includes(rel);
|
|
151
|
+
}
|
|
152
|
+
|
|
56
153
|
export function isGenerated(rel: string): boolean {
|
|
57
154
|
const norm = rel.replace(/\\/g, '/');
|
|
58
155
|
return GENERATED.some((g) => (g.endsWith('/') ? norm.startsWith(g) : norm === g));
|
|
59
156
|
}
|
|
60
157
|
|
|
158
|
+
/**
|
|
159
|
+
* True when `rel` is listed as a measured ratchet baseline.
|
|
160
|
+
*
|
|
161
|
+
* This does NOT assert exclusivity against `GENERATED` — it only reads its own
|
|
162
|
+
* array. `classifyConflicts` establishes the precedence (generated first), so a
|
|
163
|
+
* path added to both arrays would route as generated while this predicate also
|
|
164
|
+
* returned true, and no test would catch it. Keep the arrays disjoint; the
|
|
165
|
+
* per-path exclusivity assertions in the test file cover today's two members
|
|
166
|
+
* only.
|
|
167
|
+
*/
|
|
168
|
+
export function isRemeasured(rel: string): boolean {
|
|
169
|
+
const norm = rel.replace(/\\/g, '/');
|
|
170
|
+
return REMEASURED.some((g) => (g.endsWith('/') ? norm.startsWith(g) : norm === g));
|
|
171
|
+
}
|
|
172
|
+
|
|
61
173
|
export interface Plan {
|
|
62
174
|
/** 0 = nothing to do or merged · 1 = needs a human · 2 = internal. */
|
|
63
175
|
exit: 0 | 1 | 2;
|
|
64
176
|
message: string;
|
|
65
177
|
/** Conflicted paths, split so the caller knows which to regenerate. */
|
|
66
178
|
generated: string[];
|
|
179
|
+
/** Conflicted measured baselines — re-measure on the merged tree, never merge. */
|
|
180
|
+
remeasured: string[];
|
|
67
181
|
authored: string[];
|
|
68
182
|
scanned: number;
|
|
69
183
|
}
|
|
@@ -75,14 +189,57 @@ export interface Plan {
|
|
|
75
189
|
* the split is the useful part: a generated conflict has one correct resolution
|
|
76
190
|
* (regenerate) and an authored one has none that a script may choose.
|
|
77
191
|
*/
|
|
78
|
-
export function classifyConflicts(files: readonly string[]): Pick<Plan, 'generated' | 'authored'> {
|
|
192
|
+
export function classifyConflicts(files: readonly string[]): Pick<Plan, 'generated' | 'remeasured' | 'authored'> {
|
|
79
193
|
const generated: string[] = [];
|
|
194
|
+
const remeasured: string[] = [];
|
|
80
195
|
const authored: string[] = [];
|
|
81
196
|
for (const f of files) {
|
|
82
|
-
|
|
83
|
-
(
|
|
197
|
+
const rel = f.trim();
|
|
198
|
+
if (rel === '') continue;
|
|
199
|
+
if (isGenerated(rel)) generated.push(rel);
|
|
200
|
+
else if (isRemeasured(rel)) remeasured.push(rel);
|
|
201
|
+
else authored.push(rel);
|
|
84
202
|
}
|
|
85
|
-
return { generated, authored };
|
|
203
|
+
return { generated, remeasured, authored };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Render the conflict report for a plan that needs a human.
|
|
208
|
+
*
|
|
209
|
+
* Pure and exported so the WORDING is testable without producing a real merge
|
|
210
|
+
* conflict. That matters more than it looks: the per-class instruction is the
|
|
211
|
+
* entire value of the classification, and while this lived inline in `main()`
|
|
212
|
+
* the whole block could be deleted and every test stayed green — the header
|
|
213
|
+
* still counted the conflict, and the paths went unnamed with no instruction.
|
|
214
|
+
*/
|
|
215
|
+
export function renderConflictReport(plan: Plan): string {
|
|
216
|
+
const out: string[] = [`❌ ${plan.message}\n`];
|
|
217
|
+
const untracked = plan.generated.filter(isUntrackedByDesign);
|
|
218
|
+
const regenerable = plan.generated.filter((f) => !isUntrackedByDesign(f));
|
|
219
|
+
if (regenerable.length > 0) {
|
|
220
|
+
out.push(`\n GENERATED (${String(regenerable.length)}) — resolve by REGENERATING, never by mixing hunks:\n`);
|
|
221
|
+
for (const f of regenerable) out.push(` · ${f}\n`);
|
|
222
|
+
out.push(' → git checkout --ours <file> && task sync && task generate-tools\n');
|
|
223
|
+
}
|
|
224
|
+
if (untracked.length > 0) {
|
|
225
|
+
out.push(
|
|
226
|
+
`\n UNTRACKED BY DESIGN (${String(untracked.length)}) — this repository does not commit these; TAKE THE DELETION:\n`,
|
|
227
|
+
);
|
|
228
|
+
for (const f of untracked) out.push(` · ${f}\n`);
|
|
229
|
+
out.push(' → git rm --cached -- <file> (the working-tree copy stays; regenerate it locally)\n');
|
|
230
|
+
}
|
|
231
|
+
if (plan.remeasured.length > 0) {
|
|
232
|
+
out.push(
|
|
233
|
+
`\n REMEASURED (${String(plan.remeasured.length)}) — RE-RUN THE MEASUREMENT on the merged tree; never merge, never pick a side:\n`,
|
|
234
|
+
);
|
|
235
|
+
for (const f of plan.remeasured) out.push(` · ${f}\n`);
|
|
236
|
+
out.push(' → resolve the tree, then re-run the gate that owns the baseline and record its number\n');
|
|
237
|
+
}
|
|
238
|
+
if (plan.authored.length > 0) {
|
|
239
|
+
out.push(`\n AUTHORED (${String(plan.authored.length)}) — a human decision, read both sides:\n`);
|
|
240
|
+
for (const f of plan.authored) out.push(` · ${f}\n`);
|
|
241
|
+
}
|
|
242
|
+
return out.join('');
|
|
86
243
|
}
|
|
87
244
|
|
|
88
245
|
/** Resolve the base this branch will actually merge into. */
|
|
@@ -118,7 +275,7 @@ export function resolveBase(repo: string, override: string | null): { base: stri
|
|
|
118
275
|
export function sync(repo: string, baseOverride: string | null, dryRun: boolean): Plan {
|
|
119
276
|
const { base, how } = resolveBase(repo, baseOverride);
|
|
120
277
|
if (base === null) {
|
|
121
|
-
return { exit: 1, message: `cannot resolve a base to update against — ${how}.`, generated: [], authored: [], scanned: 0 };
|
|
278
|
+
return { exit: 1, message: `cannot resolve a base to update against — ${how}.`, generated: [], remeasured: [], authored: [], scanned: 0 };
|
|
122
279
|
}
|
|
123
280
|
|
|
124
281
|
const fetched = sh('git', ['fetch', 'origin', '--prune'], repo);
|
|
@@ -129,6 +286,7 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean)
|
|
|
129
286
|
exit: 0,
|
|
130
287
|
message: `unverified — could not fetch origin (${fetched.err.split('\n')[0] ?? '?'}). Base freshness NOT checked.`,
|
|
131
288
|
generated: [],
|
|
289
|
+
remeasured: [],
|
|
132
290
|
authored: [],
|
|
133
291
|
scanned: 0,
|
|
134
292
|
};
|
|
@@ -136,13 +294,14 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean)
|
|
|
136
294
|
|
|
137
295
|
const behind = sh('git', ['rev-list', '--count', `HEAD..${base}`], repo).out.trim();
|
|
138
296
|
if (behind === '' || behind === '0') {
|
|
139
|
-
return { exit: 0, message: `already current with ${base} (${how}).`, generated: [], authored: [], scanned: 1 };
|
|
297
|
+
return { exit: 0, message: `already current with ${base} (${how}).`, generated: [], remeasured: [], authored: [], scanned: 1 };
|
|
140
298
|
}
|
|
141
299
|
if (dryRun) {
|
|
142
300
|
return {
|
|
143
301
|
exit: 0,
|
|
144
302
|
message: `${behind} commit(s) behind ${base} (${how}) — would merge it in. Dry run, nothing changed.`,
|
|
145
303
|
generated: [],
|
|
304
|
+
remeasured: [],
|
|
146
305
|
authored: [],
|
|
147
306
|
scanned: 1,
|
|
148
307
|
};
|
|
@@ -156,6 +315,7 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean)
|
|
|
156
315
|
`merged ${base} in (${how}, was ${behind} behind). REGENERATE derived files now — a clean ` +
|
|
157
316
|
'auto-merge of a generated file is still wrong.',
|
|
158
317
|
generated: [],
|
|
318
|
+
remeasured: [],
|
|
159
319
|
authored: [],
|
|
160
320
|
scanned: 1,
|
|
161
321
|
};
|
|
@@ -166,7 +326,7 @@ export function sync(repo: string, baseOverride: string | null, dryRun: boolean)
|
|
|
166
326
|
return {
|
|
167
327
|
exit: 1,
|
|
168
328
|
message:
|
|
169
|
-
`merge of ${base} (${how}) hit ${String(split.generated.length + split.authored.length)} conflict(s). ` +
|
|
329
|
+
`merge of ${base} (${how}) hit ${String(split.generated.length + split.remeasured.length + split.authored.length)} conflict(s). ` +
|
|
170
330
|
'NOT auto-resolved: a content conflict is where a parallel session\'s work disappears.',
|
|
171
331
|
...split,
|
|
172
332
|
scanned: 1,
|
|
@@ -211,7 +371,8 @@ export function main(argv?: readonly string[]): number {
|
|
|
211
371
|
' Merges the PR base into the current branch so the PR does not go stale.\n' +
|
|
212
372
|
' Resolves the base from the open PR when there is one. A conflict is\n' +
|
|
213
373
|
' reported and never auto-resolved; generated and authored conflicts are\n' +
|
|
214
|
-
' listed separately because only the first has one correct resolution
|
|
374
|
+
' listed separately because only the first has one correct resolution;\n' +
|
|
375
|
+
' measured ratchet baselines are a third class, re-measured not merged.\n',
|
|
215
376
|
);
|
|
216
377
|
reportScanned({ gate: 'sync_pr_branch', scanned: 0, units: 'base ref(s)', roots: ['origin'], allowEmpty: 'help output' });
|
|
217
378
|
return 0;
|
|
@@ -232,18 +393,7 @@ export function main(argv?: readonly string[]): number {
|
|
|
232
393
|
}
|
|
233
394
|
|
|
234
395
|
if (plan.exit === 1) {
|
|
235
|
-
process.stdout.write(
|
|
236
|
-
if (plan.generated.length > 0) {
|
|
237
|
-
process.stdout.write(
|
|
238
|
-
`\n GENERATED (${String(plan.generated.length)}) — resolve by REGENERATING, never by mixing hunks:\n`,
|
|
239
|
-
);
|
|
240
|
-
for (const f of plan.generated) process.stdout.write(` · ${f}\n`);
|
|
241
|
-
process.stdout.write(' → git checkout --ours <file> && task sync && task generate-tools\n');
|
|
242
|
-
}
|
|
243
|
-
if (plan.authored.length > 0) {
|
|
244
|
-
process.stdout.write(`\n AUTHORED (${String(plan.authored.length)}) — a human decision, read both sides:\n`);
|
|
245
|
-
for (const f of plan.authored) process.stdout.write(` · ${f}\n`);
|
|
246
|
-
}
|
|
396
|
+
process.stdout.write(renderConflictReport(plan));
|
|
247
397
|
} else if (plan.message.startsWith('unverified')) {
|
|
248
398
|
// Loud even under --quiet: unverified reported silently is
|
|
249
399
|
// indistinguishable from verified, which is the defect being closed.
|
|
@@ -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(
|