@ngockhoale/ukit 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +5 -3
- package/manifests/platform.full.yaml +126 -0
- package/package.json +1 -1
- package/src/core/runtimeConfig.js +26 -1
- package/src/index/impactCatalog.js +12 -0
- package/src/index/taskRouting.js +3 -0
- package/templates/.claude/hooks/post-edit-verify.sh +13 -0
- package/templates/.claude/hooks/pre-edit-backup.sh +13 -0
- package/templates/.claude/hooks/stale-spec-guard.sh +13 -0
- package/templates/.claude/settings.json +20 -0
- package/templates/.claude/ukit/index/anchor-search.mjs +99 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +3 -0
- package/templates/.claude/ukit/index/post-edit-verify.mjs +206 -0
- package/templates/.claude/ukit/index/pre-edit-backup.mjs +84 -0
- package/templates/.claude/ukit/index/route-task.mjs +6 -0
- package/templates/.claude/ukit/index/safe-patch.mjs +97 -0
- package/templates/.claude/ukit/index/stale-spec-check.mjs +192 -0
- package/templates/.claude/ukit/runtime/safe-patch-core.mjs +140 -0
- package/templates/.claude/ukit/runtime/text-profile.mjs +139 -0
- package/templates/.codex/README.md +3 -2
- package/templates/AGENTS.md +9 -2
- package/templates/CLAUDE.md +10 -2
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to UKit are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.3.0 - 2026-05-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added UKit Safe Patch Protocol guardrails for risky AI edits: stale/ambiguous `old_string` detection, whole-file shared-risk `Write` blocking, anchor-search helper, safe patch helper, text-profile detection, and pre-edit rollback bytes for shared-risk files.
|
|
12
|
+
- Added post-edit delta verification for backed-up shared-risk edits so unexpected broad rewrites are flagged against the Safe Patch hunk/changed-line budget and rollback manifests record after hashes.
|
|
13
|
+
- Added Safe Patch backup retention and bounded delta-diff fallback so rollback data does not grow forever and very large edits avoid unbounded LCS memory use.
|
|
14
|
+
- Added byte-first UTF-8/BOM/newline profile handling so helper-mediated patches preserve UTF-8 BOM/no-BOM, LF/CRLF, Vietnamese/CJK/emoji content, and reject unsafe binary/invalid UTF-8 edit paths.
|
|
15
|
+
- Added `safePatch` runtime config defaults under `.ukit/storage/config.json` while keeping the end-user workflow unchanged: normal users still only need `ukit install`; helpers remain internal guardrails.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Promoted shared-risk edit routing to surface an internal `editGuard=anchor-required` hint for installed runtime/template targets.
|
|
20
|
+
- Extended impact-risk classification to installed runtime paths such as `.claude/hooks/**`, `.claude/ukit/**`, and `.codex/**`.
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
|
|
24
|
+
- Added focused Safe Patch Protocol hook/helper coverage and install wiring coverage.
|
|
25
|
+
|
|
7
26
|
## 1.2.2 - 2026-05-05
|
|
8
27
|
|
|
9
28
|
- Removed the `.claude/ukit/.env` / `.env.example` config surface so UKit runtime tuning lives in `.ukit/storage/config.json` only.
|
package/README.md
CHANGED
|
@@ -68,13 +68,14 @@ If maintainers roll out a newer CLI build, the in-project workflow still stays t
|
|
|
68
68
|
- `.ukit/` — hidden shared runtime storage for config, cache, and cross-agent memory
|
|
69
69
|
- `docs/` — PROJECT / MEMORY / STATUS / TASKS / WORKLOG baseline
|
|
70
70
|
|
|
71
|
-
## UKit v1.
|
|
71
|
+
## UKit v1.3.0 Runtime
|
|
72
72
|
|
|
73
73
|
UKit now installs a hidden shared local runtime at `.ukit/` for production-oriented state that should survive across agent sessions:
|
|
74
74
|
|
|
75
75
|
- `.ukit/storage/config.json` — runtime defaults for compact/router/memory/validation/subagent hints
|
|
76
76
|
- `.ukit/storage/cache/` — reusable prompt-cache, compact history, compact-pressure state, and output summaries
|
|
77
77
|
- `.ukit/storage/memory/` — cross-agent local memory
|
|
78
|
+
- `.ukit/storage/backups/` — rollback bytes for risky Safe Patch Protocol edits
|
|
78
79
|
|
|
79
80
|
If an older repo still has a visible legacy `ukit/` runtime folder, rerunning `ukit install` now migrates the shared runtime into hidden `.ukit/` when the target paths are free.
|
|
80
81
|
|
|
@@ -85,7 +86,7 @@ When long sessions approach the compact threshold, UKit now uses a conservative
|
|
|
85
86
|
- compact only safe-zone history/noise
|
|
86
87
|
- preserve active task, rules, decisions, and current code focus
|
|
87
88
|
|
|
88
|
-
UKit v1.
|
|
89
|
+
UKit v1.3.0 keeps the same shared runtime contract while adding Safe Patch Protocol guardrails and the local AI task queue alongside living project status routing:
|
|
89
90
|
|
|
90
91
|
- install globally with `npm install -g @ngockhoale/ukit`
|
|
91
92
|
- keep using the exact same human workflow inside projects: `ukit install`
|
|
@@ -95,8 +96,9 @@ UKit v1.2.2 keeps the same shared runtime contract while adding a local AI task
|
|
|
95
96
|
- auto-route open-ended “what next?” / “continue” prompts to the `next-step` skill with a visible freshness cue when status may be stale
|
|
96
97
|
- auto-route explicit handoff/wrap-up requests to the `update-status` skill while skipping trivial/no-state-change tasks
|
|
97
98
|
- keep concrete debug/implementation/review prompts primary, so project status never replaces source/index-first task work
|
|
99
|
+
- quietly guard risky AI edits with Safe Patch Protocol: stale/ambiguous specs are blocked, shared-risk whole-file writes are discouraged, and internal helpers preserve UTF-8 BOM/no-BOM plus LF/CRLF for multilingual text
|
|
98
100
|
|
|
99
|
-
UKit v1.
|
|
101
|
+
UKit v1.3.0 also keeps the fast path improvements from the recent runtime releases:
|
|
100
102
|
|
|
101
103
|
- Vietnamese prompts now normalize more effectively for English-heavy code symbols and paths
|
|
102
104
|
- localized simple direct-target lanes skip extra previous-context / recent-output work when it would not change the next action
|
|
@@ -838,6 +838,9 @@ items:
|
|
|
838
838
|
targetPath: .claude/settings.json
|
|
839
839
|
requires:
|
|
840
840
|
- hook-protect-files
|
|
841
|
+
- hook-stale-spec-guard
|
|
842
|
+
- hook-pre-edit-backup
|
|
843
|
+
- hook-post-edit-verify
|
|
841
844
|
- hook-auto-allow-bash
|
|
842
845
|
- hook-block-dangerous
|
|
843
846
|
- hook-auto-prune-bash
|
|
@@ -869,6 +872,42 @@ items:
|
|
|
869
872
|
packs:
|
|
870
873
|
- core
|
|
871
874
|
|
|
875
|
+
- id: hook-stale-spec-guard
|
|
876
|
+
type: hook
|
|
877
|
+
sourceTemplate: .claude/hooks/stale-spec-guard.sh
|
|
878
|
+
targetPath: .claude/hooks/stale-spec-guard.sh
|
|
879
|
+
requires:
|
|
880
|
+
- ukit-index-stale-spec-check-script
|
|
881
|
+
mergeStrategy: overwrite_with_backup
|
|
882
|
+
variables: []
|
|
883
|
+
enabledByDefault: true
|
|
884
|
+
packs:
|
|
885
|
+
- core
|
|
886
|
+
|
|
887
|
+
- id: hook-pre-edit-backup
|
|
888
|
+
type: hook
|
|
889
|
+
sourceTemplate: .claude/hooks/pre-edit-backup.sh
|
|
890
|
+
targetPath: .claude/hooks/pre-edit-backup.sh
|
|
891
|
+
requires:
|
|
892
|
+
- ukit-index-pre-edit-backup-script
|
|
893
|
+
mergeStrategy: overwrite_with_backup
|
|
894
|
+
variables: []
|
|
895
|
+
enabledByDefault: true
|
|
896
|
+
packs:
|
|
897
|
+
- core
|
|
898
|
+
|
|
899
|
+
- id: hook-post-edit-verify
|
|
900
|
+
type: hook
|
|
901
|
+
sourceTemplate: .claude/hooks/post-edit-verify.sh
|
|
902
|
+
targetPath: .claude/hooks/post-edit-verify.sh
|
|
903
|
+
requires:
|
|
904
|
+
- ukit-index-post-edit-verify-script
|
|
905
|
+
mergeStrategy: overwrite_with_backup
|
|
906
|
+
variables: []
|
|
907
|
+
enabledByDefault: true
|
|
908
|
+
packs:
|
|
909
|
+
- core
|
|
910
|
+
|
|
872
911
|
- id: hook-auto-allow-bash
|
|
873
912
|
type: hook
|
|
874
913
|
sourceTemplate: .claude/hooks/auto-allow-bash.sh
|
|
@@ -961,6 +1000,93 @@ items:
|
|
|
961
1000
|
packs:
|
|
962
1001
|
- core
|
|
963
1002
|
|
|
1003
|
+
- id: ukit-runtime-text-profile-script
|
|
1004
|
+
type: config
|
|
1005
|
+
sourceTemplate: .claude/ukit/runtime/text-profile.mjs
|
|
1006
|
+
targetPath: .claude/ukit/runtime/text-profile.mjs
|
|
1007
|
+
requires: []
|
|
1008
|
+
mergeStrategy: overwrite_with_backup
|
|
1009
|
+
variables: []
|
|
1010
|
+
enabledByDefault: true
|
|
1011
|
+
packs:
|
|
1012
|
+
- core
|
|
1013
|
+
|
|
1014
|
+
- id: ukit-runtime-safe-patch-core-script
|
|
1015
|
+
type: config
|
|
1016
|
+
sourceTemplate: .claude/ukit/runtime/safe-patch-core.mjs
|
|
1017
|
+
targetPath: .claude/ukit/runtime/safe-patch-core.mjs
|
|
1018
|
+
requires:
|
|
1019
|
+
- ukit-runtime-text-profile-script
|
|
1020
|
+
mergeStrategy: overwrite_with_backup
|
|
1021
|
+
variables: []
|
|
1022
|
+
enabledByDefault: true
|
|
1023
|
+
packs:
|
|
1024
|
+
- core
|
|
1025
|
+
|
|
1026
|
+
- id: ukit-index-anchor-search-script
|
|
1027
|
+
type: config
|
|
1028
|
+
sourceTemplate: .claude/ukit/index/anchor-search.mjs
|
|
1029
|
+
targetPath: .claude/ukit/index/anchor-search.mjs
|
|
1030
|
+
requires:
|
|
1031
|
+
- ukit-runtime-text-profile-script
|
|
1032
|
+
- ukit-runtime-safe-patch-core-script
|
|
1033
|
+
mergeStrategy: overwrite_with_backup
|
|
1034
|
+
variables: []
|
|
1035
|
+
enabledByDefault: true
|
|
1036
|
+
packs:
|
|
1037
|
+
- core
|
|
1038
|
+
|
|
1039
|
+
- id: ukit-index-safe-patch-script
|
|
1040
|
+
type: config
|
|
1041
|
+
sourceTemplate: .claude/ukit/index/safe-patch.mjs
|
|
1042
|
+
targetPath: .claude/ukit/index/safe-patch.mjs
|
|
1043
|
+
requires:
|
|
1044
|
+
- ukit-index-anchor-search-script
|
|
1045
|
+
mergeStrategy: overwrite_with_backup
|
|
1046
|
+
variables: []
|
|
1047
|
+
enabledByDefault: true
|
|
1048
|
+
packs:
|
|
1049
|
+
- core
|
|
1050
|
+
|
|
1051
|
+
- id: ukit-index-stale-spec-check-script
|
|
1052
|
+
type: config
|
|
1053
|
+
sourceTemplate: .claude/ukit/index/stale-spec-check.mjs
|
|
1054
|
+
targetPath: .claude/ukit/index/stale-spec-check.mjs
|
|
1055
|
+
requires:
|
|
1056
|
+
- ukit-runtime-text-profile-script
|
|
1057
|
+
- ukit-runtime-safe-patch-core-script
|
|
1058
|
+
mergeStrategy: overwrite_with_backup
|
|
1059
|
+
variables: []
|
|
1060
|
+
enabledByDefault: true
|
|
1061
|
+
packs:
|
|
1062
|
+
- core
|
|
1063
|
+
|
|
1064
|
+
- id: ukit-index-pre-edit-backup-script
|
|
1065
|
+
type: config
|
|
1066
|
+
sourceTemplate: .claude/ukit/index/pre-edit-backup.mjs
|
|
1067
|
+
targetPath: .claude/ukit/index/pre-edit-backup.mjs
|
|
1068
|
+
requires:
|
|
1069
|
+
- ukit-runtime-text-profile-script
|
|
1070
|
+
- ukit-runtime-safe-patch-core-script
|
|
1071
|
+
mergeStrategy: overwrite_with_backup
|
|
1072
|
+
variables: []
|
|
1073
|
+
enabledByDefault: true
|
|
1074
|
+
packs:
|
|
1075
|
+
- core
|
|
1076
|
+
|
|
1077
|
+
- id: ukit-index-post-edit-verify-script
|
|
1078
|
+
type: config
|
|
1079
|
+
sourceTemplate: .claude/ukit/index/post-edit-verify.mjs
|
|
1080
|
+
targetPath: .claude/ukit/index/post-edit-verify.mjs
|
|
1081
|
+
requires:
|
|
1082
|
+
- ukit-runtime-text-profile-script
|
|
1083
|
+
- ukit-runtime-safe-patch-core-script
|
|
1084
|
+
mergeStrategy: overwrite_with_backup
|
|
1085
|
+
variables: []
|
|
1086
|
+
enabledByDefault: true
|
|
1087
|
+
packs:
|
|
1088
|
+
- core
|
|
1089
|
+
|
|
964
1090
|
- id: ukit-index-core-lib
|
|
965
1091
|
type: config
|
|
966
1092
|
sourceTemplate: .claude/ukit/index/lib/index-core.mjs
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
49
49
|
const safeOverrides = isPlainObject(overrides) ? overrides : {};
|
|
50
50
|
|
|
51
51
|
return mergeObjects({
|
|
52
|
-
version: '1.
|
|
52
|
+
version: '1.3.0',
|
|
53
53
|
agent: 'claude-code',
|
|
54
54
|
compact: {
|
|
55
55
|
enabled: true,
|
|
@@ -122,6 +122,17 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
122
122
|
maxRetries: 1,
|
|
123
123
|
confidenceThreshold: 50,
|
|
124
124
|
},
|
|
125
|
+
safePatch: {
|
|
126
|
+
enabled: true,
|
|
127
|
+
strictSharedRisk: true,
|
|
128
|
+
largeFileLineThreshold: 800,
|
|
129
|
+
largeFileByteThreshold: 200_000,
|
|
130
|
+
backupEnabled: true,
|
|
131
|
+
backupRetentionDays: 30,
|
|
132
|
+
deltaMaxChangedLines: 120,
|
|
133
|
+
deltaMaxHunks: 3,
|
|
134
|
+
deltaMaxDiffCells: 2_000_000,
|
|
135
|
+
},
|
|
125
136
|
subagents: {
|
|
126
137
|
enabled: true,
|
|
127
138
|
smallTaskModel: 'unic-lite',
|
|
@@ -271,6 +282,20 @@ export function validateRuntimeConfig(config) {
|
|
|
271
282
|
pushPositiveNumberError(errors, config.validation.confidenceThreshold, 'validation.confidenceThreshold');
|
|
272
283
|
}
|
|
273
284
|
|
|
285
|
+
if (!isPlainObject(config.safePatch)) {
|
|
286
|
+
errors.push('safePatch must be an object.');
|
|
287
|
+
} else {
|
|
288
|
+
pushBooleanError(errors, config.safePatch.enabled, 'safePatch.enabled');
|
|
289
|
+
pushBooleanError(errors, config.safePatch.strictSharedRisk, 'safePatch.strictSharedRisk');
|
|
290
|
+
pushPositiveNumberError(errors, config.safePatch.largeFileLineThreshold, 'safePatch.largeFileLineThreshold');
|
|
291
|
+
pushPositiveNumberError(errors, config.safePatch.largeFileByteThreshold, 'safePatch.largeFileByteThreshold');
|
|
292
|
+
pushBooleanError(errors, config.safePatch.backupEnabled, 'safePatch.backupEnabled');
|
|
293
|
+
pushPositiveNumberError(errors, config.safePatch.backupRetentionDays, 'safePatch.backupRetentionDays');
|
|
294
|
+
pushPositiveNumberError(errors, config.safePatch.deltaMaxChangedLines, 'safePatch.deltaMaxChangedLines');
|
|
295
|
+
pushPositiveNumberError(errors, config.safePatch.deltaMaxHunks, 'safePatch.deltaMaxHunks');
|
|
296
|
+
pushPositiveNumberError(errors, config.safePatch.deltaMaxDiffCells, 'safePatch.deltaMaxDiffCells');
|
|
297
|
+
}
|
|
298
|
+
|
|
274
299
|
if (!isPlainObject(config.subagents)) {
|
|
275
300
|
errors.push('subagents must be an object.');
|
|
276
301
|
} else {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
const SHARED_IMPACT_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
regex: /^\.claude\/hooks\//,
|
|
4
|
+
label: 'installed-hook-runtime',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
regex: /^\.claude\/ukit\//,
|
|
8
|
+
label: 'installed-helper-runtime',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
regex: /^\.codex\//,
|
|
12
|
+
label: 'installed-codex-runtime',
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
regex: /^src\/index\//,
|
|
4
16
|
label: 'shared-index-runtime',
|
package/src/index/taskRouting.js
CHANGED
|
@@ -157,6 +157,7 @@ export function buildRouteSummary({
|
|
|
157
157
|
?? [...primaryCommands, ...fallbackCommands],
|
|
158
158
|
);
|
|
159
159
|
const policyMode = verificationRecommendation?.executionPolicy?.policyMode ?? null;
|
|
160
|
+
const editGuardHint = isSharedImpactFile(routingContext.targetFile) ? 'anchor-required' : null;
|
|
160
161
|
const compactHelperLane = nextAction?.type === 'pull-indexed-context'
|
|
161
162
|
&& typeof contextRecommendation?.command === 'string'
|
|
162
163
|
&& contextRecommendation.command.trim();
|
|
@@ -175,6 +176,7 @@ export function buildRouteSummary({
|
|
|
175
176
|
formatCompactSegment('targets', primaryTargets),
|
|
176
177
|
formatCompactSegment('tests', relatedTests),
|
|
177
178
|
formatCompactSegment('styles', styleFiles),
|
|
179
|
+
editGuardHint ? `editGuard=${editGuardHint}` : null,
|
|
178
180
|
delegationRecommendation?.hint ? `delegate=${delegationRecommendation.hint}` : null,
|
|
179
181
|
policyMode ? `policy=${policyMode}` : null,
|
|
180
182
|
].filter(Boolean).join(' | ');
|
|
@@ -184,6 +186,7 @@ export function buildRouteSummary({
|
|
|
184
186
|
fallbackCommands,
|
|
185
187
|
preferredOrder,
|
|
186
188
|
policyMode,
|
|
189
|
+
editGuardHint,
|
|
187
190
|
intentMode: routingContext.intentMode ?? null,
|
|
188
191
|
delegateHint: delegationRecommendation?.hint ?? null,
|
|
189
192
|
nextActionType: nextAction?.type ?? null,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse hook: verify risky Edit|Write delta after rollback bytes exist.
|
|
3
|
+
# Matched on: Edit|Write
|
|
4
|
+
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
|
+
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/post-edit-verify.mjs"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$SCRIPT" ]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: create rollback bytes for risky Edit|Write operations.
|
|
3
|
+
# Matched on: Edit|Write
|
|
4
|
+
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
|
+
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/pre-edit-backup.mjs"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$SCRIPT" ]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreToolUse hook: prevent stale/ambiguous risky Edit specs and whole-file risky Write.
|
|
3
|
+
# Matched on: Edit|Write
|
|
4
|
+
|
|
5
|
+
INPUT=$(cat)
|
|
6
|
+
PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
|
+
SCRIPT="$PROJECT_ROOT/.claude/ukit/index/stale-spec-check.mjs"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$SCRIPT" ]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
printf '%s' "$INPUT" | node "$SCRIPT"
|
|
@@ -62,6 +62,16 @@
|
|
|
62
62
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/protect-files.sh\"",
|
|
63
63
|
"timeout": 8
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
"type": "command",
|
|
67
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/stale-spec-guard.sh\"",
|
|
68
|
+
"timeout": 8
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "command",
|
|
72
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-edit-backup.sh\"",
|
|
73
|
+
"timeout": 8
|
|
74
|
+
},
|
|
65
75
|
{
|
|
66
76
|
"type": "command",
|
|
67
77
|
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-router.sh\"",
|
|
@@ -96,6 +106,16 @@
|
|
|
96
106
|
}
|
|
97
107
|
],
|
|
98
108
|
"PostToolUse": [
|
|
109
|
+
{
|
|
110
|
+
"matcher": "Edit|Write",
|
|
111
|
+
"hooks": [
|
|
112
|
+
{
|
|
113
|
+
"type": "command",
|
|
114
|
+
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-verify.sh\"",
|
|
115
|
+
"timeout": 8
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
99
119
|
{
|
|
100
120
|
"matcher": "Bash",
|
|
101
121
|
"hooks": [
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { analyzeTextFile, publicTextProfile } from '../runtime/text-profile.mjs';
|
|
2
|
+
import { buildLineWindow, countOccurrences, lineNumberForIndex, resolveProjectFile, summarizeSnippet } from '../runtime/safe-patch-core.mjs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
7
|
+
const args = { json: false, contextLines: 3 };
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
if (arg === '--json') args.json = true;
|
|
11
|
+
else if (arg === '--file') args.file = argv[++i];
|
|
12
|
+
else if (arg === '--query' || arg === '--anchor') args.query = argv[++i];
|
|
13
|
+
else if (arg === '--context-lines') args.contextLines = Number(argv[++i]);
|
|
14
|
+
else if (arg === '--start-line') args.startLine = Number(argv[++i]);
|
|
15
|
+
else if (arg === '--end-line') args.endLine = Number(argv[++i]);
|
|
16
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
17
|
+
}
|
|
18
|
+
return args;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
return 'Usage: node .claude/ukit/index/anchor-search.mjs --file <path> --query <anchor> [--json] [--context-lines N]';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function searchAnchor({ projectRoot = process.cwd(), filePath, query, contextLines = 3, startLine = null, endLine = null } = {}) {
|
|
26
|
+
const resolved = resolveProjectFile(projectRoot, filePath);
|
|
27
|
+
if (!resolved) throw new Error('Missing --file.');
|
|
28
|
+
if (!query) throw new Error('Missing --query.');
|
|
29
|
+
|
|
30
|
+
const profile = await analyzeTextFile(resolved.absolute);
|
|
31
|
+
if (profile.binaryLike || !profile.utf8Valid) {
|
|
32
|
+
return {
|
|
33
|
+
status: 'non-text',
|
|
34
|
+
file: resolved.relative,
|
|
35
|
+
query,
|
|
36
|
+
count: 0,
|
|
37
|
+
matches: [],
|
|
38
|
+
profile: publicTextProfile(profile),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const occurrence = countOccurrences(profile.text, query);
|
|
43
|
+
let matches = occurrence.indexes.map((index) => {
|
|
44
|
+
const line = lineNumberForIndex(profile.text, index);
|
|
45
|
+
const window = buildLineWindow(profile.text, line, 0);
|
|
46
|
+
return { index, line, snippet: summarizeSnippet(window.text, 160) };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (Number.isFinite(startLine) || Number.isFinite(endLine)) {
|
|
50
|
+
const minLine = Number.isFinite(startLine) ? startLine : 1;
|
|
51
|
+
const maxLine = Number.isFinite(endLine) ? endLine : Number.MAX_SAFE_INTEGER;
|
|
52
|
+
matches = matches.filter((match) => match.line >= minLine && match.line <= maxLine);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const count = matches.length;
|
|
56
|
+
const status = count === 1 ? 'unique' : count === 0 ? 'not-found' : 'ambiguous';
|
|
57
|
+
const firstLine = matches[0]?.line ?? 1;
|
|
58
|
+
return {
|
|
59
|
+
status,
|
|
60
|
+
file: resolved.relative,
|
|
61
|
+
query,
|
|
62
|
+
count,
|
|
63
|
+
matches,
|
|
64
|
+
...(count === 1 ? { window: buildLineWindow(profile.text, firstLine, Number.isFinite(contextLines) ? contextLines : 3) } : {}),
|
|
65
|
+
profile: publicTextProfile(profile),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
const args = parseArgs();
|
|
71
|
+
if (args.help || !args.file || !args.query) {
|
|
72
|
+
const help = usage();
|
|
73
|
+
process.stdout.write(args.json ? `${JSON.stringify({ help })}\n` : `${help}\n`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const result = await searchAnchor({
|
|
77
|
+
projectRoot: process.env.CLAUDE_PROJECT_DIR || process.cwd(),
|
|
78
|
+
filePath: args.file,
|
|
79
|
+
query: args.query,
|
|
80
|
+
contextLines: args.contextLines,
|
|
81
|
+
startLine: args.startLine,
|
|
82
|
+
endLine: args.endLine,
|
|
83
|
+
});
|
|
84
|
+
if (args.json) {
|
|
85
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
86
|
+
} else {
|
|
87
|
+
process.stdout.write(`[ukit:anchor] status=${result.status} count=${result.count} file=${result.file} query=${JSON.stringify(result.query)}\n`);
|
|
88
|
+
for (const match of result.matches.slice(0, 5)) {
|
|
89
|
+
process.stdout.write(`- line ${match.line}: ${match.snippet}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.argv[1] && path.basename(fileURLToPath(import.meta.url)) === path.basename(process.argv[1])) {
|
|
95
|
+
main().catch((error) => {
|
|
96
|
+
process.stderr.write(`[ukit:anchor] ERROR: ${error?.message || error}\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -2828,6 +2828,9 @@ async function detectPackageManager(rootDir) {
|
|
|
2828
2828
|
}
|
|
2829
2829
|
|
|
2830
2830
|
const IMPACT_SHARED_PATTERNS = [
|
|
2831
|
+
{ regex: /^\.claude\/hooks\//, label: 'installed-hook-runtime' },
|
|
2832
|
+
{ regex: /^\.claude\/ukit\//, label: 'installed-helper-runtime' },
|
|
2833
|
+
{ regex: /^\.codex\//, label: 'installed-codex-runtime' },
|
|
2831
2834
|
{ regex: /^src\/index\//, label: 'shared-index-runtime' },
|
|
2832
2835
|
{
|
|
2833
2836
|
regex: /^src\/core\/(runInstallPipeline|applyPlan|buildPlan|diffPlan|metadata|migrateLegacy|uninstall|runtimeConfig|runtimePaths)\.js$/,
|