@ngockhoale/ukit 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/ukit +12 -4
  3. package/manifests/engineConformance.yaml +29 -0
  4. package/manifests/platform.full.yaml +13 -0
  5. package/package.json +2 -1
  6. package/scripts/audit/decision-coverage.mjs +295 -0
  7. package/scripts/bench/outline-savings.mjs +19 -4
  8. package/scripts/bench/parallel-agents.mjs +15 -4
  9. package/scripts/bench/runGold.mjs +22 -4
  10. package/scripts/bench/v3-ceremony.mjs +7 -1
  11. package/scripts/bug/triage.mjs +56 -17
  12. package/scripts/index/build-index.mjs +94 -28
  13. package/scripts/index/query-index.mjs +48 -14
  14. package/scripts/index/refresh-index.mjs +142 -62
  15. package/scripts/perf/audit-perf.mjs +8 -2
  16. package/scripts/skill/audit-skill.mjs +54 -25
  17. package/src/bug/triageBug.js +9 -6
  18. package/src/cli/adapters.js +6 -0
  19. package/src/cli/commands/code.js +7 -1
  20. package/src/cli/commands/indexArgs.js +4 -2
  21. package/src/cli/commands/indexTools.js +8 -1
  22. package/src/cli/commands/install.js +13 -0
  23. package/src/cli/commands/memory.js +10 -3
  24. package/src/cli/commands/status.js +17 -1
  25. package/src/cli/commands/update.js +7 -0
  26. package/src/context/detectProjectContext.js +3 -1
  27. package/src/core/codeintel/analogy.js +1 -1
  28. package/src/core/codeintel/diagnostics.js +9 -6
  29. package/src/core/codeintel/graph.js +14 -8
  30. package/src/core/codeintel/impact.js +0 -1
  31. package/src/core/codeintel/invalidation.js +5 -3
  32. package/src/core/codeintel/packet.js +11 -0
  33. package/src/core/codeintel/router.js +17 -3
  34. package/src/core/codeintel/semanticProvider.js +1 -1
  35. package/src/core/codeintel/summaries.js +11 -7
  36. package/src/core/compact/contextBudget.js +26 -12
  37. package/src/core/compact/index.js +15 -8
  38. package/src/core/docContracts.js +10 -2
  39. package/src/core/experiments/deliberation.js +321 -0
  40. package/src/core/experiments/dynamicWorkflow.js +492 -0
  41. package/src/core/fileOps.js +8 -1
  42. package/src/core/gatewayProbe.js +29 -1
  43. package/src/core/gatewayResilienceEnv.js +44 -3
  44. package/src/core/handoffDocValidator.js +3 -1
  45. package/src/core/hookChainDoctor.js +16 -1
  46. package/src/core/memory/deltaOverlays.js +448 -0
  47. package/src/core/memory/learningCandidates.js +302 -0
  48. package/src/core/memory/migrate.js +59 -32
  49. package/src/core/memory/recordStore.js +24 -1
  50. package/src/core/memory/store.js +44 -8
  51. package/src/core/memory/storeV2.js +48 -33
  52. package/src/core/memory/userMemory.js +11 -10
  53. package/src/core/output/index.js +15 -10
  54. package/src/core/permissionPolicy.js +8 -0
  55. package/src/core/runtimeConfig.js +224 -4
  56. package/src/core/sensitiveValueScanner.js +10 -2
  57. package/src/core/taskBudgetValidator.js +12 -17
  58. package/src/core/taskProgressGuard.js +59 -9
  59. package/src/core/unattendedDoctor.js +5 -2
  60. package/src/core/uninstall.js +37 -8
  61. package/src/decision/client.js +371 -0
  62. package/src/decision/lease.js +198 -0
  63. package/src/decision/preflight.js +492 -0
  64. package/src/decision/protocol.js +308 -0
  65. package/src/decision/registry.js +384 -0
  66. package/src/decision/shadow.js +281 -0
  67. package/src/decision/statePacket.js +165 -0
  68. package/src/diagnostics/failurePatterns.js +2 -1
  69. package/src/diagnostics/ledgerFiles.js +3 -1
  70. package/src/diagnostics/routeOutcomes.js +1 -29
  71. package/src/index/buildIndex.js +23 -11
  72. package/src/index/impactContext.js +21 -2
  73. package/src/index/importResolution.js +13 -7
  74. package/src/index/queryIndex.js +11 -5
  75. package/src/index/resolveContext.js +16 -6
  76. package/src/index/taskRouting.js +63 -2
  77. package/src/index/verificationPlan.js +12 -1
  78. package/src/learning/patternProposals.js +6 -0
  79. package/src/render/renderTemplate.js +1 -1
  80. package/src/skill/auditSkill.js +3 -1
  81. package/src/stack/detectStack.js +3 -1
  82. package/template_project/.claude/agents/handoff-planner.md +2 -5
  83. package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
  84. package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
  85. package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
  86. package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
  87. package/template_project/.claude/hooks/handoff-resume.sh +10 -1
  88. package/template_project/.claude/hooks/protect-files.sh +0 -1
  89. package/template_project/.claude/hooks/record-execution.mjs +13 -1
  90. package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
  91. package/template_project/.claude/hooks/session-episode.sh +9 -2
  92. package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
  93. package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
  94. package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
  95. package/template_project/.claude/ukit/index/route-task.mjs +444 -0
  96. package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
  97. package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
  98. package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
  99. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
  100. package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
  101. package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
  102. package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
  103. package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
  104. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
  105. package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
  106. package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
  107. package/template_project/ukit/storage/config.json +34 -1
  108. package/template_project/.claude/ukit/skill-router-state.json +0 -1
  109. package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
