@event4u/agent-config 7.3.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +4 -1
  2. package/CHANGELOG.md +81 -0
  3. package/README.md +2 -2
  4. package/dist/agent-src/commands/fix/pr-comments.md +42 -13
  5. package/dist/agent-src/commands/review-changes.md +14 -7
  6. package/dist/agent-src/rules/artifact-engagement-recording.md +9 -0
  7. package/dist/agent-src/rules/cli-output-handling.md +18 -0
  8. package/dist/agent-src/rules/domain-safety-pii.md +4 -3
  9. package/dist/agent-src/rules/icon-consistency.md +1 -0
  10. package/dist/agent-src/rules/legal-safety-floor.md +2 -2
  11. package/dist/agent-src/rules/untrusted-input-defense.md +8 -5
  12. package/dist/agent-src/skills/adr-create/SKILL.md +5 -1
  13. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  14. package/dist/agent-src/skills/blade-ui/SKILL.md +4 -0
  15. package/dist/agent-src/skills/brand-to-tokens/SKILL.md +2 -1
  16. package/dist/agent-src/skills/code-review/SKILL.md +4 -0
  17. package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
  18. package/dist/agent-src/skills/design-intelligence/SKILL.md +40 -0
  19. package/dist/agent-src/skills/design-review/SKILL.md +23 -11
  20. package/dist/agent-src/skills/design-system-capture/SKILL.md +41 -0
  21. package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +62 -0
  22. package/dist/agent-src/skills/design-tokens/SKILL.md +1 -0
  23. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +6 -0
  24. package/dist/agent-src/skills/flux/SKILL.md +4 -0
  25. package/dist/agent-src/skills/iconography/SKILL.md +1 -0
  26. package/dist/agent-src/skills/image-editing/evals/triggers.json +13 -13
  27. package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +136 -0
  28. package/dist/agent-src/skills/judge-artifact-completeness/calibration/fixtures.json +149 -0
  29. package/dist/agent-src/skills/judge-artifact-completeness/evals/triggers.json +17 -0
  30. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/architecture-score.json +73 -0
  31. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/pr-review-score.json +65 -0
  32. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/roadmap-score.json +73 -0
  33. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +73 -0
  34. package/dist/agent-src/skills/judge-injection-defense/SKILL.md +99 -0
  35. package/dist/agent-src/skills/judge-injection-defense/calibration/fixtures.json +24 -0
  36. package/dist/agent-src/skills/judge-injection-defense/evals/triggers.json +17 -0
  37. package/dist/agent-src/skills/judge-injection-defense/fixtures/behavioral-fixtures.json +63 -0
  38. package/dist/agent-src/skills/judge-injection-defense/fixtures/coverage-report.md +53 -0
  39. package/dist/agent-src/skills/judge-injection-defense/fixtures/perturbation-taxonomy.json +110 -0
  40. package/dist/agent-src/skills/judge-synthesis/SKILL.md +134 -0
  41. package/dist/agent-src/skills/judge-synthesis/evals/triggers.json +17 -0
  42. package/dist/agent-src/skills/logo-generation/SKILL.md +1 -1
  43. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  44. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +18 -0
  45. package/dist/agent-src/skills/react-shadcn-ui/reference/registry.md +59 -0
  46. package/dist/agent-src/skills/refine-ticket/SKILL.md +5 -1
  47. package/dist/agent-src/skills/release-comms/SKILL.md +1 -1
  48. package/dist/agent-src/skills/roadmap-management/SKILL.md +4 -0
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/script-writing/SKILL.md +1 -1
  51. package/dist/agent-src/skills/sql-writing/SKILL.md +1 -1
  52. package/dist/agent-src/skills/systematic-debugging/SKILL.md +1 -1
  53. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +4 -0
  54. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  55. package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
  56. package/dist/agent-src/skills/typography-system/SKILL.md +2 -1
  57. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  58. package/dist/cli/registry.js +1 -0
  59. package/dist/cli/registry.js.map +1 -1
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +109 -43
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +3 -3
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +8 -5
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +7 -4
  68. package/dist/mcp/registry-manifest.json +3 -3
  69. package/dist/router.json +1 -1
  70. package/dist/server/schemas/settings.js +6 -0
  71. package/dist/server/schemas/settings.js.map +1 -1
  72. package/docs/architecture.md +2 -2
  73. package/docs/catalog.md +28 -20
  74. package/docs/command-flows.md +1 -1
  75. package/docs/enforcement-by-host.md +9 -0
  76. package/docs/governance-advantage.md +72 -0
  77. package/docs/guidelines/design-antipatterns.md +26 -0
  78. package/docs/guidelines/design-canon.md +62 -0
  79. package/package.json +2 -1
  80. package/src/config/agent-settings.template.yml +21 -0
  81. package/src/scripts/_cli/cmd_analyze_session.ts +347 -0
  82. package/src/scripts/_dispatch.bash +8 -0
  83. package/src/scripts/_lib/token_count.ts +110 -35
  84. package/src/scripts/audit_initial_context.ts +37 -12
  85. package/src/scripts/bench_quality_run.ts +253 -0
  86. package/src/scripts/bench_rule_load_latency.ts +178 -0
  87. package/src/scripts/check_kernel_prefix_stability.ts +0 -0
  88. package/src/scripts/check_quality_regression.ts +244 -0
  89. package/src/scripts/check_token_quality_golden.ts +253 -0
  90. package/src/scripts/check_token_regression.ts +266 -0
  91. package/src/scripts/design_slop_rules.test.ts +193 -0
  92. package/src/scripts/design_slop_rules.ts +502 -0
  93. package/src/scripts/external_sources_denylist.json +7 -1
  94. package/src/scripts/hook_manifest.yaml +26 -3
  95. package/src/scripts/hooks/design_slop_hook.ts +181 -0
  96. package/src/scripts/hooks/rtk_wrap_hook.ts +228 -0
  97. package/src/scripts/lint_agent_security.ts +1 -0
  98. package/src/scripts/lint_confusables.ts +197 -0
  99. package/src/scripts/lint_design_slop.ts +245 -0
  100. package/src/scripts/probe_host_compliance.ts +145 -0
