@gajae-code/coding-agent 0.11.7 → 0.11.8
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 +55 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/config/keybindings.d.ts +33 -3
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +15 -2
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
- package/dist/types/main.d.ts +8 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +3 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +25 -0
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +11 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +17 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/image-gen.d.ts +47 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli.ts +18 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/keybindings.ts +157 -24
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +17 -2
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +5 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/internal-urls/local-protocol.ts +33 -16
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +25 -10
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/components/model-selector.ts +42 -8
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +20 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +7 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +46 -54
- package/src/modes/interactive-mode.ts +41 -27
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +13 -6
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +18 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +240 -65
- package/src/sdk/bus/lifecycle-commands.ts +47 -3
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +27 -2
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +145 -33
- package/src/sdk/bus/telegram-daemon.ts +835 -110
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +47 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +2 -0
- package/src/sdk/startup-capability.ts +9 -1
- package/src/session/agent-session.ts +52 -5
- package/src/session/blob-store.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +854 -84
- package/src/session/internal/managed-session-storage.ts +251 -199
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +310 -327
- package/src/session/session-storage.ts +128 -11
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +198 -88
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/image-gen.ts +178 -6
- package/src/tools/index.ts +4 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/url-guard.ts +3 -1
- package/src/web/scrapers/docs-rs.ts +30 -5
|
@@ -1,44 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { getAgentDir } from "@gajae-code/utils";
|
|
3
3
|
import { YAML } from "bun";
|
|
4
|
+
import { UnknownModelProfileError, validateModelProfileName } from "../config/model-profile-contract";
|
|
5
|
+
import { mergeModelProfiles } from "../config/model-profiles";
|
|
4
6
|
import { ModelsConfigSchema } from "../config/models-config-schema";
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
* The coordinator runs in the shipped MCP process and must stay outside the
|
|
8
|
-
* session host import graph. Keep this identity catalog in the coordinator
|
|
9
|
-
* boundary; profile activation and model registry code belong to that host.
|
|
10
|
-
*/
|
|
11
|
-
const BUILTIN_MODEL_PROFILE_NAMES = [
|
|
12
|
-
"codex-eco",
|
|
13
|
-
"codex-medium",
|
|
14
|
-
"codex-pro",
|
|
15
|
-
"opencodego",
|
|
16
|
-
"claude-opus",
|
|
17
|
-
"claude-fable",
|
|
18
|
-
"glm-eco",
|
|
19
|
-
"glm-medium",
|
|
20
|
-
"glm-pro",
|
|
21
|
-
"kimi-coding-plan-eco",
|
|
22
|
-
"kimi-coding-plan-medium",
|
|
23
|
-
"kimi-coding-plan-pro",
|
|
24
|
-
"mimo-eco",
|
|
25
|
-
"mimo-medium",
|
|
26
|
-
"mimo-pro",
|
|
27
|
-
"grok-eco",
|
|
28
|
-
"grok-medium",
|
|
29
|
-
"grok-pro",
|
|
30
|
-
"grok-build-pro",
|
|
31
|
-
"cursor-eco",
|
|
32
|
-
"cursor-medium",
|
|
33
|
-
"cursor-pro",
|
|
34
|
-
"minimax-eco",
|
|
35
|
-
"minimax-medium",
|
|
36
|
-
"minimax-pro",
|
|
37
|
-
"opus-codex",
|
|
38
|
-
"codex-opencodego",
|
|
39
|
-
"fable-opus-codex",
|
|
40
|
-
] as const;
|
|
41
|
-
|
|
42
8
|
export interface CoordinatorModelProfile {
|
|
43
9
|
name: string;
|
|
44
10
|
}
|
|
@@ -53,8 +19,6 @@ export type CoordinatorModelProfileLoader = () =>
|
|
|
53
19
|
| Promise<Map<string, CoordinatorModelProfile>>;
|
|
54
20
|
|
|
55
21
|
const MAX_ECHOED_MPRESET_LENGTH = 128;
|
|
56
|
-
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([["codex-standard", "codex-medium"]]);
|
|
57
|
-
|
|
58
22
|
/**
|
|
59
23
|
* Thrown by the default loader when `models.yml` exists but is invalid or
|
|
60
24
|
* unreadable. This lets the resolver fail closed with a distinct, stable reason
|
|
@@ -69,35 +33,25 @@ export class CoordinatorModelProfileRegistryError extends Error {
|
|
|
69
33
|
}
|
|
70
34
|
}
|
|
71
35
|
|
|
72
|
-
function
|
|
73
|
-
|
|
36
|
+
function coordinatorModelProfiles(
|
|
37
|
+
profiles?: Parameters<typeof mergeModelProfiles>[0],
|
|
38
|
+
): Map<string, CoordinatorModelProfile> {
|
|
39
|
+
return new Map([...mergeModelProfiles(profiles).keys()].map(name => [name, { name }]));
|
|
74
40
|
}
|
|
75
41
|
|
|
76
42
|
export const loadCoordinatorModelProfiles: CoordinatorModelProfileLoader = async () => {
|
|
77
43
|
const modelsFile = Bun.file(path.join(getAgentDir(), "models.yml"));
|
|
78
|
-
if (!(await modelsFile.exists())) return
|
|
44
|
+
if (!(await modelsFile.exists())) return coordinatorModelProfiles();
|
|
79
45
|
try {
|
|
80
46
|
const parsed = YAML.parse(await modelsFile.text());
|
|
81
47
|
const config = ModelsConfigSchema.safeParse(parsed);
|
|
82
48
|
if (!config.success) throw config.error;
|
|
83
|
-
|
|
84
|
-
for (const name of Object.keys(config.data.profiles ?? {})) profiles.set(name, { name });
|
|
85
|
-
return profiles;
|
|
49
|
+
return coordinatorModelProfiles(config.data.profiles);
|
|
86
50
|
} catch (error) {
|
|
87
51
|
throw new CoordinatorModelProfileRegistryError(error);
|
|
88
52
|
}
|
|
89
53
|
};
|
|
90
54
|
|
|
91
|
-
function sortedProfileNames(profiles: ReadonlyMap<string, CoordinatorModelProfile>): string[] {
|
|
92
|
-
return [...profiles.keys()].sort((left, right) => left.localeCompare(right));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveCoordinatorModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
|
|
96
|
-
if (profiles.has(profileName)) return profileName;
|
|
97
|
-
const replacement = LEGACY_MODEL_PROFILE_ALIASES.get(profileName);
|
|
98
|
-
return replacement && profiles.has(replacement) ? replacement : profileName;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
55
|
export type CoordinatorMpresetResolution =
|
|
102
56
|
| { ok: true; mpreset: string | null }
|
|
103
57
|
| { ok: false; reason: "unknown_model_profile"; mpreset: string; available_profiles: string[] }
|
|
@@ -120,7 +74,7 @@ export async function resolveCoordinatorMpreset(
|
|
|
120
74
|
loadProfiles: CoordinatorModelProfileLoader,
|
|
121
75
|
): Promise<CoordinatorMpresetResolution> {
|
|
122
76
|
if (raw === undefined || raw === null) return { ok: true, mpreset: null };
|
|
123
|
-
const requested = typeof raw === "string" ? raw
|
|
77
|
+
const requested = typeof raw === "string" ? raw : "";
|
|
124
78
|
const echoed = requested.slice(0, MAX_ECHOED_MPRESET_LENGTH);
|
|
125
79
|
let profiles: Map<string, CoordinatorModelProfile>;
|
|
126
80
|
try {
|
|
@@ -131,24 +85,17 @@ export async function resolveCoordinatorMpreset(
|
|
|
131
85
|
}
|
|
132
86
|
throw error;
|
|
133
87
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
ok: false,
|
|
148
|
-
reason: "unknown_model_profile",
|
|
149
|
-
mpreset: echoed,
|
|
150
|
-
available_profiles: sortedProfileNames(profiles),
|
|
151
|
-
};
|
|
88
|
+
try {
|
|
89
|
+
const canonical = validateModelProfileName(requested, profiles);
|
|
90
|
+
return { ok: true, mpreset: canonical };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof UnknownModelProfileError)
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "unknown_model_profile",
|
|
96
|
+
mpreset: error.details.requestedProfile.slice(0, MAX_ECHOED_MPRESET_LENGTH),
|
|
97
|
+
available_profiles: error.details.availableProfiles,
|
|
98
|
+
};
|
|
99
|
+
throw error;
|
|
152
100
|
}
|
|
153
|
-
return { ok: true, mpreset: canonical };
|
|
154
101
|
}
|
|
@@ -382,7 +382,7 @@ Auto-answer has a clarity cap: unless the architect confidence is `high` and unc
|
|
|
382
382
|
|
|
383
383
|
### Step 2b″: Refine Free-Text Answers
|
|
384
384
|
|
|
385
|
-
When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections)
|
|
385
|
+
When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections). Then confirm with exactly one `ask` that nothing is lost or misrepresented: the `ask` question body MUST render the full structured interpretation — every non-empty canonical section, verbatim — before the confirmation prompt. The user is approving that specific interpretation, so it must be visible inside the question body; never ask "does this capture it?" / "이 해석이 맞아?" without first displaying the interpretation itself. A confirmation `ask` whose body omits the interpretation it is asking about is a hard error: re-issue it with the interpretation shown. Apply `language.instruction` when present.
|
|
386
386
|
|
|
387
387
|
Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
|
|
388
388
|
|
|
@@ -601,7 +601,7 @@ When ambiguity ≤ threshold (or hard cap / early exit):
|
|
|
601
601
|
|
|
602
602
|
**4a. Closure / Acceptance Guard.** Even when ambiguity ≤ threshold, do not treat the math as completion. Run an independent readiness audit from the full main-session perspective (including explore findings, established facts, and triggers the scorer may not have fully weighed). Confirm every active topology component has goal/constraint/criteria coverage, no unresolved or disputed trigger remains on a path that matters, no disputed established fact lacks a `superseded_by` resolution, and no low-confidence auto-answer is standing in for user-confirmed truth above the clarity cap. If a material gap exists, explicitly override the gate to the user — "The math says ready, but I am not accepting it yet because {gap}" — and ask the single highest-impact follow-up, returning to Phase 2. Record any override in `state.closure_overrides`.
|
|
603
603
|
|
|
604
|
-
**4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask
|
|
604
|
+
**4b. Restate gate.** Once closure passes, collapse the agreed answers into ONE sentence goal that covers every active component, and confirm it with a single `ask` whose body MUST begin by stating that one-sentence goal verbatim, followed by: "If someone read only this line, would they reach the same outcome you have in mind?" The goal line must be visible inside the `ask` body; never ask the confirmation without first displaying the collapsed goal it refers to. Offer **Yes, crystallize**, **Adjust wording**, and **Missing scope**, plus free-text, applying `language.instruction` when present. Because this gate has options, it MUST go through `ask`: do not print the Restate question and options as assistant prose with `Question:`/`Options:` labels. If the Restate gate was already printed that way, immediately call `ask` with the same question/options before accepting or waiting for any answer. On "Adjust wording" / "Missing scope", collect the exact correction with one follow-up `ask`, route it back through Step 2c scoring and established-facts maintenance (a correction can change ambiguity), then re-run closure and ask the Restate gate again. Cap at two loops; if alignment is not reached, return to Phase 2 with a targeted question instead of forcing a goal line. Persist the confirmed line as `state.restated_goal`.
|
|
605
605
|
|
|
606
606
|
1. **Generate the specification** using opus model with the prompt-safe transcript. If the full interview transcript or initial context is too large, include the summary plus all concrete decisions, acceptance criteria, unresolved gaps, and ontology snapshots; never overflow the prompt with raw oversized context.
|
|
607
607
|
- Apply `language.instruction` when present so user-facing prose in the spec preserves the session language; keep code identifiers, file paths, commands, JSON/settings keys, and quoted source text unchanged.
|
|
@@ -34,6 +34,8 @@ For corrupt, tampered, unreadable, or stale current-session ralplan state, run `
|
|
|
34
34
|
|
|
35
35
|
Ralplan is planning only. It may inspect context and draft plan/spec/proposal artifacts, but those remain `pending approval` until explicit current-turn or structured-UI execution approval. Before that approval, do not mutate product source, run mutation-oriented shell, commit, push, open PRs, invoke execution skills, or delegate implementation.
|
|
36
36
|
|
|
37
|
+
Explicitly naming `ultragoal` or `team` (including `/skill:` and `gjc` forms) counts as opting into execution for that skill — do not re-ask for the same consent.
|
|
38
|
+
|
|
37
39
|
Persist planning artifacts and handoffs through the ralplan CLI writer, never direct `.gjc/` edits:
|
|
38
40
|
Direct `write`, `edit`, or `ast_edit` calls against `.gjc/_session-{sessionid}/specs`, `.gjc/_session-{sessionid}/plans`, `.gjc/_session-{sessionid}/state`, or any other `.gjc/` path are forbidden unless an explicit force override is active.
|
|
39
41
|
|
|
@@ -82,7 +84,7 @@ The consensus workflow:
|
|
|
82
84
|
- For every confirmed open item, embed the resolved outcome into the final plan under an **## Intent Reconciliation** section so the `pending approval` artifact records each decision; record any item the user explicitly defers as an open confirmation under that same section.
|
|
83
85
|
d. Persist the reconciliation with `gjc ralplan --write --stage post-interview --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json`, then return the receipt/path plus a compact status (reconciled-clean / reconciled-with-revision / open-confirmations-pending) instead of pasting the full body.
|
|
84
86
|
7. On reconciliation completion, re-check the review join gate (Critic `OKAY` plus Architect `CLEAR`/`APPROVE` for the same Planner artifact/pass), mark the plan `pending approval` unless explicit execution approval has already been captured, persist the ADR/final plan via `gjc ralplan --write --stage final --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT`, and do not directly edit `.gjc/_session-{sessionid}/plans`. Final plan must include ADR (Decision, Drivers, Alternatives considered, Why chosen, Consequences, Follow-ups) and, when present, the **## Intent Reconciliation** section.
|
|
85
|
-
8. **
|
|
87
|
+
8. **Final approval gate (with explicit-execution exception):** If the user already explicitly named an execution skill in the current turn or via the structured approval UI (`ultragoal`, `/skill:ultragoal`, `gjc ultragoal`, `team`, `/skill:team`, `gjc team`, or "Approve execution via ultragoal/team"), that is execution approval — skip the re-ask and proceed to step 9 with that skill. Otherwise, **always** present the finalized plan via the `ask` tool (regardless of `--interactive`) with `workflowGate: { stage: "ralplan", kind: "approval" }` on the final question so RPC/headless clients receive a `ralplan`/`approval` workflow gate, not a deep-interview question gate. Use these options:
|
|
86
88
|
- **Refine further** — re-run the consensus loop / request changes, then return here
|
|
87
89
|
- **Approve execution via ultragoal (Recommended)** — goal-tracked autonomous execution
|
|
88
90
|
- **Approve execution via team** — only when tmux-based interactive worker parallelization is required
|
package/src/edit/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
-
import { prompt } from "@gajae-code/utils";
|
|
2
|
+
import { $pickenv, prompt } from "@gajae-code/utils";
|
|
3
3
|
import type * as z from "zod/v4";
|
|
4
4
|
import {
|
|
5
5
|
executeHashlineSingle,
|
|
@@ -352,11 +352,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
352
352
|
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
353
353
|
|
|
354
354
|
constructor(private readonly session: ToolSession) {
|
|
355
|
-
const {
|
|
356
|
-
|
|
357
|
-
PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
|
|
358
|
-
PI_EDIT_VARIANT: envEditVariant = "auto",
|
|
359
|
-
} = Bun.env;
|
|
355
|
+
const { PI_EDIT_FUZZY: editFuzzy = "auto", PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto" } = Bun.env;
|
|
356
|
+
const envEditVariant = $pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT") ?? "auto";
|
|
360
357
|
|
|
361
358
|
this.#editMode = resolveConfiguredEditMode(envEditVariant);
|
|
362
359
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
package/src/edit/modes/patch.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
restoreLineEndings,
|
|
45
45
|
stripBom,
|
|
46
46
|
} from "../normalize";
|
|
47
|
+
import { withEditPathMutation } from "../path-mutation-lock";
|
|
47
48
|
import { readEditFileText, serializeEditFileText } from "../read-file";
|
|
48
49
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
49
50
|
import {
|
|
@@ -93,6 +94,15 @@ export interface ApplyPatchOptions {
|
|
|
93
94
|
fuzzyThreshold?: number;
|
|
94
95
|
allowFuzzy?: boolean;
|
|
95
96
|
fs?: FileSystem;
|
|
97
|
+
/**
|
|
98
|
+
* When false, skip durable cross-process file locks (in-process path mutex
|
|
99
|
+
* still serializes). Defaults to true only for the real `defaultFileSystem`
|
|
100
|
+
* (or when `fs` is omitted). Disk-backed adapters that are not
|
|
101
|
+
* `defaultFileSystem` (notably production `LspFileSystem` via
|
|
102
|
+
* `executePatchSingle`) MUST pass `crossProcessLock: true` explicitly —
|
|
103
|
+
* object identity is not a durable-lock capability probe.
|
|
104
|
+
*/
|
|
105
|
+
crossProcessLock?: boolean;
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1409,13 +1419,29 @@ function applyHunksToContent(
|
|
|
1409
1419
|
|
|
1410
1420
|
/**
|
|
1411
1421
|
* Apply a patch operation to the filesystem.
|
|
1422
|
+
*
|
|
1423
|
+
* Concurrent mutations of the same absolute path are serialized (in-process always;
|
|
1424
|
+
* cross-process file lock when using the real filesystem) so disjoint concurrent
|
|
1425
|
+
* edits cannot silently overwrite each other (#2900).
|
|
1412
1426
|
*/
|
|
1413
1427
|
export async function applyPatch(input: PatchInput, options: ApplyPatchOptions): Promise<ApplyPatchResult> {
|
|
1414
|
-
|
|
1428
|
+
const resolvePath = (p: string): string => resolveToCwd(p, options.cwd);
|
|
1429
|
+
const absolutePath = resolvePath(input.path);
|
|
1430
|
+
const mutationPaths = [absolutePath];
|
|
1431
|
+
if (input.rename) {
|
|
1432
|
+
const destPath = resolvePath(input.rename);
|
|
1433
|
+
if (destPath !== absolutePath) mutationPaths.push(destPath);
|
|
1434
|
+
}
|
|
1435
|
+
const usingDefaultFs = options.fs === undefined || options.fs === defaultFileSystem;
|
|
1436
|
+
// dryRun/preview must stay read-only: never create durable `<path>.lock` dirs
|
|
1437
|
+
// (would fail on read-only parents and mutate the FS during preview) (#2900 review).
|
|
1438
|
+
const crossProcess = options.dryRun === true ? false : (options.crossProcessLock ?? usingDefaultFs);
|
|
1439
|
+
return withEditPathMutation(mutationPaths, () => applyNormalizedPatch(input, options), { crossProcess });
|
|
1415
1440
|
}
|
|
1416
1441
|
|
|
1417
1442
|
/**
|
|
1418
1443
|
* Apply a normalized patch operation to the filesystem.
|
|
1444
|
+
* Caller must already hold path mutation rights when concurrent writers exist.
|
|
1419
1445
|
* @internal
|
|
1420
1446
|
*/
|
|
1421
1447
|
async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOptions): Promise<ApplyPatchResult> {
|
|
@@ -1514,6 +1540,12 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
1514
1540
|
const isMove = Boolean(input.rename) && destPath !== absolutePath;
|
|
1515
1541
|
|
|
1516
1542
|
if (!dryRun) {
|
|
1543
|
+
// Commit-time CAS: reject if another writer mutated the file after our
|
|
1544
|
+
// authoritative read (e.g. a process that did not take the path lock).
|
|
1545
|
+
const commitContent = await readExistingPatchFile(fs, absolutePath, input.path);
|
|
1546
|
+
if (commitContent !== originalContent) {
|
|
1547
|
+
throw new ApplyPatchError(`concurrent edit conflict: file changed since read: ${input.path}`);
|
|
1548
|
+
}
|
|
1517
1549
|
if (isMove) {
|
|
1518
1550
|
const parentDir = path.dirname(destPath);
|
|
1519
1551
|
if (parentDir && parentDir !== ".") {
|
|
@@ -1735,11 +1767,16 @@ export async function executePatchSingle(
|
|
|
1735
1767
|
|
|
1736
1768
|
const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
|
|
1737
1769
|
const patchFileSystem = new LspFileSystem(writethrough, signal, batchRequest, beginDeferredDiagnosticsForPath);
|
|
1770
|
+
// Production edit path uses LspFileSystem (disk-backed writethrough), which is
|
|
1771
|
+
// not `defaultFileSystem` by object identity. Force durable cross-process
|
|
1772
|
+
// locking so multi-process agents cannot silently race past the path mutex
|
|
1773
|
+
// (#2900 review: do not infer lock capability from FileSystem identity).
|
|
1738
1774
|
const result = await applyPatch(input, {
|
|
1739
1775
|
cwd: session.cwd,
|
|
1740
1776
|
fs: patchFileSystem,
|
|
1741
1777
|
fuzzyThreshold,
|
|
1742
1778
|
allowFuzzy,
|
|
1779
|
+
crossProcessLock: true,
|
|
1743
1780
|
});
|
|
1744
1781
|
|
|
1745
1782
|
// Post-write verification: only meaningful for in-place updates where the
|
|
@@ -55,6 +55,7 @@ void import("@gajae-code/natives")
|
|
|
55
55
|
// Native unavailable; fuzzy matching uses the TS fallback.
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
import { withEditPathMutation } from "../path-mutation-lock";
|
|
58
59
|
import { readEditFileText, serializeEditFileText } from "../read-file";
|
|
59
60
|
import type { EditToolDetails, LspBatchRequest } from "../renderer";
|
|
60
61
|
|
|
@@ -1087,6 +1088,47 @@ export async function executeReplaceSingle(
|
|
|
1087
1088
|
}
|
|
1088
1089
|
|
|
1089
1090
|
const absolutePath = resolvePlanPath(session, path);
|
|
1091
|
+
return withEditPathMutation([absolutePath], () =>
|
|
1092
|
+
executeReplaceSingleUnderLock({
|
|
1093
|
+
session,
|
|
1094
|
+
path,
|
|
1095
|
+
params,
|
|
1096
|
+
signal,
|
|
1097
|
+
batchRequest,
|
|
1098
|
+
allowFuzzy,
|
|
1099
|
+
fuzzyThreshold,
|
|
1100
|
+
writethrough,
|
|
1101
|
+
beginDeferredDiagnosticsForPath,
|
|
1102
|
+
absolutePath,
|
|
1103
|
+
old_text,
|
|
1104
|
+
new_text,
|
|
1105
|
+
all,
|
|
1106
|
+
}),
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function executeReplaceSingleUnderLock(
|
|
1111
|
+
options: ExecuteReplaceSingleOptions & {
|
|
1112
|
+
absolutePath: string;
|
|
1113
|
+
old_text: string;
|
|
1114
|
+
new_text: string;
|
|
1115
|
+
all: boolean | undefined;
|
|
1116
|
+
},
|
|
1117
|
+
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
|
|
1118
|
+
const {
|
|
1119
|
+
path,
|
|
1120
|
+
signal,
|
|
1121
|
+
batchRequest,
|
|
1122
|
+
allowFuzzy,
|
|
1123
|
+
fuzzyThreshold,
|
|
1124
|
+
writethrough,
|
|
1125
|
+
beginDeferredDiagnosticsForPath,
|
|
1126
|
+
absolutePath,
|
|
1127
|
+
old_text,
|
|
1128
|
+
new_text,
|
|
1129
|
+
all,
|
|
1130
|
+
} = options;
|
|
1131
|
+
|
|
1090
1132
|
const rawContent = await readEditFileText(absolutePath, path);
|
|
1091
1133
|
const { bom, text: content } = stripBom(rawContent);
|
|
1092
1134
|
const originalEnding = detectLineEnding(content);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-scoped mutation coordinator for edit tools.
|
|
3
|
+
*
|
|
4
|
+
* Serializes concurrent read→compute→write windows against the same absolute
|
|
5
|
+
* path so independent sessions cannot silently overwrite each other's successful
|
|
6
|
+
* disjoint edits (https://github.com/Yeachan-Heo/gajae-code/issues/2900).
|
|
7
|
+
*
|
|
8
|
+
* - Always: in-process async mutex keyed by resolved absolute path.
|
|
9
|
+
* - Optionally: durable cross-process `<path>.lock` via `withFileLock` for real
|
|
10
|
+
* filesystem mutations (subagents / separate processes).
|
|
11
|
+
*
|
|
12
|
+
* Nested acquisition of the same path is not supported; callers that already
|
|
13
|
+
* hold the lock must not re-enter.
|
|
14
|
+
*/
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { type FileLockOptions, withFileLock } from "../config/file-lock";
|
|
17
|
+
|
|
18
|
+
type AsyncMutex = {
|
|
19
|
+
acquire(): Promise<() => void>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const pathMutexes = new Map<string, AsyncMutex>();
|
|
23
|
+
|
|
24
|
+
function createAsyncMutex(): AsyncMutex {
|
|
25
|
+
let locked = false;
|
|
26
|
+
const waiters: Array<() => void> = [];
|
|
27
|
+
return {
|
|
28
|
+
async acquire(): Promise<() => void> {
|
|
29
|
+
if (!locked) {
|
|
30
|
+
locked = true;
|
|
31
|
+
return () => release();
|
|
32
|
+
}
|
|
33
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
34
|
+
waiters.push(resolve);
|
|
35
|
+
await promise;
|
|
36
|
+
return () => release();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function release(): void {
|
|
41
|
+
const next = waiters.shift();
|
|
42
|
+
if (next) {
|
|
43
|
+
next();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
locked = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mutexFor(absolutePath: string): AsyncMutex {
|
|
51
|
+
const key = path.resolve(absolutePath);
|
|
52
|
+
let mutex = pathMutexes.get(key);
|
|
53
|
+
if (!mutex) {
|
|
54
|
+
mutex = createAsyncMutex();
|
|
55
|
+
pathMutexes.set(key, mutex);
|
|
56
|
+
}
|
|
57
|
+
return mutex;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function withInProcessPathLock<T>(absolutePath: string, fn: () => Promise<T>): Promise<T> {
|
|
61
|
+
const release = await mutexFor(absolutePath).acquire();
|
|
62
|
+
try {
|
|
63
|
+
return await fn();
|
|
64
|
+
} finally {
|
|
65
|
+
release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Default lock budget: long enough for large writes + formatter writeback. */
|
|
70
|
+
const DEFAULT_CROSS_PROCESS_LOCK: FileLockOptions = {
|
|
71
|
+
staleMs: 60_000,
|
|
72
|
+
retries: 600,
|
|
73
|
+
retryDelayMs: 50,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type EditPathMutationOptions = {
|
|
77
|
+
/**
|
|
78
|
+
* When true (default), also acquire a durable cross-process file lock.
|
|
79
|
+
* Disable for injectible/in-memory filesystem tests that do not touch disk.
|
|
80
|
+
*/
|
|
81
|
+
crossProcess?: boolean;
|
|
82
|
+
fileLock?: FileLockOptions;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run `fn` while holding exclusive mutation rights for every absolute path.
|
|
87
|
+
* Paths are locked in lexicographic order to avoid deadlocks on multi-path ops
|
|
88
|
+
* (e.g. rename source + destination).
|
|
89
|
+
*/
|
|
90
|
+
export async function withEditPathMutation<T>(
|
|
91
|
+
absolutePaths: readonly string[],
|
|
92
|
+
fn: () => Promise<T>,
|
|
93
|
+
options: EditPathMutationOptions = {},
|
|
94
|
+
): Promise<T> {
|
|
95
|
+
const unique = [...new Set(absolutePaths.map(entry => path.resolve(entry)))].sort();
|
|
96
|
+
if (unique.length === 0) return fn();
|
|
97
|
+
|
|
98
|
+
const crossProcess = options.crossProcess !== false;
|
|
99
|
+
const fileLock = { ...DEFAULT_CROSS_PROCESS_LOCK, ...options.fileLock };
|
|
100
|
+
|
|
101
|
+
const runAt = async (index: number): Promise<T> => {
|
|
102
|
+
if (index >= unique.length) return fn();
|
|
103
|
+
const target = unique[index];
|
|
104
|
+
const next = () => runAt(index + 1);
|
|
105
|
+
return withInProcessPathLock(target, async () => {
|
|
106
|
+
if (!crossProcess) return next();
|
|
107
|
+
return withFileLock(target, next, fileLock);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return runAt(0);
|
|
112
|
+
}
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface PythonExecutorOptions {
|
|
|
61
61
|
/** @internal Bridge session id, set by `executePython` before delegating. */
|
|
62
62
|
bridgeSessionId?: string;
|
|
63
63
|
/** @internal Bridge endpoint info, set by `executePython` before delegating. */
|
|
64
|
-
bridge?: { url: string;
|
|
64
|
+
bridge?: { url: string; capability: string };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export interface PythonKernelExecutor {
|
|
@@ -106,6 +106,7 @@ export interface PythonResult {
|
|
|
106
106
|
interface PythonSession {
|
|
107
107
|
sessionId: string;
|
|
108
108
|
kernel: PythonKernel;
|
|
109
|
+
bridgeCapability?: string;
|
|
109
110
|
ownerIds: Set<string>;
|
|
110
111
|
hasFallbackOwner: boolean;
|
|
111
112
|
queue: Promise<void>;
|
|
@@ -261,14 +262,14 @@ function buildKernelEnv(options: {
|
|
|
261
262
|
sessionFile?: string;
|
|
262
263
|
artifactsDir?: string;
|
|
263
264
|
bridgeSessionId?: string;
|
|
264
|
-
bridge?: { url: string;
|
|
265
|
+
bridge?: { url: string; capability: string };
|
|
265
266
|
}): Record<string, string> | undefined {
|
|
266
267
|
const env: Record<string, string> = {};
|
|
267
268
|
if (options.sessionFile) env.PI_SESSION_FILE = options.sessionFile;
|
|
268
269
|
if (options.artifactsDir) env.PI_ARTIFACTS_DIR = options.artifactsDir;
|
|
269
270
|
if (options.bridge && options.bridgeSessionId) {
|
|
270
271
|
env.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
271
|
-
env.
|
|
272
|
+
env.PI_TOOL_BRIDGE_CAPABILITY = options.bridge.capability;
|
|
272
273
|
env.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId;
|
|
273
274
|
}
|
|
274
275
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
@@ -328,6 +329,7 @@ async function acquireSession(sessionId: string, cwd: string, options: PythonExe
|
|
|
328
329
|
const session: PythonSession = {
|
|
329
330
|
sessionId,
|
|
330
331
|
kernel,
|
|
332
|
+
bridgeCapability: options.bridge?.capability,
|
|
331
333
|
ownerIds: new Set(),
|
|
332
334
|
hasFallbackOwner: false,
|
|
333
335
|
queue: Promise.resolve(),
|
|
@@ -361,12 +363,25 @@ async function replaceSessionKernel(
|
|
|
361
363
|
throw new PythonExecutionCancelledError(false);
|
|
362
364
|
}
|
|
363
365
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
const bridge = options.bridge;
|
|
367
|
+
const previousCapability = bridge?.capability;
|
|
368
|
+
const nextCapability = bridge ? crypto.randomUUID() : undefined;
|
|
369
|
+
if (bridge && nextCapability) bridge.capability = nextCapability;
|
|
370
|
+
let next: PythonKernel | undefined;
|
|
371
|
+
try {
|
|
372
|
+
next = await startKernel(cwd, options);
|
|
373
|
+
if (sessions.get(session.sessionId) !== session) {
|
|
374
|
+
throw new PythonExecutionCancelledError(false);
|
|
375
|
+
}
|
|
376
|
+
session.kernel = next;
|
|
377
|
+
session.bridgeCapability = nextCapability;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
await next?.shutdown().catch(() => undefined);
|
|
380
|
+
if (bridge && previousCapability && bridge.capability === nextCapability) {
|
|
381
|
+
bridge.capability = previousCapability;
|
|
382
|
+
}
|
|
383
|
+
throw err;
|
|
368
384
|
}
|
|
369
|
-
session.kernel = next;
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
async function resetSession(sessionId: string): Promise<void> {
|
|
@@ -481,8 +496,8 @@ async function executeWithKernel(
|
|
|
481
496
|
displayOutputs.push({ type: "status", event });
|
|
482
497
|
});
|
|
483
498
|
const unregisterBridge =
|
|
484
|
-
options?.toolSession && options?.bridgeSessionId
|
|
485
|
-
? registerPyToolBridge(options.bridgeSessionId, {
|
|
499
|
+
options?.toolSession && options?.bridgeSessionId && options.bridge
|
|
500
|
+
? registerPyToolBridge(options.bridgeSessionId, options.bridge.capability, {
|
|
486
501
|
toolSession: options.toolSession,
|
|
487
502
|
signal: options.signal,
|
|
488
503
|
emitStatus,
|
|
@@ -578,7 +593,8 @@ async function ensureKernelAvailable(cwd: string, options: PythonExecutorOptions
|
|
|
578
593
|
async function ensureToolBridge(options: PythonExecutorOptions): Promise<void> {
|
|
579
594
|
if (!options.toolSession || options.bridge) return;
|
|
580
595
|
try {
|
|
581
|
-
|
|
596
|
+
const bridge = await ensurePyToolBridge();
|
|
597
|
+
options.bridge = { ...bridge, capability: crypto.randomUUID() };
|
|
582
598
|
} catch (err) {
|
|
583
599
|
logger.warn("Failed to start Python tool bridge", {
|
|
584
600
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -607,6 +623,9 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
|
|
|
607
623
|
await resetSession(sessionId);
|
|
608
624
|
}
|
|
609
625
|
const session = await acquireSession(sessionId, cwd, options);
|
|
626
|
+
if (options.bridge && session.bridgeCapability) {
|
|
627
|
+
options.bridge.capability = session.bridgeCapability;
|
|
628
|
+
}
|
|
610
629
|
return await runQueued(session, options, async () => {
|
|
611
630
|
if (options.signal?.aborted) {
|
|
612
631
|
throw new PythonExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -453,10 +453,10 @@ if "__gjc_prelude_loaded__" not in globals():
|
|
|
453
453
|
|
|
454
454
|
if all(
|
|
455
455
|
_k in os.environ
|
|
456
|
-
for _k in ("PI_TOOL_BRIDGE_URL", "
|
|
456
|
+
for _k in ("PI_TOOL_BRIDGE_URL", "PI_TOOL_BRIDGE_CAPABILITY", "PI_TOOL_BRIDGE_SESSION")
|
|
457
457
|
):
|
|
458
458
|
tool = _ToolProxy(
|
|
459
459
|
os.environ["PI_TOOL_BRIDGE_URL"],
|
|
460
|
-
os.environ["
|
|
460
|
+
os.environ["PI_TOOL_BRIDGE_CAPABILITY"],
|
|
461
461
|
os.environ["PI_TOOL_BRIDGE_SESSION"],
|
|
462
462
|
)
|