aligndev 0.0.0 → 0.18.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.
Files changed (73) hide show
  1. package/README.md +166 -1
  2. package/bin/aligndev.mjs +3 -0
  3. package/dist/alignfirst-cli.d.ts +10 -0
  4. package/dist/alignfirst-cli.js +56 -0
  5. package/dist/cli.d.ts +15 -0
  6. package/dist/cli.js +95 -0
  7. package/dist/code/claude-agent.d.ts +14 -0
  8. package/dist/code/claude-agent.js +168 -0
  9. package/dist/code/code-cli.d.ts +72 -0
  10. package/dist/code/code-cli.js +630 -0
  11. package/dist/code/codex-agent.d.ts +15 -0
  12. package/dist/code/codex-agent.js +168 -0
  13. package/dist/code/codex-rollout.d.ts +18 -0
  14. package/dist/code/codex-rollout.js +114 -0
  15. package/dist/code/coding-agent.d.ts +4 -0
  16. package/dist/code/coding-agent.js +6 -0
  17. package/dist/code/models.d.ts +14 -0
  18. package/dist/code/models.js +89 -0
  19. package/dist/code/prompt.d.ts +11 -0
  20. package/dist/code/prompt.js +26 -0
  21. package/dist/code/quota.d.ts +25 -0
  22. package/dist/code/quota.js +248 -0
  23. package/dist/code/run-agent.d.ts +60 -0
  24. package/dist/code/run-agent.js +212 -0
  25. package/dist/code/session-file.d.ts +50 -0
  26. package/dist/code/session-file.js +263 -0
  27. package/dist/command-form.d.ts +6 -0
  28. package/dist/command-form.js +7 -0
  29. package/dist/config.d.ts +22 -0
  30. package/dist/config.js +72 -0
  31. package/dist/errors.d.ts +2 -0
  32. package/dist/errors.js +6 -0
  33. package/dist/guide/code-guide.d.ts +4 -0
  34. package/dist/guide/code-guide.js +17 -0
  35. package/dist/guide/guide-cli.d.ts +3 -0
  36. package/dist/guide/guide-cli.js +81 -0
  37. package/dist/guide/render-template.d.ts +4 -0
  38. package/dist/guide/render-template.js +62 -0
  39. package/dist/guide/topics.d.ts +3 -0
  40. package/dist/guide/topics.js +16 -0
  41. package/dist/output.d.ts +3 -0
  42. package/dist/output.js +1 -0
  43. package/dist/project/discovery.d.ts +42 -0
  44. package/dist/project/discovery.js +287 -0
  45. package/dist/project/format.d.ts +6 -0
  46. package/dist/project/format.js +31 -0
  47. package/dist/project/guide.d.ts +3 -0
  48. package/dist/project/guide.js +69 -0
  49. package/dist/project/layout.d.ts +36 -0
  50. package/dist/project/layout.js +128 -0
  51. package/dist/project/markers.d.ts +18 -0
  52. package/dist/project/markers.js +90 -0
  53. package/dist/project/ports.d.ts +3 -0
  54. package/dist/project/ports.js +55 -0
  55. package/dist/project/project-cli.d.ts +17 -0
  56. package/dist/project/project-cli.js +227 -0
  57. package/dist/project/render.d.ts +10 -0
  58. package/dist/project/render.js +144 -0
  59. package/dist/project/status.d.ts +24 -0
  60. package/dist/project/status.js +110 -0
  61. package/dist/templates.d.ts +1 -0
  62. package/dist/templates.js +5 -0
  63. package/package.json +38 -3
  64. package/templates/guide/code.md +252 -0
  65. package/templates/guide/playbook/channel-handling.md +110 -0
  66. package/templates/guide/playbook/consultation.md +71 -0
  67. package/templates/guide/playbook/discord-message-tool.md +37 -0
  68. package/templates/guide/playbook/playbook.md +139 -0
  69. package/templates/guide/playbook/project-lifecycle.md +100 -0
  70. package/templates/guide/playbook/project-workspace-setup.md +186 -0
  71. package/templates/guide/playbook/slack-message-tool.md +23 -0
  72. package/templates/guide/playbook/working-session.md +588 -0
  73. package/templates/guide/project.md +33 -0
