ai-foreman 1.0.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 +523 -0
- package/dist/adapters/claude.js +150 -0
- package/dist/adapters/codex.js +155 -0
- package/dist/adapters/types.js +4 -0
- package/dist/cli/events.js +45 -0
- package/dist/cli/tickets.js +482 -0
- package/dist/config.js +119 -0
- package/dist/foreman.js +445 -0
- package/dist/index.js +300 -0
- package/dist/log.js +18 -0
- package/dist/markers.js +20 -0
- package/dist/notify.js +49 -0
- package/dist/permissions/policy.js +84 -0
- package/dist/roles.js +49 -0
- package/dist/tickets/blockers.js +22 -0
- package/dist/tickets/commands.js +438 -0
- package/dist/tickets/config.js +133 -0
- package/dist/tickets/events.js +19 -0
- package/dist/tickets/importer.js +11 -0
- package/dist/tickets/queue.js +62 -0
- package/dist/tickets/renderMarkdown.js +223 -0
- package/dist/tickets/stateDb.js +252 -0
- package/dist/tickets/ticketLoader.js +84 -0
- package/dist/tickets/ticketSchema.js +31 -0
- package/dist/tickets/validate.js +74 -0
- package/dist/util/asyncQueue.js +40 -0
- package/foreman.yaml +98 -0
- package/package.json +60 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { AsyncQueue } from "../util/asyncQueue.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pure function: parse one JSONL event from `codex exec --json` output into
|
|
6
|
+
* structured actions. Exported for unit testing; production code calls this
|
|
7
|
+
* via handleEvent().
|
|
8
|
+
*/
|
|
9
|
+
export function parseCodexLine(raw) {
|
|
10
|
+
const type = raw.type;
|
|
11
|
+
if (type === "thread.started") {
|
|
12
|
+
const sessionId = raw.thread_id;
|
|
13
|
+
return { events: [], sessionId };
|
|
14
|
+
}
|
|
15
|
+
if (type === "item.completed") {
|
|
16
|
+
const item = raw.item;
|
|
17
|
+
if (!item)
|
|
18
|
+
return { events: [] };
|
|
19
|
+
if (item.type === "agent_message") {
|
|
20
|
+
const text = item.text;
|
|
21
|
+
if (!text)
|
|
22
|
+
return { events: [] };
|
|
23
|
+
return { events: [{ kind: "text", text }], text };
|
|
24
|
+
}
|
|
25
|
+
if (item.type === "command_execution") {
|
|
26
|
+
return {
|
|
27
|
+
events: [{ kind: "tool", name: "command_execution", input: { command: item.command } }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { events: [] };
|
|
31
|
+
}
|
|
32
|
+
if (type === "error") {
|
|
33
|
+
const msg = raw.error?.message
|
|
34
|
+
?? JSON.stringify(raw);
|
|
35
|
+
return { events: [{ kind: "error", message: msg }] };
|
|
36
|
+
}
|
|
37
|
+
return { events: [] };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Drives Codex CLI as a subprocess. Each sendTurn() spawns `codex exec --json`
|
|
41
|
+
* and reads JSONL from stdout. Session continuity is maintained via the
|
|
42
|
+
* thread_id captured from the first run and passed as `resume <id>` on
|
|
43
|
+
* subsequent turns.
|
|
44
|
+
*
|
|
45
|
+
* NOTE: opts.permission is accepted for interface conformance but is not
|
|
46
|
+
* invoked. Codex manages its own tool sandboxing via --sandbox workspace-write.
|
|
47
|
+
*/
|
|
48
|
+
export class CodexAdapter {
|
|
49
|
+
opts;
|
|
50
|
+
agent = "codex";
|
|
51
|
+
eventQueue = new AsyncQueue();
|
|
52
|
+
_sessionId;
|
|
53
|
+
_closed = false;
|
|
54
|
+
_activeProc;
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
this.opts = opts;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Prepend role system text to a turn instruction when `systemPromptAppend` is
|
|
60
|
+
* set. Codex reads everything from the prompt (no persistent system prompt), so
|
|
61
|
+
* role guidance must ride along with each turn.
|
|
62
|
+
*/
|
|
63
|
+
buildInstruction(instruction) {
|
|
64
|
+
return this.opts.systemPromptAppend
|
|
65
|
+
? `${this.opts.systemPromptAppend}\n\n${instruction}`
|
|
66
|
+
: instruction;
|
|
67
|
+
}
|
|
68
|
+
/** Build the `codex exec` argument list for the given instruction. */
|
|
69
|
+
buildArgs(instruction) {
|
|
70
|
+
const args = ["exec", "--json", "--sandbox", "workspace-write", "-C", this.opts.cwd];
|
|
71
|
+
if (this.opts.model)
|
|
72
|
+
args.push("-m", this.opts.model);
|
|
73
|
+
if (this.opts.effort) {
|
|
74
|
+
args.push("-c", `model_reasoning_effort=${this.opts.effort}`);
|
|
75
|
+
}
|
|
76
|
+
else if (this.opts.fast) {
|
|
77
|
+
args.push("-c", "model_reasoning_effort=low");
|
|
78
|
+
}
|
|
79
|
+
if (this._sessionId) {
|
|
80
|
+
args.push("resume", this._sessionId);
|
|
81
|
+
}
|
|
82
|
+
args.push(instruction);
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
sendTurn(instruction) {
|
|
86
|
+
if (this._closed)
|
|
87
|
+
return Promise.reject(new Error("builder is closed"));
|
|
88
|
+
const args = this.buildArgs(this.buildInstruction(instruction));
|
|
89
|
+
const textParts = [];
|
|
90
|
+
const stderrChunks = [];
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const proc = spawn("codex", args, {
|
|
93
|
+
cwd: this.opts.cwd,
|
|
94
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
95
|
+
});
|
|
96
|
+
this._activeProc = proc;
|
|
97
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
98
|
+
rl.on("line", (line) => {
|
|
99
|
+
if (!line.trim())
|
|
100
|
+
return;
|
|
101
|
+
let raw;
|
|
102
|
+
try {
|
|
103
|
+
raw = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const result = parseCodexLine(raw);
|
|
109
|
+
if (result.sessionId)
|
|
110
|
+
this._sessionId = result.sessionId;
|
|
111
|
+
if (result.text)
|
|
112
|
+
textParts.push(result.text);
|
|
113
|
+
for (const ev of result.events)
|
|
114
|
+
this.eventQueue.push(ev);
|
|
115
|
+
});
|
|
116
|
+
proc.stderr.on("data", (chunk) => {
|
|
117
|
+
stderrChunks.push(chunk);
|
|
118
|
+
});
|
|
119
|
+
proc.on("close", (code) => {
|
|
120
|
+
this._activeProc = undefined;
|
|
121
|
+
let text = textParts.join("\n");
|
|
122
|
+
if (!text && code !== 0) {
|
|
123
|
+
text = Buffer.concat(stderrChunks).toString().trim();
|
|
124
|
+
}
|
|
125
|
+
const result = {
|
|
126
|
+
text,
|
|
127
|
+
isError: code !== 0,
|
|
128
|
+
numTurns: 1,
|
|
129
|
+
costUsd: 0,
|
|
130
|
+
};
|
|
131
|
+
this.eventQueue.push({ kind: "turn-complete", result });
|
|
132
|
+
resolve(result);
|
|
133
|
+
});
|
|
134
|
+
proc.on("error", (err) => {
|
|
135
|
+
this._activeProc = undefined;
|
|
136
|
+
this.eventQueue.push({ kind: "error", message: err.message });
|
|
137
|
+
reject(err);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
sessionId() {
|
|
142
|
+
return this._sessionId;
|
|
143
|
+
}
|
|
144
|
+
events() {
|
|
145
|
+
return this.eventQueue;
|
|
146
|
+
}
|
|
147
|
+
close() {
|
|
148
|
+
if (this._closed)
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
this._closed = true;
|
|
151
|
+
this._activeProc?.kill("SIGTERM");
|
|
152
|
+
this.eventQueue.close();
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Print a compact live feed of builder activity. */
|
|
2
|
+
export async function printEvents(events) {
|
|
3
|
+
let atLineStart = true;
|
|
4
|
+
for await (const ev of events) {
|
|
5
|
+
if (ev.kind === "text") {
|
|
6
|
+
if (ev.text) {
|
|
7
|
+
process.stdout.write(ev.text);
|
|
8
|
+
atLineStart = ev.text.endsWith("\n");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
else if (ev.kind === "tool") {
|
|
12
|
+
if (!atLineStart) {
|
|
13
|
+
process.stdout.write("\n");
|
|
14
|
+
atLineStart = true;
|
|
15
|
+
}
|
|
16
|
+
console.log(` -> ${ev.name} ${briefInput(ev.input)}`);
|
|
17
|
+
}
|
|
18
|
+
else if (ev.kind === "turn-complete") {
|
|
19
|
+
if (!atLineStart) {
|
|
20
|
+
process.stdout.write("\n");
|
|
21
|
+
atLineStart = true;
|
|
22
|
+
}
|
|
23
|
+
const tag = ev.result.isError ? "turn errored" : "turn complete";
|
|
24
|
+
const cost = ev.result.costUsd > 0 ? ` ($${ev.result.costUsd.toFixed(4)})` : "";
|
|
25
|
+
console.log(` - ${tag}${cost}`);
|
|
26
|
+
}
|
|
27
|
+
else if (ev.kind === "error") {
|
|
28
|
+
if (!atLineStart) {
|
|
29
|
+
process.stdout.write("\n");
|
|
30
|
+
atLineStart = true;
|
|
31
|
+
}
|
|
32
|
+
console.log(` ! error: ${ev.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!atLineStart)
|
|
36
|
+
process.stdout.write("\n");
|
|
37
|
+
}
|
|
38
|
+
function briefInput(input) {
|
|
39
|
+
if (input && typeof input === "object") {
|
|
40
|
+
const o = input;
|
|
41
|
+
const key = o.command ?? o.file_path ?? o.path ?? o.pattern ?? "";
|
|
42
|
+
return String(key).slice(0, 80);
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
}
|