ai-foreman 1.0.1 → 1.0.2
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/dist/adapters/claude.d.ts +34 -0
- package/dist/adapters/codex.d.ts +46 -0
- package/dist/adapters/types.d.ts +72 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +71 -0
- package/dist/cli/events.d.ts +3 -0
- package/dist/cli/start.d.ts +2 -0
- package/dist/cli/start.js +181 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +43 -0
- package/dist/cli/tickets.d.ts +3 -0
- package/dist/config.d.ts +27 -0
- package/dist/foreman.d.ts +74 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -284
- package/dist/log.d.ts +12 -0
- package/dist/markers.d.ts +6 -0
- package/dist/notify.d.ts +9 -0
- package/dist/permissions/policy.d.ts +20 -0
- package/dist/roles.d.ts +24 -0
- package/dist/tickets/blockers.d.ts +5 -0
- package/dist/tickets/commands.d.ts +89 -0
- package/dist/tickets/config.d.ts +40 -0
- package/dist/tickets/events.d.ts +14 -0
- package/dist/tickets/importer.d.ts +1 -0
- package/dist/tickets/queue.d.ts +36 -0
- package/dist/tickets/renderMarkdown.d.ts +15 -0
- package/dist/tickets/stateDb.d.ts +90 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketSchema.d.ts +170 -0
- package/dist/tickets/validate.d.ts +10 -0
- package/dist/util/asyncQueue.d.ts +12 -0
- package/package.json +20 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
-
import { select, text, isCancel } from "@clack/prompts";
|
|
7
|
-
import { loadConfig } from "./config.js";
|
|
8
|
-
import { Log } from "./log.js";
|
|
9
|
-
import { PermissionPolicy } from "./permissions/policy.js";
|
|
10
|
-
import { ClaudeAdapter } from "./adapters/claude.js";
|
|
11
|
-
import { CodexAdapter } from "./adapters/codex.js";
|
|
12
|
-
import { Foreman, createPermissionHandler } from "./foreman.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
13
4
|
import { buildTicketsCommand } from "./cli/tickets.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { loadRoleBundle } from "./roles.js";
|
|
5
|
+
import { buildStartCommand } from "./cli/start.js";
|
|
6
|
+
import { buildStatusCommand } from "./cli/status.js";
|
|
7
|
+
import { buildDoctorCommand } from "./cli/doctor.js";
|
|
18
8
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))?.version;
|
|
19
9
|
const program = new Command();
|
|
20
10
|
program
|
|
@@ -22,279 +12,15 @@ program
|
|
|
22
12
|
.description("Keep Codex / Claude Code builders moving through their step list.")
|
|
23
13
|
.version(PACKAGE_VERSION);
|
|
24
14
|
program.addCommand(buildTicketsCommand());
|
|
25
|
-
program
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.argument("<project>", "path to the project directory the builder works in")
|
|
29
|
-
.requiredOption("-s, --steps <n>", "number of steps to drive")
|
|
30
|
-
.option("-a, --agent <agent>", "builder agent (claude | codex)", "claude")
|
|
31
|
-
.option("-m, --model <model>", "override the builder's model")
|
|
32
|
-
.option("-r, --resume <sessionId>", "resume a prior builder session")
|
|
33
|
-
.option("--continue", "resume the most recent logged session for this project")
|
|
34
|
-
.option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
|
|
35
|
-
.option("-y, --yes", "skip pre-flight confirmation prompt")
|
|
36
|
-
.option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
|
|
37
|
-
.option("--fast", "fast mode — lower latency (maps to effort=low for codex)")
|
|
38
|
-
.option("--no-qa", "disable per-ticket QA review (enabled by default)")
|
|
39
|
-
.action(async (project, opts) => {
|
|
40
|
-
const steps = Number.parseInt(opts.steps, 10);
|
|
41
|
-
if (!Number.isInteger(steps) || steps < 1) {
|
|
42
|
-
fail("--steps must be a positive integer");
|
|
43
|
-
}
|
|
44
|
-
const VALID_AGENTS = ["claude", "codex"];
|
|
45
|
-
if (!VALID_AGENTS.includes(opts.agent)) {
|
|
46
|
-
fail(`unknown agent "${opts.agent}" — choose: ${VALID_AGENTS.join(" | ")}`);
|
|
47
|
-
}
|
|
48
|
-
const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
49
|
-
if (opts.effort && !VALID_EFFORT.includes(opts.effort)) {
|
|
50
|
-
fail(`unknown effort "${opts.effort}" — choose: ${VALID_EFFORT.join(" | ")}`);
|
|
51
|
-
}
|
|
52
|
-
const cwd = resolve(project);
|
|
53
|
-
if (!existsSync(cwd))
|
|
54
|
-
fail(`project directory not found: ${cwd}`);
|
|
55
|
-
// Locate ticket progress tracker: explicit --tickets > auto-detect standard paths
|
|
56
|
-
const TRACKER_SEARCH_PATHS = ["docs/ticket-progress.md", "ticket-progress.md"];
|
|
57
|
-
let ticketsContent;
|
|
58
|
-
let trackerRelPath;
|
|
59
|
-
if (opts.tickets) {
|
|
60
|
-
const ticketPath = resolve(opts.tickets);
|
|
61
|
-
if (!existsSync(ticketPath))
|
|
62
|
-
fail(`ticket file not found: ${ticketPath}`);
|
|
63
|
-
ticketsContent = readFileSync(ticketPath, "utf8");
|
|
64
|
-
trackerRelPath = relative(cwd, ticketPath);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
for (const rel of TRACKER_SEARCH_PATHS) {
|
|
68
|
-
const abs = join(cwd, rel);
|
|
69
|
-
if (existsSync(abs)) {
|
|
70
|
-
ticketsContent = readFileSync(abs, "utf8");
|
|
71
|
-
trackerRelPath = rel;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (opts.resume && opts.continue) {
|
|
77
|
-
fail("choose either --resume <sessionId> or --continue, not both");
|
|
78
|
-
}
|
|
79
|
-
const resumeSessionId = opts.resume ??
|
|
80
|
-
(opts.continue ? findLastSessionId(join(cwd, ".foreman")) : undefined);
|
|
81
|
-
if (opts.continue && !resumeSessionId) {
|
|
82
|
-
fail(`no previous session id found under ${join(cwd, ".foreman")}`);
|
|
83
|
-
}
|
|
84
|
-
const config = loadConfig(join(cwd, "foreman.yaml"));
|
|
85
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
86
|
-
const logPath = join(cwd, ".foreman", `${stamp}.jsonl`);
|
|
87
|
-
const log = new Log(logPath);
|
|
88
|
-
const policy = new PermissionPolicy(config.permissions, cwd);
|
|
89
|
-
const roleBundle = loadRoleBundle("builder", { projectDir: cwd });
|
|
90
|
-
const adapterOpts = {
|
|
91
|
-
cwd,
|
|
92
|
-
model: opts.model,
|
|
93
|
-
resumeSessionId,
|
|
94
|
-
permission: createPermissionHandler(policy, log),
|
|
95
|
-
effort: opts.effort,
|
|
96
|
-
fast: opts.fast,
|
|
97
|
-
systemPromptAppend: roleBundle.system || undefined,
|
|
98
|
-
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
99
|
-
};
|
|
100
|
-
const builder = opts.agent === "codex"
|
|
101
|
-
? new CodexAdapter(adapterOpts)
|
|
102
|
-
: await ClaudeAdapter.create(adapterOpts);
|
|
103
|
-
// Commander surfaces --no-qa as opts.qa === false; when --qa/--no-qa is not
|
|
104
|
-
// passed, opts.qa is undefined and we fall back to the config default.
|
|
105
|
-
const qaEnabled = opts.qa !== false && config.qa.enabled !== false;
|
|
106
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd);
|
|
107
|
-
const modifiers = [
|
|
108
|
-
opts.model ? `model=${opts.model}` : null,
|
|
109
|
-
opts.effort ? `effort=${opts.effort}` : null,
|
|
110
|
-
opts.fast ? "fast" : null,
|
|
111
|
-
qaEnabled ? null : "qa=off",
|
|
112
|
-
].filter(Boolean).join(" ");
|
|
113
|
-
console.log(`foreman: driving a ${opts.agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
|
|
114
|
-
console.log(`foreman: project ${cwd}`);
|
|
115
|
-
if (trackerRelPath)
|
|
116
|
-
console.log(`foreman: tracker ${trackerRelPath}`);
|
|
117
|
-
console.log(`foreman: log ${logPath}\n`);
|
|
118
|
-
const viewer = printEvents(builder.events());
|
|
119
|
-
try {
|
|
120
|
-
// Pre-flight: builder lists the next N tickets or steps; user confirms before any step runs
|
|
121
|
-
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
122
|
-
await foreman.runPreflight(steps, ticketsContent);
|
|
123
|
-
if (!opts.yes) {
|
|
124
|
-
while (true) {
|
|
125
|
-
console.log();
|
|
126
|
-
const action = await select({
|
|
127
|
-
message: "How does this plan look?",
|
|
128
|
-
options: [
|
|
129
|
-
{ value: "proceed", label: "Proceed — start implementing" },
|
|
130
|
-
{ value: "feedback", label: "Give feedback — revise the plan" },
|
|
131
|
-
{ value: "cancel", label: "Cancel" },
|
|
132
|
-
],
|
|
133
|
-
});
|
|
134
|
-
if (isCancel(action) || action === "cancel") {
|
|
135
|
-
console.log("ai-foreman: cancelled");
|
|
136
|
-
await builder.close();
|
|
137
|
-
await viewer;
|
|
138
|
-
process.exit(0);
|
|
139
|
-
}
|
|
140
|
-
if (action === "proceed") {
|
|
141
|
-
console.log();
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
const fb = await text({
|
|
145
|
-
message: "Your feedback:",
|
|
146
|
-
validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
|
|
147
|
-
});
|
|
148
|
-
if (isCancel(fb)) {
|
|
149
|
-
console.log("ai-foreman: cancelled");
|
|
150
|
-
await builder.close();
|
|
151
|
-
await viewer;
|
|
152
|
-
process.exit(0);
|
|
153
|
-
}
|
|
154
|
-
console.log();
|
|
155
|
-
await foreman.sendPreflightFeedback(String(fb));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const result = await foreman.runBatch(steps, trackerRelPath);
|
|
159
|
-
await builder.close();
|
|
160
|
-
await viewer;
|
|
161
|
-
console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
|
|
162
|
-
console.log(`foreman: outcome — ${result.outcome}`);
|
|
163
|
-
if (result.detail)
|
|
164
|
-
console.log(`foreman: ${result.detail}`);
|
|
165
|
-
const sid = builder.sessionId();
|
|
166
|
-
if (sid)
|
|
167
|
-
console.log(`foreman: resume this builder with --resume ${sid}`);
|
|
168
|
-
process.exit(result.outcome === "needs-human" ? 2 : 0);
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
await builder.close().catch(() => { });
|
|
172
|
-
log.write("error", { message: String(err) });
|
|
173
|
-
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
program
|
|
177
|
-
.command("status")
|
|
178
|
-
.description("Summarize the most recent foreman run for a project.")
|
|
179
|
-
.argument("<project>", "path to the project directory")
|
|
180
|
-
.action((project) => {
|
|
181
|
-
const dir = join(resolve(project), ".foreman");
|
|
182
|
-
if (!existsSync(dir))
|
|
183
|
-
fail(`no foreman runs found under ${dir}`);
|
|
184
|
-
const logs = readdirSync(dir)
|
|
185
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
186
|
-
.sort();
|
|
187
|
-
if (logs.length === 0)
|
|
188
|
-
fail(`no foreman runs found under ${dir}`);
|
|
189
|
-
const latest = join(dir, logs[logs.length - 1]);
|
|
190
|
-
const records = readFileSync(latest, "utf8")
|
|
191
|
-
.split("\n")
|
|
192
|
-
.filter(Boolean)
|
|
193
|
-
.map((line) => JSON.parse(line));
|
|
194
|
-
const steps = records.filter((r) => r.event === "step");
|
|
195
|
-
const escalations = records.filter((r) => r.event === "escalation");
|
|
196
|
-
const batchEnd = records.reverse().find((r) => r.event === "batch-end");
|
|
197
|
-
console.log(`foreman: latest run ${logs[logs.length - 1]}`);
|
|
198
|
-
console.log(`foreman: ${steps.length} step record(s), ${escalations.length} escalation(s)`);
|
|
199
|
-
if (batchEnd) {
|
|
200
|
-
console.log(`foreman: outcome — ${batchEnd.outcome} (${batchEnd.completed}/${batchEnd.requested})`);
|
|
201
|
-
if (batchEnd.detail)
|
|
202
|
-
console.log(`foreman: ${batchEnd.detail}`);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
console.log("ai-foreman: run is still in progress or did not finish");
|
|
206
|
-
}
|
|
207
|
-
for (const esc of escalations) {
|
|
208
|
-
console.log(` escalated: ${esc.tool} — ${esc.reason}`);
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
program
|
|
212
|
-
.command("doctor")
|
|
213
|
-
.description("Check Foreman, agent CLIs, config, and optional ticket tracker readiness.")
|
|
214
|
-
.argument("[project]", "path to the project directory", ".")
|
|
215
|
-
.action((project) => {
|
|
216
|
-
const cwd = resolve(project);
|
|
217
|
-
let errors = 0;
|
|
218
|
-
const report = (ok, label, detail) => {
|
|
219
|
-
console.log(`${ok ? "ok" : "!!"} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
220
|
-
if (!ok)
|
|
221
|
-
errors++;
|
|
222
|
-
};
|
|
223
|
-
const warn = (label, detail) => {
|
|
224
|
-
console.log(`-- ${label}${detail ? ` — ${detail}` : ""}`);
|
|
225
|
-
};
|
|
226
|
-
report(Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10) >= 20, "node >=20", process.version);
|
|
227
|
-
report(Boolean(PACKAGE_VERSION), "foreman package version", PACKAGE_VERSION);
|
|
228
|
-
report(existsSync(cwd), "project directory exists", cwd);
|
|
229
|
-
if (!existsSync(cwd))
|
|
230
|
-
process.exit(1);
|
|
231
|
-
try {
|
|
232
|
-
loadConfig(join(cwd, "foreman.yaml"));
|
|
233
|
-
report(true, "foreman.yaml", existsSync(join(cwd, "foreman.yaml")) ? "valid" : "not present, using defaults");
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
report(false, "foreman.yaml", err instanceof Error ? err.message : String(err));
|
|
237
|
-
}
|
|
238
|
-
const claude = commandVersion("claude");
|
|
239
|
-
if (claude.ok)
|
|
240
|
-
warn("claude CLI found", claude.detail);
|
|
241
|
-
else
|
|
242
|
-
warn("claude CLI not found", "Claude adapter may still work through the SDK if credentials are configured");
|
|
243
|
-
const codex = commandVersion("codex");
|
|
244
|
-
if (codex.ok)
|
|
245
|
-
warn("codex CLI found", codex.detail);
|
|
246
|
-
else
|
|
247
|
-
warn("codex CLI not found", "required only for --agent codex");
|
|
248
|
-
if (isTicketsInitialized(cwd)) {
|
|
249
|
-
try {
|
|
250
|
-
const result = cmdValidate(cwd);
|
|
251
|
-
report(result.clean, ".tickets validation", result.clean ? "clean" : `${result.issues.length} issue(s)`);
|
|
252
|
-
}
|
|
253
|
-
catch (err) {
|
|
254
|
-
report(false, ".tickets validation", err instanceof Error ? err.message : String(err));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
warn(".tickets", "not initialized; start will run in plain mode");
|
|
259
|
-
}
|
|
260
|
-
process.exit(errors === 0 ? 0 : 1);
|
|
261
|
-
});
|
|
15
|
+
program.addCommand(buildStartCommand());
|
|
16
|
+
program.addCommand(buildStatusCommand());
|
|
17
|
+
program.addCommand(buildDoctorCommand());
|
|
262
18
|
// pnpm passes its `--` separator through to the script; strip it so Commander
|
|
263
19
|
// sees the subcommand args correctly.
|
|
264
20
|
const argv = [...process.argv];
|
|
265
21
|
if (argv[2] === "--")
|
|
266
22
|
argv.splice(2, 1);
|
|
267
|
-
program.parseAsync(argv).catch((err) =>
|
|
268
|
-
|
|
269
|
-
console.error(`foreman: ${message}`);
|
|
23
|
+
program.parseAsync(argv).catch((err) => {
|
|
24
|
+
console.error(`foreman: ${String(err)}`);
|
|
270
25
|
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
function commandVersion(command) {
|
|
273
|
-
const result = spawnSync(command, ["--version"], {
|
|
274
|
-
encoding: "utf8",
|
|
275
|
-
timeout: 3000,
|
|
276
|
-
});
|
|
277
|
-
if (result.error)
|
|
278
|
-
return { ok: false, detail: result.error.message };
|
|
279
|
-
if (result.status !== 0)
|
|
280
|
-
return { ok: false, detail: result.stderr.trim() };
|
|
281
|
-
return { ok: true, detail: (result.stdout.trim() || result.stderr.trim()).slice(0, 120) };
|
|
282
|
-
}
|
|
283
|
-
/** Read the most recent batch-end sessionId from the last .foreman log, for auto-resume. */
|
|
284
|
-
function findLastSessionId(dir) {
|
|
285
|
-
if (!existsSync(dir))
|
|
286
|
-
return undefined;
|
|
287
|
-
const logs = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
288
|
-
if (logs.length === 0)
|
|
289
|
-
return undefined;
|
|
290
|
-
const lines = readFileSync(join(dir, logs[logs.length - 1]), "utf8")
|
|
291
|
-
.split("\n")
|
|
292
|
-
.filter(Boolean);
|
|
293
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
294
|
-
const r = JSON.parse(lines[i]);
|
|
295
|
-
if (r.event === "batch-end" && typeof r.sessionId === "string") {
|
|
296
|
-
return r.sessionId;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
26
|
+
});
|
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A single structured log record. */
|
|
2
|
+
export interface LogRecord {
|
|
3
|
+
ts: string;
|
|
4
|
+
event: "batch-start" | "batch-end" | "step" | "permission" | "escalation" | "needs_input" | "preflight" | "ticket-populate" | "qa" | "qa-fix" | "error";
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** Append-only JSONL logger. One line per record, plus an echo to the console. */
|
|
8
|
+
export declare class Log {
|
|
9
|
+
private readonly path;
|
|
10
|
+
constructor(path: string);
|
|
11
|
+
write(event: LogRecord["event"], fields: Record<string, unknown>): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STEP_STATUS protocol strings. Extracted from foreman.ts so tests can import
|
|
3
|
+
* them without pulling in @clack/prompts (which requires Node >=20 for styleText).
|
|
4
|
+
*/
|
|
5
|
+
export declare const MARKER_SPEC = "End EVERY turn with exactly one marker line as the LAST line, nothing after it:\n STEP_STATUS: done | ticket=\"T001\" summary=\"what you just did\" next=\"the next ticket or step\"\n STEP_STATUS: blocked | ticket=\"T001\" reason=\"why you cannot proceed\"\n STEP_STATUS: plan_complete | ticket=\"T001\" summary=\"what you just did\"\n STEP_STATUS: needs_input | question=\"your question for the user\" choices=\"Option A|Option B|Option C\"\nUse \"done\" after finishing a ticket or step when more remain, \"plan_complete\" after\nthe final ticket or step, \"blocked\" if you cannot proceed without help, and \"needs_input\"\nif you need the user to make a decision before continuing.\nAlways include ticket=\"<ticket-id>\" in done/blocked/plan_complete markers when working from a ticket queue.\nAfter \"done\" or \"plan_complete\", foreman may run a QA pass on your work \u2014 expect a\nfollow-up instruction asking you to triple-check accuracy, tests, and ticket satisfaction.";
|
|
6
|
+
export declare const QA_MARKER_SPEC = "End EVERY turn with exactly one marker line as the LAST line, nothing after it:\n STEP_STATUS: qa_pass | summary=\"confirmed everything checks out\"\n STEP_STATUS: qa_fail | issues=\"bullet-list of concrete problems found\"\n STEP_STATUS: blocked | reason=\"why QA itself cannot proceed\"\n STEP_STATUS: needs_input | question=\"...\" choices=\"...\"";
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire a non-interactive desktop notification. Always best-effort — if the
|
|
3
|
+
* platform tool is unavailable the error is silently swallowed.
|
|
4
|
+
*
|
|
5
|
+
* macOS : osascript (built-in)
|
|
6
|
+
* Linux : notify-send (libnotify)
|
|
7
|
+
* WSL : powershell.exe Windows toast (no extra install required)
|
|
8
|
+
*/
|
|
9
|
+
export declare function fireNotification(title: string, body: string): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PermissionConfig } from "../config.js";
|
|
2
|
+
import type { PermissionRequest } from "../adapters/types.js";
|
|
3
|
+
export interface Classification {
|
|
4
|
+
decision: "allow" | "escalate";
|
|
5
|
+
reason: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Rules-based permission classifier. No LLM: routine requests are auto-approved,
|
|
9
|
+
* anything risky or unrecognized escalates to the human (fail safe).
|
|
10
|
+
*/
|
|
11
|
+
export declare class PermissionPolicy {
|
|
12
|
+
private readonly config;
|
|
13
|
+
private readonly cwd;
|
|
14
|
+
constructor(config: PermissionConfig, cwd: string);
|
|
15
|
+
classify(req: PermissionRequest): Classification;
|
|
16
|
+
private classifyBash;
|
|
17
|
+
private matchesAllowedPrefix;
|
|
18
|
+
private filePath;
|
|
19
|
+
private insideCwd;
|
|
20
|
+
}
|
package/dist/roles.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RoleBundle {
|
|
2
|
+
system: string;
|
|
3
|
+
skills: string[];
|
|
4
|
+
model: string | null;
|
|
5
|
+
effort: string | null;
|
|
6
|
+
/** Which tier provided this bundle. */
|
|
7
|
+
source: "compiled" | "library" | "fallback";
|
|
8
|
+
}
|
|
9
|
+
export type LibraryGetAgent = (role: string) => {
|
|
10
|
+
system: string;
|
|
11
|
+
skills: string[];
|
|
12
|
+
model: string | null;
|
|
13
|
+
effort: string | null;
|
|
14
|
+
};
|
|
15
|
+
export interface LoadRoleBundleOpts {
|
|
16
|
+
/** Target repo root — checked for `.rafi/compiled/<role>/`. */
|
|
17
|
+
projectDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Override the library getter. Omit to use the real `special-agents` library.
|
|
20
|
+
* Pass `null` to simulate the library being absent (forces tier 3).
|
|
21
|
+
*/
|
|
22
|
+
libraryGetAgent?: LibraryGetAgent | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function loadRoleBundle(role: string, opts?: LoadRoleBundleOpts): RoleBundle;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { TicketDef } from "./ticketSchema.js";
|
|
2
|
+
import type { TicketState } from "./stateDb.js";
|
|
3
|
+
export declare function resolveBlockers(ticket: TicketDef, states: Map<string, TicketState>): string[];
|
|
4
|
+
export type DisplayStatus = "next" | "in_progress" | "blocked" | "done" | "canceled";
|
|
5
|
+
export declare function computeDisplayStatus(storedStatus: string, blockedBy: string[]): DisplayStatus;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { TicketDef } from "./ticketSchema.js";
|
|
2
|
+
import type { ValidationResult } from "./stateDb.js";
|
|
3
|
+
import { type TicketsConfig, resolveTicketPaths } from "./config.js";
|
|
4
|
+
import { StateDb } from "./stateDb.js";
|
|
5
|
+
import { runAllValidation } from "./validate.js";
|
|
6
|
+
export interface TicketContext {
|
|
7
|
+
config: TicketsConfig;
|
|
8
|
+
projectDir: string;
|
|
9
|
+
paths: ReturnType<typeof resolveTicketPaths>;
|
|
10
|
+
tickets: TicketDef[];
|
|
11
|
+
db: StateDb;
|
|
12
|
+
}
|
|
13
|
+
export declare function openContext(projectDir: string): TicketContext;
|
|
14
|
+
export interface InitOptions {
|
|
15
|
+
appName?: string;
|
|
16
|
+
timezone?: string;
|
|
17
|
+
queueLimit?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function cmdInit(projectDir: string, opts: InitOptions): void;
|
|
20
|
+
export interface UpdateOptions {
|
|
21
|
+
status?: string;
|
|
22
|
+
actor?: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
nextAction?: string;
|
|
25
|
+
currentStep?: string;
|
|
26
|
+
owner?: string;
|
|
27
|
+
validationResult?: ValidationResult;
|
|
28
|
+
validationCommands?: string;
|
|
29
|
+
validationNotes?: string;
|
|
30
|
+
evidence?: string;
|
|
31
|
+
lastError?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function cmdUpdate(projectDir: string, ticketId: string, opts: UpdateOptions): void;
|
|
34
|
+
export interface CompleteOptions {
|
|
35
|
+
actor?: string;
|
|
36
|
+
summary?: string;
|
|
37
|
+
validationResult?: ValidationResult;
|
|
38
|
+
validationCommands?: string;
|
|
39
|
+
evidence?: string;
|
|
40
|
+
validationNotes?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function cmdComplete(projectDir: string, ticketId: string, opts: CompleteOptions): void;
|
|
43
|
+
export interface BlockOptions {
|
|
44
|
+
blockedBy?: string[];
|
|
45
|
+
blockerType?: string;
|
|
46
|
+
summary?: string;
|
|
47
|
+
actor?: string;
|
|
48
|
+
unblockCriteria?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function cmdBlock(projectDir: string, ticketId: string, opts: BlockOptions): void;
|
|
51
|
+
export declare function cmdUnblock(projectDir: string, ticketId: string, opts: {
|
|
52
|
+
summary?: string;
|
|
53
|
+
actor?: string;
|
|
54
|
+
}): void;
|
|
55
|
+
export declare function cmdCancel(projectDir: string, ticketId: string, opts: {
|
|
56
|
+
summary: string;
|
|
57
|
+
actor?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
export interface DiscoverOptions {
|
|
60
|
+
sourceTicket?: string;
|
|
61
|
+
proposedTicket?: string;
|
|
62
|
+
priorityGuess?: string;
|
|
63
|
+
area?: string;
|
|
64
|
+
summary: string;
|
|
65
|
+
rationale?: string;
|
|
66
|
+
needsDecisionFrom?: string;
|
|
67
|
+
actor?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function cmdDiscover(projectDir: string, opts: DiscoverOptions): number;
|
|
70
|
+
export declare function cmdAcceptFutureWork(projectDir: string, futureWorkId: number, opts: {
|
|
71
|
+
ticketId: string;
|
|
72
|
+
order: number;
|
|
73
|
+
actor?: string;
|
|
74
|
+
}): void;
|
|
75
|
+
export declare function cmdReorder(projectDir: string, ticketId: string, opts: {
|
|
76
|
+
afterTicketId?: string;
|
|
77
|
+
order?: number;
|
|
78
|
+
actor?: string;
|
|
79
|
+
}): void;
|
|
80
|
+
export declare function cmdRender(projectDir: string): void;
|
|
81
|
+
export interface ValidateResult {
|
|
82
|
+
issues: ReturnType<typeof runAllValidation>;
|
|
83
|
+
clean: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function cmdValidate(projectDir: string): ValidateResult;
|
|
86
|
+
export declare function cmdQueue(projectDir: string, limit?: number): import("./queue.js").QueueRow[];
|
|
87
|
+
export declare function cmdArchive(projectDir: string, _opts: {
|
|
88
|
+
olderThanDays?: number;
|
|
89
|
+
}): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface TicketsPaths {
|
|
2
|
+
tickets: string;
|
|
3
|
+
stateDb: string;
|
|
4
|
+
trackerRules: string;
|
|
5
|
+
progressDoc: string;
|
|
6
|
+
archiveDoc: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TicketsRendering {
|
|
9
|
+
preserveLegacyLlmQueueHeading: boolean;
|
|
10
|
+
includeRulesInProgressDoc: boolean;
|
|
11
|
+
maxWorkLogRows: number;
|
|
12
|
+
maxValidationSnapshotRows: number;
|
|
13
|
+
maxRecentCompletedRows: number;
|
|
14
|
+
generatedDocWarning: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TicketsBehavior {
|
|
17
|
+
regenerateProgressDocAfterEveryUpdate: boolean;
|
|
18
|
+
requireValidationEvidenceForDone: boolean;
|
|
19
|
+
blockOnUnresolvedDependencies: boolean;
|
|
20
|
+
useAtomicFileWrites: boolean;
|
|
21
|
+
backupBeforeImportOrMigration: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface TicketsConfig {
|
|
24
|
+
appName: string;
|
|
25
|
+
queueLimit: number;
|
|
26
|
+
timezone: string;
|
|
27
|
+
paths: TicketsPaths;
|
|
28
|
+
rendering: TicketsRendering;
|
|
29
|
+
behavior: TicketsBehavior;
|
|
30
|
+
}
|
|
31
|
+
export declare const DEFAULT_TICKETS_CONFIG: TicketsConfig;
|
|
32
|
+
export declare function loadTicketsConfig(projectDir: string): TicketsConfig;
|
|
33
|
+
export declare function isTicketsInitialized(projectDir: string): boolean;
|
|
34
|
+
export declare function resolveTicketPaths(config: TicketsConfig, projectDir: string): {
|
|
35
|
+
tickets: string;
|
|
36
|
+
stateDb: string;
|
|
37
|
+
trackerRules: string;
|
|
38
|
+
progressDoc: string;
|
|
39
|
+
archiveDoc: string;
|
|
40
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StateDb } from "./stateDb.js";
|
|
2
|
+
export declare function nowTimestamp(timezone: string): string;
|
|
3
|
+
export declare function logEvent(db: StateDb, opts: {
|
|
4
|
+
timestamp: string;
|
|
5
|
+
actor: string | null;
|
|
6
|
+
ticketId: string | null;
|
|
7
|
+
eventType: string;
|
|
8
|
+
oldStatus: string | null;
|
|
9
|
+
newStatus: string | null;
|
|
10
|
+
summary: string;
|
|
11
|
+
validation?: string | null;
|
|
12
|
+
evidence?: string | null;
|
|
13
|
+
payload?: Record<string, unknown>;
|
|
14
|
+
}): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function importFromMarkdown(_progressDocPath: string): never;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TicketDef } from "./ticketSchema.js";
|
|
2
|
+
import type { TicketState } from "./stateDb.js";
|
|
3
|
+
export interface QueueRow {
|
|
4
|
+
rank: number;
|
|
5
|
+
ticket: string;
|
|
6
|
+
title: string;
|
|
7
|
+
status: string;
|
|
8
|
+
priority: string;
|
|
9
|
+
area: string;
|
|
10
|
+
dependsOn: string;
|
|
11
|
+
blockedBy: string;
|
|
12
|
+
size: string;
|
|
13
|
+
risk: string;
|
|
14
|
+
nextAction: string;
|
|
15
|
+
requiredTests: string;
|
|
16
|
+
evidence: string;
|
|
17
|
+
likelyFiles: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ActiveStatusRow {
|
|
20
|
+
ticket: string;
|
|
21
|
+
title: string;
|
|
22
|
+
status: string;
|
|
23
|
+
priority: string;
|
|
24
|
+
area: string;
|
|
25
|
+
owner: string;
|
|
26
|
+
lastWorkedAt: string;
|
|
27
|
+
completedAt: string;
|
|
28
|
+
dependsOn: string;
|
|
29
|
+
blockers: string;
|
|
30
|
+
nextAction: string;
|
|
31
|
+
acceptanceTestGate: string;
|
|
32
|
+
evidence: string;
|
|
33
|
+
futureWorkNotes: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function buildNextQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, queueLimit: number): QueueRow[];
|
|
36
|
+
export declare function buildActiveStatusRows(queueRows: QueueRow[], ticketDefs: TicketDef[], states: Map<string, TicketState>): ActiveStatusRow[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TicketsConfig } from "./config.js";
|
|
2
|
+
import type { TicketDef } from "./ticketSchema.js";
|
|
3
|
+
import type { StateDb, TicketState } from "./stateDb.js";
|
|
4
|
+
export declare const DEFAULT_TRACKER_RULES = "# Tracker Rules\n\n## Purpose\nThis tracker is the operational build control surface for humans and builder agents.\n\n## First file to read\nRead `docs/ticket-progress.md` first. Do not scan the full backlog to choose work\nunless the queue is empty or the user explicitly asks for broader planning.\n\n## How to choose work\nChoose the first row in `LLM_NEXT_QUEUE` where `Status` is `next` and `Blocked By` is `None`.\n\n## Required queue maintenance rule\nEvery ticket update must regenerate `docs/ticket-progress.md`, including `LLM_NEXT_QUEUE`.\nUse `foreman tickets update <id>` \u2014 never manually edit generated sections.\n\n## Blocked work\nDo not implement blocked rows unless the blocker itself is the approved next work.\n\n## Done policy\nNever mark a ticket `done` without required validation evidence or a documented test exception.\nUse `foreman tickets complete <id> --validation-result passed --evidence \"...\"`.\n\n## New work policy\nNewly discovered work belongs in the Discovered Future Work Inbox.\nUse `foreman tickets discover --summary \"...\" --rationale \"...\"`.\n\n## Generated document policy\nDo not manually edit generated sections of `docs/ticket-progress.md`.\nUse the `foreman tickets` commands instead.\n\n## STEP_STATUS protocol\nWhen Foreman is running this project, include the ticket ID in your done marker:\n STEP_STATUS: done | ticket=\"T001\" summary=\"implemented the feature\"\nUse `foreman tickets discover` for newly discovered work during a build run.\n";
|
|
5
|
+
export interface RenderInput {
|
|
6
|
+
config: TicketsConfig;
|
|
7
|
+
projectDir: string;
|
|
8
|
+
ticketDefs: TicketDef[];
|
|
9
|
+
states: Map<string, TicketState>;
|
|
10
|
+
db: StateDb;
|
|
11
|
+
now?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function renderProgressDoc(input: RenderInput): string;
|
|
14
|
+
export declare function writeProgressDoc(progressDocPath: string, content: string, atomic: boolean): void;
|
|
15
|
+
export declare function renderAndWrite(input: RenderInput): void;
|