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.
Files changed (87) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/dist/agents/orchestrator.d.ts +3 -6
  3. package/dist/agents/orchestrator.d.ts.map +1 -1
  4. package/dist/agents/orchestrator.js +39 -843
  5. package/dist/agents/orchestrator.js.map +1 -1
  6. package/dist/agents/subagent-runner.d.ts +18 -0
  7. package/dist/agents/subagent-runner.d.ts.map +1 -0
  8. package/dist/agents/subagent-runner.js +231 -0
  9. package/dist/agents/subagent-runner.js.map +1 -0
  10. package/dist/agents/subagent-runner.test.d.ts +2 -0
  11. package/dist/agents/subagent-runner.test.d.ts.map +1 -0
  12. package/dist/agents/subagent-runner.test.js +74 -0
  13. package/dist/agents/subagent-runner.test.js.map +1 -0
  14. package/dist/agents/task-delegator.d.ts +28 -0
  15. package/dist/agents/task-delegator.d.ts.map +1 -0
  16. package/dist/agents/task-delegator.js +542 -0
  17. package/dist/agents/task-delegator.js.map +1 -0
  18. package/dist/agents/task-delegator.test.d.ts +2 -0
  19. package/dist/agents/task-delegator.test.d.ts.map +1 -0
  20. package/dist/agents/task-delegator.test.js +83 -0
  21. package/dist/agents/task-delegator.test.js.map +1 -0
  22. package/dist/agents/turn-guards.d.ts +41 -0
  23. package/dist/agents/turn-guards.d.ts.map +1 -0
  24. package/dist/agents/turn-guards.js +325 -0
  25. package/dist/agents/turn-guards.js.map +1 -0
  26. package/dist/agents/turn-guards.test.d.ts +2 -0
  27. package/dist/agents/turn-guards.test.d.ts.map +1 -0
  28. package/dist/agents/turn-guards.test.js +123 -0
  29. package/dist/agents/turn-guards.test.js.map +1 -0
  30. package/dist/commands/agents.d.ts +5 -28
  31. package/dist/commands/agents.d.ts.map +1 -1
  32. package/dist/commands/agents.js +11 -1021
  33. package/dist/commands/agents.js.map +1 -1
  34. package/dist/commands/autopilot.d.ts +30 -0
  35. package/dist/commands/autopilot.d.ts.map +1 -0
  36. package/dist/commands/autopilot.js +372 -0
  37. package/dist/commands/autopilot.js.map +1 -0
  38. package/dist/commands/hunt.d.ts +3 -0
  39. package/dist/commands/hunt.d.ts.map +1 -0
  40. package/dist/commands/hunt.js +181 -0
  41. package/dist/commands/hunt.js.map +1 -0
  42. package/dist/commands/mcp.d.ts +3 -0
  43. package/dist/commands/mcp.d.ts.map +1 -0
  44. package/dist/commands/mcp.js +339 -0
  45. package/dist/commands/mcp.js.map +1 -0
  46. package/dist/commands/onboard.d.ts +3 -0
  47. package/dist/commands/onboard.d.ts.map +1 -0
  48. package/dist/commands/onboard.js +109 -0
  49. package/dist/commands/onboard.js.map +1 -0
  50. package/dist/config/index.d.ts.map +1 -1
  51. package/dist/config/index.js +2 -7
  52. package/dist/config/index.js.map +1 -1
  53. package/dist/indexing/indexer.d.ts +1 -1
  54. package/dist/indexing/indexer.d.ts.map +1 -1
  55. package/dist/indexing/indexer.js +112 -89
  56. package/dist/indexing/indexer.js.map +1 -1
  57. package/dist/indexing/indexer.test.js +58 -2
  58. package/dist/indexing/indexer.test.js.map +1 -1
  59. package/dist/indexing/watcher.js +1 -1
  60. package/dist/indexing/watcher.js.map +1 -1
  61. package/dist/indexing/watcher.test.js +53 -16
  62. package/dist/indexing/watcher.test.js.map +1 -1
  63. package/dist/model.d.ts.map +1 -1
  64. package/dist/model.js +32 -426
  65. package/dist/model.js.map +1 -1
  66. package/dist/router/fallback.test.js +67 -0
  67. package/dist/router/fallback.test.js.map +1 -1
  68. package/dist/router/index.d.ts.map +1 -1
  69. package/dist/router/index.js +23 -2
  70. package/dist/router/index.js.map +1 -1
  71. package/dist/session/manager.d.ts +2 -2
  72. package/dist/session/manager.d.ts.map +1 -1
  73. package/dist/session/manager.js +9 -6
  74. package/dist/session/manager.js.map +1 -1
  75. package/dist/session/manager.test.js +5 -0
  76. package/dist/session/manager.test.js.map +1 -1
  77. package/dist/session/sqlite.d.ts +4 -0
  78. package/dist/session/sqlite.d.ts.map +1 -1
  79. package/dist/session/sqlite.js +17 -0
  80. package/dist/session/sqlite.js.map +1 -1
  81. package/dist/session/sqlite.test.js +10 -1
  82. package/dist/session/sqlite.test.js.map +1 -1
  83. package/dist/types.d.ts +17 -5
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/types.test.js +19 -0
  86. package/dist/types.test.js.map +1 -1
  87. 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 { detectFalseCompletion, falseCompletionWarning, detectFalseCompletionOnDisk, isScopeOverstatedSummary, scopeOverstatementWarning, isUnsubstantiatedProgressReport, unsubstantiatedProgressWarning, countAchievementItems, ClaimLedger, detectUngroundedClaim, ungroundedClaimWarning, isGreenStateClaim, greenStateWarning, isUngroundedProjectClaim, ungroundedProjectClaimWarning, isNegativeExistenceClaim, negativeExistenceWarning, isReviewTask, isReviewDeliverable, isReviewWithoutSourceInspection, reviewWithoutSourceInspectionWarning, claimedTestCountWithoutRun, claimedTestCountWithoutRunWarning, detectUngroundedWorksClaim, ungroundedWorksWarning, isUncitedArchClaim, uncitedArchClaimWarning, RUNTIME_EXERCISE_RE, validateCitations, citationValidationWarning, collectCitationClaims, buildJudgePrompt, parseJudgeResponse, judgeClaimWarning, validateProseReferences, proseRefWarning } from './agents/completion-guard.js';
