@gajae-code/coding-agent 0.8.1 → 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 +63 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -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/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/config/settings.d.ts +8 -0
- 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/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -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 +28 -1
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- 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/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- 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 +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +53 -1
- package/dist/types/session/agent-storage.d.ts +1 -0
- 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 +3 -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/computer.d.ts +2 -2
- 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/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -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/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- 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/notify-cli.ts +91 -7
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
- package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
- package/src/defaults/gjc/skills/team/SKILL.md +17 -14
- 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 +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +44 -2
- package/src/gjc-runtime/session-state-sidecar.ts +263 -20
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +187 -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 +1663 -159
- 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 +124 -4
- package/src/internal-urls/docs-index.generated.ts +16 -7
- package/src/main.ts +95 -43
- 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/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +51 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- 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/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +19 -6
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- 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 +29 -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/computer.md +2 -2
- 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/runtime-api-key.ts +16 -0
- package/src/sdk.ts +52 -28
- package/src/session/agent-session.ts +951 -132
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- 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 +218 -71
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- 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/computer.ts +246 -43
- 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/tools/telegram-send.ts +6 -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/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -420,6 +420,10 @@ FROM model_usage_legacy
|
|
|
420
420
|
this.#authStore.setCache(key, value, expiresAtSec);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
deleteCachePrefix(prefix: string): void {
|
|
424
|
+
this.#authStore.deleteCachePrefix?.(prefix);
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
/**
|
|
424
428
|
* Deletes expired cache entries. Call periodically for cleanup.
|
|
425
429
|
*/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staleness pruning for auto-read file-mention entries.
|
|
3
|
+
*
|
|
4
|
+
* A `@path` mention is superseded once a newer read or mention of the same
|
|
5
|
+
* resolved path appears later in the branch. Stale bodies are replaced with an
|
|
6
|
+
* explicit digest notice (never silently deleted) so context shrinks without
|
|
7
|
+
* losing the reference.
|
|
8
|
+
*/
|
|
9
|
+
import type { FileMentionMessage } from "./messages";
|
|
10
|
+
import type { SessionEntry, SessionMessageEntry } from "./session-manager";
|
|
11
|
+
|
|
12
|
+
export interface FileMentionPruneResult {
|
|
13
|
+
changed: SessionMessageEntry[];
|
|
14
|
+
bytesSaved: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Notice injected in place of a superseded file-mention body. */
|
|
18
|
+
export function stalefileMentionNotice(path: string): string {
|
|
19
|
+
return `(pruned: a newer view of \`${path}\` appears later in the conversation; use the read tool to fetch it again)`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function pruneStaleFileMentions(
|
|
23
|
+
entries: readonly SessionEntry[],
|
|
24
|
+
resolveAbs: (path: string) => string,
|
|
25
|
+
): FileMentionPruneResult {
|
|
26
|
+
// Latest branch index at which each absolute path was shown (read or mention).
|
|
27
|
+
const latestShownIndex = new Map<string, number>();
|
|
28
|
+
entries.forEach((entry, i) => {
|
|
29
|
+
if (entry.type !== "message") return;
|
|
30
|
+
const msg = entry.message;
|
|
31
|
+
if (msg.role === "fileMention") {
|
|
32
|
+
for (const file of msg.files) {
|
|
33
|
+
if (file.duplicate || file.pruned) continue;
|
|
34
|
+
latestShownIndex.set(resolveAbs(file.path), i);
|
|
35
|
+
}
|
|
36
|
+
} else if (msg.role === "toolResult") {
|
|
37
|
+
const resolved = (msg.details as { resolvedPath?: unknown } | undefined)?.resolvedPath;
|
|
38
|
+
if (typeof resolved === "string" && resolved) latestShownIndex.set(resolveAbs(resolved), i);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const changed: SessionMessageEntry[] = [];
|
|
43
|
+
let bytesSaved = 0;
|
|
44
|
+
entries.forEach((entry, i) => {
|
|
45
|
+
if (entry.type !== "message" || entry.message.role !== "fileMention") return;
|
|
46
|
+
const msg = entry.message as FileMentionMessage;
|
|
47
|
+
let mutated = false;
|
|
48
|
+
for (const file of msg.files) {
|
|
49
|
+
if (file.duplicate || file.pruned || !file.content) continue;
|
|
50
|
+
const latest = latestShownIndex.get(resolveAbs(file.path));
|
|
51
|
+
if (latest !== undefined && latest > i) {
|
|
52
|
+
const notice = stalefileMentionNotice(file.path);
|
|
53
|
+
bytesSaved += Math.max(0, Buffer.byteLength(file.content, "utf-8") - Buffer.byteLength(notice, "utf-8"));
|
|
54
|
+
file.content = notice;
|
|
55
|
+
file.pruned = true;
|
|
56
|
+
file.image = undefined;
|
|
57
|
+
file.lineCount = undefined;
|
|
58
|
+
mutated = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (mutated) changed.push(entry as SessionMessageEntry);
|
|
62
|
+
});
|
|
63
|
+
return { changed, bytesSaved };
|
|
64
|
+
}
|
package/src/session/messages.ts
CHANGED
|
@@ -192,6 +192,10 @@ export interface FileMentionMessage {
|
|
|
192
192
|
/** Why the file contents were omitted from auto-read. */
|
|
193
193
|
skippedReason?: "tooLarge";
|
|
194
194
|
image?: ImageContent;
|
|
195
|
+
/** Set when this mention duplicated a recently shown path (compact note, no full body). */
|
|
196
|
+
duplicate?: boolean;
|
|
197
|
+
/** Set when a stale entry was pruned to a digest notice (never silently deleted). */
|
|
198
|
+
pruned?: boolean;
|
|
195
199
|
}>;
|
|
196
200
|
timestamp: number;
|
|
197
201
|
}
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
Snowflake,
|
|
28
28
|
toError,
|
|
29
29
|
} from "@gajae-code/utils";
|
|
30
|
+
import type { TtsrInjectionRecord } from "../export/ttsr";
|
|
30
31
|
import { writeTextAtomic } from "../gjc-runtime/state-writer";
|
|
32
|
+
|
|
31
33
|
import * as git from "../utils/git";
|
|
32
34
|
import { ArtifactManager } from "./artifacts";
|
|
33
35
|
import {
|
|
@@ -221,6 +223,10 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
221
223
|
type: "ttsr_injection";
|
|
222
224
|
/** Names of rules that were injected */
|
|
223
225
|
injectedRules: string[];
|
|
226
|
+
/** Rich rule injection records with repeat state. */
|
|
227
|
+
injectedRuleRecords?: TtsrInjectionRecord[];
|
|
228
|
+
/** TTSR manager message count when this injection was recorded. */
|
|
229
|
+
ttsrMessageCount?: number;
|
|
224
230
|
}
|
|
225
231
|
|
|
226
232
|
/** Persisted MCP discovery selection state for a session branch. */
|
|
@@ -313,6 +319,11 @@ export interface SessionContext {
|
|
|
313
319
|
models: Record<string, string>;
|
|
314
320
|
/** Names of TTSR rules that have been injected this session */
|
|
315
321
|
injectedTtsrRules: string[];
|
|
322
|
+
/** Rich TTSR rule injection records for repeat resume. */
|
|
323
|
+
injectedTtsrRuleRecords?: TtsrInjectionRecord[];
|
|
324
|
+
/** TTSR manager message count for repeat resume. */
|
|
325
|
+
ttsrMessageCount?: number;
|
|
326
|
+
|
|
316
327
|
/** MCP tool names selected through discovery for this session branch. */
|
|
317
328
|
selectedMCPToolNames: string[];
|
|
318
329
|
/** Whether this branch contains an explicit persisted MCP selection entry. */
|
|
@@ -612,6 +623,9 @@ export function buildSessionContext(
|
|
|
612
623
|
serviceTier: undefined,
|
|
613
624
|
models: {},
|
|
614
625
|
injectedTtsrRules: [],
|
|
626
|
+
injectedTtsrRuleRecords: [],
|
|
627
|
+
ttsrMessageCount: 0,
|
|
628
|
+
|
|
615
629
|
selectedMCPToolNames: [],
|
|
616
630
|
hasPersistedMCPToolSelection: false,
|
|
617
631
|
mode: "none",
|
|
@@ -632,6 +646,8 @@ export function buildSessionContext(
|
|
|
632
646
|
serviceTier: undefined,
|
|
633
647
|
models: {},
|
|
634
648
|
injectedTtsrRules: [],
|
|
649
|
+
injectedTtsrRuleRecords: [],
|
|
650
|
+
ttsrMessageCount: 0,
|
|
635
651
|
selectedMCPToolNames: [],
|
|
636
652
|
hasPersistedMCPToolSelection: false,
|
|
637
653
|
mode: "none",
|
|
@@ -656,6 +672,9 @@ export function buildSessionContext(
|
|
|
656
672
|
const models: Record<string, string> = {};
|
|
657
673
|
let compaction: CompactionEntry | null = null;
|
|
658
674
|
const injectedTtsrRulesSet = new Set<string>();
|
|
675
|
+
const injectedTtsrRuleRecords = new Map<string, TtsrInjectionRecord>();
|
|
676
|
+
let ttsrMessageCount = 0;
|
|
677
|
+
|
|
659
678
|
let selectedMCPToolNames: string[] = [];
|
|
660
679
|
let hasPersistedMCPToolSelection = false;
|
|
661
680
|
let mode = "none";
|
|
@@ -695,9 +714,19 @@ export function buildSessionContext(
|
|
|
695
714
|
} else if (entry.type === "compaction") {
|
|
696
715
|
compaction = entry;
|
|
697
716
|
} else if (entry.type === "ttsr_injection") {
|
|
698
|
-
// Collect injected TTSR rule names
|
|
717
|
+
// Collect injected TTSR rule names and richer records when present.
|
|
699
718
|
for (const ruleName of entry.injectedRules) {
|
|
700
719
|
injectedTtsrRulesSet.add(ruleName);
|
|
720
|
+
if (!injectedTtsrRuleRecords.has(ruleName)) {
|
|
721
|
+
injectedTtsrRuleRecords.set(ruleName, { name: ruleName, lastInjectedAt: 0 });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
for (const record of entry.injectedRuleRecords ?? []) {
|
|
725
|
+
injectedTtsrRulesSet.add(record.name);
|
|
726
|
+
injectedTtsrRuleRecords.set(record.name, record);
|
|
727
|
+
}
|
|
728
|
+
if (typeof entry.ttsrMessageCount === "number" && Number.isFinite(entry.ttsrMessageCount)) {
|
|
729
|
+
ttsrMessageCount = entry.ttsrMessageCount;
|
|
701
730
|
}
|
|
702
731
|
} else if (entry.type === "mcp_tool_selection") {
|
|
703
732
|
selectedMCPToolNames = [...entry.selectedToolNames];
|
|
@@ -709,6 +738,7 @@ export function buildSessionContext(
|
|
|
709
738
|
}
|
|
710
739
|
|
|
711
740
|
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
741
|
+
const injectedTtsrRuleRecordsArray = Array.from(injectedTtsrRuleRecords.values());
|
|
712
742
|
|
|
713
743
|
// Build messages and collect corresponding entries
|
|
714
744
|
// When there's a compaction, we need to:
|
|
@@ -797,6 +827,9 @@ export function buildSessionContext(
|
|
|
797
827
|
serviceTier,
|
|
798
828
|
models,
|
|
799
829
|
injectedTtsrRules,
|
|
830
|
+
injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
|
|
831
|
+
ttsrMessageCount,
|
|
832
|
+
|
|
800
833
|
selectedMCPToolNames,
|
|
801
834
|
hasPersistedMCPToolSelection,
|
|
802
835
|
mode,
|
|
@@ -810,6 +843,9 @@ function cloneSessionContext(context: SessionContext): SessionContext {
|
|
|
810
843
|
messages: cloneJsonSemantic(context.messages),
|
|
811
844
|
models: { ...context.models },
|
|
812
845
|
injectedTtsrRules: [...context.injectedTtsrRules],
|
|
846
|
+
injectedTtsrRuleRecords: context.injectedTtsrRuleRecords?.map(record => ({ ...record })),
|
|
847
|
+
ttsrMessageCount: context.ttsrMessageCount,
|
|
848
|
+
|
|
813
849
|
selectedMCPToolNames: [...context.selectedMCPToolNames],
|
|
814
850
|
modeData: cloneJsonSemantic(context.modeData),
|
|
815
851
|
};
|
|
@@ -1308,6 +1344,45 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
1308
1344
|
);
|
|
1309
1345
|
}
|
|
1310
1346
|
|
|
1347
|
+
function stripUndefinedPlainObjectFields(value: unknown, path = "entry"): unknown {
|
|
1348
|
+
if (value === undefined) return undefined;
|
|
1349
|
+
if (value === null || typeof value !== "object") return value;
|
|
1350
|
+
if (Array.isArray(value)) {
|
|
1351
|
+
let changed = false;
|
|
1352
|
+
const result: unknown[] = new Array(value.length);
|
|
1353
|
+
for (let index = 0; index < value.length; index++) {
|
|
1354
|
+
const item = value[index];
|
|
1355
|
+
if (item === undefined) {
|
|
1356
|
+
throw new Error(`Session entry contains undefined array item at ${path}[${index}]`);
|
|
1357
|
+
}
|
|
1358
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}[${index}]`);
|
|
1359
|
+
if (next !== item) changed = true;
|
|
1360
|
+
result[index] = next;
|
|
1361
|
+
}
|
|
1362
|
+
return changed ? result : value;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1366
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
1367
|
+
|
|
1368
|
+
let changed = false;
|
|
1369
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
1370
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1371
|
+
if (item === undefined) {
|
|
1372
|
+
changed = true;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}.${key}`);
|
|
1376
|
+
if (next !== item) changed = true;
|
|
1377
|
+
entries.push([key, next]);
|
|
1378
|
+
}
|
|
1379
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function normalizeSessionEntryForStorage(entry: SessionEntry): SessionEntry {
|
|
1383
|
+
return stripUndefinedPlainObjectFields(entry) as SessionEntry;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1311
1386
|
const RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS = new Set([
|
|
1312
1387
|
"id",
|
|
1313
1388
|
"type",
|
|
@@ -2254,10 +2329,12 @@ class NdjsonFileWriter {
|
|
|
2254
2329
|
}
|
|
2255
2330
|
}
|
|
2256
2331
|
|
|
2332
|
+
const DEFAULT_WELCOME_RECENT_SESSION_LIMIT = 20;
|
|
2333
|
+
|
|
2257
2334
|
/** Get recent sessions for display in welcome screen */
|
|
2258
2335
|
export async function getRecentSessions(
|
|
2259
2336
|
sessionDir: string,
|
|
2260
|
-
limit =
|
|
2337
|
+
limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
|
|
2261
2338
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2262
2339
|
): Promise<RecentSessionInfo[]> {
|
|
2263
2340
|
const sessions = await getSortedSessions(sessionDir, storage);
|
|
@@ -3734,7 +3811,8 @@ export class SessionManager {
|
|
|
3734
3811
|
}
|
|
3735
3812
|
|
|
3736
3813
|
#appendEntry(entry: SessionEntry): void {
|
|
3737
|
-
const
|
|
3814
|
+
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
3815
|
+
const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
|
|
3738
3816
|
this.#fileEntries.push(residentEntry);
|
|
3739
3817
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
3740
3818
|
this.#leafId = residentEntry.id;
|
|
@@ -3915,6 +3993,25 @@ export class SessionManager {
|
|
|
3915
3993
|
return entry.id;
|
|
3916
3994
|
}
|
|
3917
3995
|
|
|
3996
|
+
/**
|
|
3997
|
+
* Append a root marker that starts a fresh active branch without changing the
|
|
3998
|
+
* session id or deleting earlier durable entries. Subsequent messages descend
|
|
3999
|
+
* from this marker, so provider context is clear while history remains
|
|
4000
|
+
* available for diagnostics/export.
|
|
4001
|
+
*/
|
|
4002
|
+
appendContextClearEntry(data?: Record<string, unknown>): string {
|
|
4003
|
+
const entry: CustomEntry = {
|
|
4004
|
+
type: "custom",
|
|
4005
|
+
customType: "context_clear",
|
|
4006
|
+
data,
|
|
4007
|
+
id: generateId(this.#byId),
|
|
4008
|
+
parentId: null,
|
|
4009
|
+
timestamp: new Date().toISOString(),
|
|
4010
|
+
};
|
|
4011
|
+
this.#appendEntry(entry);
|
|
4012
|
+
return entry.id;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3918
4015
|
/**
|
|
3919
4016
|
* Write mutated message entries back into the canonical entry store by id.
|
|
3920
4017
|
*
|
|
@@ -4006,13 +4103,15 @@ export class SessionManager {
|
|
|
4006
4103
|
* @param ruleNames Names of rules that were injected
|
|
4007
4104
|
* @returns Entry id
|
|
4008
4105
|
*/
|
|
4009
|
-
appendTtsrInjection(ruleNames: string[]): string {
|
|
4106
|
+
appendTtsrInjection(ruleNames: string[], records?: TtsrInjectionRecord[], ttsrMessageCount?: number): string {
|
|
4010
4107
|
const entry: TtsrInjectionEntry = {
|
|
4011
4108
|
type: "ttsr_injection",
|
|
4012
4109
|
id: generateId(this.#byId),
|
|
4013
4110
|
parentId: this.#leafId,
|
|
4014
4111
|
timestamp: new Date().toISOString(),
|
|
4015
4112
|
injectedRules: ruleNames,
|
|
4113
|
+
injectedRuleRecords: records,
|
|
4114
|
+
ttsrMessageCount,
|
|
4016
4115
|
};
|
|
4017
4116
|
this.#appendEntry(entry);
|
|
4018
4117
|
return entry.id;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import * as logger from "@gajae-code/utils/logger";
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
activeSnapshotPath,
|
|
6
|
+
activeStateDir,
|
|
7
|
+
assertNonEmptyGjcSessionId,
|
|
8
|
+
modeStatePath,
|
|
9
|
+
} from "../gjc-runtime/session-layout";
|
|
3
10
|
import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-runtime/session-resolution";
|
|
4
11
|
import {
|
|
5
12
|
type ActiveSessionScope,
|
|
6
13
|
readActiveEntries,
|
|
7
14
|
rebuildActiveSnapshot,
|
|
8
15
|
removeActiveEntry,
|
|
16
|
+
setActiveStateCacheInvalidator,
|
|
9
17
|
writeActiveEntry,
|
|
10
18
|
} from "../gjc-runtime/state-writer";
|
|
11
19
|
import { CANONICAL_GJC_WORKFLOW_SKILLS, type CanonicalGjcWorkflowSkill } from "./canonical-skills";
|
|
@@ -614,14 +622,108 @@ async function mergeVisibleEntries(
|
|
|
614
622
|
return collapsePlanningPipeline(visibleEntries).toSorted(comparePipelineEntry);
|
|
615
623
|
}
|
|
616
624
|
|
|
617
|
-
export
|
|
618
|
-
|
|
625
|
+
export type VisibleSkillActiveStateCacheTier = "security" | "hud";
|
|
626
|
+
export interface ReadVisibleSkillActiveStateOptions {
|
|
627
|
+
tier?: VisibleSkillActiveStateCacheTier;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
interface ActiveStateStatSignature {
|
|
631
|
+
path: string;
|
|
632
|
+
mtimeMs: number;
|
|
633
|
+
size: number;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
interface ActiveStateSignature {
|
|
637
|
+
sessionPath: ActiveStateStatSignature | null;
|
|
638
|
+
activeDir: ActiveStateStatSignature | null;
|
|
639
|
+
activeEntries: ActiveStateStatSignature[];
|
|
640
|
+
ralplanModeState: ActiveStateStatSignature | null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
interface VisibleSkillActiveStateCacheEntry {
|
|
644
|
+
signature: ActiveStateSignature;
|
|
645
|
+
value: SkillActiveState | null;
|
|
646
|
+
cachedAtMs: number;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const HUD_VISIBLE_STATE_TTL_MS = 500;
|
|
650
|
+
const SECURITY_VISIBLE_STATE_TTL_MS = 30_000;
|
|
651
|
+
const visibleSkillActiveStateCache = new Map<string, VisibleSkillActiveStateCacheEntry>();
|
|
652
|
+
|
|
653
|
+
async function statSignature(filePath: string): Promise<ActiveStateStatSignature | null> {
|
|
619
654
|
try {
|
|
620
|
-
|
|
655
|
+
const stat = await fs.stat(filePath);
|
|
656
|
+
return { path: filePath, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
621
657
|
} catch (error) {
|
|
622
|
-
if (error
|
|
658
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
623
659
|
throw error;
|
|
624
660
|
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function signaturesEqual(a: ActiveStateSignature, b: ActiveStateSignature): boolean {
|
|
664
|
+
const statEqual = (left: ActiveStateStatSignature | null, right: ActiveStateStatSignature | null): boolean =>
|
|
665
|
+
left === right ||
|
|
666
|
+
(Boolean(left) &&
|
|
667
|
+
Boolean(right) &&
|
|
668
|
+
left?.path === right?.path &&
|
|
669
|
+
left?.mtimeMs === right?.mtimeMs &&
|
|
670
|
+
left?.size === right?.size);
|
|
671
|
+
if (!statEqual(a.sessionPath, b.sessionPath)) return false;
|
|
672
|
+
if (!statEqual(a.activeDir, b.activeDir)) return false;
|
|
673
|
+
if (!statEqual(a.ralplanModeState, b.ralplanModeState)) return false;
|
|
674
|
+
if (a.activeEntries.length !== b.activeEntries.length) return false;
|
|
675
|
+
return a.activeEntries.every((entry, index) => statEqual(entry, b.activeEntries[index] ?? null));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function computeActiveStateSignature(cwd: string, sessionId: string): Promise<ActiveStateSignature> {
|
|
679
|
+
const resolvedCwd = path.resolve(cwd);
|
|
680
|
+
const { sessionPath } = getSkillActiveStatePaths(resolvedCwd, sessionId);
|
|
681
|
+
const activeDirPath = activeStateDir(resolvedCwd, sessionId);
|
|
682
|
+
const activeDir = await statSignature(activeDirPath);
|
|
683
|
+
let activeEntries: ActiveStateStatSignature[] = [];
|
|
684
|
+
try {
|
|
685
|
+
const names = await fs.readdir(activeDirPath);
|
|
686
|
+
activeEntries = (
|
|
687
|
+
await Promise.all(
|
|
688
|
+
names
|
|
689
|
+
.filter(name => name.endsWith(".json"))
|
|
690
|
+
.sort()
|
|
691
|
+
.map(name => statSignature(path.join(activeDirPath, name))),
|
|
692
|
+
)
|
|
693
|
+
).filter((entry): entry is ActiveStateStatSignature => entry !== null);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
sessionPath: await statSignature(sessionPath),
|
|
699
|
+
activeDir,
|
|
700
|
+
activeEntries,
|
|
701
|
+
ralplanModeState: await statSignature(modeStatePath(resolvedCwd, sessionId, "ralplan")),
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function visibleActiveStateCacheKey(cwd: string, sessionId: string): string {
|
|
706
|
+
return `${path.resolve(cwd)}\0${sessionId}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function invalidateVisibleSkillActiveStateCache(cwd?: string, sessionId?: string): void {
|
|
710
|
+
if (!cwd && !sessionId) {
|
|
711
|
+
visibleSkillActiveStateCache.clear();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const resolvedCwd = cwd ? path.resolve(cwd) : undefined;
|
|
715
|
+
const suffix = sessionId ? `\0${sessionId}` : undefined;
|
|
716
|
+
for (const key of [...visibleSkillActiveStateCache.keys()]) {
|
|
717
|
+
if (resolvedCwd && !key.startsWith(`${resolvedCwd}\0`)) continue;
|
|
718
|
+
if (suffix && !key.endsWith(suffix)) continue;
|
|
719
|
+
visibleSkillActiveStateCache.delete(key);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function readVisibleSkillActiveStateUncached(
|
|
724
|
+
cwd: string,
|
|
725
|
+
resolvedSessionId: string,
|
|
726
|
+
): Promise<SkillActiveState | null> {
|
|
625
727
|
const { sessionPath } = getSkillActiveStatePaths(cwd, resolvedSessionId);
|
|
626
728
|
const sessionState = await readRawActiveStateForHandoff(sessionPath, false);
|
|
627
729
|
const activeSkills = await mergeVisibleEntries(cwd, sessionState, resolvedSessionId);
|
|
@@ -639,6 +741,40 @@ export async function readVisibleSkillActiveState(cwd: string, sessionId?: strin
|
|
|
639
741
|
};
|
|
640
742
|
}
|
|
641
743
|
|
|
744
|
+
setActiveStateCacheInvalidator(invalidateVisibleSkillActiveStateCache);
|
|
745
|
+
|
|
746
|
+
export async function readVisibleSkillActiveState(
|
|
747
|
+
cwd: string,
|
|
748
|
+
sessionId?: string,
|
|
749
|
+
opts?: ReadVisibleSkillActiveStateOptions,
|
|
750
|
+
): Promise<SkillActiveState | null> {
|
|
751
|
+
let resolvedSessionId: string;
|
|
752
|
+
try {
|
|
753
|
+
resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (error instanceof SessionResolutionError && error.code === "no_session") return null;
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
const resolvedCwd = path.resolve(cwd);
|
|
759
|
+
const cacheKey = visibleActiveStateCacheKey(resolvedCwd, resolvedSessionId);
|
|
760
|
+
const tier = opts?.tier ?? "security";
|
|
761
|
+
const now = Date.now();
|
|
762
|
+
const cached = visibleSkillActiveStateCache.get(cacheKey);
|
|
763
|
+
if (tier === "hud" && cached && now - cached.cachedAtMs < HUD_VISIBLE_STATE_TTL_MS) return cached.value;
|
|
764
|
+
|
|
765
|
+
const signature = await computeActiveStateSignature(resolvedCwd, resolvedSessionId);
|
|
766
|
+
if (
|
|
767
|
+
cached &&
|
|
768
|
+
signaturesEqual(cached.signature, signature) &&
|
|
769
|
+
(tier === "hud" || now - cached.cachedAtMs < SECURITY_VISIBLE_STATE_TTL_MS)
|
|
770
|
+
) {
|
|
771
|
+
return cached.value;
|
|
772
|
+
}
|
|
773
|
+
const value = await readVisibleSkillActiveStateUncached(resolvedCwd, resolvedSessionId);
|
|
774
|
+
visibleSkillActiveStateCache.set(cacheKey, { signature, value, cachedAtMs: now });
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
|
|
642
778
|
function activeStateWriterAudit(verb: string, sessionScope?: ActiveSessionScope | string) {
|
|
643
779
|
const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
|
|
644
780
|
return { category: "state" as const, verb, owner: "gjc-runtime" as const, ...(sessionId ? { sessionId } : {}) };
|
|
@@ -33,10 +33,18 @@ function planningPhaseBlockMessage(skill: CanonicalGjcWorkflowSkill): string {
|
|
|
33
33
|
return DEEP_INTERVIEW_MUTATION_BLOCK_MESSAGE;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit"]);
|
|
36
|
+
const BLOCKED_TOOL_NAMES = new Set(["edit", "write", "ast_edit", "bash"]);
|
|
37
37
|
const ARCHIVE_OR_SQLITE_BASE_RE = /^(.+?\.(?:tar\.gz|sqlite3|sqlite|db3|zip|tgz|tar|db))(?:$|:)/i;
|
|
38
38
|
const INTERNAL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
39
39
|
const VIM_FILE_SWITCH_RE = /^\s*:(?:e|e!|edit|edit!)(?:\s+([^<\r\n]+))?(?:<CR>|\r|\n|$)/i;
|
|
40
|
+
const BASH_MUTATION_COMMAND_RE =
|
|
41
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:tee|touch|rm|mkdir|cp|mv|install|truncate)\b([^;&|\n]*)|(?:^|[^<>])(?:>>?|\d>>?)\s*([^\s;&|]+)/gi;
|
|
42
|
+
const BASH_IN_PLACE_MUTATION_COMMAND_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:sed|perl)\b([^;&|\n]*)/gi;
|
|
43
|
+
const BASH_OPAQUE_INTERPRETER_WRITE_RE =
|
|
44
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:-c|-e)\b[^;&|\n]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
45
|
+
const BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE =
|
|
46
|
+
/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(?:python3?|node|ruby)\b[^;&|\n]*(?:<<[-]?\s*['"]?\w+['"]?)[\s\S]*(?:open\s*\(|writeFile(?:Sync)?\s*\(|\.write\s*\()/i;
|
|
47
|
+
const BASH_DD_OUTPUT_RE = /(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?dd\b([^;&|\n]*)/gi;
|
|
40
48
|
|
|
41
49
|
type ToolWithEditMode = AgentTool & {
|
|
42
50
|
mode?: unknown;
|
|
@@ -214,7 +222,7 @@ async function getActivePlanningSkill(
|
|
|
214
222
|
): Promise<ActivePlanningSkill | null> {
|
|
215
223
|
const resolvedSessionId = await resolveBoundarySessionId(cwd, sessionId);
|
|
216
224
|
if (!resolvedSessionId) return null;
|
|
217
|
-
const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId);
|
|
225
|
+
const skillState = await readVisibleSkillActiveState(cwd, resolvedSessionId, { tier: "security" });
|
|
218
226
|
if (!skillState) return null;
|
|
219
227
|
const activeEntries = listActiveSkills(skillState).filter(entry =>
|
|
220
228
|
entryMatchesContext(entry, resolvedSessionId, threadId),
|
|
@@ -329,11 +337,66 @@ function extractEditTargets(args: unknown, tool: ToolWithEditMode): ExtractedTar
|
|
|
329
337
|
if (targets.paths.length === 0) targets.unknown = true;
|
|
330
338
|
return targets;
|
|
331
339
|
}
|
|
340
|
+
function shellWords(argsText: string): string[] {
|
|
341
|
+
return argsText.match(/(?:[^\s'"\\]+|'[^']*'|"[^"]*")+/g) ?? [];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function cleanShellWord(value: string): string {
|
|
345
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
346
|
+
}
|
|
332
347
|
|
|
348
|
+
function extractBashTargets(args: unknown): ExtractedTargets {
|
|
349
|
+
const record = getRecord(args);
|
|
350
|
+
const command = safeString(record?.command);
|
|
351
|
+
const targets: ExtractedTargets = { paths: [], unknown: false };
|
|
352
|
+
if (!command.trim()) {
|
|
353
|
+
targets.unknown = true;
|
|
354
|
+
return targets;
|
|
355
|
+
}
|
|
356
|
+
if (BASH_OPAQUE_INTERPRETER_WRITE_RE.test(command) || BASH_HEREDOC_OPAQUE_INTERPRETER_WRITE_RE.test(command)) {
|
|
357
|
+
targets.unknown = true;
|
|
358
|
+
}
|
|
359
|
+
for (const match of command.matchAll(BASH_DD_OUTPUT_RE)) {
|
|
360
|
+
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
361
|
+
const output = parts.find(part => part.startsWith("of="));
|
|
362
|
+
if (output) addPath(targets, output.slice(3));
|
|
363
|
+
else targets.unknown = true;
|
|
364
|
+
}
|
|
365
|
+
for (const match of command.matchAll(BASH_IN_PLACE_MUTATION_COMMAND_RE)) {
|
|
366
|
+
const parts = shellWords(match[1] ?? "").map(cleanShellWord);
|
|
367
|
+
const hasInPlaceFlag = parts.some(part => /^-.*i/.test(part));
|
|
368
|
+
if (!hasInPlaceFlag) continue;
|
|
369
|
+
const target = [...parts].reverse().find(part => part && !part.startsWith("-"));
|
|
370
|
+
if (target) addPath(targets, target);
|
|
371
|
+
else targets.unknown = true;
|
|
372
|
+
}
|
|
373
|
+
for (const match of command.matchAll(BASH_MUTATION_COMMAND_RE)) {
|
|
374
|
+
const redirected = match[2]?.trim();
|
|
375
|
+
if (redirected) {
|
|
376
|
+
addPath(targets, cleanShellWord(redirected));
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const parts = shellWords(match[1] ?? "");
|
|
380
|
+
const commandName = match[0]
|
|
381
|
+
?.match(/(?:^|[;&|\n])\s*(?:\w+=[^\s]+\s+)*(?:sudo\s+)?(tee|touch|rm|mkdir|cp|mv|install|truncate)\b/i)?.[1]
|
|
382
|
+
?.toLowerCase();
|
|
383
|
+
const targetParts =
|
|
384
|
+
commandName === "cp" || commandName === "mv" || commandName === "install" || commandName === "truncate"
|
|
385
|
+
? parts.slice(-1)
|
|
386
|
+
: parts;
|
|
387
|
+
for (const part of targetParts) {
|
|
388
|
+
const cleaned = cleanShellWord(part);
|
|
389
|
+
if (!cleaned || cleaned.startsWith("-")) continue;
|
|
390
|
+
addPath(targets, cleaned);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return targets;
|
|
394
|
+
}
|
|
333
395
|
function extractTargets(tool: ToolWithEditMode, args: unknown): ExtractedTargets {
|
|
334
396
|
if (tool.name === "write") return extractWriteTargets(args);
|
|
335
397
|
if (tool.name === "ast_edit") return extractAstEditTargets(args);
|
|
336
398
|
if (tool.name === "edit") return extractEditTargets(args, tool);
|
|
399
|
+
if (tool.name === "bash") return extractBashTargets(args);
|
|
337
400
|
return { paths: [], unknown: true };
|
|
338
401
|
}
|
|
339
402
|
|
|
@@ -490,6 +553,7 @@ async function isNeutralTempPath(cwd: string, rawPath: string): Promise<boolean>
|
|
|
490
553
|
async function planningBlockedTargets(cwd: string, targets: ExtractedTargets): Promise<string[]> {
|
|
491
554
|
const blocked: string[] = [];
|
|
492
555
|
for (const rawPath of targets.paths) {
|
|
556
|
+
if (isAllowlistedPath(cwd, rawPath)) continue;
|
|
493
557
|
if (!(await isNeutralTempPath(cwd, rawPath))) blocked.push(rawPath);
|
|
494
558
|
}
|
|
495
559
|
return blocked;
|
|
@@ -523,7 +587,7 @@ export async function getDeepInterviewMutationDecision(
|
|
|
523
587
|
): Promise<DeepInterviewMutationDecision> {
|
|
524
588
|
if (!BLOCKED_TOOL_NAMES.has(input.tool.name)) return { blocked: false, targets: [] };
|
|
525
589
|
const targets = extractTargets(input.tool, input.args);
|
|
526
|
-
if (input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
|
|
590
|
+
if (input.tool.name !== "bash" && input.enforceWorkflowState !== false && hasBlockedGjcTarget(input.cwd, targets)) {
|
|
527
591
|
const stateSkill = firstBlockedWorkflowStateSkill(input.cwd, targets);
|
|
528
592
|
const command = stateSkill ? sanctionedWorkflowStateCommand(stateSkill) : "gjc <workflow-command>";
|
|
529
593
|
return {
|