@gajae-code/coding-agent 0.5.2 → 0.5.4
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 +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -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/tmux-sessions.d.ts +7 -1
- 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/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 -0
- package/dist/types/modes/types.d.ts +1 -0
- 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 +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- 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/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- 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-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- 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/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- 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 +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- 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 +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
|
@@ -8,7 +8,7 @@ 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
14
|
type GjcTmuxSessionsForGc,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const STORE = "tmux_sessions" as const;
|
|
21
21
|
const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
|
|
22
|
+
const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
22
23
|
|
|
23
24
|
function pathExists(path: string): boolean {
|
|
24
25
|
try {
|
|
@@ -65,39 +66,62 @@ async function hasLiveWorktreeForBranch(project: string, branch: string): Promis
|
|
|
65
66
|
return entries.some(entry => branchMatches(entry.branch, branch));
|
|
66
67
|
}
|
|
67
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
|
+
|
|
68
114
|
async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
|
|
69
115
|
const { name, project, branch } = session;
|
|
70
|
-
if (
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
root: project,
|
|
77
|
-
pid_status: "none",
|
|
78
|
-
status: "stale",
|
|
79
|
-
stale: true,
|
|
80
|
-
removable: true,
|
|
81
|
-
action: "none",
|
|
82
|
-
reason: "project_missing",
|
|
83
|
-
detail: detail(project, branch),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (!(await hasLiveWorktreeForBranch(project, branch))) {
|
|
87
|
-
return {
|
|
88
|
-
store: STORE,
|
|
89
|
-
id: name,
|
|
90
|
-
path: project,
|
|
91
|
-
root: project,
|
|
92
|
-
pid_status: "none",
|
|
93
|
-
status: "stale",
|
|
94
|
-
stale: true,
|
|
95
|
-
removable: true,
|
|
96
|
-
action: "none",
|
|
97
|
-
reason: "branch_no_worktree",
|
|
98
|
-
detail: detail(project, branch),
|
|
99
|
-
};
|
|
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);
|
|
100
122
|
}
|
|
123
|
+
if (!pathExists(project)) return staleRecord(session, "project_missing");
|
|
124
|
+
if (!(await hasLiveWorktreeForBranch(project, branch))) return staleRecord(session, "branch_no_worktree");
|
|
101
125
|
return {
|
|
102
126
|
store: STORE,
|
|
103
127
|
id: name,
|
|
@@ -113,9 +137,34 @@ async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcR
|
|
|
113
137
|
};
|
|
114
138
|
}
|
|
115
139
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
);
|
|
119
168
|
if (!pathExists(tags.project)) return true;
|
|
120
169
|
return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
|
|
121
170
|
}
|
|
@@ -154,8 +203,8 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
154
203
|
}
|
|
155
204
|
}
|
|
156
205
|
|
|
157
|
-
for (const
|
|
158
|
-
records.push(
|
|
206
|
+
for (const session of sessions.untagged) {
|
|
207
|
+
records.push(classifyUntaggedSession(session));
|
|
159
208
|
}
|
|
160
209
|
|
|
161
210
|
return { records, errors };
|
|
@@ -165,7 +214,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
165
214
|
return { removed: false, skipped: "not_removable_tmux_session" };
|
|
166
215
|
}
|
|
167
216
|
try {
|
|
168
|
-
if (!(await revalidateRemovable(record
|
|
217
|
+
if (!(await revalidateRemovable(record, ctx.env))) {
|
|
169
218
|
return { removed: false, skipped: TOCTOU_SKIP };
|
|
170
219
|
}
|
|
171
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
|
}
|
|
@@ -1247,7 +1247,7 @@ const CLI_REPLAY_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
|
1247
1247
|
const CLI_REPLAY_DEFAULT_TIMEOUT_MS = 10_000;
|
|
1248
1248
|
const CLI_REPLAY_MIN_TIMEOUT_MS = 1_000;
|
|
1249
1249
|
const CLI_REPLAY_MAX_TIMEOUT_MS = 30_000;
|
|
1250
|
-
const CLI_REPLAY_EXEMPT_REASON_CODES =
|
|
1250
|
+
const CLI_REPLAY_EXEMPT_REASON_CODES = [
|
|
1251
1251
|
"unsafe_side_effect",
|
|
1252
1252
|
"requires_credentials",
|
|
1253
1253
|
"requires_network",
|
|
@@ -1255,8 +1255,10 @@ const CLI_REPLAY_EXEMPT_REASON_CODES = new Set([
|
|
|
1255
1255
|
"destructive",
|
|
1256
1256
|
"interactive_only",
|
|
1257
1257
|
"platform_unavailable",
|
|
1258
|
-
]
|
|
1258
|
+
] as const;
|
|
1259
|
+
const CLI_REPLAY_EXEMPT_REASON_CODE_SET = new Set<string>(CLI_REPLAY_EXEMPT_REASON_CODES);
|
|
1259
1260
|
const CLI_REPLAY_ENV_BASE: Record<string, string> = { CI: "1", NO_COLOR: "1", GJC_ULTRAGOAL_REPLAY: "1" };
|
|
1261
|
+
const CLI_REPLAY_EXEMPT_REASON_CODE_LIST = CLI_REPLAY_EXEMPT_REASON_CODES.join(", ");
|
|
1260
1262
|
const CLI_REPLAY_SAFE_ENV_NAMES = new Set(["LANG", "LC_ALL", "LC_CTYPE", "TZ"]);
|
|
1261
1263
|
const CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN =
|
|
1262
1264
|
/^(?:NODE_OPTIONS|GIT_EXTERNAL_DIFF|GIT_SSH|GIT_SSH_COMMAND|GIT_PAGER|PATH|LD_PRELOAD|LD_LIBRARY_PATH)$|^(?:GIT_CONFIG|DYLD_|BUN_|NPM_CONFIG_)|(?:^|_)OPTIONS$|PRELOAD$/;
|
|
@@ -1568,8 +1570,10 @@ async function validateReplayExemptFallback(
|
|
|
1568
1570
|
const exempt = qualityGateObject(record.replayExempt);
|
|
1569
1571
|
if (!exempt) return false;
|
|
1570
1572
|
const reasonCode = requiredStringField(exempt, "reasonCode", `${fieldName}.replayExempt`);
|
|
1571
|
-
if (!
|
|
1572
|
-
throw new Error(
|
|
1573
|
+
if (!CLI_REPLAY_EXEMPT_REASON_CODE_SET.has(reasonCode))
|
|
1574
|
+
throw new Error(
|
|
1575
|
+
`qualityGate ${fieldName}.replayExempt.reasonCode must be one of: ${CLI_REPLAY_EXEMPT_REASON_CODE_LIST}`,
|
|
1576
|
+
);
|
|
1573
1577
|
const reason = requiredStringField(exempt, "reason", `${fieldName}.replayExempt`);
|
|
1574
1578
|
if (!isSubstantiveEvidence(reason) || reason.length < 30)
|
|
1575
1579
|
throw new Error(`qualityGate ${fieldName}.replayExempt.reason must be audited and substantive`);
|
|
@@ -63,7 +63,16 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
|
|
|
63
63
|
throw new Error(`artifact://${id} not found`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// F20: cap the materialized artifact so reading a huge spilled artifact cannot
|
|
67
|
+
// buffer GBs into memory (the range selector is applied downstream, so without a
|
|
68
|
+
// cap a `artifact://id:range` over a multi-GB artifact still reads it whole).
|
|
69
|
+
const MAX_ARTIFACT_READ_BYTES = 16 * 1024 * 1024;
|
|
70
|
+
const file = Bun.file(foundPath);
|
|
71
|
+
const fullSize = file.size;
|
|
72
|
+
const content =
|
|
73
|
+
fullSize > MAX_ARTIFACT_READ_BYTES
|
|
74
|
+
? `${await file.slice(0, MAX_ARTIFACT_READ_BYTES).text()}\n\n[Artifact truncated: first ${MAX_ARTIFACT_READ_BYTES} of ${fullSize} bytes shown; use a narrower range or a specialized tool for the full content.]`
|
|
75
|
+
: await file.text();
|
|
67
76
|
return {
|
|
68
77
|
url: url.href,
|
|
69
78
|
content,
|