@gajae-code/coding-agent 0.8.2 → 0.9.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 +29 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +8 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +7 -7
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/settings-schema.ts +68 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +164 -15
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1593 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +14 -5
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +9 -1
- package/src/modes/rpc/rpc-mode.ts +29 -3
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config.ts +16 -4
- package/src/notifications/index.ts +78 -5
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +85 -8
- package/src/notifications/threaded-render.ts +17 -1
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +32 -22
- package/src/session/agent-session.ts +732 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +65 -0
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/web/scrapers/youtube.ts +24 -12
package/src/tools/output-meta.ts
CHANGED
|
@@ -554,13 +554,17 @@ function getSpillConfig(s: Settings | undefined) {
|
|
|
554
554
|
| "tools.artifactSpillThreshold"
|
|
555
555
|
| "tools.artifactTailBytes"
|
|
556
556
|
| "tools.artifactTailLines"
|
|
557
|
-
| "tools.artifactHeadBytes"
|
|
557
|
+
| "tools.artifactHeadBytes"
|
|
558
|
+
| "tools.maxInlineResultBytes"
|
|
559
|
+
| "tools.readArtifactSpillThreshold";
|
|
558
560
|
const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
|
|
559
561
|
return {
|
|
560
562
|
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
563
|
+
readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
|
|
561
564
|
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
562
565
|
tailLines: get("tools.artifactTailLines"),
|
|
563
566
|
headBytes: get("tools.artifactHeadBytes") * 1024,
|
|
567
|
+
maxInlineBytes: get("tools.maxInlineResultBytes") * 1024,
|
|
564
568
|
};
|
|
565
569
|
}
|
|
566
570
|
|
|
@@ -596,8 +600,12 @@ async function spillLargeResultToArtifact(
|
|
|
596
600
|
): Promise<AgentToolResult> {
|
|
597
601
|
const sessionManager = context?.sessionManager;
|
|
598
602
|
if (!sessionManager) return result;
|
|
599
|
-
|
|
600
|
-
|
|
603
|
+
const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
604
|
+
// `read` manages its own per-range truncation, but the combined multi-range
|
|
605
|
+
// output has no cap — enforce a read-specific (higher) combined threshold
|
|
606
|
+
// instead of exempting read entirely. 0 disables read spill (backstop only).
|
|
607
|
+
const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
|
|
608
|
+
if (effectiveThreshold <= 0) return result;
|
|
601
609
|
|
|
602
610
|
// Skip if tool already saved an artifact
|
|
603
611
|
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
@@ -614,7 +622,7 @@ async function spillLargeResultToArtifact(
|
|
|
614
622
|
|
|
615
623
|
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
616
624
|
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
617
|
-
if (totalBytes <=
|
|
625
|
+
if (totalBytes <= effectiveThreshold) return result;
|
|
618
626
|
|
|
619
627
|
// Save full output as artifact
|
|
620
628
|
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
@@ -691,6 +699,106 @@ async function spillLargeResultToArtifact(
|
|
|
691
699
|
return { ...result, content: newContent, details: newDetails };
|
|
692
700
|
}
|
|
693
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Absolute inline-size backstop enforced after {@link spillLargeResultToArtifact}.
|
|
704
|
+
*
|
|
705
|
+
* The threshold-based spill above has escape hatches: it early-returns for the
|
|
706
|
+
* `read` tool and for results that already carry an `artifactId` (a tool may set
|
|
707
|
+
* partial truncation meta yet still emit oversized inline text). This backstop
|
|
708
|
+
* closes those gaps: when `tools.maxInlineResultBytes` is configured (> 0), any
|
|
709
|
+
* final result whose inline text exceeds the cap is force-saved to an artifact
|
|
710
|
+
* (reusing an existing artifactId to avoid double-artifacting) and truncated to a
|
|
711
|
+
* head+tail view that fits the cap. Disabled by default (opt-in pending
|
|
712
|
+
* measurement); a 0 cap returns the result untouched.
|
|
713
|
+
*/
|
|
714
|
+
async function enforceInlineResultBackstop(
|
|
715
|
+
result: AgentToolResult,
|
|
716
|
+
toolName: string,
|
|
717
|
+
context: AgentToolContext | undefined,
|
|
718
|
+
): Promise<AgentToolResult> {
|
|
719
|
+
const { maxInlineBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
720
|
+
if (maxInlineBytes <= 0) return result;
|
|
721
|
+
|
|
722
|
+
const textParts: string[] = [];
|
|
723
|
+
for (const block of result.content) {
|
|
724
|
+
if (block.type === "text" && block.text) {
|
|
725
|
+
textParts.push(block.text);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (textParts.length === 0) return result;
|
|
729
|
+
|
|
730
|
+
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
731
|
+
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
732
|
+
if (totalBytes <= maxInlineBytes) return result;
|
|
733
|
+
|
|
734
|
+
// Reuse an existing artifact (avoid double-artifacting); otherwise save the
|
|
735
|
+
// full output so the truncated view keeps a reference to the complete text.
|
|
736
|
+
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
737
|
+
let artifactId = existingMeta?.truncation?.artifactId;
|
|
738
|
+
if (!artifactId) {
|
|
739
|
+
artifactId = (await context?.sessionManager?.saveArtifact(fullText, toolName)) ?? undefined;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Budget head+tail below the cap, reserving room for the elision marker so the
|
|
743
|
+
// composed `<head>\n<marker>\n<tail>` view never exceeds the configured cap.
|
|
744
|
+
const MARKER_RESERVE = 256;
|
|
745
|
+
const budget = maxInlineBytes - MARKER_RESERVE;
|
|
746
|
+
const useMiddle = headBytes > 0 && budget > 0;
|
|
747
|
+
let truncated = useMiddle
|
|
748
|
+
? truncateMiddle(fullText, {
|
|
749
|
+
maxBytes: budget,
|
|
750
|
+
maxLines: tailLines * 2,
|
|
751
|
+
maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
|
|
752
|
+
maxHeadLines: tailLines,
|
|
753
|
+
})
|
|
754
|
+
: truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
|
|
755
|
+
|
|
756
|
+
// Defensive clamp: guarantee the contract even for pathological marker sizes.
|
|
757
|
+
if (Buffer.byteLength(truncated.content, "utf-8") > maxInlineBytes) {
|
|
758
|
+
truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const newContent: (TextContent | ImageContent)[] = [];
|
|
762
|
+
for (const block of result.content) {
|
|
763
|
+
if (block.type !== "text") {
|
|
764
|
+
newContent.push(block);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
newContent.push({ type: "text", text: truncated.content });
|
|
768
|
+
|
|
769
|
+
const outputLines = truncated.outputLines ?? truncated.totalLines;
|
|
770
|
+
const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
|
|
771
|
+
const truncationMeta: TruncationMeta =
|
|
772
|
+
truncated.truncatedBy === "middle"
|
|
773
|
+
? {
|
|
774
|
+
direction: "middle",
|
|
775
|
+
truncatedBy: "middle",
|
|
776
|
+
totalLines: truncated.totalLines,
|
|
777
|
+
totalBytes: truncated.totalBytes,
|
|
778
|
+
outputLines,
|
|
779
|
+
outputBytes,
|
|
780
|
+
maxBytes: maxInlineBytes,
|
|
781
|
+
elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
|
|
782
|
+
elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
|
|
783
|
+
artifactId,
|
|
784
|
+
}
|
|
785
|
+
: {
|
|
786
|
+
direction: "tail",
|
|
787
|
+
truncatedBy: truncated.truncatedBy ?? "bytes",
|
|
788
|
+
totalLines: truncated.totalLines,
|
|
789
|
+
totalBytes: truncated.totalBytes,
|
|
790
|
+
outputLines,
|
|
791
|
+
outputBytes,
|
|
792
|
+
maxBytes: maxInlineBytes,
|
|
793
|
+
shownRange: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
|
|
794
|
+
artifactId,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
|
|
798
|
+
const newDetails = { ...(result.details ?? {}), meta: newMeta };
|
|
799
|
+
return { ...result, content: newContent, details: newDetails };
|
|
800
|
+
}
|
|
801
|
+
|
|
694
802
|
// =============================================================================
|
|
695
803
|
// Tool wrapper
|
|
696
804
|
// =============================================================================
|
|
@@ -711,6 +819,10 @@ async function wrappedExecute(
|
|
|
711
819
|
// Spill large results to artifact, truncate to tail
|
|
712
820
|
result = await spillLargeResultToArtifact(result, this.name, context);
|
|
713
821
|
|
|
822
|
+
// Absolute inline-size backstop: catches oversized text the threshold spill
|
|
823
|
+
// skipped (read exemption, tools with pre-existing partial artifact meta).
|
|
824
|
+
result = await enforceInlineResultBackstop(result, this.name, context);
|
|
825
|
+
|
|
714
826
|
// Append notices from meta
|
|
715
827
|
const meta: OutputMeta | undefined = result.details?.meta;
|
|
716
828
|
if (meta) {
|
package/src/tools/search.ts
CHANGED
|
@@ -44,7 +44,10 @@ const searchSchema = z
|
|
|
44
44
|
paths: z
|
|
45
45
|
.array(z.string().describe("file, directory, glob, or internal URL to search"))
|
|
46
46
|
.min(1)
|
|
47
|
-
.
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
"files, directories, globs, or internal URLs to search (defaults to the working directory when omitted)",
|
|
50
|
+
),
|
|
48
51
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
49
52
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
50
53
|
skip: z
|
|
@@ -236,7 +239,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
236
239
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
237
240
|
_toolContext?: AgentToolContext,
|
|
238
241
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
239
|
-
const { pattern,
|
|
242
|
+
const { pattern, i, gitignore, skip } = params;
|
|
243
|
+
// Recurring real-world failure mode: models omit `paths` for repo-wide
|
|
244
|
+
// searches. Default to the working directory instead of failing the call.
|
|
245
|
+
const paths = params.paths ?? ["."];
|
|
240
246
|
|
|
241
247
|
return untilAborted(signal, async () => {
|
|
242
248
|
const normalizedPattern = pattern.trim();
|
package/src/tools/skill.ts
CHANGED
|
@@ -37,6 +37,8 @@ function normalizeSkillName(name: string | undefined): string {
|
|
|
37
37
|
return (name ?? "").trim();
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const SKILL_NAME_GLOB_PATTERN = /[*?[\]{}]/;
|
|
41
|
+
|
|
40
42
|
type SkillToolInput = z.infer<typeof skillSchema>;
|
|
41
43
|
|
|
42
44
|
export interface SkillToolDetails {
|
|
@@ -87,6 +89,19 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
87
89
|
if (!requestedName) {
|
|
88
90
|
throw new ToolError("skill tool: `name` is required");
|
|
89
91
|
}
|
|
92
|
+
// Fail fast on glob/wildcard names (e.g. `*`). A model that sees the
|
|
93
|
+
// `--skills '*'` launch filter may echo the glob into the skill tool.
|
|
94
|
+
// Without this guard `*` slips through to the generic unknown-skill
|
|
95
|
+
// path only because no skill is literally named `*`, so a future
|
|
96
|
+
// odd skill name could get dispatched — and the actionable signal
|
|
97
|
+
// that names are concrete (not globs) is lost. Reject before any
|
|
98
|
+
// dispatch or handoff state mutation so the call ends immediately
|
|
99
|
+
// instead of spawning skill work that burns the whole turn budget.
|
|
100
|
+
if (SKILL_NAME_GLOB_PATTERN.test(requestedName)) {
|
|
101
|
+
throw new ToolError(
|
|
102
|
+
`skill tool: "${requestedName}" is not a valid skill name. The name must be a single concrete skill (e.g. "ralplan"), not a glob or wildcard pattern. Pass one exact skill name as shown in /skill:<name>.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
90
105
|
const activeState = this.#session.getActiveSkillState?.();
|
|
91
106
|
const activeSkill = normalizeSkillName(activeState?.skill);
|
|
92
107
|
if (activeSkill && requestedName === activeSkill) {
|
|
@@ -159,24 +159,32 @@ async function resolveMentionPath(
|
|
|
159
159
|
return best?.candidate.path ?? null;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Default inline cap for auto-read file mentions, deliberately below the
|
|
164
|
+
* read-tool cap (`DEFAULT_MAX_BYTES`, 50KB) so an incidental `@path` mention
|
|
165
|
+
* injects a smaller snippet than an explicit `read`. Overridable per call.
|
|
166
|
+
*/
|
|
167
|
+
export const DEFAULT_FILE_MENTION_INLINE_BYTES = 20 * 1024;
|
|
168
|
+
|
|
169
|
+
function buildTextOutput(textContent: string, maxInlineBytes?: number): { output: string; lineCount: number } {
|
|
170
|
+
const cap = maxInlineBytes && maxInlineBytes > 0 ? maxInlineBytes : DEFAULT_FILE_MENTION_INLINE_BYTES;
|
|
163
171
|
const allLines = textContent.split("\n");
|
|
164
172
|
const totalFileLines = allLines.length;
|
|
165
|
-
const truncation = truncateHead(textContent);
|
|
173
|
+
const truncation = truncateHead(textContent, { maxBytes: cap });
|
|
166
174
|
|
|
167
175
|
if (truncation.firstLineExceedsLimit) {
|
|
168
176
|
const firstLine = allLines[0] ?? "";
|
|
169
177
|
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
170
|
-
const snippet = truncateHeadBytes(firstLine,
|
|
178
|
+
const snippet = truncateHeadBytes(firstLine, cap);
|
|
171
179
|
let outputText = snippet.text;
|
|
172
180
|
|
|
173
181
|
if (outputText.length > 0) {
|
|
174
182
|
outputText += `\n\n[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
|
|
175
|
-
|
|
183
|
+
cap,
|
|
176
184
|
)} limit. Showing first ${formatBytes(snippet.bytes)} of the line.]`;
|
|
177
185
|
} else {
|
|
178
186
|
outputText = `[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
|
|
179
|
-
|
|
187
|
+
cap,
|
|
180
188
|
)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
181
189
|
}
|
|
182
190
|
|
|
@@ -277,7 +285,18 @@ export function extractFileMentions(text: string): string[] {
|
|
|
277
285
|
export async function generateFileMentionMessages(
|
|
278
286
|
filePaths: string[],
|
|
279
287
|
cwd: string,
|
|
280
|
-
options?: {
|
|
288
|
+
options?: {
|
|
289
|
+
autoResizeImages?: boolean;
|
|
290
|
+
useHashLines?: boolean;
|
|
291
|
+
/** Inline byte cap for auto-read text. Default: DEFAULT_FILE_MENTION_INLINE_BYTES. */
|
|
292
|
+
maxInlineBytes?: number;
|
|
293
|
+
/**
|
|
294
|
+
* Resolved paths already shown (read or mentioned) recently in the
|
|
295
|
+
* conversation. A mention of one of these emits a compact duplicate note
|
|
296
|
+
* instead of re-injecting the full body.
|
|
297
|
+
*/
|
|
298
|
+
recentlyShownPaths?: ReadonlySet<string>;
|
|
299
|
+
},
|
|
281
300
|
): Promise<AgentMessage[]> {
|
|
282
301
|
if (filePaths.length === 0) return [];
|
|
283
302
|
|
|
@@ -290,12 +309,25 @@ export async function generateFileMentionMessages(
|
|
|
290
309
|
return mentionCandidatesPromise;
|
|
291
310
|
};
|
|
292
311
|
|
|
312
|
+
const shownInBatch = new Set<string>();
|
|
293
313
|
for (const filePath of filePaths) {
|
|
294
314
|
const resolvedPath = await resolveMentionPath(filePath, cwd, getMentionCandidates);
|
|
295
315
|
if (!resolvedPath) {
|
|
296
316
|
continue;
|
|
297
317
|
}
|
|
298
318
|
const absolutePath = resolveReadPath(resolvedPath, cwd);
|
|
319
|
+
// Duplicate suppression, keyed by absolute path: same path shown earlier this
|
|
320
|
+
// batch or recently in the conversation → compact note, no re-read (the path
|
|
321
|
+
// stays referenced so the model can re-fetch via the read tool).
|
|
322
|
+
if (shownInBatch.has(absolutePath) || options?.recentlyShownPaths?.has(absolutePath)) {
|
|
323
|
+
files.push({
|
|
324
|
+
path: resolvedPath,
|
|
325
|
+
content: `(already shown in this conversation; not re-read — use the read tool to fetch \`${resolvedPath}\` again)`,
|
|
326
|
+
duplicate: true,
|
|
327
|
+
});
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
shownInBatch.add(absolutePath);
|
|
299
331
|
try {
|
|
300
332
|
const stat = await Bun.file(absolutePath).stat();
|
|
301
333
|
if (stat.isDirectory()) {
|
|
@@ -354,7 +386,7 @@ export async function generateFileMentionMessages(
|
|
|
354
386
|
}
|
|
355
387
|
|
|
356
388
|
const content = await Bun.file(absolutePath).text();
|
|
357
|
-
let { output, lineCount } = buildTextOutput(content);
|
|
389
|
+
let { output, lineCount } = buildTextOutput(content, options?.maxInlineBytes);
|
|
358
390
|
if (options?.useHashLines) {
|
|
359
391
|
output = formatHashLines(output);
|
|
360
392
|
}
|
package/src/utils/git.ts
CHANGED
|
@@ -1317,6 +1317,79 @@ export async function clean(
|
|
|
1317
1317
|
await runEffect(cwd, args, { signal: options.signal });
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1321
|
+
// API: bisect
|
|
1322
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1323
|
+
|
|
1324
|
+
export interface BisectCommitInfo {
|
|
1325
|
+
sha: string;
|
|
1326
|
+
author: string;
|
|
1327
|
+
date: string;
|
|
1328
|
+
subject: string;
|
|
1329
|
+
stat: string;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Thin wrappers over `git bisect`. The marking commands (`good`/`bad`/`skip`)
|
|
1334
|
+
* return the raw {@link GitCommandResult} without throwing so callers can both
|
|
1335
|
+
* detect a genuine failure (non-zero exit) and scan the output for the
|
|
1336
|
+
* "<sha> is the first bad commit" conclusion. `reset` is best-effort and never
|
|
1337
|
+
* throws so it is safe to call from a `finally` block to restore the worktree.
|
|
1338
|
+
*/
|
|
1339
|
+
export const bisect = {
|
|
1340
|
+
/** Begin a bisect session (does not seed good/bad endpoints). */
|
|
1341
|
+
async start(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1342
|
+
await runEffect(cwd, ["bisect", "start"], { signal });
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
/** Mark a revision (or current HEAD when omitted) good. Returns raw output. */
|
|
1346
|
+
async good(cwd: string, ref?: string, signal?: AbortSignal): Promise<GitCommandResult> {
|
|
1347
|
+
return runCommand(cwd, ref ? ["bisect", "good", ref] : ["bisect", "good"], { signal });
|
|
1348
|
+
},
|
|
1349
|
+
|
|
1350
|
+
/** Mark a revision (or current HEAD when omitted) bad. Returns raw output. */
|
|
1351
|
+
async bad(cwd: string, ref?: string, signal?: AbortSignal): Promise<GitCommandResult> {
|
|
1352
|
+
return runCommand(cwd, ref ? ["bisect", "bad", ref] : ["bisect", "bad"], { signal });
|
|
1353
|
+
},
|
|
1354
|
+
|
|
1355
|
+
/** Skip the current (untestable) revision. Returns raw output. */
|
|
1356
|
+
async skip(cwd: string, signal?: AbortSignal): Promise<GitCommandResult> {
|
|
1357
|
+
return runCommand(cwd, ["bisect", "skip"], { signal });
|
|
1358
|
+
},
|
|
1359
|
+
|
|
1360
|
+
/** Read the replayable bisect log for the current session. */
|
|
1361
|
+
async log(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
1362
|
+
return runText(cwd, ["bisect", "log"], { signal });
|
|
1363
|
+
},
|
|
1364
|
+
|
|
1365
|
+
/** End the bisect session, restoring the original HEAD. Never throws. */
|
|
1366
|
+
async reset(cwd: string, signal?: AbortSignal): Promise<void> {
|
|
1367
|
+
await runCommand(cwd, ["bisect", "reset"], { signal });
|
|
1368
|
+
},
|
|
1369
|
+
|
|
1370
|
+
/** True when `ancestor` is a first-parent-or-merge ancestor of `descendant`. */
|
|
1371
|
+
async isAncestor(cwd: string, ancestor: string, descendant: string, signal?: AbortSignal): Promise<boolean> {
|
|
1372
|
+
const result = await runCommand(cwd, ["merge-base", "--is-ancestor", ancestor, descendant], {
|
|
1373
|
+
readOnly: true,
|
|
1374
|
+
signal,
|
|
1375
|
+
});
|
|
1376
|
+
return result.exitCode === 0;
|
|
1377
|
+
},
|
|
1378
|
+
|
|
1379
|
+
/** Resolve author/date/subject and a diffstat for a commit (for the receipt). */
|
|
1380
|
+
async describe(cwd: string, sha: string, signal?: AbortSignal): Promise<BisectCommitInfo> {
|
|
1381
|
+
// Newline-delimited so the format string carries no NUL byte (argv cannot
|
|
1382
|
+
// contain NUL); %s is already a single line, so the four fields never wrap.
|
|
1383
|
+
const meta = await runText(cwd, ["show", "-s", "--format=%H%n%an <%ae>%n%aI%n%s", sha], {
|
|
1384
|
+
readOnly: true,
|
|
1385
|
+
signal,
|
|
1386
|
+
});
|
|
1387
|
+
const [resolvedSha = sha, author = "", date = "", subject = ""] = meta.split("\n");
|
|
1388
|
+
const stat = await runText(cwd, ["show", "--stat", "--format=", sha], { readOnly: true, signal });
|
|
1389
|
+
return { sha: resolvedSha, author, date, subject, stat: stat.trim() };
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1320
1393
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1321
1394
|
// API: ls
|
|
1322
1395
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -10,44 +10,56 @@ import { extractWithParallel, findParallelApiKey, getParallelExtractContent } fr
|
|
|
10
10
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
11
11
|
import { buildResult, formatMediaDuration, formatNumber } from "./types";
|
|
12
12
|
|
|
13
|
-
interface YouTubeUrl {
|
|
13
|
+
export interface YouTubeUrl {
|
|
14
14
|
videoId: string;
|
|
15
15
|
playlistId?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[a-zA-Z0-9_-]{11}$/;
|
|
19
|
+
|
|
20
|
+
function isYouTubeHost(hostname: string): boolean {
|
|
21
|
+
return hostname === "youtube.com" || hostname === "m.youtube.com" || hostname === "music.youtube.com";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isYouTubeEmbedHost(hostname: string): boolean {
|
|
25
|
+
return isYouTubeHost(hostname) || hostname === "youtube-nocookie.com";
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Parse YouTube URL into components
|
|
20
30
|
*/
|
|
21
|
-
function parseYouTubeUrl(url: string): YouTubeUrl | null {
|
|
31
|
+
export function parseYouTubeUrl(url: string): YouTubeUrl | null {
|
|
22
32
|
try {
|
|
23
33
|
const parsed = new URL(url);
|
|
24
34
|
const hostname = parsed.hostname.replace(/^www\./, "");
|
|
25
35
|
|
|
26
|
-
// youtube.com/watch?v=VIDEO_ID
|
|
27
|
-
if ((hostname
|
|
36
|
+
// youtube.com/watch?v=VIDEO_ID and music.youtube.com/watch?v=VIDEO_ID
|
|
37
|
+
if (isYouTubeHost(hostname) && parsed.pathname === "/watch") {
|
|
28
38
|
const videoId = parsed.searchParams.get("v");
|
|
29
39
|
const playlistId = parsed.searchParams.get("list") || undefined;
|
|
30
|
-
if (videoId) return { videoId, playlistId };
|
|
40
|
+
if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) return { videoId, playlistId };
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
// youtube.com/v/VIDEO_ID
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
// youtube.com/v/VIDEO_ID, youtube.com/embed/VIDEO_ID, youtube.com/live/VIDEO_ID,
|
|
44
|
+
// and youtube-nocookie.com/embed/VIDEO_ID. Require the id segment to end
|
|
45
|
+
// before matching so malformed overlong ids are not silently truncated.
|
|
46
|
+
if (isYouTubeEmbedHost(hostname)) {
|
|
47
|
+
const match = parsed.pathname.match(/^\/(v|embed|live)\/([a-zA-Z0-9_-]{11})(?:$|\/)/);
|
|
36
48
|
if (match) return { videoId: match[2] };
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
// youtu.be/VIDEO_ID
|
|
40
52
|
if (hostname === "youtu.be") {
|
|
41
53
|
const videoId = parsed.pathname.slice(1).split("/")[0];
|
|
42
|
-
if (videoId &&
|
|
54
|
+
if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) {
|
|
43
55
|
return { videoId };
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
// youtube.com/shorts/VIDEO_ID
|
|
48
|
-
if (hostname
|
|
59
|
+
// youtube.com/shorts/VIDEO_ID and m.youtube.com/shorts/VIDEO_ID
|
|
60
|
+
if (isYouTubeHost(hostname) && parsed.pathname.startsWith("/shorts/")) {
|
|
49
61
|
const videoId = parsed.pathname.replace("/shorts/", "").split("/")[0];
|
|
50
|
-
if (videoId &&
|
|
62
|
+
if (videoId && YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) {
|
|
51
63
|
return { videoId };
|
|
52
64
|
}
|
|
53
65
|
}
|