@hasna/hooks 0.5.0 → 0.6.1

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 (37) hide show
  1. package/README.md +81 -8
  2. package/bin/index.js +1756 -382
  3. package/bin/serve.js +5258 -0
  4. package/dist/cf/provision.d.ts +24 -0
  5. package/dist/config.d.ts +18 -0
  6. package/dist/db/legacy-import.d.ts +1 -1
  7. package/dist/db/migrations/004_hooks_table.d.ts +9 -0
  8. package/dist/db/pg-migrations.d.ts +1 -1
  9. package/dist/db/storage-sync.d.ts +26 -6
  10. package/dist/index.d.ts +18 -2
  11. package/dist/index.js +5351 -286
  12. package/dist/lib/custom-install.d.ts +19 -0
  13. package/dist/lib/manifest.d.ts +70 -0
  14. package/dist/lib/resolve.d.ts +19 -0
  15. package/dist/lib/run.d.ts +37 -0
  16. package/dist/lib/store.d.ts +69 -0
  17. package/dist/lib/sync.d.ts +35 -0
  18. package/dist/serve.d.ts +36 -0
  19. package/dist/storage.d.ts +2 -2
  20. package/dist/storage.js +133 -42
  21. package/hooks/codewith-native-common.test.ts +15 -2
  22. package/hooks/hook-scanoutput/README.md +151 -0
  23. package/hooks/hook-scanoutput/package.json +12 -0
  24. package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
  25. package/hooks/hook-scanoutput/src/hook.ts +319 -0
  26. package/hooks/hook-workspace-repos-guard/README.md +63 -0
  27. package/hooks/hook-workspace-repos-guard/package.json +12 -0
  28. package/hooks/hook-workspace-repos-guard/src/hook.test.ts +312 -0
  29. package/hooks/hook-workspace-repos-guard/src/hook.ts +352 -0
  30. package/hooks/hook-workspace-repos-guard/tsconfig.json +21 -0
  31. package/hooks/mention-context/README.md +109 -0
  32. package/hooks/mention-context/package.json +9 -0
  33. package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
  34. package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
  35. package/hooks/mention-context/src/hook.test.ts +68 -0
  36. package/hooks/mention-context/src/test_run_capture.py +345 -0
  37. package/package.json +9 -5
