@gajae-code/coding-agent 0.11.6 → 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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- 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 +70 -7
- 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/runner-artifact.d.ts +8 -0
- 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 +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -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 +7 -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 +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- 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 +6 -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 +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -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/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- 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/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -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 +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- 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/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -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/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- 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 +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- 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 +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- 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 +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- 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 +4 -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/btw-user.md +3 -8
- 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/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- 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 +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -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 +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- 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 +68 -20
- 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 +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- 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 +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- 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 +199 -89
- 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/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as http from "node:http";
|
|
9
|
+
import * as https from "node:https";
|
|
10
|
+
import * as net from "node:net";
|
|
8
11
|
import * as os from "node:os";
|
|
9
12
|
import * as path from "node:path";
|
|
10
13
|
import { isEnoent, logger } from "@gajae-code/utils";
|
|
11
14
|
import * as git from "../../../utils/git";
|
|
15
|
+
import { isPrivateOrSpecialAddress, validatePublicHttpUrl } from "../../../web/insane/url-guard";
|
|
12
16
|
|
|
13
17
|
import type { MarketplaceCatalog, MarketplaceSourceType } from "./types";
|
|
14
18
|
import { isValidNameSegment } from "./types";
|
|
@@ -194,6 +198,192 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
|
|
|
194
198
|
|
|
195
199
|
/** Relative path from a marketplace root to its catalog file. */
|
|
196
200
|
const CATALOG_RELATIVE_PATH = path.join(".claude-plugin", "marketplace.json");
|
|
201
|
+
const URL_FETCH_TIMEOUT_MS = 60_000;
|
|
202
|
+
const URL_FETCH_MAX_REDIRECTS = 5;
|
|
203
|
+
const URL_FETCH_MAX_HEADER_BYTES = 16 * 1024;
|
|
204
|
+
/** Direct JSON catalogs are metadata; 2 MiB leaves ample room without permitting unbounded buffering. */
|
|
205
|
+
const URL_FETCH_MAX_BYTES = 2 * 1024 * 1024;
|
|
206
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
207
|
+
|
|
208
|
+
function withAbortSignal<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
209
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
210
|
+
const deferred = Promise.withResolvers<T>();
|
|
211
|
+
const abort = () => deferred.reject(signal.reason);
|
|
212
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
213
|
+
operation.then(deferred.resolve, deferred.reject).finally(() => signal.removeEventListener("abort", abort));
|
|
214
|
+
return deferred.promise;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizePeerAddress(address: string): string {
|
|
218
|
+
const normalized = address.toLowerCase();
|
|
219
|
+
return normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeUrlHostname(hostname: string): string {
|
|
223
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function validateCatalogUrl(rawUrl: string, signal: AbortSignal) {
|
|
227
|
+
const url = new URL(rawUrl);
|
|
228
|
+
const hostname = normalizeUrlHostname(url.hostname);
|
|
229
|
+
// URL.hostname retains IPv6 brackets in Node/Bun, while net.isIP expects the bare literal.
|
|
230
|
+
if (net.isIP(hostname) === 6) {
|
|
231
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
232
|
+
return { ok: false as const, reason: `unsupported scheme ${url.protocol}` };
|
|
233
|
+
if (url.username || url.password) return { ok: false as const, reason: "URL credentials are not allowed" };
|
|
234
|
+
if (isPrivateOrSpecialAddress(hostname))
|
|
235
|
+
return { ok: false as const, reason: "private, loopback, link-local, or reserved IP literal" };
|
|
236
|
+
return { ok: true as const, url, addresses: [hostname] };
|
|
237
|
+
}
|
|
238
|
+
return withAbortSignal(validatePublicHttpUrl(rawUrl), signal);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function openCatalogResponse(url: URL, addresses: string[], signal: AbortSignal): Promise<http.IncomingMessage> {
|
|
242
|
+
const hostname = normalizeUrlHostname(url.hostname);
|
|
243
|
+
const approved = addresses.map(address => ({ address, family: net.isIP(address) }));
|
|
244
|
+
const approvedPeers = new Set(approved.map(record => normalizePeerAddress(record.address)));
|
|
245
|
+
const lookup: net.LookupFunction = (requestedHostname, options, callback) => {
|
|
246
|
+
const requestedFamily = options.family === "IPv4" ? 4 : options.family === "IPv6" ? 6 : (options.family ?? 0);
|
|
247
|
+
const matching = approved.filter(record => requestedFamily === 0 || record.family === requestedFamily);
|
|
248
|
+
if (normalizeUrlHostname(requestedHostname) !== hostname || matching.length === 0) {
|
|
249
|
+
const error = Object.assign(new Error("No approved address for marketplace host"), { code: "ENOTFOUND" });
|
|
250
|
+
callback(error, options.all ? [] : "", 0);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (options.all) callback(null, matching);
|
|
254
|
+
else callback(null, matching[0].address, matching[0].family);
|
|
255
|
+
};
|
|
256
|
+
const deferred = Promise.withResolvers<http.IncomingMessage>();
|
|
257
|
+
const options: https.RequestOptions = {
|
|
258
|
+
protocol: url.protocol,
|
|
259
|
+
hostname,
|
|
260
|
+
port: url.port || undefined,
|
|
261
|
+
path: `${url.pathname}${url.search}`,
|
|
262
|
+
method: "GET",
|
|
263
|
+
headers: { Accept: "application/json", "Accept-Encoding": "identity", Connection: "close", Host: url.host },
|
|
264
|
+
agent: false,
|
|
265
|
+
insecureHTTPParser: false,
|
|
266
|
+
lookup,
|
|
267
|
+
maxHeaderSize: URL_FETCH_MAX_HEADER_BYTES,
|
|
268
|
+
signal,
|
|
269
|
+
...(url.protocol === "https:"
|
|
270
|
+
? { rejectUnauthorized: true, servername: net.isIP(hostname) === 0 ? hostname : undefined }
|
|
271
|
+
: {}),
|
|
272
|
+
};
|
|
273
|
+
const requestFn = url.protocol === "https:" ? https.request : http.request;
|
|
274
|
+
const request = requestFn(options, response => {
|
|
275
|
+
const peer = response.socket.remoteAddress;
|
|
276
|
+
if (!peer || isPrivateOrSpecialAddress(peer) || !approvedPeers.has(normalizePeerAddress(peer))) {
|
|
277
|
+
response.destroy();
|
|
278
|
+
deferred.reject(
|
|
279
|
+
new Error(`Refusing marketplace catalog response from unapproved connected peer ${peer ?? "unknown"}`),
|
|
280
|
+
);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
deferred.resolve(response);
|
|
284
|
+
});
|
|
285
|
+
request.once("error", deferred.reject);
|
|
286
|
+
const abort = () => {
|
|
287
|
+
request.destroy(signal.reason);
|
|
288
|
+
deferred.reject(signal.reason);
|
|
289
|
+
};
|
|
290
|
+
if (signal.aborted) abort();
|
|
291
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
292
|
+
request.once("close", () => signal.removeEventListener("abort", abort));
|
|
293
|
+
request.end();
|
|
294
|
+
return deferred.promise;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function readCatalogResponse(
|
|
298
|
+
response: http.IncomingMessage,
|
|
299
|
+
source: string,
|
|
300
|
+
signal: AbortSignal,
|
|
301
|
+
): Promise<string> {
|
|
302
|
+
const rawContentLengths: string[] = [];
|
|
303
|
+
let hasTransferEncoding = false;
|
|
304
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) {
|
|
305
|
+
const name = response.rawHeaders[index]?.toLowerCase();
|
|
306
|
+
if (name === "content-length") rawContentLengths.push(response.rawHeaders[index + 1] ?? "");
|
|
307
|
+
if (name === "transfer-encoding") hasTransferEncoding = true;
|
|
308
|
+
}
|
|
309
|
+
if (rawContentLengths.length > 1 || (rawContentLengths.length > 0 && hasTransferEncoding)) {
|
|
310
|
+
response.destroy();
|
|
311
|
+
throw new Error(`Marketplace catalog from ${source} has ambiguous response framing`);
|
|
312
|
+
}
|
|
313
|
+
const contentLength = response.headers["content-length"];
|
|
314
|
+
if (contentLength !== undefined) {
|
|
315
|
+
const declaredBytes =
|
|
316
|
+
typeof contentLength === "string" && /^\d+$/.test(contentLength) ? Number(contentLength) : NaN;
|
|
317
|
+
if (!Number.isSafeInteger(declaredBytes)) {
|
|
318
|
+
response.destroy();
|
|
319
|
+
throw new Error(`Marketplace catalog from ${source} has an invalid Content-Length`);
|
|
320
|
+
}
|
|
321
|
+
if (declaredBytes > URL_FETCH_MAX_BYTES) {
|
|
322
|
+
response.destroy();
|
|
323
|
+
throw new Error(`Marketplace catalog from ${source} exceeds the maximum size of 2 MiB`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const abort = () => response.destroy(signal.reason);
|
|
328
|
+
if (signal.aborted) {
|
|
329
|
+
abort();
|
|
330
|
+
throw signal.reason;
|
|
331
|
+
}
|
|
332
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
333
|
+
const chunks: Buffer[] = [];
|
|
334
|
+
let receivedBytes = 0;
|
|
335
|
+
try {
|
|
336
|
+
for await (const chunk of response) {
|
|
337
|
+
const bytes = Buffer.from(chunk);
|
|
338
|
+
receivedBytes += bytes.byteLength;
|
|
339
|
+
if (receivedBytes > URL_FETCH_MAX_BYTES) {
|
|
340
|
+
response.destroy();
|
|
341
|
+
throw new Error(`Marketplace catalog from ${source} exceeds the maximum size of 2 MiB`);
|
|
342
|
+
}
|
|
343
|
+
chunks.push(bytes);
|
|
344
|
+
}
|
|
345
|
+
return Buffer.concat(chunks, receivedBytes).toString("utf8");
|
|
346
|
+
} finally {
|
|
347
|
+
signal.removeEventListener("abort", abort);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function fetchMarketplaceCatalogUrl(source: string): Promise<string> {
|
|
352
|
+
const deadline = AbortSignal.timeout(URL_FETCH_TIMEOUT_MS);
|
|
353
|
+
let currentUrl = source;
|
|
354
|
+
try {
|
|
355
|
+
for (let redirectCount = 0; ; redirectCount++) {
|
|
356
|
+
const guard = await validateCatalogUrl(currentUrl, deadline);
|
|
357
|
+
if (!guard.ok) {
|
|
358
|
+
throw new Error(`Refusing marketplace catalog URL: target URL is not public HTTP(S): ${guard.reason}`);
|
|
359
|
+
}
|
|
360
|
+
const response = await openCatalogResponse(guard.url, guard.addresses, deadline);
|
|
361
|
+
const status = response.statusCode ?? 0;
|
|
362
|
+
if (REDIRECT_STATUSES.has(status)) {
|
|
363
|
+
if (redirectCount >= URL_FETCH_MAX_REDIRECTS) {
|
|
364
|
+
response.destroy();
|
|
365
|
+
throw new Error(`Too many redirects fetching marketplace catalog from ${source}`);
|
|
366
|
+
}
|
|
367
|
+
const location = response.headers.location;
|
|
368
|
+
response.destroy();
|
|
369
|
+
if (!location)
|
|
370
|
+
throw new Error(`Marketplace catalog redirect from ${currentUrl} is missing a Location header`);
|
|
371
|
+
currentUrl = new URL(location, guard.url).toString();
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (status < 200 || status >= 300) {
|
|
375
|
+
response.destroy();
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Failed to fetch marketplace catalog from ${currentUrl}: HTTP ${status} ${response.statusMessage ?? ""}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return await readCatalogResponse(response, currentUrl, deadline);
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (deadline.aborted) throw new Error(`Timed out fetching marketplace catalog from ${source}`);
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
197
387
|
|
|
198
388
|
/**
|
|
199
389
|
* Expand a `~/...` path to an absolute path using os.homedir().
|
|
@@ -249,13 +439,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
|
|
|
249
439
|
}
|
|
250
440
|
|
|
251
441
|
// type === "url"
|
|
252
|
-
const
|
|
253
|
-
if (!response.ok) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
`Failed to fetch marketplace catalog from ${source}: HTTP ${response.status} ${response.statusText}`,
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
const text = await response.text();
|
|
442
|
+
const text = await fetchMarketplaceCatalogUrl(source);
|
|
259
443
|
const catalog = parseMarketplaceCatalog(text, source);
|
|
260
444
|
|
|
261
445
|
const catalogDir = path.join(cacheDir, catalog.name);
|
|
@@ -705,7 +705,7 @@ function applyGjcTmuxRootTerminalTitleProfile(context: {
|
|
|
705
705
|
}
|
|
706
706
|
|
|
707
707
|
function shouldSetGjcTmuxRootTerminalTitle(parsed: Args, env: NodeJS.ProcessEnv): boolean {
|
|
708
|
-
return !parsed.noTitle && !env.PI_NO_TITLE;
|
|
708
|
+
return !parsed.noTitle && !(env.GJC_NO_TITLE || env.PI_NO_TITLE);
|
|
709
709
|
}
|
|
710
710
|
|
|
711
711
|
function buildTmuxRenameWindowArgs(title: string, target?: string): string[] {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
5
|
+
import { Process } from "@gajae-code/natives";
|
|
4
6
|
import { readLinuxProcStartTime } from "./linux-proc";
|
|
5
7
|
import { assertSafePathComponent } from "./session-layout";
|
|
6
|
-
import { lifecyclePaths } from "./tmux-owner-isolation";
|
|
8
|
+
import { lifecyclePaths, type OwnerIntent, observeOwnerTerminal } from "./tmux-owner-isolation";
|
|
7
9
|
|
|
8
10
|
export const MANAGED_OWNER_SUPERVISOR_ARG = "--internal-managed-owner-supervisor";
|
|
9
11
|
export const MANAGED_OWNER_CHILD_TOKEN_ENV = "GJC_MANAGED_OWNER_CHILD_TOKEN";
|
|
@@ -14,6 +16,15 @@ export const MANAGED_OWNER_STATE_DIR_ENV = "GJC_TMUX_OWNER_STATE_DIR";
|
|
|
14
16
|
export const MANAGED_OWNER_RUN_ID_ENV = "GJC_MANAGED_OWNER_RUN_ID";
|
|
15
17
|
export const MANAGED_OWNER_INCARNATION_ENV = "GJC_MANAGED_OWNER_INCARNATION";
|
|
16
18
|
|
|
19
|
+
let bootstrapSigtermPending = false;
|
|
20
|
+
const captureBootstrapSigterm = () => {
|
|
21
|
+
bootstrapSigtermPending = true;
|
|
22
|
+
};
|
|
23
|
+
if (process.argv.includes(MANAGED_OWNER_SUPERVISOR_ARG)) {
|
|
24
|
+
process.removeAllListeners("SIGTERM");
|
|
25
|
+
process.on("SIGTERM", captureBootstrapSigterm);
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export interface ManagedOwnerBinding {
|
|
18
29
|
schema_version: 2;
|
|
19
30
|
generation: string;
|
|
@@ -52,7 +63,14 @@ function requiredEnvironment(name: string): string {
|
|
|
52
63
|
return value;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
function lifecycleRoot(): {
|
|
66
|
+
function lifecycleRoot(): {
|
|
67
|
+
root: string;
|
|
68
|
+
stateDir: string;
|
|
69
|
+
generation: string;
|
|
70
|
+
sessionId: string;
|
|
71
|
+
runId: string;
|
|
72
|
+
incarnation: string;
|
|
73
|
+
} {
|
|
56
74
|
const stateDir = requiredEnvironment(MANAGED_OWNER_STATE_DIR_ENV);
|
|
57
75
|
const sessionId = requiredEnvironment(MANAGED_OWNER_SESSION_ID_ENV);
|
|
58
76
|
const generation = requiredEnvironment(MANAGED_OWNER_GENERATION_ENV);
|
|
@@ -68,7 +86,7 @@ function lifecycleRoot(): { root: string; generation: string; sessionId: string;
|
|
|
68
86
|
if (!path.isAbsolute(stateDir)) throw new Error("managed_owner_lifecycle_path_unsafe");
|
|
69
87
|
const root = lifecyclePaths(stateDir, sessionId, generation).root;
|
|
70
88
|
if (!root.startsWith(`${path.resolve(stateDir)}${path.sep}`)) throw new Error("managed_owner_lifecycle_path_unsafe");
|
|
71
|
-
return { root, generation, sessionId, runId, incarnation };
|
|
89
|
+
return { root, stateDir, generation, sessionId, runId, incarnation };
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
function commandDigest(command: readonly string[]): string {
|
|
@@ -106,8 +124,14 @@ export function isManagedOwnerSupervisorArgv(args: readonly string[]): boolean {
|
|
|
106
124
|
|
|
107
125
|
/** Runs one exact child and publishes authority only for a directly observed Linux signal 6. */
|
|
108
126
|
export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
109
|
-
const { root, generation, sessionId, runId, incarnation } = lifecycleRoot();
|
|
127
|
+
const { root, stateDir, generation, sessionId, runId, incarnation } = lifecycleRoot();
|
|
110
128
|
const command = childCommand();
|
|
129
|
+
let sigtermPending = bootstrapSigtermPending;
|
|
130
|
+
const captureEarlySigterm = () => {
|
|
131
|
+
sigtermPending = true;
|
|
132
|
+
};
|
|
133
|
+
process.removeAllListeners("SIGTERM");
|
|
134
|
+
process.on("SIGTERM", captureEarlySigterm);
|
|
111
135
|
const supervisorStartTime = await readLinuxProcStartTime(process.pid);
|
|
112
136
|
if (!supervisorStartTime) throw new Error("managed_owner_supervisor_start_time_unavailable");
|
|
113
137
|
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
@@ -135,7 +159,62 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
|
135
159
|
});
|
|
136
160
|
const childStartTime = await readLinuxProcStartTime(child.pid);
|
|
137
161
|
if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
|
|
162
|
+
const childProcess = Process.fromPid(child.pid);
|
|
163
|
+
if (!childProcess) throw new Error("managed_owner_child_reference_unavailable");
|
|
164
|
+
if (process.platform === "linux" && childProcess.incarnation !== `linux:${childStartTime}`)
|
|
165
|
+
throw new Error("managed_owner_child_incarnation_mismatch");
|
|
166
|
+
let childExited = false;
|
|
167
|
+
let sigtermRelayed = false;
|
|
168
|
+
let relayedIntent: OwnerIntent | null = null;
|
|
169
|
+
let relayedAt: string | null = null;
|
|
170
|
+
const relaySigterm = () => {
|
|
171
|
+
if (childExited || sigtermRelayed) return;
|
|
172
|
+
let candidateIntent: OwnerIntent | null = null;
|
|
173
|
+
try {
|
|
174
|
+
const candidate = JSON.parse(
|
|
175
|
+
fsSync.readFileSync(lifecyclePaths(stateDir, sessionId, generation).intentFile, "utf8"),
|
|
176
|
+
) as Partial<OwnerIntent>;
|
|
177
|
+
candidateIntent = typeof candidate.dispatch_id === "string" ? (candidate as OwnerIntent) : null;
|
|
178
|
+
} catch {
|
|
179
|
+
candidateIntent = null;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
if (!childProcess.signalRoot(15)) return;
|
|
183
|
+
} catch {
|
|
184
|
+
// The child exited between intent capture and delivery.
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
sigtermRelayed = true;
|
|
188
|
+
relayedAt = new Date().toISOString();
|
|
189
|
+
relayedIntent = candidateIntent;
|
|
190
|
+
};
|
|
191
|
+
sigtermPending ||= bootstrapSigtermPending;
|
|
192
|
+
process.removeListener("SIGTERM", captureBootstrapSigterm);
|
|
193
|
+
process.removeListener("SIGTERM", captureEarlySigterm);
|
|
194
|
+
process.on("SIGTERM", relaySigterm);
|
|
195
|
+
if (sigtermPending) relaySigterm();
|
|
138
196
|
const exitCode = await child.exited;
|
|
197
|
+
childExited = true;
|
|
198
|
+
process.removeListener("SIGTERM", relaySigterm);
|
|
199
|
+
const terminalIntent = relayedIntent as OwnerIntent | null;
|
|
200
|
+
const terminalObservedAt = relayedAt as string | null;
|
|
201
|
+
if (sigtermRelayed && terminalObservedAt && terminalIntent) {
|
|
202
|
+
await observeOwnerTerminal({
|
|
203
|
+
schema_version: 1,
|
|
204
|
+
op: "observe_terminal",
|
|
205
|
+
session_id: sessionId,
|
|
206
|
+
owner_generation: generation,
|
|
207
|
+
state_dir: stateDir,
|
|
208
|
+
socket_key: process.env.GJC_TMUX_OWNER_SERVER_KEY ?? "",
|
|
209
|
+
observer: "raw_monitor",
|
|
210
|
+
observed_at: terminalObservedAt,
|
|
211
|
+
signal: "SIGTERM",
|
|
212
|
+
exit_code: exitCode,
|
|
213
|
+
exit_kind: "supervisor_child_exit",
|
|
214
|
+
reason: "managed_owner_supervisor_exit",
|
|
215
|
+
operator_dispatch_id: terminalIntent.dispatch_id,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
139
218
|
if (child.signalCode === "SIGABRT") {
|
|
140
219
|
const receipt: ManagedOwnerSigabrtReceipt = {
|
|
141
220
|
schema_version: 2,
|
|
@@ -11,6 +11,15 @@ import {
|
|
|
11
11
|
type RalplanIndexRow,
|
|
12
12
|
summarizeRalplanIndex,
|
|
13
13
|
} from "./ledger-event-renderer";
|
|
14
|
+
import {
|
|
15
|
+
assertCwdMatchesRepositoryBinding,
|
|
16
|
+
assertPathUnderRepositoryBinding,
|
|
17
|
+
captureRepositoryBinding,
|
|
18
|
+
parseRepositoryBinding,
|
|
19
|
+
publicRepositoryBinding,
|
|
20
|
+
type RepositoryBinding,
|
|
21
|
+
RepositoryBindingError,
|
|
22
|
+
} from "./repository-binding";
|
|
14
23
|
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
15
24
|
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
16
25
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
@@ -179,6 +188,43 @@ async function readActiveRunId(cwd: string, sessionId: string): Promise<string |
|
|
|
179
188
|
return candidate;
|
|
180
189
|
}
|
|
181
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Read the authoritative repository binding from ralplan run state and fail closed
|
|
193
|
+
* when the active cwd no longer matches (handoff / QA / stage write) (#2901).
|
|
194
|
+
*/
|
|
195
|
+
async function enforceRalplanRepositoryBinding(cwd: string, sessionId: string): Promise<RepositoryBinding> {
|
|
196
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
197
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
198
|
+
if (existingRead.kind === "corrupt") {
|
|
199
|
+
throw new RalplanCommandError(
|
|
200
|
+
2,
|
|
201
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to proceed at ${statePath}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (existingRead.kind === "absent") {
|
|
205
|
+
// No prior seed — capture live authority so subsequent handoffs still have a stamp.
|
|
206
|
+
return publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
207
|
+
}
|
|
208
|
+
const raw = existingRead.value.repository_binding ?? existingRead.value.repositoryBinding;
|
|
209
|
+
try {
|
|
210
|
+
if (raw === undefined) {
|
|
211
|
+
// Legacy seeds without a field: stamp current cwd and require future writes match it.
|
|
212
|
+
return publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
213
|
+
}
|
|
214
|
+
const binding = parseRepositoryBinding(raw);
|
|
215
|
+
await assertCwdMatchesRepositoryBinding(cwd, binding);
|
|
216
|
+
return publicRepositoryBinding(binding);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof RepositoryBindingError) {
|
|
219
|
+
throw new RalplanCommandError(2, `ralplan repository binding rejected: ${error.message}`);
|
|
220
|
+
}
|
|
221
|
+
throw new RalplanCommandError(
|
|
222
|
+
2,
|
|
223
|
+
`ralplan repository binding rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
182
228
|
/**
|
|
183
229
|
* Run-state phases that an artifact write must never reopen. Once ralplan has
|
|
184
230
|
* reached a terminal/handed-off phase, a stray `--write` must not regress
|
|
@@ -651,6 +697,24 @@ async function buildRalplanHud(options: {
|
|
|
651
697
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
652
698
|
const plannerState = parsePlannerStateArgs(args);
|
|
653
699
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
700
|
+
// Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
|
|
701
|
+
const repositoryBinding = await enforceRalplanRepositoryBinding(cwd, resolved.sessionId);
|
|
702
|
+
// Artifact file paths (when --artifact points at a file) must stay under the bound root.
|
|
703
|
+
const rawArtifact = flagValue(args, "--artifact");
|
|
704
|
+
if (rawArtifact && !isRestrictedRoleAgentBash()) {
|
|
705
|
+
const candidate = path.isAbsolute(rawArtifact) ? rawArtifact : path.resolve(cwd, rawArtifact);
|
|
706
|
+
try {
|
|
707
|
+
const stat = await fs.stat(candidate);
|
|
708
|
+
if (stat.isFile()) {
|
|
709
|
+
assertPathUnderRepositoryBinding(repositoryBinding, candidate);
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (error instanceof RepositoryBindingError) {
|
|
713
|
+
throw new RalplanCommandError(2, `ralplan repository binding rejected: ${error.message}`);
|
|
714
|
+
}
|
|
715
|
+
// Non-file / missing path is inline content; resolveArtifactContent already handled it.
|
|
716
|
+
}
|
|
717
|
+
}
|
|
654
718
|
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
655
719
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
656
720
|
|
|
@@ -696,6 +760,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
696
760
|
stage: persisted.stage,
|
|
697
761
|
stage_n: persisted.stageN,
|
|
698
762
|
sha256: persisted.sha256,
|
|
763
|
+
repository_binding: repositoryBinding,
|
|
699
764
|
created_at: persisted.createdAt,
|
|
700
765
|
};
|
|
701
766
|
if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
|
|
@@ -809,7 +874,7 @@ function resolveConsensusArgs(args: readonly string[], cwd: string): ConsensusHa
|
|
|
809
874
|
async function seedRalplanState(
|
|
810
875
|
cwd: string,
|
|
811
876
|
resolved: ConsensusHandoffArgs,
|
|
812
|
-
): Promise<{ statePath: string; runId: string }> {
|
|
877
|
+
): Promise<{ statePath: string; runId: string; repositoryBinding: RepositoryBinding }> {
|
|
813
878
|
const statePath = ralplanStatePath(cwd, resolved.sessionId);
|
|
814
879
|
// Reuse an existing run id when present so a re-invocation of `gjc ralplan "task"` doesn't
|
|
815
880
|
// orphan in-progress artifacts under a fresh run id.
|
|
@@ -817,6 +882,11 @@ async function seedRalplanState(
|
|
|
817
882
|
const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
|
|
818
883
|
assertSafePathComponent(runId, "run-id");
|
|
819
884
|
const now = new Date().toISOString();
|
|
885
|
+
// When an active seed already carries authority, re-entry must match it (fail closed).
|
|
886
|
+
// Otherwise stamp the current cwd as the durable binding for this run.
|
|
887
|
+
const repositoryBinding = existingRunId
|
|
888
|
+
? await enforceRalplanRepositoryBinding(cwd, resolved.sessionId)
|
|
889
|
+
: publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
820
890
|
const payload: Record<string, unknown> = {
|
|
821
891
|
active: true,
|
|
822
892
|
current_phase: "planner",
|
|
@@ -827,6 +897,7 @@ async function seedRalplanState(
|
|
|
827
897
|
task: resolved.task,
|
|
828
898
|
run_id: runId,
|
|
829
899
|
updated_at: now,
|
|
900
|
+
repository_binding: repositoryBinding,
|
|
830
901
|
};
|
|
831
902
|
if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
|
|
832
903
|
if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
|
|
@@ -849,7 +920,7 @@ async function seedRalplanState(
|
|
|
849
920
|
},
|
|
850
921
|
});
|
|
851
922
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
|
|
852
|
-
return { statePath, runId };
|
|
923
|
+
return { statePath, runId, repositoryBinding };
|
|
853
924
|
}
|
|
854
925
|
|
|
855
926
|
async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
@@ -857,7 +928,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
857
928
|
if (!resolved.task) {
|
|
858
929
|
throw new RalplanCommandError(2, 'gjc ralplan requires a task description, e.g. `gjc ralplan "<task>"`.');
|
|
859
930
|
}
|
|
860
|
-
const { statePath, runId } = await seedRalplanState(cwd, resolved);
|
|
931
|
+
const { statePath, runId, repositoryBinding } = await seedRalplanState(cwd, resolved);
|
|
861
932
|
const mode = resolved.deliberate ? "deliberate" : "short";
|
|
862
933
|
await syncRalplanHud({
|
|
863
934
|
cwd,
|
|
@@ -875,6 +946,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
875
946
|
state_path: statePath,
|
|
876
947
|
run_id: runId,
|
|
877
948
|
handoff: "/skill:ralplan",
|
|
949
|
+
repository_binding: repositoryBinding,
|
|
878
950
|
};
|
|
879
951
|
const stdout = resolved.json
|
|
880
952
|
? renderCliWriteReceipt({ ok: true, ...summary })
|