@gajae-code/coding-agent 0.5.0 → 0.5.1
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 +19 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent.d.ts +6 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profiles.ts +24 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +11 -0
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +25 -8
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/rpc/rpc-mode.ts +151 -33
- package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +17 -3
- package/src/session/agent-session.ts +77 -8
- package/src/session/blob-store.ts +59 -3
- package/src/session/session-manager.ts +4 -4
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +9 -0
- package/src/tools/ask.ts +56 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
} from "../skill-state/active-state";
|
|
12
12
|
import { initialPhaseForSkill } from "../skill-state/initial-phase";
|
|
13
13
|
import {
|
|
14
|
-
buildDeepInterviewHudSummary,
|
|
15
14
|
buildRalplanHudSummary,
|
|
16
15
|
buildTeamHudSummary,
|
|
17
16
|
buildUltragoalHudSummary,
|
|
17
|
+
deriveDeepInterviewHud,
|
|
18
18
|
} from "../skill-state/workflow-hud";
|
|
19
19
|
import {
|
|
20
20
|
type AuditEntry,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type WorkflowStateReceipt,
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
|
+
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
29
30
|
import { renderStateGraph, type StateGraphFormat } from "./state-graph";
|
|
30
31
|
import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
|
|
31
32
|
import {
|
|
@@ -833,31 +834,9 @@ function buildHudForMode(
|
|
|
833
834
|
): WorkflowHudSummary | undefined {
|
|
834
835
|
const updatedAt = new Date().toISOString();
|
|
835
836
|
const phase = typeof payload.current_phase === "string" ? payload.current_phase : undefined;
|
|
836
|
-
const stateField = isPlainObject(payload.state) ? (payload.state as Record<string, unknown>) : {};
|
|
837
837
|
switch (mode) {
|
|
838
|
-
case "deep-interview":
|
|
839
|
-
|
|
840
|
-
const v = (stateField as Record<string, unknown>)[key] ?? (payload as Record<string, unknown>)[key];
|
|
841
|
-
return guard(v) ? v : undefined;
|
|
842
|
-
};
|
|
843
|
-
const isNumber = (v: unknown): v is number => typeof v === "number";
|
|
844
|
-
const isString = (v: unknown): v is string => typeof v === "string";
|
|
845
|
-
const isArray = (v: unknown): v is unknown[] => Array.isArray(v);
|
|
846
|
-
const ambiguity = pick("current_ambiguity", isNumber);
|
|
847
|
-
const threshold = pick("threshold", isNumber);
|
|
848
|
-
const rounds = pick("rounds", isArray);
|
|
849
|
-
const targetComponent = pick("last_targeted_component_id", isString);
|
|
850
|
-
const weakestDimension = pick("weakest_dimension", isString);
|
|
851
|
-
return buildDeepInterviewHudSummary({
|
|
852
|
-
phase,
|
|
853
|
-
ambiguity,
|
|
854
|
-
threshold,
|
|
855
|
-
roundCount: rounds?.length,
|
|
856
|
-
targetComponent,
|
|
857
|
-
weakestDimension,
|
|
858
|
-
updatedAt,
|
|
859
|
-
});
|
|
860
|
-
}
|
|
838
|
+
case "deep-interview":
|
|
839
|
+
return deriveDeepInterviewHud(payload, { updatedAt });
|
|
861
840
|
case "ralplan": {
|
|
862
841
|
const stage =
|
|
863
842
|
typeof payload.current_phase === "string"
|
|
@@ -888,6 +867,24 @@ function buildHudForMode(
|
|
|
888
867
|
counts[status] = (counts[status] ?? 0) + 1;
|
|
889
868
|
}
|
|
890
869
|
const currentGoalRaw = goals.find(g => g.status === "active") ?? goals.find(g => g.status === "pending");
|
|
870
|
+
const rawLedger = payload.latestLedgerEvent;
|
|
871
|
+
const latestLedgerEvent =
|
|
872
|
+
rawLedger && typeof rawLedger === "object" && !Array.isArray(rawLedger)
|
|
873
|
+
? {
|
|
874
|
+
event:
|
|
875
|
+
typeof (rawLedger as Record<string, unknown>).event === "string"
|
|
876
|
+
? ((rawLedger as Record<string, unknown>).event as string)
|
|
877
|
+
: undefined,
|
|
878
|
+
goalId:
|
|
879
|
+
typeof (rawLedger as Record<string, unknown>).goalId === "string"
|
|
880
|
+
? ((rawLedger as Record<string, unknown>).goalId as string)
|
|
881
|
+
: undefined,
|
|
882
|
+
timestamp:
|
|
883
|
+
typeof (rawLedger as Record<string, unknown>).timestamp === "string"
|
|
884
|
+
? ((rawLedger as Record<string, unknown>).timestamp as string)
|
|
885
|
+
: undefined,
|
|
886
|
+
}
|
|
887
|
+
: undefined;
|
|
891
888
|
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
892
889
|
return buildUltragoalHudSummary({
|
|
893
890
|
status,
|
|
@@ -900,6 +897,7 @@ function buildHudForMode(
|
|
|
900
897
|
: undefined,
|
|
901
898
|
counts,
|
|
902
899
|
goals: goals.map(g => ({ id: g.id as string, title: g.title as string, status: g.status as string })),
|
|
900
|
+
latestLedgerEvent,
|
|
903
901
|
updatedAt,
|
|
904
902
|
});
|
|
905
903
|
}
|
|
@@ -1009,7 +1007,10 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1009
1007
|
receipt.from_phase = fromPhase;
|
|
1010
1008
|
receipt.to_phase = trimmedPhase;
|
|
1011
1009
|
|
|
1012
|
-
const merged =
|
|
1010
|
+
const merged =
|
|
1011
|
+
mode === "deep-interview"
|
|
1012
|
+
? (mergeDeepInterviewEnvelope(existingPayload, payload) as Record<string, unknown>)
|
|
1013
|
+
: mergeWithNullDelete(existingPayload, payload);
|
|
1013
1014
|
merged.skill = mode;
|
|
1014
1015
|
merged.current_phase = trimmedPhase;
|
|
1015
1016
|
merged.active = active;
|
|
@@ -1173,7 +1174,11 @@ async function handleWrite(
|
|
|
1173
1174
|
? (innerState.current_phase as string).trim()
|
|
1174
1175
|
: undefined;
|
|
1175
1176
|
let merged: Record<string, unknown>;
|
|
1176
|
-
if (
|
|
1177
|
+
if (mode === "deep-interview") {
|
|
1178
|
+
// Deep-interview keeps interview data nested under `state` and merges rounds
|
|
1179
|
+
// losslessly by durable key; never flatten or delete `state` (that drops recorder history).
|
|
1180
|
+
merged = mergeDeepInterviewEnvelope(existingPayload, payload, { replace: hasFlag(args, "--replace") });
|
|
1181
|
+
} else if (hasFlag(args, "--replace")) {
|
|
1177
1182
|
merged = { ...payload };
|
|
1178
1183
|
} else {
|
|
1179
1184
|
merged = mergeWithNullDelete(existingPayload, payload);
|
|
@@ -1423,8 +1428,20 @@ async function handleHandoff(
|
|
|
1423
1428
|
});
|
|
1424
1429
|
|
|
1425
1430
|
const calleeInitial = initialPhaseForSkill(callee);
|
|
1426
|
-
const normalizedCaller =
|
|
1427
|
-
|
|
1431
|
+
const normalizedCaller =
|
|
1432
|
+
caller === "deep-interview"
|
|
1433
|
+
? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCaller, caller).state) as Record<
|
|
1434
|
+
string,
|
|
1435
|
+
unknown
|
|
1436
|
+
>)
|
|
1437
|
+
: migrateWorkflowState(existingCaller, caller).state;
|
|
1438
|
+
const normalizedCallee =
|
|
1439
|
+
callee === "deep-interview"
|
|
1440
|
+
? (normalizeDeepInterviewEnvelope(migrateWorkflowState(existingCallee, callee).state) as Record<
|
|
1441
|
+
string,
|
|
1442
|
+
unknown
|
|
1443
|
+
>)
|
|
1444
|
+
: migrateWorkflowState(existingCallee, callee).state;
|
|
1428
1445
|
const mergedCalleeState: Record<string, unknown> = {
|
|
1429
1446
|
...normalizedCallee,
|
|
1430
1447
|
skill: callee,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GC adapter for team workers (`.gjc/state/team/<name>/workers/<id>/` heartbeat
|
|
3
|
+
* + lifecycle). Liveness-only: numeric PID status dominates lifecycle/heartbeat
|
|
4
|
+
* signals.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { listHarnessRootRegistriesForGc } from "../harness-control-plane/storage";
|
|
9
|
+
import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
|
|
10
|
+
import { listTeamWorkerGcRecords, pruneTeamWorkerGcRecord } from "./team-runtime";
|
|
11
|
+
|
|
12
|
+
function uniqueTeamRootsFromHarnessRoots(roots: string[]): string[] {
|
|
13
|
+
return [...new Set(roots.map(root => path.join(path.dirname(root), "team")))].sort();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const teamWorkersGcAdapter: GcStoreAdapter = {
|
|
17
|
+
store: "team_workers",
|
|
18
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
19
|
+
const records: GcRecord[] = [];
|
|
20
|
+
const errors: GcCollectResult["errors"] = [];
|
|
21
|
+
const registries = await listHarnessRootRegistriesForGc(ctx.env);
|
|
22
|
+
for (const registry of registries) {
|
|
23
|
+
if (registry.error) errors.push({ store: "team_workers", scope: registry.file, message: registry.error });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const teamRoots = uniqueTeamRootsFromHarnessRoots(
|
|
27
|
+
registries.flatMap(registry => registry.roots.map(entry => entry.root)),
|
|
28
|
+
);
|
|
29
|
+
for (const teamRoot of teamRoots) {
|
|
30
|
+
try {
|
|
31
|
+
records.push(...(await listTeamWorkerGcRecords(teamRoot, ctx.probe)));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
34
|
+
if (code === "ENOENT") continue;
|
|
35
|
+
errors.push({ store: "team_workers", scope: teamRoot, message: (error as Error).message });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { records, errors };
|
|
40
|
+
},
|
|
41
|
+
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
42
|
+
try {
|
|
43
|
+
const removed = await pruneTeamWorkerGcRecord(record, ctx.probe);
|
|
44
|
+
return removed ? { removed: true } : { removed: false, skipped: "worker_no_longer_dead" };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { removed: false, error: (error as Error).message };
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
5
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
|
+
import type { GcPidProbe, GcRecord } from "./gc-runtime";
|
|
7
8
|
|
|
8
9
|
import { applyGjcTmuxProfile, GJC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
|
|
9
10
|
import {
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
writeWorkflowEnvelopeAtomic,
|
|
19
20
|
} from "./state-writer";
|
|
20
21
|
import {
|
|
22
|
+
buildGjcTmuxExactOptionTarget,
|
|
21
23
|
buildGjcTmuxUntaggedSessionHint,
|
|
22
24
|
GJC_TMUX_PROFILE_OPTION,
|
|
23
25
|
GJC_TMUX_PROFILE_VALUE,
|
|
@@ -677,6 +679,174 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
677
679
|
throw error;
|
|
678
680
|
}
|
|
679
681
|
}
|
|
682
|
+
function isPositivePid(value: unknown): value is number {
|
|
683
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function collectTeamGcWorkerPids(
|
|
687
|
+
heartbeat: WorkerHeartbeatFile | null,
|
|
688
|
+
lifecycle: GjcTeamWorkerLifecycle | null,
|
|
689
|
+
): number[] {
|
|
690
|
+
const pids: number[] = [];
|
|
691
|
+
if (isPositivePid(heartbeat?.pid)) pids.push(heartbeat.pid);
|
|
692
|
+
if (isPositivePid(lifecycle?.pid) && !pids.includes(lifecycle.pid)) pids.push(lifecycle.pid);
|
|
693
|
+
return pids;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
interface TeamGcPidClassification {
|
|
697
|
+
removable: boolean;
|
|
698
|
+
pidStatus: "dead" | "alive" | "eperm" | "unknown" | "none";
|
|
699
|
+
pid?: number;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Liveness-only, fail-closed: a worker is removable ONLY when it has at least
|
|
704
|
+
* one authoritative pid and EVERY candidate pid probes dead (ESRCH). Any alive,
|
|
705
|
+
* EPERM, or unknown candidate (heartbeat OR lifecycle) keeps the worker, so a
|
|
706
|
+
* dead heartbeat pid can never override a live lifecycle pid.
|
|
707
|
+
*/
|
|
708
|
+
function classifyTeamGcWorkerPids(pids: number[], probe: GcPidProbe): TeamGcPidClassification {
|
|
709
|
+
if (pids.length === 0) return { removable: false, pidStatus: "none" };
|
|
710
|
+
const statuses = pids.map(pid => ({ pid, status: gcProbeStatus(probe, pid) }));
|
|
711
|
+
const kept = statuses.find(entry => entry.status !== "dead");
|
|
712
|
+
if (kept) return { removable: false, pidStatus: kept.status, pid: kept.pid };
|
|
713
|
+
return { removable: true, pidStatus: "dead", pid: statuses[0]?.pid };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function gcProbeStatus(probe: GcPidProbe, pid: number): "dead" | "alive" | "eperm" | "unknown" {
|
|
717
|
+
const result = probe(pid);
|
|
718
|
+
if (result.status === "dead") return "dead";
|
|
719
|
+
return result.reason ?? "unknown";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function teamGcRecordDetail(heartbeat: WorkerHeartbeatFile | null, lifecycle: GjcTeamWorkerLifecycle | null): string {
|
|
723
|
+
return [
|
|
724
|
+
`heartbeat=${heartbeat ? "present" : "missing"}`,
|
|
725
|
+
...(heartbeat ? [`heartbeat_alive=${heartbeat.alive}`, `last_turn_at=${heartbeat.last_turn_at}`] : []),
|
|
726
|
+
`lifecycle=${lifecycle?.lifecycle_state ?? "missing"}`,
|
|
727
|
+
...(lifecycle?.pane_id ? [`pane_id=${lifecycle.pane_id}`] : []),
|
|
728
|
+
...(lifecycle?.stop_reason ? [`stop_reason=${lifecycle.stop_reason}`] : []),
|
|
729
|
+
].join(" ");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** @internal */
|
|
733
|
+
export async function listTeamWorkerGcRecords(teamRoot: string, probe: GcPidProbe): Promise<GcRecord[]> {
|
|
734
|
+
const teamEntries = await fs.readdir(teamRoot, { withFileTypes: true });
|
|
735
|
+
const records: GcRecord[] = [];
|
|
736
|
+
for (const teamEntry of teamEntries) {
|
|
737
|
+
if (!teamEntry.isDirectory()) continue;
|
|
738
|
+
const teamName = teamEntry.name;
|
|
739
|
+
const teamDirPath = path.join(teamRoot, teamName);
|
|
740
|
+
let workerEntries: import("node:fs").Dirent[];
|
|
741
|
+
try {
|
|
742
|
+
workerEntries = await fs.readdir(path.join(teamDirPath, "workers"), { withFileTypes: true });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (isEnoent(error)) continue;
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const workerEntry of workerEntries) {
|
|
749
|
+
if (!workerEntry.isDirectory()) continue;
|
|
750
|
+
const workerId = workerEntry.name;
|
|
751
|
+
const dir = path.join(teamDirPath, "workers", workerId);
|
|
752
|
+
let heartbeat: WorkerHeartbeatFile | null = null;
|
|
753
|
+
let lifecycle: GjcTeamWorkerLifecycle | null = null;
|
|
754
|
+
try {
|
|
755
|
+
heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(dir, "heartbeat.json"));
|
|
756
|
+
lifecycle = await readJsonFile<GjcTeamWorkerLifecycle>(path.join(dir, "lifecycle.json"));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
records.push({
|
|
759
|
+
store: "team_workers",
|
|
760
|
+
id: `${teamName}/${workerId}`,
|
|
761
|
+
root: teamRoot,
|
|
762
|
+
path: dir,
|
|
763
|
+
pid_status: "none",
|
|
764
|
+
status: "malformed",
|
|
765
|
+
stale: false,
|
|
766
|
+
removable: false,
|
|
767
|
+
action: "none",
|
|
768
|
+
reason: "worker_state_malformed_kept",
|
|
769
|
+
error: error instanceof Error ? error.message : String(error),
|
|
770
|
+
});
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const pids = collectTeamGcWorkerPids(heartbeat, lifecycle);
|
|
774
|
+
const { removable, pidStatus, pid } = classifyTeamGcWorkerPids(pids, probe);
|
|
775
|
+
const terminalLifecycle = lifecycle?.lifecycle_state === "failed" || lifecycle?.lifecycle_state === "stopped";
|
|
776
|
+
const status = removable
|
|
777
|
+
? "dead"
|
|
778
|
+
: pidStatus === "none" && terminalLifecycle
|
|
779
|
+
? "terminal_lifecycle"
|
|
780
|
+
: pidStatus === "none"
|
|
781
|
+
? "no_pid"
|
|
782
|
+
: pidStatus;
|
|
783
|
+
records.push({
|
|
784
|
+
store: "team_workers",
|
|
785
|
+
id: `${teamName}/${workerId}`,
|
|
786
|
+
root: teamRoot,
|
|
787
|
+
path: dir,
|
|
788
|
+
pid,
|
|
789
|
+
pid_status: pidStatus,
|
|
790
|
+
status,
|
|
791
|
+
stale: removable,
|
|
792
|
+
removable,
|
|
793
|
+
action: "none",
|
|
794
|
+
reason: removable
|
|
795
|
+
? "worker_all_pids_dead"
|
|
796
|
+
: pidStatus === "none" && terminalLifecycle
|
|
797
|
+
? "terminal_lifecycle_without_pid_kept"
|
|
798
|
+
: pidStatus === "none"
|
|
799
|
+
? "worker_pid_missing_kept"
|
|
800
|
+
: `worker_pid_${pidStatus}_kept`,
|
|
801
|
+
detail: teamGcRecordDetail(heartbeat, lifecycle),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return records;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** @internal */
|
|
809
|
+
export async function pruneTeamWorkerGcRecord(record: GcRecord, probe: GcPidProbe): Promise<boolean> {
|
|
810
|
+
if (!record.path || !record.id.includes("/")) return false;
|
|
811
|
+
const [teamName, workerId] = record.id.split("/", 2);
|
|
812
|
+
if (!teamName || !workerId) return false;
|
|
813
|
+
const teamDirPath = path.dirname(path.dirname(record.path));
|
|
814
|
+
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(record.path, "heartbeat.json"));
|
|
815
|
+
const lifecycle = await readJsonFile<GjcTeamWorkerLifecycle>(path.join(record.path, "lifecycle.json"));
|
|
816
|
+
const pids = collectTeamGcWorkerPids(heartbeat, lifecycle);
|
|
817
|
+
if (!classifyTeamGcWorkerPids(pids, probe).removable) return false;
|
|
818
|
+
|
|
819
|
+
const claimDir = path.join(teamDirPath, "claims");
|
|
820
|
+
try {
|
|
821
|
+
for (const entry of await fs.readdir(claimDir, { withFileTypes: true })) {
|
|
822
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
823
|
+
const claimPath = path.join(claimDir, entry.name);
|
|
824
|
+
const claim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
825
|
+
if (claim?.owner !== workerId) continue;
|
|
826
|
+
await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "gc-team-worker"));
|
|
827
|
+
}
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (!isEnoent(error)) throw error;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
for (const task of await readTasks(teamDirPath)) {
|
|
833
|
+
if (task.claim?.owner !== workerId && task.assignee !== workerId) continue;
|
|
834
|
+
if (task.status === "completed" || task.status === "failed") continue;
|
|
835
|
+
await writeTask(teamDirPath, {
|
|
836
|
+
...task,
|
|
837
|
+
status: "pending",
|
|
838
|
+
assignee: undefined,
|
|
839
|
+
claim: undefined,
|
|
840
|
+
version: task.version + 1,
|
|
841
|
+
updated_at: now(),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Remove the stale worker record dir itself so a removable record always
|
|
846
|
+
// results in an observable removal, even when it owns no claims/tasks.
|
|
847
|
+
await fs.rm(record.path, { recursive: true, force: true });
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
680
850
|
function stateCategoryForJsonPath(filePath: string): "state" | "ledger" {
|
|
681
851
|
return filePath.endsWith(".jsonl") || filePath.includes(`${path.sep}telemetry${path.sep}`) ? "ledger" : "state";
|
|
682
852
|
}
|
|
@@ -1633,7 +1803,7 @@ function buildTeamTmuxLeaderRequirementMessage(detail?: string): string {
|
|
|
1633
1803
|
}
|
|
1634
1804
|
function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): string {
|
|
1635
1805
|
const result = Bun.spawnSync(
|
|
1636
|
-
[tmuxCommand, "show-options", "-qv", "-t",
|
|
1806
|
+
[tmuxCommand, "show-options", "-qv", "-t", buildGjcTmuxExactOptionTarget(sessionName), GJC_TMUX_PROFILE_OPTION],
|
|
1637
1807
|
{
|
|
1638
1808
|
stdout: "pipe",
|
|
1639
1809
|
stderr: "pipe",
|
|
@@ -1645,7 +1815,14 @@ function readGjcTmuxProfileValue(tmuxCommand: string, sessionName: string): stri
|
|
|
1645
1815
|
|
|
1646
1816
|
function retagGjcLaunchedTmuxSession(tmuxCommand: string, sessionName: string): boolean {
|
|
1647
1817
|
const result = Bun.spawnSync(
|
|
1648
|
-
[
|
|
1818
|
+
[
|
|
1819
|
+
tmuxCommand,
|
|
1820
|
+
"set-option",
|
|
1821
|
+
"-t",
|
|
1822
|
+
buildGjcTmuxExactOptionTarget(sessionName),
|
|
1823
|
+
GJC_TMUX_PROFILE_OPTION,
|
|
1824
|
+
GJC_TMUX_PROFILE_VALUE,
|
|
1825
|
+
],
|
|
1649
1826
|
{
|
|
1650
1827
|
stdout: "pipe",
|
|
1651
1828
|
stderr: "pipe",
|
|
@@ -32,6 +32,20 @@ export function resolveGjcTmuxCommand(env: NodeJS.ProcessEnv = process.env): str
|
|
|
32
32
|
return env[GJC_TMUX_COMMAND_ENV]?.trim() || env.GJC_TEAM_TMUX_COMMAND?.trim() || "tmux";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build the exact-session target for tmux *option* commands
|
|
37
|
+
* (`show-options` / `set-option`) and `display-message -t`.
|
|
38
|
+
*
|
|
39
|
+
* Session-scoped commands such as `kill-session` / `attach-session` resolve a
|
|
40
|
+
* bare exact target (`=NAME`), but tmux 3.6a refuses to resolve a bare `=NAME`
|
|
41
|
+
* for option/display commands. Appending the empty window separator (`=NAME:`)
|
|
42
|
+
* keeps the exact-session match while giving tmux the window-qualified target
|
|
43
|
+
* those commands require. See gajae-code#580.
|
|
44
|
+
*/
|
|
45
|
+
export function buildGjcTmuxExactOptionTarget(sessionName: string): string {
|
|
46
|
+
return `=${sessionName}:`;
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
export const GJC_TMUX_UNTAGGED_REASON = "gjc_tmux_session_untagged";
|
|
36
50
|
|
|
37
51
|
export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GC adapter for gjc-tagged tmux sessions. Stale iff `@gjc-project` path is gone
|
|
3
|
+
* OR `@gjc-branch` has no live git worktree. Removal is a spec-authorized
|
|
4
|
+
* destructive `kill-session`, gated by exact-target re-read + revalidation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
import { worktree } from "../utils/git";
|
|
10
|
+
import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
|
|
11
|
+
import { GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
|
|
12
|
+
import {
|
|
13
|
+
type GjcTmuxSessionStatus,
|
|
14
|
+
listTmuxSessionsForGc,
|
|
15
|
+
readTmuxSessionTagsForGc,
|
|
16
|
+
removeGjcTmuxSession,
|
|
17
|
+
} from "./tmux-sessions";
|
|
18
|
+
|
|
19
|
+
const STORE = "tmux_sessions" as const;
|
|
20
|
+
const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
|
|
21
|
+
|
|
22
|
+
function pathExists(path: string): boolean {
|
|
23
|
+
try {
|
|
24
|
+
return fs.existsSync(path);
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detail(project?: string, branch?: string): string | undefined {
|
|
31
|
+
const parts = [];
|
|
32
|
+
if (project) parts.push(`project=${project}`);
|
|
33
|
+
if (branch) parts.push(`branch=${branch}`);
|
|
34
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unclassifiedRecord(id: string, reason: string, project?: string, branch?: string): GcRecord {
|
|
38
|
+
return {
|
|
39
|
+
store: STORE,
|
|
40
|
+
id,
|
|
41
|
+
path: project,
|
|
42
|
+
root: project,
|
|
43
|
+
pid_status: "none",
|
|
44
|
+
status: "unclassified",
|
|
45
|
+
stale: false,
|
|
46
|
+
removable: false,
|
|
47
|
+
action: "none",
|
|
48
|
+
reason,
|
|
49
|
+
detail: detail(project, branch),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function branchMatches(candidate: string | undefined, branch: string): boolean {
|
|
54
|
+
if (!candidate) return false;
|
|
55
|
+
const branchNames = new Set([
|
|
56
|
+
branch,
|
|
57
|
+
branch.startsWith("refs/heads/") ? branch.slice("refs/heads/".length) : `refs/heads/${branch}`,
|
|
58
|
+
]);
|
|
59
|
+
return branchNames.has(candidate);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function hasLiveWorktreeForBranch(project: string, branch: string): Promise<boolean> {
|
|
63
|
+
const entries = await worktree.list(project);
|
|
64
|
+
return entries.some(entry => branchMatches(entry.branch, branch));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
|
|
68
|
+
const { name, project, branch } = session;
|
|
69
|
+
if (!project || !branch) return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
|
|
70
|
+
if (!pathExists(project)) {
|
|
71
|
+
return {
|
|
72
|
+
store: STORE,
|
|
73
|
+
id: name,
|
|
74
|
+
path: project,
|
|
75
|
+
root: project,
|
|
76
|
+
pid_status: "none",
|
|
77
|
+
status: "stale",
|
|
78
|
+
stale: true,
|
|
79
|
+
removable: true,
|
|
80
|
+
action: "none",
|
|
81
|
+
reason: "project_missing",
|
|
82
|
+
detail: detail(project, branch),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!(await hasLiveWorktreeForBranch(project, branch))) {
|
|
86
|
+
return {
|
|
87
|
+
store: STORE,
|
|
88
|
+
id: name,
|
|
89
|
+
path: project,
|
|
90
|
+
root: project,
|
|
91
|
+
pid_status: "none",
|
|
92
|
+
status: "stale",
|
|
93
|
+
stale: true,
|
|
94
|
+
removable: true,
|
|
95
|
+
action: "none",
|
|
96
|
+
reason: "branch_no_worktree",
|
|
97
|
+
detail: detail(project, branch),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
store: STORE,
|
|
102
|
+
id: name,
|
|
103
|
+
path: project,
|
|
104
|
+
root: project,
|
|
105
|
+
pid_status: "none",
|
|
106
|
+
status: "live",
|
|
107
|
+
stale: false,
|
|
108
|
+
removable: false,
|
|
109
|
+
action: "none",
|
|
110
|
+
reason: "project_and_branch_worktree_present",
|
|
111
|
+
detail: detail(project, branch),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function revalidateRemovable(name: string, env: NodeJS.ProcessEnv): Promise<boolean> {
|
|
116
|
+
const tags = readTmuxSessionTagsForGc(name, env);
|
|
117
|
+
if (tags.profile !== GJC_TMUX_PROFILE_VALUE || !tags.project || !tags.branch) return false;
|
|
118
|
+
if (!pathExists(tags.project)) return true;
|
|
119
|
+
return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
123
|
+
store: STORE,
|
|
124
|
+
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
125
|
+
const records: GcRecord[] = [];
|
|
126
|
+
const errors: GcCollectResult["errors"] = [];
|
|
127
|
+
let sessions: ReturnType<typeof listTmuxSessionsForGc>;
|
|
128
|
+
try {
|
|
129
|
+
sessions = listTmuxSessionsForGc(ctx.env);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
records,
|
|
133
|
+
errors: [
|
|
134
|
+
{
|
|
135
|
+
store: STORE,
|
|
136
|
+
scope: "list_sessions",
|
|
137
|
+
message: error instanceof Error ? error.message : String(error),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const session of sessions.tagged) {
|
|
144
|
+
try {
|
|
145
|
+
records.push(await classifyTaggedSession(session));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
errors.push({
|
|
148
|
+
store: STORE,
|
|
149
|
+
scope: session.name,
|
|
150
|
+
message: error instanceof Error ? error.message : String(error),
|
|
151
|
+
});
|
|
152
|
+
records.push(unclassifiedRecord(session.name, "worktree_list_failed", session.project, session.branch));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const name of sessions.untagged) {
|
|
157
|
+
records.push(unclassifiedRecord(name, "untagged_tmux_session"));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { records, errors };
|
|
161
|
+
},
|
|
162
|
+
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
163
|
+
if (record.store !== STORE || record.status !== "stale" || !record.removable) {
|
|
164
|
+
return { removed: false, skipped: "not_removable_tmux_session" };
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
if (!(await revalidateRemovable(record.id, ctx.env))) {
|
|
168
|
+
return { removed: false, skipped: TOCTOU_SKIP };
|
|
169
|
+
}
|
|
170
|
+
removeGjcTmuxSession(record.id, ctx.env);
|
|
171
|
+
return { removed: true };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { removed: false, error: error instanceof Error ? error.message : String(error) };
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildGjcTmuxExactOptionTarget,
|
|
2
3
|
buildGjcTmuxProfileCommands,
|
|
3
4
|
buildGjcTmuxSessionName,
|
|
4
5
|
buildGjcTmuxUntaggedSessionError,
|
|
@@ -23,6 +24,17 @@ export interface GjcTmuxSessionStatus {
|
|
|
23
24
|
project?: string;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
export interface GjcTmuxSessionTagsForGc {
|
|
28
|
+
profile?: string;
|
|
29
|
+
project?: string;
|
|
30
|
+
branch?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GjcTmuxSessionsForGc {
|
|
34
|
+
tagged: GjcTmuxSessionStatus[];
|
|
35
|
+
untagged: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
|
|
27
39
|
const tmuxCommand = resolveGjcTmuxCommand(env);
|
|
28
40
|
const result = Bun.spawnSync([tmuxCommand, ...args], { stdout: "pipe", stderr: "pipe", env });
|
|
@@ -107,6 +119,16 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
|
|
|
107
119
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
/** @internal */
|
|
123
|
+
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
124
|
+
const tagged = listGjcTmuxSessions(env);
|
|
125
|
+
const taggedNames = new Set(tagged.map(session => session.name));
|
|
126
|
+
const untagged = listRawTmuxSessionNames(env)
|
|
127
|
+
.filter(name => !taggedNames.has(name))
|
|
128
|
+
.sort((a, b) => a.localeCompare(b));
|
|
129
|
+
return { tagged, untagged };
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
export function findGjcTmuxSessionByBranch(
|
|
111
133
|
branch: string,
|
|
112
134
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -148,7 +170,33 @@ export function createGjcTmuxSession(env: NodeJS.ProcessEnv = process.env): GjcT
|
|
|
148
170
|
}
|
|
149
171
|
|
|
150
172
|
function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv): string {
|
|
151
|
-
return runTmux(
|
|
173
|
+
return runTmux(
|
|
174
|
+
["show-options", "-qv", "-t", buildGjcTmuxExactOptionTarget(sessionName), GJC_TMUX_PROFILE_OPTION],
|
|
175
|
+
env,
|
|
176
|
+
).trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
180
|
+
try {
|
|
181
|
+
return (
|
|
182
|
+
runTmux(["show-options", "-qv", "-t", buildGjcTmuxExactOptionTarget(sessionName), option], env).trim() ||
|
|
183
|
+
undefined
|
|
184
|
+
);
|
|
185
|
+
} catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @internal */
|
|
191
|
+
export function readTmuxSessionTagsForGc(
|
|
192
|
+
sessionName: string,
|
|
193
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
194
|
+
): GjcTmuxSessionTagsForGc {
|
|
195
|
+
return {
|
|
196
|
+
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, env),
|
|
197
|
+
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
|
|
198
|
+
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
|
|
199
|
+
};
|
|
152
200
|
}
|
|
153
201
|
|
|
154
202
|
export function removeGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
|