@@ -0,0 +1,72 @@
1
+ # The governance advantage — why compile-time, config-space governance is the moat
2
+
3
+ Most agent-config packages are a pile of skills. This one's defensible edge is
4
+ not skill *count* — it is **where the governance lives**: compiled into every
5
+ host's own instruction layer at projection time, so it travels with the config
6
+ and is present on every host by construction. Runtime-hook systems can only
7
+ enforce where the host exposes a tool lifecycle — a minority of hosts.
8
+
9
+ This is a **structural** claim, not a behaviour claim. See the honesty boundary
10
+ at the end.
11
+
12
+ ## The two enforcement layers (and their reach)
13
+
14
+ | Layer | How it works | Reach |
15
+ |---|---|---|
16
+ | **Compile-time (config space)** | Rules + Iron Laws are compiled into each host's native instruction format at projection time (`.cursorrules`, `.windsurfrules`, `copilot-instructions.md`, `GEMINI.md`, Claude/Augment native rule dirs). | **Every projection target.** |
17
+ | **Runtime hooks** | On hosts exposing a tool lifecycle (`PreToolUse`/`PostToolUse`/`Stop`), a guard can deterministically block a call (e.g. `block_no_verify`). | **Hook-capable hosts only** (~2 of the supported set). |
18
+
19
+ Full per-host breakdown: [`enforcement-by-host.md`](enforcement-by-host.md).
20
+
21
+ The point: a runtime-first competitor's strongest enforcement simply **does not
22
+ exist** on a host that has no hook surface. The compile-time layer is there on
23
+ *that same host* because it rides in the config the host already reads.
24
+
25
+ ## Governance in action — three concrete traces
26
+
27
+ ### 1. A safety-floor rule survives projection into a static host
28
+ `commit-policy` (`tier: safety-floor`) is compiled into Cursor's `.cursorrules`
29
+ and Copilot's `copilot-instructions.md`. The host has no hook to block a
30
+ `git commit`, but the constraint is **in the instruction file the host loads at
31
+ session start** — present by construction, not dependent on a runtime primitive
32
+ the host lacks (a static host gives it no runtime attention guarantee, but a
33
+ runtime-only system gives it nothing at all here). A runtime-only system contributes *nothing* on these hosts.
34
+
35
+ ### 2. Intent routing, not a 500-artefact dump
36
+ A prompt does not load all 258 skills. `dist/router.json` (88 rules, intent →
37
+ skill, keyword/phrase/path triggers) carries the right skill on intent. The
38
+ governance value is *selection discipline* — the host gets the relevant rule
39
+ set, not the whole catalogue as context ballast.
40
+
41
+ ### 3. The enforcement spectrum is explicit, not implied
42
+ On Claude Code / Cline (MCP) the safety-floor constraints get **both** layers:
43
+ compile-time instruction **and** a deterministic runtime block. On Cursor /
44
+ Windsurf / Copilot / Gemini they get the compile-time layer alone. We state
45
+ which host gets which — we never imply "deterministic everywhere".
46
+
47
+ ## What this is NOT (honesty boundary)
48
+
49
+ - **Not "un-strippable".** A human can edit any projected file. The claim is
50
+ *present-by-construction on every host* — a host's runtime cannot selectively
51
+ ignore it the way it ignores an absent runtime hook.
52
+ - **Not a measured behaviour-change claim.** A length-controlled A/B
53
+ (hardened-constraint blocks vs a length-matched placebo) measured **honest-null
54
+ at the discipline ceiling** — the cooperative rules already catch the test
55
+ traps, so *adding emphasis* changed nothing measurable. The moat is that
56
+ governance is **present and portable across hosts**, not that more governance
57
+ prose provably changes a model's behaviour.
58
+ - **Not deterministic on static hosts.** Compile-time governance is
59
+ model-cooperative; only hook-capable hosts get a hard block.
60
+
61
+ ## Why it's hard to copy
62
+
63
+ A runtime-first system would have to re-author its governance to live in
64
+ config space and project into every host's native format — i.e. become a
65
+ content+governance layer — to match the reach. That is a different architecture,
66
+ not a feature bolt-on. Meanwhile this package adds runtime hooks as an opt-in
67
+ *superset* where hosts allow it, without depending on them for the floor.
68
+
69
+ ## See also
70
+
71
+ - [`enforcement-by-host.md`](enforcement-by-host.md) — per-host enforcement mechanism.
72
+ - [`capability-matrix.md`](capability-matrix.md) — which artifact type projects to which host.
@@ -26,6 +26,7 @@ hierarchy, reduced-motion, focus indicator) is in `lint_design_quality`
26
26
  |---|---|---|
