@event4u/agent-config 9.16.0 → 9.18.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +80 -130
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/challenge-me/vision.md +5 -0
  7. package/dist/agent-src/commands/commit.md +1 -1
  8. package/dist/agent-src/commands/condense.md +19 -64
  9. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  10. package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
  11. package/dist/agent-src/commands/feature/plan.md +20 -0
  12. package/dist/agent-src/commands/feature/roadmap.md +19 -0
  13. package/dist/agent-src/commands/memory/promote.md +14 -1
  14. package/dist/agent-src/commands/mode.md +9 -5
  15. package/dist/agent-src/commands/optimize/deep.md +72 -11
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/roadmap/create.md +29 -0
  18. package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
  19. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  20. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  21. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  22. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  23. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  24. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  25. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
  26. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  27. package/dist/agent-src/profiles/agency.yml +3 -3
  28. package/dist/agent-src/profiles/content_creator.yml +2 -2
  29. package/dist/agent-src/profiles/developer.yml +2 -2
  30. package/dist/agent-src/profiles/finance.yml +1 -1
  31. package/dist/agent-src/profiles/founder.yml +2 -2
  32. package/dist/agent-src/profiles/ops.yml +2 -2
  33. package/dist/agent-src/rules/active-remediation.md +2 -0
  34. package/dist/agent-src/rules/architecture.md +4 -2
  35. package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
  36. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  37. package/dist/agent-src/rules/brand-consistency.md +10 -62
  38. package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
  39. package/dist/agent-src/rules/broken-access-control.md +4 -0
  40. package/dist/agent-src/rules/cli-output-handling.md +5 -0
  41. package/dist/agent-src/rules/code-comment-discipline.md +3 -0
  42. package/dist/agent-src/rules/commit-conventions.md +2 -0
  43. package/dist/agent-src/rules/context-hygiene.md +3 -2
  44. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  45. package/dist/agent-src/rules/design-fidelity.md +2 -0
  46. package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
  47. package/dist/agent-src/rules/docker-commands.md +4 -0
  48. package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
  49. package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
  50. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  51. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  52. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
  53. package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
  54. package/dist/agent-src/rules/improve-before-implement.md +4 -0
  55. package/dist/agent-src/rules/laravel-routing.md +4 -0
  56. package/dist/agent-src/rules/legal-safety-floor.md +3 -0
  57. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  58. package/dist/agent-src/rules/media-governance-routing.md +5 -0
  59. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  60. package/dist/agent-src/rules/no-roadmap-references.md +2 -0
  61. package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
  62. package/dist/agent-src/rules/output-discipline.md +9 -2
  63. package/dist/agent-src/rules/php-coding.md +2 -0
  64. package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
  65. package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
  66. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
  67. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  68. package/dist/agent-src/rules/rule-type-governance.md +2 -0
  69. package/dist/agent-src/rules/scale-discipline.md +3 -0
  70. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  71. package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
  72. package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
  73. package/dist/agent-src/rules/skill-quality.md +2 -0
  74. package/dist/agent-src/rules/source-confidentiality.md +4 -0
  75. package/dist/agent-src/rules/source-discovery-gate.md +3 -0
  76. package/dist/agent-src/rules/source-of-truth.md +2 -0
  77. package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
  78. package/dist/agent-src/rules/think-before-action.md +5 -0
  79. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  80. package/dist/agent-src/rules/ui-audit-gate.md +2 -0
  81. package/dist/agent-src/rules/user-interaction.md +3 -4
  82. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  83. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  84. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  85. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  86. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  87. package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
  88. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
  89. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
  90. package/dist/agent-src/skills/design-review/SKILL.md +4 -141
  91. package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
  92. package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
  93. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
  94. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
  95. package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
  96. package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
  97. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
  98. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  99. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  100. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  101. package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
  102. package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
  103. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  104. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  105. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  106. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  107. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  108. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  109. package/dist/agent-src/templates/agent-settings.md +13 -15
  110. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  111. package/dist/agent-src/templates/persona.md +2 -2
  112. package/dist/agent-src/templates/roadmaps.md +13 -1
  113. package/dist/agent-src/templates/rule.md +1 -1
  114. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  115. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  116. package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  117. package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  118. package/dist/agent-src/templates/skill.md +1 -1
  119. package/dist/cli/registry.js +3 -0
  120. package/dist/cli/registry.js.map +1 -1
  121. package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
  122. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  123. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
  124. package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
  125. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  126. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  127. package/dist/cli-delegate/cmd_conformance.js +4 -3
  128. package/dist/cli-delegate/cmd_converge.js +3 -3
  129. package/dist/cli-delegate/cmd_doctor.js +3 -2
  130. package/dist/cli-delegate/cmd_explain.js +36 -36
  131. package/dist/cli-delegate/cmd_export.js +4 -4
  132. package/dist/cli-delegate/cmd_handoff.js +1169 -0
  133. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  134. package/dist/cli-delegate/cmd_migrate.js +2 -2
  135. package/dist/cli-delegate/cmd_prune.js +4 -4
  136. package/dist/cli-delegate/cmd_route_audit.js +249 -0
  137. package/dist/cli-delegate/cmd_route_explain.js +285 -0
  138. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  139. package/dist/cli-delegate/cmd_sync.js +4 -4
  140. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  141. package/dist/cli-delegate/cmd_update.js +3 -3
  142. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  143. package/dist/cli-delegate/cmd_validate.js +4 -4
  144. package/dist/cli-delegate/cmd_versions.js +3 -3
  145. package/dist/discovery/deprecation-report.md +1 -1
  146. package/dist/discovery/discovery-manifest.json +84 -84
  147. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  148. package/dist/discovery/discovery-manifest.summary.md +1 -1
  149. package/dist/discovery/orphan-report.md +1 -1
  150. package/dist/discovery/packs.json +2 -2
  151. package/dist/discovery/trust-report.md +1 -1
  152. package/dist/discovery/workspaces.json +2 -2
  153. package/dist/hooks/dispatch.js +602 -507
  154. package/dist/install/install.mjs +34 -0
  155. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  156. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  157. package/dist/mcp/registry-manifest.json +4 -4
  158. package/dist/mcp/server.json +2 -2
  159. package/dist/mcp/server.mjs +25 -3
  160. package/dist/router.json +1 -1
  161. package/dist/scripts/_lib/agent_src.js +39 -6
  162. package/dist/scripts/_lib/agent_src.js.map +1 -1
  163. package/dist/server/schemas/settings.js +25 -0
  164. package/dist/server/schemas/settings.js.map +1 -1
  165. package/docs/guidelines/abstraction-thresholds.md +53 -0
  166. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  167. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  168. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  169. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  170. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  171. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  172. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  173. package/llms.txt +1 -0
  174. package/package.json +1 -1
  175. package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
  176. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
  177. package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
  178. package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
  179. package/src/config/agent-settings.template.yml +68 -0
  180. package/src/config/evaluator-budgets.json +8 -11
  181. package/src/config/gate-coverage.yml +70 -0
  182. package/src/scripts/_cli/cmd_explain.ts +41 -36
  183. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  184. package/src/scripts/_cli/cmd_route_audit.ts +290 -0
  185. package/src/scripts/_cli/cmd_route_explain.ts +269 -0
  186. package/src/scripts/_cli/handoff_generate.ts +453 -0
  187. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  188. package/src/scripts/_dispatch.bash +32 -0
  189. package/src/scripts/_lib/agent_src.ts +36 -6
  190. package/src/scripts/_lib/git_env.ts +40 -0
  191. package/src/scripts/_lib/md_table.ts +27 -0
  192. package/src/scripts/_lib/planning_settings.ts +43 -0
  193. package/src/scripts/_lib/release_material.ts +156 -0
  194. package/src/scripts/_lib/router_match.ts +301 -0
  195. package/src/scripts/annotate_r1_outcomes.ts +258 -0
  196. package/src/scripts/check_completion_review.ts +1140 -0
  197. package/src/scripts/check_condensation.ts +2 -1
  198. package/src/scripts/check_finding_dispositions.ts +278 -0
  199. package/src/scripts/check_pack_size.ts +4 -3
  200. package/src/scripts/check_references.ts +1 -0
  201. package/src/scripts/check_release_highlights.ts +300 -0
  202. package/src/scripts/check_release_pr_shape.ts +4 -0
  203. package/src/scripts/check_release_surface_equality.ts +132 -0
  204. package/src/scripts/check_reply_consistency.ts +187 -138
  205. package/src/scripts/compile_router.ts +20 -3
  206. package/src/scripts/dispatch_r2_reviewer.ts +968 -0
  207. package/src/scripts/eval_ui_triviality.ts +156 -0
  208. package/src/scripts/handoff_context_hook.ts +148 -0
  209. package/src/scripts/hook_manifest.yaml +17 -7
  210. package/src/scripts/hooks/concern_registry.ts +2 -0
  211. package/src/scripts/lint_abstraction_thresholds.ts +327 -0
  212. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  213. package/src/scripts/lint_plan_risk_register.ts +931 -0
  214. package/src/scripts/lint_profile_personas.ts +153 -0
  215. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  216. package/src/scripts/lint_trigger_collisions.ts +257 -0
  217. package/src/scripts/release.ts +156 -32
  218. package/src/scripts/release_drill.ts +491 -0
  219. package/src/scripts/router_telemetry.ts +16 -290
  220. package/src/scripts/schemas/command.schema.json +33 -0
  221. package/src/scripts/schemas/rule.schema.json +10 -0
  222. package/src/scripts/self_review_gate.ts +41 -3
  223. package/src/scripts/skill_linter.ts +3 -1
  224. package/src/scripts/smoke/router.sh +1 -1
  225. package/src/scripts/update_counts.ts +60 -12
  226. package/src/server/schemas/settings.ts +41 -0
  227. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `agent-config handoff` — pick a recent session, generate a handoff,
