@gajae-code/coding-agent 0.18.12 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
package/src/cli/update-cli.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
-
import { exactReplaceRetained, exactUnlink } from "@gajae-code/natives";
|
|
13
|
+
import { exactReplaceRetained, exactUnlink, exactUnlinkDirect } from "@gajae-code/natives";
|
|
14
14
|
import { $which, APP_NAME, isCompiledBinary, isEnoent, logger, redactCrashSecrets, VERSION } from "@gajae-code/utils";
|
|
15
15
|
import { $ } from "bun";
|
|
16
16
|
import chalk from "chalk";
|
|
@@ -287,6 +287,40 @@ function resolveNpmManagedTarget(
|
|
|
287
287
|
return undefined;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Detect a project-local package install: `<...>/node_modules/.bin/<name>` with
|
|
292
|
+
* or without an executable extension.
|
|
293
|
+
*
|
|
294
|
+
* On Windows a dependency-installed CLI is a real `.exe` copied into
|
|
295
|
+
* `node_modules/.bin`, so the `.cmd`/`.ps1` shim rules never match it and
|
|
296
|
+
* `gjc update` used to treat it as a standalone binary and try to replace it
|
|
297
|
+
* (#6842). That file belongs to the package manager and to the project's
|
|
298
|
+
* dependency tree: replacing it breaks the lockfile contract and, on Windows,
|
|
299
|
+
* fails on the file's ACL. The caller must redirect to the package manager
|
|
300
|
+
* instead of downloading a replacement.
|
|
301
|
+
*/
|
|
302
|
+
function resolveProjectLocalPackageInstall(
|
|
303
|
+
ompPath: string,
|
|
304
|
+
platform: PathPlatform = process.platform,
|
|
305
|
+
): { packageRoot: string } | undefined {
|
|
306
|
+
const pathApi = pathApiForPlatform(platform);
|
|
307
|
+
const resolved = pathApi.normalize(ompPath);
|
|
308
|
+
const binDir = pathApi.dirname(resolved);
|
|
309
|
+
if (pathApi.basename(binDir).toLowerCase() !== ".bin") return undefined;
|
|
310
|
+
if (pathApi.basename(pathApi.dirname(binDir)).toLowerCase() !== "node_modules") return undefined;
|
|
311
|
+
const extension = pathApi.extname(resolved).toLowerCase();
|
|
312
|
+
const basename = extension ? pathApi.basename(resolved, extension) : pathApi.basename(resolved);
|
|
313
|
+
if (basename.toLowerCase() !== APP_NAME.toLowerCase()) return undefined;
|
|
314
|
+
return { packageRoot: pathApi.dirname(binDir) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function resolveProjectLocalPackageInstallForTest(
|
|
318
|
+
ompPath: string,
|
|
319
|
+
platform: PathPlatform,
|
|
320
|
+
): { packageRoot: string } | undefined {
|
|
321
|
+
return resolveProjectLocalPackageInstall(ompPath, platform);
|
|
322
|
+
}
|
|
323
|
+
|
|
290
324
|
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
291
325
|
if (resolveNpmManagedTarget(ompPath)) return "npm";
|
|
292
326
|
if (!bunBinDir) return "binary";
|
|
@@ -401,6 +435,15 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
401
435
|
);
|
|
402
436
|
}
|
|
403
437
|
|
|
438
|
+
const projectLocalInstall = ompPath ? resolveProjectLocalPackageInstall(ompPath) : undefined;
|
|
439
|
+
if (ompPath && projectLocalInstall) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
formatUnsupportedTargetMessage(
|
|
442
|
+
`${APP_NAME} at ${ompPath} is installed by a package manager in this project's node_modules; update the dependency instead of replacing that file`,
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
404
447
|
if (ompPath && isShimPath(ompPath, bunBinDir)) {
|
|
405
448
|
if (isProtectedSourcePath(userPath)) {
|
|
406
449
|
throw new Error(formatUnsupportedTargetMessage(`Refusing to install over a source checkout at ${userPath}`));
|
|
@@ -1351,14 +1394,179 @@ function binaryStagingPath(targetPath: string, stamp: string): string {
|
|
|
1351
1394
|
return `${targetPath.slice(0, -extension.length)}.new.${stamp}${extension}`;
|
|
1352
1395
|
}
|
|
1353
1396
|
|
|
1397
|
+
const UPDATE_FILE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
|
|
1398
|
+
const EXACT_REPLACE_DESTINATION_NAME = /^\.gjc-exact-replace-destination-([0-9a-f]+)-([0-9a-f]+)$/u;
|
|
1399
|
+
// Do not race a separate process that is still completing a native replacement in this directory.
|
|
1400
|
+
const EXACT_REPLACE_CLEANUP_GRACE_MS = 10 * 60 * 1000;
|
|
1401
|
+
|
|
1402
|
+
function isUpdateStagingName(targetPath: string, name: string): boolean {
|
|
1403
|
+
const extension = path.extname(targetPath);
|
|
1404
|
+
const prefix = `${path.basename(targetPath, extension)}.new.`;
|
|
1405
|
+
if (!name.startsWith(prefix)) return false;
|
|
1406
|
+
const suffix = name.slice(prefix.length);
|
|
1407
|
+
if (extension && !suffix.endsWith(extension)) return false;
|
|
1408
|
+
const id = extension ? suffix.slice(0, -extension.length) : suffix;
|
|
1409
|
+
return UPDATE_FILE_ID.test(id);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function isUpdateBackupName(targetPath: string, name: string): boolean {
|
|
1413
|
+
const prefix = `${path.basename(targetPath)}.bak.`;
|
|
1414
|
+
if (name.startsWith(prefix) && UPDATE_FILE_ID.test(name.slice(prefix.length))) return true;
|
|
1415
|
+
const legacyPrefix = `${path.basename(targetPath)}-`;
|
|
1416
|
+
if (!name.startsWith(legacyPrefix)) return false;
|
|
1417
|
+
const legacyName = name.slice(legacyPrefix.length);
|
|
1418
|
+
const marker = legacyName.indexOf(".bak.");
|
|
1419
|
+
if (marker < 0) return false;
|
|
1420
|
+
const version = legacyName.slice(0, marker);
|
|
1421
|
+
const timestamp = legacyName.slice(marker + ".bak.".length);
|
|
1422
|
+
return (
|
|
1423
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version) && /^[0-9A-Za-z._-]+$/u.test(timestamp)
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export function isUpdateStagingNameForTest(targetPath: string, name: string): boolean {
|
|
1428
|
+
return isUpdateStagingName(targetPath, name);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
export function isUpdateBackupNameForTest(targetPath: string, name: string): boolean {
|
|
1432
|
+
return isUpdateBackupName(targetPath, name);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
async function removeExactUpdateArtifact(filePath: string, parent: DirectoryIdentity): Promise<string | undefined> {
|
|
1436
|
+
try {
|
|
1437
|
+
const snapshot = await snapshotRegularFile(filePath);
|
|
1438
|
+
if (!snapshot) return undefined;
|
|
1439
|
+
if (snapshot.identity.parentDev !== parent.dev || snapshot.identity.parentIno !== parent.ino)
|
|
1440
|
+
return `parent_identity_changed:${filePath}`;
|
|
1441
|
+
const removed = exactUnlinkDirect(
|
|
1442
|
+
filePath,
|
|
1443
|
+
toNativeIdentity(snapshot.identity, `.gjc-update-cleanup-${randomUUID()}`),
|
|
1444
|
+
);
|
|
1445
|
+
if (removed.ok || removed.code === "not_found") return undefined;
|
|
1446
|
+
const retained = [
|
|
1447
|
+
removed.detachedPath ? `detached=${removed.detachedPath}` : undefined,
|
|
1448
|
+
removed.retainedPlaceholderPath ? `placeholder=${removed.retainedPlaceholderPath}` : undefined,
|
|
1449
|
+
removed.retainedUnknownPath ? `unknown=${removed.retainedUnknownPath}` : undefined,
|
|
1450
|
+
removed.retainedSuccessorPath ? `successor=${removed.retainedSuccessorPath}` : undefined,
|
|
1451
|
+
].filter((entry): entry is string => entry !== undefined);
|
|
1452
|
+
return `exact_unlink_${removed.code ?? "failed"}:${filePath}${retained.length ? `|${retained.join("|")}` : ""}`;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return `snapshot_failed:${filePath}:${error instanceof Error ? error.message : String(error)}`;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Reclaim only generated update artifacts while holding the install lock.
|
|
1460
|
+
* Keep a stage only while an active activation record owns it, and retain at
|
|
1461
|
+
* most the rollback backup the record can still need.
|
|
1462
|
+
*/
|
|
1463
|
+
async function cleanupBinaryUpdateArtifacts(
|
|
1464
|
+
targetPath: string,
|
|
1465
|
+
originalParent: DirectoryIdentity,
|
|
1466
|
+
retainedRetryCandidatePath?: string,
|
|
1467
|
+
): Promise<string | undefined> {
|
|
1468
|
+
const parentPath = path.dirname(path.resolve(targetPath));
|
|
1469
|
+
const warnings: string[] = [];
|
|
1470
|
+
try {
|
|
1471
|
+
const currentParent = await snapshotDirectory(parentPath);
|
|
1472
|
+
if (!sameDirectoryIdentity(originalParent, currentParent)) return "installation_parent_changed";
|
|
1473
|
+
const entries = await fs.promises.readdir(parentPath, { withFileTypes: true });
|
|
1474
|
+
const activation = await readActivationRecord(targetPath);
|
|
1475
|
+
if (activation.status === "malformed" || activation.status === "foreign") {
|
|
1476
|
+
warnings.push(`activation_record_untrusted:${activation.reason}`);
|
|
1477
|
+
} else {
|
|
1478
|
+
const record = activation.status === "valid" ? activation.record : undefined;
|
|
1479
|
+
const terminal = record?.phase === "verified" || record?.phase === "rolled_back";
|
|
1480
|
+
const pinnedStages = new Set<string>();
|
|
1481
|
+
if (record && !terminal) pinnedStages.add(path.resolve(record.stagingPath));
|
|
1482
|
+
if (retainedRetryCandidatePath) pinnedStages.add(path.resolve(retainedRetryCandidatePath));
|
|
1483
|
+
for (const entry of entries) {
|
|
1484
|
+
if (!isUpdateStagingName(targetPath, entry.name)) continue;
|
|
1485
|
+
const stagingPath = path.join(parentPath, entry.name);
|
|
1486
|
+
if (pinnedStages.has(stagingPath)) continue;
|
|
1487
|
+
const warning = await removeExactUpdateArtifact(stagingPath, originalParent);
|
|
1488
|
+
if (warning) warnings.push(warning);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const recognizedBackup = record && isUpdateBackupName(targetPath, record.backupName);
|
|
1492
|
+
let preserveBackup: string | undefined;
|
|
1493
|
+
let mayPruneBackups = activation.status === "missing";
|
|
1494
|
+
if (record && recognizedBackup) {
|
|
1495
|
+
if (record.phase === "rolled_back" || !record.baseline.exists) {
|
|
1496
|
+
mayPruneBackups = true;
|
|
1497
|
+
} else if (record.phase === "verified" || record.phase === "published" || record.phase === "uncertain") {
|
|
1498
|
+
if (record.backupIdentity) {
|
|
1499
|
+
const baseline = await snapshotRegularFile(path.join(parentPath, record.backupName));
|
|
1500
|
+
if (baseline && sameFileIdentity(record.backupIdentity, baseline.identity)) {
|
|
1501
|
+
preserveBackup = record.backupName;
|
|
1502
|
+
mayPruneBackups = true;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
} else if (record.phase === "staged" || record.phase === "pending_activation") {
|
|
1506
|
+
const current = await snapshotRegularFile(targetPath);
|
|
1507
|
+
const baselineUnchanged = record.baseline.exists
|
|
1508
|
+
? !!current && !!record.targetIdentity && sameFileIdentity(record.targetIdentity, current.identity)
|
|
1509
|
+
: current === undefined;
|
|
1510
|
+
if (baselineUnchanged) {
|
|
1511
|
+
const transactionBackup = await snapshotRegularFile(path.join(parentPath, record.backupName));
|
|
1512
|
+
if (!transactionBackup) mayPruneBackups = true;
|
|
1513
|
+
else if (
|
|
1514
|
+
record.backupIdentity &&
|
|
1515
|
+
sameFileIdentity(record.backupIdentity, transactionBackup.identity)
|
|
1516
|
+
) {
|
|
1517
|
+
preserveBackup = record.backupName;
|
|
1518
|
+
mayPruneBackups = true;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (mayPruneBackups) {
|
|
1524
|
+
for (const entry of entries) {
|
|
1525
|
+
if (!isUpdateBackupName(targetPath, entry.name) || entry.name === preserveBackup) continue;
|
|
1526
|
+
const warning = await removeExactUpdateArtifact(path.join(parentPath, entry.name), originalParent);
|
|
1527
|
+
if (warning) warnings.push(warning);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (process.platform !== "win32") {
|
|
1533
|
+
const staleBefore = BigInt(Date.now() - EXACT_REPLACE_CLEANUP_GRACE_MS) * 1_000_000n;
|
|
1534
|
+
for (const entry of entries) {
|
|
1535
|
+
const match = EXACT_REPLACE_DESTINATION_NAME.exec(entry.name);
|
|
1536
|
+
if (!match) continue;
|
|
1537
|
+
const placeholderPath = path.join(parentPath, entry.name);
|
|
1538
|
+
const placeholder = await snapshotRegularFile(placeholderPath);
|
|
1539
|
+
if (!placeholder) continue;
|
|
1540
|
+
const { identity } = placeholder;
|
|
1541
|
+
if (
|
|
1542
|
+
identity.parentDev !== originalParent.dev ||
|
|
1543
|
+
identity.parentIno !== originalParent.ino ||
|
|
1544
|
+
identity.size !== "0" ||
|
|
1545
|
+
identity.nlink !== "1" ||
|
|
1546
|
+
BigInt(identity.ino).toString(16) !== match[1]
|
|
1547
|
+
)
|
|
1548
|
+
continue;
|
|
1549
|
+
// The current flow has returned from its synchronous native calls; older
|
|
1550
|
+
// process IDs get the grace window above before their residue is reaped.
|
|
1551
|
+
if (match[2] !== process.pid.toString(16) && BigInt(identity.mtimeNs) >= staleBefore) continue;
|
|
1552
|
+
const warning = await removeExactUpdateArtifact(placeholderPath, originalParent);
|
|
1553
|
+
if (warning) warnings.push(warning);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
} catch (error) {
|
|
1557
|
+
warnings.push(error instanceof Error ? error.message : String(error));
|
|
1558
|
+
}
|
|
1559
|
+
return warnings.length > 0 ? warnings.join("\n") : undefined;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1354
1562
|
/**
|
|
1355
1563
|
* Orchestrate download → fsync → replace → verify with a strict ordering
|
|
1356
1564
|
* contract: the downloaded temp binary MUST be flushed to stable storage
|
|
1357
1565
|
* before it is published (renamed into place) or exec'd for verification.
|
|
1358
1566
|
*
|
|
1359
1567
|
* If fsync fails the temp bytes are not durable, so we abort before
|
|
1360
|
-
* replacement/verification
|
|
1361
|
-
*
|
|
1568
|
+
* replacement/verification. Final cleanup removes the exact staging identity
|
|
1569
|
+
* only when no activation record still owns it.
|
|
1362
1570
|
*/
|
|
1363
1571
|
export async function runBinaryUpdateFlow(
|
|
1364
1572
|
targetPath: string,
|
|
@@ -1370,15 +1578,19 @@ export async function runBinaryUpdateFlow(
|
|
|
1370
1578
|
const tempPath = binaryStagingPath(targetPath, stamp);
|
|
1371
1579
|
const backupPath = `${targetPath}.bak.${stamp}`;
|
|
1372
1580
|
const releaseLock = await acquireBinaryUpdateLock(targetPath);
|
|
1581
|
+
let originalParent: DirectoryIdentity | undefined;
|
|
1582
|
+
let verification: InstalledVersionVerification | undefined;
|
|
1583
|
+
let failed = false;
|
|
1584
|
+
let failure: unknown;
|
|
1373
1585
|
try {
|
|
1374
1586
|
const originalTarget = await snapshotRegularFile(targetPath);
|
|
1375
|
-
|
|
1587
|
+
originalParent = await snapshotDirectory(path.dirname(targetPath));
|
|
1376
1588
|
if (!originalParent) throw new Error("installation_parent_unavailable");
|
|
1377
1589
|
await flow.download(url, tempPath);
|
|
1378
1590
|
await flow.fsync(tempPath);
|
|
1379
1591
|
|
|
1380
1592
|
flow.beforeReplace?.();
|
|
1381
|
-
|
|
1593
|
+
verification = await flow.replace({
|
|
1382
1594
|
targetPath,
|
|
1383
1595
|
tempPath,
|
|
1384
1596
|
backupPath,
|
|
@@ -1387,9 +1599,38 @@ export async function runBinaryUpdateFlow(
|
|
|
1387
1599
|
originalParent,
|
|
1388
1600
|
verifyInstalledVersion: flow.verifyInstalledVersion,
|
|
1389
1601
|
});
|
|
1390
|
-
}
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
failed = true;
|
|
1604
|
+
failure = error;
|
|
1605
|
+
}
|
|
1606
|
+
const retainedRetryCandidatePath =
|
|
1607
|
+
failure instanceof ActivationRetryCandidateRepointError ? failure.candidatePath : undefined;
|
|
1608
|
+
const cleanupWarning = originalParent
|
|
1609
|
+
? await cleanupBinaryUpdateArtifacts(targetPath, originalParent, retainedRetryCandidatePath)
|
|
1610
|
+
: undefined;
|
|
1611
|
+
try {
|
|
1391
1612
|
await releaseLock();
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (!failed) {
|
|
1615
|
+
failed = true;
|
|
1616
|
+
failure = error;
|
|
1617
|
+
} else {
|
|
1618
|
+
logger.warn("Update install lock cleanup failed", {
|
|
1619
|
+
warning: error instanceof Error ? error.message : String(error),
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1392
1622
|
}
|
|
1623
|
+
if (failed) {
|
|
1624
|
+
if (cleanupWarning)
|
|
1625
|
+
logger.warn("Update artifact cleanup failed after unsuccessful update", { warning: cleanupWarning });
|
|
1626
|
+
throw failure;
|
|
1627
|
+
}
|
|
1628
|
+
if (!verification) throw new Error("binary_update_verification_missing");
|
|
1629
|
+
if (!cleanupWarning) return verification;
|
|
1630
|
+
return {
|
|
1631
|
+
...verification,
|
|
1632
|
+
cleanupWarning: [verification.cleanupWarning, cleanupWarning].filter(Boolean).join("\n"),
|
|
1633
|
+
};
|
|
1393
1634
|
}
|
|
1394
1635
|
|
|
1395
1636
|
export async function acquireBinaryUpdateLock(targetPath: string): Promise<() => Promise<void>> {
|
package/src/commands/harness.ts
CHANGED
|
@@ -47,7 +47,11 @@ import {
|
|
|
47
47
|
} from "../harness-control-plane/lifecycle-reconciliation";
|
|
48
48
|
import { type ResolvedOwner, RuntimeOwner, resolveOwner, resolveOwnerLive } from "../harness-control-plane/owner";
|
|
49
49
|
import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
RECEIPT_SPOOL_DIR_ENV,
|
|
52
|
+
resolveReceiptSpoolDir,
|
|
53
|
+
trustedReceiptSpoolDir,
|
|
54
|
+
} from "../harness-control-plane/receipt-spool";
|
|
51
55
|
import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
|
|
52
56
|
import { createSdkSessionTransport, spawnNormalHarnessSession } from "../harness-control-plane/sdk-transport";
|
|
53
57
|
import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
|
|
@@ -1004,8 +1008,9 @@ export default class Harness extends Command {
|
|
|
1004
1008
|
`${GJC_TMUX_OWNER_STATE_DIR_ENV}=${shellQuote(ownerStateDir)}`,
|
|
1005
1009
|
`${GJC_TMUX_OWNER_SERVER_KEY_ENV}=${shellQuote(socketKey)}`,
|
|
1006
1010
|
];
|
|
1007
|
-
|
|
1008
|
-
|
|
1011
|
+
const trustedSpoolDir = resolveReceiptSpoolDir();
|
|
1012
|
+
if (trustedSpoolDir) envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(trustedSpoolDir)}`);
|
|
1013
|
+
else envAssignments.unshift(`-u ${RECEIPT_SPOOL_DIR_ENV}`);
|
|
1009
1014
|
if (process.env.GJC_HARNESS_TEST_NODE_MODULES)
|
|
1010
1015
|
envAssignments.push(`GJC_HARNESS_TEST_NODE_MODULES=${shellQuote(process.env.GJC_HARNESS_TEST_NODE_MODULES)}`);
|
|
1011
1016
|
if (process.env.GJC_SDK_DISABLE)
|
|
@@ -1203,19 +1208,20 @@ export default class Harness extends Command {
|
|
|
1203
1208
|
}
|
|
1204
1209
|
const fallbackReason = tmux.reason;
|
|
1205
1210
|
const cmd = this.#buildOwnerCommand(sessionId);
|
|
1211
|
+
const trustedSpoolDir = resolveReceiptSpoolDir();
|
|
1212
|
+
const childEnv: NodeJS.ProcessEnv = {
|
|
1213
|
+
...process.env,
|
|
1214
|
+
GJC_HARNESS_STATE_ROOT: root,
|
|
1215
|
+
...(process.env.GJC_HARNESS_TEST_NODE_MODULES
|
|
1216
|
+
? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
|
|
1217
|
+
: {}),
|
|
1218
|
+
...(process.env.GJC_SDK_DISABLE ? { GJC_SDK_DISABLE: process.env.GJC_SDK_DISABLE } : {}),
|
|
1219
|
+
};
|
|
1220
|
+
if (trustedSpoolDir) childEnv[RECEIPT_SPOOL_DIR_ENV] = trustedSpoolDir;
|
|
1221
|
+
else delete childEnv[RECEIPT_SPOOL_DIR_ENV];
|
|
1206
1222
|
const child = Bun.spawn(cmd, {
|
|
1207
1223
|
cwd,
|
|
1208
|
-
env:
|
|
1209
|
-
...process.env,
|
|
1210
|
-
GJC_HARNESS_STATE_ROOT: root,
|
|
1211
|
-
...(process.env[RECEIPT_SPOOL_DIR_ENV]
|
|
1212
|
-
? { [RECEIPT_SPOOL_DIR_ENV]: process.env[RECEIPT_SPOOL_DIR_ENV] }
|
|
1213
|
-
: {}),
|
|
1214
|
-
...(process.env.GJC_HARNESS_TEST_NODE_MODULES
|
|
1215
|
-
? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
|
|
1216
|
-
: {}),
|
|
1217
|
-
...(process.env.GJC_SDK_DISABLE ? { GJC_SDK_DISABLE: process.env.GJC_SDK_DISABLE } : {}),
|
|
1218
|
-
},
|
|
1224
|
+
env: childEnv,
|
|
1219
1225
|
stdout: "ignore",
|
|
1220
1226
|
stderr: "ignore",
|
|
1221
1227
|
stdin: "ignore",
|
|
@@ -1374,7 +1380,11 @@ export default class Harness extends Command {
|
|
|
1374
1380
|
if (!owner.live || !owner.socketPath) return false;
|
|
1375
1381
|
const priorSpoolDir = input[RECEIPT_SPOOL_DIR_ENV];
|
|
1376
1382
|
try {
|
|
1377
|
-
|
|
1383
|
+
const fromEnv = resolveReceiptSpoolDir();
|
|
1384
|
+
const fromInput = trustedReceiptSpoolDir(typeof priorSpoolDir === "string" ? priorSpoolDir : undefined);
|
|
1385
|
+
const trustedSpoolDir = fromEnv ?? fromInput;
|
|
1386
|
+
if (trustedSpoolDir) input[RECEIPT_SPOOL_DIR_ENV] = trustedSpoolDir;
|
|
1387
|
+
else delete input[RECEIPT_SPOOL_DIR_ENV];
|
|
1378
1388
|
const res = (await callEndpoint(owner.socketPath, { verb, input })) as { ok?: boolean };
|
|
1379
1389
|
writeJson(res);
|
|
1380
1390
|
if (res?.ok === false) process.exitCode = 1;
|
package/src/commands/migrate.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Import MCP servers, skills, and instructions from other coding agents into GJC.
|
|
2
|
+
* Import MCP servers, skills, slash commands, and instructions from other coding agents into GJC.
|
|
3
3
|
*/
|
|
4
4
|
import { Command, Flags } from "@gajae-code/utils/cli";
|
|
5
5
|
import { type MigrateCommandArgs, runMigrateCommand } from "../cli/migrate-cli";
|
|
6
6
|
|
|
7
7
|
export default class Migrate extends Command {
|
|
8
|
-
static description = "Import MCP servers
|
|
8
|
+
static description = "Import MCP servers, skills, slash commands, and instructions from other coding agents";
|
|
9
9
|
|
|
10
10
|
static examples = [
|
|
11
11
|
"gjc migrate --from claude-code",
|
|
@@ -25,7 +25,7 @@ export default class Migrate extends Command {
|
|
|
25
25
|
default: false,
|
|
26
26
|
}),
|
|
27
27
|
force: Flags.boolean({
|
|
28
|
-
description: "Overwrite existing skills, MCP servers, or instruction files instead of skipping them",
|
|
28
|
+
description: "Overwrite existing skills, commands, MCP servers, or instruction files instead of skipping them",
|
|
29
29
|
default: false,
|
|
30
30
|
}),
|
|
31
31
|
"dry-run": Flags.boolean({ description: "Preview the migration without writing anything", default: false }),
|
package/src/commands/sdk.ts
CHANGED
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
} from "@gajae-code/natives";
|
|
13
13
|
import { getAgentDir, logger, postmortem } from "@gajae-code/utils";
|
|
14
14
|
import { CliParseError, Command } from "@gajae-code/utils/cli";
|
|
15
|
-
import {
|
|
15
|
+
import { startupProjectEnvSnapshot } from "@gajae-code/utils/env";
|
|
16
|
+
import { type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
|
|
16
17
|
import type { Args as ParsedArgs } from "../cli/args";
|
|
17
18
|
import { isSafeSdkInternalAgentDir, scanPublicCommand } from "../cli/public-command-entry";
|
|
18
19
|
import { PublicCommandFailure } from "../cli/public-command-errors";
|
|
19
|
-
import { FileLockAcquireError } from "../config/file-lock";
|
|
20
|
+
import { FileLockAcquireError, isFileLockAcquireTimeout } from "../config/file-lock";
|
|
20
21
|
import { parseModelString } from "../config/model-resolver";
|
|
21
22
|
import { Settings } from "../config/settings";
|
|
22
23
|
import { trustedCoordinatorEnv } from "../gjc-runtime/sidecar-bootstrap-env";
|
|
@@ -561,6 +562,65 @@ export function startMemoryBackendAfterReadiness(start: () => Promise<void>): vo
|
|
|
561
562
|
});
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
const SDK_CWD_EQUALITY_TEST_SEAMS = [
|
|
566
|
+
"GJC_SDK_TEST_HANG_MODEL_PROFILE",
|
|
567
|
+
"GJC_SDK_TEST_FAIL_AFTER_REGISTRATION",
|
|
568
|
+
"GJC_SDK_TEST_EXIT_AFTER_READY",
|
|
569
|
+
"GJC_SDK_TEST_REJECT_AFTER_READY",
|
|
570
|
+
] as const;
|
|
571
|
+
|
|
572
|
+
type SdkCwdEqualityTestSeam = (typeof SDK_CWD_EQUALITY_TEST_SEAMS)[number];
|
|
573
|
+
|
|
574
|
+
function isSdkCwdEqualityTestSeam(name: string): name is SdkCwdEqualityTestSeam {
|
|
575
|
+
return (SDK_CWD_EQUALITY_TEST_SEAMS as readonly string[]).includes(name);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function sdkCwdEqualitySnapshots(cwd: string): ProjectEnvSnapshot[] {
|
|
579
|
+
// The startup snapshot keeps a declaration Bun loaded before a later chdir.
|
|
580
|
+
return [projectEnvSnapshot(cwd), startupProjectEnvSnapshot()];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Cwd-equality test seams accept only an operator value. A project dotenv line,
|
|
585
|
+
* including one Bun expands from `$PWD`, never arms the seam.
|
|
586
|
+
*/
|
|
587
|
+
export function sdkCwdEqualityTestSeamActive(
|
|
588
|
+
name: string,
|
|
589
|
+
cwd: string,
|
|
590
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
591
|
+
snapshots: readonly ProjectEnvSnapshot[] = sdkCwdEqualitySnapshots(cwd),
|
|
592
|
+
): boolean {
|
|
593
|
+
if (!isSdkCwdEqualityTestSeam(name) || env[name] !== cwd) return false;
|
|
594
|
+
// Provenance only. trustedCoordinatorEnv trims, so a second compare to cwd
|
|
595
|
+
// would drop a trusted path that still matches exactly, including trailing space.
|
|
596
|
+
return snapshots.every(snapshot => trustedCoordinatorEnv(name, env, snapshot) !== undefined);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Copy `env` without project-dotenv values for the four cwd-equality seams. */
|
|
600
|
+
export function omitUntrustedSdkCwdEqualitySeams(
|
|
601
|
+
env: NodeJS.ProcessEnv,
|
|
602
|
+
snapshots: readonly ProjectEnvSnapshot[],
|
|
603
|
+
): NodeJS.ProcessEnv {
|
|
604
|
+
const denied: string[] = [];
|
|
605
|
+
for (const name of SDK_CWD_EQUALITY_TEST_SEAMS) {
|
|
606
|
+
const raw = env[name];
|
|
607
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
608
|
+
if (snapshots.some(snapshot => trustedCoordinatorEnv(name, env, snapshot) === undefined)) denied.push(name);
|
|
609
|
+
}
|
|
610
|
+
if (denied.length === 0) return env;
|
|
611
|
+
const next: NodeJS.ProcessEnv = { ...env };
|
|
612
|
+
for (const name of denied) delete next[name];
|
|
613
|
+
return next;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function dropUntrustedSdkCwdEqualitySeams(env: NodeJS.ProcessEnv, snapshots: readonly ProjectEnvSnapshot[]): void {
|
|
617
|
+
const next = omitUntrustedSdkCwdEqualitySeams(env, snapshots);
|
|
618
|
+
if (next === env) return;
|
|
619
|
+
for (const name of SDK_CWD_EQUALITY_TEST_SEAMS) {
|
|
620
|
+
if (next[name] === undefined) delete env[name];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
564
624
|
/** Runs the same persisted AgentSession bootstrap used by the production CLI. */
|
|
565
625
|
export async function runSessionHost(
|
|
566
626
|
timing: {
|
|
@@ -592,6 +652,11 @@ export async function runSessionHost(
|
|
|
592
652
|
const agentDir = process.env.GJC_AGENT_DIR;
|
|
593
653
|
if (!agentDir) throw new Error("GJC_AGENT_DIR is required for sdk session-host-internal.");
|
|
594
654
|
const cwd = timing.cwd ?? process.cwd();
|
|
655
|
+
const cwdSeamSnapshots = sdkCwdEqualitySnapshots(cwd);
|
|
656
|
+
// Drop rejected copies before startup work can spawn a child that inherits them.
|
|
657
|
+
dropUntrustedSdkCwdEqualitySeams(process.env, cwdSeamSnapshots);
|
|
658
|
+
const cwdEqualitySeam = (name: SdkCwdEqualityTestSeam): boolean =>
|
|
659
|
+
sdkCwdEqualityTestSeamActive(name, cwd, process.env, cwdSeamSnapshots);
|
|
595
660
|
if ((await fs.realpath(request.cwd)) !== (await fs.realpath(cwd)))
|
|
596
661
|
throw new Error(`Lifecycle worktree mismatch: expected ${request.cwd}, got ${cwd}.`);
|
|
597
662
|
if (
|
|
@@ -1423,7 +1488,7 @@ export async function runSessionHost(
|
|
|
1423
1488
|
try {
|
|
1424
1489
|
const startupThinkingLevel = request.modelId ? parseModelString(request.modelId)?.thinkingLevel : undefined;
|
|
1425
1490
|
await beforeCutoff(() =>
|
|
1426
|
-
|
|
1491
|
+
cwdEqualitySeam("GJC_SDK_TEST_HANG_MODEL_PROFILE")
|
|
1427
1492
|
? new Promise<void>(() => {})
|
|
1428
1493
|
: applyModelProfiles({
|
|
1429
1494
|
session,
|
|
@@ -1450,7 +1515,7 @@ export async function runSessionHost(
|
|
|
1450
1515
|
const startup = await beforeCutoff<SdkStartupResult>(() => capability.promise);
|
|
1451
1516
|
if (startup.status !== "started") throw startup.failure;
|
|
1452
1517
|
throwIfStartupInterrupted();
|
|
1453
|
-
if (
|
|
1518
|
+
if (cwdEqualitySeam("GJC_SDK_TEST_FAIL_AFTER_REGISTRATION"))
|
|
1454
1519
|
throw new Error("Lifecycle test failure after SDK host registration.");
|
|
1455
1520
|
|
|
1456
1521
|
await beforeCutoff(() => session.sessionManager.ensureOnDisk());
|
|
@@ -1519,11 +1584,11 @@ export async function runSessionHost(
|
|
|
1519
1584
|
// Readiness is published; the session is live. Memory startup issues one LLM
|
|
1520
1585
|
// request per queued rollout, so it must run outside the readiness window and
|
|
1521
1586
|
// its failure must never take the session down.
|
|
1522
|
-
if (
|
|
1587
|
+
if (cwdEqualitySeam("GJC_SDK_TEST_EXIT_AFTER_READY")) {
|
|
1523
1588
|
process.stderr.write("GJC_SDK_TEST_EXIT_AFTER_READY\n");
|
|
1524
1589
|
process.exit(7);
|
|
1525
1590
|
}
|
|
1526
|
-
if (
|
|
1591
|
+
if (cwdEqualitySeam("GJC_SDK_TEST_REJECT_AFTER_READY")) {
|
|
1527
1592
|
// Simulates the post-readiness liveness watcher rejecting. With the
|
|
1528
1593
|
// post-ready startup receipt removed, this unhandled rejection kills the
|
|
1529
1594
|
// host without writing any startup receipt; the broker must classify the
|
|
@@ -1768,6 +1833,7 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
|
|
|
1768
1833
|
const command = resolveSdkInternalSpawnCommand("broker-internal");
|
|
1769
1834
|
const brokerEnvironment = { ...command.env };
|
|
1770
1835
|
delete brokerEnvironment.GJC_BROKER_TRAMPOLINE_RETAIN_UNTIL_PUBLICATION;
|
|
1836
|
+
dropUntrustedSdkCwdEqualitySeams(brokerEnvironment, sdkCwdEqualitySnapshots(process.cwd()));
|
|
1771
1837
|
let child: ChildProcess;
|
|
1772
1838
|
try {
|
|
1773
1839
|
child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
|
|
@@ -1990,9 +2056,8 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
|
|
|
1990
2056
|
return await singleStartupAttempt(deadline);
|
|
1991
2057
|
} catch (error) {
|
|
1992
2058
|
if (
|
|
1993
|
-
!(error
|
|
2059
|
+
!isFileLockAcquireTimeout(error) ||
|
|
1994
2060
|
!startupFenceAcquired ||
|
|
1995
|
-
error.code !== "acquire_timeout" ||
|
|
1996
2061
|
path.resolve(error.filePath) !==
|
|
1997
2062
|
path.resolve(path.join(agentDir, "sdk", "sessions", "index.jsonl")) ||
|
|
1998
2063
|
sessionIndexTimeoutRetries >= MAX_SESSION_INDEX_TIMEOUT_RETRIES
|
|
@@ -2172,14 +2237,19 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
|
|
|
2172
2237
|
}
|
|
2173
2238
|
}
|
|
2174
2239
|
: undefined;
|
|
2240
|
+
const brokerSettings = await Settings.loadForScope({ cwd: process.cwd(), agentDir });
|
|
2175
2241
|
const candidate = new Broker({
|
|
2176
2242
|
agentDir,
|
|
2177
2243
|
startupAbortSignal: startupAbortController.signal,
|
|
2178
2244
|
startupCheckpointDeadline: effectiveCheckpointDeadline,
|
|
2179
2245
|
onStartupReady: () => clearTimeout(startupWatchdog),
|
|
2180
|
-
masterOrphanGraceMs:
|
|
2181
|
-
|
|
2182
|
-
|
|
2246
|
+
masterOrphanGraceMs: brokerSettings.get("sdk.masterOrphanGraceMs"),
|
|
2247
|
+
abandonedGraceMs: brokerSettings.get("sdk.brokerAbandonedGraceMs"),
|
|
2248
|
+
// Only the detached autostart entry retires itself when nobody is left to
|
|
2249
|
+
// use it. A supervised `gjc sdk broker run` service is owned by its
|
|
2250
|
+
// operator's process manager, which reads an owned-root completion as a
|
|
2251
|
+
// clean exit and would not restart it.
|
|
2252
|
+
reapWhenAbandoned: !publicBrokerRun,
|
|
2183
2253
|
resolveDirectoryMigration: async cwd => {
|
|
2184
2254
|
const settings = await Settings.loadForScope({ cwd, agentDir });
|
|
2185
2255
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { INTENT_FIELD, type ThinkingLevel } from "@gajae-code/agent-core";
|
|
2
2
|
import type { Api, Model } from "@gajae-code/ai/core";
|
|
3
3
|
import { Markdown } from "@gajae-code/tui";
|
|
4
|
-
import { prompt } from "@gajae-code/utils";
|
|
4
|
+
import { prompt, sanitizeText } from "@gajae-code/utils";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
|
|
7
7
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
@@ -81,7 +81,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
81
81
|
const toolArgsById = new Map<string, { name: string; args?: Record<string, unknown> }>();
|
|
82
82
|
const writeThinkingLine = (text: string) => {
|
|
83
83
|
if (!process.stdout.isTTY) return;
|
|
84
|
-
const line = chalk.dim(`… ${text}`);
|
|
84
|
+
const line = chalk.dim(`… ${sanitizeText(text)}`);
|
|
85
85
|
process.stdout.write(`\r\x1b[2K${line}`);
|
|
86
86
|
thinkingLineActive = true;
|
|
87
87
|
};
|
|
@@ -118,7 +118,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
118
118
|
clearThinkingLine();
|
|
119
119
|
const assistantMessage = event.message as { stopReason?: string; errorMessage?: string };
|
|
120
120
|
if (assistantMessage.stopReason === "error" && assistantMessage.errorMessage) {
|
|
121
|
-
process.stdout.write(`● Error: ${assistantMessage.errorMessage}\n`);
|
|
121
|
+
process.stdout.write(`● Error: ${sanitizeText(assistantMessage.errorMessage)}\n`);
|
|
122
122
|
}
|
|
123
123
|
const messageText = extractMessageText(event.message?.content ?? []);
|
|
124
124
|
if (messageText) {
|
|
@@ -131,10 +131,10 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
131
131
|
const stored = toolArgsById.get(event.toolCallId) ?? { name: event.toolName };
|
|
132
132
|
toolArgsById.delete(event.toolCallId);
|
|
133
133
|
clearThinkingLine();
|
|
134
|
-
const toolLabel = formatToolLabel(stored.name);
|
|
134
|
+
const toolLabel = sanitizeText(formatToolLabel(stored.name));
|
|
135
135
|
const symbol = event.isError ? "" : "";
|
|
136
136
|
process.stdout.write(`${symbol} ${toolLabel}\n`);
|
|
137
|
-
const argsLines = formatToolArgs(stored.args);
|
|
137
|
+
const argsLines = formatToolArgs(stored.args).map(line => sanitizeText(line));
|
|
138
138
|
if (argsLines.length > 0) {
|
|
139
139
|
process.stdout.write(`${formatToolArgsBlock(argsLines)}\n`);
|
|
140
140
|
}
|
|
@@ -201,7 +201,7 @@ function extractMessageText(content: Array<{ type: string; text?: string }>): st
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function writeAssistantMessage(message: string): void {
|
|
204
|
-
const lines = renderMarkdownLines(message);
|
|
204
|
+
const lines = renderMarkdownLines(sanitizeText(message));
|
|
205
205
|
if (lines.length === 0) return;
|
|
206
206
|
let firstContentIndex = lines.findIndex(line => line.trim().length > 0);
|
|
207
207
|
if (firstContentIndex === -1) {
|
|
@@ -5,6 +5,7 @@ import { prompt } from "@gajae-code/utils";
|
|
|
5
5
|
import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
|
|
6
6
|
import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
|
|
7
7
|
import type { ConventionalAnalysis } from "../../commit/types";
|
|
8
|
+
import { escapePromptMetadata } from "../../session/messages";
|
|
8
9
|
import { toReasoningEffort } from "../../thinking";
|
|
9
10
|
import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
|
|
10
11
|
|
|
@@ -41,13 +42,21 @@ export async function generateConventionalAnalysis({
|
|
|
41
42
|
diff,
|
|
42
43
|
}: ConventionalAnalysisInput): Promise<ConventionalAnalysis> {
|
|
43
44
|
const userContent = prompt.render(analysisUserPrompt, {
|
|
44
|
-
context_files:
|
|
45
|
+
context_files:
|
|
46
|
+
contextFiles && contextFiles.length > 0
|
|
47
|
+
? contextFiles.map(file => ({
|
|
48
|
+
path: escapePromptMetadata(file.path),
|
|
49
|
+
content: escapePromptMetadata(file.content, { preserveNewlines: true }),
|
|
50
|
+
}))
|
|
51
|
+
: undefined,
|
|
45
52
|
user_context: userContext,
|
|
46
53
|
types_description: typesDescription,
|
|
47
|
-
recent_commits: recentCommits
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
recent_commits: recentCommits
|
|
55
|
+
?.map(subject => escapePromptMetadata(subject, { preserveNewlines: true }))
|
|
56
|
+
.join("\n"),
|
|
57
|
+
scope_candidates: escapePromptMetadata(scopeCandidates, { preserveNewlines: true }),
|
|
58
|
+
stat: escapePromptMetadata(stat, { preserveNewlines: true }),
|
|
59
|
+
diff: escapePromptMetadata(diff, { preserveNewlines: true }),
|
|
51
60
|
});
|
|
52
61
|
|
|
53
62
|
const response = await completeSimple(
|