@ngockhoale/ukit 2.7.5 → 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 +89 -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/ompConfigMerge.js +7 -2
- package/src/core/permissionDoctor.js +59 -4
- package/src/core/permissionPolicy.js +1 -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/settings.json +0 -1
- 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
- package/templates/.omp/config.yml +9 -8
|
@@ -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) {
|
|
@@ -130,7 +130,12 @@ export function mergeOmpConfig(existingText, renderedText) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// §7.4 — bash.patterns union: existing-only entries (dedupe by match) appended
|
|
133
|
-
// before the trailing `- match: "*"` allow; `prompt` re-emitted as `
|
|
133
|
+
// before the trailing `- match: "*"` allow; `prompt` re-emitted as `allow`.
|
|
134
|
+
// Under rendered yolo a `deny` is a silent hard block, so coercing prompt→deny
|
|
135
|
+
// would only ADD false bricks (worst case a legacy `*` pattern denying all
|
|
136
|
+
// bash) without adding real safety — the canonical deny set + block-dangerous
|
|
137
|
+
// hook still gate destructive commands. prompt→allow is behavior-identical
|
|
138
|
+
// to the trailing `*` allow and stays honest about what yolo can run.
|
|
134
139
|
const existingPatterns = Array.isArray(existing?.bash?.patterns) ? existing.bash.patterns : [];
|
|
135
140
|
const renderedPatterns = Array.isArray(rendered?.bash?.patterns) ? rendered.bash.patterns : [];
|
|
136
141
|
const renderedMatches = new Set(renderedPatterns.map((p) => p?.match));
|
|
@@ -140,7 +145,7 @@ export function mergeOmpConfig(existingText, renderedText) {
|
|
|
140
145
|
if (renderedMatches.has(pattern.match)) continue;
|
|
141
146
|
let approval = pattern.approval;
|
|
142
147
|
if (approval === 'prompt') {
|
|
143
|
-
approval = '
|
|
148
|
+
approval = 'allow';
|
|
144
149
|
report.coercedPromptPatterns.push(pattern.match);
|
|
145
150
|
}
|
|
146
151
|
extraPatterns.push({ ...pattern, approval });
|
|
@@ -17,10 +17,11 @@ import { findOpencodeArtifacts, opencodeSteerMessage } from './opencodeSteer.js'
|
|
|
17
17
|
// doctor section and `--json`.
|
|
18
18
|
//
|
|
19
19
|
// FAIL classes (each → named failing check + report.failures entry + exit 1):
|
|
20
|
-
// unattended-prompt
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// unattended-prompt prompt/ask reachable while permissionMode=unattended
|
|
21
|
+
// omp-approval-mode-not-pinned omp approvalMode ≠ 'yolo' under unattended
|
|
22
|
+
// deny-loss canonical deny entry missing from a host surface
|
|
23
|
+
// unsafe-eval-mismatch surface eval approval ≠ compiled evalPosture
|
|
24
|
+
// unrendered-token literal {{token}} left in a rendered artifact
|
|
24
25
|
// INFO: reachable prompts under interactive/safe-auto. SKIP: absent host surface.
|
|
25
26
|
|
|
26
27
|
const UNRESOLVED_TOKEN_PATTERN = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
@@ -210,7 +211,32 @@ export async function inspectPermissions({ projectRoot, ompPath = 'omp' }) {
|
|
|
210
211
|
.filter((w) => w !== 'unsafe-eval-mismatch' && w !== 'prompt-reachable-under-unattended')
|
|
211
212
|
.map((w) => `${h}:${w}`));
|
|
212
213
|
|
|
214
|
+
// TASK-003 / cycle-41 — C34/C35 blind spot: literal prompt/ask counting never
|
|
215
|
+
// sees `approvalMode: write` or a missing pin, yet unattended still needs the
|
|
216
|
+
// yolo pin (C35 decided; here we only detect drift, never re-litigate it).
|
|
217
|
+
const pinApplicable = omp.exists && declared === 'unattended';
|
|
218
|
+
const pinFail = pinApplicable && ompTools.approvalMode !== 'yolo';
|
|
219
|
+
|
|
220
|
+
// Preserved-pattern audit — omp bash.patterns deny/allow entries outside the
|
|
221
|
+
// canonical set (post-TASK-001 shape) are merges carrying legacy/custom
|
|
222
|
+
// rules; visible as advisory, never a FAIL.
|
|
223
|
+
const canonicalOmpMatches = new Set(
|
|
224
|
+
CANONICAL_DENY.map((c) => c.omp).filter((m) => typeof m === 'string'),
|
|
225
|
+
);
|
|
226
|
+
const preservedPatternMatches = bashPatterns
|
|
227
|
+
.filter(
|
|
228
|
+
(e) =>
|
|
229
|
+
e &&
|
|
230
|
+
(e.approval === 'deny' || e.approval === 'allow') &&
|
|
231
|
+
typeof e.match === 'string' &&
|
|
232
|
+
!canonicalOmpMatches.has(e.match) &&
|
|
233
|
+
// the template's own trailing catch-all allow is rendered, not preserved
|
|
234
|
+
!(e.match === '*' && e.approval === 'allow'),
|
|
235
|
+
)
|
|
236
|
+
.map((e) => e.match);
|
|
237
|
+
|
|
213
238
|
if (promptsFail) failures.push('unattended-prompt');
|
|
239
|
+
if (pinFail) failures.push('omp-approval-mode-not-pinned');
|
|
214
240
|
if (denyMissingAll.length > 0) failures.push('deny-loss');
|
|
215
241
|
if (evalMismatch) failures.push('unsafe-eval-mismatch');
|
|
216
242
|
if (unresolvedTokens.length > 0) failures.push('unrendered-token');
|
|
@@ -248,6 +274,35 @@ export async function inspectPermissions({ projectRoot, ompPath = 'omp' }) {
|
|
|
248
274
|
: 'no host permission surfaces installed',
|
|
249
275
|
},
|
|
250
276
|
),
|
|
277
|
+
check(
|
|
278
|
+
'omp approvalMode pinned under unattended',
|
|
279
|
+
!pinFail,
|
|
280
|
+
'install-repairable',
|
|
281
|
+
'Run ukit install to re-pin approvalMode to yolo.',
|
|
282
|
+
{
|
|
283
|
+
...(pinApplicable ? {} : { applicable: false }),
|
|
284
|
+
detail: !omp.exists
|
|
285
|
+
? '.omp/config.yml absent'
|
|
286
|
+
: declared !== 'unattended'
|
|
287
|
+
? `mode ${declared} allows prompts — pin not required`
|
|
288
|
+
: `tools.approvalMode=${ompTools.approvalMode ?? 'absent'} (required: yolo)`,
|
|
289
|
+
},
|
|
290
|
+
),
|
|
291
|
+
check(
|
|
292
|
+
'preserved omp bash.patterns audit',
|
|
293
|
+
true,
|
|
294
|
+
'advisory',
|
|
295
|
+
'Review preserved entries; drop them or rerun ukit install after pruning the source config.',
|
|
296
|
+
{
|
|
297
|
+
...(omp.exists ? {} : { applicable: false }),
|
|
298
|
+
...(preservedPatternMatches.length > 0 ? { severity: 'warning' } : {}),
|
|
299
|
+
detail: !omp.exists
|
|
300
|
+
? '.omp/config.yml absent'
|
|
301
|
+
: preservedPatternMatches.length === 0
|
|
302
|
+
? 'no non-canonical bash.patterns entries preserved'
|
|
303
|
+
: `non-canonical preserved entries: ${preservedPatternMatches.join(', ')}`,
|
|
304
|
+
},
|
|
305
|
+
),
|
|
251
306
|
check(
|
|
252
307
|
'deny coverage vs canonical set',
|
|
253
308
|
denyMissingAll.length === 0,
|
|
@@ -35,7 +35,6 @@ export const CANONICAL_DENY = Object.freeze([
|
|
|
35
35
|
{ id: 'rm-rf-home', claude: 'Bash(rm -rf ~:*)', omp: 'rm -rf ~*', codex: 'rm -rf ~', hardDeny: true },
|
|
36
36
|
{ id: 'rm-rf-parent', claude: 'Bash(rm -rf ..:*)', omp: 'rm -rf ..', codex: 'rm -rf ..', hardDeny: true },
|
|
37
37
|
{ id: 'rm-rf-dot', claude: 'Bash(rm -rf .:*)', omp: 'rm -rf .', codex: 'rm -rf .', hardDeny: true },
|
|
38
|
-
{ id: 'rm-rf-any', claude: 'Bash(rm -rf:*)', omp: 'rm -rf *', codex: 'rm -rf' },
|
|
39
38
|
{ id: 'dd-raw-device', claude: 'Bash(dd if=/dev/:*)', omp: 'dd if=/dev/*', codex: 'dd if=/dev/', hardDeny: true },
|
|
40
39
|
{ id: 'write-raw-device', claude: 'Bash(*> /dev/sda:*)', omp: '*> /dev/sda*', codex: '> /dev/sda', hardDeny: true },
|
|
41
40
|
{ id: 'mkfs', claude: 'Bash(mkfs:*)', omp: 'mkfs.*', codex: 'mkfs', hardDeny: true },
|
|
@@ -48,6 +47,7 @@ export const CANONICAL_PROTECTED = Object.freeze([
|
|
|
48
47
|
'release-credentials', // npm tokens, signing keys, .env secrets
|
|
49
48
|
'protected-files', // protect-files.sh guarded paths
|
|
50
49
|
'stale-spec-guard', // spec/source drift guard on edits
|
|
50
|
+
'sensitive-data-guard', // secret/PII scrub gate on reads, bash, prompts
|
|
51
51
|
]);
|
|
52
52
|
|
|
53
53
|
const TIER_RANK = new Map(PERMISSION_PRECEDENCE.map((tier, i) => [tier, i]));
|
|
@@ -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 '
|