@ngockhoale/ukit 3.0.3 → 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.
- package/CHANGELOG.md +17 -0
- package/bin/ukit +12 -4
- package/manifests/engineConformance.yaml +29 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +2 -1
- package/scripts/audit/decision-coverage.mjs +295 -0
- package/scripts/bench/outline-savings.mjs +19 -4
- package/scripts/bench/parallel-agents.mjs +15 -4
- package/scripts/bench/runGold.mjs +22 -4
- package/scripts/bench/v3-ceremony.mjs +7 -1
- package/scripts/bug/triage.mjs +56 -17
- package/scripts/index/build-index.mjs +94 -28
- package/scripts/index/query-index.mjs +48 -14
- package/scripts/index/refresh-index.mjs +142 -62
- package/scripts/perf/audit-perf.mjs +8 -2
- package/scripts/skill/audit-skill.mjs +54 -25
- package/src/bug/triageBug.js +9 -6
- package/src/cli/adapters.js +6 -0
- package/src/cli/commands/code.js +7 -1
- package/src/cli/commands/indexArgs.js +4 -2
- package/src/cli/commands/indexTools.js +8 -1
- package/src/cli/commands/install.js +13 -0
- package/src/cli/commands/memory.js +10 -3
- package/src/cli/commands/status.js +17 -1
- package/src/cli/commands/update.js +7 -0
- package/src/context/detectProjectContext.js +3 -1
- package/src/core/codeintel/analogy.js +1 -1
- package/src/core/codeintel/diagnostics.js +9 -6
- package/src/core/codeintel/graph.js +14 -8
- package/src/core/codeintel/impact.js +0 -1
- package/src/core/codeintel/invalidation.js +5 -3
- package/src/core/codeintel/packet.js +11 -0
- package/src/core/codeintel/router.js +17 -3
- package/src/core/codeintel/semanticProvider.js +1 -1
- package/src/core/codeintel/summaries.js +11 -7
- package/src/core/compact/contextBudget.js +26 -12
- package/src/core/compact/index.js +15 -8
- package/src/core/docContracts.js +10 -2
- package/src/core/experiments/deliberation.js +321 -0
- package/src/core/experiments/dynamicWorkflow.js +492 -0
- package/src/core/fileOps.js +8 -1
- package/src/core/gatewayProbe.js +29 -1
- package/src/core/gatewayResilienceEnv.js +44 -3
- package/src/core/handoffDocValidator.js +3 -1
- package/src/core/hookChainDoctor.js +16 -1
- package/src/core/memory/deltaOverlays.js +448 -0
- package/src/core/memory/learningCandidates.js +302 -0
- package/src/core/memory/migrate.js +59 -32
- package/src/core/memory/recordStore.js +24 -1
- package/src/core/memory/store.js +44 -8
- package/src/core/memory/storeV2.js +48 -33
- package/src/core/memory/userMemory.js +11 -10
- package/src/core/output/index.js +15 -10
- package/src/core/permissionPolicy.js +8 -0
- package/src/core/runtimeConfig.js +224 -4
- package/src/core/sensitiveValueScanner.js +10 -2
- package/src/core/taskBudgetValidator.js +12 -17
- package/src/core/taskProgressGuard.js +59 -9
- package/src/core/unattendedDoctor.js +5 -2
- package/src/core/uninstall.js +37 -8
- package/src/decision/client.js +371 -0
- package/src/decision/lease.js +198 -0
- package/src/decision/preflight.js +492 -0
- package/src/decision/protocol.js +308 -0
- package/src/decision/registry.js +384 -0
- package/src/decision/shadow.js +281 -0
- package/src/decision/statePacket.js +165 -0
- package/src/diagnostics/failurePatterns.js +2 -1
- package/src/diagnostics/ledgerFiles.js +3 -1
- package/src/diagnostics/routeOutcomes.js +1 -29
- package/src/index/buildIndex.js +23 -11
- package/src/index/impactContext.js +21 -2
- package/src/index/importResolution.js +13 -7
- package/src/index/queryIndex.js +11 -5
- package/src/index/resolveContext.js +16 -6
- package/src/index/taskRouting.js +63 -2
- package/src/index/verificationPlan.js +12 -1
- package/src/learning/patternProposals.js +6 -0
- package/src/render/renderTemplate.js +1 -1
- package/src/skill/auditSkill.js +3 -1
- package/src/stack/detectStack.js +3 -1
- package/template_project/.claude/agents/handoff-planner.md +2 -5
- package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
- package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
- package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
- package/template_project/.claude/hooks/handoff-resume.sh +10 -1
- package/template_project/.claude/hooks/protect-files.sh +0 -1
- package/template_project/.claude/hooks/record-execution.mjs +13 -1
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +71 -5
- package/template_project/.claude/hooks/session-episode.sh +9 -2
- package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
- package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
- package/template_project/.claude/ukit/index/lib/index-core.mjs +123 -39
- package/template_project/.claude/ukit/index/route-task.mjs +444 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
- package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
- package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
- package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
- package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
- package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
- package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
- package/template_project/ukit/storage/config.json +34 -1
- package/template_project/.claude/ukit/skill-router-state.json +0 -1
- 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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
const
|
|
17
|
-
const { value:
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
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
|
|
16
|
-
const
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
const summary = await buildCodeIndex({
|
|
62
|
-
rootDir,
|
|
63
|
-
discoverySnapshot: staleness ? staleness.snapshot : null,
|
|
64
|
-
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return argv[exact + 1];
|
|
86
|
-
}
|
|
139
|
+
function extractFlagValue(argv, flag) {
|
|
140
|
+
let value = null;
|
|
141
|
+
const nextArgs = [];
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
5
|
-
const rootDir = process.cwd();
|
|
4
|
+
const USAGE = 'Usage: yarn skill:audit <init|status|record> <skill-name>';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
package/src/bug/triageBug.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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}`;
|
package/src/cli/adapters.js
CHANGED
|
@@ -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
|
}
|
package/src/cli/commands/code.js
CHANGED
|
@@ -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
|
|
22
|
+
value,
|
|
17
23
|
rest: [...args.slice(0, index), ...args.slice(index + 2)],
|
|
18
24
|
};
|
|
19
25
|
}
|