@@ -0,0 +1,109 @@
1
+ # mention-context
2
+
3
+ A `UserPromptSubmit` hook. When a prompt mentions `hasna/<repo>` or `hasnaxyz/<repo>`,
4
+ it injects a short context block for that repository — local checkout HEAD, installed
5
+ version, worktrees, npm version, GitHub default branch and open PRs.
6
+
7
+ This is the first Python hook in this repository. Everything else under `hooks/` is
8
+ TypeScript. The language is not a preference: this hook runs on **every prompt
9
+ submission** under a sub-second budget, and starting a Python interpreter that exits
10
+ immediately when no token matches is measurably cheaper here than the alternative that
11
+ was available when it was written. A TypeScript port is a reasonable future change; it
12
+ is a rewrite, not a move, and it is out of scope for the defect this directory was
13
+ created to fix.
14
+
15
+ ## Layout
16
+
17
+ ```
18
+ src/hasna-mention-context.py the hook
19
+ src/hasna-mention-warm.py the out-of-band cache warmer — see "The two files are a pair"
20
+ src/hook.test.ts bun-test wrapper — runs the Python suite under `bun test`
21
+ src/test_run_capture.py the Python regression suite
22
+ ```
23
+
24
+ ## Running the tests
25
+
26
+ ```bash
27
+ bun test hooks/mention-context # via the wrapper, as CI runs it
28
+ python3 hooks/mention-context/src/test_run_capture.py -v # directly
29
+ ```
30
+
31
+ The wrapper exists so the Python suite runs under the repository's existing `bun test`
32
+ step with no CI workflow change. It **fails** rather than skips when `python3` is
33
+ absent: a skip is indistinguishable from a pass in the summary line, and a regression
34
+ test that can silently not run is not a regression test.
35
+
36
+ ## The two files are a pair, and each resolves the other by directory
37
+
38
+ `hasna-mention-context.py` and `hasna-mention-warm.py` must be installed **into the same
39
+ directory**. Neither takes a configured path for the other; each derives the other's
40
+ location from its own `__file__`. The coupling runs in both directions and the two
41
+ failures look nothing alike.
42
+
43
+ **Warmer missing, hook present — silent.** The hook computes
44
+ `WARM_BIN = <its own dir>/hasna-mention-warm.py` and guards the call with
45
+ `os.path.isfile`, so `request_warm()` simply returns. Nothing raises, nothing is logged,
46
+ and the prompt block still renders. What stops is the cold-cache self-heal: the hook
47
+ hands every degraded or GitHub-less token to the warmer precisely so the *next* prompt
48
+ is clean, and that hand-off never happens. A repository mentioned for the first time is
49
+ degraded once and then stays degraded on every later prompt, instead of being clean
50
+ thereafter. It does not recover on its own, because the hook's own live GitHub probe
51
+ cannot close the gap — by the measurement recorded in the hook's header, `gh api graphql`
52
+ takes 1.02–1.18 s against a whole-hook deadline of 0.900 s, so the probe is killed before
53
+ it can write the cache. The warmer is the only thing that reliably fills it.
54
+
55
+ **Hook missing, warmer present — loud.** The warmer computes
56
+ `HOOK_PATH = <its own dir>/hasna-mention-context.py` and imports it at *module scope*
57
+ (`H = load_hook()`), with no guard, to keep one definition of the cache paths, the entry
58
+ shapes and the sanitizer. An absent hook is an immediate `FileNotFoundError` and the
59
+ warmer does not start at all.
60
+
61
+ Because the warmer imports the hook, the hook's `CACHE_DIR`, cache entry shape and
62
+ sanitizer are a **contract with a second program in this directory**, not private
63
+ details. Changing them means changing both files together.
64
+
65
+ Note that the scheduled warming path does not depend on co-location: the cron entry
66
+ invokes the warmer by absolute path. Co-location is what the hook's self-heal path needs.
67
+
68
+ ## Installation
69
+
70
+ This directory is the source. The hook is installed by copying
71
+ `src/hasna-mention-context.py` to the path registered in the agent's settings
72
+ (`~/.hasna/hooks/bin/hasna-mention-context.py` on the current fleet) and is registered
73
+ there by absolute path under `UserPromptSubmit`.
74
+
75
+ Copy `src/hasna-mention-warm.py` to that **same directory** in the same step, and
76
+ schedule it. On the current fleet it runs from cron at minutes 1, 11, 21, 31, 41 and 51
77
+ under `flock`, and it holds its own lock, so overlapping runs are harmless. Installing
78
+ the hook alone is a supported thing to do — the hook works without the warmer — but it
79
+ costs the self-heal described above, silently, so it should be a decision rather than an
80
+ oversight.
81
+
82
+ Installation is deliberately **not** performed by merging this directory. The hook
83
+ renders into every agent's prompt on every firing, so landing the source and updating
84
+ the live path are two separately verified steps.
85
+
86
+ ## The defect this directory was created to fix
87
+
88
+ `run_capture` derived its temp-file path from its `tag` argument alone, and
89
+ `probe_local_head` passed a constant `tag="gitlog"`. Repository probes run concurrently
90
+ against one shared temp directory, so every mentioned repository's `git log` wrote to
91
+ and read back the same `gitlog.out`, and the reader got whatever the last writer left.
92
+
93
+ The emitted value was always a **real sha from a real repository — just the wrong
94
+ one**, which is why it read as correct. Four consecutive firings, each a different wrong
95
+ pairing:
96
+
97
+ ```
98
+ 17:44 loops=82a3acf (right) accounts=27cffd7 (right) logs=absent
99
+ 17:48 loops=27cffd7 (WRONG) logs=82a3acf (WRONG) <- clean swap
100
+ 18:11 loops=27cffd7 (WRONG) logs=146a70e (right)
101
+ 18:17 loops=82a3acf (right) logs=82a3acf (WRONG) <- duplicate sha
102
+ ```
103
+
104
+ The race needs two or three mentions in one prompt. `MAX_TOKENS = 3`, so a single-repo
105
+ mention produces one probe, no concurrency, and always the correct answer.
106
+
107
+ The fix is in `run_capture`, which owns path construction, rather than at the call site:
108
+ threading `org`/`word` into `probe_local_head` would have matched its siblings but left
109
+ the invariant unenforced, so a fifth call site added later would inherit the bug.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "mention-context",
3
+ "version": "0.1.0",
4
+ "description": "UserPromptSubmit hook that injects repository context for @hasna/<repo> mentions",
5
+ "type": "module",
6
+ "main": "./src/hasna-mention-context.py",
7
+ "author": "Hasna",
8
+ "license": "Apache-2.0"
9
+ }