daedalus-cli 3.63.2 → 3.65.0
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 +22 -3
- package/dist/agents/orchestrator.d.ts +3 -6
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +39 -843
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/agents/subagent-runner.d.ts +18 -0
- package/dist/agents/subagent-runner.d.ts.map +1 -0
- package/dist/agents/subagent-runner.js +231 -0
- package/dist/agents/subagent-runner.js.map +1 -0
- package/dist/agents/subagent-runner.test.d.ts +2 -0
- package/dist/agents/subagent-runner.test.d.ts.map +1 -0
- package/dist/agents/subagent-runner.test.js +74 -0
- package/dist/agents/subagent-runner.test.js.map +1 -0
- package/dist/agents/task-delegator.d.ts +28 -0
- package/dist/agents/task-delegator.d.ts.map +1 -0
- package/dist/agents/task-delegator.js +542 -0
- package/dist/agents/task-delegator.js.map +1 -0
- package/dist/agents/task-delegator.test.d.ts +2 -0
- package/dist/agents/task-delegator.test.d.ts.map +1 -0
- package/dist/agents/task-delegator.test.js +83 -0
- package/dist/agents/task-delegator.test.js.map +1 -0
- package/dist/agents/turn-guards.d.ts +41 -0
- package/dist/agents/turn-guards.d.ts.map +1 -0
- package/dist/agents/turn-guards.js +325 -0
- package/dist/agents/turn-guards.js.map +1 -0
- package/dist/agents/turn-guards.test.d.ts +2 -0
- package/dist/agents/turn-guards.test.d.ts.map +1 -0
- package/dist/agents/turn-guards.test.js +123 -0
- package/dist/agents/turn-guards.test.js.map +1 -0
- package/dist/commands/agents.d.ts +5 -28
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +11 -1021
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/autopilot.d.ts +30 -0
- package/dist/commands/autopilot.d.ts.map +1 -0
- package/dist/commands/autopilot.js +372 -0
- package/dist/commands/autopilot.js.map +1 -0
- package/dist/commands/hunt.d.ts +3 -0
- package/dist/commands/hunt.d.ts.map +1 -0
- package/dist/commands/hunt.js +181 -0
- package/dist/commands/hunt.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +339 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/onboard.d.ts +3 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +109 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -7
- package/dist/config/index.js.map +1 -1
- package/dist/indexing/indexer.d.ts +1 -1
- package/dist/indexing/indexer.d.ts.map +1 -1
- package/dist/indexing/indexer.js +112 -89
- package/dist/indexing/indexer.js.map +1 -1
- package/dist/indexing/indexer.test.js +58 -2
- package/dist/indexing/indexer.test.js.map +1 -1
- package/dist/indexing/watcher.js +1 -1
- package/dist/indexing/watcher.js.map +1 -1
- package/dist/indexing/watcher.test.js +53 -16
- package/dist/indexing/watcher.test.js.map +1 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +32 -426
- package/dist/model.js.map +1 -1
- package/dist/router/fallback.test.js +67 -0
- package/dist/router/fallback.test.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +23 -2
- package/dist/router/index.js.map +1 -1
- package/dist/session/manager.d.ts +2 -2
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +9 -6
- package/dist/session/manager.js.map +1 -1
- package/dist/session/manager.test.js +5 -0
- package/dist/session/manager.test.js.map +1 -1
- package/dist/session/sqlite.d.ts +4 -0
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +17 -0
- package/dist/session/sqlite.js.map +1 -1
- package/dist/session/sqlite.test.js +10 -1
- package/dist/session/sqlite.test.js.map +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.test.js +19 -0
- package/dist/types.test.js.map +1 -1
- package/package.json +2 -2
package/dist/model.js
CHANGED
|
@@ -4,8 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { BUILTIN_TOOLS, POWER_TOOLS } from './tools/definitions.js';
|
|
5
5
|
import { executeToolCalls } from './tools/executor.js';
|
|
6
6
|
import { getSessionTodos } from './tools/builtin/todo.js';
|
|
7
|
-
import {
|
|
8
|
-
import { ReadStallDetector,
|
|
7
|
+
import { ClaimLedger, RUNTIME_EXERCISE_RE } from './agents/completion-guard.js';
|
|
8
|
+
import { ReadStallDetector, DivergenceDetector, isStaleReadFailure } from './agents/loop-guards.js';
|
|
9
|
+
import { checkTurnCompletionGuards } from './agents/turn-guards.js';
|
|
9
10
|
import { mcpRegistry } from './tools/mcp/registry.js';
|
|
10
11
|
import { DaedalusSpinner } from './tools/daedalus-spinner.js';
|
|
11
12
|
import { calculateSessionTokens, pruneMessages } from './session/tokens.js';
|
|
@@ -532,437 +533,42 @@ export function createModelFunctions(deps) {
|
|
|
532
533
|
}
|
|
533
534
|
const cleanContent = stripToolCallMarkup(fullContent);
|
|
534
535
|
lastContent = cleanContent;
|
|
535
|
-
// Divergence guard: if this assistant block is near-identical to one already emitted
|
|
536
|
-
// this turn (and it produced no new tool calls — i.e. it's just re-stating work), force
|
|
537
|
-
// the agent to either change the repo or report the blocker honestly. The FIRST repeat
|
|
538
|
-
// is a soft warning; a SECOND consecutive repeat is a runaway loop — halt the turn.
|
|
539
|
-
// NOTE: We exempt rewrites following guard warnings/file-missing errors and substantive
|
|
540
|
-
// review deliverables (audits/assessments), because the model is delivering the requested
|
|
541
|
-
// analysis and not trapped in an infinite modification loop.
|
|
542
|
-
const hasRecentGuardWarning = messages.slice(-6).some((m) => typeof m.content === 'string' &&
|
|
543
|
-
(m.content.startsWith('[SYSTEM WARNING]') || m.content.startsWith('[FILE-MISSING]') || m.content.startsWith('[CHECK]')));
|
|
544
|
-
const isReviewContent = isReviewDeliverable(cleanContent);
|
|
545
|
-
if (!hasRecentGuardWarning && !isReviewContent && toolCallArray.length === 0 && divergence.register(cleanContent)) {
|
|
546
|
-
const repeats = divergence.consecutiveRepeats;
|
|
547
|
-
if (repeats >= 2) {
|
|
548
|
-
openBlock();
|
|
549
|
-
closeAssistantBlock(cleanContent.length, Date.now() - overallStart, totalToolCalls, router.lastRoutedModel, turnUsageOut, router.lastRoutedTier, { showCost: config.ui?.showCost, selfCorrections: toolContext.selfCorrectionCount });
|
|
550
|
-
console.log(dim(`\n [STOP] Runaway loop: same output re-emitted ${repeats} times with no progress. Closing turn.`));
|
|
551
|
-
toolContext.maxTurnsCause = 'repeated identical output without progress (repetition guard tripped) — the agent emitted the same response multiple times';
|
|
552
|
-
return { content: `${cleanContent}\n\n[SELF-CORRECT] I repeated the same output ${repeats} times without making progress. I am stopping this turn rather than looping.`, toolCalls: [] };
|
|
553
|
-
}
|
|
554
|
-
console.log(dim(`\n [CHECK] Detected near-duplicate of prior output — not making progress.`));
|
|
555
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
556
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
557
|
-
messages.push({
|
|
558
|
-
role: 'user',
|
|
559
|
-
content: `[SYSTEM WARNING] This response is nearly identical to output you already produced this turn. You are looping on repeated text instead of making progress. Do NOT re-state completed work. Either (1) take a concrete next action (read the failing test, fix the code, verify), or (2) if you are blocked, report the blocker concisely and stop.`,
|
|
560
|
-
});
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
// Claim-grounding guard: flag a factual claim about a repo artifact the agent never
|
|
564
|
-
// inspected this session (no read/search/terminal on that file). Catches bare
|
|
565
|
-
// overclaims like "path/url are unused" or "rate limiting is already implemented"
|
|
566
|
-
// that have no tool evidence behind them. Forces the agent to verify before asserting.
|
|
567
|
-
if (toolCallArray.length === 0) {
|
|
568
|
-
const ungrounded = detectUngroundedClaim(cleanContent, claimLedger);
|
|
569
|
-
if (ungrounded) {
|
|
570
|
-
const key = `claim:${ungrounded}`;
|
|
571
|
-
if (toolContext.firedCompletionGuards?.has(key))
|
|
572
|
-
continue;
|
|
573
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(key);
|
|
574
|
-
console.log(dim(`\n [CHECK] Claim about ${ungrounded} is ungrounded (no inspection this session).`));
|
|
575
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
576
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
577
|
-
messages.push({
|
|
578
|
-
role: 'user',
|
|
579
|
-
content: ungroundedClaimWarning(ungrounded),
|
|
580
|
-
});
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
// Project-level claim guard (broadens #138): flag a claim that the project HAS a
|
|
585
|
-
// feature/dependency the agent never observed in tool output this session (no file
|
|
586
|
-
// read/search/terminal mentioning it). Catches the "review a codebase you never opened"
|
|
587
|
-
// failure where the agent invents helmet / circuit-breaker / favorites / glassmorphism
|
|
588
|
-
// etc. — none of which name a file, so the file-paired guard above cannot catch them.
|
|
589
536
|
if (toolCallArray.length === 0) {
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(key);
|
|
614
|
-
console.log(dim(`\n [CHECK] Claim that "${negClaim}" is missing is ungrounded (no search/list run this session).`));
|
|
615
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
616
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
617
|
-
messages.push({
|
|
618
|
-
role: 'user',
|
|
619
|
-
content: negativeExistenceWarning(negClaim),
|
|
620
|
-
});
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (toolCallArray.length === 0) {
|
|
625
|
-
// Only treat this as a "planned but omitted JSON" failure when the model
|
|
626
|
-
// actually emitted tool-call markup (the structured <tool_call> block) but
|
|
627
|
-
// no parseable JSON. A bare mention of tool names in prose (e.g. an audit
|
|
628
|
-
// report saying "I ran read_file and terminal") must NOT trip this — that
|
|
629
|
-
// is normal narration, and forcing a retry loops on a finished report.
|
|
630
|
-
const narratedToolCalls = parseTextToolCalls(fullContent);
|
|
631
|
-
if (narratedToolCalls.length >= 1) {
|
|
632
|
-
console.log(dim(`\n [RETRY] Model planned tools but emitted no valid JSON. Re-issuing the request.`));
|
|
633
|
-
totalCompletionTokens += turnUsageOut ?? 0;
|
|
634
|
-
messages.push({
|
|
635
|
-
role: 'user',
|
|
636
|
-
content: `[SYSTEM WARNING] You emitted a <tool_call> block but it was not valid JSON. Please output the proper JSON array of tool calls now.`,
|
|
637
|
-
});
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
if (currentComplexity && process.env.DAEDALUS_DEBUG === 'true') {
|
|
641
|
-
console.log(dim(` [ROUTE] Task summary: start ${taskComplexity ?? 'n/a'} → end ${currentComplexity} | ${totalCompletionTokens + (turnUsageOut ?? 0)} output tokens | ${escalationCount} escalation(s)`));
|
|
642
|
-
}
|
|
643
|
-
// Hard guard: do not let the agent end the turn claiming whole-task
|
|
644
|
-
// completion while its todo list still has open items. A false "done"
|
|
645
|
-
// report would mislead an end user who trusts it. Force reconciliation.
|
|
646
|
-
const closingTodos = getSessionTodos(toolContext.sessionId);
|
|
647
|
-
if (closingTodos.length > 0 && detectFalseCompletion(cleanContent, closingTodos)) {
|
|
648
|
-
const remaining = closingTodos.filter((t) => t.status !== 'completed').length;
|
|
649
|
-
console.log(dim(`\n [CHECK] Verifying completion claim — ${remaining} todo(s) still open.`));
|
|
650
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
651
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
652
|
-
messages.push({
|
|
653
|
-
role: 'user',
|
|
654
|
-
content: falseCompletionWarning(remaining),
|
|
655
|
-
});
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
// Hard guard (on-disk): do not let the agent claim a fix/completion for a file it
|
|
659
|
-
// only ever reverted patches against this session and never successfully wrote.
|
|
660
|
-
// Catches the false "All issues resolved" report where the edit was attempted but
|
|
661
|
-
// reverted by the syntax guard and never actually landed on disk.
|
|
662
|
-
const falselyClaimed = detectFalseCompletionOnDisk(cleanContent, toolContext);
|
|
663
|
-
if (falselyClaimed) {
|
|
664
|
-
console.log(dim(`\n [CHECK] Verifying completion claim — no successful patch to ${falselyClaimed} this session (only reverts).`));
|
|
665
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
666
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
667
|
-
messages.push({
|
|
668
|
-
role: 'user',
|
|
669
|
-
content: `[SYSTEM WARNING] You claimed a fix/completion involving ${falselyClaimed}, but this session has NO successful patch to that file — only patches the syntax guard reverted. Reconcile with disk reality: either (1) actually apply and verify the change (run build/test and confirm it on disk), or (2) report the blocker honestly instead of claiming it is done. Do NOT report completion for changes that were not written.`,
|
|
670
|
-
});
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
// Hard guard (scope): do not let a closing summary present a deliverable checklist
|
|
674
|
-
// ("Task 1 ... Task 3 ...") as complete while the todo list still has open items. That
|
|
675
|
-
// is a scope over-statement (e.g. relabeling a partial feature as fully shipped). Force
|
|
676
|
-
// the summary to be scoped to what actually landed or honestly mark the partial items.
|
|
677
|
-
const scopeTodos = getSessionTodos(toolContext.sessionId);
|
|
678
|
-
if (scopeTodos.length > 0 && isScopeOverstatedSummary(cleanContent, scopeTodos)) {
|
|
679
|
-
const remaining = scopeTodos.filter((t) => t.status !== 'completed').length;
|
|
680
|
-
console.log(dim(`\n [CHECK] Verifying completion claim — summary enumerates tasks as done but ${remaining} todo(s) still open.`));
|
|
681
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
682
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
683
|
-
messages.push({
|
|
684
|
-
role: 'user',
|
|
685
|
-
content: scopeOverstatementWarning(remaining),
|
|
686
|
-
});
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
// Hard guard (unsubstantiated progress): do not let a turn end with a deliverable
|
|
690
|
-
// checklist of completed work (✅ lists / numbered / bulleted achievement lists) that
|
|
691
|
-
// has no task tracker reconciling it AND no on-disk verification per claim. This is the
|
|
692
|
-
// "Current State Analysis: ✅ X / ✅ Y / Key Improvements Made: 1... 2... 3..." shape that
|
|
693
|
-
// slips past the todo-gated guards whenever the agent didn't use the todo tool. Force the
|
|
694
|
-
// agent to reconcile each claimed item with disk reality before concluding.
|
|
695
|
-
if (isUnsubstantiatedProgressReport(cleanContent)) {
|
|
696
|
-
const key = 'unsubstantiated-progress';
|
|
697
|
-
if (!toolContext.firedCompletionGuards?.has(key)) {
|
|
698
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(key);
|
|
699
|
-
const itemCount = countAchievementItems(cleanContent);
|
|
700
|
-
console.log(dim(`\n [CHECK] Verifying completion claim — ${itemCount} deliverables enumerated as done without a reconciling task list or per-item verification.`));
|
|
701
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
702
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
703
|
-
messages.push({
|
|
704
|
-
role: 'user',
|
|
705
|
-
content: unsubstantiatedProgressWarning(itemCount),
|
|
706
|
-
});
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
// Inspection-before-review gate: when the session is a "review the project" request
|
|
711
|
-
// and the agent produces a multi-section review deliverable with ZERO file observations
|
|
712
|
-
// this session, it is reviewing a codebase it never opened. Halt the turn and force a
|
|
713
|
-
// real inspection instead of letting a fabricated review loop. (Catches the runaway
|
|
714
|
-
// "fabricated review from a single passing typecheck" failure at the first deliverable.)
|
|
715
|
-
if (isReviewTask(userTask) && isReviewDeliverable(cleanContent) && claimLedger.totalObservations === 0) {
|
|
716
|
-
openBlock();
|
|
717
|
-
closeAssistantBlock(cleanContent.length, Date.now() - overallStart, totalToolCalls, router.lastRoutedModel, turnUsageOut, router.lastRoutedTier, { showCost: config.ui?.showCost, selfCorrections: toolContext.selfCorrectionCount });
|
|
718
|
-
toolContext.maxTurnsCause = 'produced a review deliverable with zero file inspections this session (review-without-inspection guard)';
|
|
719
|
-
console.log(dim(`\n [STOP] Review produced with zero file inspections this session — halting.`));
|
|
720
|
-
return { content: `${cleanContent}\n\n[SELF-CORRECT] I described the project's architecture/features but have not inspected a single file this session. I am stopping rather than fabricating a review. I should read the code before reviewing.`, toolCalls: [] };
|
|
721
|
-
}
|
|
722
|
-
// Fix 1: Upgraded review gate — reading only walkthrough.md / README.md does NOT
|
|
723
|
-
// satisfy the inspection requirement. Require at least MIN_SOURCE_READS real source
|
|
724
|
-
// files (.ts/.js/.py/etc.) before a multi-section review deliverable is allowed.
|
|
725
|
-
if (isReviewTask(userTask) && isReviewWithoutSourceInspection(cleanContent, claimLedger)) {
|
|
726
|
-
const srcCount = claimLedger.sourceFileObservations;
|
|
727
|
-
console.log(dim(`\n [CHECK] Review deliverable produced after reading only ${srcCount} source file(s) — insufficient inspection.`));
|
|
728
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
729
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
730
|
-
messages.push({
|
|
731
|
-
role: 'user',
|
|
732
|
-
content: reviewWithoutSourceInspectionWarning(srcCount),
|
|
733
|
-
});
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
// Audit-hallucination hardening: a review deliverable that makes structural claims
|
|
737
|
-
// (architecture / type-safety / entry point / "no any leakage" / "patch tool used
|
|
738
|
-
// for all modifications" / etc.) but cites NO source location must be challenged.
|
|
739
|
-
// Vague, uncited praise is exactly what a self-audit fabricates; force a file:line or
|
|
740
|
-
// an explicit "this is a high-level impression" framing. Gate on the OUTPUT shape
|
|
741
|
-
// (isReviewDeliverable) so it fires whenever a review-shaped report is emitted,
|
|
742
|
-
// regardless of how the user phrased the request.
|
|
743
|
-
// NOTE: this guard must NOT use a permanent de-dup key. A model can dodge a one-shot
|
|
744
|
-
// warning by adding cosmetic labels ("... (Verified)") that are NOT citations — the
|
|
745
|
-
// guard would see it already fired and stay silent. Instead it re-fires whenever the
|
|
746
|
-
// content still makes uncited structural claims; CITATION_RE naturally stops it once
|
|
747
|
-
// a real file:line appears. Hard-cap retries so a stubborn model cannot loop forever.
|
|
748
|
-
if (isReviewTask(userTask) || isReviewDeliverable(cleanContent)) {
|
|
749
|
-
const archTerm = isUncitedArchClaim(cleanContent);
|
|
750
|
-
if (archTerm) {
|
|
751
|
-
if ((toolContext.archGuardHits ?? 0) >= 3) {
|
|
752
|
-
// Give up gracefully: the model refuses to cite; let it finish but mark the cause.
|
|
753
|
-
toolContext.maxTurnsCause = 'repeated uncited architectural claims despite 3 citation warnings (audit guard)';
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
toolContext.archGuardHits = (toolContext.archGuardHits ?? 0) + 1;
|
|
757
|
-
console.log(dim(`\n [CHECK] Review makes uncited structural claim "${archTerm}" (no file:line).`));
|
|
758
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
759
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
760
|
-
messages.push({
|
|
761
|
-
role: 'user',
|
|
762
|
-
content: uncitedArchClaimWarning(archTerm),
|
|
763
|
-
});
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
// Layer-1 citation validator (audit hardening): citations must point at REAL code, not
|
|
769
|
-
// just exist as text. If the report cites file:NN anchors that fail verification
|
|
770
|
-
// (file missing, line out of range, or the claimed symbol absent from the cited line),
|
|
771
|
-
// force a correction. This is what turns "citations required" into "citations checked" —
|
|
772
|
-
// the difference between a real audit and decorated prose. Gated on the same review shape.
|
|
773
|
-
if (isReviewTask(userTask) || isReviewDeliverable(cleanContent)) {
|
|
774
|
-
const citationFails = validateCitations(cleanContent, { readLines });
|
|
775
|
-
if (citationFails.length > 0) {
|
|
776
|
-
const key = 'citation-validation';
|
|
777
|
-
if (!toolContext.firedCompletionGuards?.has(key)) {
|
|
778
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(key);
|
|
779
|
-
console.log(dim(`\n [CHECK] Review cites ${citationFails.length} source location(s) that do not check out against the codebase.`));
|
|
780
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
781
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
782
|
-
messages.push({
|
|
783
|
-
role: 'user',
|
|
784
|
-
content: citationValidationWarning(citationFails),
|
|
785
|
-
});
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
// Layer-1b prose file-reference validator (audit hardening): catches referenced files
|
|
790
|
-
// by NAME (the format audits actually use) that Layer 1's inline `path:NN` scan misses —
|
|
791
|
-
// both a named file that does not exist and the "no test file exists" false-negative
|
|
792
|
-
// class that previously slipped through. Soft-gated by the same firedCompletionGuards key.
|
|
793
|
-
const proseFails = validateProseReferences(cleanContent, { readLines, fileExists });
|
|
794
|
-
if (proseFails.length > 0) {
|
|
795
|
-
const key = 'prose-ref-validation';
|
|
796
|
-
if (!toolContext.firedCompletionGuards?.has(key)) {
|
|
797
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(key);
|
|
798
|
-
console.log(dim(`\n [CHECK] Review references ${proseFails.length} file(s) whose existence does not check out against the codebase.`));
|
|
799
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
800
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
801
|
-
messages.push({
|
|
802
|
-
role: 'user',
|
|
803
|
-
content: proseRefWarning(proseFails),
|
|
804
|
-
});
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
// Layer-2 semantic judge (audit hardening): Layer 1 confirms a citation points at a real
|
|
809
|
-
// symbol on a real line. Layer 2 asks ONE model call to judge whether the PROSE CLAIM actually
|
|
810
|
-
// follows from the cited code — catching a correct anchor with a wrong interpretation. Batched
|
|
811
|
-
// into a single completion per audit report (not per citation) to keep cost bounded. Soft guard:
|
|
812
|
-
// capped at 3 challenges so a misbehaving judge cannot trap the turn in a loop. If the judge
|
|
813
|
-
// call itself fails, we degrade to Layer 1 only (do NOT block the audit on a judge error).
|
|
814
|
-
if ((isReviewTask(userTask) || isReviewDeliverable(cleanContent)) && (toolContext.judgeGuardHits ?? 0) < 3) {
|
|
815
|
-
const claims = collectCitationClaims(cleanContent, { readLines }, 8);
|
|
816
|
-
if (claims.length > 0) {
|
|
817
|
-
try {
|
|
818
|
-
const judgePrompt = buildJudgePrompt(claims);
|
|
819
|
-
const jr = await router.chat.completions.create({
|
|
820
|
-
model: config.modelOverride || 'auto',
|
|
821
|
-
messages: [{ role: 'user', content: judgePrompt }],
|
|
822
|
-
temperature: 0,
|
|
823
|
-
max_tokens: 1024,
|
|
824
|
-
});
|
|
825
|
-
const judgeRaw = messageText(jr.choices?.[0]?.message?.content ?? '');
|
|
826
|
-
const verdicts = parseJudgeResponse(judgeRaw, claims);
|
|
827
|
-
const unsupported = verdicts.filter((v) => !v.supported);
|
|
828
|
-
if (unsupported.length > 0) {
|
|
829
|
-
toolContext.judgeGuardHits = (toolContext.judgeGuardHits ?? 0) + 1;
|
|
830
|
-
console.log(dim(`\n [CHECK] Semantic judge found ${unsupported.length} claim(s) not supported by cited code.`));
|
|
831
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
832
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
833
|
-
messages.push({
|
|
834
|
-
role: 'user',
|
|
835
|
-
content: judgeClaimWarning(unsupported),
|
|
836
|
-
});
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
catch (judgeErr) {
|
|
841
|
-
// Judge unavailable (offline / rate-limited / parse failure): degrade to Layer 1 only.
|
|
842
|
-
if (process.env.DAEDALUS_DEBUG === 'true') {
|
|
843
|
-
console.log(dim(` [judge] Layer-2 verification skipped (${String(judgeErr)}).`));
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
// Fix 3: Test-count claim without any real npm test run this session. Fires when the
|
|
850
|
-
// agent asserts a specific passing count (e.g. "9 tests passing") but lastVerifyPassCount
|
|
851
|
-
// is undefined (no real test run was observed). Prevents walkthrough-sourced count invention.
|
|
852
|
-
const noRunClaimed = claimedTestCountWithoutRun(cleanContent, toolContext.lastVerifyPassCount);
|
|
853
|
-
if (noRunClaimed) {
|
|
854
|
-
console.log(dim(`\n [CHECK] Test-count claim ("${noRunClaimed} passing") made with no real npm test run this session.`));
|
|
855
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
856
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
857
|
-
messages.push({
|
|
858
|
-
role: 'user',
|
|
859
|
-
content: claimedTestCountWithoutRunWarning(noRunClaimed),
|
|
860
|
-
});
|
|
861
|
-
continue;
|
|
537
|
+
const guardResult = await checkTurnCompletionGuards({
|
|
538
|
+
cleanContent,
|
|
539
|
+
fullContent,
|
|
540
|
+
userTask,
|
|
541
|
+
messages,
|
|
542
|
+
toolContext,
|
|
543
|
+
router,
|
|
544
|
+
config,
|
|
545
|
+
claimLedger,
|
|
546
|
+
readStall,
|
|
547
|
+
divergence,
|
|
548
|
+
readLines,
|
|
549
|
+
fileExists,
|
|
550
|
+
verifyBreakerTrippedThisTurn,
|
|
551
|
+
verifyBreakerTrippedLastTurn,
|
|
552
|
+
currentComplexity,
|
|
553
|
+
taskComplexity,
|
|
554
|
+
totalCompletionTokens,
|
|
555
|
+
turnUsageOut,
|
|
556
|
+
escalationCount,
|
|
557
|
+
});
|
|
558
|
+
if (guardResult.updateVerifyBreaker) {
|
|
559
|
+
toolContext.verifyBreakerTrippedLastTurn = verifyBreakerTrippedThisTurn || verifyBreakerTrippedLastTurn;
|
|
862
560
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// functions — the graded run built an endpoint, reported "wired in" after tests passed,
|
|
867
|
-
// and it was actually broken until the user exercised it. Forces real verification.
|
|
868
|
-
if (detectUngroundedWorksClaim(cleanContent, claimLedger)) {
|
|
869
|
-
// Debounce: the same "works/verified without a probe" claim must not be
|
|
870
|
-
// re-flagged every turn — that trains the user/model to ignore the guard.
|
|
871
|
-
// Fire once per session (recorded in firedCompletionGuards), then stop; the
|
|
872
|
-
// divergence/repetition guard closes any remaining loop.
|
|
873
|
-
const worksKey = 'works-claim';
|
|
874
|
-
if (toolContext.firedCompletionGuards?.has(worksKey)) {
|
|
875
|
-
continue;
|
|
561
|
+
if (guardResult.status === 'continue') {
|
|
562
|
+
if (guardResult.addedTokens) {
|
|
563
|
+
totalCompletionTokens += guardResult.addedTokens;
|
|
876
564
|
}
|
|
877
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(worksKey);
|
|
878
|
-
console.log(dim(`\n [CHECK] "Works/verified" claim made with no live runtime probe (curl/HTTP/integration test) this session.`));
|
|
879
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
880
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
881
|
-
messages.push({
|
|
882
|
-
role: 'user',
|
|
883
|
-
content: ungroundedWorksWarning(),
|
|
884
|
-
});
|
|
885
565
|
continue;
|
|
886
566
|
}
|
|
887
|
-
|
|
888
|
-
// built artifact (node dist/cli.js, npm run start, a runtime probe) exited non-zero with a
|
|
889
|
-
// hard error this session, yet it still claims the project "works" / "CLI executed" /
|
|
890
|
-
// "verified" / "build+tests pass". A failed run cannot be reported as success. Block once
|
|
891
|
-
// per session and force a real re-run or an honest blocker report. This is the gap that
|
|
892
|
-
// let the greenfield run declare "Project Complete ✅" from a crashed CLI.
|
|
893
|
-
if (toolContext.lastRuntimeFailure && (detectUngroundedWorksClaim(cleanContent, claimLedger) || isGreenBuildTestClaim(cleanContent))) {
|
|
894
|
-
const rfKey = 'runtime-failure';
|
|
895
|
-
if (!toolContext.firedCompletionGuards?.has(rfKey)) {
|
|
896
|
-
(toolContext.firedCompletionGuards ??= new Set()).add(rfKey);
|
|
897
|
-
const rf = toolContext.lastRuntimeFailure;
|
|
898
|
-
console.log(dim(`\n [CHECK] Completion claim conflicts with a FAILED run this session — \`${rf.command}\` exited non-zero.`));
|
|
899
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
900
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
901
|
-
messages.push({
|
|
902
|
-
role: 'user',
|
|
903
|
-
content: `[SYSTEM WARNING] You reported the project works / the CLI ran / build+tests pass, but a terminal run you executed THIS session FAILED: ` +
|
|
904
|
-
`\`${rf.command}\` exited non-zero with: ${rf.error}. A failed run cannot be reported as a success. ` +
|
|
905
|
-
`Either (1) actually re-run the command and confirm a clean exit (code 0) before claiming it works, ` +
|
|
906
|
-
`or (2) report the blocker honestly (paste the error). Do NOT claim green/working from a run that errored.`,
|
|
907
|
-
});
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
// Layer B: idle re-read breaker. If the turn spent its budget re-reading the same
|
|
912
|
-
// file (the "fix was already present" spin) with no edit, force it to report the
|
|
913
|
-
// blocker honestly instead of looping. Close the turn with a concise note.
|
|
914
|
-
if (readStall.stalled) {
|
|
567
|
+
if (guardResult.status === 'halt') {
|
|
915
568
|
openBlock();
|
|
916
569
|
closeAssistantBlock(cleanContent.length, Date.now() - overallStart, totalToolCalls, router.lastRoutedModel, turnUsageOut, router.lastRoutedTier, { showCost: config.ui?.showCost, selfCorrections: toolContext.selfCorrectionCount });
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
toolContext.maxTurnsCause = `stuck re-reading the same file without making changes (idle re-read guard, ${readStall.readCount} reads)`;
|
|
920
|
-
return { content: `${cleanContent}\n\n[SELF-CORRECT] I re-read the same file ${readStall.readCount} times without making changes — the change is likely already present on disk. Report the actual on-disk state to the user rather than continuing to read.`, toolCalls: [] };
|
|
921
|
-
}
|
|
922
|
-
// Layer C: verification-claim guard. Block a green build/test claim when the verify
|
|
923
|
-
// command tripped the circuit breaker this turn (or last turn) and no fresh
|
|
924
|
-
// successful run cleared it — forces a real re-run or an honest blocker report.
|
|
925
|
-
if (isGreenBuildTestClaim(cleanContent) && (verifyBreakerTrippedThisTurn || verifyBreakerTrippedLastTurn)) {
|
|
926
|
-
console.log(dim(`\n [CHECK] Verifying completion claim — build/test command tripped the circuit breaker; no fresh successful run observed.`));
|
|
927
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
928
|
-
toolContext.verifyBreakerTrippedLastTurn = true;
|
|
929
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
930
|
-
messages.push({
|
|
931
|
-
role: 'user',
|
|
932
|
-
content: `[SYSTEM WARNING] You reported the build/tests pass, but the verify command tripped the circuit breaker this session (no progress) and no fresh successful run was observed. Do NOT claim green without re-running the command and seeing a real pass. Either (1) run \`npm run build && npm run test\` again and confirm real output, or (2) report the blocker honestly (e.g. the command hung / was blocked).`,
|
|
933
|
-
});
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
toolContext.verifyBreakerTrippedLastTurn = verifyBreakerTrippedThisTurn || verifyBreakerTrippedLastTurn;
|
|
937
|
-
// Layer D: fabricated test-count guard. If the assistant's final summary asserts a
|
|
938
|
-
// specific passing-test count that disagrees with the last REAL `npm test` output,
|
|
939
|
-
// reject it and force the true number. Prevents inventing "21 tests passing" when the
|
|
940
|
-
// run actually reported 9.
|
|
941
|
-
const testCorrection = fabricatedTestCountCorrection(cleanContent, toolContext.lastVerifyPassCount);
|
|
942
|
-
if (testCorrection) {
|
|
943
|
-
console.log(dim(`\n [CHECK] Verifying test-count claim — summary count disagrees with last real test run.`));
|
|
944
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
945
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
946
|
-
messages.push({
|
|
947
|
-
role: 'user',
|
|
948
|
-
content: testCorrection,
|
|
949
|
-
});
|
|
950
|
-
continue;
|
|
951
|
-
}
|
|
952
|
-
// Layer D2: green-state / clean-state claim vs last real verify run. Catches the
|
|
953
|
-
// subset-omission overclaim: a true passing count ("9 validation tests passing")
|
|
954
|
-
// slips past the count-fabrication guard, but the overall `npm test` was RED. If the
|
|
955
|
-
// agent asserts tests/build pass or a clean state while the most recent actual verify
|
|
956
|
-
// run this session FAILED, force a re-run or an honest report of what actually failed.
|
|
957
|
-
if (isGreenStateClaim(cleanContent) && toolContext.lastVerifyPassed === false) {
|
|
958
|
-
console.log(dim(`\n [CHECK] Verifying green-state claim — last real verify run this session FAILED.`));
|
|
959
|
-
toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
|
|
960
|
-
messages.push({ role: 'assistant', content: cleanContent });
|
|
961
|
-
messages.push({
|
|
962
|
-
role: 'user',
|
|
963
|
-
content: greenStateWarning(),
|
|
964
|
-
});
|
|
965
|
-
continue;
|
|
570
|
+
toolContext.maxTurnsCause = guardResult.maxTurnsCause;
|
|
571
|
+
return { content: guardResult.content, toolCalls: [] };
|
|
966
572
|
}
|
|
967
573
|
openBlock();
|
|
968
574
|
writeAssistantChunk(cleanContent);
|