4
+ * seed a fresh session (road-to-agent-handoff-resume Phase 4).
5
+ *
6
+ * Default TTY flow: numbered picker over the unified session list
7
+ * (chat-history primary; Claude-transcript + Codex-session adapters),
8
+ * then the Phase-2 deterministic generator writes
9
+ * `agents/runtime/state/handoff-context.md`, which the Phase-3
10
+ * session_start hook injects into the NEXT session exactly once.
11
+ *
12
+ * Flags:
13
+ * --list print the sessions and exit
14
+ * --json with --list: machine-readable output
15
+ * --session <id> skip the picker (exact id or unique prefix)
16
+ * --print emit the handoff to stdout instead of the state
17
+ * file (legacy copy-paste mode)
18
+ * --launch <host> spawn a fresh interactive host session after
19
+ * generating (claude: hook-seeded; codex: handoff as
20
+ * initial prompt — no hook surface there)
21
+ * --llm narrative-polish seam; v1: clear not-implemented error
22
+ * --root <path> repo root override (default: cwd)
23
+ *
24
+ * Exit codes: 0 ok · 1 no sessions / session not found · 2 invocation error
25
+ */
26
+
27
+ import { spawnSync } from 'node:child_process';
28
+ import * as fs from 'node:fs';
29
+ import process from 'node:process';
30
+ import * as path from 'node:path';
31
+ import { fileURLToPath, pathToFileURL } from 'node:url';
32
+
33
+ import {
34
+ build_handoff,
35
+ handoff_target_path,
36
+ LlmPolishNotImplementedError,
37
+ write_handoff,
38
+ } from './handoff_generate.js';
39
+ import { list_handoff_sessions } from './handoff_sessions.js';
40
+ import type { HandoffSession } from './handoff_sessions.js';
41
+
42
+ const PROG = 'agent-config handoff';
43
+ const USAGE = `usage: ${PROG} [--list] [--json] [--session <id>] [--print] [--launch <host>] [--llm] [--root <path>]`;
44
+
45
+ const _HELP = `${USAGE}
46
+
47
+ Pick a recent session, generate a deterministic handoff, and seed a fresh
48
+ session with it. Without flags (in a TTY): numbered picker, then the handoff
49
+ lands in agents/runtime/state/handoff-context.md and auto-injects into the
50
+ next session on hook-capable hosts.
51
+
52
+ options:
53
+ --list print the recent sessions and exit
54
+ --json with --list: JSON output
55
+ --session <id> generate for this session id (exact or unique prefix)
56
+ --print print the handoff to stdout (no state file)
57
+ --launch <host> start a fresh interactive session (claude | codex)
58
+ --llm optional LLM polish — not implemented in v1
59
+ --root <path> repo root (default: current directory)
60
+ -h, --help show this help
61
+
62
+ exit codes:
63
+ 0 handoff generated (or list printed)
64
+ 1 no sessions found / session id not found
65
+ 2 invocation error (bad flag, non-TTY without --session, unsupported host)
66
+ `;
67
+
68
+ class ArgparseExit extends Error {
69
+ constructor(public readonly code: number) {
70
+ super(`exit ${code}`);
71
+ }
72
+ }
73
+
74
+ // ---------------------------------------------------------------------
75
+ // launch adapter table (v1: claude + codex live, gemini stubbed)
76
+ // ---------------------------------------------------------------------
77
+
78
+ export interface LaunchAdapter {
79
+ supports_launch: boolean;
80
+ /** 'hook' = state file + session_start injection; 'initial-prompt' = argv. */
81
+ seeding: 'hook' | 'initial-prompt';
82
+ build_argv: (handoffText: string) => string[];
83
+ }
84
+
85
+ export const LAUNCH_ADAPTERS: Record<string, LaunchAdapter> = {
86
+ claude: {
87
+ supports_launch: true,
88
+ seeding: 'hook',
89
+ build_argv: () => ['claude'],
90
+ },
91
+ codex: {
92
+ // Codex is a bundle host without hooks — seed via the initial prompt.
93
+ supports_launch: true,
94
+ seeding: 'initial-prompt',
95
+ build_argv: (handoffText: string) => ['codex', handoffText],
96
+ },
97
+ gemini: {
98
+ supports_launch: false,
99
+ seeding: 'hook',
100
+ build_argv: () => ['gemini'],
101
+ },
102
+ };
103
+
104
+ // ---------------------------------------------------------------------
105
+ // options + seams
106
+ // ---------------------------------------------------------------------
107
+
108
+ export interface Opts {
109
+ list: boolean;
110
+ json: boolean;
111
+ session: string | null;
112
+ print: boolean;
113
+ launch: string | null;
114
+ llm: boolean;
115
+ root: string | null;
116
+ }
117
+
118
+ type Runner = (cmd: string[]) => number;
119
+
120
+ export interface MainOptions {
121
+ out?: (text: string) => void;
122
+ err?: (text: string) => void;
123
+ /** TTY availability override (default: stdin AND stdout are TTYs). */
124
+ isTTY?: boolean;
125
+ /** Picker input; returns the raw line or null on read failure. */
126
+ readLine?: (prompt: string) => string | null;
127
+ /** Interactive child spawn (default: spawnSync stdio-inherit). */
128
+ runner?: Runner;
129
+ }
130
+
131
+ function _default_runner(cmd: string[]): number {
132
+ const r = spawnSync(cmd[0] as string, cmd.slice(1), { stdio: 'inherit' });
133
+ if (r.error) {
134
+ process.stderr.write(`${PROG}: cannot run ${cmd[0]}: ${String(r.error)}\n`);
135
+ return 127;
136
+ }
137
+ return r.status ?? 1;
138
+ }
139
+
140
+ /**
141
+ * Synchronous /dev/tty line read (never fd 0 — EAGAIN under go-task
142
+ * `interactive: true` silently auto-aborts a fd-0 prompt).
143
+ */
144
+ function _tty_read_line(prompt: string): string | null {
145
+ try {
146
+ process.stdout.write(prompt);
147
+ const fd = fs.openSync('/dev/tty', 'rs');
148
+ try {
149
+ const buf = Buffer.alloc(1);
150
+ let line = '';
151
+ for (;;) {
152
+ let n = 0;
153
+ try {
154
+ n = fs.readSync(fd, buf, 0, 1, null);
155
+ } catch (exc) {
156
+ const code = (exc as NodeJS.ErrnoException).code;
157
+ if (code === 'EAGAIN' || code === 'EINTR') continue;
158
+ return null;
159
+ }
160
+ if (n === 0) break;
161
+ const ch = buf.toString('utf-8');
162
+ if (ch === '\n') break;
163
+ line += ch;
164
+ }
165
+ return line;
166
+ } finally {
167
+ fs.closeSync(fd);
168
+ }
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ // ---------------------------------------------------------------------
175
+ // arg parsing (hand-rolled, house style — no commander in _cli/)
176
+ // ---------------------------------------------------------------------
177
+
178
+ function _argError(err: (t: string) => void, message: string): never {
179
+ err(`${USAGE}\n${PROG}: error: ${message}\n`);
180
+ throw new ArgparseExit(2);
181
+ }
182
+
183
+ export function _parse(argv: string[], err: (t: string) => void): Opts {
184
+ const opts: Opts = {
185
+ list: false,
186
+ json: false,
187
+ session: null,
188
+ print: false,
189
+ launch: null,
190
+ llm: false,
191
+ root: null,
192
+ };
193
+ let i = 0;
194
+ const takeValue = (flag: string): string => {
195
+ const arg = argv[i] as string;
196
+ const eq = arg.indexOf('=');
197
+ if (eq >= 0) return arg.slice(eq + 1);
198
+ i += 1;
199
+ const value = argv[i];
200
+ if (value === undefined) _argError(err, `argument ${flag}: expected one argument`);
201
+ return value;
202
+ };
203
+ while (i < argv.length) {
204
+ const arg = argv[i] as string;
205
+ const flag = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
206
+ switch (flag) {
207
+ case '-h':
208
+ case '--help':
209
+ process.stdout.write(_HELP);
210
+ throw new ArgparseExit(0);
211
+ case '--list':
212
+ opts.list = true;
213
+ break;
214
+ case '--json':
215
+ opts.json = true;
216
+ break;
217
+ case '--print':
218
+ opts.print = true;
219
+ break;
220
+ case '--llm':
221
+ opts.llm = true;
222
+ break;
223
+ case '--session':
224
+ opts.session = takeValue('--session');
225
+ break;
226
+ case '--launch':
227
+ opts.launch = takeValue('--launch');
228
+ break;
229
+ case '--root':
230
+ opts.root = takeValue('--root');
231
+ break;
232
+ default:
233
+ _argError(err, `unrecognized arguments: ${arg}`);
234
+ }
235
+ i += 1;
236
+ }
237
+ return opts;
238
+ }
239
+
240
+ // ---------------------------------------------------------------------
241
+ // rendering
242
+ // ---------------------------------------------------------------------
243
+
244
+ function _fmt_date(iso: string | null): string {
245
+ if (!iso) return 'unknown-date';
246
+ return iso.slice(0, 16).replace('T', ' ');
247
+ }
248
+
249
+ export function render_session_line(s: HandoffSession, index: number): string {
250
+ const branch = s.branch ? ` · ${s.branch}` : '';
251
+ return `${String(index + 1).padStart(2)}. ${_fmt_date(s.endedAt ?? s.startedAt)} · ${s.source}${branch} · ${s.summary}`;
252
+ }
253
+
254
+ function _render_list(sessions: HandoffSession[], json: boolean): string {
255
+ if (json) return JSON.stringify(sessions, null, 2) + '\n';
256
+ return sessions.map((s, i) => render_session_line(s, i)).join('\n') + '\n';
257
+ }
258
+
259
+ // ---------------------------------------------------------------------
260
+ // main
261
+ // ---------------------------------------------------------------------
262
+
263
+ export function main(argv: string[] | null = null, options: MainOptions = {}): number {
264
+ const out = options.out ?? ((t: string): void => void process.stdout.write(t));
265
+ const err = options.err ?? ((t: string): void => void process.stderr.write(t));
266
+ const isTTY = options.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
267
+ const readLine = options.readLine ?? _tty_read_line;
268
+ const runner = options.runner ?? _default_runner;
269
+
270
+ let opts: Opts;
271
+ try {
272
+ opts = _parse(argv ?? process.argv.slice(2), err);
273
+ } catch (exc) {
274
+ if (exc instanceof ArgparseExit) return exc.code;
275
+ throw exc;
276
+ }
277
+
278
+ // The bash dispatcher strips a global `--root` and exports it as
279
+ // AGENT_CONFIG_PROJECT_ROOT instead — honor both channels.
280
+ const envRoot =
281
+ process.env.AGENT_CONFIG_ROOT_OVERRIDE === '1'
282
+ ? process.env.AGENT_CONFIG_PROJECT_ROOT || null
283
+ : null;
284
+ const root = path.resolve(opts.root ?? envRoot ?? process.cwd());
285
+ const sessions = list_handoff_sessions({ cwd: root });
286
+
287
+ if (opts.list) {
288
+ if (sessions.length === 0) {
289
+ out('no recent sessions found for this repo\n');
290
+ return 1;
291
+ }
292
+ out(_render_list(sessions, opts.json));
293
+ return 0;
294
+ }
295
+
296
+ if (sessions.length === 0) {
297
+ err(`${PROG}: no recent sessions found for this repo\n`);
298
+ return 1;
299
+ }
300
+
301
+ if (opts.launch && !(opts.launch in LAUNCH_ADAPTERS)) {
302
+ err(`${USAGE}\n${PROG}: error: unknown --launch host '${opts.launch}' (claude | codex)\n`);
303
+ return 2;
304
+ }
305
+ const adapter = opts.launch ? (LAUNCH_ADAPTERS[opts.launch] as LaunchAdapter) : null;
306
+ if (opts.launch && adapter && !adapter.supports_launch) {
307
+ err(`${PROG}: --launch ${opts.launch} is not supported yet (hook injection already works there)\n`);
308
+ return 2;
309
+ }
310
+
311
+ // resolve the session — flag, or picker in a TTY
312
+ let picked: HandoffSession | undefined;
313
+ if (opts.session) {
314
+ const matches = sessions.filter((s) => s.id === opts.session || s.id.startsWith(opts.session as string));
315
+ if (matches.length === 0) {
316
+ err(`${PROG}: session '${opts.session}' not found — run --list\n`);
317
+ return 1;
318
+ }
319
+ if (matches.length > 1) {
320
+ err(`${PROG}: session prefix '${opts.session}' is ambiguous (${matches.length} matches)\n`);
321
+ return 2;
322
+ }
323
+ picked = matches[0];
324
+ } else if (isTTY) {
325
+ out('Recent sessions:\n');
326
+ out(_render_list(sessions, false));
327
+ const raw = readLine(`Pick a session [1-${sessions.length}]: `);
328
+ const n = Number((raw ?? '').trim());
329
+ if (!Number.isInteger(n) || n < 1 || n > sessions.length) {
330
+ err(`${PROG}: invalid selection\n`);
331
+ return 2;
332
+ }
333
+ picked = sessions[n - 1];
334
+ } else {
335
+ out(_render_list(sessions, false));
336
+ err(`${PROG}: non-interactive invocation needs --session <id> (list above)\n`);
337
+ return 2;
338
+ }
339
+ if (!picked) return 2;
340
+
341
+ let text: string;
342
+ try {
343
+ text = build_handoff(picked, { cwd: root, llm: opts.llm });
344
+ } catch (exc) {
345
+ if (exc instanceof LlmPolishNotImplementedError) {
346
+ err(`${PROG}: ${exc.message}\n`);
347
+ return 2;
348
+ }
349
+ throw exc;
350
+ }
351
+
352
+ if (opts.print) {
353
+ out(text);
354
+ return 0;
355
+ }
356
+
357
+ if (adapter && adapter.seeding === 'initial-prompt') {
358
+ // no state file on this path — nothing would consume it
359
+ out(`launching ${opts.launch} with the handoff as initial prompt…\n`);
360
+ return runner(adapter.build_argv(text));
361
+ }
362
+
363
+ // Only the most recent handoff persists — a second `handoff` run before
364
+ // the next session start replaces the unconsumed file (by design; the
365
+ // hook consumes exactly one).
366
+ const existed = fs.existsSync(handoff_target_path(root));
367
+ const target = write_handoff(text, { cwd: root });
368
+ if (existed) out('note: replaced an unconsumed handoff — only the most recent one persists.\n');
369
+ out(`handoff written: ${target}\n`);
370
+ out('start a new session — the handoff will be injected automatically (hook-capable hosts).\n');
371
+
372
+ if (adapter) {
373
+ out(`launching ${opts.launch}…\n`);
374
+ return runner(adapter.build_argv(text));
375
+ }
376
+ return 0;
377
+ }
378
+
379
+ // ---------------------------------------------------------------------
380
+ // entry guard (symlink-safe; same shape as cmd_analyze_session.ts —
381
+ // under the cli-delegate bundle the argv-URL compare matches the bundle)
382
+ // ---------------------------------------------------------------------
383
+
384
+ function _isCliEntry(): boolean {
385
+ if (process.argv[1] === undefined) {
386
+ return false;
387
+ }
388
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
389
+ if (import.meta.url === argvUrl) {
390
+ return true;
391
+ }
392
+ // Symlinked invocation (installed projection, /var → /private/var):
393
+ // compare realpaths so the entry guard still fires.
394
+ try {
395
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
396
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
397
+ return here === argv;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+
403
+ if (_isCliEntry()) {
404
+ process.exitCode = main();
405
+ }
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `agent-config route:audit [--last N] [--record] [--weekly] [--json]` —
4
+ * replay the shared router matcher over the last N USER prompts of the
5
+ * repo-local chat-history log and render matched-vs-unmatched per prompt.
6
+ * Read-only over the inputs; no LLM call; deterministic.
7
+ *
8
+ * Measurement level (ADR-126): trigger matching only — what the host actually
9
+ * invoked on those turns is NOT measured here. "Should-have-matched" is the
10
+ * human reviewer's judgment over this table, not a computed verdict.
11
+ *
12
+ * `--record` appends one JSONL record per audited prompt to the opt-in
13
+ * routing recorder (`telemetry.routing_recorder.enabled`, default OFF —
14
+ * disabled means zero file IO and a silent exit contribution). The record is
15
+ * PII-excluded by construction: it carries a truncated sha256 of the prompt,
16
+ * never the prompt text, plus matched rule ids / trigger labels and an
17
+ * enforcement join against `agents/runtime/state/rule-trips.json` where a
18
+ * same-named hook concern exists. Delete-and-rerun rebuilds it from chat
19
+ * history — losing it changes no answer (state-store test, ADR-124 § 6).
20
+ *
21
+ * `--weekly` renders the rolling last-7-days picture from the recorder log.
22
+ * Resolver authority note: this instrument is corpus-growth raw material for
23
+ * `internal/bench/layer1-resolver-PREREG.md` (its P2 condition) as a side
24
+ * effect only — the PREREG stays the sole authority over any resolver
25
+ * revival; nothing here builds or scores a resolver.
26
+ *
27
+ * Exit codes: 0 rendered (even when the recorder is disabled), 1 no prompts /
28
+ * no records to audit, 2 invocation or IO error.
29
+ */
30
+
31
+ import * as crypto from 'node:crypto';
32
+ import * as fs from 'node:fs';
33
+ import * as path from 'node:path';
34
+ import { fileURLToPath, pathToFileURL } from 'node:url';
35
+
36
+ import { match_prompt, type Router } from '../_lib/router_match.js';
37
+ import { load_agent_settings } from '../_lib/agent_settings.js';
38
+ import { read_entries } from '../chat_history.js';
39
+
40
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
41
+ const ROUTER_JSON = path.join(REPO_ROOT, 'dist', 'router.json');
42
+
43
+ // cache-version: v1 — rebuildable audit log: delete-and-rerun rebuilds it from
44
+ // chat history (`route:audit --record`); losing it changes no answer.
45
+ export const RECORDER_REL = 'agents/runtime/state/routing-telemetry.jsonl';
46
+ const RULE_TRIPS_REL = 'agents/runtime/state/rule-trips.json';
47
+ export const RECORD_SCHEMA_VERSION = 1;
48
+
49
+ export const MEASUREMENT_HEADER =
50
+ 'Measurement level: trigger matching only — what the host actually invokes is NOT measured here (ADR-126).';
51
+ export const PREREG_AUTHORITY_LINE =
52
+ 'Resolver authority: internal/bench/layer1-resolver-PREREG.md — this corpus feeds its P2 condition as a side effect; nothing here revives a resolver.';
53
+
54
+ function recorder_path(): string {
55
+ return process.env['AGENT_ROUTING_TELEMETRY_FILE'] ?? path.join(REPO_ROOT, RECORDER_REL);
56
+ }
57
+
58
+ export function recorder_enabled(): boolean {
59
+ try {
60
+ const settings = load_agent_settings({ cwd: REPO_ROOT }) as Record<string, unknown>;
61
+ const t = settings['telemetry'];
62
+ if (t === null || typeof t !== 'object' || Array.isArray(t)) return false;
63
+ const rr = (t as Record<string, unknown>)['routing_recorder'];
64
+ if (rr === null || typeof rr !== 'object' || Array.isArray(rr)) return false;
65
+ const enabled = (rr as Record<string, unknown>)['enabled'];
66
+ return enabled === true || enabled === 'true' || enabled === 'on' || enabled === 'yes';
67
+ } catch {
68
+ return false; // unparseable means disabled — default-off doctrine
69
+ }
70
+ }
71
+
72
+ interface AuditedPrompt {
73
+ prompt: string;
74
+ matched: { rule: string; tier: string; triggers: string[] }[];
75
+ }
76
+
77
+ function _trigger_label(trigger: Record<string, unknown>): string {
78
+ for (const kind of ['keyword', 'phrase', 'command', 'path_prefix', 'file_pattern']) {
79
+ if (kind in trigger) return `${kind}: ${String(trigger[kind])}`;
80
+ }
81
+ return JSON.stringify(trigger);
82
+ }
83
+
84
+ export function audit_prompts(router: Router, prompts: string[]): AuditedPrompt[] {
85
+ return prompts.map((prompt) => {
86
+ const result = match_prompt(router, prompt, 'full', null, null);
87
+ const byRule = new Map<string, { tier: string; triggers: string[] }>();
88
+ for (const mt of result.matched_triggers) {
89
+ const id = String(mt.rule);
90
+ const entry = byRule.get(id) ?? { tier: mt.tier.replace('_', '-'), triggers: [] };
91
+ entry.triggers.push(_trigger_label(mt.trigger as Record<string, unknown>));
92
+ byRule.set(id, entry);
93
+ }
94
+ const matched = [...byRule.entries()]
95
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
96
+ .map(([rule, v]) => ({ rule, tier: v.tier, triggers: v.triggers }));
97
+ return { prompt, matched };
98
+ });
99
+ }
100
+
101
+ /** Last N user prompts from the repo chat-history log, oldest first. */
102
+ export function load_user_prompts(last: number, history_path?: string | null): string[] {
103
+ let entries: Array<{ t?: unknown; text?: unknown }> = [];
104
+ try {
105
+ entries = read_entries({ path: history_path ?? null }) as Array<{ t?: unknown; text?: unknown }>;
106
+ } catch {
107
+ return [];
108
+ }
109
+ const prompts: string[] = [];
110
+ for (const e of entries) {
111
+ if (e === null || typeof e !== 'object') continue;
112
+ if (e.t !== 'user' || typeof e.text !== 'string') continue;
113
+ const text = e.text.trim();
114
+ // Host-injected meta turns are not user prompts (same filter as the
115
+ // handoff session reader).
116
+ if (text === '' || text.startsWith('<') || text.startsWith('Caveat:')) continue;
117
+ prompts.push(text);
118
+ }
119
+ return prompts.slice(-last);
120
+ }
121
+
122
+ interface TripsDoc {
123
+ concerns?: Record<string, { block?: number; warn?: number }>;
124
+ }
125
+
126
+ function _load_trips(): TripsDoc {
127
+ try {
128
+ return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, RULE_TRIPS_REL), 'utf-8')) as TripsDoc;
129
+ } catch {
130
+ return {};
131
+ }
132
+ }
133
+
134
+ export function build_records(audits: AuditedPrompt[], now_iso: string): Record<string, unknown>[] {
135
+ const trips = _load_trips();
136
+ return audits.map((a) => {
137
+ const enforcement_trips: Record<string, { block: number; warn: number }> = {};
138
+ for (const m of a.matched) {
139
+ const concern = trips.concerns?.[m.rule];
140
+ if (concern) {
141
+ enforcement_trips[m.rule] = { block: concern.block ?? 0, warn: concern.warn ?? 0 };
142
+ }
143
+ }
144
+ // PII-exclusion-by-construction: no free-form prompt field exists in
145
+ // this record type — only a truncated digest and closed-shape labels.
146
+ return {
147
+ schema_version: RECORD_SCHEMA_VERSION,
148
+ ts: now_iso,
149
+ prompt_sha16: crypto.createHash('sha256').update(a.prompt).digest('hex').slice(0, 16),
150
+ matched: a.matched,
151
+ enforcement_trips,
152
+ };
153
+ });
154
+ }
155
+
156
+ export function append_records(records: Record<string, unknown>[], target: string): number {
157
+ if (process.env['AGENT_CONFIG_REPLAY'] === '1') return 0; // replay mode: no state writes
158
+ fs.mkdirSync(path.dirname(target), { recursive: true });
159
+ const fd = fs.openSync(target, 'a', 0o644);
160
+ try {
161
+ for (const r of records) fs.writeSync(fd, JSON.stringify(r) + '\n');
162
+ fs.fsyncSync(fd);
163
+ } finally {
164
+ fs.closeSync(fd);
165
+ }
166
+ return records.length;
167
+ }
168
+
169
+ export function render_weekly(target: string, now: Date): { text: string; records: number } {
170
+ let lines: string[] = [];
171
+ try {
172
+ lines = fs.readFileSync(target, 'utf-8').split('\n').filter(Boolean);
173
+ } catch {
174
+ lines = [];
175
+ }
176
+ const cutoff = now.getTime() - 7 * 24 * 60 * 60 * 1000;
177
+ const perRule = new Map<string, number>();
178
+ let inWindow = 0;
179
+ for (const line of lines) {
180
+ let rec: { ts?: string; matched?: { rule: string }[] };
181
+ try {
182
+ rec = JSON.parse(line) as typeof rec;
183
+ } catch {
184
+ continue;
185
+ }
186
+ const ts = Date.parse(rec.ts ?? '');
187
+ if (Number.isNaN(ts) || ts < cutoff) continue;
188
+ inWindow += 1;
189
+ for (const m of rec.matched ?? []) {
190
+ perRule.set(m.rule, (perRule.get(m.rule) ?? 0) + 1);
191
+ }
192
+ }
193
+ const out: string[] = [MEASUREMENT_HEADER, ''];
194
+ out.push(`routing recorder — rolling 7-day window (${inWindow} recorded prompt(s))`);
195
+ if (inWindow === 0) {
196
+ out.push(' (no records in the window — enable telemetry.routing_recorder and run `route:audit --record`)');
197
+ } else {
198
+ const rows = [...perRule.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
199
+ for (const [rule, n] of rows) out.push(` ${String(n).padStart(4)} × ${rule}`);
200
+ }
201
+ out.push('');
202
+ out.push(PREREG_AUTHORITY_LINE);
203
+ return { text: out.join('\n') + '\n', records: inWindow };
204
+ }
205
+
206
+ export function main(argv: string[]): number {
207
+ const args = [...argv];
208
+ const as_json = args.includes('--json');
209
+ const weekly = args.includes('--weekly');
210
+ const record = args.includes('--record');
211
+ let last = 10;
212
+ for (let i = 0; i < args.length; i++) {
213
+ const a = args[i] as string;
214
+ if (a === '--last') last = Number(args[++i] ?? '10') || 10;
215
+ else if (a.startsWith('--last=')) last = Number(a.slice('--last='.length)) || 10;
216
+ else if (a === '-h' || a === '--help') {
217
+ process.stdout.write('usage: agent-config route:audit [--last N] [--record] [--weekly] [--json]\n');
218
+ return 0;
219
+ }
220
+ }
221
+
222
+ if (weekly) {
223
+ const { text, records } = render_weekly(recorder_path(), new Date());
224
+ process.stdout.write(text);
225
+ return records === 0 ? 1 : 0;
226
+ }
227
+
228
+ let router: Router;
229
+ try {
230
+ router = JSON.parse(fs.readFileSync(ROUTER_JSON, 'utf-8')) as Router;
231
+ } catch (e) {
232
+ process.stderr.write(`route:audit: cannot read ${ROUTER_JSON}: ${String(e)}\n`);
233
+ return 2;
234
+ }
235
+ const prompts = load_user_prompts(last);
236
+ if (prompts.length === 0) {
237
+ process.stdout.write(MEASUREMENT_HEADER + '\n\n');
238
+ process.stdout.write('no user prompts found in the chat-history log — nothing to audit\n');
239
+ return 1;
240
+ }
241
+ const audits = audit_prompts(router, prompts);
242
+
243
+ let recorded = 0;
244
+ let recorder_note = '';
245
+ if (record) {
246
+ if (recorder_enabled()) {
247
+ recorded = append_records(build_records(audits, new Date().toISOString()), recorder_path());
248
+ recorder_note = `recorded ${recorded} prompt(s) to ${RECORDER_REL}`;
249
+ } else {
250
+ recorder_note = 'recorder disabled (telemetry.routing_recorder.enabled is off — default) — nothing written';
251
+ }
252
+ }
253
+
254
+ if (as_json) {
255
+ process.stdout.write(
256
+ JSON.stringify(
257
+ { measurement_level: MEASUREMENT_HEADER, audited: audits.length, recorded, prompts: audits },
258
+ null,
259
+ 2,
260
+ ) + '\n',
261
+ );
262
+ return 0;
263
+ }
264
+ const out: string[] = [MEASUREMENT_HEADER, ''];
265
+ out.push(`audited the last ${audits.length} user prompt(s) — matched rules per prompt`);
266
+ out.push('(should-have-matched is the reviewer\'s judgment over this table, not a computed verdict)');
267
+ out.push('');
268
+ audits.forEach((a, i) => {
269
+ const shown = a.prompt.length > 100 ? a.prompt.slice(0, 97) + '...' : a.prompt;
270
+ out.push(`${String(i + 1).padStart(3)}. ${JSON.stringify(shown)}`);
271
+ if (a.matched.length === 0) {
272
+ out.push(' → (no trigger matched — kernel only)');
273
+ } else {
274
+ for (const m of a.matched) {
275
+ out.push(` → ${m.rule} [${m.tier}] (${m.triggers.join(' · ')})`);
276
+ }
277
+ }
278
+ });
279
+ if (recorder_note) {
280
+ out.push('');
281
+ out.push(recorder_note);
282
+ }
283
+ process.stdout.write(out.join('\n') + '\n');
284
+ return 0;
285
+ }
286
+
287
+ const _HERE = fileURLToPath(import.meta.url);
288
+ if (process.argv[1] && (import.meta.url === pathToFileURL(process.argv[1]).href || path.resolve(process.argv[1]) === _HERE)) {
289
+ process.exitCode = main(process.argv.slice(2));
290
+ }