@gajae-code/coding-agent 0.11.3 → 0.11.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 +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -12,12 +12,11 @@ import * as path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
import { isEnoent, logger, pathIsWithin } from "@gajae-code/utils";
|
|
14
14
|
|
|
15
|
-
import { cachePlugin } from "./cache";
|
|
15
|
+
import { cachePlugin, getCachedPluginPath } from "./cache";
|
|
16
16
|
import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
|
|
17
17
|
import {
|
|
18
18
|
addInstalledPlugin,
|
|
19
19
|
addMarketplaceEntry,
|
|
20
|
-
collectReferencedPaths,
|
|
21
20
|
getInstalledPlugin,
|
|
22
21
|
getMarketplaceEntry,
|
|
23
22
|
readInstalledPluginsRegistry,
|
|
@@ -255,6 +254,7 @@ export class MarketplaceManager {
|
|
|
255
254
|
if (existing && existing.length > 0 && !force) {
|
|
256
255
|
throw new Error(`Plugin "${pluginId}" is already installed. Use force option to reinstall.`);
|
|
257
256
|
}
|
|
257
|
+
const existingCachePaths = existing ? await this.#validateCacheDeletionTargets(pluginId, existing) : [];
|
|
258
258
|
|
|
259
259
|
// 4. Resolve source path.
|
|
260
260
|
// marketplaceClonePath is the marketplace root — the directory containing .Anthropic model-plugin/
|
|
@@ -289,6 +289,9 @@ export class MarketplaceManager {
|
|
|
289
289
|
let cachePath!: string;
|
|
290
290
|
try {
|
|
291
291
|
version = await this.#resolvePluginVersion(pluginEntry, sourcePath);
|
|
292
|
+
if ([marketplace, name, version].some(component => /[. ]$/.test(component))) {
|
|
293
|
+
throw new Error(`Invalid cache identity for plugin "${pluginId}"`);
|
|
294
|
+
}
|
|
292
295
|
cachePath = await cachePlugin(sourcePath, this.#opts.pluginsCacheDir, marketplace, name, version);
|
|
293
296
|
await this.#writeEmbeddedLspConfig(pluginEntry, cachePath);
|
|
294
297
|
} finally {
|
|
@@ -311,11 +314,13 @@ export class MarketplaceManager {
|
|
|
311
314
|
? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
|
|
312
315
|
: Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
|
|
313
316
|
]);
|
|
314
|
-
const referenced =
|
|
317
|
+
const referenced = this.#collectReferencedCacheIdentities(userReg, projectReg);
|
|
318
|
+
const cachePathIdentity = this.#cacheDeletionIdentity(cachePath);
|
|
315
319
|
|
|
316
|
-
for (const
|
|
317
|
-
|
|
318
|
-
|
|
320
|
+
for (const existingCachePath of existingCachePaths) {
|
|
321
|
+
const existingIdentity = this.#cacheDeletionIdentity(existingCachePath);
|
|
322
|
+
if (existingIdentity !== cachePathIdentity && !referenced.has(existingIdentity)) {
|
|
323
|
+
await fs.rm(existingCachePath, { recursive: true, force: true });
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
}
|
|
@@ -395,6 +400,84 @@ export class MarketplaceManager {
|
|
|
395
400
|
return "0.0.0";
|
|
396
401
|
}
|
|
397
402
|
|
|
403
|
+
async #validateCacheDeletionTargets(pluginId: string, entries: readonly InstalledPluginEntry[]): Promise<string[]> {
|
|
404
|
+
const parsed = parsePluginId(pluginId);
|
|
405
|
+
if (!parsed) {
|
|
406
|
+
throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targets: string[] = [];
|
|
410
|
+
for (const entry of entries) {
|
|
411
|
+
if ([parsed.marketplace, parsed.name, entry.version].some(component => /[. ]$/.test(component))) {
|
|
412
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let targetPath: string;
|
|
416
|
+
try {
|
|
417
|
+
targetPath = getCachedPluginPath(
|
|
418
|
+
this.#opts.pluginsCacheDir,
|
|
419
|
+
parsed.marketplace,
|
|
420
|
+
parsed.name,
|
|
421
|
+
entry.version,
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": invalid recorded cache identity`, {
|
|
425
|
+
cause: error,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (entry.installPath !== targetPath) {
|
|
430
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": recorded install path is not its cache path`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const stat = await fs.lstat(targetPath);
|
|
435
|
+
if (stat.isSymbolicLink()) {
|
|
436
|
+
throw new Error(`Refusing to remove plugin "${pluginId}": cache path is a symbolic link or junction`);
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (!isEnoent(error)) throw error;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
targets.push(targetPath);
|
|
443
|
+
}
|
|
444
|
+
return targets;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#cacheDeletionIdentity(cachePath: string): string {
|
|
448
|
+
// Model Windows path identity on every host so reference protection is
|
|
449
|
+
// deterministic: case is ignored and trailing dots/spaces are discarded.
|
|
450
|
+
return path
|
|
451
|
+
.resolve(cachePath)
|
|
452
|
+
.split(path.sep)
|
|
453
|
+
.map(component => component.replace(/[. ]+$/, "").toLowerCase())
|
|
454
|
+
.join(path.sep);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#collectReferencedCacheIdentities(...registries: InstalledPluginsRegistry[]): Set<string> {
|
|
458
|
+
const identities = new Set<string>();
|
|
459
|
+
for (const registry of registries) {
|
|
460
|
+
for (const [pluginId, entries] of Object.entries(registry.plugins)) {
|
|
461
|
+
const parsed = parsePluginId(pluginId);
|
|
462
|
+
if (!parsed) continue;
|
|
463
|
+
for (const entry of entries) {
|
|
464
|
+
try {
|
|
465
|
+
const cachePath = getCachedPluginPath(
|
|
466
|
+
this.#opts.pluginsCacheDir,
|
|
467
|
+
parsed.marketplace,
|
|
468
|
+
parsed.name,
|
|
469
|
+
entry.version,
|
|
470
|
+
);
|
|
471
|
+
identities.add(this.#cacheDeletionIdentity(cachePath));
|
|
472
|
+
} catch {
|
|
473
|
+
// Invalid registry entries cannot identify a permitted cache target.
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return identities;
|
|
479
|
+
}
|
|
480
|
+
|
|
398
481
|
async uninstallPlugin(pluginId: string, scope?: "user" | "project"): Promise<void> {
|
|
399
482
|
const parsed = parsePluginId(pluginId);
|
|
400
483
|
if (!parsed) {
|
|
@@ -434,6 +517,7 @@ export class MarketplaceManager {
|
|
|
434
517
|
const targetEntries = targetScope === "project" ? projectEntries! : userEntries!;
|
|
435
518
|
const targetReg = targetScope === "project" ? projectReg : userReg;
|
|
436
519
|
const registryPath = this.#registryPath(targetScope);
|
|
520
|
+
const targetCachePaths = await this.#validateCacheDeletionTargets(pluginId, targetEntries);
|
|
437
521
|
|
|
438
522
|
const updatedReg = removeInstalledPlugin(targetReg, pluginId);
|
|
439
523
|
await writeInstalledPluginsRegistry(registryPath, updatedReg);
|
|
@@ -445,11 +529,11 @@ export class MarketplaceManager {
|
|
|
445
529
|
? readInstalledPluginsRegistry(this.#opts.projectInstalledRegistryPath)
|
|
446
530
|
: Promise.resolve({ version: 2 as const, plugins: {} as Record<string, InstalledPluginEntry[]> }),
|
|
447
531
|
]);
|
|
448
|
-
const referenced =
|
|
532
|
+
const referenced = this.#collectReferencedCacheIdentities(freshUserReg, freshProjectReg);
|
|
449
533
|
|
|
450
|
-
for (const
|
|
451
|
-
if (!referenced.has(
|
|
452
|
-
await fs.rm(
|
|
534
|
+
for (const targetCachePath of targetCachePaths) {
|
|
535
|
+
if (!referenced.has(this.#cacheDeletionIdentity(targetCachePath))) {
|
|
536
|
+
await fs.rm(targetCachePath, { recursive: true, force: true });
|
|
453
537
|
}
|
|
454
538
|
}
|
|
455
539
|
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
} from "./deep-interview-ambiguity";
|
|
9
9
|
import {
|
|
10
10
|
answerHash,
|
|
11
|
+
assertDeepInterviewInputWithinLimit,
|
|
11
12
|
assertDeepInterviewIntentManifest,
|
|
13
|
+
assertDeepInterviewStructuredResponseWithinLimit,
|
|
12
14
|
createDeepInterviewIntentManifest,
|
|
13
15
|
type DeepInterviewEstablishedFact,
|
|
14
16
|
type DeepInterviewIntentItem,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
type DeepInterviewStateEnvelope,
|
|
18
20
|
type DeepInterviewTriggerMetadata,
|
|
19
21
|
deriveRoundKey,
|
|
22
|
+
MAX_USER_RESPONSE_LENGTH,
|
|
20
23
|
normalizeDeepInterviewEnvelope,
|
|
21
24
|
questionHash,
|
|
22
25
|
reviewDeepInterviewIntent,
|
|
@@ -426,6 +429,9 @@ export async function appendOrMergeDeepInterviewRound(
|
|
|
426
429
|
input: DeepInterviewAnswerInput,
|
|
427
430
|
options: { sessionId?: string } = {},
|
|
428
431
|
): Promise<{ action: AppendOrMergeAction; record: DeepInterviewRoundRecord; disputedFactIds?: string[] }> {
|
|
432
|
+
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
433
|
+
if (input.customInput !== undefined)
|
|
434
|
+
assertDeepInterviewInputWithinLimit(input.customInput, MAX_USER_RESPONSE_LENGTH, "user_response");
|
|
429
435
|
const envelope = await readEnvelope(statePath);
|
|
430
436
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
431
437
|
const shell = buildAnswerShell({
|
|
@@ -547,6 +553,7 @@ export async function enrichDeepInterviewRoundScoring(
|
|
|
547
553
|
input: DeepInterviewScoringInput,
|
|
548
554
|
options: { sessionId?: string } = {},
|
|
549
555
|
): Promise<{ record: DeepInterviewRoundRecord }> {
|
|
556
|
+
assertDeepInterviewStructuredResponseWithinLimit(input);
|
|
550
557
|
const envelope = await readEnvelope(statePath);
|
|
551
558
|
const interviewId = input.interviewId ?? interviewIdOf(envelope);
|
|
552
559
|
const rounds = readRounds(envelope);
|
|
@@ -7,11 +7,14 @@ import { syncSkillActiveState } from "../skill-state/active-state";
|
|
|
7
7
|
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
8
8
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
9
9
|
import {
|
|
10
|
+
assertDeepInterviewInputWithinLimit,
|
|
10
11
|
assertDeepInterviewIntentReview,
|
|
11
12
|
type DeepInterviewIntentCategory,
|
|
12
13
|
type DeepInterviewIntentItem,
|
|
13
14
|
type DeepInterviewIntentManifest,
|
|
14
15
|
type DeepInterviewIntentReview,
|
|
16
|
+
MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
|
|
17
|
+
MAX_INITIAL_CONTEXT_LENGTH,
|
|
15
18
|
normalizeDeepInterviewEnvelope,
|
|
16
19
|
reviewDeepInterviewIntent,
|
|
17
20
|
} from "./deep-interview-state";
|
|
@@ -559,6 +562,7 @@ async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): P
|
|
|
559
562
|
ideaParts.push(arg);
|
|
560
563
|
}
|
|
561
564
|
const idea = ideaParts.join(" ").trim();
|
|
565
|
+
assertDeepInterviewInputWithinLimit(idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
|
|
562
566
|
const effectiveResolution: DeepInterviewResolution = resolution ?? "standard";
|
|
563
567
|
const trace = hasFlag(args, "--trace") && idea ? await buildDeepInterviewTraceSummary(cwd, idea) : undefined;
|
|
564
568
|
return {
|
|
@@ -632,6 +636,11 @@ export async function persistDeepInterviewSpec(
|
|
|
632
636
|
cwd: string,
|
|
633
637
|
resolved: ResolvedDeepInterviewSpecWriteArgs,
|
|
634
638
|
): Promise<PersistedDeepInterviewSpec> {
|
|
639
|
+
assertDeepInterviewInputWithinLimit(
|
|
640
|
+
resolved.spec,
|
|
641
|
+
MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH,
|
|
642
|
+
"structured deep-interview response",
|
|
643
|
+
);
|
|
635
644
|
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
636
645
|
const existingRead = await readExistingStateForMutation(statePath);
|
|
637
646
|
if (existingRead.kind === "corrupt" && !resolved.force) {
|
|
@@ -731,6 +740,7 @@ export async function persistDeepInterviewSpec(
|
|
|
731
740
|
|
|
732
741
|
async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepInterviewArgs): Promise<string> {
|
|
733
742
|
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
743
|
+
assertDeepInterviewInputWithinLimit(resolved.idea, MAX_INITIAL_CONTEXT_LENGTH, "initial_idea");
|
|
734
744
|
const now = new Date().toISOString();
|
|
735
745
|
const payload: Record<string, unknown> = {
|
|
736
746
|
active: true,
|
|
@@ -397,6 +397,126 @@ const MAX_INTENT_STATEMENT_LENGTH = 1_000;
|
|
|
397
397
|
const MAX_INTENT_RATIONALE_LENGTH = 500;
|
|
398
398
|
const MAX_INTENT_EVIDENCE_LENGTH = 80;
|
|
399
399
|
|
|
400
|
+
// =============================================================================
|
|
401
|
+
// Input validation: free-text field allowlist + size limits (ouroboros parity)
|
|
402
|
+
// =============================================================================
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* User-input fields that legitimately carry prose (goals, prompts, descriptions,
|
|
406
|
+
* answers). Shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose
|
|
407
|
+
* here and must NOT be rejected as structural injection. Structural fields (ids,
|
|
408
|
+
* categories, hashes) stay strictly validated by their own guards.
|
|
409
|
+
*/
|
|
410
|
+
export const DEEP_INTERVIEW_FREETEXT_FIELDS: ReadonlySet<string> = new Set([
|
|
411
|
+
"initial_context",
|
|
412
|
+
"initial_idea",
|
|
413
|
+
"initial_context_summary",
|
|
414
|
+
"user_response",
|
|
415
|
+
"answer",
|
|
416
|
+
"goal",
|
|
417
|
+
"objective",
|
|
418
|
+
"prompt",
|
|
419
|
+
"description",
|
|
420
|
+
"statement",
|
|
421
|
+
"restated_goal",
|
|
422
|
+
"evidence",
|
|
423
|
+
"excerpt",
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
/** DoS-prevention character-count caps, matching ask-schema string-length semantics. */
|
|
427
|
+
export const MAX_INITIAL_CONTEXT_LENGTH = 50_000;
|
|
428
|
+
export const MAX_USER_RESPONSE_LENGTH = 10_000;
|
|
429
|
+
/** Maximum serialized JavaScript-string length for one LLM-produced structured response. */
|
|
430
|
+
export const MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH = 100_000;
|
|
431
|
+
|
|
432
|
+
/** Count Unicode code points without allocating an intermediate character array. */
|
|
433
|
+
export function deepInterviewCharacterCount(value: string): number {
|
|
434
|
+
let count = 0;
|
|
435
|
+
for (const _character of value) count++;
|
|
436
|
+
return count;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function isDeepInterviewFreeTextField(name: string): boolean {
|
|
440
|
+
return DEEP_INTERVIEW_FREETEXT_FIELDS.has(name);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Assert that one structured deep-interview response is JSON-serializable and
|
|
445
|
+
* bounded by JavaScript string length. This deliberately does not measure or
|
|
446
|
+
* cap accumulated persisted interview state.
|
|
447
|
+
*/
|
|
448
|
+
export function assertDeepInterviewStructuredResponseWithinLimit(value: unknown): void {
|
|
449
|
+
let serialized: string | undefined;
|
|
450
|
+
try {
|
|
451
|
+
serialized = JSON.stringify(value, (_key, nestedValue: unknown) => {
|
|
452
|
+
if (
|
|
453
|
+
typeof nestedValue === "bigint" ||
|
|
454
|
+
typeof nestedValue === "function" ||
|
|
455
|
+
typeof nestedValue === "symbol" ||
|
|
456
|
+
(typeof nestedValue === "number" && !Number.isFinite(nestedValue))
|
|
457
|
+
)
|
|
458
|
+
throw new Error("invalid structured deep-interview response");
|
|
459
|
+
return nestedValue;
|
|
460
|
+
});
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error("invalid structured deep-interview response");
|
|
463
|
+
}
|
|
464
|
+
if (typeof serialized !== "string") throw new Error("invalid structured deep-interview response");
|
|
465
|
+
if (deepInterviewCharacterCount(serialized) > MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH)
|
|
466
|
+
throw new Error(
|
|
467
|
+
`structured deep-interview response exceeds max length ${MAX_DEEP_INTERVIEW_STRUCTURED_RESPONSE_LENGTH}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Assert a free-text input is within its size cap. Never inspects content for shell
|
|
473
|
+
* metacharacters — free-text fields accept prose verbatim; this only bounds length.
|
|
474
|
+
*/
|
|
475
|
+
export function assertDeepInterviewInputWithinLimit(value: string, max: number, fieldName = "input"): void {
|
|
476
|
+
if (typeof value !== "string") throw new Error(`${fieldName} must be a string`);
|
|
477
|
+
if (deepInterviewCharacterCount(value) > max) throw new Error(`${fieldName} exceeds max length ${max}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Validate user-supplied deep-interview prose before an envelope is persisted. */
|
|
481
|
+
export function assertDeepInterviewEnvelopeInputLimits(envelope: Record<string, unknown>): void {
|
|
482
|
+
const state =
|
|
483
|
+
typeof envelope.state === "object" && envelope.state !== null && !Array.isArray(envelope.state)
|
|
484
|
+
? (envelope.state as Record<string, unknown>)
|
|
485
|
+
: {};
|
|
486
|
+
for (const field of ["initial_idea", "initial_context", "initial_context_summary"] as const) {
|
|
487
|
+
const nestedValue = state[field];
|
|
488
|
+
if (nestedValue !== undefined)
|
|
489
|
+
assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_INITIAL_CONTEXT_LENGTH, `state.${field}`);
|
|
490
|
+
const topLevelValue = envelope[field];
|
|
491
|
+
if (topLevelValue !== undefined)
|
|
492
|
+
assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_INITIAL_CONTEXT_LENGTH, field);
|
|
493
|
+
}
|
|
494
|
+
for (const field of ["user_response", "answer"] as const) {
|
|
495
|
+
const nestedValue = state[field];
|
|
496
|
+
if (nestedValue !== undefined)
|
|
497
|
+
assertDeepInterviewInputWithinLimit(nestedValue as string, MAX_USER_RESPONSE_LENGTH, `state.${field}`);
|
|
498
|
+
const topLevelValue = envelope[field];
|
|
499
|
+
if (topLevelValue !== undefined)
|
|
500
|
+
assertDeepInterviewInputWithinLimit(topLevelValue as string, MAX_USER_RESPONSE_LENGTH, field);
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(state.rounds)) return;
|
|
503
|
+
for (const [index, round] of state.rounds.entries()) {
|
|
504
|
+
if (typeof round !== "object" || round === null || Array.isArray(round)) continue;
|
|
505
|
+
const record = round as Record<string, unknown>;
|
|
506
|
+
for (const field of ["custom_input", "customInput", "user_response", "answer"] as const) {
|
|
507
|
+
const value = record[field];
|
|
508
|
+
// A structured scorer may use an unrelated object-valued `answer`; only prose
|
|
509
|
+
// values in a legacy answer slot are user input subject to this cap.
|
|
510
|
+
if (value === undefined || (field === "answer" && typeof value !== "string")) continue;
|
|
511
|
+
assertDeepInterviewInputWithinLimit(
|
|
512
|
+
value as string,
|
|
513
|
+
MAX_USER_RESPONSE_LENGTH,
|
|
514
|
+
`state.rounds[${index}].${field}`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
400
520
|
export interface DeepInterviewIntentItem {
|
|
401
521
|
id: string;
|
|
402
522
|
category: DeepInterviewIntentCategory;
|
|
@@ -437,7 +557,7 @@ export function isCanonicalDeepInterviewAnswerHash(value: unknown): value is str
|
|
|
437
557
|
function isEvidenceReference(value: unknown, answerHashValue: string): value is string {
|
|
438
558
|
return (
|
|
439
559
|
typeof value === "string" &&
|
|
440
|
-
value
|
|
560
|
+
deepInterviewCharacterCount(value) <= MAX_INTENT_EVIDENCE_LENGTH &&
|
|
441
561
|
value === `answer_hash:${answerHashValue}`
|
|
442
562
|
);
|
|
443
563
|
}
|
|
@@ -451,7 +571,11 @@ function assertIntentItem(value: unknown): asserts value is DeepInterviewIntentI
|
|
|
451
571
|
!id.startsWith(`${category}:`)
|
|
452
572
|
)
|
|
453
573
|
throw new Error("invalid intent category");
|
|
454
|
-
if (
|
|
574
|
+
if (
|
|
575
|
+
typeof statement !== "string" ||
|
|
576
|
+
!statement.trim() ||
|
|
577
|
+
deepInterviewCharacterCount(statement) > MAX_INTENT_STATEMENT_LENGTH
|
|
578
|
+
)
|
|
455
579
|
throw new Error(`intent item ${id} requires a bounded statement`);
|
|
456
580
|
}
|
|
457
581
|
|
|
@@ -541,7 +665,7 @@ export function reviewDeepInterviewIntent(
|
|
|
541
665
|
substitution.replacement_ids.some(id => typeof id !== "string" || !observedIds.has(id)) ||
|
|
542
666
|
typeof substitution.rationale !== "string" ||
|
|
543
667
|
!substitution.rationale.trim() ||
|
|
544
|
-
substitution.rationale
|
|
668
|
+
deepInterviewCharacterCount(substitution.rationale) > MAX_INTENT_RATIONALE_LENGTH
|
|
545
669
|
)
|
|
546
670
|
throw new Error("invalid intent substitution");
|
|
547
671
|
}
|
|
@@ -39,7 +39,11 @@ function renderRecord(record: GcRecord): string {
|
|
|
39
39
|
|
|
40
40
|
export function buildGcReportText(report: GcReport): string {
|
|
41
41
|
const lines: string[] = [];
|
|
42
|
-
|
|
42
|
+
if (report.operation === "repair_session_index") {
|
|
43
|
+
lines.push("gjc gc — session-index repair (other stores are report-only)");
|
|
44
|
+
} else {
|
|
45
|
+
lines.push(report.dry_run ? "gjc gc — dry run (no changes made; pass --prune to remove)" : "gjc gc — prune");
|
|
46
|
+
}
|
|
43
47
|
lines.push("");
|
|
44
48
|
|
|
45
49
|
for (const store of GC_STORES) {
|
|
@@ -53,6 +57,20 @@ export function buildGcReportText(report: GcReport): string {
|
|
|
53
57
|
lines.push("");
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
if (report.session_index) {
|
|
61
|
+
const index = report.session_index;
|
|
62
|
+
lines.push(`Session index: ${index.status}; valid prefix sequence=${index.valid_prefix_seq}`);
|
|
63
|
+
if (index.quarantine_path) lines.push(` Quarantined suffix: ${index.quarantine_path}`);
|
|
64
|
+
if (index.reason) lines.push(` ${index.reason}`);
|
|
65
|
+
if (index.status === "corrupt")
|
|
66
|
+
lines.push(" Run `gjc gc --repair-session-index` to quarantine the corrupt suffix.");
|
|
67
|
+
if (index.status === "unsupported")
|
|
68
|
+
lines.push(" Upgrade GJC before attempting a repair; no index data was changed.");
|
|
69
|
+
if (index.status === "repaired")
|
|
70
|
+
lines.push(" Restart or re-register hosts whose only registration was in the quarantined suffix.");
|
|
71
|
+
lines.push("");
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
if (report.errors.length > 0) {
|
|
57
75
|
lines.push(`Errors (${report.errors.length})`);
|
|
58
76
|
for (const err of report.errors) lines.push(` [${err.store}/${err.scope}] ${err.message}`);
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* - Dry-run by default: nothing is deleted unless `--prune`/`--force`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
17
|
+
import { SessionIndex } from "../sdk/broker/session-index";
|
|
18
|
+
import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
|
|
19
|
+
|
|
16
20
|
import { buildGcReportText } from "./gc-render";
|
|
17
21
|
|
|
18
22
|
export type GcStore = "harness_leases" | "team_workers" | "file_locks" | "tmux_sessions" | "registry_entries";
|
|
@@ -115,11 +119,21 @@ export interface GcCounts {
|
|
|
115
119
|
>;
|
|
116
120
|
}
|
|
117
121
|
|
|
122
|
+
export interface GcSessionIndexHealth {
|
|
123
|
+
status: "healthy" | "corrupt" | "repaired" | "unsupported" | "repair_failed";
|
|
124
|
+
valid_prefix_seq: number;
|
|
125
|
+
snapshot_seq?: number;
|
|
126
|
+
reason?: string;
|
|
127
|
+
quarantine_path?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
export interface GcReport {
|
|
119
131
|
dry_run: boolean;
|
|
132
|
+
operation?: "dry_run" | "prune" | "repair_session_index";
|
|
120
133
|
stores: Record<GcStore, GcRecord[]>;
|
|
121
134
|
counts: GcCounts;
|
|
122
135
|
errors: GcError[];
|
|
136
|
+
session_index?: GcSessionIndexHealth;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
export interface GcRunResult {
|
|
@@ -225,6 +239,7 @@ function computeCounts(stores: Record<GcStore, GcRecord[]>, errors: GcError[]):
|
|
|
225
239
|
interface ParsedGcArgs {
|
|
226
240
|
json: boolean;
|
|
227
241
|
prune: boolean;
|
|
242
|
+
repairSessionIndex: boolean;
|
|
228
243
|
help: boolean;
|
|
229
244
|
}
|
|
230
245
|
|
|
@@ -233,6 +248,8 @@ class GcUsageError extends Error {}
|
|
|
233
248
|
function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
234
249
|
let json = false;
|
|
235
250
|
let prune = false;
|
|
251
|
+
let repairSessionIndex = false;
|
|
252
|
+
|
|
236
253
|
let dryRun = false;
|
|
237
254
|
let help = false;
|
|
238
255
|
for (const arg of argv) {
|
|
@@ -245,6 +262,10 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
245
262
|
case "--force":
|
|
246
263
|
prune = true;
|
|
247
264
|
break;
|
|
265
|
+
case "--repair-session-index":
|
|
266
|
+
repairSessionIndex = true;
|
|
267
|
+
break;
|
|
268
|
+
|
|
248
269
|
case "--dry-run":
|
|
249
270
|
dryRun = true;
|
|
250
271
|
break;
|
|
@@ -256,9 +277,11 @@ function parseGcArgs(argv: string[]): ParsedGcArgs {
|
|
|
256
277
|
throw new GcUsageError(`unknown_flag:${arg}`);
|
|
257
278
|
}
|
|
258
279
|
}
|
|
280
|
+
if (repairSessionIndex && prune) throw new GcUsageError("repair_session_index_cannot_combine_with_prune");
|
|
281
|
+
if (repairSessionIndex && dryRun) throw new GcUsageError("repair_session_index_cannot_combine_with_dry_run");
|
|
259
282
|
// Explicit --dry-run always wins over --prune/--force.
|
|
260
283
|
if (dryRun) prune = false;
|
|
261
|
-
return { json, prune, help };
|
|
284
|
+
return { json, prune, repairSessionIndex, help };
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
/**
|
|
@@ -336,6 +359,40 @@ export function computeExitCode(report: GcReport): number {
|
|
|
336
359
|
return 0;
|
|
337
360
|
}
|
|
338
361
|
|
|
362
|
+
function resolveGcAgentDir(env: NodeJS.ProcessEnv): string {
|
|
363
|
+
return env.GJC_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || getAgentDir();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function collectSessionIndexHealth(repair: boolean, agentDir: string): Promise<GcSessionIndexHealth> {
|
|
367
|
+
const index = new SessionIndex(agentDir);
|
|
368
|
+
try {
|
|
369
|
+
if (repair) {
|
|
370
|
+
const result = await index.repair();
|
|
371
|
+
return {
|
|
372
|
+
status: result.status === "unsupported" ? "unsupported" : result.repaired ? "repaired" : "healthy",
|
|
373
|
+
valid_prefix_seq: result.validPrefixSeq,
|
|
374
|
+
snapshot_seq: result.snapshotSeq,
|
|
375
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
376
|
+
...(result.quarantinePath ? { quarantine_path: result.quarantinePath } : {}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const diagnosis = await index.diagnose();
|
|
380
|
+
return {
|
|
381
|
+
status: diagnosis.status,
|
|
382
|
+
valid_prefix_seq: diagnosis.validPrefixSeq,
|
|
383
|
+
snapshot_seq: diagnosis.snapshotSeq,
|
|
384
|
+
...(diagnosis.reason ? { reason: diagnosis.reason } : {}),
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
388
|
+
return {
|
|
389
|
+
status: error instanceof UnsupportedStateVersionError ? "unsupported" : "repair_failed",
|
|
390
|
+
valid_prefix_seq: 0,
|
|
391
|
+
reason,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
339
396
|
export async function runGjcGcCommand(
|
|
340
397
|
argv: string[],
|
|
341
398
|
cwd: string = process.cwd(),
|
|
@@ -357,7 +414,13 @@ export async function runGjcGcCommand(
|
|
|
357
414
|
const resolvedAdapters = adapters ?? (await defaultGcAdapters());
|
|
358
415
|
const ctx: GcContext = { probe: gcPidProbe, force: parsed.prune, env, cwd };
|
|
359
416
|
const report = await collectGcReport(resolvedAdapters, ctx, parsed.prune);
|
|
360
|
-
|
|
417
|
+
report.operation = parsed.repairSessionIndex ? "repair_session_index" : parsed.prune ? "prune" : "dry_run";
|
|
418
|
+
report.session_index = await collectSessionIndexHealth(parsed.repairSessionIndex, resolveGcAgentDir(env));
|
|
419
|
+
const sessionIndexFailed =
|
|
420
|
+
report.session_index?.status === "corrupt" ||
|
|
421
|
+
report.session_index?.status === "unsupported" ||
|
|
422
|
+
report.session_index?.status === "repair_failed";
|
|
423
|
+
const status = sessionIndexFailed ? 1 : computeExitCode(report);
|
|
361
424
|
const stdout = parsed.json ? `${JSON.stringify(report, null, 2)}\n` : buildGcReportText(report);
|
|
362
425
|
return { stdout, stderr: "", status };
|
|
363
426
|
}
|
|
@@ -367,12 +430,14 @@ export function gcHelpText(): string {
|
|
|
367
430
|
"gjc gc - garbage-collect stale GJC session/PID records",
|
|
368
431
|
"",
|
|
369
432
|
"USAGE",
|
|
370
|
-
" $ gjc gc [--prune|--force] [--json]",
|
|
433
|
+
" $ gjc gc [--prune|--force] [--repair-session-index] [--json]",
|
|
434
|
+
|
|
371
435
|
"",
|
|
372
436
|
"FLAGS",
|
|
373
437
|
" --prune, --force Actually remove stale records (default: dry-run report only)",
|
|
374
438
|
" --dry-run Force report-only mode (overrides --prune/--force)",
|
|
375
439
|
" -j, --json Emit machine-readable JSON",
|
|
440
|
+
" --repair-session-index Explicitly quarantine a corrupt session-index suffix and retain its valid prefix",
|
|
376
441
|
"",
|
|
377
442
|
"Liveness-only: a record is removed only when its owning process is dead",
|
|
378
443
|
"(ESRCH). Live / permission-denied / unknown processes are always kept.",
|