aligndev 0.0.0 → 0.19.0
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/README.md +166 -1
- package/bin/aligndev.mjs +3 -0
- package/dist/alignfirst-cli.d.ts +10 -0
- package/dist/alignfirst-cli.js +56 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +95 -0
- package/dist/code/claude-agent.d.ts +14 -0
- package/dist/code/claude-agent.js +168 -0
- package/dist/code/code-cli.d.ts +72 -0
- package/dist/code/code-cli.js +630 -0
- package/dist/code/codex-agent.d.ts +15 -0
- package/dist/code/codex-agent.js +168 -0
- package/dist/code/codex-rollout.d.ts +18 -0
- package/dist/code/codex-rollout.js +114 -0
- package/dist/code/coding-agent.d.ts +4 -0
- package/dist/code/coding-agent.js +6 -0
- package/dist/code/models.d.ts +14 -0
- package/dist/code/models.js +89 -0
- package/dist/code/prompt.d.ts +11 -0
- package/dist/code/prompt.js +26 -0
- package/dist/code/quota.d.ts +25 -0
- package/dist/code/quota.js +248 -0
- package/dist/code/run-agent.d.ts +60 -0
- package/dist/code/run-agent.js +212 -0
- package/dist/code/session-file.d.ts +50 -0
- package/dist/code/session-file.js +263 -0
- package/dist/command-form.d.ts +6 -0
- package/dist/command-form.js +7 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +72 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/guide/code-guide.d.ts +4 -0
- package/dist/guide/code-guide.js +17 -0
- package/dist/guide/guide-cli.d.ts +3 -0
- package/dist/guide/guide-cli.js +81 -0
- package/dist/guide/render-template.d.ts +4 -0
- package/dist/guide/render-template.js +62 -0
- package/dist/guide/topics.d.ts +3 -0
- package/dist/guide/topics.js +16 -0
- package/dist/output.d.ts +3 -0
- package/dist/output.js +1 -0
- package/dist/project/discovery.d.ts +42 -0
- package/dist/project/discovery.js +287 -0
- package/dist/project/format.d.ts +6 -0
- package/dist/project/format.js +31 -0
- package/dist/project/guide.d.ts +3 -0
- package/dist/project/guide.js +69 -0
- package/dist/project/layout.d.ts +36 -0
- package/dist/project/layout.js +128 -0
- package/dist/project/markers.d.ts +18 -0
- package/dist/project/markers.js +90 -0
- package/dist/project/ports.d.ts +3 -0
- package/dist/project/ports.js +55 -0
- package/dist/project/project-cli.d.ts +17 -0
- package/dist/project/project-cli.js +227 -0
- package/dist/project/render.d.ts +10 -0
- package/dist/project/render.js +144 -0
- package/dist/project/status.d.ts +24 -0
- package/dist/project/status.js +110 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +5 -0
- package/package.json +38 -3
- package/templates/guide/code.md +252 -0
- package/templates/guide/playbook/channel-handling.md +110 -0
- package/templates/guide/playbook/consultation.md +71 -0
- package/templates/guide/playbook/discord-message-tool.md +37 -0
- package/templates/guide/playbook/playbook.md +139 -0
- package/templates/guide/playbook/project-lifecycle.md +100 -0
- package/templates/guide/playbook/project-workspace-setup.md +186 -0
- package/templates/guide/playbook/slack-message-tool.md +23 -0
- package/templates/guide/playbook/working-session.md +588 -0
- package/templates/guide/project.md +33 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { buildAgentEnv } from "./run-agent.js";
|
|
4
|
+
const QUOTA_TIMEOUT_MS = 30_000;
|
|
5
|
+
const CLAUDE_PRIVACY_OPT_OUTS = [
|
|
6
|
+
"DISABLE_TELEMETRY",
|
|
7
|
+
"DISABLE_ERROR_REPORTING",
|
|
8
|
+
"DISABLE_FEEDBACK_COMMAND",
|
|
9
|
+
"DISABLE_BUG_COMMAND",
|
|
10
|
+
"CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY",
|
|
11
|
+
];
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const DEFAULT_QUOTA_PROCESS_ADAPTER = {
|
|
14
|
+
execute: executeQuotaProcess,
|
|
15
|
+
spawn: spawnQuotaProcess,
|
|
16
|
+
};
|
|
17
|
+
export const readQuota = createQuotaReader();
|
|
18
|
+
export function createQuotaReader(adapter = DEFAULT_QUOTA_PROCESS_ADAPTER, timeoutMs = QUOTA_TIMEOUT_MS) {
|
|
19
|
+
return async (agent, context) => {
|
|
20
|
+
const env = buildAgentEnv(context.env, context.unset);
|
|
21
|
+
return agent === "claude"
|
|
22
|
+
? readClaudeQuota({ ...context, env: translateClaudePrivacyOptOut(env) }, adapter, timeoutMs)
|
|
23
|
+
: readCodexQuota({ ...context, env }, adapter, timeoutMs);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function translateClaudePrivacyOptOut(env) {
|
|
27
|
+
if (env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC === undefined)
|
|
28
|
+
return env;
|
|
29
|
+
delete env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
|
|
30
|
+
for (const name of CLAUDE_PRIVACY_OPT_OUTS)
|
|
31
|
+
env[name] = "1";
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
async function readClaudeQuota(context, adapter, timeoutMs) {
|
|
35
|
+
const { stdout } = await adapter.execute("claude", ["-p", "/usage", "--tools", "", "--output-format", "json", "--no-session-persistence"], {
|
|
36
|
+
cwd: context.cwd,
|
|
37
|
+
env: context.env,
|
|
38
|
+
timeout: timeoutMs,
|
|
39
|
+
});
|
|
40
|
+
return `Claude Code quota\n\n${parseClaudeQuota(stdout)}`;
|
|
41
|
+
}
|
|
42
|
+
export function parseClaudeQuota(stdout) {
|
|
43
|
+
let value;
|
|
44
|
+
try {
|
|
45
|
+
value = JSON.parse(stdout);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Claude Code returned malformed usage JSON.");
|
|
49
|
+
}
|
|
50
|
+
if (!isRecord(value))
|
|
51
|
+
throw new Error("Claude Code returned an unexpected usage response.");
|
|
52
|
+
const result = value.result;
|
|
53
|
+
if (value.is_error === true || value.subtype !== "success" || typeof result !== "string") {
|
|
54
|
+
throw new Error("Claude Code could not read the current usage limits.");
|
|
55
|
+
}
|
|
56
|
+
const insightsStart = result.search(/^What's contributing to your limits usage\?/m);
|
|
57
|
+
const limits = (insightsStart === -1 ? result : result.slice(0, insightsStart)).trimEnd();
|
|
58
|
+
if (!/\d+% used/.test(limits))
|
|
59
|
+
throw new Error("Claude Code returned no usage limits.");
|
|
60
|
+
return limits;
|
|
61
|
+
}
|
|
62
|
+
async function readCodexQuota(context, adapter, timeoutMs) {
|
|
63
|
+
const response = await requestCodexQuota(context, adapter, timeoutMs);
|
|
64
|
+
return formatCodexQuota(response);
|
|
65
|
+
}
|
|
66
|
+
function requestCodexQuota(context, adapter, timeoutMs) {
|
|
67
|
+
const child = adapter.spawn("codex", ["app-server"], {
|
|
68
|
+
cwd: context.cwd,
|
|
69
|
+
env: context.env,
|
|
70
|
+
});
|
|
71
|
+
return exchangeCodexMessages(child, timeoutMs);
|
|
72
|
+
}
|
|
73
|
+
function exchangeCodexMessages(child, timeoutMs) {
|
|
74
|
+
const detachGuard = guardChild(child);
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let settled = false;
|
|
77
|
+
const finish = (error, result) => {
|
|
78
|
+
if (settled)
|
|
79
|
+
return;
|
|
80
|
+
settled = true;
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
detachGuard();
|
|
83
|
+
child.kill("SIGTERM");
|
|
84
|
+
if (error)
|
|
85
|
+
reject(error);
|
|
86
|
+
else
|
|
87
|
+
resolve(result);
|
|
88
|
+
};
|
|
89
|
+
const timer = setTimeout(() => finish(new Error("Codex timed out while reading usage limits.")), timeoutMs);
|
|
90
|
+
const readStderr = observeCodexOutput(child, finish);
|
|
91
|
+
observeCodexLifecycle(child, finish, () => settled, readStderr);
|
|
92
|
+
initializeCodex(child);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function guardChild(child) {
|
|
96
|
+
const kill = () => {
|
|
97
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
98
|
+
child.kill("SIGTERM");
|
|
99
|
+
};
|
|
100
|
+
process.on("exit", kill);
|
|
101
|
+
return () => process.off("exit", kill);
|
|
102
|
+
}
|
|
103
|
+
function observeCodexOutput(child, finish) {
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
child.stdout.setEncoding("utf8");
|
|
107
|
+
child.stdout.on("data", (chunk) => {
|
|
108
|
+
stdout += chunk;
|
|
109
|
+
stdout = drainJsonLines(stdout, (message) => handleCodexMessage(child, message, finish));
|
|
110
|
+
});
|
|
111
|
+
child.stderr.setEncoding("utf8");
|
|
112
|
+
child.stderr.on("data", (chunk) => {
|
|
113
|
+
stderr += chunk;
|
|
114
|
+
});
|
|
115
|
+
return () => stderr;
|
|
116
|
+
}
|
|
117
|
+
function drainJsonLines(buffer, onMessage) {
|
|
118
|
+
const lines = buffer.split("\n");
|
|
119
|
+
const rest = lines.pop() ?? "";
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
if (line.trim() === "")
|
|
122
|
+
continue;
|
|
123
|
+
let value;
|
|
124
|
+
try {
|
|
125
|
+
value = JSON.parse(line);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (isRecord(value))
|
|
131
|
+
onMessage(value);
|
|
132
|
+
}
|
|
133
|
+
return rest;
|
|
134
|
+
}
|
|
135
|
+
function handleCodexMessage(child, message, finish) {
|
|
136
|
+
if (message.id === 0 && message.result !== undefined) {
|
|
137
|
+
sendCodexMessage(child, { method: "initialized", params: {} });
|
|
138
|
+
sendCodexMessage(child, { method: "account/rateLimits/read", id: 1 });
|
|
139
|
+
}
|
|
140
|
+
if (message.id !== 1)
|
|
141
|
+
return;
|
|
142
|
+
if (isRecord(message.error)) {
|
|
143
|
+
const detail = message.error.message;
|
|
144
|
+
finish(new Error(typeof detail === "string"
|
|
145
|
+
? `Codex could not read usage limits: ${detail}`
|
|
146
|
+
: "Codex could not read the current usage limits."));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
finish(undefined, message.result);
|
|
150
|
+
}
|
|
151
|
+
function sendCodexMessage(child, message) {
|
|
152
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
153
|
+
}
|
|
154
|
+
function observeCodexLifecycle(child, finish, isSettled, readStderr) {
|
|
155
|
+
child.stdin.on("error", (error) => {
|
|
156
|
+
finish(new Error(`Codex could not read usage limits: ${error.message}`));
|
|
157
|
+
});
|
|
158
|
+
child.on("error", (error) => finish(error));
|
|
159
|
+
child.on("close", (code) => {
|
|
160
|
+
if (isSettled())
|
|
161
|
+
return;
|
|
162
|
+
const detail = readStderr().trim();
|
|
163
|
+
finish(new Error(detail !== ""
|
|
164
|
+
? `Codex could not read usage limits: ${detail}`
|
|
165
|
+
: `Codex app-server exited with code ${code ?? "unknown"}.`));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function initializeCodex(child) {
|
|
169
|
+
sendCodexMessage(child, {
|
|
170
|
+
method: "initialize",
|
|
171
|
+
id: 0,
|
|
172
|
+
params: {
|
|
173
|
+
clientInfo: {
|
|
174
|
+
name: "aligndev",
|
|
175
|
+
title: "AlignFirst aligndev",
|
|
176
|
+
version: "0.0.0",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function executeQuotaProcess(file, args, options) {
|
|
182
|
+
const { stdout } = await execFileAsync(file, args, {
|
|
183
|
+
...options,
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
});
|
|
186
|
+
return { stdout };
|
|
187
|
+
}
|
|
188
|
+
function spawnQuotaProcess(file, args, options) {
|
|
189
|
+
return spawn(file, args, { ...options, stdio: ["pipe", "pipe", "pipe"] });
|
|
190
|
+
}
|
|
191
|
+
export function formatCodexQuota(response, formatTime = formatLocalTime) {
|
|
192
|
+
const bucket = parseCodexBucket(response);
|
|
193
|
+
return `Codex quota\n\n${formatBucket(bucket, formatTime)}`;
|
|
194
|
+
}
|
|
195
|
+
function parseCodexBucket(response) {
|
|
196
|
+
if (!isRecord(response))
|
|
197
|
+
throw new Error("Codex returned an unexpected usage response.");
|
|
198
|
+
const bucket = parseBucket(response.rateLimits);
|
|
199
|
+
if (bucket === undefined) {
|
|
200
|
+
throw new Error("Codex returned no usage windows for the current account.");
|
|
201
|
+
}
|
|
202
|
+
return bucket;
|
|
203
|
+
}
|
|
204
|
+
function parseBucket(value) {
|
|
205
|
+
if (!isRecord(value))
|
|
206
|
+
return;
|
|
207
|
+
const windows = [parseWindow(value.primary), parseWindow(value.secondary)].filter((window) => window !== undefined);
|
|
208
|
+
if (windows.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
return { windows };
|
|
211
|
+
}
|
|
212
|
+
function parseWindow(value) {
|
|
213
|
+
if (!isRecord(value) || typeof value.usedPercent !== "number")
|
|
214
|
+
return;
|
|
215
|
+
return {
|
|
216
|
+
usedPercent: value.usedPercent,
|
|
217
|
+
windowDurationMins: typeof value.windowDurationMins === "number" ? value.windowDurationMins : null,
|
|
218
|
+
resetsAt: typeof value.resetsAt === "number" ? value.resetsAt : null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function formatBucket(bucket, formatTime) {
|
|
222
|
+
const windows = bucket.windows.map((window, index) => {
|
|
223
|
+
const duration = formatDuration(window.windowDurationMins, index);
|
|
224
|
+
const reset = window.resetsAt === null ? "" : ` · resets ${formatTime(window.resetsAt)}`;
|
|
225
|
+
return ` ${duration}: ${window.usedPercent}% used${reset}`;
|
|
226
|
+
});
|
|
227
|
+
return `Codex\n${windows.join("\n")}`;
|
|
228
|
+
}
|
|
229
|
+
function formatDuration(minutes, index) {
|
|
230
|
+
if (minutes === null)
|
|
231
|
+
return index === 0 ? "Primary window" : "Secondary window";
|
|
232
|
+
if (minutes % 10_080 === 0)
|
|
233
|
+
return plural(minutes / 10_080, "week");
|
|
234
|
+
if (minutes % 1_440 === 0)
|
|
235
|
+
return plural(minutes / 1_440, "day");
|
|
236
|
+
if (minutes % 60 === 0)
|
|
237
|
+
return plural(minutes / 60, "hour");
|
|
238
|
+
return plural(minutes, "minute");
|
|
239
|
+
}
|
|
240
|
+
function plural(value, unit) {
|
|
241
|
+
return `${value} ${unit}${value === 1 ? "" : "s"}`;
|
|
242
|
+
}
|
|
243
|
+
function formatLocalTime(timestampSeconds) {
|
|
244
|
+
return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(timestampSeconds * 1000));
|
|
245
|
+
}
|
|
246
|
+
function isRecord(value) {
|
|
247
|
+
return typeof value === "object" && value !== null;
|
|
248
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import type { Output } from "../output.js";
|
|
3
|
+
import { type CompletionUpdate } from "./session-file.js";
|
|
4
|
+
export interface RunConfig {
|
|
5
|
+
prompt: string;
|
|
6
|
+
sessionFilePath: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
resume?: string;
|
|
9
|
+
executableModel: string | undefined;
|
|
10
|
+
skipPermissions: boolean;
|
|
11
|
+
additionalDirectories: readonly string[];
|
|
12
|
+
unset: readonly string[];
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
}
|
|
15
|
+
export interface RunResult {
|
|
16
|
+
status: "succeeded" | "failed";
|
|
17
|
+
authRequired: boolean;
|
|
18
|
+
sessionId: string | null;
|
|
19
|
+
result: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentProtocolState {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
result?: string;
|
|
24
|
+
failure?: string;
|
|
25
|
+
protocolComplete: boolean;
|
|
26
|
+
protocolFailed: boolean;
|
|
27
|
+
authEvidence: boolean;
|
|
28
|
+
contextTokens?: number;
|
|
29
|
+
peakContextTokens?: number;
|
|
30
|
+
streamTotalTokens?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface AgentAssessment {
|
|
33
|
+
succeeded: boolean;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
result?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
authEvidence: boolean;
|
|
38
|
+
contextTokens?: number;
|
|
39
|
+
contextCompacted?: boolean;
|
|
40
|
+
contextTokensError?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface AgentAdapter {
|
|
43
|
+
executable: string;
|
|
44
|
+
buildArgs(config: RunConfig): string[];
|
|
45
|
+
createState(): AgentProtocolState;
|
|
46
|
+
interpretLine(line: string, state: AgentProtocolState): string | undefined;
|
|
47
|
+
assess(state: AgentProtocolState): AgentAssessment;
|
|
48
|
+
isAuthenticationError(message: string): boolean;
|
|
49
|
+
authenticationMessage(detail?: string): string;
|
|
50
|
+
failureMessage?(state: AgentProtocolState): string;
|
|
51
|
+
}
|
|
52
|
+
export type SpawnAgentProcess = (command: string, args: readonly string[], options: {
|
|
53
|
+
cwd: string;
|
|
54
|
+
env: NodeJS.ProcessEnv;
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"];
|
|
56
|
+
}) => ChildProcess;
|
|
57
|
+
export declare function runAgent(config: RunConfig, adapter: AgentAdapter, out: Output, spawnProcess?: SpawnAgentProcess): Promise<RunResult>;
|
|
58
|
+
export declare function buildAgentEnv(baseEnv: NodeJS.ProcessEnv, unset: readonly string[]): NodeJS.ProcessEnv;
|
|
59
|
+
export declare function buildTerminationUpdate(signal: NodeJS.Signals, state: AgentProtocolState, now: Date): CompletionUpdate;
|
|
60
|
+
export declare function isCompacted(state: AgentProtocolState): boolean;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendTranscript, applyCompletion } from "./session-file.js";
|
|
3
|
+
const TERMINATION_GRACE_MS = 2000;
|
|
4
|
+
// A coding-agent session exports its own identity to every command it runs; when the assistant
|
|
5
|
+
// runs in one, the coder must not inherit it.
|
|
6
|
+
const ASSISTANT_SESSION_VARIABLES = [
|
|
7
|
+
"CLAUDECODE",
|
|
8
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
9
|
+
"CLAUDE_CODE_CHILD_SESSION",
|
|
10
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
11
|
+
"CLAUDE_CODE_MESSAGING_SOCKET",
|
|
12
|
+
"CLAUDE_CODE_MESSAGING_TOKEN",
|
|
13
|
+
"CLAUDE_CODE_SESSION_ATTENDED",
|
|
14
|
+
"CLAUDE_CODE_EXECPATH",
|
|
15
|
+
"CLAUDE_PID",
|
|
16
|
+
"CLAUDE_EFFORT",
|
|
17
|
+
"CODEX_SESSION_ID",
|
|
18
|
+
"CODEX_THREAD_ID",
|
|
19
|
+
"CODEX_CI",
|
|
20
|
+
"CODEX_SANDBOX_NETWORK_DISABLED",
|
|
21
|
+
];
|
|
22
|
+
export async function runAgent(config, adapter, out, spawnProcess = spawnDirectChild) {
|
|
23
|
+
const state = adapter.createState();
|
|
24
|
+
const outcome = await spawnAgent(config, adapter, state, out, spawnProcess);
|
|
25
|
+
const assessment = adapter.assess(state);
|
|
26
|
+
const failed = outcome.exitCode !== 0 || !assessment.succeeded;
|
|
27
|
+
// Authentication evidence matters only when the run failed. Claude can recover after retrying
|
|
28
|
+
// a 401, so evidence from a successful run must not produce an auth_required result.
|
|
29
|
+
const authRequired = failed && (assessment.authEvidence || adapter.isAuthenticationError(outcome.stderr));
|
|
30
|
+
const result = selectResult(adapter, state, assessment, outcome, authRequired);
|
|
31
|
+
applyCompletion(config.sessionFilePath, {
|
|
32
|
+
status: failed ? "failed" : "succeeded",
|
|
33
|
+
endedAt: new Date().toISOString(),
|
|
34
|
+
exitReason: authRequired ? "auth_required" : failed ? "error" : "completed",
|
|
35
|
+
sessionId: assessment.sessionId ?? null,
|
|
36
|
+
result,
|
|
37
|
+
contextTokens: assessment.contextTokens ?? null,
|
|
38
|
+
contextCompacted: assessment.contextCompacted ?? false,
|
|
39
|
+
contextTokensError: assessment.contextTokensError ?? null,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
status: failed ? "failed" : "succeeded",
|
|
43
|
+
authRequired,
|
|
44
|
+
sessionId: assessment.sessionId ?? null,
|
|
45
|
+
result,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function spawnDirectChild(command, args, options) {
|
|
49
|
+
return spawn(command, args, options);
|
|
50
|
+
}
|
|
51
|
+
function selectResult(adapter, state, assessment, outcome, authRequired) {
|
|
52
|
+
if (authRequired) {
|
|
53
|
+
return adapter.authenticationMessage(assessment.error ?? nonblank(outcome.stderr));
|
|
54
|
+
}
|
|
55
|
+
if (assessment.succeeded && outcome.exitCode === 0 && assessment.result !== undefined) {
|
|
56
|
+
return assessment.result;
|
|
57
|
+
}
|
|
58
|
+
return (assessment.error ??
|
|
59
|
+
nonblank(outcome.stderr) ??
|
|
60
|
+
(!assessment.succeeded ? adapter.failureMessage?.(state) : undefined) ??
|
|
61
|
+
`${adapter.executable} exited with code ${outcome.exitCode ?? "unknown"}`);
|
|
62
|
+
}
|
|
63
|
+
function spawnAgent(config, adapter, state, out, spawnProcess) {
|
|
64
|
+
const child = spawnProcess(adapter.executable, adapter.buildArgs(config), {
|
|
65
|
+
cwd: config.cwd,
|
|
66
|
+
env: buildAgentEnv(config.env, config.unset),
|
|
67
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
const detachChildGuards = guardChildLifecycle(child, config.sessionFilePath, state);
|
|
70
|
+
let buffer = "";
|
|
71
|
+
let stderr = "";
|
|
72
|
+
let inputError;
|
|
73
|
+
child.stdout?.setEncoding("utf-8");
|
|
74
|
+
child.stdout?.on("data", (chunk) => {
|
|
75
|
+
buffer += chunk;
|
|
76
|
+
buffer = drainLines(buffer, (line) => emitLine(config, adapter, line, state, out));
|
|
77
|
+
});
|
|
78
|
+
child.stderr?.setEncoding("utf-8");
|
|
79
|
+
child.stderr?.on("data", (chunk) => {
|
|
80
|
+
stderr += chunk;
|
|
81
|
+
});
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
let settled = false;
|
|
84
|
+
const finish = (outcome) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
detachChildGuards();
|
|
89
|
+
resolve(outcome);
|
|
90
|
+
};
|
|
91
|
+
child.on("close", (code) => {
|
|
92
|
+
if (buffer.trim() !== "")
|
|
93
|
+
emitLine(config, adapter, buffer, state, out);
|
|
94
|
+
finish({
|
|
95
|
+
exitCode: inputError === undefined ? code : 1,
|
|
96
|
+
stderr: inputError === undefined
|
|
97
|
+
? stderr
|
|
98
|
+
: `${stderr}\nCould not send prompt: ${inputError.message}`,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
child.on("error", (err) => finish({ exitCode: 1, stderr: err.message }));
|
|
102
|
+
child.stdin?.on("error", (error) => {
|
|
103
|
+
inputError = error;
|
|
104
|
+
child.kill("SIGTERM");
|
|
105
|
+
});
|
|
106
|
+
child.stdin?.end(config.prompt, "utf8");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function emitLine(config, adapter, line, state, out) {
|
|
110
|
+
const rendered = adapter.interpretLine(line, state);
|
|
111
|
+
if (rendered === undefined)
|
|
112
|
+
return;
|
|
113
|
+
appendTranscript(config.sessionFilePath, `${rendered}\n`);
|
|
114
|
+
out.write(`${rendered}\n`);
|
|
115
|
+
}
|
|
116
|
+
function drainLines(buffer, onLine) {
|
|
117
|
+
const lines = buffer.split("\n");
|
|
118
|
+
const rest = lines.pop() ?? "";
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (line.trim() !== "")
|
|
121
|
+
onLine(line);
|
|
122
|
+
}
|
|
123
|
+
return rest;
|
|
124
|
+
}
|
|
125
|
+
export function buildAgentEnv(baseEnv, unset) {
|
|
126
|
+
const env = { ...baseEnv };
|
|
127
|
+
for (const name of ASSISTANT_SESSION_VARIABLES)
|
|
128
|
+
delete env[name];
|
|
129
|
+
for (const name of unset) {
|
|
130
|
+
const trimmed = name.trim();
|
|
131
|
+
if (trimmed !== "")
|
|
132
|
+
delete env[trimmed];
|
|
133
|
+
}
|
|
134
|
+
return env;
|
|
135
|
+
}
|
|
136
|
+
function guardChildLifecycle(child, sessionFilePath, state) {
|
|
137
|
+
// Seal the session, ask the child to stop, then force-kill it after a short grace period so
|
|
138
|
+
// interrupted runs leave neither a stale session record nor an orphaned child process.
|
|
139
|
+
const isAlive = () => child.exitCode === null && child.signalCode === null;
|
|
140
|
+
const signalChild = (signal) => {
|
|
141
|
+
if (!isAlive())
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
child.kill(signal);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// The child exited between the liveness check and the signal.
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const forceKill = () => signalChild("SIGKILL");
|
|
151
|
+
const onSignal = async (signal) => {
|
|
152
|
+
try {
|
|
153
|
+
applyCompletion(sessionFilePath, buildTerminationUpdate(signal, state, new Date()));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Session sealing is best-effort when the file is missing or malformed.
|
|
157
|
+
}
|
|
158
|
+
signalChild("SIGTERM");
|
|
159
|
+
await waitForChildExit(child, TERMINATION_GRACE_MS);
|
|
160
|
+
forceKill();
|
|
161
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
162
|
+
};
|
|
163
|
+
const onSigint = () => onSignal("SIGINT");
|
|
164
|
+
const onSigterm = () => onSignal("SIGTERM");
|
|
165
|
+
process.on("SIGINT", onSigint);
|
|
166
|
+
process.on("SIGTERM", onSigterm);
|
|
167
|
+
process.on("exit", forceKill);
|
|
168
|
+
return () => {
|
|
169
|
+
process.off("SIGINT", onSigint);
|
|
170
|
+
process.off("SIGTERM", onSigterm);
|
|
171
|
+
process.off("exit", forceKill);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function waitForChildExit(child, graceMs) {
|
|
175
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
const onExit = () => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve();
|
|
181
|
+
};
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
child.off("exit", onExit);
|
|
184
|
+
resolve();
|
|
185
|
+
}, graceMs);
|
|
186
|
+
child.once("exit", onExit);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
export function buildTerminationUpdate(signal, state, now) {
|
|
190
|
+
return {
|
|
191
|
+
status: "failed",
|
|
192
|
+
endedAt: now.toISOString(),
|
|
193
|
+
exitReason: "terminated",
|
|
194
|
+
sessionId: state.sessionId ?? null,
|
|
195
|
+
result: `Terminated by ${signal} before completion.`,
|
|
196
|
+
contextTokens: state.contextTokens ?? null,
|
|
197
|
+
contextCompacted: isCompacted(state),
|
|
198
|
+
contextTokensError: null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Occupancy only falls when the agent compacts the conversation, so a peak above the final figure
|
|
202
|
+
// is the compaction itself.
|
|
203
|
+
export function isCompacted(state) {
|
|
204
|
+
const { contextTokens, peakContextTokens } = state;
|
|
205
|
+
if (contextTokens === undefined || peakContextTokens === undefined)
|
|
206
|
+
return false;
|
|
207
|
+
return peakContextTokens > contextTokens;
|
|
208
|
+
}
|
|
209
|
+
function nonblank(value) {
|
|
210
|
+
const trimmed = value.trim();
|
|
211
|
+
return trimmed === "" ? undefined : trimmed;
|
|
212
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CodingAgent } from "./coding-agent.js";
|
|
2
|
+
export interface SessionFrontmatter {
|
|
3
|
+
status: "running" | "succeeded" | "failed";
|
|
4
|
+
agent: CodingAgent | null;
|
|
5
|
+
protocol: string | null;
|
|
6
|
+
ticket: string | null;
|
|
7
|
+
model: string | null;
|
|
8
|
+
sessionId: string | null;
|
|
9
|
+
command: string;
|
|
10
|
+
meta: string | null;
|
|
11
|
+
pid: number | null;
|
|
12
|
+
pidStartTime: string | null;
|
|
13
|
+
cwd: string | null;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
endedAt: string | null;
|
|
16
|
+
exitReason: string | null;
|
|
17
|
+
contextTokens: number | null;
|
|
18
|
+
contextCompacted: boolean;
|
|
19
|
+
contextTokensError: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare const RESULT_MARKER = "\n---- Result ----\n";
|
|
22
|
+
export declare function resolveSessionFilePath(sessionsDir: string, ticket: string | undefined, now: Date, fileExists?: (path: string) => boolean): string;
|
|
23
|
+
export declare function writeInitialSessionFile(sessionFilePath: string, frontmatter: SessionFrontmatter): void;
|
|
24
|
+
export interface CompletionUpdate {
|
|
25
|
+
status: "succeeded" | "failed";
|
|
26
|
+
endedAt: string;
|
|
27
|
+
exitReason: "completed" | "error" | "terminated" | "auth_required";
|
|
28
|
+
sessionId: string | null;
|
|
29
|
+
result: string;
|
|
30
|
+
contextTokens?: number | null;
|
|
31
|
+
contextCompacted?: boolean;
|
|
32
|
+
contextTokensError?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export declare function applyCompletion(sessionFilePath: string, update: CompletionUpdate): void;
|
|
35
|
+
export declare function appendTranscript(sessionFilePath: string, text: string): void;
|
|
36
|
+
export interface SessionCompletion {
|
|
37
|
+
frontmatter: SessionFrontmatter;
|
|
38
|
+
result: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
export declare function readCompletion(sessionFilePath: string): SessionCompletion;
|
|
41
|
+
export declare function reconcileSessionFile(sessionFilePath: string, now?: Date): SessionCompletion;
|
|
42
|
+
export declare function readPidStartTime(pid: number): string | null;
|
|
43
|
+
export interface SessionRecord {
|
|
44
|
+
path: string;
|
|
45
|
+
frontmatter: SessionFrontmatter;
|
|
46
|
+
}
|
|
47
|
+
export declare function listSessionRecords(sessionsDir: string): SessionRecord[];
|
|
48
|
+
export declare function findNewestSessionFile(dir: string): string | undefined;
|
|
49
|
+
export declare function serializeFrontmatter(frontmatter: SessionFrontmatter): string;
|
|
50
|
+
export declare function parseFrontmatter(block: string): SessionFrontmatter;
|