aw-ecc 1.4.32 → 1.4.47

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 (258) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex/hooks/aw-post-tool-use.sh +8 -2
  3. package/.codex/hooks/aw-session-start.sh +11 -4
  4. package/.codex/hooks/aw-stop.sh +8 -2
  5. package/.codex/hooks/aw-user-prompt-submit.sh +10 -2
  6. package/.codex/hooks.json +8 -8
  7. package/.cursor/INSTALL.md +7 -5
  8. package/.cursor/hooks/adapter.js +41 -4
  9. package/.cursor/hooks/after-agent-response.js +62 -0
  10. package/.cursor/hooks/before-submit-prompt.js +7 -1
  11. package/.cursor/hooks/post-tool-use-failure.js +21 -0
  12. package/.cursor/hooks/post-tool-use.js +39 -0
  13. package/.cursor/hooks/shared/aw-phase-definitions.js +53 -0
  14. package/.cursor/hooks/shared/aw-phase-runner.js +3 -1
  15. package/.cursor/hooks/subagent-start.js +22 -4
  16. package/.cursor/hooks/subagent-stop.js +18 -1
  17. package/.cursor/hooks.json +23 -2
  18. package/.opencode/package.json +1 -1
  19. package/AGENTS.md +3 -3
  20. package/README.md +5 -5
  21. package/commands/adk.md +52 -0
  22. package/commands/build.md +22 -9
  23. package/commands/deploy.md +12 -0
  24. package/commands/execute.md +9 -0
  25. package/commands/feature.md +333 -0
  26. package/commands/investigate.md +18 -5
  27. package/commands/plan.md +23 -9
  28. package/commands/publish.md +65 -0
  29. package/commands/review.md +12 -0
  30. package/commands/ship.md +12 -0
  31. package/commands/test.md +12 -0
  32. package/commands/verify.md +9 -0
  33. package/hooks/hooks.json +36 -0
  34. package/manifests/install-components.json +8 -0
  35. package/manifests/install-modules.json +83 -0
  36. package/manifests/install-profiles.json +7 -0
  37. package/package.json +1 -1
  38. package/scripts/ci/validate-rules.js +51 -0
  39. package/scripts/cursor-aw-home/hooks.json +23 -2
  40. package/scripts/cursor-aw-hooks/adapter.js +41 -4
  41. package/scripts/cursor-aw-hooks/before-submit-prompt.js +7 -1
  42. package/scripts/hooks/aw-usage-commit-created.js +32 -0
  43. package/scripts/hooks/aw-usage-post-tool-use-failure.js +56 -0
  44. package/scripts/hooks/aw-usage-post-tool-use.js +242 -0
  45. package/scripts/hooks/aw-usage-prompt-submit.js +112 -0
  46. package/scripts/hooks/aw-usage-session-start.js +48 -0
  47. package/scripts/hooks/aw-usage-stop.js +182 -0
  48. package/scripts/hooks/aw-usage-telemetry-send.js +84 -0
  49. package/scripts/hooks/cost-tracker.js +3 -23
  50. package/scripts/hooks/shared/aw-phase-definitions.js +53 -0
  51. package/scripts/hooks/shared/aw-phase-runner.js +3 -1
  52. package/scripts/lib/aw-hook-contract.js +2 -2
  53. package/scripts/lib/aw-pricing.js +306 -0
  54. package/scripts/lib/aw-usage-telemetry.js +472 -0
  55. package/scripts/lib/codex-hook-config.js +8 -8
  56. package/scripts/lib/cursor-hook-config.js +25 -10
  57. package/scripts/lib/install-targets/cursor-project.js +3 -0
  58. package/scripts/lib/install-targets/helpers.js +20 -3
  59. package/skills/aw-adk/SKILL.md +317 -0
  60. package/skills/aw-adk/agents/analyzer.md +113 -0
  61. package/skills/aw-adk/agents/comparator.md +113 -0
  62. package/skills/aw-adk/agents/grader.md +115 -0
  63. package/skills/aw-adk/assets/eval_review.html +76 -0
  64. package/skills/aw-adk/eval-viewer/generate_review.py +164 -0
  65. package/skills/aw-adk/eval-viewer/viewer.html +181 -0
  66. package/skills/aw-adk/evals/eval-colocated-placement.md +84 -0
  67. package/skills/aw-adk/evals/eval-create-agent.md +90 -0
  68. package/skills/aw-adk/evals/eval-create-command.md +98 -0
  69. package/skills/aw-adk/evals/eval-create-eval.md +89 -0
  70. package/skills/aw-adk/evals/eval-create-rule.md +99 -0
  71. package/skills/aw-adk/evals/eval-create-skill.md +97 -0
  72. package/skills/aw-adk/evals/eval-delete-agent.md +79 -0
  73. package/skills/aw-adk/evals/eval-delete-command.md +89 -0
  74. package/skills/aw-adk/evals/eval-delete-rule.md +86 -0
  75. package/skills/aw-adk/evals/eval-delete-skill.md +90 -0
  76. package/skills/aw-adk/evals/eval-meta-eval-coverage.md +78 -0
  77. package/skills/aw-adk/evals/eval-meta-eval-determinism.md +81 -0
  78. package/skills/aw-adk/evals/eval-meta-eval-false-pass.md +81 -0
  79. package/skills/aw-adk/evals/eval-score-accuracy.md +95 -0
  80. package/skills/aw-adk/evals/eval-type-redirect.md +68 -0
  81. package/skills/aw-adk/evals/evals.json +96 -0
  82. package/skills/aw-adk/references/artifact-wiring.md +162 -0
  83. package/skills/aw-adk/references/cross-ide-mapping.md +71 -0
  84. package/skills/aw-adk/references/eval-placement-guide.md +183 -0
  85. package/skills/aw-adk/references/external-resources.md +75 -0
  86. package/skills/aw-adk/references/getting-started.md +66 -0
  87. package/skills/aw-adk/references/registry-structure.md +152 -0
  88. package/skills/aw-adk/references/rubric-agent.md +36 -0
  89. package/skills/aw-adk/references/rubric-command.md +36 -0
  90. package/skills/aw-adk/references/rubric-eval.md +36 -0
  91. package/skills/aw-adk/references/rubric-meta-eval.md +132 -0
  92. package/skills/aw-adk/references/rubric-rule.md +36 -0
  93. package/skills/aw-adk/references/rubric-skill.md +36 -0
  94. package/skills/aw-adk/references/schemas.md +222 -0
  95. package/skills/aw-adk/references/template-agent.md +251 -0
  96. package/skills/aw-adk/references/template-command.md +279 -0
  97. package/skills/aw-adk/references/template-eval.md +176 -0
  98. package/skills/aw-adk/references/template-rule.md +119 -0
  99. package/skills/aw-adk/references/template-skill.md +123 -0
  100. package/skills/aw-adk/references/type-classifier.md +98 -0
  101. package/skills/aw-adk/references/writing-good-agents.md +227 -0
  102. package/skills/aw-adk/references/writing-good-commands.md +258 -0
  103. package/skills/aw-adk/references/writing-good-evals.md +271 -0
  104. package/skills/aw-adk/references/writing-good-rules.md +214 -0
  105. package/skills/aw-adk/references/writing-good-skills.md +159 -0
  106. package/skills/aw-adk/scripts/aggregate-benchmark.py +190 -0
  107. package/skills/aw-adk/scripts/lint-artifact.sh +211 -0
  108. package/skills/aw-adk/scripts/score-artifact.sh +179 -0
  109. package/skills/aw-adk/scripts/trigger-eval.py +192 -0
  110. package/skills/aw-build/SKILL.md +19 -2
  111. package/skills/aw-deploy/SKILL.md +65 -3
  112. package/skills/aw-design/SKILL.md +156 -0
  113. package/skills/aw-design/references/highrise-tokens.md +394 -0
  114. package/skills/aw-design/references/micro-interactions.md +76 -0
  115. package/skills/aw-design/references/prompt-template.md +160 -0
  116. package/skills/aw-design/references/quality-checklist.md +70 -0
  117. package/skills/aw-design/references/self-review.md +497 -0
  118. package/skills/aw-design/references/stitch-workflow.md +127 -0
  119. package/skills/aw-feature/SKILL.md +293 -0
  120. package/skills/aw-investigate/SKILL.md +17 -0
  121. package/skills/aw-plan/SKILL.md +34 -3
  122. package/skills/aw-publish/SKILL.md +300 -0
  123. package/skills/aw-publish/evals/eval-confirmation-gate.md +60 -0
  124. package/skills/aw-publish/evals/eval-intent-detection.md +111 -0
  125. package/skills/aw-publish/evals/eval-push-modes.md +67 -0
  126. package/skills/aw-publish/evals/eval-rules-push.md +60 -0
  127. package/skills/aw-publish/evals/evals.json +29 -0
  128. package/skills/aw-publish/references/push-modes.md +38 -0
  129. package/skills/aw-review/SKILL.md +88 -9
  130. package/skills/aw-rules-review/SKILL.md +124 -0
  131. package/skills/aw-rules-review/agents/openai.yaml +3 -0
  132. package/skills/aw-rules-review/scripts/generate-review-template.mjs +323 -0
  133. package/skills/aw-ship/SKILL.md +16 -0
  134. package/skills/aw-spec/SKILL.md +15 -0
  135. package/skills/aw-tasks/SKILL.md +15 -0
  136. package/skills/aw-test/SKILL.md +16 -0
  137. package/skills/aw-yolo/SKILL.md +4 -0
  138. package/skills/diagnose/SKILL.md +121 -0
  139. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  140. package/skills/finish-only-when-green/SKILL.md +265 -0
  141. package/skills/grill-me/SKILL.md +24 -0
  142. package/skills/grill-with-docs/SKILL.md +92 -0
  143. package/skills/grill-with-docs/adr-format.md +47 -0
  144. package/skills/grill-with-docs/context-format.md +67 -0
  145. package/skills/improve-codebase-architecture/SKILL.md +75 -0
  146. package/skills/improve-codebase-architecture/deepening.md +37 -0
  147. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  148. package/skills/improve-codebase-architecture/language.md +53 -0
  149. package/skills/local-ghl-setup-from-screenshot/SKILL.md +538 -0
  150. package/skills/tdd/SKILL.md +115 -0
  151. package/skills/tdd/deep-modules.md +33 -0
  152. package/skills/tdd/interface-design.md +31 -0
  153. package/skills/tdd/mocking.md +59 -0
  154. package/skills/tdd/refactoring.md +10 -0
  155. package/skills/tdd/tests.md +61 -0
  156. package/skills/to-issues/SKILL.md +62 -0
  157. package/skills/to-prd/SKILL.md +75 -0
  158. package/skills/using-aw-skills/SKILL.md +170 -237
  159. package/skills/using-aw-skills/hooks/session-start.sh +11 -41
  160. package/skills/zoom-out/SKILL.md +24 -0
  161. package/.cursor/rules/common-agents.md +0 -53
  162. package/.cursor/rules/common-aw-routing.md +0 -43
  163. package/.cursor/rules/common-coding-style.md +0 -52
  164. package/.cursor/rules/common-development-workflow.md +0 -33
  165. package/.cursor/rules/common-git-workflow.md +0 -28
  166. package/.cursor/rules/common-hooks.md +0 -34
  167. package/.cursor/rules/common-patterns.md +0 -35
  168. package/.cursor/rules/common-performance.md +0 -59
  169. package/.cursor/rules/common-security.md +0 -33
  170. package/.cursor/rules/common-testing.md +0 -33
  171. package/.cursor/skills/api-and-interface-design/SKILL.md +0 -75
  172. package/.cursor/skills/article-writing/SKILL.md +0 -85
  173. package/.cursor/skills/aw-brainstorm/SKILL.md +0 -115
  174. package/.cursor/skills/aw-build/SKILL.md +0 -152
  175. package/.cursor/skills/aw-build/evals/build-stage-cases.json +0 -28
  176. package/.cursor/skills/aw-debug/SKILL.md +0 -49
  177. package/.cursor/skills/aw-deploy/SKILL.md +0 -101
  178. package/.cursor/skills/aw-deploy/evals/deploy-stage-cases.json +0 -32
  179. package/.cursor/skills/aw-execute/SKILL.md +0 -47
  180. package/.cursor/skills/aw-execute/references/mode-code.md +0 -47
  181. package/.cursor/skills/aw-execute/references/mode-docs.md +0 -28
  182. package/.cursor/skills/aw-execute/references/mode-infra.md +0 -44
  183. package/.cursor/skills/aw-execute/references/mode-migration.md +0 -58
  184. package/.cursor/skills/aw-execute/references/worker-implementer.md +0 -26
  185. package/.cursor/skills/aw-execute/references/worker-parallel-worker.md +0 -23
  186. package/.cursor/skills/aw-execute/references/worker-quality-reviewer.md +0 -23
  187. package/.cursor/skills/aw-execute/references/worker-spec-reviewer.md +0 -23
  188. package/.cursor/skills/aw-execute/scripts/build-worker-bundle.js +0 -229
  189. package/.cursor/skills/aw-finish/SKILL.md +0 -111
  190. package/.cursor/skills/aw-investigate/SKILL.md +0 -109
  191. package/.cursor/skills/aw-plan/SKILL.md +0 -368
  192. package/.cursor/skills/aw-prepare/SKILL.md +0 -118
  193. package/.cursor/skills/aw-review/SKILL.md +0 -118
  194. package/.cursor/skills/aw-ship/SKILL.md +0 -115
  195. package/.cursor/skills/aw-spec/SKILL.md +0 -104
  196. package/.cursor/skills/aw-tasks/SKILL.md +0 -138
  197. package/.cursor/skills/aw-test/SKILL.md +0 -118
  198. package/.cursor/skills/aw-verify/SKILL.md +0 -51
  199. package/.cursor/skills/aw-yolo/SKILL.md +0 -111
  200. package/.cursor/skills/browser-testing-with-devtools/SKILL.md +0 -81
  201. package/.cursor/skills/bun-runtime/SKILL.md +0 -84
  202. package/.cursor/skills/ci-cd-and-automation/SKILL.md +0 -71
  203. package/.cursor/skills/code-simplification/SKILL.md +0 -74
  204. package/.cursor/skills/content-engine/SKILL.md +0 -88
  205. package/.cursor/skills/context-engineering/SKILL.md +0 -74
  206. package/.cursor/skills/deprecation-and-migration/SKILL.md +0 -75
  207. package/.cursor/skills/documentation-and-adrs/SKILL.md +0 -75
  208. package/.cursor/skills/documentation-lookup/SKILL.md +0 -90
  209. package/.cursor/skills/frontend-slides/SKILL.md +0 -184
  210. package/.cursor/skills/frontend-slides/STYLE_PRESETS.md +0 -330
  211. package/.cursor/skills/frontend-ui-engineering/SKILL.md +0 -68
  212. package/.cursor/skills/git-workflow-and-versioning/SKILL.md +0 -75
  213. package/.cursor/skills/idea-refine/SKILL.md +0 -84
  214. package/.cursor/skills/incremental-implementation/SKILL.md +0 -75
  215. package/.cursor/skills/investor-materials/SKILL.md +0 -96
  216. package/.cursor/skills/investor-outreach/SKILL.md +0 -76
  217. package/.cursor/skills/market-research/SKILL.md +0 -75
  218. package/.cursor/skills/mcp-server-patterns/SKILL.md +0 -67
  219. package/.cursor/skills/nextjs-turbopack/SKILL.md +0 -44
  220. package/.cursor/skills/performance-optimization/SKILL.md +0 -77
  221. package/.cursor/skills/security-and-hardening/SKILL.md +0 -70
  222. package/.cursor/skills/using-aw-skills/SKILL.md +0 -290
  223. package/.cursor/skills/using-aw-skills/evals/skill-trigger-cases.tsv +0 -25
  224. package/.cursor/skills/using-aw-skills/evals/test-skill-triggers.sh +0 -171
  225. package/.cursor/skills/using-aw-skills/hooks/hooks.json +0 -9
  226. package/.cursor/skills/using-aw-skills/hooks/session-start.sh +0 -67
  227. package/.cursor/skills/using-platform-skills/SKILL.md +0 -163
  228. package/.cursor/skills/using-platform-skills/evals/platform-selection-cases.json +0 -52
  229. /package/.cursor/rules/{golang-coding-style.md → golang-coding-style.mdc} +0 -0
  230. /package/.cursor/rules/{golang-hooks.md → golang-hooks.mdc} +0 -0
  231. /package/.cursor/rules/{golang-patterns.md → golang-patterns.mdc} +0 -0
  232. /package/.cursor/rules/{golang-security.md → golang-security.mdc} +0 -0
  233. /package/.cursor/rules/{golang-testing.md → golang-testing.mdc} +0 -0
  234. /package/.cursor/rules/{kotlin-coding-style.md → kotlin-coding-style.mdc} +0 -0
  235. /package/.cursor/rules/{kotlin-hooks.md → kotlin-hooks.mdc} +0 -0
  236. /package/.cursor/rules/{kotlin-patterns.md → kotlin-patterns.mdc} +0 -0
  237. /package/.cursor/rules/{kotlin-security.md → kotlin-security.mdc} +0 -0
  238. /package/.cursor/rules/{kotlin-testing.md → kotlin-testing.mdc} +0 -0
  239. /package/.cursor/rules/{php-coding-style.md → php-coding-style.mdc} +0 -0
  240. /package/.cursor/rules/{php-hooks.md → php-hooks.mdc} +0 -0
  241. /package/.cursor/rules/{php-patterns.md → php-patterns.mdc} +0 -0
  242. /package/.cursor/rules/{php-security.md → php-security.mdc} +0 -0
  243. /package/.cursor/rules/{php-testing.md → php-testing.mdc} +0 -0
  244. /package/.cursor/rules/{python-coding-style.md → python-coding-style.mdc} +0 -0
  245. /package/.cursor/rules/{python-hooks.md → python-hooks.mdc} +0 -0
  246. /package/.cursor/rules/{python-patterns.md → python-patterns.mdc} +0 -0
  247. /package/.cursor/rules/{python-security.md → python-security.mdc} +0 -0
  248. /package/.cursor/rules/{python-testing.md → python-testing.mdc} +0 -0
  249. /package/.cursor/rules/{swift-coding-style.md → swift-coding-style.mdc} +0 -0
  250. /package/.cursor/rules/{swift-hooks.md → swift-hooks.mdc} +0 -0
  251. /package/.cursor/rules/{swift-patterns.md → swift-patterns.mdc} +0 -0
  252. /package/.cursor/rules/{swift-security.md → swift-security.mdc} +0 -0
  253. /package/.cursor/rules/{swift-testing.md → swift-testing.mdc} +0 -0
  254. /package/.cursor/rules/{typescript-coding-style.md → typescript-coding-style.mdc} +0 -0
  255. /package/.cursor/rules/{typescript-hooks.md → typescript-hooks.mdc} +0 -0
  256. /package/.cursor/rules/{typescript-patterns.md → typescript-patterns.mdc} +0 -0
  257. /package/.cursor/rules/{typescript-security.md → typescript-security.mdc} +0 -0
  258. /package/.cursor/rules/{typescript-testing.md → typescript-testing.mdc} +0 -0
