@ngockhoale/ukit 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to UKit are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.3.1 - 2026-05-06
8
+
9
+ ### Fixed
10
+
11
+ - Fixed `ukit install` failing with `ENOENT` when a published/global package is missing optional `templates/.gitignore`; install now uses a built-in fallback.
12
+
13
+ ### Tests
14
+
15
+ - Added a regression test for package artifacts without `templates/.gitignore`.
16
+
17
+ ## 1.3.0 - 2026-05-06
18
+
19
+ ### Added
20
+
21
+ - 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.
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+
27
+ ### Changed
28
+
29
+ - Promoted shared-risk edit routing to surface an internal `editGuard=anchor-required` hint for installed runtime/template targets.
30
+ - Extended impact-risk classification to installed runtime paths such as `.claude/hooks/**`, `.claude/ukit/**`, and `.codex/**`.
31
+
32
+ ### Tests
33
+
34
+ - Added focused Safe Patch Protocol hook/helper coverage and install wiring coverage.
35
+
7
36
  ## 1.2.2 - 2026-05-05
8
37
 
9
38
  - 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.2.2 Runtime
71
+ ## UKit v1.3.1 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.2.2 keeps the same shared runtime contract while adding a local AI task queue alongside living project status routing:
89
+ UKit v1.3.1 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.2.2 also keeps the fast path improvements from the recent runtime releases:
101
+ UKit v1.3.1 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,14 @@ const BINARY_TEMPLATE_EXTENSIONS = new Set([
25
25
  '.pdf',
26
26
  ]);
27
27
 
28
+ const FALLBACK_ROOT_GITIGNORE_TEMPLATE = 'node_modules/\n.env\n*.log\n.DS_Store\n';
29
+
30
+ function isRootGitignoreItem(item) {
31
+ return item?.id === 'gitignore-root'
32
+ && item?.sourceTemplate === '.gitignore'
33
+ && item?.targetPath === '.gitignore';
34
+ }
35
+
28
36
  function safeResolve(basePath, relativePath) {
29
37
  const resolvedPath = path.resolve(basePath, relativePath);
30
38
  const relativeToBase = path.relative(basePath, resolvedPath);
@@ -118,7 +126,27 @@ export async function buildInstallPlan({
118
126
  });
119
127
  } else {
120
128
  const sourcePath = safeResolve(templatesRoot, item.sourceTemplate);
121
- const stat = await fs.stat(sourcePath);
129
+ let stat;
130
+ try {
131
+ stat = await fs.stat(sourcePath);
132
+ } catch (error) {
133
+ if (error?.code === 'ENOENT' && isRootGitignoreItem(item)) {
134
+ const targetPath = safeResolve(projectRoot, item.targetPath);
135
+ explicitTargetPaths.add(targetPath);
136
+ planEntries.push({
137
+ id: item.id,
138
+ type: item.type,
139
+ sourcePath: null,
140
+ targetPath,
141
+ mergeStrategy: item.mergeStrategy,
142
+ renderedContent: FALLBACK_ROOT_GITIGNORE_TEMPLATE,
143
+ requires: item.requires ?? [],
144
+ mode: 0o644,
145
+ });
146
+ continue;
147
+ }
148
+ throw error;
149
+ }
122
150
  if (stat.isDirectory()) {
123
151
  const nestedFiles = await walkDirectory(sourcePath);
124
152
  const nestedStats = await Promise.all(nestedFiles.map((filePath) => fs.stat(filePath)));
@@ -49,7 +49,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
49
49
  const safeOverrides = isPlainObject(overrides) ? overrides : {};
50
50
 
51
51
  return mergeObjects({
52
- version: '1.2.2',
52
+ version: '1.3.1',
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',
@@ -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$/,