@@ -1,36 +1,102 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
 
4
- import { buildCodeIndex, buildDerivedIndexArtifacts } from '../../src/index/buildIndex.js';
5
-
6
- const args = process.argv.slice(2);
7
- const rootArg = readFlagValue(args, '--root');
8
- const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
9
-
10
- const summary = await buildCodeIndex({ rootDir });
11
- await buildDerivedIndexArtifacts(rootDir);
12
-
13
- console.log('[index:build] completed');
14
- console.log(`root: ${rootDir}`);
15
- console.log(`files: ${summary.fileCount}`);
16
- console.log(`symbols: ${summary.symbolCount}`);
17
- console.log(`imports: ${summary.importCount}`);
18
- console.log(`tests-map: ${summary.testsMapCount}`);
19
- console.log(`hotspots: ${summary.hotspotCount}`);
20
- console.log(`parsed: ${summary.parsedCodeFileCount}`);
21
- console.log(`reused: ${summary.reusedCodeFileCount}`);
22
- console.log(`indexDir: ${summary.indexDir}`);
23
-
24
- function readFlagValue(argv, flag) {
25
- const exact = argv.indexOf(flag);
26
- if (exact >= 0 && argv[exact + 1]) {
27
- return argv[exact + 1];
5
+ import {
6
+ buildCodeIndex,
7
+ buildDerivedIndexArtifacts,
8
+ inspectIndexStaleness,
9
+ mapLimit,
10
+ INDEX_IO_CONCURRENCY,
11
+ } from '../../src/index/buildIndex.js';
12
+
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const { value: rootArg, args: remainingArgs } = extractFlagValue(args, '--root');
16
+ const unexpected = remainingArgs.filter((arg) => arg.startsWith('--'));
17
+ if (unexpected.length > 0) {
18
+ throw new Error(`unexpected argument: ${unexpected[0]}`);
19
+ }
20
+ const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
21
+
22
+ // One enumeration serves both the readability check and the build: the
23
+ // snapshot is handed to buildCodeIndex, which reuses it instead of
24
+ // rediscovering the repository.
25
+ const { snapshot } = await inspectIndexStaleness({ rootDir, maxAgeMs: 0 });
26
+ const unreadable = await collectUnreadableFiles(rootDir, snapshot?.files ?? []);
27
+ for (const filePath of unreadable) {
28
+ console.error(`[index:build] skipped unreadable file: ${filePath}`);
28
29
  }
29
30
 
30
- const withEquals = argv.find((item) => item.startsWith(`${flag}=`));
31
- if (withEquals) {
32
- return withEquals.slice(flag.length + 1);
31
+ const summary = await buildCodeIndex({ rootDir, discoverySnapshot: snapshot });
32
+ await buildDerivedIndexArtifacts(rootDir);
33
+
34
+ console.log('[index:build] completed');
35
+ console.log(`root: ${rootDir}`);
36
+ console.log(`files: ${summary.fileCount}`);
37
+ console.log(`symbols: ${summary.symbolCount}`);
38
+ console.log(`imports: ${summary.importCount}`);
39
+ console.log(`tests-map: ${summary.testsMapCount}`);
40
+ console.log(`hotspots: ${summary.hotspotCount}`);
41
+ console.log(`parsed: ${summary.parsedCodeFileCount}`);
42
+ console.log(`reused: ${summary.reusedCodeFileCount}`);
43
+ if (unreadable.length > 0) {
44
+ console.log(`skipped-unreadable: ${unreadable.length}`);
33
45
  }
46
+ console.log(`indexDir: ${summary.indexDir}`);
47
+ }
48
+
49
+ // The parse worker inside buildCodeIndex skips files it cannot read; the CLI
50
+ // surfaces them so a partial index is never silent.
51
+ async function collectUnreadableFiles(rootDir, entries) {
52
+ const unreadable = await mapLimit(entries, INDEX_IO_CONCURRENCY, async (entry) => {
53
+ try {
54
+ await fs.access(entry.absolutePath, fs.constants.R_OK);
55
+ return null;
56
+ } catch {
57
+ return path.relative(rootDir, entry.absolutePath).split(path.sep).join('/');
58
+ }
59
+ });
60
+ return unreadable.filter(Boolean).sort();
61
+ }
62
+
63
+ function extractFlagValue(argv, flag) {
64
+ let value = null;
65
+ const nextArgs = [];
66
+
67
+ for (let i = 0; i < argv.length; i += 1) {
68
+ const arg = argv[i];
69
+
70
+ if (arg === flag) {
71
+ if (value !== null) {
72
+ nextArgs.push(arg);
73
+ continue;
74
+ }
75
+ const maybeValue = argv[i + 1];
76
+ if (!maybeValue || maybeValue.startsWith('--')) {
77
+ throw new Error(`Missing value for ${flag}.`);
78
+ }
79
+ value = maybeValue;
80
+ i += 1;
81
+ continue;
82
+ }
83
+
84
+ if (arg.startsWith(`${flag}=`)) {
85
+ if (value === null) {
86
+ value = arg.slice(flag.length + 1);
87
+ continue;
88
+ }
89
+ }
90
+
91
+ nextArgs.push(arg);
92
+ }
93
+
94
+ return { value, args: nextArgs };
95
+ }
34
96
 
35
- return null;
97
+ try {
98
+ await main();
99
+ } catch (error) {
100
+ console.error(error && error.message ? error.message : error);
101
+ process.exit(1);
36
102
  }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
 
4
5
  import {
@@ -7,28 +8,40 @@ import {
7
8
  import {
8
9
  buildCodeIndex,
9
10
  buildDerivedIndexArtifacts,
11
+ inspectIndexStaleness,
10
12
  isIndexStale,
13
+ mapLimit,
14
+ INDEX_IO_CONCURRENCY,
11
15
  DEFAULT_INDEX_CACHE_MAX_AGE_MS,
12
16
  } from '../../src/index/buildIndex.js';
13
17
  import { parseIndexArgs, QUERY_FLAG_DEFINITIONS } from '../../src/cli/commands/indexArgs.js';
14
18
 
15
- const args = process.argv.slice(2);
16
- const { value: rootArg, args: afterRootArgs } = extractFlagValue(args, '--root');
17
- const { value: changedArg, args: remainingArgs } = extractFlagValue(afterRootArgs, '--changed');
18
- const changedFiles = changedArg
19
- ? changedArg.split(',').map((item) => item.trim()).filter(Boolean)
20
- : [];
21
- const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
19
+ async function main() {
20
+ const args = process.argv.slice(2);
21
+ const { value: rootArg, args: afterRootArgs } = extractFlagValue(args, '--root');
22
+ const { value: changedArg, args: remainingArgs } = extractFlagValue(afterRootArgs, '--changed');
23
+ const changedFiles = changedArg
24
+ ? changedArg.split(',').map((item) => item.trim()).filter(Boolean)
25
+ : [];
26
+ const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
27
+
28
+ const { tokens, flags } = parseIndexArgs(remainingArgs, QUERY_FLAG_DEFINITIONS);
29
+ const query = tokens.join(' ').trim();
30
+ if (!query) {
31
+ console.error('Usage: yarn index:query -- "<error|symbol|path>" [--limit N] [--changed fileA,fileB]');
32
+ process.exitCode = 1;
33
+ return;
34
+ }
22
35
 
23
- const { tokens, flags } = parseIndexArgs(remainingArgs, QUERY_FLAG_DEFINITIONS);
24
- const query = tokens.join(' ').trim();
25
- if (!query) {
26
- console.error('Usage: yarn index:query -- "<error|symbol|path>" [--limit N] [--changed fileA,fileB]');
27
- process.exitCode = 1;
28
- } else {
29
36
  const stale = await isIndexStale({ rootDir, maxAgeMs: DEFAULT_INDEX_CACHE_MAX_AGE_MS });
30
37
  if (stale) {
31
- await buildCodeIndex({ rootDir });
38
+ // One enumeration serves the readability check and the rebuild.
39
+ const { snapshot } = await inspectIndexStaleness({ rootDir, maxAgeMs: 0 });
40
+ const unreadable = await collectUnreadableFiles(rootDir, snapshot?.files ?? []);
41
+ for (const filePath of unreadable) {
42
+ console.error(`[index:query] skipped unreadable file: ${filePath}`);
43
+ }
44
+ await buildCodeIndex({ rootDir, discoverySnapshot: snapshot });
32
45
  await buildDerivedIndexArtifacts(rootDir);
33
46
  }
34
47
 
@@ -59,6 +72,20 @@ if (!query) {
59
72
  }
60
73
  }
61
74
 
75
+ // The parse worker inside buildCodeIndex skips files it cannot read; the CLI
76
+ // surfaces them so a partial index is never silent.
77
+ async function collectUnreadableFiles(rootDir, entries) {
78
+ const unreadable = await mapLimit(entries, INDEX_IO_CONCURRENCY, async (entry) => {
79
+ try {
80
+ await fs.access(entry.absolutePath, fs.constants.R_OK);
81
+ return null;
82
+ } catch {
83
+ return path.relative(rootDir, entry.absolutePath).split(path.sep).join('/');
84
+ }
85
+ });
86
+ return unreadable.filter(Boolean).sort();
87
+ }
88
+
62
89
  function extractFlagValue(argv, flag) {
63
90
  let value = null;
64
91
  const nextArgs = [];
@@ -92,3 +119,10 @@ function extractFlagValue(argv, flag) {
92
119
 
93
120
  return { value, args: nextArgs };
94
121
  }
122
+
123
+ try {
124
+ await main();
125
+ } catch (error) {
126
+ console.error(error && error.message ? error.message : error);
127
+ process.exit(1);
128
+ }
@@ -5,46 +5,63 @@ import path from 'node:path';
5
5
  import {
6
6
  buildCodeIndex,
7
7
  inspectIndexStaleness,
8
+ mapLimit,
9
+ INDEX_IO_CONCURRENCY,
8
10
  DEFAULT_INDEX_CACHE_MAX_AGE_MS,
9
11
  } from '../../src/index/buildIndex.js';
10
12
 
11
- const args = process.argv.slice(2);
12
- const rootArg = readFlagValue(args, '--root');
13
- const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
13
+ async function main() {
14
+ const args = process.argv.slice(2);
15
+ const { value: rootArg, args: afterRootArgs } = extractFlagValue(args, '--root');
16
+ const { value: changedArg, args: remainingArgs } = extractFlagValue(afterRootArgs, '--changed');
17
+ const force = remainingArgs.includes('--force');
18
+ const unexpected = remainingArgs.filter((arg) => arg !== '--force');
19
+ if (unexpected.length > 0) {
20
+ throw new Error(`unexpected argument: ${unexpected[0]}`);
21
+ }
14
22
 
15
- const changedArg = readFlagValue(args, '--changed');
16
- const changedFiles = changedArg
17
- ? changedArg.split(',').map((item) => item.trim()).filter(Boolean)
18
- : [];
23
+ const rootDir = rootArg ? path.resolve(rootArg) : process.cwd();
24
+ const hintedFiles = changedArg
25
+ ? changedArg.split(',').map((item) => item.trim()).filter(Boolean)
26
+ : [];
19
27
 
20
- const force = readBooleanFlag(args, '--force');
21
- const lastRefreshMs = await getLastRefreshTime(rootDir);
28
+ // Hints that exist but cannot be read would be silently dropped by the parse
29
+ // worker; surface them and keep them out of the merge set. ENOENT hints are
30
+ // kept — a deleted file is a legitimate removal hint.
31
+ const { readable: changedFiles, unreadable: unreadableHints } = await partitionReadableHints(rootDir, hintedFiles);
32
+ for (const filePath of unreadableHints) {
33
+ console.error(`[index:refresh] skipped unreadable file: ${filePath}`);
34
+ }
22
35
 
23
- if (changedFiles.length > 0 && !force) {
24
- // Hints carry the change set, so no staleness enumeration is needed:
25
- // buildCodeIndex merges the hinted paths into the existing artifacts and
26
- // falls back to the bounded full discovery path on its own when the hints
27
- // are unsafe or the previous artifacts cannot be merged (TASK-023).
28
- const summary = await buildCodeIndex({ rootDir, changedFiles });
36
+ const lastRefreshMs = await getLastRefreshTime(rootDir);
29
37
 
30
- console.log('[index:refresh] completed');
31
- console.log(`mode: ${summary.mode}`);
32
- if (lastRefreshMs !== null) {
33
- console.log(`lastRefreshBefore: ${new Date(lastRefreshMs).toISOString()}`);
34
- }
35
- console.log(`changed-files-hint: ${changedFiles.join(', ')}`);
36
- console.log(`root: ${rootDir}`);
37
- console.log(`files: ${summary.fileCount}`);
38
- console.log(`parsed: ${summary.parsedCodeFileCount}`);
39
- console.log(`reused: ${summary.reusedCodeFileCount}`);
40
- if (summary.removedFiles.length > 0) {
41
- console.log(`removed: ${summary.removedFiles.length}`);
38
+ if (changedFiles.length > 0 && !force) {
39
+ // Hints carry the change set, so no staleness enumeration is needed:
40
+ // buildCodeIndex merges the hinted paths into the existing artifacts and
41
+ // falls back to the bounded full discovery path on its own when the hints
42
+ // are unsafe or the previous artifacts cannot be merged (TASK-023).
43
+ const summary = await buildCodeIndex({ rootDir, changedFiles });
44
+
45
+ console.log('[index:refresh] completed');
46
+ console.log(`mode: ${summary.mode}`);
47
+ if (lastRefreshMs !== null) {
48
+ console.log(`lastRefreshBefore: ${new Date(lastRefreshMs).toISOString()}`);
49
+ }
50
+ console.log(`changed-files-hint: ${changedFiles.join(', ')}`);
51
+ console.log(`root: ${rootDir}`);
52
+ console.log(`files: ${summary.fileCount}`);
53
+ console.log(`parsed: ${summary.parsedCodeFileCount}`);
54
+ console.log(`reused: ${summary.reusedCodeFileCount}`);
55
+ if (summary.removedFiles.length > 0) {
56
+ console.log(`removed: ${summary.removedFiles.length}`);
57
+ }
58
+ console.log(`indexDir: ${summary.indexDir}`);
59
+ return;
42
60
  }
43
- console.log(`indexDir: ${summary.indexDir}`);
44
- } else {
45
- const staleness = force
46
- ? null
47
- : await inspectIndexStaleness({ rootDir, maxAgeMs: DEFAULT_INDEX_CACHE_MAX_AGE_MS });
61
+
62
+ // One enumeration serves the staleness check, the readability check, and the
63
+ // build: the snapshot is handed to buildCodeIndex, which reuses it.
64
+ const staleness = await inspectIndexStaleness({ rootDir, maxAgeMs: DEFAULT_INDEX_CACHE_MAX_AGE_MS });
48
65
  const stale = force || staleness.stale;
49
66
 
50
67
  if (!stale) {
@@ -56,45 +73,101 @@ if (changedFiles.length > 0 && !force) {
56
73
  console.log(`changed-files-hint: ${changedFiles.join(', ')}`);
57
74
  }
58
75
  console.log(`root: ${rootDir}`);
59
- } else {
60
- // Reuse the staleness check's discovery snapshot: one enumeration per refresh.
61
- const summary = await buildCodeIndex({
62
- rootDir,
63
- discoverySnapshot: staleness ? staleness.snapshot : null,
64
- });
76
+ return;
77
+ }
65
78
 
66
- console.log('[index:refresh] completed');
67
- console.log(`mode: ${summary.mode}`);
68
- if (lastRefreshMs !== null) {
69
- console.log(`lastRefreshBefore: ${new Date(lastRefreshMs).toISOString()}`);
79
+ const unreadable = await collectUnreadableFiles(rootDir, staleness.snapshot?.files ?? []);
80
+ for (const filePath of unreadable) {
81
+ console.error(`[index:refresh] skipped unreadable file: ${filePath}`);
82
+ }
83
+
84
+ const summary = await buildCodeIndex({
85
+ rootDir,
86
+ discoverySnapshot: staleness.snapshot,
87
+ });
88
+
89
+ console.log('[index:refresh] completed');
90
+ console.log(`mode: ${summary.mode}`);
91
+ if (lastRefreshMs !== null) {
92
+ console.log(`lastRefreshBefore: ${new Date(lastRefreshMs).toISOString()}`);
93
+ }
94
+ if (changedFiles.length > 0) {
95
+ console.log(`changed-files-hint: ${changedFiles.join(', ')}`);
96
+ }
97
+ console.log(`root: ${rootDir}`);
98
+ console.log(`files: ${summary.fileCount}`);
99
+ console.log(`parsed: ${summary.parsedCodeFileCount}`);
100
+ console.log(`reused: ${summary.reusedCodeFileCount}`);
101
+ if (unreadable.length > 0) {
102
+ console.log(`skipped-unreadable: ${unreadable.length}`);
103
+ }
104
+ console.log(`indexDir: ${summary.indexDir}`);
105
+ }
106
+
107
+ // The parse worker inside buildCodeIndex skips files it cannot read; the CLI
108
+ // surfaces them so a partial index is never silent.
109
+ async function collectUnreadableFiles(rootDir, entries) {
110
+ const unreadable = await mapLimit(entries, INDEX_IO_CONCURRENCY, async (entry) => {
111
+ try {
112
+ await fs.access(entry.absolutePath, fs.constants.R_OK);
113
+ return null;
114
+ } catch {
115
+ return path.relative(rootDir, entry.absolutePath).split(path.sep).join('/');
70
116
  }
71
- if (changedFiles.length > 0) {
72
- console.log(`changed-files-hint: ${changedFiles.join(', ')}`);
117
+ });
118
+ return unreadable.filter(Boolean).sort();
119
+ }
120
+
121
+ async function partitionReadableHints(rootDir, hints) {
122
+ const readable = [];
123
+ const unreadable = [];
124
+ for (const hint of hints) {
125
+ try {
126
+ await fs.access(path.resolve(rootDir, hint), fs.constants.R_OK);
127
+ readable.push(hint);
128
+ } catch (error) {
129
+ if (error?.code === 'ENOENT') {
130
+ readable.push(hint); // deleted file — a legitimate removal hint
131
+ } else {
132
+ unreadable.push(hint);
133
+ }
73
134
  }
74
- console.log(`root: ${rootDir}`);
75
- console.log(`files: ${summary.fileCount}`);
76
- console.log(`parsed: ${summary.parsedCodeFileCount}`);
77
- console.log(`reused: ${summary.reusedCodeFileCount}`);
78
- console.log(`indexDir: ${summary.indexDir}`);
79
135
  }
136
+ return { readable, unreadable };
80
137
  }
81
138
 
82
- function readFlagValue(argv, flag) {
83
- const exact = argv.indexOf(flag);
84
- if (exact >= 0 && argv[exact + 1]) {
85
- return argv[exact + 1];
86
- }
139
+ function extractFlagValue(argv, flag) {
140
+ let value = null;
141
+ const nextArgs = [];
87
142
 
88
- const withEquals = argv.find((item) => item.startsWith(`${flag}=`));
89
- if (withEquals) {
90
- return withEquals.slice(flag.length + 1);
91
- }
143
+ for (let i = 0; i < argv.length; i += 1) {
144
+ const arg = argv[i];
92
145
 
93
- return null;
94
- }
146
+ if (arg === flag) {
147
+ if (value !== null) {
148
+ nextArgs.push(arg);
149
+ continue;
150
+ }
151
+ const maybeValue = argv[i + 1];
152
+ if (!maybeValue || maybeValue.startsWith('--')) {
153
+ throw new Error(`Missing value for ${flag}.`);
154
+ }
155
+ value = maybeValue;
156
+ i += 1;
157
+ continue;
158
+ }
159
+
160
+ if (arg.startsWith(`${flag}=`)) {
161
+ if (value === null) {
162
+ value = arg.slice(flag.length + 1);
163
+ continue;
164
+ }
165
+ }
95
166
 
96
- function readBooleanFlag(argv, flag) {
97
- return argv.includes(flag);
167
+ nextArgs.push(arg);
168
+ }
169
+
170
+ return { value, args: nextArgs };
98
171
  }
99
172
 
100
173
  async function getLastRefreshTime(rootDir) {
@@ -113,3 +186,10 @@ async function getLastRefreshTime(rootDir) {
113
186
  return null;
114
187
  }
115
188
  }
189
+
190
+ try {
191
+ await main();
192
+ } catch (error) {
193
+ console.error(error && error.message ? error.message : error);
194
+ process.exit(1);
195
+ }
@@ -53,6 +53,12 @@ const CLUSTER_GAP_MS = 1500;
53
53
  // is a finding in its own right, not merely a term inside a chain total.
54
54
  const HOTSPOT_P50_MS = 50;
55
55
 
56
+ // TASK-010: every spawned child carries an explicit timeout + killSignal — a
57
+ // wedged hook script or git child must fail the benchmark sample, not hang the
58
+ // audit forever (same class as BUG-C22-20/21). UKIT_PERF_SPAWN_TIMEOUT_MS
59
+ // overrides the budget for tests.
60
+ const SPAWN_TIMEOUT_MS = Number(process.env.UKIT_PERF_SPAWN_TIMEOUT_MS) || 60 * 1000; // 1 min
61
+
56
62
  // A hook row costs the wrapper `bash`, its `node` runtime, and the advisory
57
63
  // telemetry `node --finish` child armed by hook-telemetry.sh.
58
64
  const PROCESSES_PER_HOOK_ROW = 3;
@@ -420,7 +426,7 @@ function benchSpawn(file, args, iterations, env) {
420
426
  const samples = [];
421
427
  for (let i = 0; i < iterations; i += 1) {
422
428
  const start = process.hrtime.bigint();
423
- spawnSync(file, args, { env, input: '{}', stdio: ['pipe', 'ignore', 'ignore'] });
429
+ spawnSync(file, args, { env, input: '{}', stdio: ['pipe', 'ignore', 'ignore'], timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL' });
424
430
  samples.push(Number(process.hrtime.bigint() - start) / 1e6);
425
431
  }
426
432
  return summarise(samples);
@@ -560,7 +566,7 @@ function recentHotPathAdditions(root, sinceDays) {
560
566
  '.claude/hooks',
561
567
  '.claude/ukit/runtime',
562
568
  ],
563
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
569
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL' },
564
570
  );
565
571
  const commits = [];
566
572
  let current = null;
@@ -1,32 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  import { initSkillAudit, recordAuditEntry, statusSkillAudit } from '../../src/skill/auditSkill.js';
3
3
 
4
- const [command, skillName] = process.argv.slice(2);
5
- const rootDir = process.cwd();
4
+ const USAGE = 'Usage: yarn skill:audit <init|status|record> <skill-name>';
6
5
 
7
- if (!command || !skillName) {
8
- console.error('Usage: yarn skill:audit <init|status|record> <skill-name>');
9
- process.exitCode = 1;
10
- } else if (command === 'init') {
11
- const result = await initSkillAudit({ rootDir, skillName });
12
- console.log(`[skill:audit init] ${skillName}`);
13
- console.log(`dir: ${result.auditDir}`);
14
- console.log(`copied: ${result.copied.join(', ') || 'none'}`);
15
- console.log(`skipped (already present): ${result.skipped.join(', ') || 'none'}`);
16
- } else if (command === 'status') {
17
- const result = await statusSkillAudit({ rootDir, skillName });
18
- console.log(`[skill:audit status] ${skillName}`);
19
- console.log(`entries: ${result.entryCount}`);
20
- console.log(`latest: ${result.latest ? JSON.stringify(result.latest) : 'none'}`);
21
- console.log(`open rationalizations: ${result.openRationalizations.length}`);
22
- } else if (command === 'record') {
23
- const stdinRaw = await readStdin();
24
- const entry = JSON.parse(stdinRaw);
25
- const record = await recordAuditEntry({ rootDir, skillName, entry });
26
- console.log(`[skill:audit record] ${skillName}`);
27
- console.log(JSON.stringify(record, null, 2));
28
- } else {
29
- console.error(`Unknown command: ${command}. Usage: yarn skill:audit <init|status|record> <skill-name>`);
6
+ async function main() {
7
+ const [command, skillName, ...extra] = process.argv.slice(2);
8
+ const rootDir = process.cwd();
9
+
10
+ if (!command || !skillName) {
11
+ console.error(USAGE);
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ if (extra.length > 0) {
16
+ throw new Error(`unexpected argument: ${extra[0]}. ${USAGE}`);
17
+ }
18
+
19
+ if (command === 'init') {
20
+ const result = await initSkillAudit({ rootDir, skillName });
21
+ console.log(`[skill:audit init] ${skillName}`);
22
+ console.log(`dir: ${result.auditDir}`);
23
+ console.log(`copied: ${result.copied.join(', ') || 'none'}`);
24
+ console.log(`skipped (already present): ${result.skipped.join(', ') || 'none'}`);
25
+ return;
26
+ }
27
+
28
+ if (command === 'status') {
29
+ const result = await statusSkillAudit({ rootDir, skillName });
30
+ console.log(`[skill:audit status] ${skillName}`);
31
+ console.log(`entries: ${result.entryCount}`);
32
+ console.log(`latest: ${result.latest ? JSON.stringify(result.latest) : 'none'}`);
33
+ console.log(`open rationalizations: ${result.openRationalizations.length}`);
34
+ return;
35
+ }
36
+
37
+ if (command === 'record') {
38
+ const stdinRaw = await readStdin();
39
+ let entry;
40
+ try {
41
+ entry = JSON.parse(stdinRaw);
42
+ } catch (error) {
43
+ throw new Error(`skill:audit record expects a JSON entry on stdin: ${error.message}`);
44
+ }
45
+ const record = await recordAuditEntry({ rootDir, skillName, entry });
46
+ console.log(`[skill:audit record] ${skillName}`);
47
+ console.log(JSON.stringify(record, null, 2));
48
+ return;
49
+ }
50
+
51
+ console.error(`Unknown command: ${command}. ${USAGE}`);
30
52
  process.exitCode = 1;
31
53
  }
32
54
 
@@ -37,3 +59,10 @@ async function readStdin() {
37
59
  }
38
60
  return Buffer.concat(chunks).toString('utf8');
39
61
  }
62
+
63
+ try {
64
+ await main();
65
+ } catch (error) {
66
+ console.error(error && error.message ? error.message : error);
67
+ process.exit(1);
68
+ }
@@ -2,6 +2,7 @@ import { queryCodeIndex } from '../index/queryIndex.js';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { detectPackageManager } from '../core/packageManager.js';
5
+ import { readJsonIfExists } from '../core/fileOps.js';
5
6
  import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from '../index/relatedTests.js';
6
7
 
7
8
  const DEEP_KEYWORDS = ['race', 'flaky', 'intermittent', 'timeout', 'deadlock'];
@@ -28,10 +29,9 @@ export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
28
29
  const directRecommendedTestFile = prioritizedResults
29
30
  .flatMap((item) => item.tests ?? [])
30
31
  .find(Boolean);
31
- const shouldLoadRelatedArtifacts = Boolean(topResult) || !directRecommendedTestFile;
32
- const relatedArtifacts = shouldLoadRelatedArtifacts
33
- ? await loadRelatedTestArtifacts({ rootDir })
34
- : null;
32
+ // `Boolean(topResult) || !directRecommendedTestFile` is always true (no
33
+ // topResult ⇒ no tests ⇒ no direct file), so artifacts load unconditionally.
34
+ const relatedArtifacts = await loadRelatedTestArtifacts({ rootDir });
35
35
  const inferredRecommendedTests = directRecommendedTestFile
36
36
  ? []
37
37
  : inferRelatedTestsFromArtifacts({
@@ -70,9 +70,12 @@ export async function triageBug({ rootDir = process.cwd(), signature } = {}) {
70
70
  ],
71
71
  };
72
72
  }
73
-
74
73
  async function buildTestCommand(rootDir, testFile) {
75
- const packageManager = await detectPackageManager(rootDir);
74
+ // Pass the parsed package.json so a declared `packageManager` field wins over
75
+ // lockfile/npm defaults — detectPackageManager(rootDir) alone ignored it.
76
+ const packageJson = await readJsonIfExists(path.join(rootDir, 'package.json')).catch(() => null);
77
+ const packageManager = await detectPackageManager(rootDir, packageJson);
78
+
76
79
 
77
80
  if (packageManager === 'pnpm') {
78
81
  return `pnpm test ${testFile}`;
@@ -105,6 +105,12 @@ export function resolveOptionalToolKeys(toolsArg) {
105
105
 
106
106
  const normalized = toolsArg.trim().toLowerCase();
107
107
 
108
+ if (!normalized) {
109
+ // `--tools=` / `--tools ""` used to fall through to an empty selection —
110
+ // silently deselecting every optional adapter. Treat it as a missing value.
111
+ throw new Error(`Missing value for --tools. Use --tools=${TOOLS_FLAG_HELP}`);
112
+ }
113
+
108
114
  if (normalized === 'all') {
109
115
  return [...DEFAULT_OPTIONAL_TOOL_KEYS];
110
116
  }
@@ -12,8 +12,14 @@ const SUBCOMMANDS = new Set(['peek', 'search', 'context', 'impact']);
12
12
  function extractFlag(args, flag) {
13
13
  const index = args.indexOf(flag);
14
14
  if (index < 0) return { value: null, rest: args };
15
+ const value = args[index + 1];
16
+ // A missing value — or the next flag swallowed as the value — used to pass
17
+ // silently (e.g. `--mode --budget 5` set mode='--budget' and dropped --budget).
18
+ if (typeof value !== 'string' || value.startsWith('--')) {
19
+ throw new Error(`Missing value for ${flag}.`);
20
+ }
15
21
  return {
16
- value: args[index + 1] ?? null,
22
+ value,
17
23
  rest: [...args.slice(0, index), ...args.slice(index + 2)],
18
24
  };
19
25
  }