@gajae-code/coding-agent 0.11.9 → 0.11.11
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 +56 -1
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +57 -2
- package/dist/types/config/settings.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +14 -9
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/modes/interactive-mode.d.ts +5 -1
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +11 -2
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
- package/dist/types/sdk/host/control/operations.d.ts +1 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +11 -11
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
- package/dist/types/session/session-manager.d.ts +34 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +39 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/resource-gc.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +48 -2
- package/src/commands/sdk.ts +91 -7
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +31 -7
- package/src/config/model-registry.ts +4 -2
- package/src/config/settings-schema.ts +61 -2
- package/src/config/settings.ts +8 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +14 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
- package/src/gjc-runtime/deep-interview-draft.ts +2 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
- package/src/gjc-runtime/deep-interview-state.ts +25 -3
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/ralplan-runtime.ts +252 -1
- package/src/gjc-runtime/state-runtime.ts +48 -11
- package/src/gjc-runtime/state-writer.ts +1 -1
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/modes/acp/acp-agent.ts +554 -60
- package/src/modes/acp/acp-event-mapper.ts +93 -6
- package/src/modes/components/model-selector.ts +15 -1
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +44 -3
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +65 -26
- package/src/modes/runtime-init.ts +1 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +82 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +222 -27
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +1191 -164
- package/src/sdk/host/control/dispatch.ts +2 -0
- package/src/sdk/host/control/operations.ts +1 -0
- package/src/sdk/host/host.ts +2 -2
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +1 -0
- package/src/sdk/session.ts +24 -1
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +290 -132
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +173 -127
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/session-manager.ts +608 -25
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +152 -42
- package/src/task/index.ts +2 -0
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +25 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/resource-gc.ts +496 -120
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
package/src/task/render.ts
CHANGED
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
type SubmitReviewDetails,
|
|
29
29
|
} from "../tools/review";
|
|
30
30
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
|
|
31
|
+
import {
|
|
32
|
+
collectProviderDegradationGroups,
|
|
33
|
+
hasActiveProviderRetryInTaskDetails,
|
|
34
|
+
providerProgressAgeLabel,
|
|
35
|
+
providerRetryPhaseLabel,
|
|
36
|
+
} from "./provider-retry-status";
|
|
31
37
|
import type { TaskResultReceipt } from "./receipt";
|
|
32
38
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
33
39
|
import type { AgentProgress, TaskParams, TaskToolDetails } from "./types";
|
|
@@ -123,6 +129,15 @@ function formatModelSubstitutionWarning(warning: { requested: string; effective:
|
|
|
123
129
|
return `Requested model substituted: ${warning.requested} -> ${warning.effective}`;
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
function renderProviderDegradationNotices(progress: readonly AgentProgress[], theme: Theme): string[] {
|
|
133
|
+
return collectProviderDegradationGroups(progress).map(group =>
|
|
134
|
+
theme.fg(
|
|
135
|
+
"warning",
|
|
136
|
+
truncateToWidth(replaceTabs(`provider degraded: ${group.count} subagents retrying on ${group.provider}`), 100),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
127
142
|
if (value === null) return "null";
|
|
128
143
|
if (typeof value === "string") {
|
|
@@ -548,14 +563,11 @@ function renderAgentProgress(
|
|
|
548
563
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
549
564
|
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
550
565
|
|
|
551
|
-
//
|
|
552
|
-
// is sleeping on a provider 429, not silently progressing. Wins over the
|
|
553
|
-
// generic running spinner because "we're waiting on a quota window" is
|
|
554
|
-
// the operationally meaningful state.
|
|
566
|
+
// A provider recovery loop is operationally distinct from normal agent work.
|
|
555
567
|
if (progress.retryState && progress.status === "running") {
|
|
556
|
-
statusLine += ` ${formatBadge("
|
|
568
|
+
statusLine += ` ${formatBadge("provider degraded", "warning", theme)}`;
|
|
557
569
|
} else if (progress.retryFailure && (progress.status === "failed" || progress.status === "aborted")) {
|
|
558
|
-
statusLine += ` ${formatBadge("
|
|
570
|
+
statusLine += ` ${formatBadge("provider failed", "error", theme)}`;
|
|
559
571
|
} else if (progress.status === "failed" || progress.status === "aborted") {
|
|
560
572
|
const statusLabel = progress.status === "failed" ? "failed" : "aborted";
|
|
561
573
|
statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
|
|
@@ -615,8 +627,10 @@ function renderAgentProgress(
|
|
|
615
627
|
// keep spinning while a child sleeps on a 3-hour provider rate-limit.
|
|
616
628
|
if (progress.retryState && progress.status === "running") {
|
|
617
629
|
const attemptLabel = progress.retryState.unbounded
|
|
618
|
-
? `attempt ${progress.retryState.attempt}`
|
|
619
|
-
:
|
|
630
|
+
? `attempt ${progress.retryState.attempt}, unbounded`
|
|
631
|
+
: `attempt ${progress.retryState.attempt} of ${progress.retryState.maxAttempts}, bounded`;
|
|
632
|
+
const phaseLabel = providerRetryPhaseLabel(progress.retryState.kind);
|
|
633
|
+
const progressAge = staticTime ? "" : ` · ${providerProgressAgeLabel(progress.retryState, Date.now())}`;
|
|
620
634
|
// `staticTime` omits the wall-clock countdown so a cached await body stays a
|
|
621
635
|
// pure function of its key (the producer already drops time-only churn).
|
|
622
636
|
let waitLabel = "";
|
|
@@ -624,7 +638,7 @@ function renderAgentProgress(
|
|
|
624
638
|
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
625
639
|
waitLabel = remainingMs > 0 ? ` in ${formatDuration(remainingMs)}` : " now";
|
|
626
640
|
}
|
|
627
|
-
const summary =
|
|
641
|
+
const summary = `${phaseLabel} · retrying ${attemptLabel}${waitLabel}${progressAge}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
|
|
628
642
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
629
643
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
630
644
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
@@ -953,23 +967,28 @@ export function renderResult(
|
|
|
953
967
|
.u32(spinnerFrame ?? 0)
|
|
954
968
|
.u32(width)
|
|
955
969
|
.digest();
|
|
956
|
-
|
|
970
|
+
const hasDynamicRetry = hasActiveProviderRetryInTaskDetails(details);
|
|
971
|
+
if (!hasDynamicRetry && cached?.key === key) return cached.lines;
|
|
957
972
|
|
|
958
973
|
const lines: string[] = [];
|
|
959
974
|
|
|
960
|
-
const
|
|
961
|
-
|
|
975
|
+
const hasProgress = Boolean(details.progress && details.progress.length > 0);
|
|
976
|
+
const shouldRenderProgress = hasProgress && (isPartial || details.results.length === 0);
|
|
977
|
+
const hasResults = details.results.length > 0;
|
|
978
|
+
if (hasResults) {
|
|
979
|
+
details.results.forEach((res, i) => {
|
|
980
|
+
const isLast = !shouldRenderProgress && i === details.results.length - 1;
|
|
981
|
+
lines.push(...renderAgentResult(res, isLast, expanded, theme));
|
|
982
|
+
});
|
|
983
|
+
}
|
|
962
984
|
if (shouldRenderProgress && details.progress) {
|
|
985
|
+
lines.push(...renderProviderDegradationNotices(details.progress, theme));
|
|
963
986
|
details.progress.forEach((progress, i) => {
|
|
964
987
|
const isLast = i === details.progress!.length - 1;
|
|
965
988
|
lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
|
|
966
989
|
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const isLast = i === details.results.length - 1;
|
|
970
|
-
lines.push(...renderAgentResult(res, isLast, expanded, theme));
|
|
971
|
-
});
|
|
972
|
-
|
|
990
|
+
}
|
|
991
|
+
if (hasResults && !shouldRenderProgress) {
|
|
973
992
|
const abortedCount = details.results.filter(r => r.status === "aborted").length;
|
|
974
993
|
const mergeFailedCount = details.results.filter(r => r.status === "merge_failed").length;
|
|
975
994
|
const successCount = details.results.filter(r => r.status === "completed").length;
|
|
@@ -997,7 +1016,7 @@ export function renderResult(
|
|
|
997
1016
|
if (lines.length === 0) {
|
|
998
1017
|
const text = fallbackText.trim() ? fallbackText : "No results";
|
|
999
1018
|
const result = [theme.fg("dim", truncateToWidth(text, width))];
|
|
1000
|
-
cached = { key, lines: result };
|
|
1019
|
+
if (!hasDynamicRetry) cached = { key, lines: result };
|
|
1001
1020
|
return result;
|
|
1002
1021
|
}
|
|
1003
1022
|
|
|
@@ -1019,9 +1038,9 @@ export function renderResult(
|
|
|
1019
1038
|
}
|
|
1020
1039
|
|
|
1021
1040
|
const indented = lines.map(line =>
|
|
1022
|
-
line.length > 0 ? truncateToWidth(` ${line}`, width, Ellipsis.Omit) : "",
|
|
1041
|
+
line.length > 0 ? truncateToWidth(` ${replaceTabs(line)}`, width, Ellipsis.Omit) : "",
|
|
1023
1042
|
);
|
|
1024
|
-
cached = { key, lines: indented };
|
|
1043
|
+
if (!hasDynamicRetry) cached = { key, lines: indented };
|
|
1025
1044
|
return indented;
|
|
1026
1045
|
},
|
|
1027
1046
|
invalidate() {
|
|
@@ -1065,16 +1084,16 @@ function renderNestedTaskTree(
|
|
|
1065
1084
|
): string[] {
|
|
1066
1085
|
const lines: string[] = [];
|
|
1067
1086
|
for (const details of detailsList) {
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1087
|
+
const inflight = details.progress;
|
|
1088
|
+
const hasInflight = Boolean(inflight && inflight.length > 0);
|
|
1089
|
+
if (details.results && details.results.length > 0) {
|
|
1070
1090
|
details.results.forEach((result, index) => {
|
|
1071
|
-
const isLast = index === details.results.length - 1;
|
|
1091
|
+
const isLast = !hasInflight && index === details.results.length - 1;
|
|
1072
1092
|
lines.push(...renderAgentResult(result, isLast, expanded, theme));
|
|
1073
1093
|
});
|
|
1074
|
-
continue;
|
|
1075
1094
|
}
|
|
1076
|
-
const inflight = details.progress;
|
|
1077
1095
|
if (inflight && inflight.length > 0) {
|
|
1096
|
+
lines.push(...renderProviderDegradationNotices(inflight, theme));
|
|
1078
1097
|
inflight.forEach((prog, index) => {
|
|
1079
1098
|
const isLast = index === inflight.length - 1;
|
|
1080
1099
|
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame, staticTime));
|
package/src/task/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Usage } from "@gajae-code/ai";
|
|
3
|
-
import { $
|
|
3
|
+
import { $env } from "@gajae-code/utils";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
5
|
import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
|
|
6
6
|
import type { TaskResultReceipt } from "./receipt";
|
|
@@ -14,23 +14,28 @@ export type AgentSource = "bundled" | "user" | "project";
|
|
|
14
14
|
export type ForkContextPolicy = "forbidden" | "allowed";
|
|
15
15
|
export type ForkContextMode = "none" | "receipt" | "last-turn" | "bounded" | "full";
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch {}
|
|
17
|
+
const parsePositiveIntegerEnvironment = (keys: string[], defaultValue: number): number => {
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
const value = $env[key];
|
|
20
|
+
if (!value || value.trim().length === 0) continue;
|
|
21
|
+
if (!/^\d+$/.test(value)) return defaultValue;
|
|
22
|
+
const number = Number(value);
|
|
23
|
+
return Number.isSafeInteger(number) && number > 0 ? number : defaultValue;
|
|
25
24
|
}
|
|
26
25
|
return defaultValue;
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
/** Maximum output bytes per agent */
|
|
30
|
-
export const MAX_OUTPUT_BYTES =
|
|
29
|
+
export const MAX_OUTPUT_BYTES = parsePositiveIntegerEnvironment(
|
|
30
|
+
["GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"],
|
|
31
|
+
500_000,
|
|
32
|
+
);
|
|
31
33
|
|
|
32
34
|
/** Maximum output lines per agent */
|
|
33
|
-
export const MAX_OUTPUT_LINES =
|
|
35
|
+
export const MAX_OUTPUT_LINES = parsePositiveIntegerEnvironment(
|
|
36
|
+
["GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"],
|
|
37
|
+
5000,
|
|
38
|
+
);
|
|
34
39
|
|
|
35
40
|
/** EventBus channel for raw subagent events */
|
|
36
41
|
export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
|
|
@@ -92,6 +97,12 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
|
|
|
92
97
|
id: z.string().max(48).refine(isValidTaskId, TASK_ID_DESCRIPTION).describe("filesystem-safe task identifier"),
|
|
93
98
|
description: z.string().describe("ui label, not seen by subagent"),
|
|
94
99
|
assignment: z.string().describe(assignmentDescription),
|
|
100
|
+
executionMode: z
|
|
101
|
+
.enum(["default", "ultragoal-red-team"])
|
|
102
|
+
.optional()
|
|
103
|
+
.describe(
|
|
104
|
+
"typed executor mode: default keeps ordinary executor behavior; ultragoal-red-team injects the Ultragoal QA/red-team prompt fragment. Prefer this over free-form assignment text (#2698).",
|
|
105
|
+
),
|
|
95
106
|
inheritContext: z
|
|
96
107
|
.enum(["none", "receipt", "last-turn", "bounded", "full"])
|
|
97
108
|
.optional()
|
|
@@ -285,6 +296,9 @@ export interface AgentProgress {
|
|
|
285
296
|
attempt: number;
|
|
286
297
|
maxAttempts: number;
|
|
287
298
|
unbounded?: boolean;
|
|
299
|
+
kind: "first_event_timeout" | "idle_stream_stall" | "provider_error";
|
|
300
|
+
provider?: string;
|
|
301
|
+
lastProviderProgressAtMs?: number;
|
|
288
302
|
delayMs: number;
|
|
289
303
|
errorMessage: string;
|
|
290
304
|
startedAtMs: number;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether an executor assignment should inject the ultragoal red-team
|
|
3
|
+
* prompt fragment.
|
|
4
|
+
*
|
|
5
|
+
* Preference order (parse, don't re-validate later):
|
|
6
|
+
* 1. Explicit typed `executionMode` on the task / ExecutorOptions — authoritative.
|
|
7
|
+
* 2. Assignment text heuristics — only explicit Ultragoal QA/red-team phrasing.
|
|
8
|
+
*
|
|
9
|
+
* Bare mentions of `executorQa` (docs, quality-gate field names, negative
|
|
10
|
+
* instructions) must not flip the mode (#2698 / #2456).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `executor.md`:
|
|
13
|
+
* "activates only when the assignment explicitly labels Executor as Ultragoal
|
|
14
|
+
* completion QA/red-team or asks for `executorQa` red-team evidence."
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const EXECUTOR_EXECUTION_MODES = ["default", "ultragoal-red-team"] as const;
|
|
18
|
+
export type ExecutorExecutionMode = (typeof EXECUTOR_EXECUTION_MODES)[number];
|
|
19
|
+
|
|
20
|
+
const ULTRAGOAL_COMPLETION_QA = /\bultragoal\s+completion\s+(?:qa|red[-\s]?team)\b/i;
|
|
21
|
+
|
|
22
|
+
/** `executorQa` within a short window of red-team / matrix / evidence framing. */
|
|
23
|
+
const EXECUTOR_QA_EVIDENCE =
|
|
24
|
+
/\bexecutorQa\b[\s\S]{0,120}\b(?:red[-\s]?team|matrix|evidence)\b|\b(?:red[-\s]?team|matrix|evidence)\b[\s\S]{0,120}\bexecutorQa\b/i;
|
|
25
|
+
|
|
26
|
+
/** Common Ultragoal skill spawn phrasing: "executor QA/red-team lane". */
|
|
27
|
+
const EXECUTOR_QA_LANE = /\bexecutor\b[\s\S]{0,48}\b(?:qa|red[-\s]?team)\s+lane\b/i;
|
|
28
|
+
|
|
29
|
+
export function isExecutorExecutionMode(value: unknown): value is ExecutorExecutionMode {
|
|
30
|
+
return value === "default" || value === "ultragoal-red-team";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a caller-supplied mode. Unknown values fail closed to `undefined`
|
|
35
|
+
* so the assignment heuristic (or default off) still applies — never invent on.
|
|
36
|
+
*/
|
|
37
|
+
export function parseExecutorExecutionMode(value: unknown): ExecutorExecutionMode | undefined {
|
|
38
|
+
if (typeof value !== "string") return undefined;
|
|
39
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
40
|
+
if (normalized === "default" || normalized === "ordinary" || normalized === "implement") {
|
|
41
|
+
return "default";
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
normalized === "ultragoal-red-team" ||
|
|
45
|
+
normalized === "ultragoal-redteam" ||
|
|
46
|
+
normalized === "red-team" ||
|
|
47
|
+
normalized === "redteam" ||
|
|
48
|
+
normalized === "executor-qa"
|
|
49
|
+
) {
|
|
50
|
+
return "ultragoal-red-team";
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function assignmentRequestsUltragoalRedTeam(assignment: string | undefined): boolean {
|
|
56
|
+
const text = assignment?.trim() ?? "";
|
|
57
|
+
if (text.length === 0) return false;
|
|
58
|
+
return ULTRAGOAL_COMPLETION_QA.test(text) || EXECUTOR_QA_EVIDENCE.test(text) || EXECUTOR_QA_LANE.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ResolveUltragoalRedTeamArgs {
|
|
62
|
+
readonly executionMode?: unknown;
|
|
63
|
+
readonly assignment?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Final activation decision for the executor prompt fragment.
|
|
68
|
+
* Typed mode wins; assignment text is a compatibility fallback only.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveUltragoalRedTeamActivation(args: ResolveUltragoalRedTeamArgs): boolean {
|
|
71
|
+
const mode = parseExecutorExecutionMode(args.executionMode);
|
|
72
|
+
if (mode === "ultragoal-red-team") return true;
|
|
73
|
+
if (mode === "default") return false;
|
|
74
|
+
return assignmentRequestsUltragoalRedTeam(args.assignment);
|
|
75
|
+
}
|
|
@@ -18,7 +18,14 @@ export interface BashRestrictionOptions {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const SHELL_CONTROL_CHARS = new Set([";", "|", "&", "<", ">", "(", ")"]);
|
|
21
|
-
|
|
21
|
+
// `~` is handled separately: bash performs tilde expansion only at positions where a
|
|
22
|
+
// tilde can open an expansion - the start of a word, and inside an assignment word
|
|
23
|
+
// directly after the first `=` or after a `:` in the assigned value. A tilde anywhere
|
|
24
|
+
// else (e.g. the git revision `HEAD~1`) is a literal character.
|
|
25
|
+
const UNSAFE_UNQUOTED_EXPANSION_CHARS = new Set(["$", "*", "?", "[", "]", "{", "}"]);
|
|
26
|
+
// A bash assignment word has a plain assignment name before its first unquoted `=`,
|
|
27
|
+
// in either the `name=value` or the compound `name+=value` form.
|
|
28
|
+
const ASSIGNMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*\+?$/u;
|
|
22
29
|
const ALLOWED_STATE_ACTIONS = new Set(["read", "write", "contract"]);
|
|
23
30
|
const CANONICAL_STATE_TARGETS = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
|
|
24
31
|
const READ_ONLY_COMMANDS = new Set(["grep", "rg", "tree", "ls", "pwd", "wc", "du", "file", "stat"]);
|
|
@@ -29,6 +36,14 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
29
36
|
let quote: "single" | "double" | null = null;
|
|
30
37
|
|
|
31
38
|
let wordStarted = false;
|
|
39
|
+
// Per-token tilde-expansion state. `assignmentNameCandidate` stays true while the
|
|
40
|
+
// characters before the first unquoted `=` could still form an assignment name (any
|
|
41
|
+
// quoting in that region disqualifies the token), `sawAssignmentEquals` records that
|
|
42
|
+
// the token is an assignment word, and `tildeExpandable` means an unquoted `~` at the
|
|
43
|
+
// current index would actually be expanded by bash.
|
|
44
|
+
let assignmentNameCandidate = true;
|
|
45
|
+
let sawAssignmentEquals = false;
|
|
46
|
+
let tildeExpandable = false;
|
|
32
47
|
for (let index = 0; index < command.length; index += 1) {
|
|
33
48
|
const char = command[index]!;
|
|
34
49
|
const next = command[index + 1];
|
|
@@ -63,11 +78,15 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
63
78
|
if (char === "'") {
|
|
64
79
|
quote = "single";
|
|
65
80
|
wordStarted = true;
|
|
81
|
+
if (!sawAssignmentEquals) assignmentNameCandidate = false;
|
|
82
|
+
tildeExpandable = false;
|
|
66
83
|
continue;
|
|
67
84
|
}
|
|
68
85
|
if (char === '"') {
|
|
69
86
|
quote = "double";
|
|
70
87
|
wordStarted = true;
|
|
88
|
+
if (!sawAssignmentEquals) assignmentNameCandidate = false;
|
|
89
|
+
tildeExpandable = false;
|
|
71
90
|
continue;
|
|
72
91
|
}
|
|
73
92
|
if (char === "`" || (char === "$" && next === "(")) {
|
|
@@ -82,17 +101,42 @@ function parseShellWords(command: string): { words: string[]; reason?: string }
|
|
|
82
101
|
if (UNSAFE_UNQUOTED_EXPANSION_CHARS.has(char)) {
|
|
83
102
|
return { words, reason: `shell expansion character '${char}' is not allowed in restricted bash commands` };
|
|
84
103
|
}
|
|
104
|
+
// Tilde expansion is positional: bash expands a tilde only where one can open an
|
|
105
|
+
// expansion, i.e. at the start of a word or, inside an assignment word, directly
|
|
106
|
+
// after the first `=` or after a `:` in the assigned value. A tilde anywhere else
|
|
107
|
+
// is literal, which is what git revision syntax such as `HEAD~1` relies on.
|
|
108
|
+
if (char === "~" && (!wordStarted || tildeExpandable)) {
|
|
109
|
+
return { words, reason: "shell expansion character '~' is not allowed in restricted bash commands" };
|
|
110
|
+
}
|
|
85
111
|
if (/\s/u.test(char)) {
|
|
86
112
|
if (wordStarted) {
|
|
87
113
|
words.push(current);
|
|
88
114
|
current = "";
|
|
89
115
|
wordStarted = false;
|
|
90
116
|
}
|
|
117
|
+
assignmentNameCandidate = true;
|
|
118
|
+
sawAssignmentEquals = false;
|
|
119
|
+
tildeExpandable = false;
|
|
91
120
|
continue;
|
|
92
121
|
}
|
|
93
122
|
if (char === "\\") {
|
|
94
123
|
return { words, reason: "backslash escapes are not allowed in restricted bash commands" };
|
|
95
124
|
}
|
|
125
|
+
if (char === "=" && !sawAssignmentEquals) {
|
|
126
|
+
// Only a plain assignment name before the first unquoted `=` makes this an
|
|
127
|
+
// assignment word whose value bash scans for tilde expansions.
|
|
128
|
+
if (assignmentNameCandidate && ASSIGNMENT_NAME_PATTERN.test(current)) {
|
|
129
|
+
sawAssignmentEquals = true;
|
|
130
|
+
tildeExpandable = true;
|
|
131
|
+
} else {
|
|
132
|
+
assignmentNameCandidate = false;
|
|
133
|
+
tildeExpandable = false;
|
|
134
|
+
}
|
|
135
|
+
} else if (char === ":" && sawAssignmentEquals) {
|
|
136
|
+
tildeExpandable = true;
|
|
137
|
+
} else {
|
|
138
|
+
tildeExpandable = false;
|
|
139
|
+
}
|
|
96
140
|
current += char;
|
|
97
141
|
wordStarted = true;
|
|
98
142
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -275,9 +275,11 @@ export interface ToolSession {
|
|
|
275
275
|
/** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
|
|
276
276
|
getAgentId?: () => string | null;
|
|
277
277
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|
|
278
|
+
getToolByName?: (name: string) => AgentTool | undefined;
|
|
279
|
+
/** Look up a registered tool with the session's execution guards applied. */
|
|
280
|
+
getToolForExecution?: (name: string) => AgentTool | undefined;
|
|
278
281
|
/** Purge undelivered queued custom messages matching the predicate. Returns counts. */
|
|
279
282
|
purgeQueuedCustomMessages?: (predicate: (message: CustomMessage) => boolean) => PurgeQueuedCustomMessagesResult;
|
|
280
|
-
getToolByName?: (name: string) => AgentTool | undefined;
|
|
281
283
|
/** Agent registry for IRC routing across live sessions. */
|
|
282
284
|
agentRegistry?: AgentRegistry;
|
|
283
285
|
/** Optional restricted bash command prefixes for read-only role agents and constrained modes. */
|