@gajae-code/coding-agent 0.11.11 → 0.12.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 +49 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +8 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +1 -1
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +8 -9
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +43 -7
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -114,8 +114,12 @@ export interface TruncationResult {
|
|
|
114
114
|
elidedLines?: number;
|
|
115
115
|
lastLinePartial?: boolean;
|
|
116
116
|
firstLineExceedsLimit?: boolean;
|
|
117
|
+
lastLineExceedsLimit?: boolean;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/** Direction vocabulary used by callers that select which end of content to retain. */
|
|
121
|
+
export type TruncationDirection = "head" | "last" | "both";
|
|
122
|
+
|
|
119
123
|
export interface TruncationOptions {
|
|
120
124
|
/** Maximum number of lines (default: 3000) */
|
|
121
125
|
maxLines?: number;
|
|
@@ -131,6 +135,8 @@ export interface TruncationOptions {
|
|
|
131
135
|
* window receives `maxLines - maxHeadLines`. Default `floor(maxLines/2)`.
|
|
132
136
|
*/
|
|
133
137
|
maxHeadLines?: number;
|
|
138
|
+
/** Direction to dispatch through truncateContent (head, last, or both). */
|
|
139
|
+
direction?: TruncationDirection;
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
/** Result from byte-level truncation helpers. */
|
|
@@ -452,6 +458,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
452
458
|
outputBytes: tail.bytes,
|
|
453
459
|
lastLinePartial: true,
|
|
454
460
|
firstLineExceedsLimit: false,
|
|
461
|
+
lastLineExceedsLimit: true,
|
|
455
462
|
};
|
|
456
463
|
}
|
|
457
464
|
break;
|
|
@@ -474,6 +481,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
474
481
|
outputBytes: tail.bytes,
|
|
475
482
|
lastLinePartial: true,
|
|
476
483
|
firstLineExceedsLimit: false,
|
|
484
|
+
lastLineExceedsLimit: true,
|
|
477
485
|
};
|
|
478
486
|
}
|
|
479
487
|
break;
|
|
@@ -516,14 +524,129 @@ export function formatMiddleElisionMarker(elidedLines: number, elidedBytes: numb
|
|
|
516
524
|
}
|
|
517
525
|
|
|
518
526
|
/**
|
|
519
|
-
*
|
|
527
|
+
* A retained source segment returned by {@link truncateMiddleWindows}.
|
|
528
|
+
* Line coordinates are 1-indexed and inclusive.
|
|
529
|
+
*/
|
|
530
|
+
export type ReadSegment =
|
|
531
|
+
| {
|
|
532
|
+
kind: "lines";
|
|
533
|
+
content: string;
|
|
534
|
+
lines: number;
|
|
535
|
+
bytes: number;
|
|
536
|
+
origin: { startLine: number; endLine: number };
|
|
537
|
+
lastLinePartial: false;
|
|
538
|
+
}
|
|
539
|
+
| {
|
|
540
|
+
kind: "partial-line";
|
|
541
|
+
content: string;
|
|
542
|
+
lines: 1;
|
|
543
|
+
bytes: number;
|
|
544
|
+
origin: { startLine: number; endLine: number };
|
|
545
|
+
sourceLineBytes: number;
|
|
546
|
+
lastLinePartial: true;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/** The actual windows retained by middle truncation. */
|
|
550
|
+
export interface ReadWindow {
|
|
551
|
+
kind: "full" | "head-only" | "tail-only" | "middle";
|
|
552
|
+
head?: ReadSegment;
|
|
553
|
+
tail?: ReadSegment;
|
|
554
|
+
// With normalized head/tail line budgets, an overlap cannot occur: a
|
|
555
|
+
// source that can fit both windows returns `full`, otherwise their kept
|
|
556
|
+
// line counts sum to less than the source. Retain this defensive value for
|
|
557
|
+
// custom/future budgets and wire compatibility.
|
|
558
|
+
overlap: "disjoint" | "adjacent" | "overlapping";
|
|
559
|
+
elidedLines: number;
|
|
560
|
+
elidedBytes: number;
|
|
561
|
+
totalLines: number;
|
|
562
|
+
totalBytes: number;
|
|
563
|
+
/** @deprecated Non-enumerable compatibility reason for older consumers. */
|
|
564
|
+
truncatedBy?: "lines" | "bytes" | "middle";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function outputLineCount(result: TruncationResult): number {
|
|
568
|
+
return result.outputLines ?? (result.content.length === 0 ? 0 : countNewlines(result.content) + 1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function outputByteCount(result: TruncationResult): number {
|
|
572
|
+
return result.outputBytes ?? Buffer.byteLength(result.content, "utf-8");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function lastSourceLineBytes(content: string): number {
|
|
576
|
+
const lineStart = content.lastIndexOf(NL) + 1;
|
|
577
|
+
return Buffer.byteLength(content.slice(lineStart), "utf-8");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function makeReadSegment(
|
|
581
|
+
result: TruncationResult,
|
|
582
|
+
startLine: number,
|
|
583
|
+
totalLines: number,
|
|
584
|
+
sourceContent: string = result.content,
|
|
585
|
+
): ReadSegment | undefined {
|
|
586
|
+
const lines = outputLineCount(result);
|
|
587
|
+
if (lines <= 0) return undefined;
|
|
588
|
+
const bytes = outputByteCount(result);
|
|
589
|
+
const endLine = Math.min(totalLines, startLine + lines - 1);
|
|
590
|
+
if (result.lastLinePartial) {
|
|
591
|
+
const partialLine = endLine;
|
|
592
|
+
return {
|
|
593
|
+
kind: "partial-line",
|
|
594
|
+
content: result.content,
|
|
595
|
+
lines: 1,
|
|
596
|
+
bytes,
|
|
597
|
+
origin: { startLine: partialLine, endLine: partialLine },
|
|
598
|
+
sourceLineBytes: lastSourceLineBytes(sourceContent),
|
|
599
|
+
lastLinePartial: true,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
kind: "lines",
|
|
604
|
+
content: result.content,
|
|
605
|
+
lines,
|
|
606
|
+
bytes,
|
|
607
|
+
origin: { startLine, endLine },
|
|
608
|
+
lastLinePartial: false,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function withMiddleWindowReason(windows: ReadWindow, truncatedBy: "lines" | "bytes" | "middle"): ReadWindow {
|
|
613
|
+
Object.defineProperty(windows, "truncatedBy", { value: truncatedBy, enumerable: false });
|
|
614
|
+
return windows;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function fullReadWindow(
|
|
618
|
+
content: string,
|
|
619
|
+
totalLines: number,
|
|
620
|
+
totalBytes: number,
|
|
621
|
+
overlap: ReadWindow["overlap"] = "disjoint",
|
|
622
|
+
): ReadWindow {
|
|
623
|
+
return {
|
|
624
|
+
kind: "full",
|
|
625
|
+
head: {
|
|
626
|
+
kind: "lines",
|
|
627
|
+
content,
|
|
628
|
+
lines: totalLines,
|
|
629
|
+
bytes: totalBytes,
|
|
630
|
+
origin: { startLine: 1, endLine: totalLines },
|
|
631
|
+
lastLinePartial: false,
|
|
632
|
+
},
|
|
633
|
+
overlap,
|
|
634
|
+
elidedLines: 0,
|
|
635
|
+
elidedBytes: 0,
|
|
636
|
+
totalLines,
|
|
637
|
+
totalBytes,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Truncate content while exposing the exact retained head/tail windows.
|
|
520
643
|
*
|
|
521
|
-
* The
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
644
|
+
* The budget split and fallback ordering intentionally mirror truncateMiddle.
|
|
645
|
+
* The overlap classification is performed before resolving an overlap to a
|
|
646
|
+
* full window, so callers can distinguish disjoint, adjacent and overlapping
|
|
647
|
+
* candidate windows even when no marker is needed.
|
|
525
648
|
*/
|
|
526
|
-
export function
|
|
649
|
+
export function truncateMiddleWindows(content: string, options: TruncationOptions = {}): ReadWindow {
|
|
527
650
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
528
651
|
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
529
652
|
const headBytes = options.maxHeadBytes ?? Math.floor(maxBytes / 2);
|
|
@@ -533,57 +656,153 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
|
|
|
533
656
|
|
|
534
657
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
535
658
|
const totalLines = countNewlines(content) + 1;
|
|
659
|
+
if (totalBytes <= maxBytes && totalLines <= maxLines) return fullReadWindow(content, totalLines, totalBytes);
|
|
660
|
+
|
|
661
|
+
const oneSided = (kind: "head-only" | "tail-only", result: TruncationResult, startLine: number): ReadWindow => {
|
|
662
|
+
const lines = outputLineCount(result);
|
|
663
|
+
const bytes = outputByteCount(result);
|
|
664
|
+
const segment = makeReadSegment(result, startLine, totalLines, content);
|
|
665
|
+
|
|
666
|
+
const windows: ReadWindow = {
|
|
667
|
+
kind,
|
|
668
|
+
...(kind === "head-only" ? (segment ? { head: segment } : {}) : segment ? { tail: segment } : {}),
|
|
669
|
+
overlap: "disjoint",
|
|
670
|
+
elidedLines: Math.max(0, totalLines - lines),
|
|
671
|
+
elidedBytes: Math.max(0, totalBytes - bytes),
|
|
672
|
+
totalLines,
|
|
673
|
+
totalBytes,
|
|
674
|
+
};
|
|
675
|
+
return withMiddleWindowReason(windows, result.truncatedBy === "lines" ? "lines" : "bytes");
|
|
676
|
+
};
|
|
536
677
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Degenerate budgets → fall back to one-sided truncation.
|
|
678
|
+
// Keep this order identical to truncateMiddle: degenerate budgets are
|
|
679
|
+
// resolved before either one-sided truncator is called.
|
|
542
680
|
if (headBytes <= 0 || headLines <= 0) {
|
|
543
|
-
|
|
681
|
+
const tail = truncateTail(content, { maxBytes: tailBytes || maxBytes, maxLines: tailLines || maxLines });
|
|
682
|
+
return oneSided("tail-only", tail, totalLines - outputLineCount(tail) + 1);
|
|
544
683
|
}
|
|
545
684
|
if (tailBytes <= 0 || tailLines <= 0) {
|
|
546
|
-
|
|
685
|
+
const head = truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
686
|
+
return oneSided("head-only", head, 1);
|
|
547
687
|
}
|
|
548
688
|
|
|
549
689
|
const head = truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
550
690
|
const tail = truncateTail(content, { maxBytes: tailBytes, maxLines: tailLines });
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
const tailBytesKept = tail.outputBytes ?? Buffer.byteLength(tail.content, "utf-8");
|
|
691
|
+
const headLinesKept = outputLineCount(head);
|
|
692
|
+
const tailLinesKept = outputLineCount(tail);
|
|
693
|
+
const headBytesKept = outputByteCount(head);
|
|
694
|
+
const tailBytesKept = outputByteCount(tail);
|
|
556
695
|
|
|
557
696
|
// Head unusable (first line exceeds budget) → tail-only.
|
|
558
|
-
if (headLinesKept === 0 || head.firstLineExceedsLimit)
|
|
697
|
+
if (headLinesKept === 0 || head.firstLineExceedsLimit) {
|
|
698
|
+
return oneSided("tail-only", tail, totalLines - tailLinesKept + 1);
|
|
699
|
+
}
|
|
559
700
|
// Tail unusable → head-only.
|
|
560
|
-
if (tailLinesKept === 0) return head;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
701
|
+
if (tailLinesKept === 0) return oneSided("head-only", head, 1);
|
|
702
|
+
|
|
703
|
+
const headEnd = headLinesKept;
|
|
704
|
+
const tailStart = totalLines - tailLinesKept + 1;
|
|
705
|
+
const overlap: ReadWindow["overlap"] =
|
|
706
|
+
headEnd + 1 < tailStart ? "disjoint" : headEnd + 1 === tailStart ? "adjacent" : "overlapping";
|
|
707
|
+
const tailPartial = tail.lastLinePartial === true;
|
|
708
|
+
|
|
709
|
+
// Adjacent/overlapping complete windows cover the whole source. Return one
|
|
710
|
+
// full segment so renderers never insert a marker for an overlap.
|
|
711
|
+
if (headLinesKept + tailLinesKept >= totalLines && !tailPartial) {
|
|
712
|
+
return fullReadWindow(content, totalLines, totalBytes, overlap);
|
|
564
713
|
}
|
|
565
714
|
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
715
|
+
const headWindow = makeReadSegment(head, 1, totalLines, content);
|
|
716
|
+
const tailWindow = makeReadSegment(tail, tailStart, totalLines, content);
|
|
717
|
+
if (!headWindow || !tailWindow) return fullReadWindow(content, totalLines, totalBytes, overlap);
|
|
718
|
+
|
|
719
|
+
const partialSourceBytes = tailWindow.kind === "partial-line" ? tailWindow.sourceLineBytes : tailWindow.bytes;
|
|
720
|
+
const elidedLines = tailPartial
|
|
721
|
+
? Math.max(0, tailStart - headEnd - 1)
|
|
722
|
+
: Math.max(0, totalLines - headLinesKept - tailLinesKept);
|
|
723
|
+
const elidedBytes = tailPartial
|
|
724
|
+
? Math.max(0, totalBytes - headBytesKept - partialSourceBytes)
|
|
725
|
+
: Math.max(0, totalBytes - headBytesKept - tailBytesKept);
|
|
726
|
+
return withMiddleWindowReason(
|
|
727
|
+
{
|
|
728
|
+
kind: "middle",
|
|
729
|
+
head: headWindow,
|
|
730
|
+
tail: tailWindow,
|
|
731
|
+
overlap,
|
|
732
|
+
elidedLines,
|
|
733
|
+
elidedBytes,
|
|
734
|
+
totalLines,
|
|
735
|
+
totalBytes,
|
|
736
|
+
},
|
|
737
|
+
"middle",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
573
740
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Truncate content keeping a head window and a tail window, eliding the middle.
|
|
743
|
+
* The composed return shape remains field-for-field compatible with the
|
|
744
|
+
* historical implementation; callers that need coordinates use
|
|
745
|
+
* truncateMiddleWindows directly.
|
|
746
|
+
*/
|
|
747
|
+
export function truncateMiddle(content: string, options: TruncationOptions = {}): TruncationResult {
|
|
748
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
749
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
750
|
+
const headBytes = options.maxHeadBytes ?? Math.floor(maxBytes / 2);
|
|
751
|
+
const tailBytes = Math.max(0, maxBytes - headBytes);
|
|
752
|
+
const headLines = options.maxHeadLines ?? Math.max(1, Math.floor(maxLines / 2));
|
|
753
|
+
const tailLines = Math.max(0, maxLines - headLines);
|
|
754
|
+
const windows = truncateMiddleWindows(content, options);
|
|
755
|
+
|
|
756
|
+
switch (windows.kind) {
|
|
757
|
+
case "full":
|
|
758
|
+
return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
759
|
+
case "tail-only":
|
|
760
|
+
return truncateTail(content, { maxBytes: tailBytes || maxBytes, maxLines: tailLines || maxLines });
|
|
761
|
+
case "head-only":
|
|
762
|
+
return truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
763
|
+
case "middle": {
|
|
764
|
+
const head = windows.head;
|
|
765
|
+
const tail = windows.tail;
|
|
766
|
+
if (!head || !tail) return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
767
|
+
if (head.lines + tail.lines >= windows.totalLines)
|
|
768
|
+
return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
769
|
+
const legacyElidedLines = Math.max(0, windows.totalLines - head.lines - tail.lines);
|
|
770
|
+
const legacyElidedBytes =
|
|
771
|
+
tail.kind === "partial-line"
|
|
772
|
+
? Math.max(0, windows.totalBytes - head.bytes - tail.bytes)
|
|
773
|
+
: windows.elidedBytes;
|
|
774
|
+
const marker = formatMiddleElisionMarker(legacyElidedLines, legacyElidedBytes);
|
|
775
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
776
|
+
return {
|
|
777
|
+
content: `${head.content}\n${marker}\n${tail.content}`,
|
|
778
|
+
truncated: true,
|
|
779
|
+
truncatedBy: "middle",
|
|
780
|
+
totalLines: windows.totalLines,
|
|
781
|
+
totalBytes: windows.totalBytes,
|
|
782
|
+
outputLines: head.lines + tail.lines + 1,
|
|
783
|
+
outputBytes: head.bytes + tail.bytes + markerBytes + 2,
|
|
784
|
+
elidedLines: legacyElidedLines,
|
|
785
|
+
elidedBytes: legacyElidedBytes,
|
|
786
|
+
lastLinePartial: tail.lastLinePartial,
|
|
787
|
+
firstLineExceedsLimit: false,
|
|
788
|
+
...(tail.lastLinePartial ? { lastLineExceedsLimit: true } : {}),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Dispatch truncation using the caller-facing direction vocabulary.
|
|
796
|
+
*/
|
|
797
|
+
export function truncateContent(content: string, options: TruncationOptions = {}): TruncationResult {
|
|
798
|
+
switch (options.direction) {
|
|
799
|
+
case "last":
|
|
800
|
+
return truncateTail(content, options);
|
|
801
|
+
case "both":
|
|
802
|
+
return truncateMiddle(content, options);
|
|
803
|
+
default:
|
|
804
|
+
return truncateHead(content, options);
|
|
805
|
+
}
|
|
587
806
|
}
|
|
588
807
|
|
|
589
808
|
// =============================================================================
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { getAgentDbPath, getAgentDir } from "@gajae-code/utils";
|
|
5
5
|
import { YAML } from "bun";
|
|
6
6
|
import { type ModelsConfig, ModelsConfigSchema } from "../config/models-config-schema";
|
|
7
|
+
import { compareRankedProviders, famousProviderIndex } from "../config/provider-ranking";
|
|
7
8
|
import { AuthStorage } from "../session/auth-storage";
|
|
8
9
|
import providerPresets from "./provider-presets.json";
|
|
9
10
|
|
|
@@ -81,11 +82,25 @@ export function findProviderPreset(value: string | undefined): ProviderPreset |
|
|
|
81
82
|
return PROVIDER_PRESETS.find(preset => preset.id === normalized || preset.aliases.includes(normalized));
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
function providerPresetRankingId(preset: ProviderPreset): string {
|
|
86
|
+
return (
|
|
87
|
+
[preset.providerId, preset.id, ...preset.aliases].find(id => famousProviderIndex(id) !== undefined) ?? preset.id
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
export function formatProviderPresetList(): string {
|
|
85
|
-
return PROVIDER_PRESETS
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
return [...PROVIDER_PRESETS]
|
|
93
|
+
.sort((left, right) =>
|
|
94
|
+
compareRankedProviders(
|
|
95
|
+
{ id: providerPresetRankingId(left), label: left.name, authState: "none" },
|
|
96
|
+
{ id: providerPresetRankingId(right), label: right.name, authState: "none" },
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
.map(preset => {
|
|
100
|
+
const aliases = preset.aliases.length > 0 ? ` (aliases: ${preset.aliases.join(", ")})` : "";
|
|
101
|
+
return `${preset.id}${aliases}: ${preset.description}`;
|
|
102
|
+
})
|
|
103
|
+
.join("\n");
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
export function parseModelList(values: readonly string[]): string[] {
|
|
@@ -71,8 +71,6 @@ export interface SkillActiveEntry {
|
|
|
71
71
|
handoff_at?: string;
|
|
72
72
|
active_subskills?: ActiveSubskillEntry[];
|
|
73
73
|
source_state_revision?: number;
|
|
74
|
-
/** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
|
|
75
|
-
committed_mode_state_revision?: number;
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
export interface SkillActiveState {
|
|
@@ -116,8 +114,6 @@ export interface SyncSkillActiveStateOptions {
|
|
|
116
114
|
handoff_at?: string;
|
|
117
115
|
active_subskills?: ActiveSubskillEntry[];
|
|
118
116
|
sourceRevision?: number;
|
|
119
|
-
/** Durable ordering token from the source workflow mode-state, not this cache file's revision. */
|
|
120
|
-
committedModeRevision?: number;
|
|
121
117
|
}
|
|
122
118
|
|
|
123
119
|
const HUD_TEXT_LIMIT = 80;
|
|
@@ -789,9 +785,6 @@ async function persistActiveEntry(
|
|
|
789
785
|
await writeActiveEntry(cwd, sessionScope, entry.skill, entry, {
|
|
790
786
|
cwd,
|
|
791
787
|
audit: activeStateWriterAudit("write-active-entry", sessionScope),
|
|
792
|
-
...(typeof entry.committed_mode_state_revision === "number"
|
|
793
|
-
? { orderingRevision: entry.committed_mode_state_revision }
|
|
794
|
-
: {}),
|
|
795
788
|
});
|
|
796
789
|
}
|
|
797
790
|
}
|
|
@@ -870,9 +863,6 @@ export async function syncSkillActiveState(options: SyncSkillActiveStateOptions)
|
|
|
870
863
|
? { active_subskills: preservedActiveSubskills }
|
|
871
864
|
: {}),
|
|
872
865
|
...(typeof options.sourceRevision === "number" ? { source_state_revision: options.sourceRevision } : {}),
|
|
873
|
-
...(typeof options.committedModeRevision === "number"
|
|
874
|
-
? { committed_mode_state_revision: options.committedModeRevision }
|
|
875
|
-
: {}),
|
|
876
866
|
};
|
|
877
867
|
const sessionScope = { sessionId: options.sessionId };
|
|
878
868
|
await removeSupersededPlanningPipelineEntries(options.cwd, sessionScope, entry);
|
|
@@ -23,6 +23,9 @@ interface RalplanHudState extends WorkflowGateHudState {
|
|
|
23
23
|
iteration?: number;
|
|
24
24
|
iterationFromIndex?: number;
|
|
25
25
|
stages?: string;
|
|
26
|
+
architectPasses?: number;
|
|
27
|
+
criticPasses?: number;
|
|
28
|
+
reviewPassBudget?: number;
|
|
26
29
|
verdict?: string;
|
|
27
30
|
latestSummary?: string;
|
|
28
31
|
pendingApproval?: boolean;
|
|
@@ -194,13 +197,31 @@ export function deriveDeepInterviewHud(
|
|
|
194
197
|
export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSummary {
|
|
195
198
|
const verdict = state.verdict?.toUpperCase();
|
|
196
199
|
const verdictSeverity =
|
|
197
|
-
verdict === "BLOCK"
|
|
200
|
+
verdict === "BLOCK" || verdict === "REJECT"
|
|
198
201
|
? "blocked"
|
|
199
202
|
: verdict === "ITERATE" || verdict === "WATCH"
|
|
200
203
|
? "warning"
|
|
201
|
-
: verdict === "APPROVE" || verdict === "CLEAR"
|
|
204
|
+
: verdict === "APPROVE" || verdict === "CLEAR" || verdict === "OKAY"
|
|
202
205
|
? "success"
|
|
203
206
|
: undefined;
|
|
207
|
+
const reviewPassChip = (
|
|
208
|
+
label: "arch" | "crit",
|
|
209
|
+
passes: number | undefined,
|
|
210
|
+
priority: number,
|
|
211
|
+
): WorkflowHudChip | null => {
|
|
212
|
+
if (
|
|
213
|
+
state.pendingApproval ||
|
|
214
|
+
typeof passes !== "number" ||
|
|
215
|
+
!Number.isFinite(passes) ||
|
|
216
|
+
passes <= 0 ||
|
|
217
|
+
typeof state.reviewPassBudget !== "number" ||
|
|
218
|
+
!Number.isFinite(state.reviewPassBudget) ||
|
|
219
|
+
state.reviewPassBudget <= 0
|
|
220
|
+
) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return chip(label, `${passes}/${state.reviewPassBudget}`, priority);
|
|
224
|
+
};
|
|
204
225
|
return {
|
|
205
226
|
version: 1,
|
|
206
227
|
summary: state.latestSummary,
|
|
@@ -217,6 +238,8 @@ export function buildRalplanHudSummary(state: RalplanHudState): WorkflowHudSumma
|
|
|
217
238
|
30,
|
|
218
239
|
),
|
|
219
240
|
chip("stages", state.stages, 35),
|
|
241
|
+
reviewPassChip("arch", state.architectPasses, 36),
|
|
242
|
+
reviewPassChip("crit", state.criticPasses, 38),
|
|
220
243
|
chip("verdict", verdict, 40, verdictSeverity),
|
|
221
244
|
]),
|
|
222
245
|
...(state.updatedAt ? { updated_at: state.updatedAt } : {}),
|
|
@@ -57,6 +57,9 @@ const BASH_OPAQUE_INTERPRETER_WRITE_RE =
|
|
|
57
57
|
const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
|
|
58
58
|
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
59
59
|
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?dd\b([^;&|\n]*)/gi;
|
|
60
|
+
/** Literal `sh|bash|zsh -c '<script>'` payloads whose nested script must also be scanned. */
|
|
61
|
+
const BASH_NESTED_SHELL_RE =
|
|
62
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:[^\s;&|]*\/)?(?:ba|z|da)?sh\s+(?:-[A-Za-z]+\s+)*-[A-Za-z]*c\s+(?:(')([^']*)'|(")([^"]*)")/g;
|
|
60
63
|
|
|
61
64
|
type ToolWithEditMode = AgentTool & {
|
|
62
65
|
mode?: unknown;
|
|
@@ -77,6 +80,12 @@ export interface WorkflowMutationGuardInput {
|
|
|
77
80
|
interface ExtractedTargets {
|
|
78
81
|
paths: string[];
|
|
79
82
|
unknown: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Set when a bash command was recognized as performing a filesystem write.
|
|
85
|
+
* During a planning phase an unrecognized bash command is allowed (reading
|
|
86
|
+
* and inspection must not be blocked); only a recognized mutation blocks.
|
|
87
|
+
*/
|
|
88
|
+
explicitMutation?: boolean;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
export interface WorkflowMutationDecision {
|
|
@@ -382,11 +391,34 @@ function cleanShellWord(value: string): string {
|
|
|
382
391
|
return value.replace(/^['"]|['"]$/g, "");
|
|
383
392
|
}
|
|
384
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Blank out single-quoted spans so shell metacharacters inside a literal
|
|
396
|
+
* argument value (e.g. `--value 'uses `x`; a > b'`) are not misread as
|
|
397
|
+
* redirections or separators. Single quotes suppress every expansion in POSIX
|
|
398
|
+
* shells, so their contents are always inert data. Double-quoted spans are left
|
|
399
|
+
* intact because they still expand `$(...)` and backticks.
|
|
400
|
+
*/
|
|
401
|
+
function maskSingleQuotedSpans(command: string): string {
|
|
402
|
+
let masked = "";
|
|
403
|
+
let inSingle = false;
|
|
404
|
+
for (const character of command) {
|
|
405
|
+
if (character === "'") {
|
|
406
|
+
inSingle = !inSingle;
|
|
407
|
+
masked += character;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
masked += inSingle && character !== "\n" ? " " : character;
|
|
411
|
+
}
|
|
412
|
+
// An unbalanced quote means the parse is unreliable; keep the original text
|
|
413
|
+
// so the scanner stays fail-closed rather than blind.
|
|
414
|
+
return inSingle ? command : masked;
|
|
415
|
+
}
|
|
416
|
+
|
|
385
417
|
function isDeviceSinkPath(value: string): boolean {
|
|
386
418
|
return DEVICE_SINK_PATHS.has(cleanShellWord(value));
|
|
387
419
|
}
|
|
388
420
|
|
|
389
|
-
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
421
|
+
function extractBashTargets(args: unknown, depth = 0): ExtractedTargets {
|
|
390
422
|
const record = getRecord(args);
|
|
391
423
|
const command = safeString(record?.command);
|
|
392
424
|
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
@@ -394,19 +426,40 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
394
426
|
targets.unknown = true;
|
|
395
427
|
return targets;
|
|
396
428
|
}
|
|
429
|
+
// A literal `sh|bash|zsh -c '<script>'` payload is a real command list. Its
|
|
430
|
+
// mutations never match the top-level patterns, so recurse into it; anything
|
|
431
|
+
// deeper than a bounded depth is not statically analyzable and fails closed.
|
|
432
|
+
for (const match of command.matchAll(BASH_NESTED_SHELL_RE)) {
|
|
433
|
+
if (depth >= 2) {
|
|
434
|
+
targets.explicitMutation = true;
|
|
435
|
+
targets.unknown = true;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
const nested = extractBashTargets({ command: match[2] ?? match[4] ?? "" }, depth + 1);
|
|
439
|
+
for (const nestedPath of nested.paths) addPath(targets, nestedPath);
|
|
440
|
+
if (nested.unknown) targets.unknown = true;
|
|
441
|
+
if (nested.explicitMutation) targets.explicitMutation = true;
|
|
442
|
+
}
|
|
443
|
+
// Nested scripts were read from the raw text above; every scanner below works
|
|
444
|
+
// on the masked view so quoted argument data cannot look like a redirection.
|
|
445
|
+
const scanned = maskSingleQuotedSpans(command);
|
|
397
446
|
if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
|
|
447
|
+
targets.explicitMutation = true;
|
|
398
448
|
targets.unknown = true;
|
|
399
449
|
}
|
|
400
450
|
// `exec 1<>file` rebinds a descriptor, so a later `>/dev/null` may not reach the device at all.
|
|
401
|
-
if (BASH_EXEC_REDIRECT_RE.test(
|
|
451
|
+
if (BASH_EXEC_REDIRECT_RE.test(scanned)) {
|
|
452
|
+
targets.explicitMutation = true;
|
|
402
453
|
targets.unknown = true;
|
|
403
454
|
}
|
|
404
|
-
for (const match of
|
|
455
|
+
for (const match of scanned.matchAll(BASH_EXTENDED_WRITE_RE)) {
|
|
456
|
+
targets.explicitMutation = true;
|
|
405
457
|
const cleaned = cleanShellWord(match[1] ?? "");
|
|
406
458
|
if (!cleaned) targets.unknown = true;
|
|
407
459
|
else if (!isDeviceSinkPath(cleaned)) addPath(targets, cleaned);
|
|
408
460
|
}
|
|
409
|
-
for (const match of
|
|
461
|
+
for (const match of scanned.matchAll(BASH_DD_OUTPUT_RE)) {
|
|
462
|
+
targets.explicitMutation = true;
|
|
410
463
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
411
464
|
// Every `of=` counts: GNU dd honors the last one, so `of=/dev/null of=real.ts` writes real.ts.
|
|
412
465
|
const outputs = parts.filter(part => part.startsWith("of="));
|
|
@@ -417,15 +470,17 @@ function extractBashTargets(args: unknown): ExtractedTargets {
|
|
|
417
470
|
else if (!isDeviceSinkPath(outputPath)) addPath(targets, outputPath);
|
|
418
471
|
}
|
|
419
472
|
}
|
|
420
|
-
for (const match of
|
|
473
|
+
for (const match of scanned.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
|
|
421
474
|
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
422
475
|
const hasInPlaceFlag = parts.some(part => /^-.*i/.test(part));
|
|
423
476
|
if (!hasInPlaceFlag) continue;
|
|
477
|
+
targets.explicitMutation = true;
|
|
424
478
|
const target = [...parts].reverse().find(part => part && !part.startsWith("-"));
|
|
425
479
|
if (target) addPath(targets, target);
|
|
426
480
|
else targets.unknown = true;
|
|
427
481
|
}
|
|
428
|
-
for (const match of
|
|
482
|
+
for (const match of scanned.matchAll(BASH_MUTATION_COMMAND_RE)) {
|
|
483
|
+
targets.explicitMutation = true;
|
|
429
484
|
const redirected = match[2]?.trim();
|
|
430
485
|
if (redirected) {
|
|
431
486
|
const cleaned = cleanShellWord(redirected);
|
|
@@ -666,7 +721,10 @@ export async function getWorkflowMutationDecision(
|
|
|
666
721
|
}
|
|
667
722
|
if (input.forceOverride) return { blocked: false, targets: [] };
|
|
668
723
|
const message = planningPhaseBlockMessage(planning.skill);
|
|
669
|
-
|
|
724
|
+
// Bash during a planning phase blocks only when a filesystem mutation was
|
|
725
|
+
// actually recognized. Inspection, reads, and CLI invocations the scanner does
|
|
726
|
+
// not model stay usable; other tools keep failing closed on unknown targets.
|
|
727
|
+
if (targets.unknown && (input.tool.name !== "bash" || targets.explicitMutation)) {
|
|
670
728
|
return {
|
|
671
729
|
blocked: true,
|
|
672
730
|
message,
|