@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -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/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -31,7 +31,12 @@ import {
|
|
|
31
31
|
removeCanonicalBlob,
|
|
32
32
|
} from "../session/blob-store";
|
|
33
33
|
import { FileSessionStorage, probeSessionRetirement, retireSessionTranscript } from "../session/session-storage";
|
|
34
|
-
|
|
34
|
+
import {
|
|
35
|
+
collectEmptyDeleteReceipts,
|
|
36
|
+
type EmptyDeleteGcRecord,
|
|
37
|
+
type EmptyDeleteGcReport,
|
|
38
|
+
runEmptyDeleteGc,
|
|
39
|
+
} from "./empty-delete-gc";
|
|
35
40
|
import { buildGcReportText } from "./gc-render";
|
|
36
41
|
import { collectSessionScopeUsage, type GcSessionScopeUsage, shouldReportSessionScope } from "./gc-session-scope";
|
|
37
42
|
|
|
@@ -168,6 +173,7 @@ export interface GcReport {
|
|
|
168
173
|
* PID-liveness axis above is unchanged by its absence or presence.
|
|
169
174
|
*/
|
|
170
175
|
disk?: GcDiskReport;
|
|
176
|
+
empty_delete_receipts?: EmptyDeleteGcReport;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
export interface GcRunResult {
|
|
@@ -274,9 +280,11 @@ interface ParsedGcArgs {
|
|
|
274
280
|
json: boolean;
|
|
275
281
|
prune: boolean;
|
|
276
282
|
repairSessionIndex: boolean;
|
|
277
|
-
/** Opt-in second axis: report (and with `--prune`, reclaim) on-disk retention. */
|
|
278
283
|
disk: boolean;
|
|
279
284
|
help: boolean;
|
|
285
|
+
emptyDeleteReceipts: boolean;
|
|
286
|
+
emptyDeleteRoots: string[];
|
|
287
|
+
emptyDeleteManifests: string[];
|
|
280
288
|
}
|
|
281
289
|
|
|
282
290
|
class GcUsageError extends Error {}
|
|
@@ -286,10 +294,13 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
286
294
|
let prune = false;
|
|
287
295
|
let repairSessionIndex = false;
|
|
288
296
|
let disk = false;
|
|
289
|
-
|
|
290
297
|
let dryRun = false;
|
|
291
298
|
let help = false;
|
|
292
|
-
|
|
299
|
+
let emptyDeleteReceipts = false;
|
|
300
|
+
const emptyDeleteRoots: string[] = [];
|
|
301
|
+
const emptyDeleteManifests: string[] = [];
|
|
302
|
+
for (let i = 0; i < argv.length; i++) {
|
|
303
|
+
const arg = argv[i]!;
|
|
293
304
|
switch (arg) {
|
|
294
305
|
case "--json":
|
|
295
306
|
case "-j":
|
|
@@ -305,7 +316,6 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
305
316
|
case "--disk":
|
|
306
317
|
disk = true;
|
|
307
318
|
break;
|
|
308
|
-
|
|
309
319
|
case "--dry-run":
|
|
310
320
|
dryRun = true;
|
|
311
321
|
break;
|
|
@@ -313,15 +323,34 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
313
323
|
case "-h":
|
|
314
324
|
help = true;
|
|
315
325
|
break;
|
|
326
|
+
case "--empty-delete-receipts":
|
|
327
|
+
emptyDeleteReceipts = true;
|
|
328
|
+
break;
|
|
329
|
+
case "--root": {
|
|
330
|
+
const value = argv[++i];
|
|
331
|
+
// A following option token is a missing operand, not a root path. Any
|
|
332
|
+
// dash-prefixed value is rejected (short flags like -j included); spell a
|
|
333
|
+
// genuinely dash-prefixed path as ./-name so it is unambiguous.
|
|
334
|
+
if (!value || value.startsWith("-")) throw new GcUsageError("missing_root");
|
|
335
|
+
emptyDeleteRoots.push(value);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "--manifest": {
|
|
339
|
+
const value = argv[++i];
|
|
340
|
+
if (!value || value.startsWith("-")) throw new GcUsageError("missing_manifest");
|
|
341
|
+
emptyDeleteManifests.push(value);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
316
344
|
default:
|
|
317
345
|
throw new GcUsageError(`unknown_flag:${arg}`);
|
|
318
346
|
}
|
|
319
347
|
}
|
|
320
348
|
if (repairSessionIndex && prune) throw new GcUsageError("repair_session_index_cannot_combine_with_prune");
|
|
349
|
+
if (!emptyDeleteReceipts && (emptyDeleteRoots.length > 0 || emptyDeleteManifests.length > 0))
|
|
350
|
+
throw new GcUsageError("empty_delete_operands_require_feature_flag");
|
|
321
351
|
if (repairSessionIndex && dryRun) throw new GcUsageError("repair_session_index_cannot_combine_with_dry_run");
|
|
322
|
-
// Explicit --dry-run always wins over --prune/--force.
|
|
323
352
|
if (dryRun) prune = false;
|
|
324
|
-
return { json, prune, repairSessionIndex, disk, help };
|
|
353
|
+
return { json, prune, repairSessionIndex, disk, help, emptyDeleteReceipts, emptyDeleteRoots, emptyDeleteManifests };
|
|
325
354
|
}
|
|
326
355
|
|
|
327
356
|
/**
|
|
@@ -404,6 +433,7 @@ export async function collectGcReport(adapters: GcStoreAdapter[], ctx: GcContext
|
|
|
404
433
|
export function computeExitCode(report: GcReport): number {
|
|
405
434
|
if (report.errors.length > 0) return 1;
|
|
406
435
|
if (!report.dry_run && report.counts.failed > 0) return 1;
|
|
436
|
+
if (report.empty_delete_receipts?.errors.length) return 1;
|
|
407
437
|
if (report.disk) {
|
|
408
438
|
if (report.disk.errors.length > 0) return 1;
|
|
409
439
|
if (!report.disk.dry_run && report.disk.totals.failed > 0) return 1;
|
|
@@ -415,6 +445,63 @@ function resolveGcAgentDir(env: NodeJS.ProcessEnv): string {
|
|
|
415
445
|
return env.GJC_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || getAgentDir();
|
|
416
446
|
}
|
|
417
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Validate operator-supplied empty-delete roots before any ordinary GC mutation.
|
|
450
|
+
*
|
|
451
|
+
* These roots are explicit operands, so a missing, symlink, non-directory, or
|
|
452
|
+
* unreadable root is a usage failure rather than a partial GC report. The
|
|
453
|
+
* collector repeats its own race-safe checks after this preflight.
|
|
454
|
+
*/
|
|
455
|
+
async function preflightEmptyDeleteRoot(root: string): Promise<void> {
|
|
456
|
+
const resolvedRoot = path.resolve(root);
|
|
457
|
+
let stat: Stats;
|
|
458
|
+
try {
|
|
459
|
+
stat = await fsp.lstat(resolvedRoot);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
462
|
+
throw new GcUsageError(`${root}: missing_root`);
|
|
463
|
+
}
|
|
464
|
+
throw new GcUsageError(`${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
465
|
+
}
|
|
466
|
+
if (stat.isSymbolicLink()) throw new GcUsageError(`${root}: symlink_root`);
|
|
467
|
+
if (!stat.isDirectory()) throw new GcUsageError(`${root}: not_directory`);
|
|
468
|
+
try {
|
|
469
|
+
await fsp.readdir(resolvedRoot);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throw new GcUsageError(`${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Collect every explicit empty-delete root once before starting any destructive
|
|
477
|
+
* operation. The second pass reuses those identities so a later root race cannot
|
|
478
|
+
* leave an earlier root pruned before the race is discovered.
|
|
479
|
+
*/
|
|
480
|
+
async function collectEmptyDeleteReceiptsForGc(
|
|
481
|
+
roots: string[],
|
|
482
|
+
): Promise<{ report: EmptyDeleteGcReport; records: Map<string, EmptyDeleteGcRecord[]> }> {
|
|
483
|
+
const collected = new Map<string, EmptyDeleteGcRecord[]>();
|
|
484
|
+
const collect = async (root: string): Promise<EmptyDeleteGcRecord[]> => {
|
|
485
|
+
const records = await collectEmptyDeleteReceipts(root);
|
|
486
|
+
collected.set(root, records);
|
|
487
|
+
return records.map(record => ({
|
|
488
|
+
...record,
|
|
489
|
+
identity: record.identity ? { ...record.identity } : undefined,
|
|
490
|
+
retainedPaths: record.retainedPaths ? { ...record.retainedPaths } : undefined,
|
|
491
|
+
}));
|
|
492
|
+
};
|
|
493
|
+
const report = await runEmptyDeleteGc({ roots, prune: false }, { collect });
|
|
494
|
+
return { report, records: collected };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function cloneEmptyDeleteRecords(records: EmptyDeleteGcRecord[]): EmptyDeleteGcRecord[] {
|
|
498
|
+
return records.map(record => ({
|
|
499
|
+
...record,
|
|
500
|
+
identity: record.identity ? { ...record.identity } : undefined,
|
|
501
|
+
retainedPaths: record.retainedPaths ? { ...record.retainedPaths } : undefined,
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
|
|
418
505
|
/**
|
|
419
506
|
* Locate and measure the managed scope for `cwd`.
|
|
420
507
|
*
|
|
@@ -482,9 +569,77 @@ export async function runGjcGcCommand(
|
|
|
482
569
|
return { stdout: gcHelpText(), stderr: "", status: 0 };
|
|
483
570
|
}
|
|
484
571
|
|
|
572
|
+
// Resolve and validate every empty-delete operand BEFORE any store collection or
|
|
573
|
+
// prune can mutate: a malformed manifest or missing operand must fail with status 2
|
|
574
|
+
// while every candidate is still untouched.
|
|
575
|
+
let emptyDeleteRoots: string[] = [];
|
|
576
|
+
if (parsed.emptyDeleteReceipts) {
|
|
577
|
+
emptyDeleteRoots = [...parsed.emptyDeleteRoots];
|
|
578
|
+
if (emptyDeleteRoots.some(root => root.includes("\0"))) {
|
|
579
|
+
return { stdout: "", stderr: "gjc gc: root_invalid\n", status: 2 };
|
|
580
|
+
}
|
|
581
|
+
// Every supplied manifest is validated, not just the last: a malformed earlier
|
|
582
|
+
// manifest must fail the run before any store collection or prune can mutate.
|
|
583
|
+
for (const manifestPath of parsed.emptyDeleteManifests) {
|
|
584
|
+
let parsedManifest: unknown;
|
|
585
|
+
try {
|
|
586
|
+
const raw = await fsp.readFile(manifestPath, "utf8");
|
|
587
|
+
parsedManifest = JSON.parse(raw);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
590
|
+
return { stdout: "", stderr: `gjc gc: manifest_invalid: ${message}\n`, status: 2 };
|
|
591
|
+
}
|
|
592
|
+
if (typeof parsedManifest !== "object" || parsedManifest === null || Array.isArray(parsedManifest)) {
|
|
593
|
+
return { stdout: "", stderr: "gjc gc: manifest_shape_invalid\n", status: 2 };
|
|
594
|
+
}
|
|
595
|
+
const manifestRoots = (parsedManifest as { roots?: unknown }).roots;
|
|
596
|
+
if (!Array.isArray(manifestRoots)) {
|
|
597
|
+
return { stdout: "", stderr: "gjc gc: manifest_roots_required\n", status: 2 };
|
|
598
|
+
}
|
|
599
|
+
for (const root of manifestRoots) {
|
|
600
|
+
if (typeof root !== "string" || root.length === 0 || root.includes("\0")) {
|
|
601
|
+
return { stdout: "", stderr: "gjc gc: manifest_root_invalid\n", status: 2 };
|
|
602
|
+
}
|
|
603
|
+
emptyDeleteRoots.push(root);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (emptyDeleteRoots.length === 0) {
|
|
607
|
+
return { stdout: "", stderr: "gjc gc: empty_delete_receipts_requires_root_or_manifest\n", status: 2 };
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
for (const root of emptyDeleteRoots) await preflightEmptyDeleteRoot(root);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
const message = error instanceof GcUsageError ? error.message : String(error);
|
|
613
|
+
return { stdout: "", stderr: `gjc gc: ${message}\n`, status: 2 };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
let emptyDeleteReceipts: EmptyDeleteGcReport | undefined;
|
|
617
|
+
let emptyDeleteRecords: Map<string, EmptyDeleteGcRecord[]> | undefined;
|
|
618
|
+
if (parsed.emptyDeleteReceipts) {
|
|
619
|
+
// Discovery is deliberately read-only and covers every root before either the
|
|
620
|
+
// empty-delete unlink or an ordinary --prune adapter can mutate anything.
|
|
621
|
+
const collected = await collectEmptyDeleteReceiptsForGc(emptyDeleteRoots);
|
|
622
|
+
emptyDeleteReceipts = collected.report;
|
|
623
|
+
emptyDeleteRecords = collected.records;
|
|
624
|
+
if (parsed.prune && emptyDeleteReceipts.errors.length === 0) {
|
|
625
|
+
emptyDeleteReceipts = await runEmptyDeleteGc(
|
|
626
|
+
{ roots: emptyDeleteRoots, prune: true },
|
|
627
|
+
{
|
|
628
|
+
collect: async root => {
|
|
629
|
+
const records = emptyDeleteRecords?.get(root);
|
|
630
|
+
if (!records) throw new Error(`${root}: empty_delete_collection_missing`);
|
|
631
|
+
return cloneEmptyDeleteRecords(records);
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
485
637
|
const resolvedAdapters = adapters ?? (await defaultGcAdapters());
|
|
486
638
|
const ctx: GcContext = { probe: gcPidProbe, force: parsed.prune, env, cwd };
|
|
487
|
-
|
|
639
|
+
// A failed explicit empty-delete discovery or cleanup is a safety gate: report
|
|
640
|
+
// ordinary candidates, but never perform their destructive prune in that run.
|
|
641
|
+
const ordinaryPrune = parsed.prune && (emptyDeleteReceipts?.errors.length ?? 0) === 0;
|
|
642
|
+
const report = await collectGcReport(resolvedAdapters, ctx, ordinaryPrune);
|
|
488
643
|
report.operation = parsed.repairSessionIndex ? "repair_session_index" : parsed.prune ? "prune" : "dry_run";
|
|
489
644
|
report.session_index = await collectSessionIndexHealth(parsed.repairSessionIndex, resolveGcAgentDir(env));
|
|
490
645
|
if (parsed.disk) {
|
|
@@ -492,9 +647,10 @@ export async function runGjcGcCommand(
|
|
|
492
647
|
agentDir: resolveGcAgentDir(env),
|
|
493
648
|
env,
|
|
494
649
|
policy: resolveGcDiskPolicy(diskPolicy),
|
|
495
|
-
prune:
|
|
650
|
+
prune: ordinaryPrune,
|
|
496
651
|
});
|
|
497
652
|
}
|
|
653
|
+
if (emptyDeleteReceipts) report.empty_delete_receipts = emptyDeleteReceipts;
|
|
498
654
|
const scopeUsage = await collectGcSessionScope(cwd, resolveGcAgentDir(env));
|
|
499
655
|
if (scopeUsage && shouldReportSessionScope(scopeUsage)) report.session_scope = scopeUsage;
|
|
500
656
|
const sessionIndexFailed =
|
|
@@ -503,7 +659,7 @@ export async function runGjcGcCommand(
|
|
|
503
659
|
report.session_index?.status === "repair_failed";
|
|
504
660
|
const status = sessionIndexFailed ? 1 : computeExitCode(report);
|
|
505
661
|
const stdout = parsed.json
|
|
506
|
-
? `${JSON.stringify(report,
|
|
662
|
+
? `${JSON.stringify(report, (_key, value) => (typeof value === "bigint" ? value.toString() : value), 2)}\n`
|
|
507
663
|
: `${buildGcReportText(report)}${report.disk ? buildGcDiskReportText(report.disk) : ""}`;
|
|
508
664
|
return { stdout, stderr: "", status };
|
|
509
665
|
}
|
|
@@ -513,7 +669,7 @@ export function gcHelpText(): string {
|
|
|
513
669
|
"gjc gc - garbage-collect stale GJC session/PID records",
|
|
514
670
|
"",
|
|
515
671
|
"USAGE",
|
|
516
|
-
" $ gjc gc [--prune|--force] [--disk] [--repair-session-index] [--json]",
|
|
672
|
+
" $ gjc gc [--prune|--force] [--disk] [--repair-session-index] [--empty-delete-receipts --root <dir>] [--json]",
|
|
517
673
|
|
|
518
674
|
"",
|
|
519
675
|
"FLAGS",
|
|
@@ -522,6 +678,13 @@ export function gcHelpText(): string {
|
|
|
522
678
|
" -j, --json Emit machine-readable JSON",
|
|
523
679
|
" --repair-session-index Explicitly quarantine a corrupt session-index suffix and retain its valid prefix",
|
|
524
680
|
" --disk Also report on-disk retention (sessions, blobs, artifacts, natives, backups)",
|
|
681
|
+
" --empty-delete-receipts Report/prune empty .gjc-delete-* under --root / --manifest",
|
|
682
|
+
" --root <dir> Operand root for --empty-delete-receipts (repeatable)",
|
|
683
|
+
' --manifest <file> JSON {"roots":[...]} for --empty-delete-receipts (repeatable)',
|
|
684
|
+
"",
|
|
685
|
+
" Operand values starting with '-' are rejected as missing operands; spell a",
|
|
686
|
+
" dash-prefixed path as ./-name. --root/--manifest without --empty-delete-receipts",
|
|
687
|
+
" is a usage error, and every supplied manifest is validated before any prune.",
|
|
525
688
|
"",
|
|
526
689
|
"Liveness-only: a record is removed only when its owning process is dead",
|
|
527
690
|
"(ESRCH). Live / permission-denied / unknown processes are always kept.",
|
|
@@ -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) {
|
|
@@ -756,9 +756,22 @@ export async function resolveRalplanTargetRoot(
|
|
|
756
756
|
stdout: "pipe",
|
|
757
757
|
stderr: "pipe",
|
|
758
758
|
});
|
|
759
|
-
|
|
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)) {
|
|
760
762
|
throw new RalplanCommandError(2, `ralplan --worktree-root is not a valid git worktree: ${canonical}`);
|
|
761
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
|
+
}
|
|
762
775
|
let worktreeRoot: string;
|
|
763
776
|
try {
|
|
764
777
|
worktreeRoot = await fs.realpath(repository.repoRoot);
|