evil-omo 3.12.0 → 3.12.2
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/dist/cli/index.js +174 -91
- package/dist/config/schema/background-task.d.ts +2 -2
- package/dist/config/schema/evil-omo-config.d.ts +2 -2
- package/dist/config/schema/hooks.d.ts +1 -0
- package/dist/create-hooks.d.ts +1 -0
- package/dist/evil-omo.schema.json +4 -6
- package/dist/features/background-agent/constants.d.ts +2 -2
- package/dist/features/background-agent/loop-detector.d.ts +4 -5
- package/dist/features/background-agent/manager.d.ts +1 -0
- package/dist/features/background-agent/session-status-classifier.d.ts +2 -0
- package/dist/features/background-agent/types.d.ts +4 -4
- package/dist/features/builtin-commands/templates/start-work.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/todo-description-override/description.d.ts +1 -0
- package/dist/hooks/todo-description-override/hook.d.ts +8 -0
- package/dist/hooks/todo-description-override/index.d.ts +1 -0
- package/dist/index.js +403 -229
- package/dist/plugin/hooks/create-core-hooks.d.ts +1 -0
- package/dist/plugin/hooks/create-tool-guard-hooks.d.ts +2 -1
- package/dist/shared/connected-providers-cache.d.ts +26 -29
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -16190,7 +16190,7 @@ async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0,
|
|
|
16190
16190
|
}
|
|
16191
16191
|
let resolved = text;
|
|
16192
16192
|
for (const [pattern, replacement] of replacements.entries()) {
|
|
16193
|
-
resolved = resolved.
|
|
16193
|
+
resolved = resolved.replaceAll(pattern, replacement);
|
|
16194
16194
|
}
|
|
16195
16195
|
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
|
16196
16196
|
return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
|
|
@@ -16289,6 +16289,7 @@ function transformToolName(toolName) {
|
|
|
16289
16289
|
function escapeRegexExceptAsterisk(str2) {
|
|
16290
16290
|
return str2.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
16291
16291
|
}
|
|
16292
|
+
var regexCache = new Map;
|
|
16292
16293
|
function matchesToolMatcher(toolName, matcher) {
|
|
16293
16294
|
if (!matcher) {
|
|
16294
16295
|
return true;
|
|
@@ -16296,8 +16297,12 @@ function matchesToolMatcher(toolName, matcher) {
|
|
|
16296
16297
|
const patterns = matcher.split("|").map((p) => p.trim());
|
|
16297
16298
|
return patterns.some((p) => {
|
|
16298
16299
|
if (p.includes("*")) {
|
|
16299
|
-
|
|
16300
|
-
|
|
16300
|
+
let regex = regexCache.get(p);
|
|
16301
|
+
if (!regex) {
|
|
16302
|
+
const escaped = escapeRegexExceptAsterisk(p);
|
|
16303
|
+
regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`, "i");
|
|
16304
|
+
regexCache.set(p, regex);
|
|
16305
|
+
}
|
|
16301
16306
|
return regex.test(toolName);
|
|
16302
16307
|
}
|
|
16303
16308
|
return p.toLowerCase() === toolName.toLowerCase();
|
|
@@ -17653,120 +17658,156 @@ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync
|
|
|
17653
17658
|
import { join as join10 } from "path";
|
|
17654
17659
|
var CONNECTED_PROVIDERS_CACHE_FILE = "connected-providers.json";
|
|
17655
17660
|
var PROVIDER_MODELS_CACHE_FILE = "provider-models.json";
|
|
17656
|
-
function
|
|
17657
|
-
|
|
17658
|
-
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
|
|
17669
|
-
|
|
17661
|
+
function createConnectedProvidersCacheStore(getCacheDir2 = getOmoOpenCodeCacheDir) {
|
|
17662
|
+
function getCacheFilePath(filename) {
|
|
17663
|
+
return join10(getCacheDir2(), filename);
|
|
17664
|
+
}
|
|
17665
|
+
let memConnected;
|
|
17666
|
+
let memProviderModels;
|
|
17667
|
+
function ensureCacheDir2() {
|
|
17668
|
+
const cacheDir = getCacheDir2();
|
|
17669
|
+
if (!existsSync8(cacheDir)) {
|
|
17670
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
17671
|
+
}
|
|
17672
|
+
}
|
|
17673
|
+
function readConnectedProvidersCache() {
|
|
17674
|
+
if (memConnected !== undefined)
|
|
17675
|
+
return memConnected;
|
|
17676
|
+
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
|
|
17677
|
+
if (!existsSync8(cacheFile)) {
|
|
17678
|
+
log("[connected-providers-cache] Cache file not found", { cacheFile });
|
|
17679
|
+
memConnected = null;
|
|
17680
|
+
return null;
|
|
17681
|
+
}
|
|
17682
|
+
try {
|
|
17683
|
+
const content = readFileSync4(cacheFile, "utf-8");
|
|
17684
|
+
const data = JSON.parse(content);
|
|
17685
|
+
log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
|
|
17686
|
+
memConnected = data.connected;
|
|
17687
|
+
return data.connected;
|
|
17688
|
+
} catch (err) {
|
|
17689
|
+
log("[connected-providers-cache] Error reading cache", { error: String(err) });
|
|
17690
|
+
memConnected = null;
|
|
17691
|
+
return null;
|
|
17692
|
+
}
|
|
17670
17693
|
}
|
|
17671
|
-
|
|
17672
|
-
const
|
|
17673
|
-
|
|
17674
|
-
log("[connected-providers-cache] Read cache", { count: data.connected.length, updatedAt: data.updatedAt });
|
|
17675
|
-
return data.connected;
|
|
17676
|
-
} catch (err) {
|
|
17677
|
-
log("[connected-providers-cache] Error reading cache", { error: String(err) });
|
|
17678
|
-
return null;
|
|
17694
|
+
function hasConnectedProvidersCache() {
|
|
17695
|
+
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
|
|
17696
|
+
return existsSync8(cacheFile);
|
|
17679
17697
|
}
|
|
17680
|
-
|
|
17681
|
-
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
log("[connected-providers-cache] Cache written", { count: connected.length });
|
|
17695
|
-
} catch (err) {
|
|
17696
|
-
log("[connected-providers-cache] Error writing cache", { error: String(err) });
|
|
17698
|
+
function writeConnectedProvidersCache(connected) {
|
|
17699
|
+
ensureCacheDir2();
|
|
17700
|
+
const cacheFile = getCacheFilePath(CONNECTED_PROVIDERS_CACHE_FILE);
|
|
17701
|
+
const data = {
|
|
17702
|
+
connected,
|
|
17703
|
+
updatedAt: new Date().toISOString()
|
|
17704
|
+
};
|
|
17705
|
+
try {
|
|
17706
|
+
writeFileSync2(cacheFile, JSON.stringify(data, null, 2));
|
|
17707
|
+
memConnected = connected;
|
|
17708
|
+
log("[connected-providers-cache] Cache written", { count: connected.length });
|
|
17709
|
+
} catch (err) {
|
|
17710
|
+
log("[connected-providers-cache] Error writing cache", { error: String(err) });
|
|
17711
|
+
}
|
|
17697
17712
|
}
|
|
17698
|
-
|
|
17699
|
-
|
|
17700
|
-
|
|
17701
|
-
|
|
17702
|
-
|
|
17703
|
-
|
|
17713
|
+
function readProviderModelsCache() {
|
|
17714
|
+
if (memProviderModels !== undefined)
|
|
17715
|
+
return memProviderModels;
|
|
17716
|
+
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
|
|
17717
|
+
if (!existsSync8(cacheFile)) {
|
|
17718
|
+
log("[connected-providers-cache] Provider-models cache file not found", { cacheFile });
|
|
17719
|
+
memProviderModels = null;
|
|
17720
|
+
return null;
|
|
17721
|
+
}
|
|
17722
|
+
try {
|
|
17723
|
+
const content = readFileSync4(cacheFile, "utf-8");
|
|
17724
|
+
const data = JSON.parse(content);
|
|
17725
|
+
log("[connected-providers-cache] Read provider-models cache", {
|
|
17726
|
+
providerCount: Object.keys(data.models).length,
|
|
17727
|
+
updatedAt: data.updatedAt
|
|
17728
|
+
});
|
|
17729
|
+
memProviderModels = data;
|
|
17730
|
+
return data;
|
|
17731
|
+
} catch (err) {
|
|
17732
|
+
log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
|
|
17733
|
+
memProviderModels = null;
|
|
17734
|
+
return null;
|
|
17735
|
+
}
|
|
17704
17736
|
}
|
|
17705
|
-
|
|
17706
|
-
const
|
|
17707
|
-
|
|
17708
|
-
log("[connected-providers-cache] Read provider-models cache", {
|
|
17709
|
-
providerCount: Object.keys(data.models).length,
|
|
17710
|
-
updatedAt: data.updatedAt
|
|
17711
|
-
});
|
|
17712
|
-
return data;
|
|
17713
|
-
} catch (err) {
|
|
17714
|
-
log("[connected-providers-cache] Error reading provider-models cache", { error: String(err) });
|
|
17715
|
-
return null;
|
|
17737
|
+
function hasProviderModelsCache() {
|
|
17738
|
+
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
|
|
17739
|
+
return existsSync8(cacheFile);
|
|
17716
17740
|
}
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17732
|
-
|
|
17733
|
-
});
|
|
17734
|
-
} catch (err) {
|
|
17735
|
-
log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
|
|
17736
|
-
}
|
|
17737
|
-
}
|
|
17738
|
-
async function updateConnectedProvidersCache(client) {
|
|
17739
|
-
if (!client?.provider?.list) {
|
|
17740
|
-
log("[connected-providers-cache] client.provider.list not available");
|
|
17741
|
-
return;
|
|
17741
|
+
function writeProviderModelsCache(data) {
|
|
17742
|
+
ensureCacheDir2();
|
|
17743
|
+
const cacheFile = getCacheFilePath(PROVIDER_MODELS_CACHE_FILE);
|
|
17744
|
+
const cacheData = {
|
|
17745
|
+
...data,
|
|
17746
|
+
updatedAt: new Date().toISOString()
|
|
17747
|
+
};
|
|
17748
|
+
try {
|
|
17749
|
+
writeFileSync2(cacheFile, JSON.stringify(cacheData, null, 2));
|
|
17750
|
+
memProviderModels = cacheData;
|
|
17751
|
+
log("[connected-providers-cache] Provider-models cache written", {
|
|
17752
|
+
providerCount: Object.keys(data.models).length
|
|
17753
|
+
});
|
|
17754
|
+
} catch (err) {
|
|
17755
|
+
log("[connected-providers-cache] Error writing provider-models cache", { error: String(err) });
|
|
17756
|
+
}
|
|
17742
17757
|
}
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17758
|
+
async function updateConnectedProvidersCache(client) {
|
|
17759
|
+
if (!client?.provider?.list) {
|
|
17760
|
+
log("[connected-providers-cache] client.provider.list not available");
|
|
17761
|
+
return;
|
|
17762
|
+
}
|
|
17763
|
+
try {
|
|
17764
|
+
const result = await client.provider.list();
|
|
17765
|
+
const connected = result.data?.connected ?? [];
|
|
17766
|
+
log("[connected-providers-cache] Fetched connected providers", {
|
|
17767
|
+
count: connected.length,
|
|
17768
|
+
providers: connected
|
|
17769
|
+
});
|
|
17770
|
+
writeConnectedProvidersCache(connected);
|
|
17771
|
+
const modelsByProvider = {};
|
|
17772
|
+
const allProviders = result.data?.all ?? [];
|
|
17773
|
+
for (const provider of allProviders) {
|
|
17774
|
+
if (provider.models) {
|
|
17775
|
+
const modelIds = Object.keys(provider.models);
|
|
17776
|
+
if (modelIds.length > 0) {
|
|
17777
|
+
modelsByProvider[provider.id] = modelIds;
|
|
17778
|
+
}
|
|
17755
17779
|
}
|
|
17756
17780
|
}
|
|
17781
|
+
log("[connected-providers-cache] Extracted models from provider list", {
|
|
17782
|
+
providerCount: Object.keys(modelsByProvider).length,
|
|
17783
|
+
totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
|
|
17784
|
+
});
|
|
17785
|
+
writeProviderModelsCache({
|
|
17786
|
+
models: modelsByProvider,
|
|
17787
|
+
connected
|
|
17788
|
+
});
|
|
17789
|
+
} catch (err) {
|
|
17790
|
+
log("[connected-providers-cache] Error updating cache", { error: String(err) });
|
|
17757
17791
|
}
|
|
17758
|
-
log("[connected-providers-cache] Extracted models from provider list", {
|
|
17759
|
-
providerCount: Object.keys(modelsByProvider).length,
|
|
17760
|
-
totalModels: Object.values(modelsByProvider).reduce((sum, ids) => sum + ids.length, 0)
|
|
17761
|
-
});
|
|
17762
|
-
writeProviderModelsCache({
|
|
17763
|
-
models: modelsByProvider,
|
|
17764
|
-
connected
|
|
17765
|
-
});
|
|
17766
|
-
} catch (err) {
|
|
17767
|
-
log("[connected-providers-cache] Error updating cache", { error: String(err) });
|
|
17768
17792
|
}
|
|
17793
|
+
return {
|
|
17794
|
+
readConnectedProvidersCache,
|
|
17795
|
+
hasConnectedProvidersCache,
|
|
17796
|
+
readProviderModelsCache,
|
|
17797
|
+
hasProviderModelsCache,
|
|
17798
|
+
writeProviderModelsCache,
|
|
17799
|
+
updateConnectedProvidersCache
|
|
17800
|
+
};
|
|
17769
17801
|
}
|
|
17802
|
+
var defaultConnectedProvidersCacheStore = createConnectedProvidersCacheStore(() => getOmoOpenCodeCacheDir());
|
|
17803
|
+
var {
|
|
17804
|
+
readConnectedProvidersCache,
|
|
17805
|
+
hasConnectedProvidersCache,
|
|
17806
|
+
readProviderModelsCache,
|
|
17807
|
+
hasProviderModelsCache,
|
|
17808
|
+
writeProviderModelsCache,
|
|
17809
|
+
updateConnectedProvidersCache
|
|
17810
|
+
} = defaultConnectedProvidersCacheStore;
|
|
17770
17811
|
|
|
17771
17812
|
// src/shared/model-availability.ts
|
|
17772
17813
|
init_logger();
|
|
@@ -20446,15 +20487,18 @@ async function handleSessionIdle(args) {
|
|
|
20446
20487
|
shouldSkipContinuation
|
|
20447
20488
|
} = args;
|
|
20448
20489
|
log(`[${HOOK_NAME}] session.idle`, { sessionID });
|
|
20490
|
+
console.error(`[TODO-DIAG] session.idle fired for ${sessionID}`);
|
|
20449
20491
|
const state2 = sessionStateStore.getState(sessionID);
|
|
20450
20492
|
if (state2.isRecovering) {
|
|
20451
20493
|
log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID });
|
|
20494
|
+
console.error(`[TODO-DIAG] BLOCKED: isRecovering=true`);
|
|
20452
20495
|
return;
|
|
20453
20496
|
}
|
|
20454
20497
|
if (state2.abortDetectedAt) {
|
|
20455
20498
|
const timeSinceAbort = Date.now() - state2.abortDetectedAt;
|
|
20456
20499
|
if (timeSinceAbort < ABORT_WINDOW_MS) {
|
|
20457
20500
|
log(`[${HOOK_NAME}] Skipped: abort detected via event ${timeSinceAbort}ms ago`, { sessionID });
|
|
20501
|
+
console.error(`[TODO-DIAG] BLOCKED: abort detected ${timeSinceAbort}ms ago`);
|
|
20458
20502
|
state2.abortDetectedAt = undefined;
|
|
20459
20503
|
return;
|
|
20460
20504
|
}
|
|
@@ -20463,6 +20507,7 @@ async function handleSessionIdle(args) {
|
|
|
20463
20507
|
const hasRunningBgTasks = backgroundManager ? backgroundManager.getTasksByParentSession(sessionID).some((task) => task.status === "running") : false;
|
|
20464
20508
|
if (hasRunningBgTasks) {
|
|
20465
20509
|
log(`[${HOOK_NAME}] Skipped: background tasks running`, { sessionID });
|
|
20510
|
+
console.error(`[TODO-DIAG] BLOCKED: background tasks running`, backgroundManager?.getTasksByParentSession(sessionID).filter((t) => t.status === "running").map((t) => t.id));
|
|
20466
20511
|
return;
|
|
20467
20512
|
}
|
|
20468
20513
|
try {
|
|
@@ -20473,10 +20518,12 @@ async function handleSessionIdle(args) {
|
|
|
20473
20518
|
const messages = normalizeSDKResponse(messagesResp, []);
|
|
20474
20519
|
if (isLastAssistantMessageAborted(messages)) {
|
|
20475
20520
|
log(`[${HOOK_NAME}] Skipped: last assistant message was aborted (API fallback)`, { sessionID });
|
|
20521
|
+
console.error(`[TODO-DIAG] BLOCKED: last assistant message aborted`);
|
|
20476
20522
|
return;
|
|
20477
20523
|
}
|
|
20478
20524
|
if (hasUnansweredQuestion(messages)) {
|
|
20479
20525
|
log(`[${HOOK_NAME}] Skipped: pending question awaiting user response`, { sessionID });
|
|
20526
|
+
console.error(`[TODO-DIAG] BLOCKED: hasUnansweredQuestion=true`);
|
|
20480
20527
|
return;
|
|
20481
20528
|
}
|
|
20482
20529
|
} catch (error) {
|
|
@@ -20488,21 +20535,27 @@ async function handleSessionIdle(args) {
|
|
|
20488
20535
|
todos = normalizeSDKResponse(response, [], { preferResponseOnMissingData: true });
|
|
20489
20536
|
} catch (error) {
|
|
20490
20537
|
log(`[${HOOK_NAME}] Todo fetch failed`, { sessionID, error: String(error) });
|
|
20538
|
+
console.error(`[TODO-DIAG] BLOCKED: todo fetch failed`, String(error));
|
|
20491
20539
|
return;
|
|
20492
20540
|
}
|
|
20493
20541
|
if (!todos || todos.length === 0) {
|
|
20542
|
+
sessionStateStore.resetContinuationProgress(sessionID);
|
|
20494
20543
|
sessionStateStore.resetContinuationProgress(sessionID);
|
|
20495
20544
|
log(`[${HOOK_NAME}] No todos`, { sessionID });
|
|
20545
|
+
console.error(`[TODO-DIAG] BLOCKED: no todos`);
|
|
20496
20546
|
return;
|
|
20497
20547
|
}
|
|
20498
20548
|
const incompleteCount = getIncompleteCount(todos);
|
|
20499
20549
|
if (incompleteCount === 0) {
|
|
20550
|
+
sessionStateStore.resetContinuationProgress(sessionID);
|
|
20500
20551
|
sessionStateStore.resetContinuationProgress(sessionID);
|
|
20501
20552
|
log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length });
|
|
20553
|
+
console.error(`[TODO-DIAG] BLOCKED: all todos complete (${todos.length})`);
|
|
20502
20554
|
return;
|
|
20503
20555
|
}
|
|
20504
20556
|
if (state2.inFlight) {
|
|
20505
20557
|
log(`[${HOOK_NAME}] Skipped: injection in flight`, { sessionID });
|
|
20558
|
+
console.error(`[TODO-DIAG] BLOCKED: inFlight=true`);
|
|
20506
20559
|
return;
|
|
20507
20560
|
}
|
|
20508
20561
|
if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && state2.lastInjectedAt && Date.now() - state2.lastInjectedAt >= FAILURE_RESET_WINDOW_MS) {
|
|
@@ -20510,20 +20563,14 @@ async function handleSessionIdle(args) {
|
|
|
20510
20563
|
log(`[${HOOK_NAME}] Reset consecutive failures after recovery window`, { sessionID, failureResetWindowMs: FAILURE_RESET_WINDOW_MS });
|
|
20511
20564
|
}
|
|
20512
20565
|
if (state2.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
|
|
20513
|
-
log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, {
|
|
20514
|
-
|
|
20515
|
-
consecutiveFailures: state2.consecutiveFailures,
|
|
20516
|
-
maxConsecutiveFailures: MAX_CONSECUTIVE_FAILURES
|
|
20517
|
-
});
|
|
20566
|
+
log(`[${HOOK_NAME}] Skipped: max consecutive failures reached`, { sessionID, consecutiveFailures: state2.consecutiveFailures });
|
|
20567
|
+
console.error(`[TODO-DIAG] BLOCKED: consecutiveFailures=${state2.consecutiveFailures} >= ${MAX_CONSECUTIVE_FAILURES}`);
|
|
20518
20568
|
return;
|
|
20519
20569
|
}
|
|
20520
20570
|
const effectiveCooldown = CONTINUATION_COOLDOWN_MS * Math.pow(2, Math.min(state2.consecutiveFailures, 5));
|
|
20521
20571
|
if (state2.lastInjectedAt && Date.now() - state2.lastInjectedAt < effectiveCooldown) {
|
|
20522
|
-
log(`[${HOOK_NAME}] Skipped: cooldown active`, {
|
|
20523
|
-
|
|
20524
|
-
effectiveCooldown,
|
|
20525
|
-
consecutiveFailures: state2.consecutiveFailures
|
|
20526
|
-
});
|
|
20572
|
+
log(`[${HOOK_NAME}] Skipped: cooldown active`, { sessionID, effectiveCooldown, consecutiveFailures: state2.consecutiveFailures });
|
|
20573
|
+
console.error(`[TODO-DIAG] BLOCKED: cooldown active (${effectiveCooldown}ms, failures=${state2.consecutiveFailures})`);
|
|
20527
20574
|
return;
|
|
20528
20575
|
}
|
|
20529
20576
|
let resolvedInfo;
|
|
@@ -20544,10 +20591,12 @@ async function handleSessionIdle(args) {
|
|
|
20544
20591
|
const resolvedAgentName = resolvedInfo?.agent;
|
|
20545
20592
|
if (resolvedAgentName && skipAgents.some((s) => getAgentConfigKey(s) === getAgentConfigKey(resolvedAgentName))) {
|
|
20546
20593
|
log(`[${HOOK_NAME}] Skipped: agent in skipAgents list`, { sessionID, agent: resolvedAgentName });
|
|
20594
|
+
console.error(`[TODO-DIAG] BLOCKED: agent '${resolvedAgentName}' in skipAgents`);
|
|
20547
20595
|
return;
|
|
20548
20596
|
}
|
|
20549
20597
|
if ((compactionGuardActive || encounteredCompaction) && !resolvedInfo?.agent) {
|
|
20550
20598
|
log(`[${HOOK_NAME}] Skipped: compaction occurred but no agent info resolved`, { sessionID });
|
|
20599
|
+
console.error(`[TODO-DIAG] BLOCKED: compaction guard + no agent`);
|
|
20551
20600
|
return;
|
|
20552
20601
|
}
|
|
20553
20602
|
if (state2.recentCompactionAt && resolvedInfo?.agent) {
|
|
@@ -20555,16 +20604,20 @@ async function handleSessionIdle(args) {
|
|
|
20555
20604
|
}
|
|
20556
20605
|
if (isContinuationStopped?.(sessionID)) {
|
|
20557
20606
|
log(`[${HOOK_NAME}] Skipped: continuation stopped for session`, { sessionID });
|
|
20607
|
+
console.error(`[TODO-DIAG] BLOCKED: isContinuationStopped=true`);
|
|
20558
20608
|
return;
|
|
20559
20609
|
}
|
|
20560
20610
|
if (shouldSkipContinuation?.(sessionID)) {
|
|
20561
20611
|
log(`[${HOOK_NAME}] Skipped: another continuation hook already injected`, { sessionID });
|
|
20612
|
+
console.error(`[TODO-DIAG] BLOCKED: shouldSkipContinuation=true (gptPermissionContinuation recently injected)`);
|
|
20562
20613
|
return;
|
|
20563
20614
|
}
|
|
20564
20615
|
const progressUpdate = sessionStateStore.trackContinuationProgress(sessionID, incompleteCount, todos);
|
|
20565
20616
|
if (shouldStopForStagnation({ sessionID, incompleteCount, progressUpdate })) {
|
|
20617
|
+
console.error(`[TODO-DIAG] BLOCKED: stagnation detected (count=${progressUpdate.stagnationCount})`);
|
|
20566
20618
|
return;
|
|
20567
20619
|
}
|
|
20620
|
+
console.error(`[TODO-DIAG] PASSED all gates! Starting countdown (${incompleteCount}/${todos.length} incomplete)`);
|
|
20568
20621
|
startCountdown({
|
|
20569
20622
|
ctx,
|
|
20570
20623
|
sessionID,
|
|
@@ -20655,6 +20708,9 @@ function createTodoContinuationHandler(args) {
|
|
|
20655
20708
|
} = args;
|
|
20656
20709
|
return async ({ event }) => {
|
|
20657
20710
|
const props = event.properties;
|
|
20711
|
+
if (event.type === "session.idle") {
|
|
20712
|
+
console.error(`[TODO-DIAG] handler received session.idle event`, { sessionID: props?.sessionID });
|
|
20713
|
+
}
|
|
20658
20714
|
if (event.type === "session.error") {
|
|
20659
20715
|
const sessionID = props?.sessionID;
|
|
20660
20716
|
if (!sessionID)
|
|
@@ -36377,6 +36433,15 @@ function takePendingCall(callID) {
|
|
|
36377
36433
|
import * as fs6 from "fs";
|
|
36378
36434
|
import { tmpdir as tmpdir4 } from "os";
|
|
36379
36435
|
import { join as join31 } from "path";
|
|
36436
|
+
var ApplyPatchMetadataSchema = zod_default.object({
|
|
36437
|
+
files: zod_default.array(zod_default.object({
|
|
36438
|
+
filePath: zod_default.string(),
|
|
36439
|
+
movePath: zod_default.string().optional(),
|
|
36440
|
+
before: zod_default.string(),
|
|
36441
|
+
after: zod_default.string(),
|
|
36442
|
+
type: zod_default.string().optional()
|
|
36443
|
+
}))
|
|
36444
|
+
});
|
|
36380
36445
|
var DEBUG3 = process.env.COMMENT_CHECKER_DEBUG === "1";
|
|
36381
36446
|
var DEBUG_FILE3 = join31(tmpdir4(), "comment-checker-debug.log");
|
|
36382
36447
|
function debugLog3(...args) {
|
|
@@ -36437,15 +36502,6 @@ function createCommentCheckerHooks(config2) {
|
|
|
36437
36502
|
debugLog3("skipping due to tool failure in output");
|
|
36438
36503
|
return;
|
|
36439
36504
|
}
|
|
36440
|
-
const ApplyPatchMetadataSchema = zod_default.object({
|
|
36441
|
-
files: zod_default.array(zod_default.object({
|
|
36442
|
-
filePath: zod_default.string(),
|
|
36443
|
-
movePath: zod_default.string().optional(),
|
|
36444
|
-
before: zod_default.string(),
|
|
36445
|
-
after: zod_default.string(),
|
|
36446
|
-
type: zod_default.string().optional()
|
|
36447
|
-
}))
|
|
36448
|
-
});
|
|
36449
36505
|
if (toolLower === "apply_patch") {
|
|
36450
36506
|
const parsed = ApplyPatchMetadataSchema.safeParse(output.metadata);
|
|
36451
36507
|
if (!parsed.success) {
|
|
@@ -36916,7 +36972,7 @@ function isTokenLimitError(text) {
|
|
|
36916
36972
|
return false;
|
|
36917
36973
|
}
|
|
36918
36974
|
const lower = text.toLowerCase();
|
|
36919
|
-
return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw
|
|
36975
|
+
return TOKEN_LIMIT_KEYWORDS.some((kw) => lower.includes(kw));
|
|
36920
36976
|
}
|
|
36921
36977
|
function parseAnthropicTokenLimitError(err) {
|
|
36922
36978
|
try {
|
|
@@ -38334,7 +38390,6 @@ function createAnthropicContextWindowLimitRecoveryHook(ctx, options) {
|
|
|
38334
38390
|
};
|
|
38335
38391
|
}
|
|
38336
38392
|
// src/hooks/think-mode/detector.ts
|
|
38337
|
-
var ENGLISH_PATTERNS = [/\bultrathink\b/i, /\bthink\b/i];
|
|
38338
38393
|
var MULTILINGUAL_KEYWORDS = [
|
|
38339
38394
|
"\uC0DD\uAC01",
|
|
38340
38395
|
"\uAC80\uD1A0",
|
|
@@ -38420,8 +38475,7 @@ var MULTILINGUAL_KEYWORDS = [
|
|
|
38420
38475
|
"fikir",
|
|
38421
38476
|
"berfikir"
|
|
38422
38477
|
];
|
|
38423
|
-
var
|
|
38424
|
-
var THINK_PATTERNS = [...ENGLISH_PATTERNS, ...MULTILINGUAL_PATTERNS];
|
|
38478
|
+
var COMBINED_THINK_PATTERN = new RegExp(`\\b(?:ultrathink|think)\\b|${MULTILINGUAL_KEYWORDS.join("|")}`, "i");
|
|
38425
38479
|
var CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
|
|
38426
38480
|
var INLINE_CODE_PATTERN = /`[^`]+`/g;
|
|
38427
38481
|
function removeCodeBlocks(text) {
|
|
@@ -38429,7 +38483,7 @@ function removeCodeBlocks(text) {
|
|
|
38429
38483
|
}
|
|
38430
38484
|
function detectThinkKeyword(text) {
|
|
38431
38485
|
const textWithoutCode = removeCodeBlocks(text);
|
|
38432
|
-
return
|
|
38486
|
+
return COMBINED_THINK_PATTERN.test(textWithoutCode);
|
|
38433
38487
|
}
|
|
38434
38488
|
function extractPromptText(parts) {
|
|
38435
38489
|
return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("");
|
|
@@ -39086,16 +39140,16 @@ async function loadPluginExtendedConfig() {
|
|
|
39086
39140
|
}
|
|
39087
39141
|
return merged;
|
|
39088
39142
|
}
|
|
39089
|
-
var
|
|
39143
|
+
var regexCache2 = new Map;
|
|
39090
39144
|
function getRegex(pattern) {
|
|
39091
|
-
let regex =
|
|
39145
|
+
let regex = regexCache2.get(pattern);
|
|
39092
39146
|
if (!regex) {
|
|
39093
39147
|
try {
|
|
39094
39148
|
regex = new RegExp(pattern);
|
|
39095
|
-
|
|
39149
|
+
regexCache2.set(pattern, regex);
|
|
39096
39150
|
} catch {
|
|
39097
39151
|
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
39098
|
-
|
|
39152
|
+
regexCache2.set(pattern, regex);
|
|
39099
39153
|
}
|
|
39100
39154
|
}
|
|
39101
39155
|
return regex;
|
|
@@ -39961,8 +40015,6 @@ function createToolExecuteAfterHandler(ctx, config2) {
|
|
|
39961
40015
|
if (!output) {
|
|
39962
40016
|
return;
|
|
39963
40017
|
}
|
|
39964
|
-
const claudeConfig = await loadClaudeHooksConfig();
|
|
39965
|
-
const extendedConfig = await loadPluginExtendedConfig();
|
|
39966
40018
|
const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
|
|
39967
40019
|
appendTranscriptEntry(input.sessionID, {
|
|
39968
40020
|
type: "tool_result",
|
|
@@ -39974,6 +40026,8 @@ function createToolExecuteAfterHandler(ctx, config2) {
|
|
|
39974
40026
|
if (isHookDisabled(config2, "PostToolUse")) {
|
|
39975
40027
|
return;
|
|
39976
40028
|
}
|
|
40029
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
40030
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
39977
40031
|
const postClient = {
|
|
39978
40032
|
session: {
|
|
39979
40033
|
messages: (opts) => ctx.client.session.messages(opts)
|
|
@@ -40154,8 +40208,6 @@ function createToolExecuteBeforeHandler(ctx, config2) {
|
|
|
40154
40208
|
output.args.todos = parsed;
|
|
40155
40209
|
log("todowrite: parsed todos string to array", { sessionID: input.sessionID });
|
|
40156
40210
|
}
|
|
40157
|
-
const claudeConfig = await loadClaudeHooksConfig();
|
|
40158
|
-
const extendedConfig = await loadPluginExtendedConfig();
|
|
40159
40211
|
appendTranscriptEntry(input.sessionID, {
|
|
40160
40212
|
type: "tool_use",
|
|
40161
40213
|
timestamp: new Date().toISOString(),
|
|
@@ -40166,6 +40218,8 @@ function createToolExecuteBeforeHandler(ctx, config2) {
|
|
|
40166
40218
|
if (isHookDisabled(config2, "PreToolUse")) {
|
|
40167
40219
|
return;
|
|
40168
40220
|
}
|
|
40221
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
40222
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
40169
40223
|
const preCtx = {
|
|
40170
40224
|
sessionId: input.sessionID,
|
|
40171
40225
|
toolName: input.tool,
|
|
@@ -43759,6 +43813,9 @@ async function injectContinuationPrompt(ctx, options) {
|
|
|
43759
43813
|
async function handleDetectedCompletion(ctx, input) {
|
|
43760
43814
|
const { sessionID, state: state3, loopState, directory, apiTimeoutMs } = input;
|
|
43761
43815
|
if (state3.ultrawork && !state3.verification_pending) {
|
|
43816
|
+
if (state3.verification_session_id) {
|
|
43817
|
+
ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
|
|
43818
|
+
}
|
|
43762
43819
|
const verificationState = loopState.markVerificationPending(sessionID);
|
|
43763
43820
|
if (!verificationState) {
|
|
43764
43821
|
log(`[${HOOK_NAME3}] Failed to transition ultrawork loop to verification`, {
|
|
@@ -43995,6 +44052,9 @@ async function handleFailedVerification(ctx, input) {
|
|
|
43995
44052
|
});
|
|
43996
44053
|
return false;
|
|
43997
44054
|
}
|
|
44055
|
+
if (state3.verification_session_id) {
|
|
44056
|
+
ctx.client.session.abort({ path: { id: state3.verification_session_id } }).catch(() => {});
|
|
44057
|
+
}
|
|
43998
44058
|
const resumedState = loopState.restartAfterFailedVerification(parentSessionID, messageCountAtStart);
|
|
43999
44059
|
if (!resumedState) {
|
|
44000
44060
|
log(`[${HOOK_NAME3}] Failed to restart loop after verification failure`, {
|
|
@@ -47600,9 +47660,9 @@ var BabysittingConfigSchema = exports_external.object({
|
|
|
47600
47660
|
});
|
|
47601
47661
|
// src/config/schema/background-task.ts
|
|
47602
47662
|
var CircuitBreakerConfigSchema = exports_external.object({
|
|
47663
|
+
enabled: exports_external.boolean().optional(),
|
|
47603
47664
|
maxToolCalls: exports_external.number().int().min(10).optional(),
|
|
47604
|
-
|
|
47605
|
-
repetitionThresholdPercent: exports_external.number().gt(0).max(100).optional()
|
|
47665
|
+
consecutiveThreshold: exports_external.number().int().min(5).optional()
|
|
47606
47666
|
});
|
|
47607
47667
|
var BackgroundTaskConfigSchema = exports_external.object({
|
|
47608
47668
|
defaultConcurrency: exports_external.number().min(1).optional(),
|
|
@@ -47798,7 +47858,8 @@ var HookNameSchema = exports_external.enum([
|
|
|
47798
47858
|
"write-existing-file-guard",
|
|
47799
47859
|
"anthropic-effort",
|
|
47800
47860
|
"hashline-read-enhancer",
|
|
47801
|
-
"read-image-resizer"
|
|
47861
|
+
"read-image-resizer",
|
|
47862
|
+
"todo-description-override"
|
|
47802
47863
|
]);
|
|
47803
47864
|
// src/config/schema/notification.ts
|
|
47804
47865
|
var NotificationConfigSchema = exports_external.object({
|
|
@@ -49201,7 +49262,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
|
|
|
49201
49262
|
- \`--worktree <path>\` (optional): absolute path to an existing git worktree to work in
|
|
49202
49263
|
- If specified and valid: hook pre-sets worktree_path in boulder.json
|
|
49203
49264
|
- If specified but invalid: you must run \`git worktree add <path> <branch>\` first
|
|
49204
|
-
- If omitted:
|
|
49265
|
+
- If omitted: work directly in the current project directory (no worktree)
|
|
49205
49266
|
|
|
49206
49267
|
## WHAT TO DO
|
|
49207
49268
|
|
|
@@ -49218,7 +49279,7 @@ var START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
|
|
|
49218
49279
|
- If ONE plan: auto-select it
|
|
49219
49280
|
- If MULTIPLE plans: show list with timestamps, ask user to select
|
|
49220
49281
|
|
|
49221
|
-
4. **Worktree Setup** (when \`worktree_path\` not already set in boulder.json):
|
|
49282
|
+
4. **Worktree Setup** (ONLY when \`--worktree\` was explicitly specified and \`worktree_path\` not already set in boulder.json):
|
|
49222
49283
|
1. \`git worktree list --porcelain\` \u2014 see available worktrees
|
|
49223
49284
|
2. Create: \`git worktree add <absolute-path> <branch-or-HEAD>\`
|
|
49224
49285
|
3. Update boulder.json to add \`"worktree_path": "<absolute-path>"\`
|
|
@@ -49280,9 +49341,41 @@ Reading plan and beginning execution...
|
|
|
49280
49341
|
|
|
49281
49342
|
- The session_id is injected by the hook - use it directly
|
|
49282
49343
|
- Always update boulder.json BEFORE starting work
|
|
49283
|
-
-
|
|
49344
|
+
- If worktree_path is set in boulder.json, all work happens inside that worktree directory
|
|
49284
49345
|
- Read the FULL plan file before delegating any tasks
|
|
49285
|
-
- Follow atlas delegation protocols (7-section format)
|
|
49346
|
+
- Follow atlas delegation protocols (7-section format)
|
|
49347
|
+
|
|
49348
|
+
## TASK BREAKDOWN (MANDATORY)
|
|
49349
|
+
|
|
49350
|
+
After reading the plan file, you MUST decompose every plan task into granular, implementation-level sub-steps and register ALL of them as task/todo items BEFORE starting any work.
|
|
49351
|
+
|
|
49352
|
+
**How to break down**:
|
|
49353
|
+
- Each plan checkbox item (e.g., \`- [ ] Add user authentication\`) must be split into concrete, actionable sub-tasks
|
|
49354
|
+
- Sub-tasks should be specific enough that each one touches a clear set of files/functions
|
|
49355
|
+
- Include: file to modify, what to change, expected behavior, and how to verify
|
|
49356
|
+
- Do NOT leave any task vague \u2014 "implement feature X" is NOT acceptable; "add validateToken() to src/auth/middleware.ts that checks JWT expiry and returns 401" IS acceptable
|
|
49357
|
+
|
|
49358
|
+
**Example breakdown**:
|
|
49359
|
+
Plan task: \`- [ ] Add rate limiting to API\`
|
|
49360
|
+
\u2192 Todo items:
|
|
49361
|
+
1. Create \`src/middleware/rate-limiter.ts\` with sliding window algorithm (max 100 req/min per IP)
|
|
49362
|
+
2. Add RateLimiter middleware to \`src/app.ts\` router chain, before auth middleware
|
|
49363
|
+
3. Add rate limit headers (X-RateLimit-Limit, X-RateLimit-Remaining) to response in \`rate-limiter.ts\`
|
|
49364
|
+
4. Add test: verify 429 response after exceeding limit in \`src/middleware/rate-limiter.test.ts\`
|
|
49365
|
+
5. Add test: verify headers are present on normal responses
|
|
49366
|
+
|
|
49367
|
+
Register these as task/todo items so progress is tracked and visible throughout the session.
|
|
49368
|
+
|
|
49369
|
+
## WORKTREE COMPLETION
|
|
49370
|
+
|
|
49371
|
+
When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
|
|
49372
|
+
1. Commit all remaining changes in the worktree
|
|
49373
|
+
2. Switch to the main working directory (the original repo, NOT the worktree)
|
|
49374
|
+
3. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
|
|
49375
|
+
4. If merge succeeds, clean up: \`git worktree remove <worktree-path>\`
|
|
49376
|
+
5. Remove the boulder.json state
|
|
49377
|
+
|
|
49378
|
+
This is the DEFAULT behavior when \`--worktree\` was used. Skip merge only if the user explicitly instructs otherwise (e.g., asks to create a PR instead).`;
|
|
49286
49379
|
|
|
49287
49380
|
// src/features/builtin-commands/templates/handoff.ts
|
|
49288
49381
|
var HANDOFF_TEMPLATE = `# Handoff Command
|
|
@@ -49664,9 +49757,6 @@ function skillToCommandInfo(skill) {
|
|
|
49664
49757
|
lazyContentLoader: skill.lazyContent
|
|
49665
49758
|
};
|
|
49666
49759
|
}
|
|
49667
|
-
function filterDiscoveredCommandsByScope(commands3, scope) {
|
|
49668
|
-
return commands3.filter((command) => command.scope === scope);
|
|
49669
|
-
}
|
|
49670
49760
|
async function discoverAllCommands(options) {
|
|
49671
49761
|
const discoveredCommands = discoverCommandsSync(process.cwd(), {
|
|
49672
49762
|
pluginsEnabled: options?.pluginsEnabled,
|
|
@@ -49674,14 +49764,17 @@ async function discoverAllCommands(options) {
|
|
|
49674
49764
|
});
|
|
49675
49765
|
const skills2 = options?.skills ?? await discoverAllSkills();
|
|
49676
49766
|
const skillCommands = skills2.map(skillToCommandInfo);
|
|
49767
|
+
const scopeOrder = ["project", "user", "opencode-project", "opencode", "builtin", "plugin"];
|
|
49768
|
+
const grouped = new Map;
|
|
49769
|
+
for (const cmd of discoveredCommands) {
|
|
49770
|
+
const list = grouped.get(cmd.scope) ?? [];
|
|
49771
|
+
list.push(cmd);
|
|
49772
|
+
grouped.set(cmd.scope, list);
|
|
49773
|
+
}
|
|
49774
|
+
const orderedCommands = scopeOrder.flatMap((scope) => grouped.get(scope) ?? []);
|
|
49677
49775
|
return [
|
|
49678
49776
|
...skillCommands,
|
|
49679
|
-
...
|
|
49680
|
-
...filterDiscoveredCommandsByScope(discoveredCommands, "user"),
|
|
49681
|
-
...filterDiscoveredCommandsByScope(discoveredCommands, "opencode-project"),
|
|
49682
|
-
...filterDiscoveredCommandsByScope(discoveredCommands, "opencode"),
|
|
49683
|
-
...filterDiscoveredCommandsByScope(discoveredCommands, "builtin"),
|
|
49684
|
-
...filterDiscoveredCommandsByScope(discoveredCommands, "plugin")
|
|
49777
|
+
...orderedCommands
|
|
49685
49778
|
];
|
|
49686
49779
|
}
|
|
49687
49780
|
async function findCommand2(commandName, options) {
|
|
@@ -53440,6 +53533,7 @@ function getErrorMessage2(error48) {
|
|
|
53440
53533
|
return "";
|
|
53441
53534
|
}
|
|
53442
53535
|
}
|
|
53536
|
+
var DEFAULT_RETRY_PATTERN = new RegExp(`\\b(${DEFAULT_CONFIG2.retry_on_errors.join("|")})\\b`);
|
|
53443
53537
|
function extractStatusCode(error48, retryOnErrors) {
|
|
53444
53538
|
if (!error48)
|
|
53445
53539
|
return;
|
|
@@ -53454,8 +53548,7 @@ function extractStatusCode(error48, retryOnErrors) {
|
|
|
53454
53548
|
if (statusCode !== undefined) {
|
|
53455
53549
|
return statusCode;
|
|
53456
53550
|
}
|
|
53457
|
-
const
|
|
53458
|
-
const pattern = new RegExp(`\\b(${codes.join("|")})\\b`);
|
|
53551
|
+
const pattern = retryOnErrors ? new RegExp(`\\b(${retryOnErrors.join("|")})\\b`) : DEFAULT_RETRY_PATTERN;
|
|
53459
53552
|
const message = getErrorMessage2(error48);
|
|
53460
53553
|
const statusMatch = message.match(pattern);
|
|
53461
53554
|
if (statusMatch) {
|
|
@@ -55146,6 +55239,46 @@ function createReadImageResizerHook(_ctx) {
|
|
|
55146
55239
|
}
|
|
55147
55240
|
};
|
|
55148
55241
|
}
|
|
55242
|
+
// src/hooks/todo-description-override/description.ts
|
|
55243
|
+
var TODOWRITE_DESCRIPTION = `Use this tool to create and manage a structured task list for tracking progress on multi-step work.
|
|
55244
|
+
|
|
55245
|
+
## Todo Format (MANDATORY)
|
|
55246
|
+
|
|
55247
|
+
Each todo title MUST encode four elements: WHERE, WHY, HOW, and EXPECTED RESULT.
|
|
55248
|
+
|
|
55249
|
+
Format: "[WHERE] [HOW] to [WHY] \u2014 expect [RESULT]"
|
|
55250
|
+
|
|
55251
|
+
GOOD:
|
|
55252
|
+
- "src/utils/validation.ts: Add validateEmail() for input sanitization \u2014 returns boolean"
|
|
55253
|
+
- "UserService.create(): Call validateEmail() before DB insert \u2014 rejects invalid emails with 400"
|
|
55254
|
+
- "validation.test.ts: Add test for missing @ sign \u2014 expect validateEmail('foo') to return false"
|
|
55255
|
+
|
|
55256
|
+
BAD:
|
|
55257
|
+
- "Implement email validation" (where? how? what result?)
|
|
55258
|
+
- "Add dark mode" (this is a feature, not a todo)
|
|
55259
|
+
- "Fix auth" (what file? what changes? what's expected?)
|
|
55260
|
+
|
|
55261
|
+
## Granularity Rules
|
|
55262
|
+
|
|
55263
|
+
Each todo MUST be a single atomic action completable in 1-3 tool calls. If it needs more, split it.
|
|
55264
|
+
|
|
55265
|
+
**Size test**: Can you complete this todo by editing one file or running one command? If not, it's too big.
|
|
55266
|
+
|
|
55267
|
+
## Task Management
|
|
55268
|
+
- One in_progress at a time. Complete it before starting the next.
|
|
55269
|
+
- Mark completed immediately after finishing each item.
|
|
55270
|
+
- Skip this tool for single trivial tasks (one-step, obvious action).`;
|
|
55271
|
+
|
|
55272
|
+
// src/hooks/todo-description-override/hook.ts
|
|
55273
|
+
function createTodoDescriptionOverrideHook() {
|
|
55274
|
+
return {
|
|
55275
|
+
"tool.definition": async (input, output) => {
|
|
55276
|
+
if (input.toolID === "todowrite") {
|
|
55277
|
+
output.description = TODOWRITE_DESCRIPTION;
|
|
55278
|
+
}
|
|
55279
|
+
}
|
|
55280
|
+
};
|
|
55281
|
+
}
|
|
55149
55282
|
// src/hooks/anthropic-effort/hook.ts
|
|
55150
55283
|
var OPUS_4_6_PATTERN = /claude-opus-4[-.]6/i;
|
|
55151
55284
|
function isClaudeProvider(providerID, modelID) {
|
|
@@ -75723,10 +75856,11 @@ Returns summary format: id, subject, status, owner, blockedBy (not full descript
|
|
|
75723
75856
|
allTasks.push(task);
|
|
75724
75857
|
}
|
|
75725
75858
|
}
|
|
75859
|
+
const taskMap = new Map(allTasks.map((t) => [t.id, t]));
|
|
75726
75860
|
const activeTasks = allTasks.filter((task) => task.status !== "completed" && task.status !== "deleted");
|
|
75727
75861
|
const summaries = activeTasks.map((task) => {
|
|
75728
75862
|
const unresolvedBlockers = task.blockedBy.filter((blockerId) => {
|
|
75729
|
-
const blockerTask =
|
|
75863
|
+
const blockerTask = taskMap.get(blockerId);
|
|
75730
75864
|
return !blockerTask || blockerTask.status !== "completed";
|
|
75731
75865
|
});
|
|
75732
75866
|
return {
|
|
@@ -76415,6 +76549,15 @@ function applyPrepend(lines, text) {
|
|
|
76415
76549
|
}
|
|
76416
76550
|
|
|
76417
76551
|
// src/tools/hashline-edit/edit-operations.ts
|
|
76552
|
+
function arraysEqual(a, b) {
|
|
76553
|
+
if (a.length !== b.length)
|
|
76554
|
+
return false;
|
|
76555
|
+
for (let i2 = 0;i2 < a.length; i2++) {
|
|
76556
|
+
if (a[i2] !== b[i2])
|
|
76557
|
+
return false;
|
|
76558
|
+
}
|
|
76559
|
+
return true;
|
|
76560
|
+
}
|
|
76418
76561
|
function applyHashlineEditsWithReport(content, edits) {
|
|
76419
76562
|
if (edits.length === 0) {
|
|
76420
76563
|
return {
|
|
@@ -76444,9 +76587,7 @@ function applyHashlineEditsWithReport(content, edits) {
|
|
|
76444
76587
|
switch (edit.op) {
|
|
76445
76588
|
case "replace": {
|
|
76446
76589
|
const next = edit.end ? applyReplaceLines(lines, edit.pos, edit.end, edit.lines, { skipValidation: true }) : applySetLine(lines, edit.pos, edit.lines, { skipValidation: true });
|
|
76447
|
-
if (next
|
|
76448
|
-
`) === lines.join(`
|
|
76449
|
-
`)) {
|
|
76590
|
+
if (arraysEqual(next, lines)) {
|
|
76450
76591
|
noopEdits += 1;
|
|
76451
76592
|
break;
|
|
76452
76593
|
}
|
|
@@ -76455,9 +76596,7 @@ function applyHashlineEditsWithReport(content, edits) {
|
|
|
76455
76596
|
}
|
|
76456
76597
|
case "append": {
|
|
76457
76598
|
const next = edit.pos ? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true }) : applyAppend(lines, edit.lines);
|
|
76458
|
-
if (next
|
|
76459
|
-
`) === lines.join(`
|
|
76460
|
-
`)) {
|
|
76599
|
+
if (arraysEqual(next, lines)) {
|
|
76461
76600
|
noopEdits += 1;
|
|
76462
76601
|
break;
|
|
76463
76602
|
}
|
|
@@ -76466,9 +76605,7 @@ function applyHashlineEditsWithReport(content, edits) {
|
|
|
76466
76605
|
}
|
|
76467
76606
|
case "prepend": {
|
|
76468
76607
|
const next = edit.pos ? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true }) : applyPrepend(lines, edit.lines);
|
|
76469
|
-
if (next
|
|
76470
|
-
`) === lines.join(`
|
|
76471
|
-
`)) {
|
|
76608
|
+
if (arraysEqual(next, lines)) {
|
|
76472
76609
|
noopEdits += 1;
|
|
76473
76610
|
break;
|
|
76474
76611
|
}
|
|
@@ -77493,6 +77630,7 @@ function createToolGuardHooks(args) {
|
|
|
77493
77630
|
const hashlineReadEnhancer = isHookEnabled("hashline-read-enhancer") ? safeHook("hashline-read-enhancer", () => createHashlineReadEnhancerHook(ctx, { hashline_edit: { enabled: pluginConfig.hashline_edit ?? false } })) : null;
|
|
77494
77631
|
const jsonErrorRecovery = isHookEnabled("json-error-recovery") ? safeHook("json-error-recovery", () => createJsonErrorRecoveryHook(ctx)) : null;
|
|
77495
77632
|
const readImageResizer = isHookEnabled("read-image-resizer") ? safeHook("read-image-resizer", () => createReadImageResizerHook(ctx)) : null;
|
|
77633
|
+
const todoDescriptionOverride = isHookEnabled("todo-description-override") ? safeHook("todo-description-override", () => createTodoDescriptionOverrideHook()) : null;
|
|
77496
77634
|
return {
|
|
77497
77635
|
commentChecker,
|
|
77498
77636
|
toolOutputTruncator,
|
|
@@ -77504,7 +77642,8 @@ function createToolGuardHooks(args) {
|
|
|
77504
77642
|
writeExistingFileGuard,
|
|
77505
77643
|
hashlineReadEnhancer,
|
|
77506
77644
|
jsonErrorRecovery,
|
|
77507
|
-
readImageResizer
|
|
77645
|
+
readImageResizer,
|
|
77646
|
+
todoDescriptionOverride
|
|
77508
77647
|
};
|
|
77509
77648
|
}
|
|
77510
77649
|
|
|
@@ -78031,8 +78170,8 @@ var MIN_STABILITY_TIME_MS2 = 10 * 1000;
|
|
|
78031
78170
|
var DEFAULT_STALE_TIMEOUT_MS = 1200000;
|
|
78032
78171
|
var DEFAULT_MESSAGE_STALENESS_TIMEOUT_MS = 1800000;
|
|
78033
78172
|
var DEFAULT_MAX_TOOL_CALLS = 200;
|
|
78034
|
-
var
|
|
78035
|
-
var
|
|
78173
|
+
var DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD = 20;
|
|
78174
|
+
var DEFAULT_CIRCUIT_BREAKER_ENABLED = true;
|
|
78036
78175
|
var MIN_RUNTIME_BEFORE_STALE_MS = 30000;
|
|
78037
78176
|
var MIN_IDLE_TIME_MS = 5000;
|
|
78038
78177
|
var POLLING_INTERVAL_MS = 3000;
|
|
@@ -78453,6 +78592,22 @@ function removeTaskToastTracking(taskId) {
|
|
|
78453
78592
|
}
|
|
78454
78593
|
}
|
|
78455
78594
|
|
|
78595
|
+
// src/features/background-agent/session-status-classifier.ts
|
|
78596
|
+
var ACTIVE_SESSION_STATUSES = new Set(["busy", "retry", "running"]);
|
|
78597
|
+
var KNOWN_TERMINAL_STATUSES = new Set(["idle", "interrupted"]);
|
|
78598
|
+
function isActiveSessionStatus(type2) {
|
|
78599
|
+
if (ACTIVE_SESSION_STATUSES.has(type2)) {
|
|
78600
|
+
return true;
|
|
78601
|
+
}
|
|
78602
|
+
if (!KNOWN_TERMINAL_STATUSES.has(type2)) {
|
|
78603
|
+
log("[background-agent] Unknown session status type encountered:", type2);
|
|
78604
|
+
}
|
|
78605
|
+
return false;
|
|
78606
|
+
}
|
|
78607
|
+
function isTerminalSessionStatus(type2) {
|
|
78608
|
+
return KNOWN_TERMINAL_STATUSES.has(type2) && type2 !== "idle";
|
|
78609
|
+
}
|
|
78610
|
+
|
|
78456
78611
|
// src/features/background-agent/task-poller.ts
|
|
78457
78612
|
var TERMINAL_TASK_STATUSES = new Set([
|
|
78458
78613
|
"completed",
|
|
@@ -78531,7 +78686,7 @@ async function checkAndInterruptStaleTasks(args) {
|
|
|
78531
78686
|
if (!startedAt || !sessionID)
|
|
78532
78687
|
continue;
|
|
78533
78688
|
const sessionStatus = sessionStatuses?.[sessionID]?.type;
|
|
78534
|
-
const sessionIsRunning = sessionStatus !== undefined && sessionStatus
|
|
78689
|
+
const sessionIsRunning = sessionStatus !== undefined && isActiveSessionStatus(sessionStatus);
|
|
78535
78690
|
const runtime = now - startedAt.getTime();
|
|
78536
78691
|
if (!task.progress?.lastUpdate) {
|
|
78537
78692
|
if (sessionIsRunning)
|
|
@@ -78587,51 +78742,57 @@ async function checkAndInterruptStaleTasks(args) {
|
|
|
78587
78742
|
// src/features/background-agent/loop-detector.ts
|
|
78588
78743
|
function resolveCircuitBreakerSettings(config4) {
|
|
78589
78744
|
return {
|
|
78745
|
+
enabled: config4?.circuitBreaker?.enabled ?? DEFAULT_CIRCUIT_BREAKER_ENABLED,
|
|
78590
78746
|
maxToolCalls: config4?.circuitBreaker?.maxToolCalls ?? config4?.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS,
|
|
78591
|
-
|
|
78592
|
-
repetitionThresholdPercent: config4?.circuitBreaker?.repetitionThresholdPercent ?? DEFAULT_CIRCUIT_BREAKER_REPETITION_THRESHOLD_PERCENT
|
|
78747
|
+
consecutiveThreshold: config4?.circuitBreaker?.consecutiveThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONSECUTIVE_THRESHOLD
|
|
78593
78748
|
};
|
|
78594
78749
|
}
|
|
78595
|
-
function recordToolCall(window, toolName, settings) {
|
|
78596
|
-
const
|
|
78597
|
-
|
|
78750
|
+
function recordToolCall(window, toolName, settings, toolInput) {
|
|
78751
|
+
const signature = createToolCallSignature(toolName, toolInput);
|
|
78752
|
+
if (window && window.lastSignature === signature) {
|
|
78753
|
+
return {
|
|
78754
|
+
lastSignature: signature,
|
|
78755
|
+
consecutiveCount: window.consecutiveCount + 1,
|
|
78756
|
+
threshold: settings.consecutiveThreshold
|
|
78757
|
+
};
|
|
78758
|
+
}
|
|
78598
78759
|
return {
|
|
78599
|
-
|
|
78600
|
-
|
|
78601
|
-
|
|
78760
|
+
lastSignature: signature,
|
|
78761
|
+
consecutiveCount: 1,
|
|
78762
|
+
threshold: settings.consecutiveThreshold
|
|
78602
78763
|
};
|
|
78603
78764
|
}
|
|
78604
|
-
function
|
|
78605
|
-
if (
|
|
78606
|
-
return
|
|
78607
|
-
|
|
78608
|
-
|
|
78609
|
-
|
|
78610
|
-
|
|
78765
|
+
function sortObject2(obj) {
|
|
78766
|
+
if (obj === null || obj === undefined)
|
|
78767
|
+
return obj;
|
|
78768
|
+
if (typeof obj !== "object")
|
|
78769
|
+
return obj;
|
|
78770
|
+
if (Array.isArray(obj))
|
|
78771
|
+
return obj.map(sortObject2);
|
|
78772
|
+
const sorted = {};
|
|
78773
|
+
const keys = Object.keys(obj).sort();
|
|
78774
|
+
for (const key of keys) {
|
|
78775
|
+
sorted[key] = sortObject2(obj[key]);
|
|
78611
78776
|
}
|
|
78612
|
-
|
|
78613
|
-
|
|
78614
|
-
|
|
78615
|
-
|
|
78616
|
-
|
|
78617
|
-
repeatedCount = count;
|
|
78618
|
-
}
|
|
78777
|
+
return sorted;
|
|
78778
|
+
}
|
|
78779
|
+
function createToolCallSignature(toolName, toolInput) {
|
|
78780
|
+
if (toolInput === undefined || toolInput === null) {
|
|
78781
|
+
return toolName;
|
|
78619
78782
|
}
|
|
78620
|
-
|
|
78621
|
-
|
|
78622
|
-
if (sampleSize < minimumSampleSize) {
|
|
78623
|
-
return { triggered: false };
|
|
78783
|
+
if (Object.keys(toolInput).length === 0) {
|
|
78784
|
+
return toolName;
|
|
78624
78785
|
}
|
|
78625
|
-
|
|
78626
|
-
|
|
78786
|
+
return `${toolName}::${JSON.stringify(sortObject2(toolInput))}`;
|
|
78787
|
+
}
|
|
78788
|
+
function detectRepetitiveToolUse(window) {
|
|
78789
|
+
if (!window || window.consecutiveCount < window.threshold) {
|
|
78627
78790
|
return { triggered: false };
|
|
78628
78791
|
}
|
|
78629
78792
|
return {
|
|
78630
78793
|
triggered: true,
|
|
78631
|
-
toolName:
|
|
78632
|
-
repeatedCount
|
|
78633
|
-
sampleSize,
|
|
78634
|
-
thresholdPercent: window.thresholdPercent
|
|
78794
|
+
toolName: window.lastSignature.split("::")[0],
|
|
78795
|
+
repeatedCount: window.consecutiveCount
|
|
78635
78796
|
};
|
|
78636
78797
|
}
|
|
78637
78798
|
|
|
@@ -78730,6 +78891,7 @@ class BackgroundManager {
|
|
|
78730
78891
|
preStartDescendantReservations;
|
|
78731
78892
|
enableParentSessionNotifications;
|
|
78732
78893
|
taskHistory = new TaskHistory;
|
|
78894
|
+
cachedCircuitBreakerSettings;
|
|
78733
78895
|
constructor(ctx, config4, options) {
|
|
78734
78896
|
this.tasks = new Map;
|
|
78735
78897
|
this.notifications = new Map;
|
|
@@ -79322,35 +79484,36 @@ class BackgroundManager {
|
|
|
79322
79484
|
}
|
|
79323
79485
|
task.progress.lastUpdate = new Date;
|
|
79324
79486
|
if (partInfo?.type === "tool" || partInfo?.tool) {
|
|
79325
|
-
const countedToolPartIDs = task.progress.countedToolPartIDs ??
|
|
79326
|
-
const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.
|
|
79487
|
+
const countedToolPartIDs = task.progress.countedToolPartIDs ?? new Set;
|
|
79488
|
+
const shouldCountToolCall = !partInfo.id || partInfo.state?.status !== "running" || !countedToolPartIDs.has(partInfo.id);
|
|
79327
79489
|
if (!shouldCountToolCall) {
|
|
79328
79490
|
return;
|
|
79329
79491
|
}
|
|
79330
79492
|
if (partInfo.id && partInfo.state?.status === "running") {
|
|
79331
|
-
|
|
79493
|
+
countedToolPartIDs.add(partInfo.id);
|
|
79494
|
+
task.progress.countedToolPartIDs = countedToolPartIDs;
|
|
79332
79495
|
}
|
|
79333
79496
|
task.progress.toolCalls += 1;
|
|
79334
79497
|
task.progress.lastTool = partInfo.tool;
|
|
79335
|
-
const circuitBreaker = resolveCircuitBreakerSettings(this.config);
|
|
79498
|
+
const circuitBreaker = this.cachedCircuitBreakerSettings ?? (this.cachedCircuitBreakerSettings = resolveCircuitBreakerSettings(this.config));
|
|
79336
79499
|
if (partInfo.tool) {
|
|
79337
|
-
task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker);
|
|
79338
|
-
|
|
79339
|
-
|
|
79340
|
-
|
|
79341
|
-
|
|
79342
|
-
|
|
79343
|
-
|
|
79344
|
-
|
|
79345
|
-
|
|
79346
|
-
|
|
79347
|
-
|
|
79348
|
-
|
|
79349
|
-
|
|
79350
|
-
|
|
79351
|
-
|
|
79352
|
-
|
|
79353
|
-
|
|
79500
|
+
task.progress.toolCallWindow = recordToolCall(task.progress.toolCallWindow, partInfo.tool, circuitBreaker, partInfo.state?.input);
|
|
79501
|
+
if (circuitBreaker.enabled) {
|
|
79502
|
+
const loopDetection = detectRepetitiveToolUse(task.progress.toolCallWindow);
|
|
79503
|
+
if (loopDetection.triggered) {
|
|
79504
|
+
log("[background-agent] Circuit breaker: consecutive tool usage detected", {
|
|
79505
|
+
taskId: task.id,
|
|
79506
|
+
agent: task.agent,
|
|
79507
|
+
sessionID,
|
|
79508
|
+
toolName: loopDetection.toolName,
|
|
79509
|
+
repeatedCount: loopDetection.repeatedCount
|
|
79510
|
+
});
|
|
79511
|
+
this.cancelTask(task.id, {
|
|
79512
|
+
source: "circuit-breaker",
|
|
79513
|
+
reason: `Subagent called ${loopDetection.toolName} ${loopDetection.repeatedCount} consecutive times (threshold: ${circuitBreaker.consecutiveThreshold}). This usually indicates an infinite loop. The task was automatically cancelled to prevent excessive token usage.`
|
|
79514
|
+
});
|
|
79515
|
+
return;
|
|
79516
|
+
}
|
|
79354
79517
|
}
|
|
79355
79518
|
}
|
|
79356
79519
|
const maxToolCalls = circuitBreaker.maxToolCalls;
|
|
@@ -79997,7 +80160,7 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|
|
79997
80160
|
continue;
|
|
79998
80161
|
}
|
|
79999
80162
|
}
|
|
80000
|
-
if (sessionStatus && sessionStatus.type
|
|
80163
|
+
if (sessionStatus && isActiveSessionStatus(sessionStatus.type)) {
|
|
80001
80164
|
log("[background-agent] Session still running, relying on event-based progress:", {
|
|
80002
80165
|
taskId: task.id,
|
|
80003
80166
|
sessionID,
|
|
@@ -80006,6 +80169,17 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
|
|
80006
80169
|
});
|
|
80007
80170
|
continue;
|
|
80008
80171
|
}
|
|
80172
|
+
if (sessionStatus && isTerminalSessionStatus(sessionStatus.type)) {
|
|
80173
|
+
await this.tryCompleteTask(task, `polling (terminal session status: ${sessionStatus.type})`);
|
|
80174
|
+
continue;
|
|
80175
|
+
}
|
|
80176
|
+
if (sessionStatus && sessionStatus.type !== "idle") {
|
|
80177
|
+
log("[background-agent] Unknown session status, treating as potentially idle:", {
|
|
80178
|
+
taskId: task.id,
|
|
80179
|
+
sessionID,
|
|
80180
|
+
sessionStatus: sessionStatus.type
|
|
80181
|
+
});
|
|
80182
|
+
}
|
|
80009
80183
|
const completionSource = sessionStatus?.type === "idle" ? "polling (idle status)" : "polling (session gone from status)";
|
|
80010
80184
|
const hasValidOutput = await this.validateSessionHasOutput(sessionID);
|
|
80011
80185
|
if (!hasValidOutput) {
|
|
@@ -97783,10 +97957,7 @@ function createPluginInterface(args) {
|
|
|
97783
97957
|
const { ctx, pluginConfig, firstMessageVariantGate, managers, hooks: hooks2, tools } = args;
|
|
97784
97958
|
return {
|
|
97785
97959
|
tool: tools,
|
|
97786
|
-
"chat.params":
|
|
97787
|
-
const handler = createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort });
|
|
97788
|
-
await handler(input, output);
|
|
97789
|
-
},
|
|
97960
|
+
"chat.params": createChatParamsHandler({ anthropicEffort: hooks2.anthropicEffort }),
|
|
97790
97961
|
"chat.headers": createChatHeadersHandler({ ctx }),
|
|
97791
97962
|
"chat.message": createChatMessageHandler3({
|
|
97792
97963
|
ctx,
|
|
@@ -97813,7 +97984,10 @@ function createPluginInterface(args) {
|
|
|
97813
97984
|
"tool.execute.after": createToolExecuteAfterHandler3({
|
|
97814
97985
|
ctx,
|
|
97815
97986
|
hooks: hooks2
|
|
97816
|
-
})
|
|
97987
|
+
}),
|
|
97988
|
+
"tool.definition": async (input, output) => {
|
|
97989
|
+
await hooks2.todoDescriptionOverride?.["tool.definition"]?.(input, output);
|
|
97990
|
+
}
|
|
97817
97991
|
};
|
|
97818
97992
|
}
|
|
97819
97993
|
|