8
- import { ReadStallDetector, isGreenBuildTestClaim, fabricatedTestCountCorrection, DivergenceDetector, isStaleReadFailure } from './agents/loop-guards.js';
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 projClaim = isUngroundedProjectClaim(cleanContent, claimLedger);
591
- if (projClaim) {
592
- const key = `proj:${projClaim}`;
593
- if (toolContext.firedCompletionGuards?.has(key))
594
- continue;
595
- (toolContext.firedCompletionGuards ??= new Set()).add(key);
596
- console.log(dim(`\n [CHECK] Project claim about "${projClaim}" is ungrounded (never observed this session).`));
597
- toolContext.selfCorrectionCount = (toolContext.selfCorrectionCount ?? 0) + 1;
598
- messages.push({ role: 'assistant', content: cleanContent });
599
- messages.push({
600
- role: 'user',
601
- content: ungroundedProjectClaimWarning(projClaim),
602
- });
603
- continue;
604
- }
605
- // Negative-existence guard: the agent asserts a config/dependency/file is
606
- // MISSING/ABSENT without ever running a search/list/grep this session. Asserting
607
- // absence requires the same grounding as asserting presence.
608
- const negClaim = isNegativeExistenceClaim(cleanContent, claimLedger);
609
- if (negClaim) {
610
- const key = `neg:${negClaim}`;
611
- if (toolContext.firedCompletionGuards?.has(key))
612
- continue;
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
- // Runtime-exercise guard: block a "feature is wired in / working / verified" claim
864
- // when no live integration probe (curl/HTTP/integration test) was recorded this
865
- // session. A green typecheck/unit-test suite does NOT prove a newly-wired integration
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
- // Layer A2: runtime-failure-aware completion block. The agent's OWN terminal run of the
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
- console.log(dim(`\n [DONE] Idle re-read stall: same file read ${readStall.readCount} times consecutively with no edit. Closing turn.`));
918
- toolContext.verifyBreakerTrippedLastTurn = verifyBreakerTrippedThisTurn || verifyBreakerTrippedLastTurn;
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);