@gajae-code/coding-agent 0.15.3 → 0.15.5
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 +26 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/config/settings-schema.ts +53 -0
- package/src/config/settings.ts +1 -1
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +5 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +20 -2
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +302 -25
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +46 -6
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
package/src/commands/sdk.ts
CHANGED
|
@@ -838,6 +838,10 @@ class SdkSessionHelp extends Command {
|
|
|
838
838
|
description: "Workspace directory for saved-session resolution (default: current directory)",
|
|
839
839
|
}),
|
|
840
840
|
op: Flags.string({ description: "Raw control or global operation" }),
|
|
841
|
+
scope: Flags.string({
|
|
842
|
+
description: "list scope: repo (default), cwd, worktree, or all",
|
|
843
|
+
options: ["repo", "cwd", "worktree", "all"],
|
|
844
|
+
}),
|
|
841
845
|
query: Flags.string({ description: "Raw query name" }),
|
|
842
846
|
"json-input": Flags.string({ description: "SDK request JSON object" }),
|
|
843
847
|
"json-input-file": Flags.string({ description: "Read SDK request JSON from a 0600 file" }),
|
|
@@ -894,6 +898,7 @@ class SdkSessionCommand extends Command {
|
|
|
894
898
|
allEvents: Boolean(flagRec["all-events"]),
|
|
895
899
|
agentDir: flagRec["agent-dir"] as string | undefined,
|
|
896
900
|
repo: flagRec.repo as string | undefined,
|
|
901
|
+
scope: flagRec.scope as string | undefined,
|
|
897
902
|
});
|
|
898
903
|
}
|
|
899
904
|
}
|
|
@@ -146,6 +146,7 @@ export const TIER_MAP_SKIP_LIST = {
|
|
|
146
146
|
},
|
|
147
147
|
"vercel-ai-gateway/google/gemini-3.7-flash": { rationale: "post-rebase catalog addition from dev; not yet curated" },
|
|
148
148
|
"zai/glm-5.3": { rationale: "post-rebase catalog addition from dev; not yet curated" },
|
|
149
|
+
"zai/glm-5.3-flash": { rationale: "post-feature catalog addition; not yet curated" },
|
|
149
150
|
"cursor/gemini-3.7-flash-high": {
|
|
150
151
|
rationale: "post-rebase catalog addition from dev; not yet curated",
|
|
151
152
|
},
|
|
@@ -247,22 +247,22 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
247
247
|
architect: "anthropic/claude-fable-5:xhigh",
|
|
248
248
|
}),
|
|
249
249
|
profile("glm-eco", ["zai"], {
|
|
250
|
-
default: "zai/glm-5.3:low",
|
|
251
|
-
executor: "zai/glm-5.3:low",
|
|
252
|
-
planner: "zai/glm-5.3:low",
|
|
250
|
+
default: "zai/glm-5.3-flash:low",
|
|
251
|
+
executor: "zai/glm-5.3-flash:low",
|
|
252
|
+
planner: "zai/glm-5.3-flash:low",
|
|
253
253
|
critic: "zai/glm-5.3:high",
|
|
254
254
|
architect: "zai/glm-5.3:high",
|
|
255
255
|
}),
|
|
256
256
|
profile("glm-medium", ["zai"], {
|
|
257
257
|
default: "zai/glm-5.3:high",
|
|
258
|
-
executor: "zai/glm-5.3:low",
|
|
258
|
+
executor: "zai/glm-5.3-flash:low",
|
|
259
259
|
planner: "zai/glm-5.3:high",
|
|
260
260
|
critic: "zai/glm-5.3:high",
|
|
261
261
|
architect: "zai/glm-5.3:max",
|
|
262
262
|
}),
|
|
263
263
|
profile("glm-pro", ["zai"], {
|
|
264
264
|
default: "zai/glm-5.3:max",
|
|
265
|
-
executor: "zai/glm-5.3:high",
|
|
265
|
+
executor: "zai/glm-5.3-flash:high",
|
|
266
266
|
planner: "zai/glm-5.3:high",
|
|
267
267
|
critic: "zai/glm-5.3:max",
|
|
268
268
|
architect: "zai/glm-5.3:max",
|
|
@@ -2154,7 +2154,9 @@ export class ModelRegistry {
|
|
|
2154
2154
|
});
|
|
2155
2155
|
if (localCompatResolvedKey) {
|
|
2156
2156
|
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
2157
|
-
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey
|
|
2157
|
+
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey, {
|
|
2158
|
+
envSourced: !localOpenAICompat.apiKey,
|
|
2159
|
+
});
|
|
2158
2160
|
} else {
|
|
2159
2161
|
keylessProviders.add(providerName);
|
|
2160
2162
|
this.#optionalAuthProviders.add(providerName);
|
|
@@ -2216,7 +2218,9 @@ export class ModelRegistry {
|
|
|
2216
2218
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
2217
2219
|
: undefined;
|
|
2218
2220
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
2219
|
-
if (resolved)
|
|
2221
|
+
if (resolved) {
|
|
2222
|
+
this.authStorage.setConfigApiKey(providerName, resolved, { envSourced: !providerConfig.apiKey });
|
|
2223
|
+
}
|
|
2220
2224
|
}
|
|
2221
2225
|
|
|
2222
2226
|
// Parse per-model overrides
|
|
@@ -3806,7 +3810,9 @@ export class ModelRegistry {
|
|
|
3806
3810
|
? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv)
|
|
3807
3811
|
: undefined;
|
|
3808
3812
|
if (resolved) this.#customProviderApiKeys.set(providerName, resolved);
|
|
3809
|
-
if (resolved)
|
|
3813
|
+
if (resolved) {
|
|
3814
|
+
this.authStorage.setConfigApiKey(providerName, resolved, { envSourced: !providerConfig.apiKey });
|
|
3815
|
+
}
|
|
3810
3816
|
}
|
|
3811
3817
|
for (const modelDef of modelDefs) {
|
|
3812
3818
|
const providerCompat = providerConfig.disableStrictTools
|
|
@@ -4547,7 +4553,7 @@ export class ModelRegistry {
|
|
|
4547
4553
|
return;
|
|
4548
4554
|
}
|
|
4549
4555
|
this.#customProviderApiKeys.set(provider, resolved);
|
|
4550
|
-
this.authStorage.setConfigApiKey(provider, resolved);
|
|
4556
|
+
this.authStorage.setConfigApiKey(provider, resolved, { envSourced: true });
|
|
4551
4557
|
}
|
|
4552
4558
|
|
|
4553
4559
|
async #peekApiKeyForProvider(
|
|
@@ -1940,6 +1940,52 @@ export const SETTINGS_SCHEMA = {
|
|
|
1940
1940
|
},
|
|
1941
1941
|
},
|
|
1942
1942
|
|
|
1943
|
+
"compaction.adaptive.enabled": {
|
|
1944
|
+
type: "boolean",
|
|
1945
|
+
default: false,
|
|
1946
|
+
ui: {
|
|
1947
|
+
tab: "context",
|
|
1948
|
+
label: "Adaptive Compaction",
|
|
1949
|
+
description: "Adjust compaction frequency by context size and call rate",
|
|
1950
|
+
},
|
|
1951
|
+
},
|
|
1952
|
+
"compaction.adaptive.baseThresholdPercent": {
|
|
1953
|
+
type: "number",
|
|
1954
|
+
default: 85,
|
|
1955
|
+
ui: {
|
|
1956
|
+
tab: "context",
|
|
1957
|
+
label: "Adaptive Base Threshold %",
|
|
1958
|
+
description: "Base context percentage used when adaptive compaction is enabled",
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
"compaction.adaptive.aggression": {
|
|
1962
|
+
type: "number",
|
|
1963
|
+
default: 0.15,
|
|
1964
|
+
ui: {
|
|
1965
|
+
tab: "context",
|
|
1966
|
+
label: "Adaptive Aggression",
|
|
1967
|
+
description: "How strongly call rate lowers the compaction threshold; use 0 to 1",
|
|
1968
|
+
},
|
|
1969
|
+
},
|
|
1970
|
+
"compaction.adaptive.turnWindow": {
|
|
1971
|
+
type: "number",
|
|
1972
|
+
default: 15,
|
|
1973
|
+
ui: {
|
|
1974
|
+
tab: "context",
|
|
1975
|
+
label: "Call-Rate Window",
|
|
1976
|
+
description: "Minutes of recent calls considered by adaptive compaction",
|
|
1977
|
+
},
|
|
1978
|
+
},
|
|
1979
|
+
"compaction.adaptive.minThresholdPercent": {
|
|
1980
|
+
type: "number",
|
|
1981
|
+
default: 50,
|
|
1982
|
+
ui: {
|
|
1983
|
+
tab: "context",
|
|
1984
|
+
label: "Adaptive Floor %",
|
|
1985
|
+
description: "Lowest context percentage adaptive compaction may lower the threshold to",
|
|
1986
|
+
},
|
|
1987
|
+
},
|
|
1988
|
+
|
|
1943
1989
|
"compaction.handoffSaveToDisk": {
|
|
1944
1990
|
type: "boolean",
|
|
1945
1991
|
default: false,
|
|
@@ -4332,6 +4378,13 @@ export interface CompactionSettings {
|
|
|
4332
4378
|
strategy: "context-full" | "handoff" | "off";
|
|
4333
4379
|
thresholdPercent: number;
|
|
4334
4380
|
thresholdTokens: number;
|
|
4381
|
+
adaptive: {
|
|
4382
|
+
enabled: boolean;
|
|
4383
|
+
baseThresholdPercent: number;
|
|
4384
|
+
aggression: number;
|
|
4385
|
+
turnWindow: number;
|
|
4386
|
+
minThresholdPercent: number;
|
|
4387
|
+
};
|
|
4335
4388
|
reserveTokens: number;
|
|
4336
4389
|
keepRecentTokens: number;
|
|
4337
4390
|
handoffSaveToDisk: boolean;
|
package/src/config/settings.ts
CHANGED
|
@@ -1174,7 +1174,7 @@ export class Settings implements NotificationSettingsReader {
|
|
|
1174
1174
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
1175
1175
|
if (key.startsWith(`${prefix}.`)) {
|
|
1176
1176
|
const suffix = key.slice(prefix.length + 1);
|
|
1177
|
-
result
|
|
1177
|
+
setByPath(result, suffix.split("."), this.get(key));
|
|
1178
1178
|
}
|
|
1179
1179
|
}
|
|
1180
1180
|
return result as unknown as GroupTypeMap[G];
|
|
@@ -112,7 +112,7 @@ function parseRootList(value: string | undefined): string[] {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function resolveManagedWorktreeRoot(root: string, configured: string | undefined): string {
|
|
115
|
-
const template = (configured?.trim() || "{repo}
|
|
115
|
+
const template = (configured?.trim() || "{repo}/.worktrees").replace(/^~(?=\/|$)/, os.homedir());
|
|
116
116
|
const resolved = template.replaceAll("{repo}", path.basename(root));
|
|
117
117
|
return path.resolve(path.isAbsolute(resolved) ? resolved : path.join(path.dirname(root), resolved));
|
|
118
118
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ViewportAnchorSource,
|
|
11
11
|
type ViewportAnchorSourceRenderer,
|
|
12
12
|
} from "@gajae-code/tui";
|
|
13
|
+
import { isLegacyDeepInterviewPlaceholder, isWorkflowPlaceholderText } from "../gjc-runtime/workflow-placeholder";
|
|
13
14
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
14
15
|
|
|
15
16
|
interface RoundQuestionModel {
|
|
@@ -132,7 +133,7 @@ function parseRoundQuestion(text: string): RoundQuestionModel | null {
|
|
|
132
133
|
.slice(headerIndex + 1)
|
|
133
134
|
.join("\n")
|
|
134
135
|
.trim();
|
|
135
|
-
if (
|
|
136
|
+
if (isWorkflowPlaceholderText(body)) return null;
|
|
136
137
|
|
|
137
138
|
const componentMatch =
|
|
138
139
|
/^Round\s+(\d+)\s+\|\s+Component:\s*(.*?)\s+\|\s+Targeting:\s*(.*?)\s+\|\s+Why now:\s*(.*?)\s+\|\s+Ambiguity:\s*(.+?)%?\s*$/i.exec(
|
|
@@ -204,11 +205,13 @@ function parseTopologyQuestion(text: string): TopologyQuestionModel | null {
|
|
|
204
205
|
if (inQuestion) questionLines.push(trimmed);
|
|
205
206
|
else contextLines.push(trimmed);
|
|
206
207
|
}
|
|
208
|
+
const question = questionLines.join("\n");
|
|
209
|
+
if (isWorkflowPlaceholderText(question)) return null;
|
|
207
210
|
return {
|
|
208
211
|
kind: "topology-question",
|
|
209
212
|
context: contextLines.join("\n") || undefined,
|
|
210
213
|
components,
|
|
211
|
-
question
|
|
214
|
+
question,
|
|
212
215
|
};
|
|
213
216
|
}
|
|
214
217
|
|
|
@@ -399,6 +402,7 @@ export function renderDeepInterviewAskQuestion(question: string, uiTheme: Theme)
|
|
|
399
402
|
export function isDeepInterviewAskQuestion(question: string): boolean {
|
|
400
403
|
if (parseTopologyQuestion(question) ?? parseRoundQuestion(question)) return true;
|
|
401
404
|
const normalized = normalizeText(question);
|
|
405
|
+
if (isLegacyDeepInterviewPlaceholder(normalized)) return false;
|
|
402
406
|
return /(?:^|\n)\s*Round\s+\d+\s*\|.*?\bAmbiguity\b/i.test(normalized);
|
|
403
407
|
}
|
|
404
408
|
|
|
@@ -155,6 +155,8 @@ export interface ExtensionUIDialogOptions {
|
|
|
155
155
|
customInput?: {
|
|
156
156
|
optionLabel: string;
|
|
157
157
|
onSubmit: (text: string) => void;
|
|
158
|
+
/** Empty/whitespace-only submissions may be rejected by the selector. */
|
|
159
|
+
allowEmpty?: boolean;
|
|
158
160
|
};
|
|
159
161
|
/**
|
|
160
162
|
* Inline free-text input for a non-answer clarification action. It is
|
|
@@ -534,6 +536,7 @@ export interface ExtensionContext {
|
|
|
534
536
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
535
537
|
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
536
538
|
preflightSignal?: AbortSignal;
|
|
539
|
+
sdkRunToken?: string;
|
|
537
540
|
},
|
|
538
541
|
): Promise<unknown>;
|
|
539
542
|
setPlanMode?(on: boolean): unknown;
|
|
@@ -752,6 +755,8 @@ export interface AgentStartEvent extends SharedAgentStartEvent {
|
|
|
752
755
|
export interface AgentFailedEvent {
|
|
753
756
|
type: "agent_failed";
|
|
754
757
|
error: AgentFailureDiagnostic;
|
|
758
|
+
/** Internal SDK queue-owner binding for exact lifecycle attribution. */
|
|
759
|
+
sdkRunToken?: string;
|
|
755
760
|
/** Attempt correlation for the failing run, when scoped (matches the
|
|
756
761
|
* agent_start/agent_end scope contract). */
|
|
757
762
|
scope?: AttemptScope;
|
|
@@ -184,6 +184,8 @@ export interface AgentStartEvent {
|
|
|
184
184
|
export interface AgentEndEvent {
|
|
185
185
|
type: "agent_end";
|
|
186
186
|
messages: AgentMessage[];
|
|
187
|
+
/** Internal SDK queue-owner binding for exact lifecycle attribution. */
|
|
188
|
+
sdkRunToken?: string;
|
|
187
189
|
/** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
|
|
188
190
|
stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
|
|
189
191
|
/** Present for maintenance checkpoints; non-aborted checkpoints are not terminal. */
|
|
@@ -78,7 +78,7 @@ const WORKTREE_BUCKET_ENV = "GJC_WORKTREE_DIR";
|
|
|
78
78
|
* what keeps two repositories that share a branch name from resolving to one worktree.
|
|
79
79
|
*/
|
|
80
80
|
const REPO_NAME_PLACEHOLDER = "{repo}";
|
|
81
|
-
const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}
|
|
81
|
+
const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}/.worktrees`;
|
|
82
82
|
|
|
83
83
|
function expandHomePrefix(value: string, home: string, pathApi: typeof path.posix): string {
|
|
84
84
|
if (value === "~") return home;
|
|
@@ -108,14 +108,36 @@ export function resolveWorktreeBucketForPath(
|
|
|
108
108
|
/**
|
|
109
109
|
* Directory that holds this repository's launch worktrees.
|
|
110
110
|
*
|
|
111
|
-
* A relative override resolves against the repository's
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* A relative override resolves against the repository's parent directory. The default
|
|
112
|
+
* `{repo}/.worktrees` template places managed worktrees inside the repository, while
|
|
113
|
+
* `{repo}.worktrees` adopts an existing sibling bucket. An absolute override is used verbatim.
|
|
114
114
|
*/
|
|
115
115
|
function resolveWorktreeBucket(repoRoot: string): string {
|
|
116
116
|
return resolveWorktreeBucketForPath(repoRoot, process.env[WORKTREE_BUCKET_ENV], os.homedir(), path);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function ensureRepositoryBucketIgnored(repoRoot: string, bucketPath: string): void {
|
|
120
|
+
const relativeBucket = path.relative(repoRoot, bucketPath);
|
|
121
|
+
if (relativeBucket.startsWith(`..${path.sep}`) || relativeBucket === ".." || path.isAbsolute(relativeBucket)) return;
|
|
122
|
+
const ignoreProbe = path.join(relativeBucket, ".gjc-worktree-probe");
|
|
123
|
+
for (const candidate of [relativeBucket, ignoreProbe]) {
|
|
124
|
+
const result = Bun.spawnSync(["git", "check-ignore", "--quiet", "--", candidate], {
|
|
125
|
+
cwd: repoRoot,
|
|
126
|
+
stdout: "ignore",
|
|
127
|
+
stderr: "pipe",
|
|
128
|
+
});
|
|
129
|
+
if (result.exitCode === 0) return;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(
|
|
132
|
+
[
|
|
133
|
+
"worktree_bucket_not_ignored",
|
|
134
|
+
"The GJC launch worktree bucket is inside the repository but is not ignored by Git.",
|
|
135
|
+
`Path: ${formatBucketPath(bucketPath)}`,
|
|
136
|
+
`Safe remediation: add /${relativeBucket.replaceAll(path.sep, "/")} to ${path.join(repoRoot, ".gitignore")}, then relaunch.`,
|
|
137
|
+
].join("\n"),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
function resolveSourceBranchSlug(repoRoot: string, baseRef: string): string {
|
|
120
142
|
const branch = tryRunGit(repoRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
121
143
|
if (branch) return sanitizePathToken(branch);
|
|
@@ -301,6 +323,7 @@ function pruneStaleWorktreePath(repoRoot: string): void {
|
|
|
301
323
|
|
|
302
324
|
function readWorktreeEntryFromPath(repoRoot: string, worktreePath: string): GitWorktreeEntry | null {
|
|
303
325
|
if (!fs.existsSync(worktreePath)) return null;
|
|
326
|
+
if (!fs.existsSync(path.join(worktreePath, ".git"))) return null;
|
|
304
327
|
const repoCommonDir = tryRunGit(repoRoot, ["rev-parse", "--git-common-dir"]);
|
|
305
328
|
const worktreeCommonDir = tryRunGit(worktreePath, ["rev-parse", "--git-common-dir"]);
|
|
306
329
|
if (!repoCommonDir || !worktreeCommonDir) return null;
|
|
@@ -379,6 +402,7 @@ export function planLaunchWorktree(
|
|
|
379
402
|
if (branchName) validateBranchName(repoRoot, branchName);
|
|
380
403
|
const worktreeSlug = mode.detached ? resolveSourceBranchSlug(repoRoot, baseRef) : sanitizePathToken(mode.name);
|
|
381
404
|
const worktreePath = path.join(resolveWorktreeBucket(repoRoot), worktreeSlug);
|
|
405
|
+
if (path.resolve(worktreePath) === path.resolve(repoRoot)) throw new Error(`worktree_path_conflict:${worktreePath}`);
|
|
382
406
|
return { enabled: true, repoRoot, worktreePath, detached: mode.detached, baseRef, branchName };
|
|
383
407
|
}
|
|
384
408
|
|
|
@@ -386,6 +410,9 @@ export function ensureLaunchWorktree(
|
|
|
386
410
|
plan: GjcLaunchWorktreePlan | { enabled: false },
|
|
387
411
|
): GjcLaunchWorktreeResult | { enabled: false } {
|
|
388
412
|
if (!plan.enabled) return { enabled: false };
|
|
413
|
+
const bucketPath = path.dirname(plan.worktreePath);
|
|
414
|
+
inspectBucketDir(bucketPath);
|
|
415
|
+
ensureRepositoryBucketIgnored(plan.repoRoot, bucketPath);
|
|
389
416
|
let allWorktrees = listWorktrees(plan.repoRoot);
|
|
390
417
|
const staleAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
|
|
391
418
|
if (staleAtPath && !fs.existsSync(staleAtPath.path)) {
|
|
@@ -393,9 +420,19 @@ export function ensureLaunchWorktree(
|
|
|
393
420
|
allWorktrees = listWorktrees(plan.repoRoot);
|
|
394
421
|
}
|
|
395
422
|
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
423
|
+
const listedAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
|
|
424
|
+
const existingAtPath = readWorktreeEntryFromPath(plan.repoRoot, plan.worktreePath);
|
|
425
|
+
if (listedAtPath && !existingAtPath) {
|
|
426
|
+
if (fs.existsSync(plan.worktreePath)) throw new Error(`worktree_path_conflict:${plan.worktreePath}`);
|
|
427
|
+
throw new Error(
|
|
428
|
+
[
|
|
429
|
+
"worktree_path_unavailable",
|
|
430
|
+
"The requested launch worktree is still registered by Git but its directory is unavailable or locked.",
|
|
431
|
+
`Path: ${formatBucketPath(plan.worktreePath)}`,
|
|
432
|
+
"Safe remediation: inspect the worktree lock and remove or repair it with git worktree remove/prune when it is no longer needed, then relaunch. GJC did not delete or replace the entry.",
|
|
433
|
+
].join("\n"),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
399
436
|
const expectedBranchRef = plan.branchName ? `refs/heads/${plan.branchName}` : null;
|
|
400
437
|
|
|
401
438
|
if (existingAtPath) {
|
|
@@ -747,13 +747,31 @@ export async function resolveRalplanTargetRoot(
|
|
|
747
747
|
if (!repository) {
|
|
748
748
|
throw new RalplanCommandError(2, `ralplan --worktree-root is not inside a git repository: ${canonical}`);
|
|
749
749
|
}
|
|
750
|
-
|
|
750
|
+
// Do not pass the caret-containing `HEAD^{commit}` revision here. On Windows,
|
|
751
|
+
// Bun may dispatch git through a `.cmd` shim, where `^` is interpreted by the
|
|
752
|
+
// command shell before git receives the argument. HEAD is sufficient after
|
|
753
|
+
// resolving a repository root: an unborn or non-commit HEAD still fails the
|
|
754
|
+
// same validation, while the argv remains shell-safe on every platform.
|
|
755
|
+
const verified = Bun.spawn(["git", "-C", canonical, "rev-parse", "--verify", "HEAD"], {
|
|
751
756
|
stdout: "pipe",
|
|
752
757
|
stderr: "pipe",
|
|
753
758
|
});
|
|
754
|
-
|
|
759
|
+
const [verifiedStatus, verifiedOutput] = await Promise.all([verified.exited, new Response(verified.stdout).text()]);
|
|
760
|
+
const headObject = verifiedOutput.trim();
|
|
761
|
+
if (verifiedStatus !== 0 || !/^[0-9a-f]{40,64}$/u.test(headObject)) {
|
|
755
762
|
throw new RalplanCommandError(2, `ralplan --worktree-root is not a valid git worktree: ${canonical}`);
|
|
756
763
|
}
|
|
764
|
+
const objectType = Bun.spawn(["git", "-C", canonical, "cat-file", "-t", headObject], {
|
|
765
|
+
stdout: "pipe",
|
|
766
|
+
stderr: "pipe",
|
|
767
|
+
});
|
|
768
|
+
const [objectTypeStatus, objectTypeOutput] = await Promise.all([
|
|
769
|
+
objectType.exited,
|
|
770
|
+
new Response(objectType.stdout).text(),
|
|
771
|
+
]);
|
|
772
|
+
if (objectTypeStatus !== 0 || objectTypeOutput.trim() !== "commit") {
|
|
773
|
+
throw new RalplanCommandError(2, `ralplan --worktree-root is not a commit worktree: ${canonical}`);
|
|
774
|
+
}
|
|
757
775
|
let worktreeRoot: string;
|
|
758
776
|
try {
|
|
759
777
|
worktreeRoot = await fs.realpath(repository.repoRoot);
|