@ngockhoale/ukit 2.3.9 → 2.3.10
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 +62 -0
- package/package.json +1 -1
- package/src/core/fileOps.js +61 -7
- package/src/index/taskRouting.js +35 -2
- package/templates/.claude/hooks/auto-allow-bash.sh +44 -7
- package/templates/.claude/hooks/auto-prune-bash.sh +44 -7
- package/templates/.claude/hooks/context-hardcap-gate.sh +18 -5
- package/templates/.claude/hooks/reset-compact-pressure.sh +47 -7
- package/templates/.claude/hooks/skill-router.sh +80 -7
- package/templates/.claude/hooks/verification-guard.sh +55 -20
- package/templates/.claude/ukit/index/route-task.mjs +36 -3
- package/templates/.claude/ukit/runtime/compact-threshold.mjs +50 -4
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +339 -62
- package/templates/.claude/ukit/runtime/token-utils.mjs +69 -6
- package/templates/.omp/hooks/pre/ukit-bridge.js +68 -16
|
@@ -544,7 +544,7 @@ const { pathToFileURL } = require('url');
|
|
|
544
544
|
routingContext = {},
|
|
545
545
|
previousContextFingerprint = null,
|
|
546
546
|
}) {
|
|
547
|
-
return buildCompactMachineKey('route-
|
|
547
|
+
return buildCompactMachineKey('route-v3', {
|
|
548
548
|
commandNamespace,
|
|
549
549
|
indexGeneratedAtMs,
|
|
550
550
|
projectFingerprint,
|
|
@@ -1577,8 +1577,10 @@ const { pathToFileURL } = require('url');
|
|
|
1577
1577
|
intentMode = null,
|
|
1578
1578
|
executionScores = {},
|
|
1579
1579
|
executionCandidates = null,
|
|
1580
|
+
signalText = '',
|
|
1580
1581
|
} = {}) {
|
|
1581
1582
|
const raw = `${promptText || ''}\n${commandText || ''}`.toLowerCase();
|
|
1583
|
+
const signalRaw = `${raw}\n${String(signalText || '').toLowerCase()}`;
|
|
1582
1584
|
const scores = executionScores;
|
|
1583
1585
|
const candidates = executionCandidates ?? buildExecutionModeCandidates({
|
|
1584
1586
|
promptText,
|
|
@@ -1591,15 +1593,45 @@ const { pathToFileURL } = require('url');
|
|
|
1591
1593
|
const strongImpactLead = scores.sharedRisk
|
|
1592
1594
|
&& (scores.impactSignal || /\b(check all affected|map all affected|across all affected)\b/.test(raw));
|
|
1593
1595
|
const boundedLocalBuildCandidate = scores.buildSignal && scores.boundedEditSignal && scores.explicitTarget && !scores.sharedRisk;
|
|
1596
|
+
const releaseVerificationContinuation = /\b(?:pending\s+diff[- ]review|self-install|verify\s+(?:consistency|release)|release\s+(?:verification|readiness))\b/.test(raw)
|
|
1597
|
+
&& /\b(?:release|tag|publish|push|report)\b/.test(raw);
|
|
1598
|
+
// A bare delivery command ("push this to git", "đẩy bộ này lên git") performs no
|
|
1599
|
+
// repository mutation the ledger could ever receipt. With no edit/review/debug/
|
|
1600
|
+
// build signal present, routing it to an investigation lane fabricates write debt
|
|
1601
|
+
// and the completion gate then demands an edit that cannot exist. Delivery-only
|
|
1602
|
+
// requests carry no completion contract instead.
|
|
1603
|
+
const deliveryWordSignal = /\bgit\s+push\b/.test(signalRaw)
|
|
1604
|
+
|| /\bpush\b[^\n]{0,60}\b(?:git|github|gitlab|remote|origin|repo)\b/.test(signalRaw)
|
|
1605
|
+
|| /\b(?:git|github|gitlab|remote|origin|repo)\b[^\n]{0,60}\bpush\b/.test(signalRaw)
|
|
1606
|
+
|| /\bday\b(?:\s+\S+){0,3}?\s+len\b/.test(signalRaw);
|
|
1607
|
+
const deliveryOnlySignal = deliveryWordSignal
|
|
1608
|
+
&& scores.editCertainty === 0
|
|
1609
|
+
&& !scores.implementSignal
|
|
1610
|
+
&& !scores.reviewSignal
|
|
1611
|
+
&& !scores.debugSignal
|
|
1612
|
+
&& !scores.failureSignal
|
|
1613
|
+
&& !scores.impactSignal
|
|
1614
|
+
&& !scores.buildSignal
|
|
1615
|
+
&& !scores.directTransformSignal
|
|
1616
|
+
&& !scores.smallFixSignal
|
|
1617
|
+
&& !scores.sharedRisk
|
|
1618
|
+
&& !targetFile;
|
|
1594
1619
|
|
|
1595
1620
|
if (isInformationalPrompt({ promptText, commandText, scores })) {
|
|
1596
1621
|
return 'informational';
|
|
1597
1622
|
}
|
|
1598
1623
|
|
|
1599
|
-
if (
|
|
1624
|
+
if (
|
|
1625
|
+
releaseVerificationContinuation
|
|
1626
|
+
|| ((intentMode === 'review-specific' || explicitReviewLead) && !scores.implementSignal)
|
|
1627
|
+
) {
|
|
1600
1628
|
return 'review-release';
|
|
1601
1629
|
}
|
|
1602
1630
|
|
|
1631
|
+
if (deliveryOnlySignal) {
|
|
1632
|
+
return 'informational';
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1603
1635
|
if (strongImpactLead || (scores.blastRadius >= 3 && (scores.ambiguity >= 2 || scores.investigationNeed >= 2))) {
|
|
1604
1636
|
return 'map-impact';
|
|
1605
1637
|
}
|
|
@@ -1699,7 +1731,7 @@ const { pathToFileURL } = require('url');
|
|
|
1699
1731
|
),
|
|
1700
1732
|
'map-impact': (
|
|
1701
1733
|
(scores.sharedRisk ? 7 : 0)
|
|
1702
|
-
+ (scores.impactSignal ? 5 :
|
|
1734
|
+
+ (scores.impactSignal ? 5 : -5)
|
|
1703
1735
|
+ (scores.ambiguity * 2)
|
|
1704
1736
|
+ scores.investigationNeed
|
|
1705
1737
|
),
|
|
@@ -2445,6 +2477,39 @@ const { pathToFileURL } = require('url');
|
|
|
2445
2477
|
const debounceMs = 10 * 60 * 1000;
|
|
2446
2478
|
const previousTs = Number(previous?.ts || 0);
|
|
2447
2479
|
const informationalNoTargetPrompt = isInformationalPrompt({ promptText, commandText }) && !filePath;
|
|
2480
|
+
// A delivery-only prompt ("push this to git", "đẩy bộ này lên git") starts a new,
|
|
2481
|
+
// mutation-free action and typically matches no skill. It must not inherit the
|
|
2482
|
+
// previous request's completion contract — carrying find-cause/map-impact debt
|
|
2483
|
+
// forward here made the Stop gate demand an edit that a push can never produce.
|
|
2484
|
+
const noRouteScores = deriveExecutionScores({
|
|
2485
|
+
promptText,
|
|
2486
|
+
commandText,
|
|
2487
|
+
targetFile: filePath,
|
|
2488
|
+
intentMode,
|
|
2489
|
+
taskType: '',
|
|
2490
|
+
});
|
|
2491
|
+
const noRouteSignalRaw = [
|
|
2492
|
+
routeSignals.promptRawText || '',
|
|
2493
|
+
routeSignals.promptNormalizedText || '',
|
|
2494
|
+
routeSignals.commandRawText || '',
|
|
2495
|
+
routeSignals.commandNormalizedText || '',
|
|
2496
|
+
].join('\n');
|
|
2497
|
+
const deliveryWordSignal = /\bgit\s+push\b/.test(noRouteSignalRaw)
|
|
2498
|
+
|| /\bpush\b[^\n]{0,60}\b(?:git|github|gitlab|remote|origin|repo)\b/.test(noRouteSignalRaw)
|
|
2499
|
+
|| /\b(?:git|github|gitlab|remote|origin|repo)\b[^\n]{0,60}\bpush\b/.test(noRouteSignalRaw)
|
|
2500
|
+
|| /\bday\b(?:\s+\S+){0,3}?\s+len\b/.test(noRouteSignalRaw);
|
|
2501
|
+
const deliveryOnlyNoRoute = deliveryWordSignal
|
|
2502
|
+
&& noRouteScores.editCertainty === 0
|
|
2503
|
+
&& !noRouteScores.implementSignal
|
|
2504
|
+
&& !noRouteScores.reviewSignal
|
|
2505
|
+
&& !noRouteScores.debugSignal
|
|
2506
|
+
&& !noRouteScores.failureSignal
|
|
2507
|
+
&& !noRouteScores.impactSignal
|
|
2508
|
+
&& !noRouteScores.buildSignal
|
|
2509
|
+
&& !noRouteScores.directTransformSignal
|
|
2510
|
+
&& !noRouteScores.smallFixSignal
|
|
2511
|
+
&& !noRouteScores.sharedRisk
|
|
2512
|
+
&& !filePath;
|
|
2448
2513
|
|
|
2449
2514
|
if (active.length === 0 || informationalNoTargetPrompt) {
|
|
2450
2515
|
const lastExplicitUserPromptText = promptText.trim()
|
|
@@ -2457,7 +2522,7 @@ const { pathToFileURL } = require('url');
|
|
|
2457
2522
|
targetFile: filePath,
|
|
2458
2523
|
contextIntent: '',
|
|
2459
2524
|
taskType: '',
|
|
2460
|
-
...(informationalNoTargetPrompt ? { executionMode: 'informational' } : {}),
|
|
2525
|
+
...((informationalNoTargetPrompt || deliveryOnlyNoRoute) ? { executionMode: 'informational' } : {}),
|
|
2461
2526
|
};
|
|
2462
2527
|
const previousContext = buildPreviousContextSnapshot({
|
|
2463
2528
|
projectRoot,
|
|
@@ -2494,8 +2559,8 @@ const { pathToFileURL } = require('url');
|
|
|
2494
2559
|
activeSkills: [],
|
|
2495
2560
|
routingContext: compactRoutingContext(routingContext),
|
|
2496
2561
|
...(previousContext ? { previousContext: compactPreviousContext(previousContext) } : {}),
|
|
2497
|
-
...(previous?.routeSummary ? { routeSummary: previous.routeSummary } : {}),
|
|
2498
|
-
...(previous?.requestKey ? { requestKey: previous.requestKey } : {}),
|
|
2562
|
+
...(previous?.routeSummary && !deliveryOnlyNoRoute ? { routeSummary: previous.routeSummary } : {}),
|
|
2563
|
+
...(previous?.requestKey && !deliveryOnlyNoRoute ? { requestKey: previous.requestKey } : {}),
|
|
2499
2564
|
}));
|
|
2500
2565
|
process.exit(0);
|
|
2501
2566
|
}
|
|
@@ -2536,6 +2601,7 @@ const { pathToFileURL } = require('url');
|
|
|
2536
2601
|
intentMode,
|
|
2537
2602
|
executionScores,
|
|
2538
2603
|
executionCandidates,
|
|
2604
|
+
signalText: `${routeSignals.promptNormalizedText || ''}\n${routeSignals.commandNormalizedText || ''}`,
|
|
2539
2605
|
});
|
|
2540
2606
|
const lastExplicitUserPromptText = promptText.trim()
|
|
2541
2607
|
|| previous?.routingContext?.lastExplicitUserPromptText
|
|
@@ -2792,7 +2858,14 @@ const { pathToFileURL } = require('url');
|
|
|
2792
2858
|
NODE
|
|
2793
2859
|
|
|
2794
2860
|
if [ -f "$THRESHOLD_SCRIPT" ]; then
|
|
2795
|
-
|
|
2861
|
+
# The 'prompt' phase only bookkeeps prompt-bearing events — a tool payload has no
|
|
2862
|
+
# prompt text and that phase exits before doing anything. Every PreToolUse/
|
|
2863
|
+
# PostToolUse call used to pay a second full node spawn for that no-op, doubling the
|
|
2864
|
+
# per-mutation hook latency. Tool payloads always carry tool_name; skip them.
|
|
2865
|
+
case "$INPUT" in
|
|
2866
|
+
*'"tool_name"'*) ;;
|
|
2867
|
+
*) printf '%s' "$INPUT" | CLAUDE_PROJECT_DIR="$PROJECT_ROOT" node "$THRESHOLD_SCRIPT" prompt 2>/dev/null || true ;;
|
|
2868
|
+
esac
|
|
2796
2869
|
fi
|
|
2797
2870
|
|
|
2798
2871
|
exit 0
|
|
@@ -9,6 +9,7 @@ ROUTE_CACHE_FILE="$PROJECT_ROOT/.claude/ukit/route-cache.json"
|
|
|
9
9
|
PROGRESS_FILE="$PROJECT_ROOT/.claude/ukit/verification-progress.json"
|
|
10
10
|
|
|
11
11
|
INPUT="$INPUT" STATE_FILE="$STATE_FILE" ROUTE_CACHE_FILE="$ROUTE_CACHE_FILE" PROGRESS_FILE="$PROGRESS_FILE" node <<'NODE'
|
|
12
|
+
const crypto = require('crypto');
|
|
12
13
|
const fs = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
|
|
@@ -41,6 +42,23 @@ function sleepSync(ms) {
|
|
|
41
42
|
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function isPidAlive(pid) {
|
|
46
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return error?.code === 'EPERM';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readLockOwner(lockPath) {
|
|
56
|
+
const owner = readJson(path.join(lockPath, 'owner'), null);
|
|
57
|
+
return owner && Number.isInteger(owner.pid) && typeof owner.token === 'string'
|
|
58
|
+
? owner
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
// mkdir-based lock, protocol-compatible with runtime token-utils withFileLock. Sync on
|
|
45
63
|
// purpose (see sleepSync). Fail-open after 5s: this hook must never hang a PreToolUse
|
|
46
64
|
// chain, and a lost progress entry only costs one repeated advisory.
|
|
@@ -48,21 +66,37 @@ function withLockSync(lockPath, fn) {
|
|
|
48
66
|
const staleMs = 10000;
|
|
49
67
|
const maxWaitMs = 5000;
|
|
50
68
|
const startedAt = Date.now();
|
|
69
|
+
const ownerToken = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
51
70
|
let held = false;
|
|
52
71
|
while (!held) {
|
|
53
72
|
try {
|
|
54
73
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
55
74
|
fs.mkdirSync(lockPath);
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(path.join(lockPath, 'owner'), JSON.stringify({
|
|
77
|
+
pid: process.pid,
|
|
78
|
+
token: ownerToken,
|
|
79
|
+
ts: Date.now(),
|
|
80
|
+
}));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
try {
|
|
83
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
84
|
+
} catch {}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
56
87
|
held = true;
|
|
57
88
|
} catch (error) {
|
|
58
89
|
if (!error || error.code !== 'EEXIST') return fn();
|
|
59
90
|
try {
|
|
60
91
|
const stat = fs.statSync(lockPath);
|
|
61
92
|
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
const owner = readLockOwner(lockPath);
|
|
94
|
+
if (!owner || !isPidAlive(owner.pid)) {
|
|
95
|
+
try {
|
|
96
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
97
|
+
continue;
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
66
100
|
}
|
|
67
101
|
} catch {}
|
|
68
102
|
if (Date.now() - startedAt > maxWaitMs) break;
|
|
@@ -73,9 +107,12 @@ function withLockSync(lockPath, fn) {
|
|
|
73
107
|
return fn();
|
|
74
108
|
} finally {
|
|
75
109
|
if (held) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
const owner = readLockOwner(lockPath);
|
|
111
|
+
if (owner?.token === ownerToken) {
|
|
112
|
+
try {
|
|
113
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
79
116
|
}
|
|
80
117
|
}
|
|
81
118
|
}
|
|
@@ -288,14 +325,12 @@ function hasCompletedRecentTargetedLane({
|
|
|
288
325
|
});
|
|
289
326
|
}
|
|
290
327
|
|
|
291
|
-
function
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
fs.writeSync(1, `${advisory}\n`);
|
|
328
|
+
function advise(message) {
|
|
329
|
+
// This hook is intentionally advisory. The executable hard gates are the completion
|
|
330
|
+
// ledger, context hard-cap, protected-file, and destructive-command guards; turning
|
|
331
|
+
// a verification-order recommendation into an exit-2 refusal gave a stale opt-in env
|
|
332
|
+
// variable a way to freeze a run before it could diagnose or land its current change.
|
|
333
|
+
fs.writeSync(1, `${message.replace(/^BLOCKED:/, 'ADVISORY:')}\n`);
|
|
299
334
|
process.exit(0);
|
|
300
335
|
}
|
|
301
336
|
|
|
@@ -433,31 +468,31 @@ if (isTargetedVerification) {
|
|
|
433
468
|
}
|
|
434
469
|
|
|
435
470
|
if (policyMode === 'confirm-then-broad' && isBroadTestCommand(command) && !explicitBroadRequested && !recentTargetedLaneCompleted) {
|
|
436
|
-
|
|
471
|
+
advise(
|
|
437
472
|
`BLOCKED: Ask the user before broad verification. Routed policy is confirm-then-broad because the index could not localize related tests confidently.${helperCommand}`,
|
|
438
473
|
);
|
|
439
474
|
}
|
|
440
475
|
|
|
441
476
|
if ((policyMode === 'auto-run-targeted' || policyMode === 'auto-run-targeted-then-fallback') && !isPrimaryCommand && missingPrimary.length > 0) {
|
|
442
|
-
|
|
477
|
+
advise(
|
|
443
478
|
`BLOCKED: Run targeted verification first. Routed policy is ${policyMode} and the preferred first command(s) are: ${missingPrimaryText}.${helperCommand}`,
|
|
444
479
|
);
|
|
445
480
|
}
|
|
446
481
|
|
|
447
482
|
if (policyMode === 'auto-run-targeted' && !explicitBroadRequested && !isPrimaryCommand) {
|
|
448
|
-
|
|
483
|
+
advise(
|
|
449
484
|
`BLOCKED: Broader verification is not the default for this routed context. Stay with targeted verification unless the user explicitly requests a full suite. Preferred order: ${preferredText}.${helperCommand}`,
|
|
450
485
|
);
|
|
451
486
|
}
|
|
452
487
|
|
|
453
488
|
if (policyMode === 'auto-run-targeted-then-fallback' && !explicitBroadRequested && isVerificationCommand(command) && !isPrimaryCommand && !isFallbackCommand) {
|
|
454
|
-
|
|
489
|
+
advise(
|
|
455
490
|
`BLOCKED: Follow the routed verification order. This command is outside the recommended fallback lane. Preferred order: ${preferredText}.${helperCommand}`,
|
|
456
491
|
);
|
|
457
492
|
}
|
|
458
493
|
|
|
459
494
|
if (policyMode === 'docs-light' && !explicitBroadRequested && isBroadTestCommand(command)) {
|
|
460
|
-
|
|
495
|
+
advise(
|
|
461
496
|
`BLOCKED: Docs-focused change detected. Broad test runs are not the default for docs-light verification. Ask first if a full suite is still required.${helperCommand}`,
|
|
462
497
|
);
|
|
463
498
|
}
|
|
@@ -450,6 +450,7 @@ async function prepareTaskRoute({
|
|
|
450
450
|
intentMode,
|
|
451
451
|
executionScores,
|
|
452
452
|
executionCandidates,
|
|
453
|
+
signalText: buildNormalizedRouteSignalText(normalizedPrompt, normalizedCommand),
|
|
453
454
|
});
|
|
454
455
|
const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
|
|
455
456
|
const visionInfo = await resolveVisionLane({
|
|
@@ -1749,8 +1750,10 @@ function deriveExecutionMode({
|
|
|
1749
1750
|
intentMode = null,
|
|
1750
1751
|
executionScores = {},
|
|
1751
1752
|
executionCandidates = null,
|
|
1753
|
+
signalText = '',
|
|
1752
1754
|
} = {}) {
|
|
1753
1755
|
const raw = `${promptText ?? ''}\n${commandText ?? ''}`.toLowerCase();
|
|
1756
|
+
const signalRaw = `${raw}\n${String(signalText || '').toLowerCase()}`;
|
|
1754
1757
|
const scores = executionScores;
|
|
1755
1758
|
const candidates = executionCandidates ?? buildExecutionModeCandidates({
|
|
1756
1759
|
promptText,
|
|
@@ -1760,18 +1763,48 @@ function deriveExecutionMode({
|
|
|
1760
1763
|
executionScores,
|
|
1761
1764
|
});
|
|
1762
1765
|
const explicitReviewLead = /\b(review this|audit this|review\b.*\brelease readiness|verify\b.*\brelease readiness)\b/.test(raw);
|
|
1766
|
+
const releaseVerificationContinuation = /\b(?:pending\s+diff[- ]review|self-install|verify\s+(?:consistency|release)|release\s+(?:verification|readiness))\b/.test(raw)
|
|
1767
|
+
&& /\b(?:release|tag|publish|push|report)\b/.test(raw);
|
|
1763
1768
|
const strongImpactLead = scores.sharedRisk
|
|
1764
1769
|
&& (scores.impactSignal || /\b(check all affected|map all affected|across all affected)\b/.test(raw));
|
|
1765
1770
|
const boundedLocalBuildCandidate = scores.buildSignal && scores.boundedEditSignal && scores.explicitTarget && !scores.sharedRisk;
|
|
1771
|
+
// A bare delivery command ("push this to git", "đẩy bộ này lên git") performs no
|
|
1772
|
+
// repository mutation the ledger could ever receipt. With no edit/review/debug/
|
|
1773
|
+
// build signal present, routing it to an investigation lane fabricates write debt
|
|
1774
|
+
// and the completion gate then demands an edit that cannot exist. Delivery-only
|
|
1775
|
+
// requests carry no completion contract instead.
|
|
1776
|
+
const deliveryWordSignal = /\bgit\s+push\b/.test(signalRaw)
|
|
1777
|
+
|| /\bpush\b[^\n]{0,60}\b(?:git|github|gitlab|remote|origin|repo)\b/.test(signalRaw)
|
|
1778
|
+
|| /\b(?:git|github|gitlab|remote|origin|repo)\b[^\n]{0,60}\bpush\b/.test(signalRaw)
|
|
1779
|
+
|| /\bday\b(?:\s+\S+){0,3}?\s+len\b/.test(signalRaw);
|
|
1780
|
+
const deliveryOnlySignal = deliveryWordSignal
|
|
1781
|
+
&& scores.editCertainty === 0
|
|
1782
|
+
&& !scores.implementSignal
|
|
1783
|
+
&& !scores.reviewSignal
|
|
1784
|
+
&& !scores.debugSignal
|
|
1785
|
+
&& !scores.failureSignal
|
|
1786
|
+
&& !scores.impactSignal
|
|
1787
|
+
&& !scores.buildSignal
|
|
1788
|
+
&& !scores.directTransformSignal
|
|
1789
|
+
&& !scores.smallFixSignal
|
|
1790
|
+
&& !scores.sharedRisk
|
|
1791
|
+
&& !targetFile;
|
|
1766
1792
|
|
|
1767
1793
|
if (isInformationalPrompt({ promptText, commandText, scores })) {
|
|
1768
1794
|
return 'informational';
|
|
1769
1795
|
}
|
|
1770
1796
|
|
|
1771
|
-
if (
|
|
1797
|
+
if (
|
|
1798
|
+
releaseVerificationContinuation
|
|
1799
|
+
|| ((intentMode === 'review-specific' || explicitReviewLead) && !scores.implementSignal)
|
|
1800
|
+
) {
|
|
1772
1801
|
return 'review-release';
|
|
1773
1802
|
}
|
|
1774
1803
|
|
|
1804
|
+
if (deliveryOnlySignal) {
|
|
1805
|
+
return 'informational';
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1775
1808
|
if (strongImpactLead || (scores.blastRadius >= 3 && (scores.ambiguity >= 2 || scores.investigationNeed >= 2))) {
|
|
1776
1809
|
return 'map-impact';
|
|
1777
1810
|
}
|
|
@@ -1871,7 +1904,7 @@ function buildExecutionModeCandidates({
|
|
|
1871
1904
|
),
|
|
1872
1905
|
'map-impact': (
|
|
1873
1906
|
(scores.sharedRisk ? 7 : 0)
|
|
1874
|
-
+ (scores.impactSignal ? 5 :
|
|
1907
|
+
+ (scores.impactSignal ? 5 : -5)
|
|
1875
1908
|
+ (scores.ambiguity * 2)
|
|
1876
1909
|
+ scores.investigationNeed
|
|
1877
1910
|
),
|
|
@@ -2784,7 +2817,7 @@ function buildRouteRequestKey({
|
|
|
2784
2817
|
previousContextSnapshot = null,
|
|
2785
2818
|
recentOutputSnapshot = null,
|
|
2786
2819
|
} = {}) {
|
|
2787
|
-
return buildCompactMachineKey('route-
|
|
2820
|
+
return buildCompactMachineKey('route-v3', {
|
|
2788
2821
|
commandNamespace,
|
|
2789
2822
|
indexGeneratedAtMs,
|
|
2790
2823
|
projectFingerprint,
|
|
@@ -499,6 +499,21 @@ export function resetCompactPressureState(rawState = {}, config = {}) {
|
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
const BOUNDARY_TAIL_BYTES = 4 * 1024 * 1024;
|
|
502
|
+
// Hook processes are short-lived, so an in-memory cache would not stop every Edit/Write
|
|
503
|
+
// from reopening and parsing a 4MB transcript tail. Persist a very short probe window in
|
|
504
|
+
// the session pressure record instead: a burst of mutations incurs one scan, not one per
|
|
505
|
+
// call. PreCompact is still immediate; this is only the fallback for host auto-compactions
|
|
506
|
+
// that skip PreCompact, where a two-second detection delay is preferable to killing the
|
|
507
|
+
// 4s hook budget on a slow volume.
|
|
508
|
+
const TRANSCRIPT_BOUNDARY_PROBE_TTL_MS = 2_000;
|
|
509
|
+
|
|
510
|
+
function normalizeTranscriptProbe(value) {
|
|
511
|
+
if (!value || typeof value !== 'object' || typeof value.path !== 'string' || !value.path) return null;
|
|
512
|
+
const checkedAt = finiteNumber(value.checkedAt, 0);
|
|
513
|
+
return checkedAt > 0
|
|
514
|
+
? { path: value.path, checkedAt, boundaryAt: finiteNumber(value.boundaryAt, 0) }
|
|
515
|
+
: null;
|
|
516
|
+
}
|
|
502
517
|
|
|
503
518
|
// Find the newest compaction recorded in the session transcript, as epoch ms (0 if none).
|
|
504
519
|
// Every real compaction — manual /compact AND harness auto-compact — appends a
|
|
@@ -551,18 +566,46 @@ export async function detectLatestCompactBoundaryAt(transcriptPath, { tailBytes
|
|
|
551
566
|
// history that was already summarised away and the hard-cap gate bricks a healthy
|
|
552
567
|
// session. Callers must persist when reset=true, or the reset would re-fire (and
|
|
553
568
|
// zero the counter) on every call.
|
|
554
|
-
export async function syncCompactPressureStateWithTranscript(
|
|
555
|
-
|
|
569
|
+
export async function syncCompactPressureStateWithTranscript(
|
|
570
|
+
rawState = null,
|
|
571
|
+
config = {},
|
|
572
|
+
transcriptPath = '',
|
|
573
|
+
{ now = Date.now(), probeTtlMs = TRANSCRIPT_BOUNDARY_PROBE_TTL_MS } = {},
|
|
574
|
+
) {
|
|
575
|
+
const normalizedPath = typeof transcriptPath === 'string' ? transcriptPath.trim() : '';
|
|
576
|
+
const previousProbe = normalizeTranscriptProbe(rawState?.transcriptBoundaryProbe);
|
|
577
|
+
// A process-persistent, session-scoped cache avoids rereading/parsing up to 4MB for every
|
|
578
|
+
// mutation during a tool burst. It keys on the path (not just time) so a resumed/new
|
|
579
|
+
// transcript always scans immediately. The short TTL is deliberately only a performance
|
|
580
|
+
// coalescing window; PreCompact remains the immediate source of truth.
|
|
581
|
+
const probeFresh = previousProbe
|
|
582
|
+
&& previousProbe.path === normalizedPath
|
|
583
|
+
&& now - previousProbe.checkedAt >= 0
|
|
584
|
+
&& now - previousProbe.checkedAt < probeTtlMs;
|
|
585
|
+
const boundaryAt = probeFresh
|
|
586
|
+
? previousProbe.boundaryAt
|
|
587
|
+
: await detectLatestCompactBoundaryAt(normalizedPath);
|
|
588
|
+
const probe = probeFresh
|
|
589
|
+
? previousProbe
|
|
590
|
+
: { path: normalizedPath, checkedAt: now, boundaryAt };
|
|
556
591
|
const accounted = finiteNumber(rawState?.lastCompactBoundaryAt, 0);
|
|
557
592
|
if (!boundaryAt || boundaryAt <= accounted) {
|
|
558
|
-
return {
|
|
593
|
+
return {
|
|
594
|
+
state: rawState && typeof rawState === 'object'
|
|
595
|
+
? { ...rawState, transcriptBoundaryProbe: probe }
|
|
596
|
+
: rawState,
|
|
597
|
+
reset: false,
|
|
598
|
+
probed: !probeFresh,
|
|
599
|
+
};
|
|
559
600
|
}
|
|
560
601
|
return {
|
|
561
602
|
state: {
|
|
562
603
|
...resetCompactPressureState(rawState ?? {}, config),
|
|
563
604
|
lastCompactBoundaryAt: boundaryAt,
|
|
605
|
+
transcriptBoundaryProbe: probe,
|
|
564
606
|
},
|
|
565
607
|
reset: true,
|
|
608
|
+
probed: !probeFresh,
|
|
566
609
|
};
|
|
567
610
|
}
|
|
568
611
|
|
|
@@ -795,6 +838,7 @@ export function buildCompactPressureState(rawState = null, config = {}) {
|
|
|
795
838
|
taskMode,
|
|
796
839
|
cooldownUntil: finiteNumber(rawState?.cooldownUntil, 0),
|
|
797
840
|
lastCompactBoundaryAt: finiteNumber(rawState?.lastCompactBoundaryAt, 0),
|
|
841
|
+
transcriptBoundaryProbe: normalizeTranscriptProbe(rawState?.transcriptBoundaryProbe),
|
|
798
842
|
routingContext: rawState?.routingContext && typeof rawState.routingContext === 'object'
|
|
799
843
|
? rawState.routingContext
|
|
800
844
|
: {},
|
|
@@ -1260,7 +1304,9 @@ async function runCli() {
|
|
|
1260
1304
|
sessionConfig,
|
|
1261
1305
|
payload.transcript_path,
|
|
1262
1306
|
);
|
|
1263
|
-
if (synced.reset) {
|
|
1307
|
+
if (synced.reset || synced.probed) {
|
|
1308
|
+
// Persist a clean probe too: this CLI runs in a fresh process on each hook,
|
|
1309
|
+
// so retaining only resets would make every prompt rescan the same tail.
|
|
1264
1310
|
await writeCompactPressureState(projectRoot, synced.state, sessionConfig);
|
|
1265
1311
|
}
|
|
1266
1312
|
} catch { /* advisory healing only — never block the prompt over it */ }
|