@gajae-code/coding-agent 0.5.0 → 0.5.2
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 +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/src/cli/args.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface Args {
|
|
|
30
30
|
mode?: Mode;
|
|
31
31
|
noSession?: boolean;
|
|
32
32
|
sessionDir?: string;
|
|
33
|
+
rpcListen?: string;
|
|
33
34
|
providerSessionId?: string;
|
|
34
35
|
fork?: string;
|
|
35
36
|
models?: string[];
|
|
@@ -145,6 +146,8 @@ export function parseArgs(args: string[]): Args {
|
|
|
145
146
|
result.noSession = true;
|
|
146
147
|
} else if (arg === "--session-dir" && i + 1 < args.length) {
|
|
147
148
|
result.sessionDir = args[++i];
|
|
149
|
+
} else if (arg === "--listen" && i + 1 < args.length) {
|
|
150
|
+
result.rpcListen = args[++i];
|
|
148
151
|
} else if (arg === "--models" && i + 1 < args.length) {
|
|
149
152
|
result.models = args[++i].split(",").map(s => s.trim());
|
|
150
153
|
} else if (arg === "--no-tools") {
|
|
@@ -266,6 +269,8 @@ export function getExtraHelpText(): string {
|
|
|
266
269
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
267
270
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
268
271
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
272
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
273
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
269
274
|
|
|
270
275
|
For complete environment variable reference, see:
|
|
271
276
|
${chalk.dim("docs/environment-variables.md")}
|
package/src/cli/fast-help.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function getExtraHelpText(): string {
|
|
|
54
54
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
55
55
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
56
56
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
57
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
58
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
57
59
|
|
|
58
60
|
For complete environment variable reference, see:
|
|
59
61
|
docs/environment-variables.md
|
package/src/cli/list-models.ts
CHANGED
|
@@ -5,7 +5,12 @@ import { type Api, getSupportedEfforts, type Model } from "@gajae-code/ai";
|
|
|
5
5
|
import { fuzzyFilter } from "@gajae-code/tui";
|
|
6
6
|
import { formatNumber } from "@gajae-code/utils";
|
|
7
7
|
import type { ModelRegistry } from "../config/model-registry";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
discoverAndLoadExtensions,
|
|
10
|
+
type ExtensionFactory,
|
|
11
|
+
loadExtensionFromFactory,
|
|
12
|
+
loadExtensions,
|
|
13
|
+
} from "../extensibility/extensions";
|
|
9
14
|
import { EventBus } from "../utils/event-bus";
|
|
10
15
|
|
|
11
16
|
interface ProviderRow {
|
|
@@ -137,6 +142,8 @@ export interface RunListModelsOptions {
|
|
|
137
142
|
cwd: string;
|
|
138
143
|
/** CLI-supplied extension paths (e.g. from `-e <path>`). */
|
|
139
144
|
additionalExtensionPaths?: string[];
|
|
145
|
+
/** In-process extension factories to load without filesystem discovery. */
|
|
146
|
+
extensionFactories?: Array<{ factory: ExtensionFactory; name: string }>;
|
|
140
147
|
/** Extension paths configured under `extensions:` in user settings. */
|
|
141
148
|
settingsExtensions?: string[];
|
|
142
149
|
/** Disabled extension ids from settings (`disabledExtensions`). */
|
|
@@ -159,6 +166,7 @@ export async function runListModelsCommand(options: RunListModelsOptions): Promi
|
|
|
159
166
|
modelRegistry,
|
|
160
167
|
cwd,
|
|
161
168
|
additionalExtensionPaths = [],
|
|
169
|
+
extensionFactories = [],
|
|
162
170
|
settingsExtensions = [],
|
|
163
171
|
disabledExtensionIds = [],
|
|
164
172
|
disableExtensionDiscovery = false,
|
|
@@ -174,6 +182,10 @@ export async function runListModelsCommand(options: RunListModelsOptions): Promi
|
|
|
174
182
|
eventBus,
|
|
175
183
|
disabledExtensionIds,
|
|
176
184
|
);
|
|
185
|
+
for (const { factory, name } of extensionFactories) {
|
|
186
|
+
const extension = await loadExtensionFromFactory(factory, cwd, eventBus, extensionsResult.runtime, name);
|
|
187
|
+
extensionsResult.extensions.push(extension);
|
|
188
|
+
}
|
|
177
189
|
|
|
178
190
|
for (const { path: extPath, error } of extensionsResult.errors) {
|
|
179
191
|
process.stderr.write(`Failed to load extension: ${extPath}: ${error}\n`);
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles `gjc setup [component]` to install the normal defaults or optional feature dependencies.
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { SqliteAuthCredentialStore } from "@gajae-code/ai";
|
|
10
|
+
import { $which, APP_NAME, getAgentDbPath, getPythonEnvDir } from "@gajae-code/utils";
|
|
8
11
|
import { $ } from "bun";
|
|
9
12
|
import chalk from "chalk";
|
|
10
13
|
import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
readGjcManagedCodexHooksStatus,
|
|
15
18
|
} from "../hooks/codex-native-hooks-config";
|
|
16
19
|
import { theme } from "../modes/theme/theme";
|
|
20
|
+
import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../setup/credential-import";
|
|
17
21
|
import {
|
|
18
22
|
formatHermesSetupResult,
|
|
19
23
|
type HermesSetupFlags,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
parseProviderCompatibility,
|
|
28
32
|
} from "../setup/provider-onboarding";
|
|
29
33
|
|
|
30
|
-
export type SetupComponent = "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
34
|
+
export type SetupComponent = "credentials" | "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
31
35
|
|
|
32
36
|
export interface SetupCommandArgs {
|
|
33
37
|
component: SetupComponent;
|
|
@@ -57,10 +61,12 @@ export interface SetupCommandArgs {
|
|
|
57
61
|
gjcCommand?: string;
|
|
58
62
|
target?: string;
|
|
59
63
|
profileDir?: string;
|
|
64
|
+
yes?: boolean;
|
|
65
|
+
dryRun?: boolean;
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
69
|
+
const VALID_COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
64
70
|
|
|
65
71
|
function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
|
|
66
72
|
return (
|
|
@@ -113,6 +119,10 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
|
|
|
113
119
|
flags.smoke = true;
|
|
114
120
|
} else if (arg === "--install") {
|
|
115
121
|
flags.install = true;
|
|
122
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
123
|
+
flags.yes = true;
|
|
124
|
+
} else if (arg === "--dry-run") {
|
|
125
|
+
flags.dryRun = true;
|
|
116
126
|
} else if (arg === "--root") {
|
|
117
127
|
flags.root = [...(flags.root ?? []), args[++i] ?? ""];
|
|
118
128
|
} else if (arg === "--repo") {
|
|
@@ -243,6 +253,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
243
253
|
case "stt":
|
|
244
254
|
await handleSttSetup(cmd.flags);
|
|
245
255
|
break;
|
|
256
|
+
case "credentials":
|
|
257
|
+
await handleCredentialsSetup(cmd.flags);
|
|
258
|
+
break;
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
|
|
@@ -472,6 +485,122 @@ async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promi
|
|
|
472
485
|
process.exit(1);
|
|
473
486
|
}
|
|
474
487
|
}
|
|
488
|
+
async function confirmImport(count: number): Promise<boolean> {
|
|
489
|
+
if (!process.stdin.isTTY) return false;
|
|
490
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
491
|
+
try {
|
|
492
|
+
const answer = (await rl.question(`Import ${count} credential(s) into ${getAgentDbPath()}? [y/N] `))
|
|
493
|
+
.trim()
|
|
494
|
+
.toLowerCase();
|
|
495
|
+
return answer === "y" || answer === "yes";
|
|
496
|
+
} finally {
|
|
497
|
+
rl.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Discover existing Claude Code / Codex CLI credentials and import them into the
|
|
503
|
+
* gjc credential store after a redacted preview + confirmation. Falls back to
|
|
504
|
+
* manual-setup guidance when nothing importable is found.
|
|
505
|
+
*/
|
|
506
|
+
async function handleCredentialsSetup(flags: { json?: boolean; yes?: boolean; dryRun?: boolean }): Promise<void> {
|
|
507
|
+
const result = await discoverExternalCredentials();
|
|
508
|
+
const redactedPlan = {
|
|
509
|
+
importable: result.importable.map(c => ({
|
|
510
|
+
provider: c.provider,
|
|
511
|
+
kind: c.kind,
|
|
512
|
+
source: c.source,
|
|
513
|
+
identity: c.identity,
|
|
514
|
+
expiresAt: c.expiresAt,
|
|
515
|
+
redactedToken: c.redactedToken,
|
|
516
|
+
})),
|
|
517
|
+
skipped: result.skipped,
|
|
518
|
+
environment: result.environment,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (result.importable.length === 0) {
|
|
522
|
+
if (flags.json) {
|
|
523
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
527
|
+
process.stdout.write(
|
|
528
|
+
chalk.yellow(
|
|
529
|
+
`\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
|
|
530
|
+
` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
|
|
531
|
+
` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!flags.json) {
|
|
538
|
+
process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
|
|
539
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (flags.dryRun) {
|
|
543
|
+
if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
|
|
544
|
+
else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const confirmed = flags.yes || (await confirmImport(result.importable.length));
|
|
549
|
+
if (!confirmed) {
|
|
550
|
+
if (flags.json) {
|
|
551
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
|
|
559
|
+
let summary: Awaited<ReturnType<typeof importCredentials>>;
|
|
560
|
+
try {
|
|
561
|
+
summary = await importCredentials(result.importable, (provider, credential) =>
|
|
562
|
+
store.upsertAuthCredentialForProvider(provider, credential),
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
store.close();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (flags.json) {
|
|
569
|
+
process.stdout.write(
|
|
570
|
+
`${JSON.stringify({
|
|
571
|
+
...redactedPlan,
|
|
572
|
+
imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
|
|
573
|
+
failed: summary.failed.map(f => ({
|
|
574
|
+
provider: f.credential.provider,
|
|
575
|
+
source: f.credential.source,
|
|
576
|
+
error: f.error,
|
|
577
|
+
})),
|
|
578
|
+
})}\n`,
|
|
579
|
+
);
|
|
580
|
+
if (summary.failed.length > 0) process.exitCode = 1;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const credential of summary.imported) {
|
|
585
|
+
process.stdout.write(
|
|
586
|
+
`${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
for (const failure of summary.failed) {
|
|
590
|
+
process.stdout.write(
|
|
591
|
+
`${chalk.red(`${theme.status.error} failed`)} ${failure.credential.provider} (${failure.credential.source}): ${failure.error}\n`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (summary.failed.length > 0) {
|
|
595
|
+
process.exitCode = 1;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function formatCredentialSummaryLine(credential: { provider: string; kind: string; source: string }): string {
|
|
602
|
+
return `${credential.provider} · ${credential.kind} (from ${credential.source})`;
|
|
603
|
+
}
|
|
475
604
|
|
|
476
605
|
/**
|
|
477
606
|
* Print setup command help.
|
|
@@ -489,6 +618,7 @@ ${chalk.bold("Components:")}
|
|
|
489
618
|
provider Optional: add a preset, OpenAI-compatible, or Anthropic-compatible API provider
|
|
490
619
|
python Optional: verify a Python 3 interpreter is reachable for code execution
|
|
491
620
|
stt Optional: install speech-to-text dependencies (openai-whisper, recording tools)
|
|
621
|
+
credentials Optional: import existing Claude Code / Codex CLI credentials
|
|
492
622
|
|
|
493
623
|
|
|
494
624
|
${chalk.bold("Provider example:")}
|
|
@@ -524,6 +654,8 @@ ${chalk.bold("Options:")}
|
|
|
524
654
|
--mutation Hermes MCP mutation classes: sessions,questions,reports,all
|
|
525
655
|
--target Hermes config file target for config-only install
|
|
526
656
|
--profile-dir Hermes profile directory for full setup install
|
|
657
|
+
--dry-run Preview discovered credentials without importing (credentials)
|
|
658
|
+
-y, --yes Import discovered credentials without an interactive prompt (credentials)
|
|
527
659
|
|
|
528
660
|
${chalk.bold("Examples:")}
|
|
529
661
|
${APP_NAME} setup Install bundled GJC default workflow skills
|
|
@@ -536,5 +668,8 @@ ${chalk.bold("Examples:")}
|
|
|
536
668
|
${APP_NAME} setup stt Install speech-to-text dependencies
|
|
537
669
|
${APP_NAME} setup stt --check Check if STT dependencies are available
|
|
538
670
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
671
|
+
${APP_NAME} setup credentials Discover & import existing Claude/Codex credentials
|
|
672
|
+
${APP_NAME} setup credentials --dry-run Preview importable credentials (redacted)
|
|
673
|
+
${APP_NAME} setup credentials --yes Import without an interactive prompt
|
|
539
674
|
`);
|
|
540
675
|
}
|
package/src/cli.ts
CHANGED
|
@@ -32,6 +32,7 @@ const commands: CommandEntry[] = [
|
|
|
32
32
|
{ name: "coordinator", load: () => import("./commands/coordinator").then(m => m.default) },
|
|
33
33
|
{ name: "team", load: () => import("./commands/team").then(m => m.default) },
|
|
34
34
|
{ name: "ultragoal", load: () => import("./commands/ultragoal").then(m => m.default) },
|
|
35
|
+
{ name: "gc", load: () => import("./commands/gc").then(m => m.default) },
|
|
35
36
|
{ name: "ralplan", load: () => import("./commands/ralplan").then(m => m.default) },
|
|
36
37
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
37
38
|
{ name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command, Flags } from "@gajae-code/utils/cli";
|
|
2
|
+
import { runGjcGcCommand } from "../gjc-runtime/gc-runtime";
|
|
3
|
+
|
|
4
|
+
export default class Gc extends Command {
|
|
5
|
+
static description = "Garbage-collect stale GJC session/PID records (dry-run by default)";
|
|
6
|
+
static strict = false;
|
|
7
|
+
static flags = {
|
|
8
|
+
json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
|
|
9
|
+
prune: Flags.boolean({ description: "Remove stale records (default: report only)", default: false }),
|
|
10
|
+
force: Flags.boolean({ description: "Alias for --prune (eligible records only)", default: false }),
|
|
11
|
+
"dry-run": Flags.boolean({ description: "Force report-only mode", default: false }),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
static examples = ["gjc gc", "gjc gc --json", "gjc gc --prune", "gjc gc --prune --json"];
|
|
15
|
+
|
|
16
|
+
async run(): Promise<void> {
|
|
17
|
+
const result = await runGjcGcCommand(this.argv, process.cwd(), process.env);
|
|
18
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
19
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
20
|
+
process.exitCode = result.status;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/commands/harness.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
|
16
16
|
import { resolveGjcTmuxCommand, sanitizeTmuxToken } from "../gjc-runtime/tmux-common";
|
|
17
17
|
import { classifyRecovery } from "../harness-control-plane/classifier";
|
|
18
18
|
import { callEndpoint, EndpointUnreachableError } from "../harness-control-plane/control-endpoint";
|
|
19
|
-
import { type ResolvedOwner, RuntimeOwner, resolveOwner } from "../harness-control-plane/owner";
|
|
19
|
+
import { type ResolvedOwner, RuntimeOwner, resolveOwner, resolveOwnerLive } from "../harness-control-plane/owner";
|
|
20
20
|
import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
|
|
21
21
|
import { RECEIPT_SPOOL_DIR_ENV } from "../harness-control-plane/receipt-spool";
|
|
22
22
|
import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
|
|
@@ -948,10 +948,14 @@ export default class Harness extends Command {
|
|
|
948
948
|
let observation = input.observation as Partial<Observation> | undefined;
|
|
949
949
|
let stateView: SessionState | null = null;
|
|
950
950
|
const sessionId = flagSession ?? (typeof input.sessionId === "string" ? input.sessionId : undefined);
|
|
951
|
+
// Session-backed classify derives owner liveness from the same lease/socket probe observe
|
|
952
|
+
// uses for routing, so a live (e.g. manual) owner is never misread as vanished/restart-clean.
|
|
953
|
+
let ownerLive = false;
|
|
951
954
|
if (sessionId) {
|
|
952
955
|
stateView = await loadState(root, sessionId);
|
|
956
|
+
ownerLive = await resolveOwnerLive(root, sessionId);
|
|
953
957
|
if (!observation) {
|
|
954
|
-
const built = await buildObservation(root, stateView,
|
|
958
|
+
const built = await buildObservation(root, stateView, ownerLive);
|
|
955
959
|
observation = built.observation;
|
|
956
960
|
stateView = await markVanishedOwnerBlocked(
|
|
957
961
|
root,
|
|
@@ -975,7 +979,7 @@ export default class Harness extends Command {
|
|
|
975
979
|
const decision = classifyRecovery({ observation: full, retryBudget: budget });
|
|
976
980
|
if (stateView) {
|
|
977
981
|
writeJson(
|
|
978
|
-
buildResponse(stateView,
|
|
982
|
+
buildResponse(stateView, ownerLive, {
|
|
979
983
|
decision,
|
|
980
984
|
observation: { ...full, lifecycle: stateView.lifecycle },
|
|
981
985
|
}),
|
package/src/commands/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
|
5
5
|
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
|
6
6
|
import { initTheme } from "../modes/theme/theme";
|
|
7
7
|
|
|
8
|
-
const COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
8
|
+
const COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
9
9
|
|
|
10
10
|
export default class Setup extends Command {
|
|
11
11
|
static description = "Install GJC defaults or optional feature dependencies";
|
|
@@ -47,6 +47,8 @@ export default class Setup extends Command {
|
|
|
47
47
|
"api-key-env": Flags.string({ description: "Read provider API key from this environment variable" }),
|
|
48
48
|
model: Flags.string({ description: "Model id to add (repeat or comma-separate)", multiple: true }),
|
|
49
49
|
"models-path": Flags.string({ description: "Override models config path" }),
|
|
50
|
+
yes: Flags.boolean({ char: "y", description: "Import discovered credentials without an interactive prompt" }),
|
|
51
|
+
"dry-run": Flags.boolean({ description: "Preview discovered credentials without importing" }),
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
async run(): Promise<void> {
|
|
@@ -79,6 +81,8 @@ export default class Setup extends Command {
|
|
|
79
81
|
gjcCommand: flags["gjc-command"],
|
|
80
82
|
target: flags.target,
|
|
81
83
|
profileDir: flags["profile-dir"],
|
|
84
|
+
yes: flags.yes,
|
|
85
|
+
dryRun: flags["dry-run"],
|
|
82
86
|
},
|
|
83
87
|
};
|
|
84
88
|
await initTheme();
|
|
@@ -16,12 +16,14 @@ export default class Ultragoal extends Command {
|
|
|
16
16
|
static delegateHelp = true;
|
|
17
17
|
|
|
18
18
|
async run(): Promise<void> {
|
|
19
|
+
const isReviewStart = this.argv.includes("review") && this.argv.includes("review-start");
|
|
19
20
|
const shouldActivateGoalMode = isUltragoalCreateGoalsInvocation(this.argv);
|
|
20
21
|
const result = await runNativeUltragoalCommand(this.argv);
|
|
21
22
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
22
23
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
23
24
|
process.exitCode = result.status;
|
|
24
|
-
if (result.status !== 0 || !shouldActivateGoalMode) return;
|
|
25
|
+
if (result.status !== 0 || (!shouldActivateGoalMode && !isReviewStart)) return;
|
|
26
|
+
if (isReviewStart && !result.createdReviewPlan && (result.reviewBlockerGoalIds?.length ?? 0) === 0) return;
|
|
25
27
|
|
|
26
28
|
const cwd = process.cwd();
|
|
27
29
|
const { objective, goalsPath } = await readUltragoalGjcObjective(cwd);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GC adapter for config file-locks (`<file>.lock` dirs holding `{pid, timestamp}`).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Stats } from "node:fs";
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { getAgentDir, getConfigRootDir, isEnoent } from "@gajae-code/utils";
|
|
9
|
+
import type {
|
|
10
|
+
GcCollectResult,
|
|
11
|
+
GcContext,
|
|
12
|
+
GcError,
|
|
13
|
+
GcPruneOutcome,
|
|
14
|
+
GcRecord,
|
|
15
|
+
GcStoreAdapter,
|
|
16
|
+
} from "../gjc-runtime/gc-runtime";
|
|
17
|
+
import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
|
|
18
|
+
import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
|
|
19
|
+
import { readFileLockInfoForGc, removeFileLockDirForGc } from "./file-lock";
|
|
20
|
+
|
|
21
|
+
const MAX_WALK_DEPTH = 6;
|
|
22
|
+
const MAX_WALK_ENTRIES = 20_000;
|
|
23
|
+
|
|
24
|
+
// High-cardinality, lock-free subtrees we never descend into. `.lock` dirs are
|
|
25
|
+
// created next to config files, never inside these.
|
|
26
|
+
const PRUNED_DIR_NAMES = new Set(["sessions", "node_modules", ".git", "blobs", "artifacts", "receipts", "events"]);
|
|
27
|
+
|
|
28
|
+
interface WalkState {
|
|
29
|
+
entries: number;
|
|
30
|
+
truncated: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Global, env-aware GJC lock roots. Per the approved scope this covers the
|
|
34
|
+
// user config root, the agent dir (honors GJC_CODING_AGENT_DIR), and the
|
|
35
|
+
// configured receipt-spool dir — NOT the invocation cwd's project `.gjc`.
|
|
36
|
+
function knownFileLockRoots(ctx: GcContext): string[] {
|
|
37
|
+
const roots = [getConfigRootDir(), getAgentDir()];
|
|
38
|
+
const spoolDir = resolveReceiptSpoolDir(ctx.env);
|
|
39
|
+
if (spoolDir) roots.push(spoolDir);
|
|
40
|
+
return Array.from(new Set(roots.map(root => path.resolve(root))));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function errorMessage(error: unknown): string {
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function keptMalformedRecord(lockDir: string): GcRecord {
|
|
48
|
+
return {
|
|
49
|
+
store: "file_locks",
|
|
50
|
+
id: lockDir,
|
|
51
|
+
path: lockDir,
|
|
52
|
+
pid_status: "none",
|
|
53
|
+
status: "malformed",
|
|
54
|
+
stale: false,
|
|
55
|
+
removable: false,
|
|
56
|
+
action: "none",
|
|
57
|
+
reason: "missing_or_malformed_file_lock_info",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
|
|
62
|
+
const info = await readFileLockInfoForGc(lockDir);
|
|
63
|
+
if (!info) return keptMalformedRecord(lockDir);
|
|
64
|
+
|
|
65
|
+
const probeResult = ctx.probe(info.pid);
|
|
66
|
+
const pidStatus = gcPidStatusLabel(probeResult);
|
|
67
|
+
const removable = probeResult.status === "dead";
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
store: "file_locks",
|
|
71
|
+
id: lockDir,
|
|
72
|
+
path: lockDir,
|
|
73
|
+
pid: info.pid,
|
|
74
|
+
pid_status: pidStatus,
|
|
75
|
+
status: pidStatus,
|
|
76
|
+
stale: removable,
|
|
77
|
+
removable,
|
|
78
|
+
action: "none",
|
|
79
|
+
reason: removable ? "file_lock_owner_pid_dead" : `file_lock_owner_pid_${pidStatus}`,
|
|
80
|
+
detail: `timestamp=${info.timestamp}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function walkForLockDirs(
|
|
85
|
+
dir: string,
|
|
86
|
+
depth: number,
|
|
87
|
+
state: WalkState,
|
|
88
|
+
lockDirs: Set<string>,
|
|
89
|
+
errors: GcError[],
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
if (state.entries >= MAX_WALK_ENTRIES) {
|
|
92
|
+
state.truncated = true;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let stat: Stats;
|
|
97
|
+
try {
|
|
98
|
+
stat = await fs.lstat(dir);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (isEnoent(error)) return;
|
|
101
|
+
errors.push({ store: "file_locks", scope: dir, message: errorMessage(error) });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
state.entries++;
|
|
106
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
|
107
|
+
|
|
108
|
+
if (path.basename(dir).endsWith(".lock")) {
|
|
109
|
+
lockDirs.add(dir);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (depth >= MAX_WALK_DEPTH) return;
|
|
114
|
+
|
|
115
|
+
let entries: string[];
|
|
116
|
+
try {
|
|
117
|
+
entries = await fs.readdir(dir);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isEnoent(error)) return;
|
|
120
|
+
errors.push({ store: "file_locks", scope: dir, message: errorMessage(error) });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (state.entries >= MAX_WALK_ENTRIES) {
|
|
126
|
+
state.truncated = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (PRUNED_DIR_NAMES.has(entry)) continue;
|
|
130
|
+
await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
135
|
+
store: "file_locks",
|
|
136
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
137
|
+
const records: GcRecord[] = [];
|
|
138
|
+
const errors: GcError[] = [];
|
|
139
|
+
const lockDirs = new Set<string>();
|
|
140
|
+
const state: WalkState = { entries: 0, truncated: false };
|
|
141
|
+
|
|
142
|
+
for (const root of knownFileLockRoots(ctx)) {
|
|
143
|
+
await walkForLockDirs(root, 0, state, lockDirs, errors);
|
|
144
|
+
if (state.truncated) break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (state.truncated) {
|
|
148
|
+
errors.push({
|
|
149
|
+
store: "file_locks",
|
|
150
|
+
scope: "discovery",
|
|
151
|
+
message: `file lock discovery capped at ${MAX_WALK_ENTRIES} entries`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const lockDir of lockDirs) {
|
|
156
|
+
try {
|
|
157
|
+
records.push(await collectLockRecord(lockDir, ctx));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
errors.push({ store: "file_locks", scope: lockDir, message: errorMessage(error) });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { records, errors };
|
|
164
|
+
},
|
|
165
|
+
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
166
|
+
const lockDir = record.path ?? record.id;
|
|
167
|
+
const info = await readFileLockInfoForGc(lockDir);
|
|
168
|
+
if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
169
|
+
|
|
170
|
+
const probeResult = ctx.probe(info.pid);
|
|
171
|
+
if (probeResult.status !== "dead") {
|
|
172
|
+
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Fail-closed owner-token guard (#606): we observed `info` (pid+timestamp)
|
|
176
|
+
// dead, but a fresh owner can reclaim a stale lock dir at this same path
|
|
177
|
+
// between the probe above and the unlink below. Pass the exact owner token
|
|
178
|
+
// so removal re-verifies the on-disk identity under the unlink and refuses
|
|
179
|
+
// to delete a recreated LIVE lock (TOCTOU).
|
|
180
|
+
try {
|
|
181
|
+
const removal = await removeFileLockDirForGc(lockDir, info);
|
|
182
|
+
if (removal === "owner_changed") {
|
|
183
|
+
return { removed: false, skipped: "file_lock_owner_changed_before_delete" };
|
|
184
|
+
}
|
|
185
|
+
if (removal === "missing") {
|
|
186
|
+
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
187
|
+
}
|
|
188
|
+
return { removed: true };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return { removed: false, error: errorMessage(error) };
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
};
|