@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/dist/types/cli.d.ts +8 -1
  3. package/dist/types/config/model-profile-activation.d.ts +1 -0
  4. package/dist/types/config/model-profile-contract.d.ts +2 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/settings-schema.d.ts +57 -2
  7. package/dist/types/config/settings.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +21 -0
  9. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  10. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +60 -0
  11. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  12. package/dist/types/gjc-runtime/state-writer.d.ts +1 -0
  13. package/dist/types/modes/acp/acp-agent.d.ts +14 -9
  14. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  15. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  16. package/dist/types/modes/components/welcome.d.ts +1 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
  18. package/dist/types/modes/interactive-mode.d.ts +5 -1
  19. package/dist/types/runtime/memory-domain.d.ts +9 -0
  20. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  21. package/dist/types/runtime/memory-guard.d.ts +39 -0
  22. package/dist/types/runtime/memory-limit.d.ts +11 -0
  23. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  24. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  25. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  26. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  27. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  28. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  29. package/dist/types/sdk/bus/index.d.ts +11 -2
  30. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  31. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  32. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
  33. package/dist/types/sdk/bus/telegram-daemon.d.ts +97 -8
  34. package/dist/types/sdk/host/control/operations.d.ts +1 -0
  35. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  36. package/dist/types/sdk/session.d.ts +7 -0
  37. package/dist/types/sdk/startup-capability.d.ts +6 -0
  38. package/dist/types/session/agent-session.d.ts +11 -11
  39. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  40. package/dist/types/session/internal/managed-session-scope.d.ts +28 -1
  41. package/dist/types/session/session-manager.d.ts +34 -0
  42. package/dist/types/task/executor.d.ts +6 -0
  43. package/dist/types/task/provider-retry-status.d.ts +13 -0
  44. package/dist/types/task/types.d.ts +39 -0
  45. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  46. package/dist/types/tools/index.d.ts +3 -1
  47. package/dist/types/tools/resource-gc.d.ts +19 -0
  48. package/package.json +8 -8
  49. package/scripts/generate-sdk-operation-inventory.ts +3 -0
  50. package/scripts/run-test-manifest.ts +6 -4
  51. package/src/cli/args.ts +14 -8
  52. package/src/cli/fast-help.ts +1 -1
  53. package/src/cli/notify-cli.ts +1 -0
  54. package/src/cli.ts +48 -2
  55. package/src/commands/sdk.ts +91 -7
  56. package/src/config/model-profile-activation.ts +1 -0
  57. package/src/config/model-profile-contract.ts +13 -0
  58. package/src/config/model-profiles.ts +31 -7
  59. package/src/config/model-registry.ts +4 -2
  60. package/src/config/settings-schema.ts +61 -2
  61. package/src/config/settings.ts +8 -0
  62. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -2
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  64. package/src/eval/js/tool-bridge.ts +1 -1
  65. package/src/extensibility/extensions/runner.ts +4 -0
  66. package/src/extensibility/extensions/types.ts +14 -0
  67. package/src/gjc-runtime/deep-interview-ambiguity.ts +16 -2
  68. package/src/gjc-runtime/deep-interview-draft.ts +2 -1
  69. package/src/gjc-runtime/deep-interview-recorder.ts +16 -1
  70. package/src/gjc-runtime/deep-interview-state.ts +25 -3
  71. package/src/gjc-runtime/linux-proc.ts +70 -36
  72. package/src/gjc-runtime/ralplan-runtime.ts +252 -1
  73. package/src/gjc-runtime/state-runtime.ts +48 -11
  74. package/src/gjc-runtime/state-writer.ts +1 -1
  75. package/src/hooks/skill-state.ts +23 -1
  76. package/src/internal-urls/docs-index.generated.ts +7 -7
  77. package/src/internal-urls/local-protocol.ts +23 -4
  78. package/src/modes/acp/acp-agent.ts +554 -60
  79. package/src/modes/acp/acp-event-mapper.ts +93 -6
  80. package/src/modes/components/model-selector.ts +15 -1
  81. package/src/modes/components/notifications-settings-editor.ts +1 -1
  82. package/src/modes/components/queue-pane.ts +55 -9
  83. package/src/modes/components/queued-message-selector.ts +53 -10
  84. package/src/modes/components/tool-status-header.ts +1 -4
  85. package/src/modes/components/welcome.ts +18 -3
  86. package/src/modes/controllers/command-controller.ts +4 -0
  87. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  88. package/src/modes/controllers/input-controller.ts +44 -3
  89. package/src/modes/controllers/selector-controller.ts +38 -8
  90. package/src/modes/interactive-mode.ts +65 -26
  91. package/src/modes/runtime-init.ts +1 -0
  92. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  93. package/src/prompts/agents/architect.md +8 -1
  94. package/src/prompts/agents/critic.md +8 -1
  95. package/src/prompts/agents/executor.md +1 -1
  96. package/src/prompts/agents/planner.md +8 -1
  97. package/src/runtime/memory-domain.ts +66 -0
  98. package/src/runtime/memory-guard-contract.ts +73 -0
  99. package/src/runtime/memory-guard.ts +292 -0
  100. package/src/runtime/memory-limit.ts +50 -0
  101. package/src/runtime-mcp/manager.ts +26 -6
  102. package/src/sdk/acp/adapter.ts +8 -1
  103. package/src/sdk/acp/final-text.ts +40 -0
  104. package/src/sdk/acp/mcp.ts +26 -0
  105. package/src/sdk/broker/identity.ts +22 -12
  106. package/src/sdk/broker/lifecycle.ts +82 -1
  107. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  108. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  109. package/src/sdk/bus/config.ts +1 -1
  110. package/src/sdk/bus/daemon-paths.ts +2 -0
  111. package/src/sdk/bus/index.ts +222 -27
  112. package/src/sdk/bus/notification-service.ts +19 -5
  113. package/src/sdk/bus/operator-runtime.ts +31 -5
  114. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  115. package/src/sdk/bus/telegram-cli.ts +6 -13
  116. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  117. package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
  118. package/src/sdk/bus/telegram-daemon-control.ts +14 -5
  119. package/src/sdk/bus/telegram-daemon.ts +1191 -164
  120. package/src/sdk/host/control/dispatch.ts +2 -0
  121. package/src/sdk/host/control/operations.ts +1 -0
  122. package/src/sdk/host/host.ts +2 -2
  123. package/src/sdk/lifecycle-session.ts +19 -3
  124. package/src/sdk/protocol/operation-inventory.generated.json +58 -0
  125. package/src/sdk/protocol/operation-registry.ts +1 -0
  126. package/src/sdk/session.ts +24 -1
  127. package/src/sdk/startup-capability.ts +6 -0
  128. package/src/session/agent-session.ts +290 -132
  129. package/src/session/fallback-chain-controller.ts +47 -8
  130. package/src/session/internal/managed-session-scope.ts +173 -127
  131. package/src/session/internal/native-publish-outcome.ts +8 -3
  132. package/src/session/session-manager.ts +608 -25
  133. package/src/skill-state/workflow-mutation-guard.ts +41 -7
  134. package/src/task/executor.ts +152 -42
  135. package/src/task/index.ts +2 -0
  136. package/src/task/provider-retry-status.ts +88 -0
  137. package/src/task/render.ts +45 -26
  138. package/src/task/types.ts +25 -11
  139. package/src/task/ultragoal-redteam-activation.ts +75 -0
  140. package/src/tools/bash-allowed-prefixes.ts +45 -1
  141. package/src/tools/index.ts +3 -1
  142. package/src/tools/resource-gc.ts +496 -120
  143. package/src/tools/subagent-render.ts +74 -16
  144. package/src/tools/subagent.ts +2 -0
  145. package/src/workflow/workflow-intent-diff.ts +4 -1
  146. package/vendor/insane-search/MANIFEST.json +4 -1
  147. package/vendor/insane-search/engine/bias_check.py +5 -0
  148. package/vendor/insane-search/engine/fetch_chain.py +67 -1
  149. package/vendor/insane-search/engine/transport.py +13 -2
  150. package/vendor/insane-search/engine/validators.py +4 -1