27
27
  | **Guidance** | Subjective taste — Visual / Typography / Color / Layout / Copy patterns, override judgment, the originality self-test | this catalog |
28
28
  | **Enforcement** | Objective floors — Q1–Q6 (WCAG contrast, font-size, line-length, reduced-motion, heading hierarchy, focus indicator) as exit-code-2 CI | `lint_design_quality` |
29
+ | **Detection** | Deterministic *aesthetic-tell* flags (P0–P3, rebuttable via DESIGN.md) — the pattern-detectable subset of the entries below | `lint_design_slop` |
29
30
  | **Audit method** | HOW to test WCAG (keyboard nav, ARIA, screen-reader procedures) | `accessibility-auditor` |
30
31
  | **Stack manifestation** | Concrete Tailwind class / hex bans, shadcn defaults | `tailwind-engineer`, `react-shadcn-ui` (per `framework-neutrality-in-generic-skills`) |
31
32
  | **Register** | Brand mode vs product mode — which patterns apply | `docs/guidelines/design-modes.md` |
@@ -34,6 +35,31 @@ This catalog is the canonical *index*; it states the pattern stack-agnostically.
34
35
  The stack-specific bans live in the apply skills and link back here by entry ID.
35
36
  The objective floors are cited from the linter, never re-eyeballed by an agent.
36
37
 
38
+ **Deterministic detector backing** (`lint_design_slop`, dependency-free, zero
39
+ runtime token cost): a pattern-detectable subset of these entries now has a
40
+ deterministic rule that `design-review` cites instead of re-deriving — `V1`,
41
+ `V3`, `V6`, `C2`, `C5`, `T4`, `T6`, `T7`, `L4`, `L8`, `M2`, `M4`, `CP1`, `CP2`,
42
+ plus the **Consistency Locks & layout caps** `V8`, `C6`, `L9`, `L10`
43
+ (rule ids `slop-<id>-*`, registry in `src/scripts/design_slop_rules.ts`). These
44
+ are **flags / rebuttable presumptions**, never hard blocks — a consumer
45
+ `DESIGN.md` that declares the pattern as intentional suppresses the flag. The
46
+ entries needing structural or aesthetic judgment (e.g. `T3` icon-tile stack,
47
+ `L2` three-identical-card grid, `V2` glassmorphism intent, `C7` theme-inversion)
48
+ stay agent-judgment-only and are deliberately **not** in the detector.
49
+
50
+ ### Consistency Locks & layout caps (taste-dials roadmap)
51
+
52
+ Within-project invariants + repetition caps. Override = declare the value in
53
+ `DESIGN.md` (`## Consistency Locks` / a brutalist or uniform-grid style note).
54
+
55
+ | # | Pattern | Why it reads as AI-generated | Override condition |
56
+ |---|---|---|---|
57
+ | V8 | Shape Lock — ≥ 4 distinct corner-radius scales in one surface | A fragmented radius system reads as no shape decision | Declare the radius scale in `DESIGN.md`; a genuine multi-tier scale documents each tier |
58
+ | C6 | Colour Lock — ≥ 3 distinct saturated accent hue families | No single accent identity; reads as default-palette sprawl | Brand genuinely uses a multi-accent system, declared in `DESIGN.md` / `brand-to-tokens` |
59
+ | C7 | Theme inversion — a light island mid-dark surface (or vice versa) not driven by the theme system | Mid-surface inversion breaks the theme rhythm | Intentional inverted callout with a documented reason — **judgment-only**, design-review calls it (deterministic detection is unreliable) |
60
+ | L9 | Section monotony — ≥ 8 sections, < 4 distinct layout families | Uniform section rhythm collapses scannability | Deliberately uniform grid (e.g. a gallery), declared as such |
61
+ | L10 | Zigzag — > 2 consecutive image-left/text-right two-column sections | The alternating-zigzag landing-page tell | A genuinely sequenced walkthrough where alternation aids comprehension |
62
+
37
63
  ---