@@ -0,0 +1,472 @@
1
+ /**
2
+ * AW Usage Telemetry — shared collection module.
3
+ *
4
+ * Two exports:
5
+ * buildEvent(hookInput, eventType, payload) — normalize cross-harness fields
6
+ * sendAsync(event) — fire-and-forget POST via detached child
7
+ *
8
+ * Called by individual hook scripts (post-tool-use, stop, etc.).
9
+ * CJS module — consistent with existing aw-ecc hook ecosystem.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const { spawn, execSync } = require('child_process');
18
+ const crypto = require('crypto');
19
+
20
+ const SENDER_SCRIPT = path.join(__dirname, '..', 'hooks', 'aw-usage-telemetry-send.js');
21
+ const AW_HOME = path.join(os.homedir(), '.aw');
22
+ const CONFIG_PATH = path.join(AW_HOME, 'telemetry', 'config.json');
23
+ const SESSION_DIR = path.join(AW_HOME, 'telemetry', 'sessions');
24
+ const DEDUPE_DIR = path.join(os.tmpdir(), 'aw-usage-telemetry-dedupe');
25
+
26
+ // ── Git config cache (once per process) ──────────────────────────────
27
+
28
+ let _gitCache = null;
29
+
30
+ function getGitInfo() {
31
+ if (_gitCache) return _gitCache;
32
+ _gitCache = { user: null, email: null };
33
+ try {
34
+ _gitCache.user = execSync('git config user.name', { encoding: 'utf8', timeout: 3000 }).trim() || null;
35
+ } catch { /* ignore */ }
36
+ try {
37
+ _gitCache.email = execSync('git config user.email', { encoding: 'utf8', timeout: 3000 }).trim() || null;
38
+ } catch { /* ignore */ }
39
+ return _gitCache;
40
+ }
41
+
42
+ // ── Telemetry config ─────────────────────────────────────────────────
43
+
44
+ let _configCache = null;
45
+
46
+ function generateMachineId() {
47
+ const raw = `${os.hostname()}:${os.userInfo().username}`;
48
+ return crypto.createHash('sha256').update(raw).digest('hex');
49
+ }
50
+
51
+ function loadConfig() {
52
+ if (_configCache) return _configCache;
53
+ try {
54
+ _configCache = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
55
+ } catch {
56
+ // Config missing or corrupt — self-heal by generating it
57
+ _configCache = { enabled: true, machine_id: generateMachineId() };
58
+ try {
59
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
60
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(_configCache, null, 2) + '\n');
61
+ } catch { /* best effort — don't block the hook */ }
62
+ }
63
+ // Backfill machine_id if config exists but field is missing
64
+ if (!_configCache.machine_id || _configCache.machine_id === 'unknown') {
65
+ _configCache.machine_id = generateMachineId();
66
+ try {
67
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
68
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(_configCache, null, 2) + '\n');
69
+ } catch { /* best effort */ }
70
+ }
71
+ return _configCache;
72
+ }
73
+
74
+ // ── Opt-out check ────────────────────────────────────────────────────
75
+
76
+ function isDisabled() {
77
+ if (process.env.DO_NOT_TRACK === '1') return true;
78
+ if (process.env.AW_TELEMETRY_DISABLED === '1') return true;
79
+ const cfg = loadConfig();
80
+ return cfg.enabled === false;
81
+ }
82
+
83
+ // ── AW version ───────────────────────────────────────────────────────
84
+
85
+ let _awVersion = null;
86
+
87
+ function parseVersionString(raw) {
88
+ if (!raw) return null;
89
+ const match = String(raw).match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/);
90
+ return match ? match[1] : null;
91
+ }
92
+
93
+ function getAwVersion() {
94
+ if (_awVersion) return _awVersion;
95
+ const envVersion = parseVersionString(process.env.AW_VERSION);
96
+ if (envVersion) {
97
+ _awVersion = envVersion;
98
+ return _awVersion;
99
+ }
100
+ try {
101
+ const cliVersion = parseVersionString(execSync('aw --version', {
102
+ encoding: 'utf8',
103
+ timeout: 1500,
104
+ stdio: ['ignore', 'pipe', 'ignore'],
105
+ }));
106
+ if (cliVersion) {
107
+ _awVersion = cliVersion;
108
+ return _awVersion;
109
+ }
110
+ } catch { /* ignore */ }
111
+ const candidates = [
112
+ path.join(AW_HOME, 'node_modules', '@ghl-ai', 'aw', 'package.json'),
113
+ ];
114
+ // Derive global npm prefix from the running node binary (no shell needed)
115
+ try {
116
+ const nodeDir = path.dirname(process.execPath);
117
+ candidates.push(path.join(nodeDir, '..', 'lib', 'node_modules', '@ghl-ai', 'aw', 'package.json'));
118
+ } catch { /* ignore */ }
119
+ try {
120
+ const globalPrefix = execSync('npm prefix -g', { encoding: 'utf8', timeout: 3000 }).trim();
121
+ candidates.push(path.join(globalPrefix, 'lib', 'node_modules', '@ghl-ai', 'aw', 'package.json'));
122
+ } catch { /* ignore */ }
123
+ // aw-ecc version as last-resort fallback
124
+ candidates.push(path.join(os.homedir(), '.aw-ecc', 'package.json'));
125
+ for (const pkgPath of candidates) {
126
+ try {
127
+ _awVersion = parseVersionString(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version) || null;
128
+ if (_awVersion) return _awVersion;
129
+ } catch { /* ignore */ }
130
+ }
131
+ _awVersion = null;
132
+ return _awVersion;
133
+ }
134
+
135
+ // ── Harness detection ────────────────────────────────────────────────
136
+
137
+ function detectHarness(input) {
138
+ // Explicit harness override from shell wrapper (e.g. Codex SessionStart has no turn_id)
139
+ if (process.env.AW_HARNESS) return process.env.AW_HARNESS;
140
+ if (input._cursor || input.conversation_id || input.cursor_version) return 'cursor';
141
+ // Codex provides turn_id on turn-scoped hooks, Claude does not
142
+ if (input.turn_id !== undefined) return 'codex';
143
+ return 'claude';
144
+ }
145
+
146
+ // ── Project hash ─────────────────────────────────────────────────────
147
+
148
+ function computeProjectHash(cwd) {
149
+ if (!cwd) return null;
150
+ return crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16);
151
+ }
152
+
153
+ // ── Session file cleanup ─────────────────────────────────────────────
154
+ // Prune session files older than SESSION_MAX_AGE_MS to prevent unbounded growth.
155
+ // Called once per session start — best-effort, never blocks.
156
+
157
+ const SESSION_MAX_AGE_MS = 72 * 60 * 60 * 1000; // 72 hours
158
+
159
+ function pruneStaleSessionFiles() {
160
+ try {
161
+ const entries = fs.readdirSync(SESSION_DIR);
162
+ const now = Date.now();
163
+ for (const entry of entries) {
164
+ if (!entry.endsWith('.json')) continue;
165
+ const filePath = path.join(SESSION_DIR, entry);
166
+ const stat = fs.statSync(filePath);
167
+ if (now - stat.mtimeMs > SESSION_MAX_AGE_MS) {
168
+ fs.unlinkSync(filePath);
169
+ }
170
+ }
171
+ } catch { /* best effort */ }
172
+ }
173
+
174
+ // ── Session model persistence ────────────────────────────────────────
175
+ // SessionStart captures the model; later hooks read it from disk.
176
+
177
+ function persistSessionModel(sessionId, model) {
178
+ if (!sessionId || !model) return;
179
+ try {
180
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
181
+ const state = readSessionState(sessionId);
182
+ fs.writeFileSync(path.join(SESSION_DIR, sessionId + '.json'), JSON.stringify({
183
+ ...state,
184
+ model,
185
+ }));
186
+ } catch { /* ignore */ }
187
+ }
188
+
189
+ function readSessionModel(sessionId) {
190
+ const state = readSessionState(sessionId);
191
+ return state.model || null;
192
+ }
193
+
194
+ function readSessionState(sessionId) {
195
+ if (!sessionId) return {};
196
+ try {
197
+ const filePath = path.join(SESSION_DIR, sessionId + '.json');
198
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
199
+ // Touch mtime so active sessions are never pruned by cleanup
200
+ try { const now = new Date(); fs.utimesSync(filePath, now, now); } catch { /* ignore */ }
201
+ return data;
202
+ } catch { return {}; }
203
+ }
204
+
205
+ function persistSessionSkill(sessionId, turnId, skill) {
206
+ if (!sessionId || !skill?.skill_name) return;
207
+ try {
208
+ fs.mkdirSync(SESSION_DIR, { recursive: true });
209
+ const state = readSessionState(sessionId);
210
+ fs.writeFileSync(path.join(SESSION_DIR, sessionId + '.json'), JSON.stringify({
211
+ ...state,
212
+ last_skill: {
213
+ turn_id: turnId || null,
214
+ skill_name: skill.skill_name,
215
+ args: skill.args || '',
216
+ source: skill.source || 'unknown',
217
+ updated_at: new Date().toISOString(),
218
+ },
219
+ }));
220
+ } catch { /* ignore */ }
221
+ }
222
+
223
+ function readSessionSkill(sessionId, turnId) {
224
+ const skill = readSessionState(sessionId)?.last_skill;
225
+ if (!skill?.skill_name) return null;
226
+ if (turnId) {
227
+ return skill.turn_id === turnId ? skill : null;
228
+ }
229
+ return skill.turn_id ? null : skill;
230
+ }
231
+
232
+ // ── Short-TTL dedupe guards ──────────────────────────────────────────
233
+
234
+ function normalizeDedupePart(value) {
235
+ if (value === undefined || value === null) return '';
236
+ if (typeof value === 'string') return value;
237
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
238
+ try {
239
+ return JSON.stringify(value);
240
+ } catch {
241
+ return String(value);
242
+ }
243
+ }
244
+
245
+ function tryAcquireDedupe(scope, parts, ttlMs = 2500) {
246
+ const normalizedParts = Array.isArray(parts) ? parts.map(normalizeDedupePart) : [normalizeDedupePart(parts)];
247
+ const digest = crypto.createHash('sha256')
248
+ .update(normalizedParts.join('\n'))
249
+ .digest('hex');
250
+ const safeScope = String(scope || 'event').replace(/[^a-z0-9_-]+/gi, '-').toLowerCase();
251
+ const lockPath = path.join(DEDUPE_DIR, `${safeScope}-${digest}.lock`);
252
+
253
+ try {
254
+ fs.mkdirSync(DEDUPE_DIR, { recursive: true });
255
+ } catch {
256
+ return true;
257
+ }
258
+
259
+ try {
260
+ const stat = fs.statSync(lockPath);
261
+ if (Date.now() - stat.mtimeMs <= ttlMs) {
262
+ return false;
263
+ }
264
+ fs.unlinkSync(lockPath);
265
+ } catch { /* no active lock */ }
266
+
267
+ try {
268
+ fs.writeFileSync(lockPath, String(Date.now()), { flag: 'wx' });
269
+ return true;
270
+ } catch {
271
+ return false;
272
+ }
273
+ }
274
+
275
+ // ── Codex internal-session filters ───────────────────────────────────
276
+
277
+ function resolvePromptText(input) {
278
+ const candidates = [
279
+ input?.prompt,
280
+ input?.user_prompt,
281
+ input?.message,
282
+ input?.text,
283
+ ];
284
+ for (const candidate of candidates) {
285
+ if (typeof candidate === 'string' && candidate.trim()) {
286
+ return candidate;
287
+ }
288
+ }
289
+ return '';
290
+ }
291
+
292
+ function isCodexInternalTaskTitlePrompt(input) {
293
+ if (detectHarness(input) !== 'codex') return false;
294
+ if (input?.transcript_path) return false;
295
+ const prompt = resolvePromptText(input);
296
+ return prompt.includes('Generate a concise UI title')
297
+ && prompt.includes('User prompt:');
298
+ }
299
+
300
+ function isCodexInternalTaskTitleCompletion(input) {
301
+ if (detectHarness(input) !== 'codex') return false;
302
+ if (input?.transcript_path) return false;
303
+ const rawMessage = typeof input?.last_assistant_message === 'string'
304
+ ? input.last_assistant_message.trim()
305
+ : '';
306
+ if (!rawMessage) return false;
307
+ try {
308
+ const parsed = JSON.parse(rawMessage);
309
+ return parsed
310
+ && typeof parsed === 'object'
311
+ && !Array.isArray(parsed)
312
+ && typeof parsed.title === 'string'
313
+ && Object.keys(parsed).length === 1;
314
+ } catch {
315
+ return false;
316
+ }
317
+ }
318
+
319
+ // ── Transcript parsing ───────────────────────────────────────────────
320
+
321
+ function buildCodexUsage(entry) {
322
+ if (entry?.type !== 'event_msg' || entry?.payload?.type !== 'token_count') return null;
323
+ const info = entry.payload?.info || {};
324
+ const usage = info.last_token_usage || info.total_token_usage;
325
+ if (!usage || typeof usage !== 'object') return null;
326
+ return {
327
+ input_tokens: usage.input_tokens ?? null,
328
+ output_tokens: usage.output_tokens ?? null,
329
+ cached_input_tokens: usage.cached_input_tokens ?? null,
330
+ reasoning_output_tokens: usage.reasoning_output_tokens ?? null,
331
+ };
332
+ }
333
+
334
+ function isCodexAssistantEntry(entry) {
335
+ return entry?.type === 'response_item'
336
+ && entry?.payload?.type === 'message'
337
+ && entry?.payload?.role === 'assistant';
338
+ }
339
+
340
+ /**
341
+ * Read the last assistant entry from a transcript JSONL file.
342
+ * Reads the last 256KB (enough for several entries) to avoid loading
343
+ * the entire file which can be 10MB+.
344
+ *
345
+ * Works across harnesses — Claude/Cursor transcripts expose assistant
346
+ * rows directly, while Codex writes `response_item` + `event_msg`
347
+ * lines and surfaces usage via `token_count`.
348
+ *
349
+ * Returns { model, stop_reason, usage } or null.
350
+ */
351
+ function readLastAssistantFromTranscript(transcriptPath) {
352
+ if (!transcriptPath) return null;
353
+ try {
354
+ const stat = fs.statSync(transcriptPath);
355
+ const TAIL_BYTES = 256 * 1024;
356
+ const start = Math.max(0, stat.size - TAIL_BYTES);
357
+ const fd = fs.openSync(transcriptPath, 'r');
358
+ const buf = Buffer.alloc(Math.min(TAIL_BYTES, stat.size));
359
+ fs.readSync(fd, buf, 0, buf.length, start);
360
+ fs.closeSync(fd);
361
+
362
+ const chunk = buf.toString('utf8');
363
+ const lines = chunk.split('\n').filter(Boolean);
364
+ let latestCodexUsage = null;
365
+
366
+ // Walk backward to find the last assistant entry for any harness.
367
+ for (let i = lines.length - 1; i >= 0; i--) {
368
+ try {
369
+ const entry = JSON.parse(lines[i]);
370
+ if (!latestCodexUsage) {
371
+ latestCodexUsage = buildCodexUsage(entry);
372
+ }
373
+ if (entry.type === 'assistant' && entry.message) {
374
+ const msg = entry.message;
375
+ return {
376
+ model: msg.model || null,
377
+ stop_reason: msg.stop_reason || null,
378
+ usage: msg.usage || null,
379
+ };
380
+ }
381
+ if (isCodexAssistantEntry(entry)) {
382
+ return {
383
+ model: null,
384
+ stop_reason: null,
385
+ usage: latestCodexUsage,
386
+ };
387
+ }
388
+ } catch { /* skip malformed lines */ }
389
+ }
390
+ } catch { /* transcript unreadable — non-blocking */ }
391
+ return null;
392
+ }
393
+
394
+ // ── buildEvent ───────────────────────────────────────────────────────
395
+
396
+ function buildEvent(hookInput, eventType, payload) {
397
+ const input = hookInput || {};
398
+ const cfg = loadConfig();
399
+ const git = getGitInfo();
400
+ const harness = detectHarness(input);
401
+
402
+ // Normalize session_id: Claude/Codex use session_id, Cursor uses conversation_id
403
+ const sessionId = input.session_id
404
+ || input._cursor?.conversation_id
405
+ || input.conversation_id
406
+ || null;
407
+
408
+ // generation_id: Cursor only
409
+ const generationId = input.generation_id || null;
410
+
411
+ // model: Claude only on SessionStart, Codex/Cursor on all hooks
412
+ // Fall back to persisted session model from SessionStart
413
+ const model = input.model
414
+ || input._cursor?.model
415
+ || readSessionModel(sessionId)
416
+ || null;
417
+
418
+ // cwd: Claude/Codex have input.cwd, Cursor uses workspace_roots
419
+ const cwd = input.cwd
420
+ || (input.workspace_roots && input.workspace_roots[0])
421
+ || null;
422
+
423
+ return {
424
+ session_id: sessionId,
425
+ generation_id: generationId,
426
+ harness,
427
+ model,
428
+ machine_id: cfg.machine_id || 'unknown',
429
+ github_user: git.user || input.user_email || null,
430
+ github_email: git.email || input.user_email || null,
431
+ project_hash: computeProjectHash(cwd),
432
+ aw_version: getAwVersion(),
433
+ event: eventType,
434
+ client_ts: new Date().toISOString(),
435
+ payload: payload || {},
436
+ };
437
+ }
438
+
439
+ // ── sendAsync ────────────────────────────────────────────────────────
440
+
441
+ function sendAsync(event) {
442
+ if (isDisabled()) return;
443
+ if (!event) return;
444
+
445
+ try {
446
+ const child = spawn('node', [SENDER_SCRIPT, JSON.stringify(event)], {
447
+ detached: true,
448
+ stdio: 'ignore',
449
+ });
450
+ child.unref();
451
+ } catch {
452
+ // Fire-and-forget — never block the hook.
453
+ }
454
+ }
455
+
456
+ module.exports = {
457
+ buildEvent,
458
+ sendAsync,
459
+ isDisabled,
460
+ detectHarness,
461
+ loadConfig,
462
+ persistSessionModel,
463
+ pruneStaleSessionFiles,
464
+ readSessionModel,
465
+ persistSessionSkill,
466
+ readSessionSkill,
467
+ readLastAssistantFromTranscript,
468
+ resolvePromptText,
469
+ tryAcquireDedupe,
470
+ isCodexInternalTaskTitlePrompt,
471
+ isCodexInternalTaskTitleCompletion,
472
+ };
@@ -7,7 +7,7 @@ const GENERATED_AW_HOOKS = Object.freeze({
7
7
  hooks: [
8
8
  {
9
9
  type: 'command',
10
- command: 'bash -lc \'bash "$HOME/.codex/hooks/aw-session-start.sh"\'',
10
+ command: 'bash "$HOME/.codex/hooks/aw-session-start.sh"',
11
11
  },
12
12
  ],
13
13
  description: 'Load AW routing context at session start',
@@ -18,10 +18,10 @@ const GENERATED_AW_HOOKS = Object.freeze({
18
18
  hooks: [
19
19
  {
20
20
  type: 'command',
21
- command: 'bash -lc \'bash "$HOME/.codex/hooks/aw-user-prompt-submit.sh"\'',
21
+ command: 'bash "$HOME/.codex/hooks/aw-user-prompt-submit.sh"',
22
22
  },
23
23
  ],
24
- description: 'Inject compact AW routing and rule reminders on each prompt',
24
+ description: 'Emit Codex prompt telemetry and inject compact AW routing reminders on each prompt',
25
25
  },
26
26
  ],
27
27
  PreToolUse: [
@@ -30,7 +30,7 @@ const GENERATED_AW_HOOKS = Object.freeze({
30
30
  hooks: [
31
31
  {
32
32
  type: 'command',
33
- command: 'bash -lc \'bash "$HOME/.codex/hooks/aw-pre-tool-use.sh"\'',
33
+ command: 'bash "$HOME/.codex/hooks/aw-pre-tool-use.sh"',
34
34
  },
35
35
  ],
36
36
  description: 'Reserved AW pre-tool-use phase for Codex home installs',
@@ -42,10 +42,10 @@ const GENERATED_AW_HOOKS = Object.freeze({
42
42
  hooks: [
43
43
  {
44
44
  type: 'command',
45
- command: 'bash -lc \'bash "$HOME/.codex/hooks/aw-post-tool-use.sh"\'',
45
+ command: 'bash "$HOME/.codex/hooks/aw-post-tool-use.sh"',
46
46
  },
47
47
  ],
48
- description: 'Reserved AW post-tool-use phase for Codex home installs',
48
+ description: 'Emit Codex post-tool-use telemetry for supported Bash-backed events',
49
49
  },
50
50
  ],
51
51
  Stop: [
@@ -53,10 +53,10 @@ const GENERATED_AW_HOOKS = Object.freeze({
53
53
  hooks: [
54
54
  {
55
55
  type: 'command',
56
- command: 'bash -lc \'bash "$HOME/.codex/hooks/aw-stop.sh"\'',
56
+ command: 'bash "$HOME/.codex/hooks/aw-stop.sh"',
57
57
  },
58
58
  ],
59
- description: 'Reserved AW stop phase for Codex home installs',
59
+ description: 'Emit Codex stop telemetry for response_completed events',
60
60
  },
61
61
  ],
62
62
  });
@@ -18,16 +18,10 @@ function buildManagedCursorHookCommand(scriptFileName) {
18
18
 
19
19
  function buildManagedCursorShellCommand(scriptFileName) {
20
20
  const scriptName = String(scriptFileName || '').replace(/\\/g, '/');
21
- const command = [
22
- 'bash -lc',
23
- JSON.stringify(
24
- `for candidate in "$PWD/.cursor/hooks/${scriptName}" "$HOME/.cursor/hooks/${scriptName}"; do ` +
25
- `if [ -f "$candidate" ]; then exec bash "$candidate"; fi; ` +
26
- 'done; exit 0'
27
- ),
28
- ].join(' ');
29
-
30
- return command;
21
+ // Simple fallback: project-level first, then home-level.
22
+ // Previous version used triple-escaped quoting inside bash -lc that
23
+ // silently failed when Cursor spawned the process (Bug #8).
24
+ return `bash -c 'f="$PWD/.cursor/hooks/${scriptName}"; [ -f "$f" ] || f="$HOME/.cursor/hooks/${scriptName}"; exec bash "$f"'`;
31
25
  }
32
26
 
33
27
  const CURSOR_HOOK_ENTRIES = Object.freeze({
@@ -113,6 +107,20 @@ const CURSOR_HOOK_ENTRIES = Object.freeze({
113
107
  description: 'Log agent completion',
114
108
  },
115
109
  ],
110
+ postToolUse: [
111
+ {
112
+ command: buildManagedCursorHookCommand('post-tool-use.js'),
113
+ event: 'postToolUse',
114
+ description: 'Telemetry: skill_invoked detection for Read tool and other non-shell/edit/MCP tools',
115
+ },
116
+ ],
117
+ postToolUseFailure: [
118
+ {
119
+ command: buildManagedCursorHookCommand('post-tool-use-failure.js'),
120
+ event: 'postToolUseFailure',
121
+ description: 'Telemetry: tool_error detection on tool failure/timeout/denial',
122
+ },
123
+ ],
116
124
  beforeTabFileRead: [
117
125
  {
118
126
  command: buildManagedCursorHookCommand('before-tab-file-read.js'),
@@ -134,6 +142,13 @@ const CURSOR_HOOK_ENTRIES = Object.freeze({
134
142
  description: 'Save state before context compaction',
135
143
  },
136
144
  ],
145
+ afterAgentResponse: [
146
+ {
147
+ command: buildManagedCursorHookCommand('after-agent-response.js'),
148
+ event: 'afterAgentResponse',
149
+ description: 'Telemetry: response_completed with tokens and cost per turn',
150
+ },
151
+ ],
137
152
  stop: [
138
153
  {
139
154
  command: buildManagedCursorHookCommand('stop.js'),
@@ -43,6 +43,9 @@ module.exports = createInstallTargetAdapter({
43
43
  repoRoot,
44
44
  sourceRelativePath,
45
45
  destinationDir: path.join(targetRoot, 'rules'),
46
+ // Cursor only loads .mdc rule files; .md is ignored.
47
+ // Flatten .md rule sources to .mdc at the Cursor target.
48
+ extensionMap: { '.md': '.mdc' },
46
49
  });
47
50
  }
48
51
 
@@ -160,7 +160,22 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat
160
160
  return operations;
161
161
  }
162
162
 
163
- function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir }) {
163
+ /**
164
+ * Transform an output filename's extension, preserving README.md
165
+ * (Cursor requires .mdc for rule files; README.md is docs and stays .md).
166
+ */
167
+ function applyExtensionMapping(fileName, extensionMap) {
168
+ if (!extensionMap || fileName === 'README.md') {
169
+ return fileName;
170
+ }
171
+ const dot = fileName.lastIndexOf('.');
172
+ if (dot === -1) return fileName;
173
+ const ext = fileName.slice(dot);
174
+ const newExt = extensionMap[ext];
175
+ return newExt ? fileName.slice(0, dot) + newExt : fileName;
176
+ }
177
+
178
+ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, destinationDir, extensionMap }) {
164
179
  const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
165
180
  const sourceRoot = path.join(repoRoot || '', normalizedSourcePath);
166
181
 
@@ -180,7 +195,8 @@ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, dest
180
195
  if (entry.isDirectory()) {
181
196
  const relativeFiles = listRelativeFiles(entryPath);
182
197
  for (const relativeFile of relativeFiles) {
183
- const flattenedFileName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
198
+ const rawName = `${namespace}-${normalizeRelativePath(relativeFile).replace(/\//g, '-')}`;
199
+ const flattenedFileName = applyExtensionMapping(rawName, extensionMap);
184
200
  operations.push(createManagedOperation({
185
201
  moduleId,
186
202
  sourceRelativePath: path.join(normalizedSourcePath, namespace, relativeFile),
@@ -189,10 +205,11 @@ function createFlatRuleOperations({ moduleId, repoRoot, sourceRelativePath, dest
189
205
  }));
190
206
  }
191
207
  } else if (entry.isFile()) {
208
+ const fileName = applyExtensionMapping(entry.name, extensionMap);
192
209
  operations.push(createManagedOperation({
193
210
  moduleId,
194
211
  sourceRelativePath: path.join(normalizedSourcePath, entry.name),
195
- destinationPath: path.join(destinationDir, entry.name),
212
+ destinationPath: path.join(destinationDir, fileName),
196
213
  strategy: 'flatten-copy',
197
214
  }));
198
215
  }