@zalom/plastic 2.0.0-alpha.9 → 2.0.2
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/PLASTIC.md +13 -136
- package/README.md +357 -133
- package/agents/plastic-enforcer.md +21 -16
- package/agents/plastic-executor.md +15 -4
- package/agents/plastic-node-research.md +30 -0
- package/agents/plastic-node-verify.md +28 -0
- package/agents/plastic-node-work.md +33 -0
- package/agents/{plastic-advisor.md → plastic-primary-advisor.md} +7 -9
- package/agents/{plastic-faux-advisor.md → plastic-secondary-advisor.md} +9 -12
- package/assets/plastic-logo.svg +1 -0
- package/bin/crap +4 -0
- package/bin/lib/context_budget.rb +43 -6
- package/bin/lib/skill_census.rb +839 -0
- package/bin/plastic +8 -0
- package/bin/plastic-skill-census +114 -0
- package/bin/test +24 -4
- package/bin/verify-change +345 -0
- package/config_asks.yml +4 -4
- package/deprecations.yml +1 -1
- package/{skills/agent-advisor/references → docs/help}/advisor-protocol.md +19 -24
- package/{skills/auto/references → docs/help}/agent-architecture.md +16 -14
- package/docs/help/completion-and-done.md +53 -0
- package/docs/help/human-report-contract.md +152 -0
- package/{skills/conventions/references → docs/help}/knowledge-graph.md +9 -0
- package/{skills/conventions/references → docs/help}/locks-and-worktrees.md +11 -13
- package/{skills/conventions/references → docs/help}/maintenance-and-revisions.md +1 -1
- package/{skills/conventions/references → docs/help}/roadmaps.md +5 -4
- package/{skills/tutorial/references → docs/help}/track-1-guided.md +28 -47
- package/{skills/tutorial/references → docs/help}/track-2-auto.md +8 -8
- package/{skills/tutorial/references → docs/help}/track-3-projects-and-roadmaps.md +24 -17
- package/hooks/call-budget +4 -0
- package/hooks/hooks.json +24 -0
- package/hooks/message-display +55 -2
- package/hooks/session-start +5 -1
- package/hooks/statusline +32 -27
- package/hooks/stop +5 -0
- package/package.json +5 -3
- package/scripts/append-ledger +2 -1
- package/scripts/dashboard.rb +267 -16
- package/scripts/day-summary +2 -1
- package/scripts/doctor.rb +680 -39
- package/scripts/end-intent +378 -56
- package/scripts/exec-worktree +5 -5
- package/scripts/file-session-intent +2 -1
- package/scripts/graph-measure +249 -0
- package/scripts/hook-call-budget +222 -0
- package/scripts/hook-capture +24 -123
- package/scripts/hook-close +2 -1
- package/scripts/hook-message-display +7 -0
- package/scripts/hook-record +24 -16
- package/scripts/hook-savepoint +27 -3
- package/scripts/hook-session-start +359 -321
- package/scripts/hook-stop +58 -0
- package/scripts/index-projection +74 -0
- package/scripts/insight-append +17 -4
- package/scripts/install.rb +9 -7
- package/scripts/lib/action_graph_shim.rb +279 -0
- package/scripts/lib/active_delivery.rb +105 -0
- package/scripts/lib/agent_models.rb +63 -25
- package/scripts/lib/arm.rb +74 -91
- package/scripts/lib/atomic_write.rb +31 -0
- package/scripts/lib/backup.rb +65 -0
- package/scripts/lib/cli/command.rb +92 -0
- package/scripts/lib/cli/commands/auto.rb +18 -0
- package/scripts/lib/cli/commands/auto_brief.rb +44 -0
- package/scripts/lib/cli/commands/auto_lock.rb +111 -0
- package/scripts/lib/cli/commands/auto_report.rb +50 -0
- package/scripts/lib/cli/commands/auto_take.rb +68 -0
- package/scripts/lib/cli/commands/backup.rb +43 -0
- package/scripts/lib/cli/commands/checkout.rb +25 -0
- package/scripts/lib/cli/commands/continue.rb +66 -0
- package/scripts/lib/cli/commands/doctor.rb +81 -0
- package/scripts/lib/cli/commands/feedback.rb +40 -0
- package/scripts/lib/cli/commands/help.rb +69 -0
- package/scripts/lib/cli/commands/hook.rb +32 -0
- package/scripts/lib/cli/commands/index.rb +23 -0
- package/scripts/lib/cli/commands/install.rb +21 -0
- package/scripts/lib/cli/commands/installer_verb.rb +37 -0
- package/scripts/lib/cli/commands/intent.rb +19 -0
- package/scripts/lib/cli/commands/intent_answer.rb +37 -0
- package/scripts/lib/cli/commands/intent_command.rb +58 -0
- package/scripts/lib/cli/commands/intent_end.rb +75 -0
- package/scripts/lib/cli/commands/intent_new.rb +62 -0
- package/scripts/lib/cli/commands/intent_note.rb +43 -0
- package/scripts/lib/cli/commands/intent_rule.rb +36 -0
- package/scripts/lib/cli/commands/intent_show.rb +32 -0
- package/scripts/lib/cli/commands/intent_spec.rb +44 -0
- package/scripts/lib/cli/commands/intent_step.rb +79 -0
- package/scripts/lib/cli/commands/intent_verify.rb +26 -0
- package/scripts/lib/cli/commands/migrate.rb +16 -0
- package/scripts/lib/cli/commands/migrate_stores.rb +31 -0
- package/scripts/lib/cli/commands/next.rb +54 -0
- package/scripts/lib/cli/commands/project.rb +19 -0
- package/scripts/lib/cli/commands/project_links.rb +46 -0
- package/scripts/lib/cli/commands/project_list.rb +20 -0
- package/scripts/lib/cli/commands/project_new.rb +95 -0
- package/scripts/lib/cli/commands/query.rb +31 -0
- package/scripts/lib/cli/commands/render.rb +35 -0
- package/scripts/lib/cli/commands/roadmap.rb +19 -0
- package/scripts/lib/cli/commands/roadmap_check.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_log.rb +54 -0
- package/scripts/lib/cli/commands/roadmap_migrate.rb +53 -0
- package/scripts/lib/cli/commands/roadmap_next.rb +77 -0
- package/scripts/lib/cli/commands/roadmap_show.rb +65 -0
- package/scripts/lib/cli/commands/rollback.rb +20 -0
- package/scripts/lib/cli/commands/search.rb +60 -0
- package/scripts/lib/cli/commands/session.rb +18 -0
- package/scripts/lib/cli/commands/session_commit.rb +42 -0
- package/scripts/lib/cli/commands/session_handoff.rb +34 -0
- package/scripts/lib/cli/commands/session_summary.rb +35 -0
- package/scripts/lib/cli/commands/status.rb +68 -0
- package/scripts/lib/cli/commands/subcommand_list.rb +36 -0
- package/scripts/lib/cli/commands/sync.rb +55 -0
- package/scripts/lib/cli/commands/uninstall.rb +20 -0
- package/scripts/lib/cli/commands/update.rb +20 -0
- package/scripts/lib/cli/commands/version.rb +53 -0
- package/scripts/lib/cli/frontier.rb +86 -0
- package/scripts/lib/cli/intent_progress.rb +36 -0
- package/scripts/lib/cli/legacy.rb +80 -0
- package/scripts/lib/cli/output.rb +137 -0
- package/scripts/lib/cli/scope.rb +134 -0
- package/scripts/lib/cli/table.rb +65 -0
- package/scripts/lib/cli.rb +94 -0
- package/scripts/lib/codex_adapter.rb +198 -0
- package/scripts/lib/compact_instructions.rb +13 -5
- package/scripts/lib/core_integrity.rb +71 -0
- package/scripts/lib/dashboard_screen.rb +40 -0
- package/scripts/lib/data_boundary.rb +132 -0
- package/scripts/lib/day_summary.rb +23 -14
- package/scripts/lib/doctor_core.rb +114 -38
- package/scripts/lib/doctor_session_ledger.rb +5 -53
- package/scripts/lib/engine_permissions.rb +88 -0
- package/scripts/lib/exec_worktree.rb +24 -21
- package/scripts/lib/feedback_report.rb +1 -1
- package/scripts/lib/graph_edges.rb +137 -0
- package/scripts/lib/graph_file.rb +246 -0
- package/scripts/lib/graph_measure.rb +645 -0
- package/scripts/lib/graph_measure_budget.rb +409 -0
- package/scripts/lib/graph_measure_cohorts.rb +487 -0
- package/scripts/lib/graph_measure_models.rb +413 -0
- package/scripts/lib/graph_measure_report.rb +532 -0
- package/scripts/lib/graph_tree.rb +98 -0
- package/scripts/lib/guarded_append.rb +155 -0
- package/scripts/lib/handoff.rb +40 -13
- package/scripts/lib/harness_adapter.rb +184 -0
- package/scripts/lib/hook_registry.rb +27 -3
- package/scripts/lib/hook_replay.rb +235 -0
- package/scripts/lib/index_entry.rb +62 -0
- package/scripts/lib/index_projection.rb +201 -0
- package/scripts/lib/insights.rb +1 -1
- package/scripts/lib/installer_core.rb +543 -71
- package/scripts/lib/intent_screen.rb +4 -4
- package/scripts/lib/intent_screen_ansi.rb +73 -12
- package/scripts/lib/intent_validator.rb +2 -2
- package/scripts/lib/lock.rb +10 -11
- package/scripts/lib/message_display.rb +350 -54
- package/scripts/lib/meter_watch.rb +185 -0
- package/scripts/lib/node_file.rb +234 -0
- package/scripts/lib/node_ids.rb +99 -0
- package/scripts/lib/node_input.rb +921 -0
- package/scripts/lib/node_input_compatibility.rb +62 -0
- package/scripts/lib/node_ledger.rb +386 -0
- package/scripts/lib/node_progress.rb +153 -0
- package/scripts/lib/node_return.rb +204 -0
- package/scripts/lib/node_worktree.rb +337 -0
- package/scripts/lib/outcome_report.rb +440 -0
- package/scripts/lib/preflight.rb +4 -6
- package/scripts/lib/project_config.rb +46 -0
- package/scripts/lib/project_validator.rb +3 -2
- package/scripts/lib/qmd_sync.rb +8 -7
- package/scripts/lib/ready_set.rb +462 -0
- package/scripts/lib/reference_archive.rb +45 -0
- package/scripts/lib/release_guard.rb +18 -0
- package/scripts/lib/report_screen.rb +1371 -47
- package/scripts/lib/rlm/corpus.rb +13 -0
- package/scripts/lib/rlm/probe.rb +29 -0
- package/scripts/lib/rlm/query.rb +22 -0
- package/scripts/lib/roadmap_graph.rb +210 -0
- package/scripts/lib/roadmap_migration.rb +95 -0
- package/scripts/lib/roadmap_queue.rb +170 -12
- package/scripts/lib/roadmap_render.rb +150 -0
- package/scripts/lib/roadmap_savepoint.rb +64 -14
- package/scripts/lib/runner_absorb.rb +703 -0
- package/scripts/lib/runner_answer.rb +206 -0
- package/scripts/lib/runner_core.rb +194 -0
- package/scripts/lib/runner_dispatch.rb +526 -0
- package/scripts/lib/runner_policy.rb +191 -0
- package/scripts/lib/runner_proposals.rb +275 -0
- package/scripts/lib/runner_rewind.rb +201 -0
- package/scripts/lib/runner_sweep.rb +231 -0
- package/scripts/lib/runner_until_empty.rb +252 -0
- package/scripts/lib/runner_watch.rb +389 -0
- package/scripts/lib/savepoint.rb +141 -19
- package/scripts/lib/scaffold_intent.rb +6 -3
- package/scripts/lib/screen_paint.rb +365 -28
- package/scripts/lib/screens/dashboard.rb +20 -0
- package/scripts/lib/screens/plan.rb +18 -0
- package/scripts/lib/screens/roadmap.rb +15 -0
- package/scripts/lib/search_index.rb +55 -0
- package/scripts/lib/session_close.rb +30 -28
- package/scripts/lib/session_git.rb +67 -19
- package/scripts/lib/session_ledger.rb +48 -4
- package/scripts/lib/session_usage.rb +190 -0
- package/scripts/lib/sqlite.rb +22 -0
- package/scripts/lib/stop_gate.rb +95 -0
- package/scripts/lib/store_discovery.rb +7 -6
- package/scripts/lib/store_layout.rb +54 -0
- package/scripts/lib/store_provisioning.rb +2 -1
- package/scripts/lib/store_sync.rb +85 -0
- package/scripts/lib/stores_move.rb +93 -0
- package/scripts/lib/untouched_scaffold.rb +51 -0
- package/scripts/lib/verify_intent.rb +36 -8
- package/scripts/lib/version_number.rb +48 -0
- package/scripts/lib/work_graph.rb +59 -0
- package/scripts/lib/work_graph_validator.rb +201 -0
- package/scripts/lib/worktree.rb +27 -32
- package/scripts/lib/worktree_sweep.rb +6 -5
- package/scripts/link-suggest +2 -1
- package/scripts/meter-watch +57 -0
- package/scripts/migrate-to-global +1 -1
- package/scripts/new-intent +4 -13
- package/scripts/node-input +92 -0
- package/scripts/node-run +225 -0
- package/scripts/node-transition +291 -0
- package/scripts/outcome-report +74 -0
- package/scripts/plastic-lock +65 -60
- package/scripts/project-links +6 -21
- package/scripts/promote-session-item +3 -2
- package/scripts/read-config +53 -9
- package/scripts/ready-set +126 -0
- package/scripts/release-check +123 -0
- package/scripts/report-screen +177 -16
- package/scripts/roadmap-graph +125 -0
- package/scripts/roadmap-savepoint +7 -0
- package/scripts/rollback.rb +5 -1
- package/scripts/runner +581 -0
- package/scripts/savepoint-note +11 -9
- package/scripts/session-commit +2 -1
- package/scripts/session-usage +56 -0
- package/scripts/skill-lint +115 -6
- package/scripts/spawn-preamble +2 -2
- package/scripts/update.rb +31 -4
- package/scripts/validate-work-graph +39 -0
- package/scripts/verify-intent +3 -2
- package/scripts/write-handoff +2 -1
- package/templates/agents.md +7 -7
- package/templates/config.yml +16 -9
- package/templates/dashboard-screen.md +22 -0
- package/templates/display-fixture.md +21 -0
- package/templates/graph.md +16 -0
- package/templates/index.md +1 -1
- package/templates/intent-screen.md +1 -1
- package/templates/node-decision.md +11 -0
- package/templates/node-research.md +13 -0
- package/templates/node-verify.md +13 -0
- package/templates/node-work.md +22 -0
- package/templates/outcome.md +8 -3
- package/templates/project.yml +1 -1
- package/templates/render.css +10 -0
- package/templates/report-plan.md +15 -0
- package/templates/report-roadmap-delivered.md +10 -0
- package/templates/report-roadmap-plan.md +9 -0
- package/templates/report-roadmap-state.md +9 -0
- package/templates/report-state.md +1 -1
- package/templates/roadmap.md +13 -0
- package/bin/plastic.js +0 -70
- package/scripts/lib/bridge.rb +0 -116
- package/skills/agent-advisor/SKILL.md +0 -92
- package/skills/auto/SKILL.md +0 -296
- package/skills/auto/evals/evals.json +0 -255
- package/skills/auto/references/end-tail.md +0 -66
- package/skills/auto/references/human-report-contract.md +0 -78
- package/skills/conventions/SKILL.md +0 -29
- package/skills/conventions/references/completion-and-done.md +0 -43
- package/skills/dashboard/SKILL.md +0 -169
- package/skills/dashboard/evals/evals.json +0 -38
- package/skills/dashboard/references/classification.md +0 -22
- package/skills/dashboard/templates/dashboard-global.md +0 -20
- package/skills/dashboard/templates/dashboard-project.md +0 -19
- package/skills/direct/SKILL.md +0 -66
- package/skills/direct/references/request-signals.md +0 -59
- package/skills/doctor/SKILL.md +0 -299
- package/skills/doctor/report.md +0 -102
- package/skills/feedback/SKILL.md +0 -98
- package/skills/feedback/references/transport-and-privacy.md +0 -65
- package/skills/feedback/report.md +0 -36
- package/skills/install/SKILL.md +0 -217
- package/skills/intent-continuing/SKILL.md +0 -154
- package/skills/intent-continuing/references/board-fill.md +0 -43
- package/skills/intent-continuing/references/boarding-matrix.md +0 -34
- package/skills/intent-continuing/references/context-management.md +0 -28
- package/skills/intent-continuing/references/liveness-ranking.md +0 -57
- package/skills/intent-creating/SKILL.md +0 -164
- package/skills/intent-creating/evals/evals.json +0 -72
- package/skills/intent-creating/references/lifecycle.md +0 -81
- package/skills/intent-creating/references/wikilinks.md +0 -8
- package/skills/intent-ending/SKILL.md +0 -176
- package/skills/intent-ending/evals/evals.json +0 -74
- package/skills/intent-executing/SKILL.md +0 -170
- package/skills/intent-executing/evals/evals.json +0 -66
- package/skills/intent-executing/implementer-prompt.md +0 -42
- package/skills/intent-executing/spec-reviewer-prompt.md +0 -27
- package/skills/intent-speccing/SKILL.md +0 -130
- package/skills/intent-speccing/evals/evals.json +0 -126
- package/skills/intent-speccing/references/design-principles.md +0 -44
- package/skills/intent-speccing/references/per-section-fill-rules.md +0 -92
- package/skills/intent-speccing/references/self-verify-checklist.md +0 -37
- package/skills/project-creating/SKILL.md +0 -162
- package/skills/project-creating/references/hubs-projects.md +0 -55
- package/skills/project-creating/references/project-scaffolding.md +0 -97
- package/skills/releasing/SKILL.md +0 -337
- package/skills/releasing/references/deprecations.md +0 -60
- package/skills/releasing/references/promotion-and-tagging.md +0 -66
- package/skills/releasing/references/release-lines.md +0 -105
- package/skills/roadmap/SKILL.md +0 -64
- package/skills/roadmap/references/file-format.md +0 -124
- package/skills/roadmap/references/operations.md +0 -112
- package/skills/rollback/SKILL.md +0 -91
- package/skills/tutorial/SKILL.md +0 -65
- package/skills/tutorial/evals/evals.json +0 -186
- package/skills/uninstall/SKILL.md +0 -75
- package/skills/update/SKILL.md +0 -126
- /package/{skills/auto/references → docs/help}/agent-report-contract.md +0 -0
- /package/{skills/intent-executing → docs/help}/code-quality-reviewer-prompt.md +0 -0
- /package/{skills/conventions/references → docs/help}/lifecycle-and-savepoints.md +0 -0
- /package/{skills/intent-executing → docs/help}/plan-reviewer-prompt.md +0 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "date"
|
|
7
|
+
require "time"
|
|
8
|
+
require "set"
|
|
9
|
+
|
|
10
|
+
# SkillCensus (intent 324): counts typed /plastic-* commands against
|
|
11
|
+
# agent-invoked Skill calls, so D19 (which Plastic verbs stay visible as
|
|
12
|
+
# slash commands) is ruled on measured use rather than taste.
|
|
13
|
+
#
|
|
14
|
+
# Two sources, never mixed. `HistoryScanner` reads ~/.claude/history.jsonl
|
|
15
|
+
# for typed commands: the transcript tree is pruned to about 45 days and
|
|
16
|
+
# would undercount typed use by two orders of magnitude. `TranscriptScanner`
|
|
17
|
+
# reads the ~/.claude/projects tree for agent invocations, loads,
|
|
18
|
+
# attribution, and mentions. `Tally` maps both sources through the name
|
|
19
|
+
# map, and `Report` renders the result as Markdown or JSON.
|
|
20
|
+
#
|
|
21
|
+
# Maintainer tool: `bin/plastic-skill-census` lives beside `bin/plastic-bench`
|
|
22
|
+
# and is never installed into ~/.plastic. Stdlib only, no network, no eval.
|
|
23
|
+
#
|
|
24
|
+
# This file and its CLI never resolve the running process's home directory
|
|
25
|
+
# or its home environment variable: `bin/` ships in the npm tarball, so a
|
|
26
|
+
# default rooted there would make a published executable read any
|
|
27
|
+
# installer's transcripts. `--history` and `--transcripts` are required,
|
|
28
|
+
# with no default.
|
|
29
|
+
module SkillCensus
|
|
30
|
+
DEFAULT_CUTOFF = "2026-09-02"
|
|
31
|
+
|
|
32
|
+
NAME_MAP = {}.freeze
|
|
33
|
+
|
|
34
|
+
# Retired without a successor (attested only).
|
|
35
|
+
RETIRED = [
|
|
36
|
+
"plastic-intent-discovering",
|
|
37
|
+
"plastic-intent-discovery",
|
|
38
|
+
"plastic-intent-curator",
|
|
39
|
+
"plastic-skill-creating",
|
|
40
|
+
"plastic-skill-evaluating",
|
|
41
|
+
"plastic-humanizer",
|
|
42
|
+
"plastic-intent-brainstorming",
|
|
43
|
+
"plastic-brainstorming",
|
|
44
|
+
"plastic-continuing",
|
|
45
|
+
"plastic-project-continuing",
|
|
46
|
+
"plastic-roadmap-continuing",
|
|
47
|
+
"plastic-intent-starting",
|
|
48
|
+
"plastic-intent-creating",
|
|
49
|
+
"plastic-intent-speccing",
|
|
50
|
+
"plastic-intent-planning",
|
|
51
|
+
"plastic-intent-executing",
|
|
52
|
+
"plastic-intent-ending",
|
|
53
|
+
"plastic-intent-continuing",
|
|
54
|
+
"plastic-install",
|
|
55
|
+
"plastic-uninstall",
|
|
56
|
+
"plastic-update",
|
|
57
|
+
"plastic-rollback",
|
|
58
|
+
"plastic-doctor",
|
|
59
|
+
"plastic-feedback",
|
|
60
|
+
"plastic-tutorial",
|
|
61
|
+
"plastic-conventions",
|
|
62
|
+
"plastic-project-creating",
|
|
63
|
+
"plastic-roadmap",
|
|
64
|
+
"plastic-dashboard",
|
|
65
|
+
"plastic-auto",
|
|
66
|
+
"plastic-direct",
|
|
67
|
+
"plastic-agent-advisor",
|
|
68
|
+
"plastic-releasing",
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
# ~/.plastic/scripts/* filenames, never skills (review 3). plastic-lock
|
|
72
|
+
# alone occurs 257 times in the live files; mapping it into plastic-doctor
|
|
73
|
+
# was the original map's worst counting bug.
|
|
74
|
+
SCRIPTS = [
|
|
75
|
+
"plastic-lock",
|
|
76
|
+
"plastic-savepoint",
|
|
77
|
+
"plastic-bench",
|
|
78
|
+
"plastic-statusline",
|
|
79
|
+
"plastic-session-start",
|
|
80
|
+
"plastic-code-gate",
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
# Agent names, never skills (review 3).
|
|
84
|
+
AGENTS = [
|
|
85
|
+
"plastic-advisor",
|
|
86
|
+
"plastic-faux-advisor",
|
|
87
|
+
"plastic-enforcer",
|
|
88
|
+
"plastic-executor",
|
|
89
|
+
"plastic-future-intent-researcher",
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
92
|
+
# Roster skills this instrument cannot observe through the Skill tool or a
|
|
93
|
+
# typed command, with the mechanism that reaches them instead (review 4).
|
|
94
|
+
# plastic-agent-advisor and plastic-direct were retired by intent 372 (family 5) into
|
|
95
|
+
# RETIRED above: their skill directories are gone, so Roster.load never yields a row for
|
|
96
|
+
# them any more, and a MECHANISMS entry that no row can reach is dead.
|
|
97
|
+
MECHANISMS = {
|
|
98
|
+
"plastic-conventions" => "read as references/*.md from inside other skills",
|
|
99
|
+
"plastic-feedback" => "routed by the SessionStart hook and PLASTIC.md",
|
|
100
|
+
}.freeze
|
|
101
|
+
|
|
102
|
+
PLASTIC_TYPED_RE = %r{\A\s*/(plastic[:-][a-z0-9][a-z0-9-]*)}.freeze
|
|
103
|
+
PLASTIC_TAGGED_RE = %r{<command-name>\s*/(plastic[:-][a-z0-9][a-z0-9-]*)</command-name>}.freeze
|
|
104
|
+
BUILTIN_TYPED_RE = %r{\A\s*/([a-zA-Z][a-zA-Z0-9_-]*)}.freeze
|
|
105
|
+
MENTION_SCAN_RE = /plastic[:-][a-z0-9][a-z0-9-]*/.freeze
|
|
106
|
+
LOAD_PREFIX = "Base directory for this skill:"
|
|
107
|
+
|
|
108
|
+
# The pre-npm plugin colon namespace maps onto the hyphen form before the
|
|
109
|
+
# name map is consulted (D10): plastic:update normalises to plastic-update.
|
|
110
|
+
def self.normalize(raw)
|
|
111
|
+
raw.sub(":", "-")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# One classified occurrence: a typed command, a call, a load, a mention.
|
|
115
|
+
# `project` is set only by HistoryScanner (history records carry it);
|
|
116
|
+
# `file` is set only by TranscriptScanner.
|
|
117
|
+
Event = Struct.new(:name, :date, :project, :file, :detail, keyword_init: true)
|
|
118
|
+
|
|
119
|
+
# -------------------------------------------------------------------
|
|
120
|
+
# Roster - the current skill directories, from SKILL.md frontmatter.
|
|
121
|
+
# -------------------------------------------------------------------
|
|
122
|
+
module Roster
|
|
123
|
+
Skill = Struct.new(:name, :user_invocable, keyword_init: true)
|
|
124
|
+
|
|
125
|
+
def self.load(skills_dir)
|
|
126
|
+
raise "skills dir not found: #{skills_dir}" unless File.directory?(skills_dir)
|
|
127
|
+
|
|
128
|
+
Dir.children(skills_dir).sort.filter_map do |entry|
|
|
129
|
+
dir = File.join(skills_dir, entry)
|
|
130
|
+
next unless File.directory?(dir)
|
|
131
|
+
|
|
132
|
+
skill_md = File.join(dir, "SKILL.md")
|
|
133
|
+
next unless File.file?(skill_md)
|
|
134
|
+
|
|
135
|
+
fm = frontmatter(File.read(skill_md))
|
|
136
|
+
Skill.new(name: "plastic-#{entry}", user_invocable: fm["user-invocable"] == true)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# skill_lint.rb's split: content.split("---", 3), parts[1] is the
|
|
141
|
+
# frontmatter block. A missing or unparsable block reads as no flags,
|
|
142
|
+
# which Roster.load turns into user_invocable: false, never a raise.
|
|
143
|
+
def self.frontmatter(content)
|
|
144
|
+
parts = content.split("---", 3)
|
|
145
|
+
return {} if parts.length < 3
|
|
146
|
+
|
|
147
|
+
YAML.safe_load(parts[1].to_s) || {}
|
|
148
|
+
rescue Psych::SyntaxError
|
|
149
|
+
{}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# -------------------------------------------------------------------
|
|
154
|
+
# HistoryScanner - typed commands from ~/.claude/history.jsonl.
|
|
155
|
+
# -------------------------------------------------------------------
|
|
156
|
+
class HistoryScanner
|
|
157
|
+
Result = Struct.new(:record_count, :first_seen, :last_seen, :typed, :mentions,
|
|
158
|
+
:builtins, :self_generated, :monthly, keyword_init: true)
|
|
159
|
+
|
|
160
|
+
def initialize(history_path, cutoff:)
|
|
161
|
+
@path = history_path
|
|
162
|
+
@cutoff = cutoff.is_a?(Date) ? cutoff : Date.parse(cutoff.to_s)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def scan
|
|
166
|
+
raise "history file not found: #{@path}" unless File.file?(@path)
|
|
167
|
+
|
|
168
|
+
record_count = 0
|
|
169
|
+
typed = []
|
|
170
|
+
mentions = []
|
|
171
|
+
builtins = Hash.new(0)
|
|
172
|
+
self_generated = []
|
|
173
|
+
first_seen = nil
|
|
174
|
+
last_seen = nil
|
|
175
|
+
|
|
176
|
+
File.foreach(@path) do |raw_line|
|
|
177
|
+
line = raw_line.strip
|
|
178
|
+
next if line.empty?
|
|
179
|
+
|
|
180
|
+
record = begin
|
|
181
|
+
JSON.parse(line)
|
|
182
|
+
rescue JSON::ParserError
|
|
183
|
+
next
|
|
184
|
+
end
|
|
185
|
+
record_count += 1
|
|
186
|
+
|
|
187
|
+
display = record["display"].to_s
|
|
188
|
+
date = ms_to_date(record["timestamp"])
|
|
189
|
+
project = record["project"]
|
|
190
|
+
|
|
191
|
+
if date
|
|
192
|
+
first_seen = date if first_seen.nil? || date < first_seen
|
|
193
|
+
last_seen = date if last_seen.nil? || date > last_seen
|
|
194
|
+
end
|
|
195
|
+
after_cutoff = date && date >= @cutoff
|
|
196
|
+
|
|
197
|
+
if (m = display.match(PLASTIC_TYPED_RE))
|
|
198
|
+
event = Event.new(name: SkillCensus.normalize(m[1]), date: date, project: project, detail: display)
|
|
199
|
+
after_cutoff ? self_generated << event : typed << event
|
|
200
|
+
elsif (m = display.match(BUILTIN_TYPED_RE))
|
|
201
|
+
builtins[m[1]] += 1 unless after_cutoff
|
|
202
|
+
elsif display.match(MENTION_SCAN_RE)
|
|
203
|
+
mentions << Event.new(name: nil, date: date, project: project, detail: display) unless after_cutoff
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
monthly = Hash.new(0)
|
|
208
|
+
typed.each { |e| monthly[e.date.strftime("%Y-%m")] += 1 if e.date }
|
|
209
|
+
|
|
210
|
+
Result.new(record_count: record_count, first_seen: first_seen, last_seen: last_seen,
|
|
211
|
+
typed: typed, mentions: mentions, builtins: builtins,
|
|
212
|
+
self_generated: self_generated, monthly: monthly)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def ms_to_date(ms)
|
|
218
|
+
return nil unless ms
|
|
219
|
+
|
|
220
|
+
Time.at(ms.to_f / 1000.0).utc.to_date
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# -------------------------------------------------------------------
|
|
225
|
+
# TranscriptScanner - calls, loads, attribution, and mentions from the
|
|
226
|
+
# ~/.claude/projects tree.
|
|
227
|
+
# -------------------------------------------------------------------
|
|
228
|
+
class TranscriptScanner
|
|
229
|
+
Call = Struct.new(:name, :tool_use_id, :file, :date, keyword_init: true)
|
|
230
|
+
Corroboration = Struct.new(:matched, :disagreements, keyword_init: true)
|
|
231
|
+
|
|
232
|
+
Result = Struct.new(
|
|
233
|
+
:files_top, :files_subagent, :record_count, :unparsable_lines,
|
|
234
|
+
:first_seen, :last_seen,
|
|
235
|
+
:calls_main, :calls_agent, :other_skills,
|
|
236
|
+
:loads, :attributed, :mentions, :scripts, :agents,
|
|
237
|
+
:transcript_typed, :self_generated, :corroboration,
|
|
238
|
+
:self_generated_calls, :self_generated_loads,
|
|
239
|
+
:self_generated_attributed, :self_generated_mentions,
|
|
240
|
+
keyword_init: true
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def initialize(transcripts_dir, cutoff:)
|
|
244
|
+
@dir = transcripts_dir
|
|
245
|
+
@cutoff = cutoff.is_a?(Date) ? cutoff : Date.parse(cutoff.to_s)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def scan
|
|
249
|
+
raise "transcripts dir not found: #{@dir}" unless File.directory?(@dir)
|
|
250
|
+
|
|
251
|
+
state = new_state
|
|
252
|
+
Dir.glob(File.join(@dir, "**", "*.jsonl")).sort.each { |path| scan_file(path, state) }
|
|
253
|
+
finish(state)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def new_state
|
|
259
|
+
{
|
|
260
|
+
files_top: 0, files_subagent: 0, record_count: 0, unparsable_lines: 0,
|
|
261
|
+
first_seen: nil, last_seen: nil,
|
|
262
|
+
calls_main: [], calls_agent: [],
|
|
263
|
+
other_skills: Hash.new { |h, k| h[k] = { calls: 0, loads: 0, attributed: 0 } },
|
|
264
|
+
loads: [], attributed: Hash.new(0), mentions: [], scripts: Hash.new(0), agents: Hash.new(0),
|
|
265
|
+
transcript_typed: [], self_generated: [],
|
|
266
|
+
self_generated_calls: 0, self_generated_loads: 0,
|
|
267
|
+
self_generated_attributed: 0, self_generated_mentions: 0,
|
|
268
|
+
seen_tool_use_ids: Set.new, seen_uuids: Set.new,
|
|
269
|
+
corroboration_by_id: Hash.new { |h, k| h[k] = {} },
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def scan_file(path, state)
|
|
274
|
+
file_is_subagent = subagent_path?(path)
|
|
275
|
+
state[file_is_subagent ? :files_subagent : :files_top] += 1
|
|
276
|
+
|
|
277
|
+
File.foreach(path) do |raw_line|
|
|
278
|
+
line = raw_line.strip
|
|
279
|
+
next if line.empty?
|
|
280
|
+
|
|
281
|
+
record = begin
|
|
282
|
+
JSON.parse(line)
|
|
283
|
+
rescue JSON::ParserError
|
|
284
|
+
state[:unparsable_lines] += 1
|
|
285
|
+
next
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
uuid = record["uuid"]
|
|
289
|
+
next if uuid && !state[:seen_uuids].add?(uuid)
|
|
290
|
+
|
|
291
|
+
state[:record_count] += 1
|
|
292
|
+
classify_record(record, path, file_is_subagent, state)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def classify_record(record, path, file_is_subagent, state)
|
|
297
|
+
date = iso_to_date(record["timestamp"])
|
|
298
|
+
track_span(date, state)
|
|
299
|
+
after_cutoff = date && date >= @cutoff
|
|
300
|
+
record_is_subagent = file_is_subagent || record["isSidechain"] == true || record.key?("agentId")
|
|
301
|
+
|
|
302
|
+
if (attribution = record["attributionSkill"])
|
|
303
|
+
if after_cutoff
|
|
304
|
+
state[:self_generated_attributed] += 1
|
|
305
|
+
elsif plastic_name?(attribution)
|
|
306
|
+
state[:attributed][attribution] += 1
|
|
307
|
+
else
|
|
308
|
+
state[:other_skills][attribution][:attributed] += 1
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
classify_tool_use(record, path, date, record_is_subagent, after_cutoff, state) if record["type"] == "assistant"
|
|
313
|
+
classify_tool_result(record, after_cutoff, state)
|
|
314
|
+
|
|
315
|
+
return unless record["type"] == "user"
|
|
316
|
+
|
|
317
|
+
classify_user_text(record, path, date, after_cutoff, record_is_subagent, state)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def track_span(date, state)
|
|
321
|
+
return unless date
|
|
322
|
+
|
|
323
|
+
state[:first_seen] = date if state[:first_seen].nil? || date < state[:first_seen]
|
|
324
|
+
state[:last_seen] = date if state[:last_seen].nil? || date > state[:last_seen]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def classify_tool_use(record, path, date, record_is_subagent, after_cutoff, state)
|
|
328
|
+
each_content_block(record) do |block|
|
|
329
|
+
next unless block.is_a?(Hash) && block["type"] == "tool_use" && block["name"] == "Skill"
|
|
330
|
+
|
|
331
|
+
raw = block.dig("input", "skill").to_s
|
|
332
|
+
next if raw.empty?
|
|
333
|
+
|
|
334
|
+
# A post-cutoff block is counted only in the self-generated bucket and
|
|
335
|
+
# never touches seen_tool_use_ids, so it cannot consume an id a
|
|
336
|
+
# pre-cutoff block would later need (D16).
|
|
337
|
+
if after_cutoff
|
|
338
|
+
state[:self_generated_calls] += 1
|
|
339
|
+
next
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
id = block["id"]
|
|
343
|
+
next if id && !state[:seen_tool_use_ids].add?(id)
|
|
344
|
+
|
|
345
|
+
# Only the plastic: colon namespace maps onto the hyphen form (D10);
|
|
346
|
+
# a plugin-prefixed non-Plastic name like claudish-to-english:claudish
|
|
347
|
+
# must appear verbatim in other_skills, not split on its own colon.
|
|
348
|
+
name = raw.start_with?("plastic:") ? SkillCensus.normalize(raw) : raw
|
|
349
|
+
call = Call.new(name: name, tool_use_id: id, file: path, date: date)
|
|
350
|
+
if plastic_name?(name)
|
|
351
|
+
(record_is_subagent ? state[:calls_agent] : state[:calls_main]) << call
|
|
352
|
+
else
|
|
353
|
+
state[:other_skills][name][:calls] += 1
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def classify_tool_result(record, after_cutoff, state)
|
|
359
|
+
return if after_cutoff
|
|
360
|
+
|
|
361
|
+
content = message_content(record)
|
|
362
|
+
return unless content.is_a?(Array)
|
|
363
|
+
|
|
364
|
+
block = content.find { |b| b.is_a?(Hash) && b["type"] == "tool_result" }
|
|
365
|
+
return unless block
|
|
366
|
+
|
|
367
|
+
tool_use_id = block["tool_use_id"]
|
|
368
|
+
return unless tool_use_id
|
|
369
|
+
|
|
370
|
+
if record["toolUseResult"].is_a?(Hash) && record["toolUseResult"]["commandName"]
|
|
371
|
+
state[:corroboration_by_id][tool_use_id][:command_name] = record["toolUseResult"]["commandName"]
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
text = block["content"]
|
|
375
|
+
text = text.filter_map { |c| c["text"] if c.is_a?(Hash) }.join if text.is_a?(Array)
|
|
376
|
+
if text.is_a?(String) && (m = text.match(/Launching skill:\s*(\S+)/))
|
|
377
|
+
state[:corroboration_by_id][tool_use_id][:launching_text] = m[1]
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def classify_user_text(record, path, date, after_cutoff, record_is_subagent, state)
|
|
382
|
+
text = extract_text(record)
|
|
383
|
+
return if text.nil? || text.empty?
|
|
384
|
+
|
|
385
|
+
if (load_name = load_name_from(text))
|
|
386
|
+
if after_cutoff
|
|
387
|
+
state[:self_generated_loads] += 1
|
|
388
|
+
elsif plastic_name?(load_name)
|
|
389
|
+
state[:loads] << Event.new(name: load_name, date: date, file: path)
|
|
390
|
+
else
|
|
391
|
+
state[:other_skills][load_name][:loads] += 1
|
|
392
|
+
end
|
|
393
|
+
remainder = text.sub(/\A.*#{Regexp.escape(LOAD_PREFIX)}[^\n]*\n?/m, "")
|
|
394
|
+
scan_occurrences(remainder, path, date, after_cutoff, state)
|
|
395
|
+
return
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
typed_name = tagged_or_bare_typed_name(text)
|
|
399
|
+
if typed_name
|
|
400
|
+
event = Event.new(name: typed_name, date: date, file: path, detail: text[0, 200])
|
|
401
|
+
unless record_is_subagent
|
|
402
|
+
after_cutoff ? state[:self_generated] << event : state[:transcript_typed] << event
|
|
403
|
+
end
|
|
404
|
+
return
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
scan_occurrences(text, path, date, after_cutoff, state)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def scan_occurrences(text, path, date, after_cutoff, state)
|
|
411
|
+
matches = text.scan(MENTION_SCAN_RE)
|
|
412
|
+
if after_cutoff
|
|
413
|
+
state[:self_generated_mentions] += matches.length
|
|
414
|
+
return
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
matches.each do |raw|
|
|
418
|
+
name = SkillCensus.normalize(raw)
|
|
419
|
+
if SCRIPTS.include?(name)
|
|
420
|
+
state[:scripts][name] += 1
|
|
421
|
+
elsif AGENTS.include?(name)
|
|
422
|
+
state[:agents][name] += 1
|
|
423
|
+
else
|
|
424
|
+
state[:mentions] << Event.new(name: name, date: date, file: path)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def finish(state)
|
|
430
|
+
disagreements = state[:corroboration_by_id].filter_map do |tool_use_id, sides|
|
|
431
|
+
next unless sides[:command_name] && sides[:launching_text]
|
|
432
|
+
next if sides[:command_name] == sides[:launching_text]
|
|
433
|
+
|
|
434
|
+
{ tool_use_id: tool_use_id, command_name: sides[:command_name], launching_text: sides[:launching_text] }
|
|
435
|
+
end
|
|
436
|
+
matched = state[:corroboration_by_id].count { |_, sides| sides[:command_name] }
|
|
437
|
+
|
|
438
|
+
Result.new(
|
|
439
|
+
files_top: state[:files_top], files_subagent: state[:files_subagent],
|
|
440
|
+
record_count: state[:record_count], unparsable_lines: state[:unparsable_lines],
|
|
441
|
+
first_seen: state[:first_seen], last_seen: state[:last_seen],
|
|
442
|
+
calls_main: state[:calls_main], calls_agent: state[:calls_agent], other_skills: state[:other_skills],
|
|
443
|
+
loads: state[:loads], attributed: state[:attributed], mentions: state[:mentions],
|
|
444
|
+
scripts: state[:scripts], agents: state[:agents],
|
|
445
|
+
transcript_typed: state[:transcript_typed], self_generated: state[:self_generated],
|
|
446
|
+
corroboration: Corroboration.new(matched: matched, disagreements: disagreements),
|
|
447
|
+
self_generated_calls: state[:self_generated_calls], self_generated_loads: state[:self_generated_loads],
|
|
448
|
+
self_generated_attributed: state[:self_generated_attributed],
|
|
449
|
+
self_generated_mentions: state[:self_generated_mentions]
|
|
450
|
+
)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def plastic_name?(name)
|
|
454
|
+
name.start_with?("plastic-")
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def subagent_path?(path)
|
|
458
|
+
path.split(File::SEPARATOR).include?("subagents")
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def iso_to_date(value)
|
|
462
|
+
return nil if value.nil?
|
|
463
|
+
|
|
464
|
+
Time.parse(value.to_s).to_date
|
|
465
|
+
rescue ArgumentError, TypeError
|
|
466
|
+
nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def message_content(record)
|
|
470
|
+
record["message"].is_a?(Hash) ? record["message"]["content"] : record["content"]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def each_content_block(record)
|
|
474
|
+
content = message_content(record)
|
|
475
|
+
return unless content.is_a?(Array)
|
|
476
|
+
|
|
477
|
+
content.each { |block| yield block }
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def extract_text(record)
|
|
481
|
+
content = message_content(record)
|
|
482
|
+
return content if content.is_a?(String)
|
|
483
|
+
return nil unless content.is_a?(Array)
|
|
484
|
+
|
|
485
|
+
content.filter_map { |b| b["text"] if b.is_a?(Hash) && b["type"] == "text" }.join("\n")
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def load_name_from(text)
|
|
489
|
+
return nil unless text.lstrip.start_with?(LOAD_PREFIX)
|
|
490
|
+
|
|
491
|
+
path = text.lstrip.sub(LOAD_PREFIX, "").strip.lines.first.to_s.strip
|
|
492
|
+
File.basename(path)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def tagged_or_bare_typed_name(text)
|
|
496
|
+
if (m = text.match(PLASTIC_TAGGED_RE))
|
|
497
|
+
return SkillCensus.normalize(m[1])
|
|
498
|
+
end
|
|
499
|
+
if (m = text.match(PLASTIC_TYPED_RE))
|
|
500
|
+
return SkillCensus.normalize(m[1])
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
nil
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# -------------------------------------------------------------------
|
|
508
|
+
# Tally - maps both sources' events onto the roster through NAME_MAP,
|
|
509
|
+
# RETIRED, and MECHANISMS.
|
|
510
|
+
# -------------------------------------------------------------------
|
|
511
|
+
class Tally
|
|
512
|
+
SkillRow = Struct.new(
|
|
513
|
+
:name, :user_invocable, :typed, :calls_main, :calls_agent, :loads,
|
|
514
|
+
:attributed, :mapped_from, :mechanism, :first_seen, :last_seen,
|
|
515
|
+
keyword_init: true
|
|
516
|
+
) do
|
|
517
|
+
def evidence?
|
|
518
|
+
typed.to_i.positive? || calls_main.to_i.positive? || calls_agent.to_i.positive?
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
Built = Struct.new(:skills, :retired, :unmapped, :map_coverage, :history, :transcript, keyword_init: true)
|
|
523
|
+
|
|
524
|
+
def initialize(history_scan, transcript_scan, roster, name_map: NAME_MAP)
|
|
525
|
+
@name_map = name_map
|
|
526
|
+
@history = history_scan
|
|
527
|
+
@transcript = transcript_scan
|
|
528
|
+
@roster = roster
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def build
|
|
532
|
+
rows = {}
|
|
533
|
+
@roster.each { |skill| rows[skill.name] = new_row(skill) }
|
|
534
|
+
retired_row = new_row(nil, name: "retired")
|
|
535
|
+
unmapped_row = new_row(nil, name: "unmapped")
|
|
536
|
+
|
|
537
|
+
apply = lambda do |raw_name, field, date = nil|
|
|
538
|
+
target = resolve(raw_name, rows)
|
|
539
|
+
row = case target
|
|
540
|
+
when :retired then retired_row
|
|
541
|
+
when :unmapped then unmapped_row
|
|
542
|
+
else rows[target]
|
|
543
|
+
end
|
|
544
|
+
row[field] += 1
|
|
545
|
+
row.mapped_from[raw_name] += 1 if target != raw_name
|
|
546
|
+
track_row_span(row, date)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
@history.typed.each { |event| apply.call(event.name, :typed, event.date) }
|
|
550
|
+
@transcript.calls_main.each { |call| apply.call(call.name, :calls_main, call.date) }
|
|
551
|
+
@transcript.calls_agent.each { |call| apply.call(call.name, :calls_agent, call.date) }
|
|
552
|
+
@transcript.loads.each { |event| apply.call(event.name, :loads, event.date) }
|
|
553
|
+
@transcript.attributed.each { |name, count| count.times { apply.call(name, :attributed) } }
|
|
554
|
+
|
|
555
|
+
map_coverage = @name_map.map do |raw, target|
|
|
556
|
+
{ raw: raw, target: target, observed: rows[target]&.mapped_from&.fetch(raw, 0) || 0 }
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
Built.new(skills: @roster.map { |s| rows[s.name] }, retired: retired_row, unmapped: unmapped_row,
|
|
560
|
+
map_coverage: map_coverage, history: @history, transcript: @transcript)
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
private
|
|
564
|
+
|
|
565
|
+
def new_row(skill, name: nil)
|
|
566
|
+
SkillRow.new(
|
|
567
|
+
name: skill&.name || name, user_invocable: skill&.user_invocable,
|
|
568
|
+
typed: 0, calls_main: 0, calls_agent: 0, loads: 0, attributed: 0,
|
|
569
|
+
mapped_from: Hash.new(0), mechanism: skill && MECHANISMS[skill.name]
|
|
570
|
+
)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def resolve(raw_name, rows)
|
|
574
|
+
return raw_name if rows.key?(raw_name)
|
|
575
|
+
|
|
576
|
+
target = @name_map[raw_name]
|
|
577
|
+
return target if target && rows.key?(target)
|
|
578
|
+
return :retired if RETIRED.include?(raw_name)
|
|
579
|
+
|
|
580
|
+
:unmapped
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def track_row_span(row, date)
|
|
584
|
+
return unless date
|
|
585
|
+
|
|
586
|
+
row.first_seen = date if row.first_seen.nil? || date < row.first_seen
|
|
587
|
+
row.last_seen = date if row.last_seen.nil? || date > row.last_seen
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# -------------------------------------------------------------------
|
|
592
|
+
# Report - Markdown and JSON rendering.
|
|
593
|
+
# -------------------------------------------------------------------
|
|
594
|
+
module Report
|
|
595
|
+
module_function
|
|
596
|
+
|
|
597
|
+
def markdown(built, history_path: nil, transcripts_path: nil, cutoff: nil)
|
|
598
|
+
lines = []
|
|
599
|
+
lines.concat(header(history_path, transcripts_path, cutoff))
|
|
600
|
+
lines.concat(method_section(cutoff))
|
|
601
|
+
lines.concat(summary_table(built))
|
|
602
|
+
lines.concat(per_skill_sections(built))
|
|
603
|
+
lines.concat(context_tables(built))
|
|
604
|
+
lines.concat(cross_check_section(built))
|
|
605
|
+
lines.concat(self_generated_section(built))
|
|
606
|
+
lines.concat(typed_events_section(built))
|
|
607
|
+
lines.join("\n") + "\n"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def header(history_path, transcripts_path, cutoff)
|
|
611
|
+
lines = ["# Plastic skill usage census", ""]
|
|
612
|
+
lines << "- history: #{history_path}" if history_path
|
|
613
|
+
lines << "- transcripts: #{transcripts_path}" if transcripts_path
|
|
614
|
+
lines << "- cutoff: #{cutoff}" if cutoff
|
|
615
|
+
lines << ""
|
|
616
|
+
lines
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def method_section(cutoff)
|
|
620
|
+
[
|
|
621
|
+
"## Method",
|
|
622
|
+
"",
|
|
623
|
+
"- Typed command: a history.jsonl record whose display matches a leading /plastic- or /plastic: form; " \
|
|
624
|
+
"a mid-sentence or backticked mention is excluded and tallied separately.",
|
|
625
|
+
"- Calls: a Skill tool_use block, split into calls_main (top-level transcript file) and calls_agent " \
|
|
626
|
+
"(subagent transcript file), because the auto pipeline dispatches its own skills.",
|
|
627
|
+
"- Loads: a user record whose text begins \"Base directory for this skill:\".",
|
|
628
|
+
"- Attribution: attributionSkill on a record measures time spent under a skill, not invocations, and " \
|
|
629
|
+
"never feeds the call count.",
|
|
630
|
+
"- Cutoff: records dated on or after #{cutoff} are excluded from every count and reported on the " \
|
|
631
|
+
"Self-generated line.",
|
|
632
|
+
"",
|
|
633
|
+
]
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def summary_table(built)
|
|
637
|
+
lines = [
|
|
638
|
+
"## Summary",
|
|
639
|
+
"",
|
|
640
|
+
"| skill | user_invocable | typed | calls_main | calls_agent | loads | attributed | evidence |",
|
|
641
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
642
|
+
]
|
|
643
|
+
(built.skills + [built.retired, built.unmapped]).each { |row| lines << summary_row(row) }
|
|
644
|
+
lines << ""
|
|
645
|
+
lines
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def summary_row(row)
|
|
649
|
+
if row.mechanism && !row.evidence?
|
|
650
|
+
"| #{row.name} | #{fmt(row.user_invocable)} | n/a | n/a | n/a | #{row.loads} | #{row.attributed} | n/a - #{row.mechanism} |"
|
|
651
|
+
else
|
|
652
|
+
"| #{row.name} | #{fmt(row.user_invocable)} | #{row.typed} | #{row.calls_main} | #{row.calls_agent} | " \
|
|
653
|
+
"#{row.loads} | #{row.attributed} | #{row.evidence? ? "measured" : "zero"} |"
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def fmt(value)
|
|
658
|
+
value.nil? ? "-" : value.to_s
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def per_skill_sections(built)
|
|
662
|
+
lines = ["## Per-skill", ""]
|
|
663
|
+
built.skills.each do |row|
|
|
664
|
+
lines << "## #{row.name}"
|
|
665
|
+
lines << ""
|
|
666
|
+
lines << "| field | value |"
|
|
667
|
+
lines << "| --- | --- |"
|
|
668
|
+
if row.mechanism && !row.evidence?
|
|
669
|
+
lines << "| evidence | n/a |"
|
|
670
|
+
lines << "| mechanism | #{row.mechanism} |"
|
|
671
|
+
else
|
|
672
|
+
lines << "| typed | #{row.typed} |"
|
|
673
|
+
lines << "| calls_main | #{row.calls_main} |"
|
|
674
|
+
lines << "| calls_agent | #{row.calls_agent} |"
|
|
675
|
+
end
|
|
676
|
+
lines << "| loads | #{row.loads} |"
|
|
677
|
+
lines << "| attributed | #{row.attributed} (time spent under this skill, not invocations) |"
|
|
678
|
+
unless row.mapped_from.empty?
|
|
679
|
+
mapped = row.mapped_from.map { |name, count| "#{name} (#{count})" }.join(", ")
|
|
680
|
+
lines << "| mapped from | #{mapped} |"
|
|
681
|
+
end
|
|
682
|
+
lines << "| first seen | #{row.first_seen} |" if row.first_seen
|
|
683
|
+
lines << "| last seen | #{row.last_seen} |" if row.last_seen
|
|
684
|
+
lines << ""
|
|
685
|
+
end
|
|
686
|
+
lines
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def context_tables(built)
|
|
690
|
+
transcript = built.transcript
|
|
691
|
+
history = built.history
|
|
692
|
+
lines = ["## Built-in commands", "", "| command | count |", "| --- | --- |"]
|
|
693
|
+
history.builtins.sort.each { |name, count| lines << "| /#{name} | #{count} |" }
|
|
694
|
+
lines << ""
|
|
695
|
+
|
|
696
|
+
lines << "## Other skills"
|
|
697
|
+
lines << ""
|
|
698
|
+
lines << "| skill | calls | loads | attributed |"
|
|
699
|
+
lines << "| --- | --- | --- | --- |"
|
|
700
|
+
transcript.other_skills.sort.each do |name, counts|
|
|
701
|
+
lines << "| #{name} | #{counts[:calls]} | #{counts[:loads]} | #{counts[:attributed]} |"
|
|
702
|
+
end
|
|
703
|
+
lines << ""
|
|
704
|
+
|
|
705
|
+
lines << "## Scripts"
|
|
706
|
+
lines << ""
|
|
707
|
+
lines << "| script | occurrences |"
|
|
708
|
+
lines << "| --- | --- |"
|
|
709
|
+
SCRIPTS.each { |name| lines << "| #{name} | #{transcript.scripts.fetch(name, 0)} |" }
|
|
710
|
+
lines << ""
|
|
711
|
+
|
|
712
|
+
lines << "## Agents"
|
|
713
|
+
lines << ""
|
|
714
|
+
lines << "| agent | occurrences |"
|
|
715
|
+
lines << "| --- | --- |"
|
|
716
|
+
AGENTS.each { |name| lines << "| #{name} | #{transcript.agents.fetch(name, 0)} |" }
|
|
717
|
+
lines << ""
|
|
718
|
+
|
|
719
|
+
lines << "## Map coverage"
|
|
720
|
+
lines << ""
|
|
721
|
+
lines << "| raw name | maps to | observed |"
|
|
722
|
+
lines << "| --- | --- | --- |"
|
|
723
|
+
built.map_coverage.each { |entry| lines << "| #{entry[:raw]} | #{entry[:target]} | #{entry[:observed]} |" }
|
|
724
|
+
lines << ""
|
|
725
|
+
lines << "Observed sums typed, calls (main plus agent), loads, and attributed occurrences for " \
|
|
726
|
+
"that raw name; it is not a single dimension."
|
|
727
|
+
lines << ""
|
|
728
|
+
lines
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def cross_check_section(built)
|
|
732
|
+
history = built.history
|
|
733
|
+
transcript = built.transcript
|
|
734
|
+
window_start = transcript.first_seen
|
|
735
|
+
history_in_window = if window_start
|
|
736
|
+
history.typed.count { |e| e.date && e.date >= window_start }
|
|
737
|
+
else
|
|
738
|
+
0
|
|
739
|
+
end
|
|
740
|
+
transcript_typed_total = transcript.transcript_typed.length
|
|
741
|
+
gap = history_in_window.zero? ? 0 : ((history_in_window - transcript_typed_total).abs.to_f / history_in_window * 100).round(1)
|
|
742
|
+
|
|
743
|
+
[
|
|
744
|
+
"## Cross-check",
|
|
745
|
+
"",
|
|
746
|
+
"History typed commands inside the transcript window (from #{window_start}): #{history_in_window}.",
|
|
747
|
+
"Transcript-typed events over the same window: #{transcript_typed_total}.",
|
|
748
|
+
"Gap: #{gap} percent.",
|
|
749
|
+
"",
|
|
750
|
+
]
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def self_generated_section(built)
|
|
754
|
+
history_self = built.history.self_generated
|
|
755
|
+
transcript = built.transcript
|
|
756
|
+
[
|
|
757
|
+
"## Self-generated",
|
|
758
|
+
"",
|
|
759
|
+
"History records excluded by the cutoff: #{history_self.length}.",
|
|
760
|
+
"",
|
|
761
|
+
"Transcript records excluded by the cutoff, per dimension (D16: the cutoff gates " \
|
|
762
|
+
"every count, not only typed commands):",
|
|
763
|
+
"",
|
|
764
|
+
"| dimension | excluded |",
|
|
765
|
+
"| --- | --- |",
|
|
766
|
+
"| typed | #{transcript.self_generated.length} |",
|
|
767
|
+
"| mentions | #{transcript.self_generated_mentions.to_i} |",
|
|
768
|
+
"| calls | #{transcript.self_generated_calls.to_i} |",
|
|
769
|
+
"| loads | #{transcript.self_generated_loads.to_i} |",
|
|
770
|
+
"| attributed | #{transcript.self_generated_attributed.to_i} |",
|
|
771
|
+
"",
|
|
772
|
+
]
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
def typed_events_section(built)
|
|
776
|
+
lines = ["## Typed events", "", "| name | date | project |", "| --- | --- | --- |"]
|
|
777
|
+
built.history.typed.each { |event| lines << "| #{event.name} | #{event.date} | #{event.project} |" }
|
|
778
|
+
lines << ""
|
|
779
|
+
lines
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def json(built, history_path: nil, transcripts_path: nil, cutoff: nil)
|
|
783
|
+
JSON.pretty_generate(build_data(built, history_path: history_path, transcripts_path: transcripts_path, cutoff: cutoff))
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def build_data(built, history_path: nil, transcripts_path: nil, cutoff: nil)
|
|
787
|
+
{
|
|
788
|
+
"history_path" => history_path,
|
|
789
|
+
"transcripts_path" => transcripts_path,
|
|
790
|
+
"cutoff" => cutoff,
|
|
791
|
+
"skills" => (built.skills + [built.retired, built.unmapped]).map { |row| row_data(row) },
|
|
792
|
+
"map_coverage" => built.map_coverage,
|
|
793
|
+
"history" => {
|
|
794
|
+
"record_count" => built.history.record_count,
|
|
795
|
+
"first_seen" => built.history.first_seen&.to_s,
|
|
796
|
+
"last_seen" => built.history.last_seen&.to_s,
|
|
797
|
+
"typed_total" => built.history.typed.length,
|
|
798
|
+
"monthly" => built.history.monthly,
|
|
799
|
+
"builtins" => built.history.builtins,
|
|
800
|
+
"self_generated" => built.history.self_generated.length,
|
|
801
|
+
},
|
|
802
|
+
"transcript" => {
|
|
803
|
+
"files_top" => built.transcript.files_top,
|
|
804
|
+
"files_subagent" => built.transcript.files_subagent,
|
|
805
|
+
"record_count" => built.transcript.record_count,
|
|
806
|
+
"unparsable_lines" => built.transcript.unparsable_lines,
|
|
807
|
+
"first_seen" => built.transcript.first_seen&.to_s,
|
|
808
|
+
"last_seen" => built.transcript.last_seen&.to_s,
|
|
809
|
+
"calls_main" => built.transcript.calls_main.length,
|
|
810
|
+
"calls_agent" => built.transcript.calls_agent.length,
|
|
811
|
+
"other_skills" => built.transcript.other_skills,
|
|
812
|
+
"loads" => built.transcript.loads.length,
|
|
813
|
+
"attributed" => built.transcript.attributed,
|
|
814
|
+
"mentions" => built.transcript.mentions.length,
|
|
815
|
+
"scripts" => built.transcript.scripts,
|
|
816
|
+
"agents" => built.transcript.agents,
|
|
817
|
+
"transcript_typed" => built.transcript.transcript_typed.length,
|
|
818
|
+
"self_generated" => built.transcript.self_generated.length,
|
|
819
|
+
"self_generated_mentions" => built.transcript.self_generated_mentions.to_i,
|
|
820
|
+
"self_generated_calls" => built.transcript.self_generated_calls.to_i,
|
|
821
|
+
"self_generated_loads" => built.transcript.self_generated_loads.to_i,
|
|
822
|
+
"self_generated_attributed" => built.transcript.self_generated_attributed.to_i,
|
|
823
|
+
"corroboration_matched" => built.transcript.corroboration.matched,
|
|
824
|
+
"corroboration_disagreements" => built.transcript.corroboration.disagreements,
|
|
825
|
+
},
|
|
826
|
+
}
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def row_data(row)
|
|
830
|
+
{
|
|
831
|
+
"name" => row.name, "user_invocable" => row.user_invocable,
|
|
832
|
+
"typed" => row.typed, "calls_main" => row.calls_main, "calls_agent" => row.calls_agent,
|
|
833
|
+
"loads" => row.loads, "attributed" => row.attributed,
|
|
834
|
+
"mapped_from" => row.mapped_from, "mechanism" => row.mechanism,
|
|
835
|
+
"first_seen" => row.first_seen&.to_s, "last_seen" => row.last_seen&.to_s,
|
|
836
|
+
}
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
end
|