@@ -0,0 +1,168 @@
1
+ import { readCodexContext } from "./codex-rollout.js";
2
+ export function createCodexAdapter() {
3
+ return {
4
+ executable: "codex",
5
+ buildArgs: buildCodexArgs,
6
+ createState: createCodexState,
7
+ interpretLine: interpretCodexLine,
8
+ assess: assessCodexState,
9
+ isAuthenticationError: isCodexAuthenticationMessage,
10
+ authenticationMessage: codexAuthenticationMessage,
11
+ failureMessage: missingCodexOutcome,
12
+ };
13
+ }
14
+ export function buildCodexArgs(config) {
15
+ const args = ["exec", "--json"];
16
+ if (config.skipPermissions) {
17
+ args.push("--dangerously-bypass-approvals-and-sandbox");
18
+ }
19
+ else {
20
+ args.push("--sandbox", "workspace-write");
21
+ }
22
+ // An `exec` option, like `--sandbox`: `exec resume` does not accept it. The session does not
23
+ // store it, so a resume passes it again.
24
+ for (const dir of config.additionalDirectories)
25
+ args.push("--add-dir", dir);
26
+ if (config.executableModel !== undefined)
27
+ args.push("--model", config.executableModel);
28
+ if (config.resume !== undefined)
29
+ args.push("resume", config.resume);
30
+ args.push("-");
31
+ return args;
32
+ }
33
+ export function createCodexState() {
34
+ return {
35
+ protocolComplete: false,
36
+ protocolFailed: false,
37
+ authEvidence: false,
38
+ };
39
+ }
40
+ export function interpretCodexLine(line, state) {
41
+ const event = parseCodexLine(line);
42
+ if (!isRecord(event))
43
+ return;
44
+ switch (event.type) {
45
+ case "thread.started":
46
+ return captureThread(event, state);
47
+ case "item.completed":
48
+ return captureCompletedItem(event, state);
49
+ case "turn.completed":
50
+ state.protocolComplete = true;
51
+ captureStreamTotal(event, state);
52
+ return;
53
+ case "turn.failed":
54
+ case "error":
55
+ return captureFailure(event, state);
56
+ case "unparsed":
57
+ return `[unparsed] ${asString(event.raw) ?? ""}`;
58
+ default:
59
+ return;
60
+ }
61
+ }
62
+ export function assessCodexState(state) {
63
+ const succeeded = state.protocolComplete && !state.protocolFailed && state.result !== undefined;
64
+ const context = readCodexContext(state.sessionId, state.streamTotalTokens);
65
+ return {
66
+ succeeded,
67
+ sessionId: state.sessionId,
68
+ result: state.result,
69
+ error: succeeded ? undefined : state.failure,
70
+ authEvidence: state.authEvidence,
71
+ contextTokens: context.contextTokens ?? undefined,
72
+ contextCompacted: context.compacted,
73
+ contextTokensError: context.error,
74
+ };
75
+ }
76
+ // The `turn.completed` usage is the thread's cumulative total, not its context occupancy. It serves
77
+ // only to pin the rollout file to this run; `codex-rollout.ts` reads the occupancy itself. Codex
78
+ // counts cached input inside `input_tokens`, so adding `cached_input_tokens` would double it.
79
+ function captureStreamTotal(event, state) {
80
+ const usage = event.usage;
81
+ if (!isRecord(usage))
82
+ return;
83
+ const total = asCount(usage.input_tokens) + asCount(usage.output_tokens);
84
+ if (total > 0)
85
+ state.streamTotalTokens = total;
86
+ }
87
+ function parseCodexLine(line) {
88
+ try {
89
+ return JSON.parse(line);
90
+ }
91
+ catch {
92
+ return { type: "unparsed", raw: line };
93
+ }
94
+ }
95
+ function captureThread(event, state) {
96
+ const id = asString(event.thread_id);
97
+ if (id !== undefined && id !== "")
98
+ state.sessionId = id;
99
+ return `[init] session ${id ?? "?"}`;
100
+ }
101
+ function captureCompletedItem(event, state) {
102
+ const item = event.item;
103
+ if (!isRecord(item))
104
+ return;
105
+ if (item.type === "agent_message") {
106
+ const text = asString(item.text);
107
+ if (text !== undefined && text !== "")
108
+ state.result = text;
109
+ return text;
110
+ }
111
+ if (item.type !== "error")
112
+ return;
113
+ const message = extractMessage(item);
114
+ if (message === undefined)
115
+ return;
116
+ recordFailure(message, state);
117
+ return `[error] ${message}`;
118
+ }
119
+ function captureFailure(event, state) {
120
+ const message = extractMessage(event);
121
+ state.protocolFailed = true;
122
+ if (message === undefined)
123
+ return;
124
+ recordFailure(message, state);
125
+ return `[error] ${message}`;
126
+ }
127
+ function recordFailure(message, state) {
128
+ state.protocolFailed = true;
129
+ state.failure = message;
130
+ if (isCodexAuthenticationMessage(message))
131
+ state.authEvidence = true;
132
+ }
133
+ function extractMessage(value) {
134
+ const direct = asString(value.message);
135
+ if (direct !== undefined)
136
+ return direct;
137
+ const error = value.error;
138
+ if (typeof error === "string")
139
+ return error;
140
+ if (!isRecord(error))
141
+ return;
142
+ return asString(error.message);
143
+ }
144
+ function isCodexAuthenticationMessage(message) {
145
+ const normalized = message.trim();
146
+ return (/^(?:error:\s*)?(?:you are |user is )?not logged in\b/i.test(normalized) ||
147
+ /^(?:error:\s*)?.*\brun [`']?codex login[`']?\b/i.test(normalized));
148
+ }
149
+ function missingCodexOutcome(state) {
150
+ if (!state.protocolComplete)
151
+ return "Codex stream ended before turn.completed.";
152
+ return "Codex turn completed without a final agent message.";
153
+ }
154
+ function codexAuthenticationMessage(detail) {
155
+ const base = "Coding agent not authenticated: an administrator must run `codex login` on the host before " +
156
+ "`aligndev code` can run again.";
157
+ const reason = detail?.trim();
158
+ return reason === undefined || reason === "" ? base : `${base}\n\n${reason}`;
159
+ }
160
+ function isRecord(value) {
161
+ return typeof value === "object" && value !== null;
162
+ }
163
+ function asString(value) {
164
+ return typeof value === "string" ? value : undefined;
165
+ }
166
+ function asCount(value) {
167
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
168
+ }
@@ -0,0 +1,18 @@
1
+ export interface CodexContextReading {
2
+ contextTokens: number | null;
3
+ compacted: boolean;
4
+ error?: string;
5
+ }
6
+ /**
7
+ * Context-window occupancy of a finished Codex run, read from its rollout file.
8
+ *
9
+ * `codex exec --json` reports only cumulative totals: the `usage` of its `turn.completed` event
10
+ * sums every model request of the thread, so it passes the context window on any run with a few
11
+ * tool calls. The rollout holds the figure Codex itself uses for "% context left" —
12
+ * `last_token_usage`, the newest request's occupancy.
13
+ *
14
+ * `streamTotal` is the total taken from that `turn.completed` event. The rollout's own total must
15
+ * equal it, which pins the file to this run and turns any change in the Codex format into a
16
+ * reported error rather than a plausible wrong number.
17
+ */
18
+ export declare function readCodexContext(threadId: string | undefined, streamTotal: number | undefined): CodexContextReading;
@@ -0,0 +1,114 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ // Codex writes one rollout per thread under `$CODEX_HOME/sessions/<year>/<month>/<day>/`. The plain
5
+ // file is `rollout-<timestamp>-<threadId>.jsonl`; a compacted thread appends `_<uuid>`.
6
+ const SESSIONS_DIR = "sessions";
7
+ const ROLLOUT_PREFIX = "rollout-";
8
+ const ROLLOUT_SUFFIX = ".jsonl";
9
+ /**
10
+ * Context-window occupancy of a finished Codex run, read from its rollout file.
11
+ *
12
+ * `codex exec --json` reports only cumulative totals: the `usage` of its `turn.completed` event
13
+ * sums every model request of the thread, so it passes the context window on any run with a few
14
+ * tool calls. The rollout holds the figure Codex itself uses for "% context left" —
15
+ * `last_token_usage`, the newest request's occupancy.
16
+ *
17
+ * `streamTotal` is the total taken from that `turn.completed` event. The rollout's own total must
18
+ * equal it, which pins the file to this run and turns any change in the Codex format into a
19
+ * reported error rather than a plausible wrong number.
20
+ */
21
+ export function readCodexContext(threadId, streamTotal) {
22
+ if (threadId === undefined || threadId === "")
23
+ return failed("Codex reported no thread id.");
24
+ if (streamTotal === undefined)
25
+ return failed("Codex reported no turn.completed usage.");
26
+ const path = findRolloutPath(threadId);
27
+ if (path === undefined)
28
+ return failed(`No Codex rollout found for thread ${threadId}.`);
29
+ const records = readUsageRecords(path);
30
+ if (records.length === 0)
31
+ return failed(`No token_count record in ${basename(path)}.`);
32
+ const last = records[records.length - 1];
33
+ if (last.total !== streamTotal) {
34
+ return failed(`Codex rollout total ${last.total} does not match the stream total ${streamTotal}; ` +
35
+ "the rollout format may have changed.");
36
+ }
37
+ // Occupancy only ever falls when Codex compacts the thread, so an earlier reading above the
38
+ // final one is the compaction itself. Matching the rollout's compaction records instead would be
39
+ // brittle: they sit nested inside response items, not as their own event type.
40
+ const peak = Math.max(...records.map((record) => record.occupancy));
41
+ return { contextTokens: last.occupancy, compacted: peak > last.occupancy };
42
+ }
43
+ function failed(error) {
44
+ return { contextTokens: null, compacted: false, error };
45
+ }
46
+ function findRolloutPath(threadId) {
47
+ const root = join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), SESSIONS_DIR);
48
+ let entries;
49
+ try {
50
+ entries = readdirSync(root, { recursive: true, encoding: "utf8" });
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ const matches = entries.filter((entry) => matchesThread(basename(entry), threadId)).sort();
56
+ // Timestamps sort lexically, so the last match is the newest rollout of the thread.
57
+ const newest = matches[matches.length - 1];
58
+ return newest === undefined ? undefined : join(root, newest);
59
+ }
60
+ function matchesThread(name, threadId) {
61
+ return (name.startsWith(ROLLOUT_PREFIX) && name.endsWith(ROLLOUT_SUFFIX) && name.includes(threadId));
62
+ }
63
+ function readUsageRecords(path) {
64
+ let content;
65
+ try {
66
+ content = readFileSync(path, "utf8");
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ const records = [];
72
+ for (const line of content.split("\n")) {
73
+ if (!line.includes('"token_count"'))
74
+ continue;
75
+ const record = parseUsageRecord(line);
76
+ if (record !== undefined)
77
+ records.push(record);
78
+ }
79
+ return records;
80
+ }
81
+ function parseUsageRecord(line) {
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(line);
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ if (!isRecord(parsed))
90
+ return;
91
+ const payload = parsed.payload;
92
+ if (!isRecord(payload) || payload.type !== "token_count")
93
+ return;
94
+ const info = payload.info;
95
+ if (!isRecord(info))
96
+ return;
97
+ const occupancy = sumUsage(info.last_token_usage);
98
+ const total = sumUsage(info.total_token_usage);
99
+ if (occupancy === undefined || total === undefined)
100
+ return;
101
+ return { occupancy, total };
102
+ }
103
+ // Codex counts cached input inside `input_tokens`, so adding `cached_input_tokens` would double it.
104
+ function sumUsage(usage) {
105
+ if (!isRecord(usage))
106
+ return;
107
+ return asCount(usage.input_tokens) + asCount(usage.output_tokens);
108
+ }
109
+ function isRecord(value) {
110
+ return typeof value === "object" && value !== null;
111
+ }
112
+ function asCount(value) {
113
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
114
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentAdapter } from "./run-agent.js";
2
+ export declare const CODING_AGENTS: readonly ["claude", "codex"];
3
+ export type CodingAgent = (typeof CODING_AGENTS)[number];
4
+ export declare function createAgentAdapter(agent: CodingAgent): AgentAdapter;
@@ -0,0 +1,6 @@
1
+ import { createClaudeAdapter } from "./claude-agent.js";
2
+ import { createCodexAdapter } from "./codex-agent.js";
3
+ export const CODING_AGENTS = ["claude", "codex"];
4
+ export function createAgentAdapter(agent) {
5
+ return agent === "claude" ? createClaudeAdapter() : createCodexAdapter();
6
+ }
@@ -0,0 +1,14 @@
1
+ import type { CodingAgent } from "./coding-agent.js";
2
+ export declare const CLAUDE_DEFAULT_MODELS: readonly ["fable", "opus", "sonnet", "haiku"];
3
+ export declare const CODEX_DEFAULT_MODELS: readonly ["astra", "sol", "terra", "luna"];
4
+ export declare function resolveModels(agent: CodingAgent, override: readonly string[] | undefined): readonly string[];
5
+ export interface ModelDiscoveryContext {
6
+ cwd: string;
7
+ env: NodeJS.ProcessEnv;
8
+ }
9
+ export type ModelCatalogRunner = (context: ModelDiscoveryContext) => Promise<string>;
10
+ export type ExecutableModelResolver = (agent: CodingAgent, selectedModel: string | undefined, context: ModelDiscoveryContext) => Promise<string | undefined>;
11
+ export declare function createExecutableModelResolver(runCatalog?: ModelCatalogRunner): ExecutableModelResolver;
12
+ export declare const resolveExecutableModel: ExecutableModelResolver;
13
+ export declare function parseBundledModelSlugs(output: string): readonly string[];
14
+ export declare function selectNewestCodexModel(slugs: readonly string[], alias: string): string | undefined;
@@ -0,0 +1,89 @@
1
+ import { execFile } from "node:child_process";
2
+ export const CLAUDE_DEFAULT_MODELS = ["fable", "opus", "sonnet", "haiku"];
3
+ export const CODEX_DEFAULT_MODELS = ["astra", "sol", "terra", "luna"];
4
+ const CODEX_ALIASES = new Set(CODEX_DEFAULT_MODELS);
5
+ const CODEX_MODEL_PATTERN = new RegExp(`^gpt-([0-9]+)(?:\\.([0-9]+))?-(${CODEX_DEFAULT_MODELS.join("|")})$`);
6
+ const CODEX_CATALOG_MAX_BUFFER = 8 * 1024 * 1024;
7
+ const CODEX_CATALOG_TIMEOUT_MS = 30_000;
8
+ export function resolveModels(agent, override) {
9
+ if (override !== undefined && override.length > 0)
10
+ return override;
11
+ return agent === "claude" ? CLAUDE_DEFAULT_MODELS : CODEX_DEFAULT_MODELS;
12
+ }
13
+ export function createExecutableModelResolver(runCatalog = runCodexModelCatalog) {
14
+ let cachedSlugs;
15
+ return async (agent, selectedModel, context) => {
16
+ if (selectedModel === undefined)
17
+ return;
18
+ if (agent === "claude" || !CODEX_ALIASES.has(selectedModel))
19
+ return selectedModel;
20
+ try {
21
+ cachedSlugs ??= parseBundledModelSlugs(await runCatalog(context));
22
+ const slug = selectNewestCodexModel(cachedSlugs, selectedModel);
23
+ if (slug !== undefined)
24
+ return slug;
25
+ throw new Error("the bundled catalog contains no matching model");
26
+ }
27
+ catch (error) {
28
+ throw new Error(codexAliasError(selectedModel, error));
29
+ }
30
+ };
31
+ }
32
+ export const resolveExecutableModel = createExecutableModelResolver();
33
+ export function parseBundledModelSlugs(output) {
34
+ let value;
35
+ try {
36
+ value = JSON.parse(output);
37
+ }
38
+ catch {
39
+ throw new Error("Codex returned malformed JSON");
40
+ }
41
+ if (!isRecord(value) || !Array.isArray(value.models)) {
42
+ throw new Error("Codex returned an unexpected model catalog shape");
43
+ }
44
+ const slugs = [];
45
+ for (const model of value.models) {
46
+ if (!isRecord(model) || typeof model.slug !== "string") {
47
+ throw new Error("Codex returned an unexpected model catalog shape");
48
+ }
49
+ slugs.push(model.slug);
50
+ }
51
+ return slugs;
52
+ }
53
+ export function selectNewestCodexModel(slugs, alias) {
54
+ const matches = slugs
55
+ .map((slug) => ({ slug, match: slug.match(CODEX_MODEL_PATTERN) }))
56
+ .filter((candidate) => candidate.match?.[3] === alias)
57
+ .sort((a, b) => {
58
+ const major = Number(b.match?.[1]) - Number(a.match?.[1]);
59
+ return major !== 0 ? major : Number(b.match?.[2] ?? 0) - Number(a.match?.[2] ?? 0);
60
+ });
61
+ return matches[0]?.slug;
62
+ }
63
+ function runCodexModelCatalog(context) {
64
+ return new Promise((resolve, reject) => {
65
+ const child = execFile("codex", ["debug", "models", "--bundled"], {
66
+ cwd: context.cwd,
67
+ env: context.env,
68
+ encoding: "utf8",
69
+ maxBuffer: CODEX_CATALOG_MAX_BUFFER,
70
+ timeout: CODEX_CATALOG_TIMEOUT_MS,
71
+ }, (error, stdout, stderr) => {
72
+ if (error) {
73
+ reject(new Error(stderr.trim() || error.message));
74
+ return;
75
+ }
76
+ resolve(stdout);
77
+ });
78
+ child.stdin?.end();
79
+ });
80
+ }
81
+ function codexAliasError(alias, error) {
82
+ const detail = error instanceof Error ? error.message : String(error);
83
+ return (`Unable to resolve Codex model alias ${JSON.stringify(alias)}: ${detail}. ` +
84
+ "Update the Codex CLI, or set `code.models` in the aligndev config to an explicit full model " +
85
+ "slug and select that value.");
86
+ }
87
+ function isRecord(value) {
88
+ return typeof value === "object" && value !== null;
89
+ }
@@ -0,0 +1,11 @@
1
+ export declare const PROTOCOLS: readonly ["spec", "plan", "aad", "description", "review", "merge"];
2
+ export type Protocol = (typeof PROTOCOLS)[number];
3
+ export interface PromptInput {
4
+ protocol?: string;
5
+ ticket?: string;
6
+ message?: string;
7
+ catchupContent?: string;
8
+ contextContent?: string;
9
+ alignfirst: string;
10
+ }
11
+ export declare function buildPrompt(input: PromptInput): string;
@@ -0,0 +1,26 @@
1
+ export const PROTOCOLS = ["spec", "plan", "aad", "description", "review", "merge"];
2
+ export function buildPrompt(input) {
3
+ const { protocol, ticket, message, catchupContent, contextContent, alignfirst } = input;
4
+ const instruction = protocol === undefined ? message : buildProtocolPrompt(alignfirst, protocol, ticket, message);
5
+ if (catchupContent === undefined && contextContent === undefined)
6
+ return instruction ?? "";
7
+ const sections = [];
8
+ if (contextContent !== undefined)
9
+ sections.push("## Project context", contextContent.trimEnd());
10
+ if (catchupContent !== undefined)
11
+ sections.push("## Ticket history", catchupContent);
12
+ sections.push("## Current instruction", currentInstruction(instruction, catchupContent));
13
+ return sections.join("\n\n");
14
+ }
15
+ function currentInstruction(instruction, catchupContent) {
16
+ const blank = instruction === undefined || instruction.trim() === "";
17
+ if (blank && catchupContent !== undefined) {
18
+ return "Summarize the ticket history briefly, including what remains unfinished.";
19
+ }
20
+ return instruction ?? "";
21
+ }
22
+ function buildProtocolPrompt(alignfirst, protocol, ticket, message) {
23
+ const ticketPart = ticket === undefined ? "" : ` Ticket ID = ${ticket}.`;
24
+ const messagePart = message === undefined ? "" : `\n\n${message}`;
25
+ return `Run \`${alignfirst} guide ${protocol}\` and follow the protocol.${ticketPart}${messagePart}`;
26
+ }
@@ -0,0 +1,25 @@
1
+ import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
+ import type { CodingAgent } from "./coding-agent.js";
3
+ export declare const readQuota: QuotaReader;
4
+ export interface QuotaContext {
5
+ cwd: string;
6
+ env: NodeJS.ProcessEnv;
7
+ unset: readonly string[];
8
+ }
9
+ export type QuotaReader = (agent: CodingAgent, context: QuotaContext) => Promise<string>;
10
+ export interface QuotaProcessAdapter {
11
+ execute(file: string, args: string[], options: QuotaProcessOptions & {
12
+ timeout: number;
13
+ }): Promise<{
14
+ stdout: string;
15
+ }>;
16
+ spawn(file: string, args: string[], options: QuotaProcessOptions): ChildProcessWithoutNullStreams;
17
+ }
18
+ interface QuotaProcessOptions {
19
+ cwd: string;
20
+ env: NodeJS.ProcessEnv;
21
+ }
22
+ export declare function createQuotaReader(adapter?: QuotaProcessAdapter, timeoutMs?: number): QuotaReader;
23
+ export declare function parseClaudeQuota(stdout: string): string;
24
+ export declare function formatCodexQuota(response: unknown, formatTime?: (timestampSeconds: number) => string): string;
25
+ export {};