@gajae-code/coding-agent 0.5.1 → 0.5.3
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 +31 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +17 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/session/streaming-output.d.ts +5 -0
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +153 -39
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +63 -13
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +88 -38
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +14 -10
- package/src/lsp/client.ts +64 -26
- package/src/lsp/defaults.json +1 -0
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +23 -1
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +60 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +5 -0
- package/src/modes/controllers/selector-controller.ts +86 -2
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +5 -2
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +39 -1
- package/src/session/agent-session.ts +190 -33
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +99 -31
- package/src/session/streaming-output.ts +54 -3
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +1 -1
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -8,9 +8,10 @@ import * as fs from "node:fs";
|
|
|
8
8
|
|
|
9
9
|
import { worktree } from "../utils/git";
|
|
10
10
|
import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
|
|
11
|
-
import { GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
|
|
11
|
+
import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX } from "./tmux-common";
|
|
12
12
|
import {
|
|
13
13
|
type GjcTmuxSessionStatus,
|
|
14
|
+
type GjcTmuxSessionsForGc,
|
|
14
15
|
listTmuxSessionsForGc,
|
|
15
16
|
readTmuxSessionTagsForGc,
|
|
16
17
|
removeGjcTmuxSession,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
|
|
19
20
|
const STORE = "tmux_sessions" as const;
|
|
20
21
|
const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
|
|
22
|
+
const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
21
23
|
|
|
22
24
|
function pathExists(path: string): boolean {
|
|
23
25
|
try {
|
|
@@ -64,39 +66,62 @@ async function hasLiveWorktreeForBranch(project: string, branch: string): Promis
|
|
|
64
66
|
return entries.some(entry => branchMatches(entry.branch, branch));
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function isSessionLive(session: Pick<GjcTmuxSessionStatus, "attached" | "panePids">): boolean {
|
|
70
|
+
return session.attached || session.panePids.length > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function liveRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
|
|
74
|
+
return {
|
|
75
|
+
store: STORE,
|
|
76
|
+
id: session.name,
|
|
77
|
+
path: session.project,
|
|
78
|
+
root: session.project,
|
|
79
|
+
pid_status: "alive",
|
|
80
|
+
status: "live",
|
|
81
|
+
stale: false,
|
|
82
|
+
removable: false,
|
|
83
|
+
action: "none",
|
|
84
|
+
reason,
|
|
85
|
+
detail: detail(session.project, session.branch),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function staleRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
|
|
90
|
+
return {
|
|
91
|
+
store: STORE,
|
|
92
|
+
id: session.name,
|
|
93
|
+
path: session.project,
|
|
94
|
+
root: session.project,
|
|
95
|
+
pid_status: "none",
|
|
96
|
+
status: "stale",
|
|
97
|
+
stale: true,
|
|
98
|
+
removable: true,
|
|
99
|
+
action: "none",
|
|
100
|
+
reason,
|
|
101
|
+
detail: `${detail(session.project, session.branch) ?? ""} createdAt=${session.createdAt}`.trim(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isOldEnoughForOrphanGc(session: GjcTmuxSessionStatus): boolean {
|
|
106
|
+
const createdAt = Date.parse(session.createdAt);
|
|
107
|
+
return Number.isFinite(createdAt) && Date.now() - createdAt >= ORPHAN_MAX_AGE_MS;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isGjcOwnedOrphan(session: GjcTmuxSessionStatus): boolean {
|
|
111
|
+
return session.name.startsWith(GJC_TMUX_SESSION_PREFIX) || session.name === "gajae_code";
|
|
112
|
+
}
|
|
113
|
+
|
|
67
114
|
async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
|
|
68
115
|
const { name, project, branch } = session;
|
|
69
|
-
if (
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
};
|
|
116
|
+
if (isSessionLive(session)) return liveRecord(session, "tmux_session_attached_or_has_live_panes");
|
|
117
|
+
if (!project || !branch) {
|
|
118
|
+
if (isGjcOwnedOrphan(session) && isOldEnoughForOrphanGc(session)) {
|
|
119
|
+
return staleRecord(session, "metadata_less_gjc_owned_idle_orphan");
|
|
120
|
+
}
|
|
121
|
+
return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
|
|
99
122
|
}
|
|
123
|
+
if (!pathExists(project)) return staleRecord(session, "project_missing");
|
|
124
|
+
if (!(await hasLiveWorktreeForBranch(project, branch))) return staleRecord(session, "branch_no_worktree");
|
|
100
125
|
return {
|
|
101
126
|
store: STORE,
|
|
102
127
|
id: name,
|
|
@@ -112,9 +137,34 @@ async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcR
|
|
|
112
137
|
};
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
function classifyUntaggedSession(session: GjcTmuxSessionStatus): GcRecord {
|
|
141
|
+
return unclassifiedRecord(session.name, "untagged_tmux_session");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function revalidateRemovable(record: GcRecord, env: NodeJS.ProcessEnv): Promise<boolean> {
|
|
145
|
+
const tags = readTmuxSessionTagsForGc(record.id, env);
|
|
146
|
+
if (
|
|
147
|
+
tags.createdAt &&
|
|
148
|
+
record.detail?.includes("createdAt=") &&
|
|
149
|
+
!record.detail.includes(`createdAt=${tags.createdAt}`)
|
|
150
|
+
) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
|
|
154
|
+
if (tags.profile !== GJC_TMUX_PROFILE_VALUE) return false;
|
|
155
|
+
if (!tags.project || !tags.branch)
|
|
156
|
+
return (
|
|
157
|
+
record.reason === "metadata_less_gjc_owned_idle_orphan" &&
|
|
158
|
+
isGjcOwnedOrphan({
|
|
159
|
+
name: record.id,
|
|
160
|
+
attached: false,
|
|
161
|
+
windows: 0,
|
|
162
|
+
panes: 0,
|
|
163
|
+
panePids: [],
|
|
164
|
+
bindings: "",
|
|
165
|
+
createdAt: tags.createdAt ?? "",
|
|
166
|
+
})
|
|
167
|
+
);
|
|
118
168
|
if (!pathExists(tags.project)) return true;
|
|
119
169
|
return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
|
|
120
170
|
}
|
|
@@ -124,7 +174,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
124
174
|
async collect(ctx: GcContext): Promise<GcCollectResult> {
|
|
125
175
|
const records: GcRecord[] = [];
|
|
126
176
|
const errors: GcCollectResult["errors"] = [];
|
|
127
|
-
let sessions:
|
|
177
|
+
let sessions: GjcTmuxSessionsForGc;
|
|
128
178
|
try {
|
|
129
179
|
sessions = listTmuxSessionsForGc(ctx.env);
|
|
130
180
|
} catch (error) {
|
|
@@ -153,8 +203,8 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
153
203
|
}
|
|
154
204
|
}
|
|
155
205
|
|
|
156
|
-
for (const
|
|
157
|
-
records.push(
|
|
206
|
+
for (const session of sessions.untagged) {
|
|
207
|
+
records.push(classifyUntaggedSession(session));
|
|
158
208
|
}
|
|
159
209
|
|
|
160
210
|
return { records, errors };
|
|
@@ -164,7 +214,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
164
214
|
return { removed: false, skipped: "not_removable_tmux_session" };
|
|
165
215
|
}
|
|
166
216
|
try {
|
|
167
|
-
if (!(await revalidateRemovable(record
|
|
217
|
+
if (!(await revalidateRemovable(record, ctx.env))) {
|
|
168
218
|
return { removed: false, skipped: TOCTOU_SKIP };
|
|
169
219
|
}
|
|
170
220
|
removeGjcTmuxSession(record.id, ctx.env);
|
|
@@ -22,17 +22,23 @@ export interface GjcTmuxSessionStatus {
|
|
|
22
22
|
branch?: string;
|
|
23
23
|
branchSlug?: string;
|
|
24
24
|
project?: string;
|
|
25
|
+
panePids: number[];
|
|
26
|
+
profile?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface GjcTmuxSessionTagsForGc {
|
|
28
30
|
profile?: string;
|
|
29
31
|
project?: string;
|
|
30
32
|
branch?: string;
|
|
33
|
+
branchSlug?: string;
|
|
34
|
+
createdAt?: string;
|
|
35
|
+
attached?: boolean;
|
|
36
|
+
panePids?: number[];
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface GjcTmuxSessionsForGc {
|
|
34
40
|
tagged: GjcTmuxSessionStatus[];
|
|
35
|
-
untagged:
|
|
41
|
+
untagged: GjcTmuxSessionStatus[];
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -68,21 +74,27 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
68
74
|
profile = "",
|
|
69
75
|
bindings = "",
|
|
70
76
|
panes = "0",
|
|
77
|
+
panePids = "",
|
|
71
78
|
branch = "",
|
|
72
79
|
branchSlug = "",
|
|
73
80
|
project = "",
|
|
74
81
|
] = line.split("\t");
|
|
75
|
-
if (!name
|
|
82
|
+
if (!name) return null;
|
|
76
83
|
return {
|
|
77
84
|
name,
|
|
78
85
|
attached: parseBooleanFlag(attached),
|
|
79
86
|
windows: parseNumber(windows),
|
|
80
87
|
panes: parseNumber(panes),
|
|
88
|
+
panePids: panePids
|
|
89
|
+
.split(",")
|
|
90
|
+
.map(pid => parseNumber(pid))
|
|
91
|
+
.filter(pid => pid > 0),
|
|
81
92
|
bindings,
|
|
82
93
|
createdAt: normalizeTmuxCreatedAt(created),
|
|
83
94
|
branch: branch || undefined,
|
|
84
95
|
branchSlug: branchSlug || undefined,
|
|
85
96
|
project: project || undefined,
|
|
97
|
+
profile: profile || undefined,
|
|
86
98
|
};
|
|
87
99
|
}
|
|
88
100
|
|
|
@@ -103,7 +115,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
103
115
|
|
|
104
116
|
function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
105
117
|
return runListSessions(
|
|
106
|
-
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
|
|
118
|
+
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
|
|
107
119
|
env,
|
|
108
120
|
);
|
|
109
121
|
}
|
|
@@ -115,17 +127,35 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
|
|
|
115
127
|
export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
|
|
116
128
|
return listSessionLines(env)
|
|
117
129
|
.map(parseSessionLine)
|
|
118
|
-
.filter((session): session is GjcTmuxSessionStatus => session
|
|
130
|
+
.filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
|
|
119
131
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
/** @internal */
|
|
123
135
|
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
124
|
-
const
|
|
136
|
+
const sessions = listSessionLines(env)
|
|
137
|
+
.map(parseSessionLine)
|
|
138
|
+
.filter((session): session is GjcTmuxSessionStatus => session != null);
|
|
139
|
+
const tagged = sessions
|
|
140
|
+
.filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
|
|
141
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
125
142
|
const taggedNames = new Set(tagged.map(session => session.name));
|
|
143
|
+
const byName = new Map(sessions.map(session => [session.name, session]));
|
|
126
144
|
const untagged = listRawTmuxSessionNames(env)
|
|
127
145
|
.filter(name => !taggedNames.has(name))
|
|
128
|
-
.
|
|
146
|
+
.map(
|
|
147
|
+
name =>
|
|
148
|
+
byName.get(name) ?? {
|
|
149
|
+
name,
|
|
150
|
+
attached: false,
|
|
151
|
+
windows: 0,
|
|
152
|
+
panes: 0,
|
|
153
|
+
panePids: [],
|
|
154
|
+
bindings: "",
|
|
155
|
+
createdAt: "",
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
129
159
|
return { tagged, untagged };
|
|
130
160
|
}
|
|
131
161
|
|
|
@@ -192,15 +222,23 @@ export function readTmuxSessionTagsForGc(
|
|
|
192
222
|
sessionName: string,
|
|
193
223
|
env: NodeJS.ProcessEnv = process.env,
|
|
194
224
|
): GjcTmuxSessionTagsForGc {
|
|
225
|
+
const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
|
|
195
226
|
return {
|
|
196
227
|
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, env),
|
|
197
228
|
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
|
|
198
229
|
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
|
|
230
|
+
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
231
|
+
createdAt: session?.createdAt,
|
|
232
|
+
attached: session?.attached,
|
|
233
|
+
panePids: session?.panePids,
|
|
199
234
|
};
|
|
200
235
|
}
|
|
201
236
|
|
|
202
237
|
export function removeGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
|
|
203
238
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
239
|
+
if (session.attached || session.panePids.length > 0) {
|
|
240
|
+
throw new Error(`gjc_tmux_session_live:${sessionName}`);
|
|
241
|
+
}
|
|
204
242
|
if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE) {
|
|
205
243
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
206
244
|
}
|
|
@@ -32,6 +32,16 @@ export interface UltragoalGuardDiagnostic {
|
|
|
32
32
|
goalId?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface UltragoalAskBlockDiagnostic {
|
|
36
|
+
active: boolean;
|
|
37
|
+
reason: string;
|
|
38
|
+
source: "absent" | "durable_state" | "durable_state_unreadable" | "ledger" | "goals_json";
|
|
39
|
+
goalsPath?: string;
|
|
40
|
+
ledgerPath?: string;
|
|
41
|
+
goalIds?: string[];
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
export interface CurrentGoalLike {
|
|
36
46
|
objective: string;
|
|
37
47
|
status?: string;
|
|
@@ -70,6 +80,48 @@ async function hasDurableUltragoalState(cwd: string): Promise<boolean> {
|
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
function isEnoent(error: unknown): boolean {
|
|
84
|
+
return (
|
|
85
|
+
typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "ENOENT"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function activeAskDiagnostic(input: {
|
|
90
|
+
reason: string;
|
|
91
|
+
source: UltragoalAskBlockDiagnostic["source"];
|
|
92
|
+
goalsPath?: string;
|
|
93
|
+
ledgerPath?: string;
|
|
94
|
+
goalIds?: string[];
|
|
95
|
+
}): UltragoalAskBlockDiagnostic {
|
|
96
|
+
return {
|
|
97
|
+
active: true,
|
|
98
|
+
reason: input.reason,
|
|
99
|
+
source: input.source,
|
|
100
|
+
goalsPath: input.goalsPath,
|
|
101
|
+
ledgerPath: input.ledgerPath,
|
|
102
|
+
goalIds: input.goalIds,
|
|
103
|
+
message: `${input.reason} Use \`gjc ultragoal record-review-blockers\` instead of asking the user.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inactiveAskDiagnostic(input: {
|
|
108
|
+
reason: string;
|
|
109
|
+
source: UltragoalAskBlockDiagnostic["source"];
|
|
110
|
+
goalsPath?: string;
|
|
111
|
+
ledgerPath?: string;
|
|
112
|
+
goalIds?: string[];
|
|
113
|
+
}): UltragoalAskBlockDiagnostic {
|
|
114
|
+
return {
|
|
115
|
+
active: false,
|
|
116
|
+
reason: input.reason,
|
|
117
|
+
source: input.source,
|
|
118
|
+
goalsPath: input.goalsPath,
|
|
119
|
+
ledgerPath: input.ledgerPath,
|
|
120
|
+
goalIds: input.goalIds,
|
|
121
|
+
message: input.reason,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
73
125
|
function requiredGoals(plan: UltragoalPlan): UltragoalGoal[] {
|
|
74
126
|
return plan.goals.filter(goal => goal.status !== "superseded");
|
|
75
127
|
}
|
|
@@ -278,6 +330,109 @@ export async function readUltragoalVerificationState(input: {
|
|
|
278
330
|
return receiptDiagnostic;
|
|
279
331
|
}
|
|
280
332
|
|
|
333
|
+
export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBlockDiagnostic> {
|
|
334
|
+
const paths = getUltragoalPaths(cwd);
|
|
335
|
+
try {
|
|
336
|
+
await fs.stat(paths.dir);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (isEnoent(error)) {
|
|
339
|
+
return inactiveAskDiagnostic({
|
|
340
|
+
reason: "No durable .gjc/ultragoal state exists.",
|
|
341
|
+
source: "absent",
|
|
342
|
+
goalsPath: paths.goalsPath,
|
|
343
|
+
ledgerPath: paths.ledgerPath,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return activeAskDiagnostic({
|
|
347
|
+
reason: `Durable .gjc/ultragoal state is present but unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
348
|
+
source: "durable_state_unreadable",
|
|
349
|
+
goalsPath: paths.goalsPath,
|
|
350
|
+
ledgerPath: paths.ledgerPath,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let plan: UltragoalPlan | null;
|
|
355
|
+
let ledger: UltragoalLedgerEvent[];
|
|
356
|
+
try {
|
|
357
|
+
plan = await readUltragoalPlan(cwd);
|
|
358
|
+
ledger = await readUltragoalLedger(cwd);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return activeAskDiagnostic({
|
|
361
|
+
reason: `Unable to read durable Ultragoal state: ${error instanceof Error ? error.message : String(error)}`,
|
|
362
|
+
source: "durable_state_unreadable",
|
|
363
|
+
goalsPath: paths.goalsPath,
|
|
364
|
+
ledgerPath: paths.ledgerPath,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (!plan) {
|
|
368
|
+
return activeAskDiagnostic({
|
|
369
|
+
reason: "Durable .gjc/ultragoal state exists but goals.json is missing or empty.",
|
|
370
|
+
source: "durable_state_unreadable",
|
|
371
|
+
goalsPath: paths.goalsPath,
|
|
372
|
+
ledgerPath: paths.ledgerPath,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (plan.goals.some(goal => goal.status === "review_blocked")) {
|
|
377
|
+
const goalIds = plan.goals.filter(goal => goal.status === "review_blocked").map(goal => goal.id);
|
|
378
|
+
return activeAskDiagnostic({
|
|
379
|
+
reason: `Ultragoal has recorded review blockers: ${goalIds.join(", ")}.`,
|
|
380
|
+
source: "goals_json",
|
|
381
|
+
goalsPath: paths.goalsPath,
|
|
382
|
+
ledgerPath: paths.ledgerPath,
|
|
383
|
+
goalIds,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const runState = getUltragoalRunCompletionState(plan);
|
|
388
|
+
if (runState.incompleteGoals.length > 0) {
|
|
389
|
+
const goalIds = runState.incompleteGoals.map(goal => goal.id);
|
|
390
|
+
return activeAskDiagnostic({
|
|
391
|
+
reason: `Ultragoal has incomplete required goals: ${goalIds.join(", ")}.`,
|
|
392
|
+
source: "goals_json",
|
|
393
|
+
goalsPath: paths.goalsPath,
|
|
394
|
+
ledgerPath: paths.ledgerPath,
|
|
395
|
+
goalIds,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const finalReceiptGoal = [...requiredGoals(plan)]
|
|
400
|
+
.reverse()
|
|
401
|
+
.find(goal => goal.completionVerification?.receiptKind === "final-aggregate");
|
|
402
|
+
if (!finalReceiptGoal) {
|
|
403
|
+
return activeAskDiagnostic({
|
|
404
|
+
reason: "Ultragoal aggregate completion is missing a final aggregate receipt.",
|
|
405
|
+
source: "durable_state",
|
|
406
|
+
goalsPath: paths.goalsPath,
|
|
407
|
+
ledgerPath: paths.ledgerPath,
|
|
408
|
+
goalIds: requiredGoals(plan).map(goal => goal.id),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const diagnostic = validateCompletionReceipt({
|
|
413
|
+
plan,
|
|
414
|
+
ledger,
|
|
415
|
+
goal: finalReceiptGoal,
|
|
416
|
+
receiptKind: "final-aggregate",
|
|
417
|
+
});
|
|
418
|
+
if (diagnostic.state !== "active_verified_complete") {
|
|
419
|
+
return activeAskDiagnostic({
|
|
420
|
+
reason: diagnostic.message,
|
|
421
|
+
source: diagnostic.state === "active_dirty_quality_gate" ? "ledger" : "durable_state",
|
|
422
|
+
goalsPath: paths.goalsPath,
|
|
423
|
+
ledgerPath: paths.ledgerPath,
|
|
424
|
+
goalIds: diagnostic.goalId ? [diagnostic.goalId] : undefined,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return inactiveAskDiagnostic({
|
|
428
|
+
reason: "Ultragoal run is verified complete.",
|
|
429
|
+
source: "durable_state",
|
|
430
|
+
goalsPath: paths.goalsPath,
|
|
431
|
+
ledgerPath: paths.ledgerPath,
|
|
432
|
+
goalIds: [finalReceiptGoal.id],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
281
436
|
export async function assertCanCompleteCurrentGoal(input: {
|
|
282
437
|
cwd: string;
|
|
283
438
|
currentGoal?: CurrentGoalLike | null;
|