38
64
 
39
65
  ## Visual
@@ -0,0 +1,62 @@
1
+ # Design Canon — named-systems grounding index
2
+
3
+ > Lazy-loaded reference. Pull this when a brief names (or a project's
4
+ > `components.json`/deps signal) a published design system, type, or colour
5
+ > reference — so the design is **grounded in named canon**, not improvised.
6
+ > Used by `design-intelligence`; cross-linked from `typography-system`,
7
+ > `iconography`, `design-tokens`, `brand-to-tokens`, `icon-consistency`.
8
+
9
+ This is a thin **index** — names, one-line summaries, and a "pull this when"
10
+ trigger. It deliberately does **not** bundle the multi-megabyte specs; fetch
11
+ the live spec only when the trigger fires (the canonical URLs below are the
12
+ fetch targets). Precedence (per `brand-source-of-truth`): **consumer brand
13
+ tokens > confirmed session decisions > named canon (this index) > generated
14
+ corpus.** Canon is a gap-filler, never an override of a registered brand value.
15
+
16
+ ## Design systems
17
+
18
+ | System | Signature traits (one-line) | Pull this when |
19
+ |---|---|---|
20
+ | **Material 3** (Google) | Dynamic colour from a seed (HCT), tonal palettes, elevation by tint not shadow, expressive motion (emphasized easing). | brief says "Material" / "Android"; deps `@mui/material`, `@angular/material`, `react-native-paper`. Spec: m3.material.io |
21
+ | **Apple HIG** | Platform-native clarity/deference/depth, SF type + Dynamic Type, system materials/vibrancy, restrained motion, large touch targets. | brief targets iOS/iPadOS/macOS/visionOS, "native Apple feel". Spec: developer.apple.com/design |
22
+ | **Fluent 2** (Microsoft) | Neutral-forward, acrylic/reveal materials, Segoe/Fluent type ramp, depth via subtle shadow, enterprise density. | brief says "Fluent"/"Windows"/"Office"; deps `@fluentui/*`. Spec: fluent2.microsoft.design |
23
+ | **Carbon** (IBM) | 2px grid, IBM Plex type, strong data-viz system, productive vs expressive themes, accessibility-first. | brief says "Carbon"/"IBM"/data-heavy enterprise; deps `@carbon/*`. Spec: carbondesignsystem.com |
24
+ | **Ant Design** | Dense enterprise/admin, comprehensive component set, token-based theming (v5), wireframe-driven. | brief says "Ant"/admin console/CRUD-heavy; deps `antd`. Spec: ant.design |
25
+ | **Atlassian** | Product-suite consistency, ADS tokens, calm neutral + bold primary, strong empty/loading patterns. | brief says "Atlassian"/Jira-like; deps `@atlaskit/*`. Spec: atlassian.design |
26
+
27
+ Grounding rule: surface the matching one-liner, then **offer to pull the live
28
+ spec** before committing to the system's conventions. Never reproduce a brand
29
+ book's marks; ground against the *system's* documented principles.
30
+
31
+ ## Typography craft (foundry + theory)
32
+
33
+ A one-line index — point `typography-system` here; do not inline the content.
34
+
35
+ - **Theory:** Butterick's *Practical Typography* (practicaltypography.com — line-length, measure, hierarchy rules), *Thinking with Type*.
36
+ - **Calibration / "in use":** Typewolf, Fonts In Use (real pairings, not defaults).
37
+ - **Quality foundries** (beyond the AI-default fonts the anti-slop catalog T7 flags): Klim, Lineto, Colophon, Commercial Type, Grilli Type; libre: League of Movable Type, Google Fonts (chosen deliberately, not reflexively).
38
+ - Pull when: a brief calls for a distinctive type voice, or to escape the Inter/Roboto/Geist default.
39
+
40
+ ## Colour references
41
+
42
+ Names + when-to-use — not the palettes themselves.
43
+
44
+ - **Accessibility / contrast:** WCAG contrast checkers (e.g. APCA, WebAIM) — verify text/non-text contrast (ties to `lint_design_quality` Q1 + anti-slop C4). Pull on any colour decision that must clear AA.
45
+ - **Culturally-specific palettes:** Nippon Colors (traditional Japanese), Chinese Colors — pull when the brand/audience is culturally situated and a Western default palette would read as generic.
46
+ - **Systematic palette tools:** the design systems above ship their own token palettes — prefer the system's palette over an ad-hoc picker when a system is in play.
47
+
48
+ ## Icon systems
49
+
50
+ Each design system ships a matching icon set — prefer it when that system is in
51
+ play (consistency over novelty, per `icon-consistency`): **Material Symbols**
52
+ (Material 3, variable weight/fill/grade), **SF Symbols** (Apple HIG, weight-
53
+ matched to SF text), **Fluent UI System Icons** (Fluent), **Carbon icons**
54
+ (Carbon). Outside a named system, the `iconography` skill resolves a single
55
+ Iconify set (Lucide / Phosphor / Heroicons / Tabler) — pick one and stay in it.
56
+ Pull when: choosing or auditing an icon library.
57
+
58
+ ## See also
59
+
60
+ - `docs/guidelines/design-antipatterns.md` — the anti-slop catalog (T7 default-font tell, C1/C5 palette tells) this canon helps you escape.
61
+ - `brand-source-of-truth` / `brand-consistency` — the precedence this index obeys.
62
+ - `design-intelligence` — the consumer that surfaces these on a brief signal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
- "version": "7.3.0",
3
+ "version": "7.5.0",
4
4
  "description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -81,6 +81,7 @@