@@ -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
- // Show retry-blocked badge so the parent immediately sees that a child
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("retrying", "warning", theme)}`;
568
+ statusLine += ` ${formatBadge("provider degraded", "warning", theme)}`;
557
569
  } else if (progress.retryFailure && (progress.status === "failed" || progress.status === "aborted")) {
558
- statusLine += ` ${formatBadge("rate-limited", "error", theme)}`;
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
- : `${progress.retryState.attempt}/${progress.retryState.maxAttempts}`;
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 = `retrying ${attemptLabel}${waitLabel}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
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
- if (cached?.key === key) return cached.lines;
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 shouldRenderProgress =
961
- Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
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
- } else if (details.results && details.results.length > 0) {
968
- details.results.forEach((res, i) => {
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 hasResults = Boolean(details.results && details.results.length > 0);
1069
- if (hasResults) {
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 { $pickenv } from "@gajae-code/utils";
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 parseNumber = (value: string | undefined, defaultValue: number): number => {
18
- if (value) {
19
- try {
20
- const number = Number.parseInt(value, 10);
21
- if (!Number.isNaN(number) && number > 0) {
22
- return number;
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 = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_BYTES", "PI_TASK_MAX_OUTPUT_BYTES"), 500_000);
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 = parseNumber($pickenv("GJC_TASK_MAX_OUTPUT_LINES", "PI_TASK_MAX_OUTPUT_LINES"), 5000);
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
- const UNSAFE_UNQUOTED_EXPANSION_CHARS = new Set(["$", "*", "?", "[", "]", "{", "}", "~"]);
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
  }
@@ -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. */