@ngockhoale/ukit 2.7.6 → 2.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/package.json +1 -1
- package/scripts/install/sync-installed-mirror.mjs +250 -0
- package/scripts/perf/audit-perf.mjs +287 -36
- package/scripts/perf/diff-perf-findings.mjs +136 -0
- package/scripts/perf/perf-findings.json +260 -206
- package/scripts/perf/perf-measure.md +271 -0
- package/src/core/hookChainDoctor.js +65 -2
- package/src/core/memory/store.js +22 -1
- package/src/core/taskBudgetValidator.js +9 -6
- package/src/core/unattendedDoctor.js +8 -1
- package/templates/.claude/hooks/auto-prune-bash.sh +19 -0
- package/templates/.claude/hooks/reinject-context.sh +22 -0
- package/templates/.claude/hooks/reset-compact-pressure.sh +19 -0
- package/templates/.claude/hooks/session-episode.sh +20 -0
- package/templates/.claude/ukit/index/task-budget-validator.mjs +6 -2
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +118 -20
- package/templates/.claude/ukit/runtime/hook-telemetry.mjs +84 -12
- package/templates/.claude/ukit/runtime/hook-telemetry.sh +50 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# perf-measure.md — raw measurement notes (TASK-233)
|
|
2
|
+
|
|
3
|
+
Generated: 2026-09-20T23:07:33.744Z
|
|
4
|
+
|
|
5
|
+
Appendix source for `docs/AI_REPORT/AI_REVIEW_BUGS_REPORT.md`. Every number in
|
|
6
|
+
`perf-findings.json` comes from one of the four sources below. TASK-236 re-runs
|
|
7
|
+
the identical command for the before/after table (SPEC §5). Two numbers from two
|
|
8
|
+
different snapshots are only comparable once their provenance line agrees.
|
|
9
|
+
|
|
10
|
+
## 1. Reproduce
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
node scripts/perf/audit-perf.mjs \
|
|
14
|
+
--telemetry-dir "/Volumes/KHOA_EXTENAL/WORKING_PROJECT/WORKING/UKit/.ukit/storage/cache/hook-latency" \
|
|
15
|
+
--out "/Volumes/KHOA_EXTENAL/WORKING_PROJECT/WORKING/UKit/scripts/perf/perf-findings.json"
|
|
16
|
+
node scripts/perf/diff-perf-findings.mjs <earlier-snapshot>.json scripts/perf/perf-findings.json
|
|
17
|
+
node --test tests/handoff/c33/perfFindings.test.js
|
|
18
|
+
|
|
19
|
+
- root: `/Volumes/KHOA_EXTENAL/WORKING_PROJECT/WORKING/UKit`
|
|
20
|
+
- settings.json: `/Volumes/KHOA_EXTENAL/WORKING_PROJECT/WORKING/UKit/templates/.claude/settings.json`
|
|
21
|
+
- hooks resolved from: `NOT FOUND`
|
|
22
|
+
- telemetry: `/Volumes/KHOA_EXTENAL/WORKING_PROJECT/WORKING/UKit/.ukit/storage/cache/hook-latency` — 62854 rows in 120 files
|
|
23
|
+
- snapshot provenance: generatedAt `2026-09-20T23:07:33.744Z`, telemetryRows: 62854, telemetryFiles: 120 — hooks append continuously, so a report is a point-in-time view, not a stable dataset
|
|
24
|
+
- benchmark iterations per subject: 2
|
|
25
|
+
|
|
26
|
+
## 2. Process floor (live spawnSync, ms)
|
|
27
|
+
|
|
28
|
+
| subject | p50 | p95 | max |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `bash -c true` | 3.13 | 3.13 | 3.13 |
|
|
31
|
+
| `node -e 0` | 16.11 | 16.11 | 16.11 |
|
|
32
|
+
| `node hook-telemetry.mjs --finish` | 19.50 | 19.50 | 19.50 |
|
|
33
|
+
|
|
34
|
+
A hook row = wrapper bash + node runtime + advisory telemetry child = **3 processes**.
|
|
35
|
+
|
|
36
|
+
## 3. Telemetry append overhead (O5 — in-process `appendTelemetryRow`, ms)
|
|
37
|
+
|
|
38
|
+
| subject | iterations | p50 | p95 | max |
|
|
39
|
+
|---|---|---|---|---|
|
|
40
|
+
| `appendTelemetryRow` → throwaway temp root | 2 | 0.407 | 0.407 | 0.407 |
|
|
41
|
+
|
|
42
|
+
Writer: `templates/.claude/ukit/runtime/hook-telemetry.mjs`. Measured against a temp project root, so auditing a tree never adds rows to the dataset it is counting.
|
|
43
|
+
|
|
44
|
+
## 4. Per-hook standalone re-run (`/bin/bash <script>` with `{}` on stdin, ms)
|
|
45
|
+
| hook | p50 | p95 | max |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| _(no settings hook resolvable from this root)_ | | | |
|
|
48
|
+
|
|
49
|
+
## 5. Router/index helper cold start (ms)
|
|
50
|
+
|
|
51
|
+
| helper | p50 | p95 | max |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `skill-router.sh` | 105.2 | 105.2 | 105.2 |
|
|
54
|
+
| `route-task.mjs` | 26.4 | 26.4 | 26.4 |
|
|
55
|
+
| `query-index.mjs` | 23.6 | 23.6 | 23.6 |
|
|
56
|
+
| `resolve-context.mjs` | 21.9 | 21.9 | 21.9 |
|
|
57
|
+
|
|
58
|
+
## 6. Per-hook telemetry (every hook with rows)
|
|
59
|
+
|
|
60
|
+
| hook | n | p50 | p95 | max | total |
|
|
61
|
+
|---|---|---|---|---|---|
|
|
62
|
+
| `hook-chain-runner` | 9453 | 80 | 375 | 2020 | 1269815 |
|
|
63
|
+
| `block-dangerous.sh` | 5107 | 83 | 237 | 426 | 461434 |
|
|
64
|
+
| `sensitive-data-guard.sh` | 5001 | 45 | 91 | 1591 | 398855 |
|
|
65
|
+
| `context-hardcap-gate.sh` | 5650 | 55 | 88 | 291 | 334184 |
|
|
66
|
+
| `auto-allow-bash.sh` | 4864 | 50 | 86 | 285 | 264888 |
|
|
67
|
+
| `compress-output.sh` | 4701 | 49 | 66 | 207 | 237025 |
|
|
68
|
+
| `record-execution.sh` | 4880 | 39 | 63 | 178 | 207226 |
|
|
69
|
+
| `handoff-model-guard.sh` | 5603 | 22 | 53 | 189 | 136670 |
|
|
70
|
+
| `verification-guard.sh` | 3520 | 33 | 61 | 137 | 136027 |
|
|
71
|
+
| `skill-router.sh` | 1089 | 52 | 134 | 311 | 68680 |
|
|
72
|
+
| `protect-files.sh` | 1544 | 21 | 156 | 344 | 58507 |
|
|
73
|
+
| `task-watchdog.sh` | 930 | 38 | 216 | 237 | 42736 |
|
|
74
|
+
| `pre-edit-backup.sh` | 732 | 50 | 72 | 216 | 35743 |
|
|
75
|
+
| `stale-spec-guard.sh` | 745 | 46 | 70 | 169 | 34187 |
|
|
76
|
+
| `post-edit-verify.sh` | 716 | 43 | 60 | 145 | 32444 |
|
|
77
|
+
| `vision-router.sh` | 325 | 73 | 220 | 395 | 29834 |
|
|
78
|
+
| `record-execution.mjs` | 2864 | 5 | 16 | 53 | 17607 |
|
|
79
|
+
| `context-window-guard.sh` | 325 | 45 | 84 | 109 | 15798 |
|
|
80
|
+
| `sensitive-data-guard.mjs` | 2812 | 4 | 6 | 15 | 11498 |
|
|
81
|
+
| `completion-gate.sh` | 119 | 80 | 108 | 174 | 9881 |
|
|
82
|
+
| `handoff-resume.sh` | 122 | 31 | 55 | 97 | 4307 |
|
|
83
|
+
| `block-dangerous.mjs` | 1632 | 1 | 2 | 3 | 2334 |
|
|
84
|
+
| `project-important.sh` | 47 | 38 | 61 | 62 | 1962 |
|
|
85
|
+
| `auto-prune-bash.sh` | 19 | 34 | 44 | 44 | 679 |
|
|
86
|
+
| `reset-compact-pressure.sh` | 19 | 34 | 38 | 38 | 647 |
|
|
87
|
+
| `probe-source-only.sh` | 20 | 10 | 22 | 22 | 216 |
|
|
88
|
+
| `probe-s2.sh` | 15 | 10 | 25 | 25 | 164 |
|
|
89
|
+
|
|
90
|
+
## 7. Stdin stage vs execution split (O3 — the wall-time tail attributed)
|
|
91
|
+
|
|
92
|
+
A row's `elapsedMs` is the stdin stage plus the hook's own execution. `stageMs`
|
|
93
|
+
(direct rows) and `stdinStageMs` (chain rows) are the first half — the producer's
|
|
94
|
+
time, not the hook's — and `exec` below is the remainder, derived from the SAME
|
|
95
|
+
row so the two halves always sum to it. A row carrying neither field predates the
|
|
96
|
+
split or never staged stdin: it is excluded, never counted as a 0ms stage.
|
|
97
|
+
|
|
98
|
+
- rows with the split: 0 of 62854
|
|
99
|
+
- overall: stage n/ams p50 / n/ams p95, exec n/ams p50 / n/ams p95
|
|
100
|
+
|
|
101
|
+
| hook | field | n | stage p50 | stage p95 | exec p50 | exec p95 |
|
|
102
|
+
|---|---|---|---|---|---|---|
|
|
103
|
+
| _(no row carries the split on this tree)_ | | | | | | |
|
|
104
|
+
|
|
105
|
+
## 8. Per tool / per event cost (chain-runner total where present, else direct sum)
|
|
106
|
+
|
|
107
|
+
| scope | calls | cost p50 | cost p95 | hook rows/call p50 | direct / chain-runner calls |
|
|
108
|
+
|---|---|---|---|---|---|
|
|
109
|
+
| PostToolUse Bash | 4701 | 87 | 150 | 2 | 2406 / 2295 |
|
|
110
|
+
| PostToolUse Edit | 610 | 129 | 222 | 3 | 351 / 259 |
|
|
111
|
+
| PostToolUse Glob | 60 | 78 | 117 | 2 | 0 / 60 |
|
|
112
|
+
| PostToolUse Grep | 275 | 15 | 101 | 2 | 0 / 275 |
|
|
113
|
+
| PostToolUse Read | 1994 | 37 | 122 | 2 | 393 / 1601 |
|
|
114
|
+
| PostToolUse Write | 103 | 132 | 219 | 4 | 46 / 57 |
|
|
115
|
+
| PreToolUse Bash | 4853 | 307 | 474 | 6 | 2519 / 2334 |
|
|
116
|
+
| PreToolUse Edit | 616 | 328 | 432 | 6 | 356 / 260 |
|
|
117
|
+
| PreToolUse Glob | 60 | 59 | 74 | 2 | 0 / 60 |
|
|
118
|
+
| PreToolUse Grep | 275 | 7 | 74 | 2 | 0 / 275 |
|
|
119
|
+
| PreToolUse Read | 2005 | 39 | 71 | 2 | 404 / 1601 |
|
|
120
|
+
| PreToolUse Write | 112 | 344 | 418 | 7 | 46 / 66 |
|
|
121
|
+
| event (unattributed) (clustered, gap ≤ 1500ms) | 775 | 313 | 1567 | 1 | 752 / 23 |
|
|
122
|
+
| event PostToolUse (clustered, gap ≤ 1500ms) | 7886 | 84 | 154 | 2 | 3339 / 4547 |
|
|
123
|
+
| event PreCompact (clustered, gap ≤ 1500ms) | 6 | 49 | 53 | 1 | 0 / 6 |
|
|
124
|
+
| event PreToolUse (clustered, gap ≤ 1500ms) | 8045 | 255 | 442 | 6 | 3431 / 4614 |
|
|
125
|
+
| event SessionEnd (clustered, gap ≤ 1500ms) | 1 | 2020 | 2020 | 1 | 0 / 1 |
|
|
126
|
+
| event SessionStart (clustered, gap ≤ 1500ms) | 113 | 119 | 289 | 2 | 38 / 75 |
|
|
127
|
+
| event Stop (clustered, gap ≤ 1500ms) | 116 | 81 | 111 | 1 | 113 / 3 |
|
|
128
|
+
| event UserPromptSubmit (clustered, gap ≤ 1500ms) | 349 | 291 | 582 | 4 | 200 / 149 |
|
|
129
|
+
|
|
130
|
+
## 9. Hook multiplicity proof (repeat-fire check)
|
|
131
|
+
|
|
132
|
+
| hook | max fires in one tool call | calls observed | fire-count distribution |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `auto-allow-bash.sh` | 6 | 4859 | {"1":4858,"6":1} |
|
|
135
|
+
| `auto-prune-bash.sh` | 2 | 17 | {"1":15,"2":2} |
|
|
136
|
+
| `block-dangerous.mjs` | 1 | 1632 | {"1":1632} |
|
|
137
|
+
| `block-dangerous.sh` | 24 | 3651 | {"1":3475,"2":59,"3":55,"6":1,"20":1,"22":58,"23":1,"24":1} |
|
|
138
|
+
| `completion-gate.sh` | 3 | 117 | {"1":116,"3":1} |
|
|
139
|
+
| `compress-output.sh` | 3 | 4699 | {"1":4698,"3":1} |
|
|
140
|
+
| `context-hardcap-gate.sh` | 46 | 5601 | {"1":5599,"5":1,"46":1} |
|
|
141
|
+
| `context-window-guard.sh` | 4 | 295 | {"1":272,"2":17,"3":5,"4":1} |
|
|
142
|
+
| `handoff-model-guard.sh` | 46 | 5553 | {"1":5550,"2":1,"5":1,"46":1} |
|
|
143
|
+
| `handoff-resume.sh` | 4 | 109 | {"1":99,"2":8,"3":1,"4":1} |
|
|
144
|
+
| `post-edit-verify.sh` | 4 | 713 | {"1":712,"4":1} |
|
|
145
|
+
| `pre-edit-backup.sh` | 6 | 727 | {"1":726,"6":1} |
|
|
146
|
+
| `probe-s2.sh` | 15 | 1 | {"15":1} |
|
|
147
|
+
| `probe-source-only.sh` | 20 | 1 | {"20":1} |
|
|
148
|
+
| `project-important.sh` | 5 | 37 | {"1":33,"2":1,"3":1,"4":1,"5":1} |
|
|
149
|
+
| `protect-files.sh` | 26 | 992 | {"1":881,"2":57,"3":1,"10":52,"26":1} |
|
|
150
|
+
| `record-execution.mjs` | 1 | 2864 | {"1":2864} |
|
|
151
|
+
| `record-execution.sh` | 4 | 4875 | {"1":4873,"3":1,"4":1} |
|
|
152
|
+
| `reset-compact-pressure.sh` | 2 | 17 | {"1":15,"2":2} |
|
|
153
|
+
| `sensitive-data-guard.mjs` | 3 | 2806 | {"1":2801,"2":4,"3":1} |
|
|
154
|
+
| `sensitive-data-guard.sh` | 21 | 4949 | {"1":4927,"2":15,"3":3,"4":1,"5":2,"21":1} |
|
|
155
|
+
| `skill-router.sh` | 26 | 1029 | {"1":1000,"2":22,"3":5,"4":1,"26":1} |
|
|
156
|
+
| `stale-spec-guard.sh` | 6 | 736 | {"1":731,"2":4,"6":1} |
|
|
157
|
+
| `task-watchdog.sh` | 4 | 925 | {"1":922,"2":2,"4":1} |
|
|
158
|
+
| `verification-guard.sh` | 6 | 3515 | {"1":3514,"6":1} |
|
|
159
|
+
| `vision-router.sh` | 4 | 295 | {"1":272,"2":17,"3":5,"4":1} |
|
|
160
|
+
|
|
161
|
+
## 10. hook-chain-runner nested per-script (already-consolidated omp path)
|
|
162
|
+
|
|
163
|
+
| script | n | p50 | p95 | max |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| `record-execution.mjs` | 2864 | 5 | 16 | 53 |
|
|
166
|
+
| `sensitive-data-guard.mjs` | 2816 | 4 | 6 | 15 |
|
|
167
|
+
| `handoff-model-guard.sh` | 2641 | 39 | 58 | 166 |
|
|
168
|
+
| `context-hardcap-gate.sh` | 2635 | 68 | 82 | 152 |
|
|
169
|
+
| `auto-allow-bash.sh` | 2350 | 72 | 82 | 426 |
|
|
170
|
+
| `compress-output.sh` | 2295 | 73 | 92 | 169 |
|
|
171
|
+
| `verification-guard.sh` | 2290 | 55 | 67 | 305 |
|
|
172
|
+
| `record-execution.sh` | 1683 | 66 | 129 | 253 |
|
|
173
|
+
| `block-dangerous.mjs` | 1636 | 1 | 2 | 3 |
|
|
174
|
+
| `sensitive-data-guard.sh` | 1634 | 60 | 76 | 203 |
|
|
175
|
+
| `block-dangerous.sh` | 737 | 79 | 150 | 425 |
|
|
176
|
+
| `skill-router.sh` | 486 | 69 | 166 | 280 |
|
|
177
|
+
| `protect-files.sh` | 336 | 50 | 59 | 360 |
|
|
178
|
+
| `stale-spec-guard.sh` | 336 | 57 | 70 | 149 |
|
|
179
|
+
| `pre-edit-backup.sh` | 325 | 59 | 72 | 115 |
|
|
180
|
+
| `post-edit-verify.sh` | 316 | 67 | 90 | 184 |
|
|
181
|
+
| `task-watchdog.sh` | 316 | 59 | 72 | 118 |
|
|
182
|
+
| `vision-router.sh` | 160 | 83 | 161 | 231 |
|
|
183
|
+
| `context-window-guard.sh` | 160 | 59 | 74 | 86 |
|
|
184
|
+
| `project-important.sh` | 87 | 15 | 104 | 529 |
|
|
185
|
+
| `auto-prune-bash.sh` | 86 | 26 | 64 | 75 |
|
|
186
|
+
| `reset-compact-pressure.sh` | 86 | 26 | 56 | 70 |
|
|
187
|
+
| `handoff-resume.sh` | 86 | 56 | 69 | 86 |
|
|
188
|
+
| `reinject-context.sh` | 6 | 49 | 53 | 53 |
|
|
189
|
+
| `completion-gate.sh` | 3 | 111 | 134 | 134 |
|
|
190
|
+
| `t234-silent.sh` | 3 | 2 | 106 | 106 |
|
|
191
|
+
| `session-episode.sh` | 1 | 2020 | 2020 | 2020 |
|
|
192
|
+
|
|
193
|
+
## 11. settings.json groups (spawn budget per call)
|
|
194
|
+
|
|
195
|
+
| event | matcher | hooks | registered timeouts | spawns/call |
|
|
196
|
+
|---|---|---|---|---|
|
|
197
|
+
| PreToolUse | Read|Grep|Glob | sensitive-data-guard.mjs:8 | 18s | 3 |
|
|
198
|
+
| PreToolUse | Edit|Write | context-hardcap-gate.sh:8 | 73s | 3 |
|
|
199
|
+
| PreToolUse | Bash | verification-guard.sh:8 | 65s | 3 |
|
|
200
|
+
| PostToolUse | Read|Grep|Glob | record-execution.mjs:8 | 18s | 3 |
|
|
201
|
+
| PostToolUse | Edit|Write | task-watchdog.sh:8 | 34s | 3 |
|
|
202
|
+
| PostToolUse | Bash | record-execution.mjs:8 | 30s | 3 |
|
|
203
|
+
| UserPromptSubmit | (all) | context-window-guard.sh:10 | 60s | 3 |
|
|
204
|
+
| Stop | (all) | completion-gate.sh:8 | 18s | 3 |
|
|
205
|
+
| PreCompact | (all) | reinject-context.sh:8 | 18s | 3 |
|
|
206
|
+
| SessionStart | (all) | handoff-resume.sh:8 | 50s | 3 |
|
|
207
|
+
| SessionEnd | (all) | session-episode.sh:8 | 18s | 3 |
|
|
208
|
+
|
|
209
|
+
## 12. Recent hot-path additions (`git log --diff-filter=A`)
|
|
210
|
+
|
|
211
|
+
| commit | date | subject | files added |
|
|
212
|
+
|---|---|---|---|
|
|
213
|
+
| `c0443e73` | 2026-09-20 | handoff: wave 2 — TASK-003 (block-dangerous.mjs port) + TASK-004 (record-execution.mjs + ledger emitter) + manifest module entries | `templates/.claude/hooks/block-dangerous.mjs`, `templates/.claude/hooks/record-execution.mjs` |
|
|
214
|
+
| `79f747b6` | 2026-09-20 | handoff: wave 1 — TASK-001 (runner .mjs module-steps) + TASK-002 (sensitive-data-guard port + field-salvage) | `templates/.claude/hooks/sensitive-data-guard.mjs`, `templates/.claude/ukit/runtime/hook-field-salvage.mjs` |
|
|
215
|
+
| `6b339905` | 2026-09-20 | handoff: wave 3 — TASK-231, TASK-232 | `templates/.claude/hooks/session-episode.sh` |
|
|
216
|
+
| `4903ee62` | 2026-09-18 | handoff: restore wave-4a files removed by stale worktree diff | `templates/.claude/hooks/project-important.sh` |
|
|
217
|
+
| `2b367850` | 2026-09-18 | handoff: wave 4a — TASK-041 (sessionstart wiring, hook template+manifest) | `templates/.claude/hooks/project-important.sh` |
|
|
218
|
+
| `5ffefb09` | 2026-09-18 | handoff: wave 3 — TASK-038 | `templates/.claude/ukit/runtime/project-important.mjs`, `templates/.claude/ukit/runtime/sensitive-value-scanner.mjs` |
|
|
219
|
+
| `ebb846b8` | 2026-09-17 | handoff: R4.5 fix round 1 — TASK-018, TASK-023 | `templates/.claude/ukit/runtime/hook-chain-budget.mjs` |
|
|
220
|
+
| `c9da9734` | 2026-09-17 | handoff: wave 6 — TASK-025 copy-back (Stop coordinator) | `templates/.claude/ukit/runtime/stop-coordinator.mjs` |
|
|
221
|
+
| `a1361afd` | 2026-09-17 | handoff: wave 5 — TASK-031 copy-back | `templates/.claude/ukit/runtime/hook-payload-store.mjs` |
|
|
222
|
+
| `66787f98` | 2026-09-17 | handoff: wave 5 — TASK-019 copy-back | `templates/.claude/ukit/runtime/hook-telemetry.mjs`, `templates/.claude/ukit/runtime/hook-telemetry.sh` |
|
|
223
|
+
| `3afc88e6` | 2026-09-17 | handoff: wave 5 — TASK-029 copy-back (context capacity negotiation) | `templates/.claude/ukit/runtime/context-capacity.mjs` |
|
|
224
|
+
| `6f224875` | 2026-09-17 | handoff: wave 3 — TASK-028 copy-back | `templates/.claude/ukit/runtime/async-lock.mjs` |
|
|
225
|
+
| `e4b647a3` | 2026-09-17 | handoff: wave 3 — TASK-016 copy-back | `templates/.claude/ukit/runtime/hook-process.mjs` |
|
|
226
|
+
| `7aa89361` | 2026-09-17 | handoff: wave 2 — TASK-015, TASK-017 | `templates/.claude/ukit/runtime/transcript-tail.mjs` |
|
|
227
|
+
| `2652bbff` | 2026-09-17 | milestone: TASK-014 bounded stdin transport (H01) | `templates/.claude/ukit/runtime/hook-input.mjs`, `templates/.claude/ukit/runtime/hook-input.sh` |
|
|
228
|
+
| `6b40da83` | 2026-09-12 | handoff: wave 1 batch 2 — TASK-016 wall-clock watchdog hook (hardPolicy=split) | `templates/.claude/hooks/task-watchdog.sh`, `templates/.claude/ukit/runtime/task-watchdog.mjs` |
|
|
229
|
+
| `0fc81692` | 2026-09-12 | handoff: wave 1 fix — anchor runtime-dir ignores, track lost TASK-014 CLI twin | `templates/.claude/ukit/index/task-budget-validator.mjs` |
|
|
230
|
+
| `cfc3b7f1` | 2026-09-08 | 2.3.0 — fail-closed sensitive-data gate; contract tables unified to executionContracts.js; tierLane wiring; evidence + vision hardening | `templates/.claude/hooks/sensitive-data-guard.sh` |
|
|
231
|
+
|
|
232
|
+
## 13. Findings emitted
|
|
233
|
+
|
|
234
|
+
| id | evidence | p50 | p95 | status | fixTask |
|
|
235
|
+
|---|---|---|---|---|---|
|
|
236
|
+
| `chain-pretooluse-read-grep-glob` | telemetry | 37 | 72 | confirmed | TASK-234 |
|
|
237
|
+
| `chain-pretooluse-edit-write` | telemetry | 331 | 429 | confirmed | TASK-234 |
|
|
238
|
+
| `chain-pretooluse-bash` | telemetry | 307 | 474 | confirmed | TASK-234 |
|
|
239
|
+
| `chain-posttooluse-read-grep-glob` | telemetry | 37 | 118 | confirmed | TASK-234 |
|
|
240
|
+
| `chain-posttooluse-edit-write` | telemetry | 130 | 222 | confirmed | TASK-234 |
|
|
241
|
+
| `chain-posttooluse-bash` | telemetry | 87 | 150 | confirmed | TASK-234 |
|
|
242
|
+
| `chain-userpromptsubmit-all` | telemetry | 291 | 582 | confirmed | TASK-234 |
|
|
243
|
+
| `chain-stop-all` | telemetry | 81 | 111 | confirmed | TASK-234 |
|
|
244
|
+
| `chain-precompact-all` | telemetry | 49 | 53 | confirmed | TASK-234 |
|
|
245
|
+
| `chain-sessionstart-all` | telemetry | 119 | 289 | confirmed | TASK-234 |
|
|
246
|
+
| `chain-sessionend-all` | telemetry | 2020 | 2020 | confirmed | TASK-234 |
|
|
247
|
+
| `hook-hook-chain-runner` | telemetry | 80 | 375 | confirmed | TASK-234 |
|
|
248
|
+
| `hook-skill-router-sh` | telemetry | 52 | 134 | confirmed | TASK-234 |
|
|
249
|
+
| `hook-vision-router-sh` | telemetry | 73 | 220 | confirmed | TASK-234 |
|
|
250
|
+
| `hook-block-dangerous-sh` | telemetry | 83 | 237 | confirmed | TASK-234 |
|
|
251
|
+
| `hook-context-hardcap-gate-sh` | telemetry | 55 | 88 | confirmed | TASK-234 |
|
|
252
|
+
| `hook-completion-gate-sh` | telemetry | 80 | 108 | confirmed | TASK-234 |
|
|
253
|
+
| `hook-project-important-tail` | telemetry | n/a | 61 | confirmed | TASK-235 |
|
|
254
|
+
| `record-execution-multiplicity` | telemetry | 39 | 63 | not-a-bug | - |
|
|
255
|
+
| `index-router-cold-start` | telemetry | 291 | 582 | confirmed | TASK-234 |
|
|
256
|
+
| `c29-c32-hot-path-additions` | git-log | n/a | n/a | confirmed | TASK-234 |
|
|
257
|
+
| `existing-chain-runner-baseline` | telemetry | 80 | 375 | confirmed | TASK-234 |
|
|
258
|
+
| `process-floor` | micro-benchmark | 38.73741700000001 | 38.73741700000001 | not-a-bug | TASK-234 |
|
|
259
|
+
| `telemetry-append-overhead` | micro-benchmark | 0.407125 | 0.407125 | not-a-bug | - |
|
|
260
|
+
| `stdin-stage-vs-exec-split` | unavailable | n/a | n/a | deferred | - |
|
|
261
|
+
| `hook-sensitive-data-guard-mjs-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
262
|
+
| `hook-context-hardcap-gate-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
263
|
+
| `hook-verification-guard-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
264
|
+
| `hook-record-execution-mjs-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
265
|
+
| `hook-task-watchdog-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
266
|
+
| `hook-context-window-guard-sh-10-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
267
|
+
| `hook-completion-gate-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
268
|
+
| `hook-reinject-context-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
269
|
+
| `hook-handoff-resume-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
270
|
+
| `hook-session-episode-sh-8-unmeasured` | unavailable | n/a | n/a | deferred | - |
|
|
271
|
+
|
|
@@ -26,6 +26,10 @@ const FAILURE_OUTCOMES = new Set([
|
|
|
26
26
|
'signal',
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
// The pre-O6 remedy, kept verbatim: a failure with no gate evidence must read
|
|
30
|
+
// exactly as it did before escalation existed (SPEC §FR-012).
|
|
31
|
+
const BASE_REMEDY = 'Inspect recent rows in .ukit/storage/cache/hook-latency/ for the failing scriptName/failureKind and fix or widen the gate budget.';
|
|
32
|
+
|
|
29
33
|
function telemetryDirFor(projectRoot) {
|
|
30
34
|
return path.join(projectRoot, '.ukit', 'storage', 'cache', 'hook-latency');
|
|
31
35
|
}
|
|
@@ -80,6 +84,13 @@ export async function inspectHookChainHealth({ projectRoot, now = Date.now() } =
|
|
|
80
84
|
|
|
81
85
|
const counts = { rowsScanned: 0, filesScanned: files.length, ok: 0 };
|
|
82
86
|
let rowsLeft = MAX_ROWS;
|
|
87
|
+
// O6 (SPEC §FR-012): a failure that landed on a fail-closed gate is a
|
|
88
|
+
// different signal from a generic timeout — it means a real budget is too
|
|
89
|
+
// tight (or a gate broke), not that "something was slow". These are the two
|
|
90
|
+
// evidence shapes that prove it, collected in the same pass that counts
|
|
91
|
+
// outcomes so no second read of the rows is needed.
|
|
92
|
+
const gateFailures = new Set();
|
|
93
|
+
let budgetExhaustedChains = 0;
|
|
83
94
|
|
|
84
95
|
for (const file of files) {
|
|
85
96
|
if (rowsLeft <= 0) break;
|
|
@@ -102,6 +113,34 @@ export async function inspectHookChainHealth({ projectRoot, now = Date.now() } =
|
|
|
102
113
|
counts.rowsScanned += 1;
|
|
103
114
|
const outcome = typeof row.outcome === 'string' && row.outcome.length > 0 ? row.outcome : 'ok';
|
|
104
115
|
counts[outcome] = (counts[outcome] ?? 0) + 1;
|
|
116
|
+
|
|
117
|
+
// Escalation evidence is only ever read off a FAILING row — a gate that
|
|
118
|
+
// ran fine is not evidence of anything, and SPEC §FR-012 scopes the
|
|
119
|
+
// budget signal to a non-ok row ("a non-ok row ... escalates only when
|
|
120
|
+
// its own outcome is budget-exhausted").
|
|
121
|
+
if (!FAILURE_OUTCOMES.has(outcome)) continue;
|
|
122
|
+
// A row that exhausted the chain budget never ran its later steps; that is
|
|
123
|
+
// the tight-budget signal whether or not any per-script detail survived
|
|
124
|
+
// into `scripts[]`. Recorded alongside the per-script scan below, never
|
|
125
|
+
// instead of it: one row can carry both a failed gate AND exhaustion
|
|
126
|
+
// (a gate step is added with failureKind 'budget-exhausted' when the
|
|
127
|
+
// budget ran out before it), and dropping either would lose signal.
|
|
128
|
+
if (row.budgetExhausted === true || outcome === 'budget-exhausted') {
|
|
129
|
+
budgetExhaustedChains += 1;
|
|
130
|
+
}
|
|
131
|
+
// Each failing entry names the step that failed. `failClosed` is the
|
|
132
|
+
// runner's OWN verdict on which paths it gated — the doctor reads that
|
|
133
|
+
// flag and never re-derives the gate list: a second copy of a
|
|
134
|
+
// security-relevant registry is a drift hazard, and only the runner knows
|
|
135
|
+
// which paths it treated as gates. Malformed entries are skipped, never
|
|
136
|
+
// fatal (a liveness/doctor check must not throw on hostile telemetry).
|
|
137
|
+
if (!Array.isArray(row.scripts)) continue;
|
|
138
|
+
for (const entry of row.scripts) {
|
|
139
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
140
|
+
if (entry.failClosed !== true) continue;
|
|
141
|
+
if (typeof entry.failureKind !== 'string' || entry.failureKind === 'ok') continue;
|
|
142
|
+
gateFailures.add(typeof entry.scriptName === 'string' ? entry.scriptName : 'unknown');
|
|
143
|
+
}
|
|
105
144
|
}
|
|
106
145
|
}
|
|
107
146
|
|
|
@@ -136,14 +175,38 @@ export async function inspectHookChainHealth({ projectRoot, now = Date.now() } =
|
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
const breakdown = failureKinds.map((kind) => `${kind}=${counts[kind]}`).join(', ');
|
|
178
|
+
const baseDetail = `${counts.rowsScanned} chain row(s) scanned — fail-closed outcomes: ${breakdown}`;
|
|
179
|
+
|
|
180
|
+
// O6 (SPEC §FR-012): escalate only when a failure actually landed ON a gate —
|
|
181
|
+
// a gate that merely ran is not evidence. The escalation is purely additive:
|
|
182
|
+
// `baseDetail`/`BASE_REMEDY` stay the leading clause so the no-escalation
|
|
183
|
+
// branch is still byte-identical to the pre-O6 output, and a warning still
|
|
184
|
+
// never blocks (severity/remediationClass are unchanged), so doctor's exit
|
|
185
|
+
// code is unaffected. This is a richer signal, not a new alert channel (§14).
|
|
186
|
+
const gateList = [...gateFailures].sort();
|
|
187
|
+
const escalations = [];
|
|
188
|
+
const advices = [];
|
|
189
|
+
if (gateList.length > 0) {
|
|
190
|
+
escalations.push(`fail-closed gate(s) failed: ${gateList.join(', ')}`);
|
|
191
|
+
advices.push(
|
|
192
|
+
`A fail-closed gate actually failed (${gateList.join(', ')}) — that is a real budget or path problem, not a slow advisory hook; widen the failing gate's budget or fix that step.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (budgetExhaustedChains > 0) {
|
|
196
|
+
escalations.push(`${budgetExhaustedChains} chain row(s) exhausted the total budget`);
|
|
197
|
+
advices.push(
|
|
198
|
+
`The chain total budget was exhausted before every step ran (${budgetExhaustedChains} row(s)) — raise it (UKIT_HOOK_CHAIN_BASE_MS or the per-script ":N" suffix) so the later steps, including any gate, still run.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
139
202
|
return {
|
|
140
203
|
label,
|
|
141
204
|
passed: false,
|
|
142
205
|
failed: true,
|
|
143
206
|
severity: 'warning',
|
|
144
207
|
remediationClass: 'advisory',
|
|
145
|
-
detail:
|
|
146
|
-
remedy:
|
|
208
|
+
detail: escalations.length > 0 ? `${baseDetail}; escalated: ${escalations.join('; ')}` : baseDetail,
|
|
209
|
+
remedy: [BASE_REMEDY, ...advices].join(' '),
|
|
147
210
|
counts,
|
|
148
211
|
};
|
|
149
212
|
}
|
package/src/core/memory/store.js
CHANGED
|
@@ -480,6 +480,13 @@ async function readProjectMemoryForId(runtimePaths, projectId) {
|
|
|
480
480
|
|
|
481
481
|
const DEFAULT_MAX_ARCHIVED_SESSIONS = 50;
|
|
482
482
|
|
|
483
|
+
// Same end-time key `archiveSessions` uses to decide whether a session is expired, so
|
|
484
|
+
// "oldest" means the same thing on both sides of the archive boundary.
|
|
485
|
+
function archivedSessionEndTime(session) {
|
|
486
|
+
const endedAt = Number(session?.endedAt ?? session?.startedAt ?? 0);
|
|
487
|
+
return Number.isFinite(endedAt) ? endedAt : 0;
|
|
488
|
+
}
|
|
489
|
+
|
|
483
490
|
async function appendSessionArchive(runtimePaths, projectId, archivedSessions, maxArchivedSessions) {
|
|
484
491
|
if (!archivedSessions || archivedSessions.length === 0) {
|
|
485
492
|
return;
|
|
@@ -493,7 +500,21 @@ async function appendSessionArchive(runtimePaths, projectId, archivedSessions, m
|
|
|
493
500
|
const archivePath = path.join(runtimePaths.projectsDir, `${sanitizeProjectId(projectId)}.archive.json`);
|
|
494
501
|
const existing = (await readMemoryJson(archivePath)) ?? { sessions: [] };
|
|
495
502
|
const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
|
|
496
|
-
|
|
503
|
+
|
|
504
|
+
// The cap must drop the OLDEST sessions, not simply the earliest-written ones: appending
|
|
505
|
+
// and slicing capped the count but evicted by insertion position, so a session archived
|
|
506
|
+
// late carrying an older end time survived while a newer one written earlier was
|
|
507
|
+
// dropped. Ties keep insertion order, which is what the old equal-timestamp behavior did.
|
|
508
|
+
const kept = [...sessions, ...archivedSessions]
|
|
509
|
+
.map((session, index) => ({ session, index }))
|
|
510
|
+
.sort((left, right) => {
|
|
511
|
+
const delta = archivedSessionEndTime(left.session) - archivedSessionEndTime(right.session);
|
|
512
|
+
return delta !== 0 ? delta : left.index - right.index;
|
|
513
|
+
})
|
|
514
|
+
.slice(-cap)
|
|
515
|
+
.map((entry) => entry.session);
|
|
516
|
+
|
|
517
|
+
await writeJson(archivePath, { sessions: kept });
|
|
497
518
|
}
|
|
498
519
|
|
|
499
520
|
async function persistProjectMemoryWithHygiene(projectRoot, runtimePaths, projectId, filePath, memory) {
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
// DEFAULT_CRITERIA — frozen thresholds used by `validateTaskFile` by default.
|
|
9
9
|
// VERIFICATION_MINUTE_TABLE — frozen literal table for `yarn test:release-core` and
|
|
10
10
|
// `node scripts/release/verify-release.mjs`. `yarn vitest run`
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// and the `yarn test` alias (`package.json`:
|
|
12
|
+
// `"test": "vitest run"`) are NOT in the table — their cost
|
|
13
|
+
// is computed dynamically (0.5 per file argument), see
|
|
14
|
+
// `estimateVerificationMinutes`.
|
|
13
15
|
// estimateVerificationMinutes — sums per-command minutes over a verification list.
|
|
14
16
|
// validateTaskFile — parses a TASK-xxx.md and returns
|
|
15
17
|
// `{ verdict, reasons: [{ criterion, detail }] }`.
|
|
@@ -39,8 +41,9 @@ export const VERIFICATION_MINUTE_TABLE = Object.freeze({
|
|
|
39
41
|
'node scripts/release/verify-release.mjs': 2,
|
|
40
42
|
});
|
|
41
43
|
|
|
42
|
-
// Per-command minute estimate. `yarn vitest run`
|
|
43
|
-
//
|
|
44
|
+
// Per-command minute estimate. `yarn vitest [run]` and the `yarn test` alias (the repo's
|
|
45
|
+
// dominant verification form) → 0.5 per FILE argument (per non-flag token after the runner
|
|
46
|
+
// word); anything else falls back to 1 flat.
|
|
44
47
|
export function estimateVerificationMinutes(commands) {
|
|
45
48
|
if (!Array.isArray(commands)) return 0;
|
|
46
49
|
let total = 0;
|
|
@@ -51,8 +54,8 @@ export function estimateVerificationMinutes(commands) {
|
|
|
51
54
|
total += VERIFICATION_MINUTE_TABLE[cmd];
|
|
52
55
|
continue;
|
|
53
56
|
}
|
|
54
|
-
if (/^yarn\s+vitest(\s+run)
|
|
55
|
-
// 0.5 minutes per non-flag argument after `run
|
|
57
|
+
if (/^yarn\s+(?:vitest(\s+run)?|test)(?=\s|$)/.test(cmd)) {
|
|
58
|
+
// 0.5 minutes per non-flag argument after the runner word (`run`, or `yarn test` itself).
|
|
56
59
|
const tokens = cmd.split(/\s+/);
|
|
57
60
|
const runIdx = tokens.indexOf('run');
|
|
58
61
|
const tail = runIdx >= 0 ? tokens.slice(runIdx + 1) : tokens.slice(2);
|
|
@@ -16,6 +16,13 @@ const INSTALL_REPAIR = 'Run ukit install';
|
|
|
16
16
|
const OMP_CONFIG_TIMEOUT_MS = 5000;
|
|
17
17
|
const COMPLETION_LOOP_HEADER = '## Unattended Completion Loop';
|
|
18
18
|
|
|
19
|
+
// B3 / FR-003 — the shipped template carries `{{token}}` placeholders that `ukit install`
|
|
20
|
+
// renders. `parse()` must not see them raw: `{{compact.autoCompactWindow}}` reads as a flow
|
|
21
|
+
// mapping in KEY position, and the `yaml` lib then warns "Keys with collection values will be
|
|
22
|
+
// stringified" on every doctor run. Same neutralization `tests/consistency/ompAgentParity.test.js`
|
|
23
|
+
// already applies; deliberately generic (no key-specific case) so a new placeholder is covered.
|
|
24
|
+
const TEMPLATE_PLACEHOLDER_PATTERN = /\{\{[^}]+\}\}/g;
|
|
25
|
+
|
|
19
26
|
async function readOmpConfig(projectRoot) {
|
|
20
27
|
const configPath = path.join(projectRoot, '.omp', 'config.yml');
|
|
21
28
|
try {
|
|
@@ -51,7 +58,7 @@ function execOmpConfigGet(projectRoot, ompPath) {
|
|
|
51
58
|
async function readTemplateDenyMatches() {
|
|
52
59
|
try {
|
|
53
60
|
const text = await fs.readFile(path.join(PACKAGE_ROOT, 'templates', '.omp', 'config.yml'), 'utf8');
|
|
54
|
-
const parsed = parse(text);
|
|
61
|
+
const parsed = parse(text.replace(TEMPLATE_PLACEHOLDER_PATTERN, '0'));
|
|
55
62
|
const patterns = parsed?.bash?.patterns;
|
|
56
63
|
if (!Array.isArray(patterns)) return [];
|
|
57
64
|
return patterns
|
|
@@ -11,6 +11,25 @@ if [ ! -f "$SETTINGS_LOCAL" ] || [ ! -f "$USAGE_FILE" ]; then
|
|
|
11
11
|
exit 0
|
|
12
12
|
fi
|
|
13
13
|
|
|
14
|
+
# O1 (TASK-009): armed AFTER the missing-state short-circuit above, so the
|
|
15
|
+
# nothing-to-prune path stays as free as it is today and only real work is measured.
|
|
16
|
+
# This hook stages no stdin (SPEC §14), so it owns its own EXIT trap: the trap
|
|
17
|
+
# returns the status it captured and the always-exit-0 posture is unchanged.
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
21
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ]; then
|
|
22
|
+
ukit_hook_telemetry_arm
|
|
23
|
+
__ukit_tel_finish() {
|
|
24
|
+
local __ukit_tel_rc=$?
|
|
25
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ] && command -v ukit_hook_telemetry_finish >/dev/null 2>&1; then
|
|
26
|
+
UKIT_TEL_RC="$__ukit_tel_rc" UKIT_TEL_EVENT="SessionStart" ukit_hook_telemetry_finish
|
|
27
|
+
fi
|
|
28
|
+
return "$__ukit_tel_rc"
|
|
29
|
+
}
|
|
30
|
+
trap __ukit_tel_finish EXIT
|
|
31
|
+
fi
|
|
32
|
+
|
|
14
33
|
NOW_UTC=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
15
34
|
|
|
16
35
|
node -e '
|
|
@@ -9,6 +9,28 @@ fi
|
|
|
9
9
|
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
10
10
|
SCRIPT_PATH="$HOOK_DIR/../ukit/runtime/reinject-context.mjs"
|
|
11
11
|
|
|
12
|
+
# O1 (TASK-009): this hook stages no stdin (SPEC §14), so the shared cleanup path in
|
|
13
|
+
# hook-input.sh never runs for it and nothing here would ever be measured. Arm the
|
|
14
|
+
# no-staging start marker and finish it from this hook's own EXIT trap: arming costs
|
|
15
|
+
# one mktemp and reads no clock, and the single bounded `--finish` child is the
|
|
16
|
+
# telemetry process rather than a hook work step. A missing runtime (pre-install
|
|
17
|
+
# tree) leaves the flag unset — exactly as unmeasured as today — and the trap returns
|
|
18
|
+
# the status it captured, so the `exit $?` below still reaches the host unchanged.
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$HOOK_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
21
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ]; then
|
|
22
|
+
ukit_hook_telemetry_arm
|
|
23
|
+
__ukit_tel_finish() {
|
|
24
|
+
local __ukit_tel_rc=$?
|
|
25
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ] && command -v ukit_hook_telemetry_finish >/dev/null 2>&1; then
|
|
26
|
+
# The event travels per call — no staged envelope exists to carry it.
|
|
27
|
+
UKIT_TEL_RC="$__ukit_tel_rc" UKIT_TEL_EVENT="PreCompact" ukit_hook_telemetry_finish
|
|
28
|
+
fi
|
|
29
|
+
return "$__ukit_tel_rc"
|
|
30
|
+
}
|
|
31
|
+
trap __ukit_tel_finish EXIT
|
|
32
|
+
fi
|
|
33
|
+
|
|
12
34
|
if [ -f "$SCRIPT_PATH" ]; then
|
|
13
35
|
UKIT_HOOK_DEADLINE_MS="${UKIT_HOOK_DEADLINE_MS:-3000}" node "$SCRIPT_PATH"
|
|
14
36
|
exit $?
|
|
@@ -24,6 +24,25 @@ if [ ! -f "$PRESSURE_FILE" ]; then
|
|
|
24
24
|
exit 0
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
+
# O1 (TASK-009): armed AFTER the no-pressure-file short-circuit above, so the common
|
|
28
|
+
# "nothing to reset" start stays unmeasured while real resets get timed. This hook
|
|
29
|
+
# stages no stdin (SPEC §14), so it owns its EXIT trap; the trap restores the captured
|
|
30
|
+
# status and the always-exit-0 posture below is unchanged.
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
# shellcheck source=/dev/null
|
|
33
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
34
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ]; then
|
|
35
|
+
ukit_hook_telemetry_arm
|
|
36
|
+
__ukit_tel_finish() {
|
|
37
|
+
local __ukit_tel_rc=$?
|
|
38
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ] && command -v ukit_hook_telemetry_finish >/dev/null 2>&1; then
|
|
39
|
+
UKIT_TEL_RC="$__ukit_tel_rc" UKIT_TEL_EVENT="SessionStart" ukit_hook_telemetry_finish
|
|
40
|
+
fi
|
|
41
|
+
return "$__ukit_tel_rc"
|
|
42
|
+
}
|
|
43
|
+
trap __ukit_tel_finish EXIT
|
|
44
|
+
fi
|
|
45
|
+
|
|
27
46
|
# The lock directory lives beside the state file; its parent exists because the file does.
|
|
28
47
|
node -e '
|
|
29
48
|
// Deadline must exceed the bounded lock wait in this block (maxWaitMs = 5000) so the
|
|
@@ -37,6 +37,26 @@ if [ "$__ukit_ep_gate" != "1" ]; then
|
|
|
37
37
|
fi
|
|
38
38
|
unset __ukit_ep_gate
|
|
39
39
|
|
|
40
|
+
# O1 (TASK-009): armed only past the gate above, so a gate-off SessionEnd stays a pure
|
|
41
|
+
# fast exit. This hook stages no stdin (SPEC §14) and owns its EXIT trap; the trap
|
|
42
|
+
# returns the captured status, so the always-exit-0 contract is unchanged. The row is
|
|
43
|
+
# not session-attributed: the staged payload is reaped below before this trap runs, and
|
|
44
|
+
# re-deriving the id in shell would mean another spawn on the teardown path.
|
|
45
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
46
|
+
# shellcheck source=/dev/null
|
|
47
|
+
source "$SCRIPT_DIR/../ukit/runtime/hook-telemetry.sh" 2>/dev/null || true
|
|
48
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ]; then
|
|
49
|
+
ukit_hook_telemetry_arm
|
|
50
|
+
__ukit_tel_finish() {
|
|
51
|
+
local __ukit_tel_rc=$?
|
|
52
|
+
if [ "${UKIT_TEL_ARMED:-}" = "1" ] && command -v ukit_hook_telemetry_finish >/dev/null 2>&1; then
|
|
53
|
+
UKIT_TEL_RC="$__ukit_tel_rc" UKIT_TEL_EVENT="SessionEnd" ukit_hook_telemetry_finish
|
|
54
|
+
fi
|
|
55
|
+
return "$__ukit_tel_rc"
|
|
56
|
+
}
|
|
57
|
+
trap __ukit_tel_finish EXIT
|
|
58
|
+
fi
|
|
59
|
+
|
|
40
60
|
# Bounded stdin read (existing hook style): cap +1 byte in background so a
|
|
41
61
|
# producer that never closes the pipe cannot park the session teardown.
|
|
42
62
|
UKIT_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/ukit-episode-in.XXXXXX")" || exit 0
|
|
@@ -27,7 +27,7 @@ const VERIFICATION_MINUTE_TABLE = Object.freeze({
|
|
|
27
27
|
'node scripts/release/verify-release.mjs': 2,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
function estimateVerificationMinutes(commands) {
|
|
30
|
+
export function estimateVerificationMinutes(commands) {
|
|
31
31
|
if (!Array.isArray(commands)) return 0;
|
|
32
32
|
let total = 0;
|
|
33
33
|
for (const raw of commands) {
|
|
@@ -37,7 +37,11 @@ function estimateVerificationMinutes(commands) {
|
|
|
37
37
|
total += VERIFICATION_MINUTE_TABLE[cmd];
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
// `yarn vitest [run]` plus the `yarn test` alias (package.json: `"test": "vitest run"`).
|
|
41
|
+
// The `(?=\s|$)` boundary is deliberate — `\b` would also swallow `yarn test:artifact` /
|
|
42
|
+
// `yarn test:liveness`, which are *different* scripts that must keep the 1-minute fallback.
|
|
43
|
+
if (/^yarn\s+(?:vitest(\s+run)?|test)(?=\s|$)/.test(cmd)) {
|
|
44
|
+
// 0.5 minutes per non-flag argument after the runner word (`run`, or `yarn test` itself).
|
|
41
45
|
const tokens = cmd.split(/\s+/);
|
|
42
46
|
const runIdx = tokens.indexOf('run');
|
|
43
47
|
const tail = runIdx >= 0 ? tokens.slice(runIdx + 1) : tokens.slice(2);
|