81
81
  "@typescript-eslint/parser": "^8.59.4",
82
82
  "eslint": "^9.39.4",
83
83
  "happy-dom": "^20.9.0",
84
+ "js-tiktoken": "^1.0.21",
84
85
  "tsx": "^4.22.3",
85
86
  "typescript": "^5.9.3",
86
87
  "vite": "^5.4.21",
@@ -722,6 +722,27 @@ hooks:
722
722
  injection_scan:
723
723
  enabled: false
724
724
 
725
+ # PreToolUse RTK-wrap nudge (token-saving Phase 3). Default-OFF. When enabled
726
+ # AND `rtk` is on PATH (a live probe — not a self-reported flag), warns (exit
727
+ # 2, never blocks) "re-run wrapped with rtk" before a single verbose CLI
728
+ # command (git/npm/cargo/docker/…), so verbose output is filtered for 60–90%
729
+ # token saving. Skips completeness-critical / piped / compound commands and
730
+ # `git diff`. No-op when rtk is absent. Opt in once you have rtk installed.
731
+ rtk_wrap:
732
+ enabled: false
733
+
734
+ # PreToolUse anti-slop nudge (road-to-anti-slop-detector Phase 3). Default-OFF.
735
+ # When enabled, runs the lint_design_slop registry against about-to-be-written
736
+ # UI content (html/css/jsx/…) and WARNS (exit 2, never blocks) on P0/P1
737
+ # aesthetic tells — the side-stripe signature, gradient-text, magic z-index,
738
+ # etc. Flags are rebuttable: declare intent in DESIGN.md, design-slop-disable,
739
+ # or proceed. Anti-loop: a file::rule signature surfaced 3x goes silent so it
740
+ # never traps the agent. The universal gate is the lint_design_slop linter/CI;
741
+ # this hook is a host-limited convenience layer (pre_tool_use fires on ~2-3
742
+ # hosts). Opt in once you trust the signal-to-noise on your UI.
743
+ design_slop:
744
+ enabled: false
745
+
725
746
  # --- Decision engine ---
726
747
  #
727
748
  # Controllable gates layered over the observability surface. Absent
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `agent-config analyze-session` — post-session report from on-disk runtime
4
+ * state.
5
+ *
6
+ * Read-only diagnostic. No daemon, no network, no model calls. It reads two
7
+ * real data sources the work-engine / context-hygiene hook leave behind and
8
+ * prints a deterministic Markdown report:
9
+ *
10
+ * 1. the work_engine state envelope `.work-state.json` (version 1) — loaded
11
+ * via `load_state` from `explain_last/state_loader.ts`. Fields read:
12
+ * - `changes` — files touched (array of {kind, stack, file, summary}).
13
+ * - `outcomes` — directive → "success" / "blocked" / … .
14
+ * - `halts` — array of recorded halts.
15
+ * 2. `agents/runtime/state/context-hygiene.json` — fields read:
16
+ * - `tool_calls` (int)
17
+ * - `consecutive_same_tool`(int)
18
+ * - `loop_detected` (bool)
19
+ *
20
+ * There is NO general token/cost data source in this package (only
21
+ * video-specific telemetry), so the report deliberately does NOT report
22
+ * tokens or cost — it prints a single honest "not tracked" line instead.
23
+ *
24
+ * Exit codes (mirroring `cmd_explain`'s `last` subject):
25
+ * 0 report rendered (even when a source file is absent — graceful note).
26
+ * 1 the work-state file is missing or unreadable.
27
+ * 2 invocation error (bad project root, bad --state-file path).
28
+ */
29
+
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import process from 'node:process';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
+
35
+ import { ProjectRootError, resolve_project_root } from '../_lib/agent_settings.js';
36
+ import { StateLoadError, load_state } from './explain_last/state_loader.js';
37
+
38
+ type Dict = Record<string, unknown>;
39
+
40
+ const _HERE = fileURLToPath(import.meta.url);
41
+
42
+ const CONTEXT_HYGIENE_RELATIVE = path.join(
43
+ 'agents',
44
+ 'runtime',
45
+ 'state',
46
+ 'context-hygiene.json',
47
+ );
48
+
49
+ /** Python `raise SystemExit(code)` analogue — propagates an explicit code. */
50
+ class SystemExitError extends Error {
51
+ constructor(public readonly code: number) {
52
+ super(`system-exit-${code}`);
53
+ }
54
+ }
55
+
56
+ /** argparse-style usage / help exit (code 2 / 0). Caught at the CLI entry. */
57
+ class ArgparseExit extends Error {
58
+ constructor(public readonly code: number) {
59
+ super(`argparse-exit-${code}`);
60
+ }
61
+ }
62
+
63
+ function eprint(line = ''): void {
64
+ process.stderr.write(line + '\n');
65
+ }
66
+
67
+ function isPlainObject(value: unknown): value is Dict {
68
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ }
70
+
71
+ function _resolve_root(arg: string | null): string {
72
+ try {
73
+ const [root] = resolve_project_root(arg, { cwd: process.cwd() });
74
+ return root;
75
+ } catch (exc) {
76
+ if (exc instanceof ProjectRootError) {
77
+ eprint(`❌ analyze-session: ${exc.message}`);
78
+ throw new SystemExitError(2);
79
+ }
80
+ throw exc;
81
+ }
82
+ }
83
+
84
+ /** `Path.relative_to(base)` — returns null when `p` is not under `base`. */
85
+ function relativeToOrNull(p: string, base: string): string | null {
86
+ const rel = path.relative(base, p);
87
+ if (rel === '') return rel;
88
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
89
+ return null;
90
+ }
91
+ return rel;
92
+ }
93
+
94
+ /**
95
+ * Read the context-hygiene snapshot. Returns the parsed object, or `null`
96
+ * when the file is absent / unreadable / not an object — the caller renders
97
+ * a graceful "not available" note rather than failing the whole report.
98
+ */
99
+ function _load_context_hygiene(project_root: string): Dict | null {
100
+ const p = path.join(project_root, CONTEXT_HYGIENE_RELATIVE);
101
+ if (!fs.existsSync(p)) {
102
+ return null;
103
+ }
104
+ let text: string;
105
+ try {
106
+ text = fs.readFileSync(p, 'utf-8');
107
+ } catch {
108
+ return null;
109
+ }
110
+ let payload: unknown;
111
+ try {
112
+ payload = JSON.parse(text);
113
+ } catch {
114
+ return null;
115
+ }
116
+ return isPlainObject(payload) ? payload : null;
117
+ }
118
+
119
+ /** Render `value` as an integer string, or `'?'` when not a finite number. */
120
+ function intOrUnknown(value: unknown): string {
121
+ if (typeof value === 'number' && Number.isFinite(value)) {
122
+ return String(Math.trunc(value));
123
+ }
124
+ return '?';
125
+ }
126
+
127
+ /**
128
+ * Build the deterministic Markdown report from already-loaded state.
129
+ *
130
+ * Pure function — no I/O, no clock, no environment. Both inputs come from the
131
+ * caller so the report is unit-testable in isolation.
132
+ *
133
+ * @param state the parsed `.work-state.json` (version 1).
134
+ * @param hygiene the parsed context-hygiene snapshot, or `null` when absent.
135
+ */
136
+ export function render_report(state: Dict, hygiene: Dict | null): string {
137
+ const lines: string[] = [];
138
+ lines.push('# Session analysis');
139
+ lines.push('');
140
+
141
+ // --- Files touched (from state.changes) ---
142
+ const changes = Array.isArray(state['changes']) ? (state['changes'] as unknown[]) : [];
143
+ lines.push(`## Files touched (${changes.length})`);
144
+ lines.push('');
145
+ if (changes.length === 0) {
146
+ lines.push('_No files recorded as changed._');
147
+ } else {
148
+ for (const change of changes) {
149
+ if (!isPlainObject(change)) continue;
150
+ const file = typeof change['file'] === 'string' ? change['file'] : '(unknown file)';
151
+ const summary =
152
+ typeof change['summary'] === 'string' && change['summary'].length > 0
153
+ ? ` — ${change['summary']}`
154
+ : '';
155
+ lines.push(`- \`${file}\`${summary}`);
156
+ }
157
+ }
158
+ lines.push('');
159
+
160
+ // --- Per-directive outcomes (from state.outcomes) ---
161
+ const outcomes = isPlainObject(state['outcomes']) ? (state['outcomes'] as Dict) : {};
162
+ const directives = Object.keys(outcomes).sort(); // deterministic order
163
+ const blocked = directives.filter((d) => outcomes[d] === 'blocked');
164
+ lines.push(`## Outcomes (${directives.length})`);
165
+ lines.push('');
166
+ if (directives.length === 0) {
167
+ lines.push('_No directive outcomes recorded._');
168
+ } else {
169
+ for (const directive of directives) {
170
+ const result = outcomes[directive];
171
+ const marker = result === 'blocked' ? '⚠️ ' : '';
172
+ lines.push(`- ${marker}\`${directive}\`: ${String(result)}`);
173
+ }
174
+ }
175
+ lines.push('');
176
+ if (blocked.length > 0) {
177
+ lines.push(`**Blocked directives (${blocked.length}):** ${blocked.join(', ')}`);
178
+ lines.push('');
179
+ }
180
+
181
+ // --- Halts (from state.halts) ---
182
+ const halts = Array.isArray(state['halts']) ? (state['halts'] as unknown[]) : [];
183
+ lines.push(`## Halts (${halts.length})`);
184
+ lines.push('');
185
+ if (halts.length === 0) {
186
+ lines.push('_No halts recorded._');
187
+ } else {
188
+ lines.push(`This session recorded ${halts.length} halt(s).`);
189
+ }
190
+ lines.push('');
191
+
192
+ // --- Tool activity (from context-hygiene) ---
193
+ lines.push('## Tool activity');
194
+ lines.push('');
195
+ if (hygiene === null) {
196
+ lines.push('_Context-hygiene snapshot not available._');
197
+ } else {
198
+ const toolCalls = intOrUnknown(hygiene['tool_calls']);
199
+ const consecutive = intOrUnknown(hygiene['consecutive_same_tool']);
200
+ const loop = hygiene['loop_detected'] === true;
201
+ lines.push(`- Tool calls: ${toolCalls}`);
202
+ lines.push(`- Consecutive same-tool calls: ${consecutive}`);
203
+ lines.push(`- Loop detected: ${loop ? 'yes' : 'no'}`);
204
+ }
205
+ lines.push('');
206
+
207
+ // --- Honest token/cost note (no per-session source in this package) ---
208
+ lines.push('## Token / cost');
209
+ lines.push('');
210
+ lines.push('Token/cost: not tracked (no per-session source).');
211
+ lines.push('');
212
+
213
+ return lines.join('\n');
214
+ }
215
+
216
+ function _analyze_session(project_root: string, state_file: string | null): number {
217
+ const target_state = state_file || path.join(project_root, '.work-state.json');
218
+ let state: Dict;
219
+ try {
220
+ state = load_state(target_state) as Dict;
221
+ } catch (exc) {
222
+ if (exc instanceof StateLoadError) {
223
+ // Never print absolute paths in errors (username leakage).
224
+ const rel = relativeToOrNull(target_state, project_root);
225
+ const shown = rel !== null ? rel : path.basename(target_state);
226
+ const msg = exc.message.split(target_state).join(shown);
227
+ eprint(`❌ analyze-session: ${msg}`);
228
+ return exc.exitCode;
229
+ }
230
+ throw exc;
231
+ }
232
+ const hygiene = _load_context_hygiene(project_root);
233
+ process.stdout.write(render_report(state, hygiene));
234
+ return 0;
235
+ }
236
+
237
+ interface Opts {
238
+ project: string | null;
239
+ state_file: string | null;
240
+ }
241
+
242
+ const PROG = 'agent-config analyze-session';
243
+ const USAGE = `usage: ${PROG} [-h] [--project PROJECT] [--state-file STATE_FILE]\n`;
244
+
245
+ const _HELP = `\
246
+ usage: ${PROG} [--project PATH] [--state-file PATH]
247
+
248
+ Print a post-session report from on-disk runtime state. Read-only;
249
+ no daemon, no network, no model calls.
250
+
251
+ Sources read:
252
+ .work-state.json (version 1)
253
+ · changes — files touched
254
+ · outcomes — per-directive success / blocked
255
+ · halts — recorded halts
256
+ agents/runtime/state/context-hygiene.json
257
+ · tool_calls · consecutive_same_tool · loop_detected
258
+
259
+ Token/cost is NOT reported — this package has no per-session token
260
+ source (only video-specific telemetry).
261
+
262
+ options:
263
+ --project PATH project root (defaults to anchor walk from cwd)
264
+ --state-file PATH .work-state.json path (default <root>/.work-state.json)
265
+ -h, --help show this help and exit
266
+
267
+ exit codes:
268
+ 0 report rendered (a missing source file is noted, not fatal)
269
+ 1 no recent run found (state file missing or unreadable)
270
+ 2 invocation error (bad project root, bad --state-file path)
271
+ `;
272
+
273
+ function _argError(msg: string): never {
274
+ process.stderr.write(USAGE);
275
+ process.stderr.write(`${PROG}: error: ${msg}\n`);
276
+ throw new ArgparseExit(2);
277
+ }
278
+
279
+ function _parse(argv: string[]): Opts {
280
+ const opts: Opts = { project: null, state_file: null };
281
+ let i = 0;
282
+ while (i < argv.length) {
283
+ const a = argv[i] as string;
284
+ if (a === '-h' || a === '--help') {
285
+ process.stdout.write(_HELP);
286
+ throw new ArgparseExit(0);
287
+ }
288
+ const eq = a.startsWith('--') ? a.indexOf('=') : -1;
289
+ const flag = eq >= 0 ? a.slice(0, eq) : a;
290
+ const inlineVal = eq >= 0 ? a.slice(eq + 1) : null;
291
+ if (flag === '--project' || flag === '--state-file') {
292
+ const dest = flag === '--project' ? 'project' : 'state_file';
293
+ let value: string;
294
+ if (inlineVal !== null) {
295
+ value = inlineVal;
296
+ } else {
297
+ if (i + 1 >= argv.length) {
298
+ _argError(`argument ${flag}: expected one argument`);
299
+ }
300
+ value = argv[i + 1] as string;
301
+ i += 1;
302
+ }
303
+ opts[dest] = value;
304
+ i += 1;
305
+ continue;
306
+ }
307
+ _argError(`unrecognized arguments: ${a}`);
308
+ }
309
+ return opts;
310
+ }
311
+
312
+ export function main(argv: string[] | null = null): number {
313
+ const argv_list = argv !== null ? Array.from(argv) : process.argv.slice(2);
314
+ const opts = _parse(argv_list);
315
+ let project_root: string;
316
+ try {
317
+ project_root = _resolve_root(opts.project);
318
+ } catch (exc) {
319
+ if (exc instanceof SystemExitError) {
320
+ return exc.code;
321
+ }
322
+ throw exc;
323
+ }
324
+ return _analyze_session(project_root, opts.state_file);
325
+ }
326
+
327
+ // --- CLI entry ---
328
+
329
+ const _isCliEntry =
330
+ process.argv[1] !== undefined &&
331
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
332
+ if (_isCliEntry || process.argv[1] === _HERE) {
333
+ try {
334
+ process.exitCode = main(process.argv.slice(2));
335
+ } catch (e) {
336
+ if (e instanceof ArgparseExit) {
337
+ process.exitCode = e.code;
338
+ } else if (e instanceof SystemExitError) {
339
+ process.exitCode = e.code;
340
+ } else {
341
+ throw e;
342
+ }
343
+ }
344
+ }
345
+
346
+ export { _parse, _resolve_root, _load_context_hygiene, _analyze_session, ArgparseExit, SystemExitError };
347
+ export type { Opts };
@@ -933,6 +933,13 @@ cmd_explain() {
933
933
  exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_explain.ts" "$@"
934
934
  }
935
935
 
936
+ # `agent-config analyze-session` — read-only post-session report from on-disk
937
+ # runtime state (.work-state.json + context-hygiene.json). No daemon, no
938
+ # network, no model calls. See scripts/_cli/cmd_analyze_session.ts.
939
+ cmd_analyze_session() {
940
+ exec_ts "$PACKAGE_ROOT/src/scripts/_cli/cmd_analyze_session.ts" "$@"
941
+ }
942
+
936
943
  main() {
937
944
  local cmd="${1-}"
938
945
  [[ $# -gt 0 ]] && shift || true
@@ -991,6 +998,7 @@ main() {
991
998
  doctor) cmd_doctor "$@" ;;
992
999
  versions) cmd_versions "$@" ;;
993
1000
  explain) cmd_explain "$@" ;;
1001
+ analyze-session) cmd_analyze_session "$@" ;;
994
1002
  help|--help|-h|"")
995
1003
  # Optional `--tier=0|1|all` filter (default 0).
996
1004
  local tier_arg="0"