peer-ai 1.0.0-next.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +403 -0
  3. package/dist/assess.d.ts +102 -0
  4. package/dist/assess.js +545 -0
  5. package/dist/check.d.ts +30 -0
  6. package/dist/check.js +253 -0
  7. package/dist/checks.d.ts +15 -0
  8. package/dist/checks.js +15 -0
  9. package/dist/cli.d.ts +10 -0
  10. package/dist/cli.js +231 -0
  11. package/dist/detect.d.ts +42 -0
  12. package/dist/detect.js +459 -0
  13. package/dist/doctor.d.ts +36 -0
  14. package/dist/doctor.js +297 -0
  15. package/dist/document.d.ts +30 -0
  16. package/dist/document.js +72 -0
  17. package/dist/enforcers.d.ts +6 -0
  18. package/dist/enforcers.js +307 -0
  19. package/dist/feedback.d.ts +67 -0
  20. package/dist/feedback.js +209 -0
  21. package/dist/files.d.ts +1 -0
  22. package/dist/files.js +72 -0
  23. package/dist/init.d.ts +31 -0
  24. package/dist/init.js +158 -0
  25. package/dist/mcp.d.ts +13 -0
  26. package/dist/mcp.js +247 -0
  27. package/dist/package-info.d.ts +4 -0
  28. package/dist/package-info.js +6 -0
  29. package/dist/pipeline.d.ts +82 -0
  30. package/dist/pipeline.js +265 -0
  31. package/dist/prompter.d.ts +23 -0
  32. package/dist/prompter.js +56 -0
  33. package/dist/render.d.ts +58 -0
  34. package/dist/render.js +557 -0
  35. package/dist/report.d.ts +3 -0
  36. package/dist/report.js +93 -0
  37. package/dist/routing.d.ts +24 -0
  38. package/dist/routing.js +121 -0
  39. package/dist/ruff.d.ts +19 -0
  40. package/dist/ruff.js +64 -0
  41. package/dist/standards.d.ts +46 -0
  42. package/dist/standards.js +130 -0
  43. package/dist/state.d.ts +22 -0
  44. package/dist/state.js +56 -0
  45. package/dist/test-helpers.d.ts +16 -0
  46. package/dist/test-helpers.js +62 -0
  47. package/dist/work.d.ts +147 -0
  48. package/dist/work.js +357 -0
  49. package/package.json +45 -0
package/dist/work.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { type ActivityId, type KnownMapItemId, type PeerAiConfig, type ReviewResult, type SkillId, type WorkItem } from "peer-ai-workflow";
2
+ import { reviewsToDo } from "./routing.ts";
3
+ import type { Output, Stage } from "./init.ts";
4
+ export declare const WORK_ITEM_SCHEMA_URL = "https://raw.githubusercontent.com/AbuMahir980/peer-ai/main/packages/workflow/schemas/work-item.schema.json";
5
+ export type Result<T> = {
6
+ ok: true;
7
+ value: T;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ export type ItemStage = WorkItem["stage"];
13
+ export type ItemKind = WorkItem["kind"];
14
+ export declare function loadWorkItem(root: string, id: string): Result<WorkItem>;
15
+ /** The next free id: the tracker's ticket prefix, or ITEM, and one more than the highest number. */
16
+ export declare function nextId(root: string, config: PeerAiConfig): string;
17
+ /** A branch name from the repo's naming pattern, when every placeholder in it is known. */
18
+ export declare function branchFor(pattern: string | undefined, id: string, title: string): string | undefined;
19
+ export interface NewWorkItem {
20
+ title: string;
21
+ kind: ItemKind;
22
+ id?: string | undefined;
23
+ track?: string | undefined;
24
+ gap?: string | undefined;
25
+ branch?: string | undefined;
26
+ next?: string | undefined;
27
+ goal?: string | undefined;
28
+ acceptance?: string[] | undefined;
29
+ sources?: string[] | undefined;
30
+ dependsOn?: string[] | undefined;
31
+ }
32
+ export declare function createWorkItem(root: string, config: PeerAiConfig, input: NewWorkItem, now: Date): Result<WorkItem>;
33
+ export interface WorkItemChanges {
34
+ title?: string | undefined;
35
+ next?: string | undefined;
36
+ branch?: string | undefined;
37
+ position?: {
38
+ activity: ActivityId;
39
+ step: number;
40
+ } | undefined;
41
+ goal?: string | undefined;
42
+ acceptance?: string[] | undefined;
43
+ sources?: string[] | undefined;
44
+ dependsOn?: string[] | undefined;
45
+ }
46
+ export declare function updateWorkItem(root: string, config: PeerAiConfig, id: string, changes: WorkItemChanges, now: Date): Result<WorkItem>;
47
+ export interface ReviewInput {
48
+ skill: SkillId;
49
+ /** Needed without a report. With one, it must match the result the report supports. */
50
+ result?: ReviewResult | undefined;
51
+ /** The review report, relative to the project root. */
52
+ report?: string | undefined;
53
+ summary?: string | undefined;
54
+ }
55
+ /**
56
+ * Records a review. With a report, Peer AI works the result out from it and refuses a result the
57
+ * report doesn't support (RFC 0002). Without one, the agent's result is recorded as unproven.
58
+ */
59
+ export interface CheckedReport {
60
+ skill: SkillId;
61
+ result: ReviewResult;
62
+ report: string;
63
+ summary: string;
64
+ }
65
+ /**
66
+ * Checks a review's report: that it's valid, is for this skill (and work item, when there is one),
67
+ * gives every rule the skill answers for a line (RFC 0004), and claims the result its findings and
68
+ * coverage support.
69
+ */
70
+ export declare function checkReport(root: string, config: PeerAiConfig, review: ReviewInput & {
71
+ report: string;
72
+ }, workItem?: string): Result<CheckedReport>;
73
+ export interface CheckReportOptions {
74
+ cwd: string;
75
+ /** The report's path, relative to the project root. */
76
+ report: string;
77
+ /** The skill it's for. Defaults to the skill the report names. */
78
+ skill?: string | undefined;
79
+ /** The work item it's for, when there is one. */
80
+ workItem?: string | undefined;
81
+ json: boolean;
82
+ }
83
+ /**
84
+ * peer-ai check-report: the same checks as the record_review tool, for a model or a person that
85
+ * works in a shell. Exit code 0 when the report passes them; 1 when it doesn't; 2 without a config.
86
+ */
87
+ export declare function runCheckReport(options: CheckReportOptions, out: Output): number;
88
+ export declare function recordReview(root: string, config: PeerAiConfig, id: string, review: ReviewInput, now: Date): Result<WorkItem>;
89
+ export declare function recordVerify(root: string, config: PeerAiConfig, id: string, result: "pass" | "fail", now: Date): Result<WorkItem>;
90
+ /**
91
+ * Moves a work item to `to`, or to the stage after its current one. It can move back to any
92
+ * earlier stage. A move to ship or done is refused, with what to fix, when the gates fail.
93
+ */
94
+ export declare function advanceWorkItem(root: string, config: PeerAiConfig, id: string, to: ItemStage | undefined, now: Date): Result<WorkItem>;
95
+ export interface CommandResult {
96
+ code: number | null;
97
+ /** The end of the combined output, which is where test runners report what failed. */
98
+ output: string;
99
+ }
100
+ export type CommandRunner = (command: string, cwd: string) => Promise<CommandResult>;
101
+ /** Runs a command from the project's config through the shell, as a person would type it. */
102
+ export declare const runCommand: CommandRunner;
103
+ export interface VerifyOutcome {
104
+ item: WorkItem;
105
+ command: string;
106
+ result: "pass" | "fail";
107
+ output: string;
108
+ }
109
+ /** Runs commands.verify and records the result, so a pass is proven rather than claimed. */
110
+ export declare function runVerify(root: string, config: PeerAiConfig, id: string, now: () => Date, run?: CommandRunner): Promise<Result<VerifyOutcome>>;
111
+ /** The checked-out branch, even before the first commit; undefined when detached or not in git. */
112
+ export declare function currentBranch(root: string): string | undefined;
113
+ /** A setup problem doctor finds, for the AI tool to fix or tell the person about before other work. */
114
+ export interface SetupProblem {
115
+ check: string;
116
+ status: "fail" | "warn";
117
+ message: string;
118
+ fix?: string;
119
+ }
120
+ export interface NextWork {
121
+ /** Every setup problem doctor finds (RFC 0007). Absent when there is none. */
122
+ setup?: {
123
+ problems: SetupProblem[];
124
+ };
125
+ branch?: string;
126
+ /** The open work item for the current branch. */
127
+ current?: WorkItem;
128
+ /** Every open work item, most recently updated first. */
129
+ open: WorkItem[];
130
+ /** The current item's required reviews, by the names their skills are installed under. */
131
+ reviews?: ReturnType<typeof reviewsToDo>;
132
+ /** For each open item with unfinished dependencies, the items it's waiting for before it can ship (RFC 0005). */
133
+ waiting?: Record<string, string[]>;
134
+ /**
135
+ * When nothing is open: what the project's stage still needs, to start as gap work items, and
136
+ * the skill to use for each gap that has one.
137
+ */
138
+ gaps?: {
139
+ stage: Stage;
140
+ needed: KnownMapItemId[];
141
+ later: KnownMapItemId[];
142
+ useSkill: Partial<Record<KnownMapItemId, string>>;
143
+ };
144
+ }
145
+ /** Doctor's failures and warnings, as next_work reports them. */
146
+ export declare function setupProblems(root: string, nodeVersion?: string, today?: Date): SetupProblem[];
147
+ export declare function nextWork(root: string, config: PeerAiConfig): NextWork;
package/dist/work.js ADDED
@@ -0,0 +1,357 @@
1
+ // Work items: create them, record where work stands, verify and review them, and move them
2
+ // through their stages. Every change is validated against the work item schema before it is
3
+ // written, and a move to ship or done must pass the same gates as `peer-ai check`, so what an
4
+ // agent records is what CI will accept.
5
+ import { execFileSync, spawn } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { isAbsolute, join, relative, resolve } from "node:path";
8
+ import { availableSkills, skillRuleIds } from "peer-ai-skills";
9
+ import { SKILL_IDS, deriveResult, validateReport, validateWorkItem, } from "peer-ai-workflow";
10
+ import { NEXT_STAGE, assess, gaps, loadConfig } from "./assess.js";
11
+ import { gateWorkItem } from "./check.js";
12
+ import { CONFIG_FILE } from "./detect.js";
13
+ import { diagnose } from "./doctor.js";
14
+ import { changedFiles, gapSkills, reviewsFor, reviewsToDo } from "./routing.js";
15
+ import { WORK_DIR, readWorkItems } from "./state.js";
16
+ export const WORK_ITEM_SCHEMA_URL = "https://raw.githubusercontent.com/AbuMahir980/peer-ai/main/packages/workflow/schemas/work-item.schema.json";
17
+ /** The order work moves through. cancelled can be reached from any stage before done. */
18
+ const ORDER = ["prepare", "build", "verify", "ship", "done"];
19
+ const CLOSED = ["done", "cancelled"];
20
+ const DEFAULT_PREFIX = "ITEM";
21
+ const failed = (error) => ({ ok: false, error });
22
+ const projectStage = (config) => config.project.stage ?? "mvp";
23
+ function tracksOf(config) {
24
+ return config.tracks.map((track) => track.id);
25
+ }
26
+ export function loadWorkItem(root, id) {
27
+ const file = readWorkItems(root).find(({ path }) => path === `${WORK_DIR}/${id}.json`);
28
+ if (file === undefined)
29
+ return failed(`There is no work item "${id}".`);
30
+ return file.item.ok ? file.item : failed(`${file.path} ${file.item.error}`);
31
+ }
32
+ function save(root, config, item) {
33
+ const result = validateWorkItem({ $schema: WORK_ITEM_SCHEMA_URL, ...item });
34
+ if (!result.ok)
35
+ return failed(`The work item would not be valid: ${result.errors.join("; ")}`);
36
+ if (item.track !== undefined && !tracksOf(config).includes(item.track)) {
37
+ return failed(`There is no track "${item.track}". The tracks are: ${tracksOf(config).join(", ")}.`);
38
+ }
39
+ mkdirSync(join(root, WORK_DIR), { recursive: true });
40
+ writeFileSync(join(root, WORK_DIR, `${item.id}.json`), `${JSON.stringify(result.value, null, 2)}\n`);
41
+ return result;
42
+ }
43
+ /** The next free id: the tracker's ticket prefix, or ITEM, and one more than the highest number. */
44
+ export function nextId(root, config) {
45
+ const prefix = (config.tracker?.ticketPrefix ?? DEFAULT_PREFIX).replace(/-+$/, "");
46
+ const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-(\\d+)$`);
47
+ const numbers = readWorkItems(root).flatMap(({ path }) => {
48
+ const match = pattern.exec(path.slice(WORK_DIR.length + 1, -".json".length));
49
+ return match?.[1] === undefined ? [] : [Number(match[1])];
50
+ });
51
+ return `${prefix}-${String(Math.max(0, ...numbers) + 1)}`;
52
+ }
53
+ /** A branch name from the repo's naming pattern, when every placeholder in it is known. */
54
+ export function branchFor(pattern, id, title) {
55
+ if (pattern === undefined)
56
+ return undefined;
57
+ const slug = title
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, "-")
60
+ .replace(/^-+|-+$/g, "")
61
+ .slice(0, 50)
62
+ .replace(/-+$/, "");
63
+ const branch = pattern.replaceAll("{ticket}", id).replaceAll("{id}", id).replaceAll("{slug}", slug);
64
+ return /[{}]/.test(branch) ? undefined : branch;
65
+ }
66
+ export function createWorkItem(root, config, input, now) {
67
+ const id = input.id ?? nextId(root, config);
68
+ if (existsSync(join(root, WORK_DIR, `${id}.json`)))
69
+ return failed(`A work item "${id}" already exists.`);
70
+ const ownTracks = config.tracks.filter((track) => track.status !== "external");
71
+ // With a single track there is nothing to choose, so the item is on it.
72
+ const track = input.track ?? (ownTracks.length === 1 ? ownTracks[0]?.id : undefined);
73
+ const branch = input.branch ?? branchFor(config.repo?.branchNaming, id, input.title);
74
+ const item = {
75
+ version: 1,
76
+ id,
77
+ title: input.title,
78
+ kind: input.kind,
79
+ stage: "prepare",
80
+ ...(track === undefined ? {} : { track }),
81
+ ...(branch === undefined ? {} : { branch }),
82
+ ...(input.gap === undefined ? {} : { gap: input.gap }),
83
+ ...defined({ goal: input.goal, acceptance: input.acceptance, sources: input.sources, dependsOn: input.dependsOn }),
84
+ next: input.next ?? "Read what this item needs, then plan it.",
85
+ updatedAt: now.toISOString(),
86
+ };
87
+ return save(root, config, item);
88
+ }
89
+ /** The fields that have a value, so an undefined argument never erases a stored one. */
90
+ const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
91
+ export function updateWorkItem(root, config, id, changes, now) {
92
+ const loaded = loadWorkItem(root, id);
93
+ if (!loaded.ok)
94
+ return loaded;
95
+ return save(root, config, { ...loaded.value, ...defined(changes), updatedAt: now.toISOString() });
96
+ }
97
+ /** Reads a review report inside the project and checks it is valid. */
98
+ function readReport(root, path) {
99
+ const full = resolve(root, path);
100
+ const inside = relative(root, full);
101
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) {
102
+ return failed(`${path} is outside the project. Save the report under .peer-ai/reports/.`);
103
+ }
104
+ if (!existsSync(full))
105
+ return failed(`There is no report at ${path}.`);
106
+ let json;
107
+ try {
108
+ json = JSON.parse(readFileSync(full, "utf8"));
109
+ }
110
+ catch (error) {
111
+ return failed(`${path} is not valid JSON: ${error.message}`);
112
+ }
113
+ const report = validateReport(json);
114
+ return report.ok ? report : failed(`${path} is not a valid review report:\n- ${report.errors.join("\n- ")}`);
115
+ }
116
+ /**
117
+ * Checks a review's report: that it's valid, is for this skill (and work item, when there is one),
118
+ * gives every rule the skill answers for a line (RFC 0004), and claims the result its findings and
119
+ * coverage support.
120
+ */
121
+ export function checkReport(root, config, review, workItem) {
122
+ const read = readReport(root, review.report);
123
+ if (!read.ok)
124
+ return read;
125
+ const report = read.value;
126
+ if (report.skill !== review.skill) {
127
+ return failed(`The report is for ${report.skill}, not ${review.skill}.`);
128
+ }
129
+ if (workItem !== undefined && report.workItem !== undefined && report.workItem !== workItem) {
130
+ return failed(`The report is for work item ${report.workItem}, not ${workItem}.`);
131
+ }
132
+ // Silence is never an answer (RFC 0004): every rule the skill answers for gets a line, even
133
+ // one that doesn't apply here.
134
+ if (availableSkills().includes(review.skill)) {
135
+ const covered = new Set(report.coverage.map((line) => line.rule));
136
+ const missing = skillRuleIds(review.skill).filter((rule) => !covered.has(rule));
137
+ if (missing.length > 0) {
138
+ const shown = missing.length > 12
139
+ ? `${missing.slice(0, 12).join(", ")} and ${String(missing.length - 12)} more`
140
+ : missing.join(", ");
141
+ return failed(`The report leaves out ${String(missing.length)} of ${review.skill}'s rules: ${shown}. Give every rule a coverage line: mark one that doesn't apply as not-applicable, with the reason.`);
142
+ }
143
+ }
144
+ const blockOn = config.gates?.blockOn ?? "critical";
145
+ const worked = deriveResult(report, blockOn);
146
+ if (report.result !== worked) {
147
+ return failed(`The report says ${report.result}, but its findings and coverage make it ${worked} (blocking level: ${blockOn}). Correct the report's result.`);
148
+ }
149
+ if (review.result !== undefined && review.result !== worked) {
150
+ return failed(`You gave ${review.result}, but the report makes it ${worked}.`);
151
+ }
152
+ return {
153
+ ok: true,
154
+ value: { skill: review.skill, result: worked, report: review.report, summary: review.summary ?? report.summary },
155
+ };
156
+ }
157
+ /**
158
+ * peer-ai check-report: the same checks as the record_review tool, for a model or a person that
159
+ * works in a shell. Exit code 0 when the report passes them; 1 when it doesn't; 2 without a config.
160
+ */
161
+ export function runCheckReport(options, out) {
162
+ const { config, errors } = loadConfig(options.cwd);
163
+ if (config === undefined) {
164
+ out.error(errors === undefined ? `There is no ${CONFIG_FILE}. Run peer-ai init first.` : `${CONFIG_FILE} is not valid.`);
165
+ return 2;
166
+ }
167
+ let skill = options.skill;
168
+ if (skill === undefined) {
169
+ try {
170
+ const named = JSON.parse(readFileSync(resolve(options.cwd, options.report), "utf8"))
171
+ .skill;
172
+ if (typeof named === "string")
173
+ skill = named;
174
+ }
175
+ catch {
176
+ // Unreadable or not JSON: checkReport says which.
177
+ }
178
+ }
179
+ if (skill === undefined || !SKILL_IDS.includes(skill)) {
180
+ const problem = `The report doesn't name one of Peer AI's skills as its "skill", such as "security-review": give it with --skill, and put it in the report.`;
181
+ if (options.json)
182
+ out.log(JSON.stringify({ ok: false, error: problem }, null, 2));
183
+ else
184
+ out.error(`✗ ${problem}`);
185
+ return 1;
186
+ }
187
+ const checked = checkReport(options.cwd, config, { skill: skill, report: options.report }, options.workItem);
188
+ if (options.json) {
189
+ out.log(JSON.stringify(checked.ok ? { ok: true, ...checked.value } : { ok: false, error: checked.error }, null, 2));
190
+ }
191
+ else if (checked.ok) {
192
+ out.log(`✓ ${options.report} passes Peer AI's checks. Its result is ${checked.value.result}.`);
193
+ }
194
+ else {
195
+ out.error(`✗ ${checked.error}`);
196
+ }
197
+ return checked.ok ? 0 : 1;
198
+ }
199
+ export function recordReview(root, config, id, review, now) {
200
+ const loaded = loadWorkItem(root, id);
201
+ if (!loaded.ok)
202
+ return loaded;
203
+ const at = now.toISOString();
204
+ let entry;
205
+ if (review.report === undefined) {
206
+ if (review.result === undefined)
207
+ return failed("Give the review's result, or the report to work it out from.");
208
+ const summary = review.summary === undefined ? {} : { summary: review.summary };
209
+ entry = { skill: review.skill, result: review.result, ...summary, unproven: true, at };
210
+ }
211
+ else {
212
+ const checked = checkReport(root, config, { ...review, report: review.report }, id);
213
+ if (!checked.ok)
214
+ return checked;
215
+ entry = { ...checked.value, at };
216
+ }
217
+ const reviews = [...(loaded.value.reviews ?? []), entry];
218
+ return save(root, config, { ...loaded.value, reviews, updatedAt: at });
219
+ }
220
+ export function recordVerify(root, config, id, result, now) {
221
+ const loaded = loadWorkItem(root, id);
222
+ if (!loaded.ok)
223
+ return loaded;
224
+ const at = now.toISOString();
225
+ return save(root, config, { ...loaded.value, lastVerify: { result, at }, updatedAt: at });
226
+ }
227
+ /**
228
+ * Moves a work item to `to`, or to the stage after its current one. It can move back to any
229
+ * earlier stage. A move to ship or done is refused, with what to fix, when the gates fail.
230
+ */
231
+ export function advanceWorkItem(root, config, id, to, now) {
232
+ const loaded = loadWorkItem(root, id);
233
+ if (!loaded.ok)
234
+ return loaded;
235
+ const item = loaded.value;
236
+ if (CLOSED.includes(item.stage) && (to === undefined || to === item.stage)) {
237
+ return failed(`${item.id} is already ${item.stage}.`);
238
+ }
239
+ const target = to ?? ORDER[ORDER.indexOf(item.stage) + 1];
240
+ if (target === undefined)
241
+ return failed(`${item.id} has no stage after ${item.stage}.`);
242
+ if (target === "verify") {
243
+ // The reviews it needs are worked out once, from what the change touched, and kept on the item
244
+ // so CI can hold it to them without the branch's history.
245
+ const read = (file) => {
246
+ try {
247
+ return readFileSync(join(root, file), "utf8");
248
+ }
249
+ catch {
250
+ return "";
251
+ }
252
+ };
253
+ const requiredReviews = reviewsFor(changedFiles(root), config, projectStage(config), read, undefined, item);
254
+ return save(root, config, { ...item, stage: target, requiredReviews, updatedAt: now.toISOString() });
255
+ }
256
+ if (target === "ship" || target === "done") {
257
+ const moved = { ...item, stage: target };
258
+ const stage = projectStage(config);
259
+ const others = readWorkItems(root).flatMap(({ item: other }) => (other.ok ? [other.value] : []));
260
+ const failures = gateWorkItem(moved, config, stage, assess(root, config, stage), others).filter((check) => check.status === "fail");
261
+ if (failures.length > 0) {
262
+ const reasons = failures.map((check) => `- ${check.message} ${check.fix ?? ""}`.trimEnd());
263
+ return failed(`${item.id} can't move to ${target} yet:\n${reasons.join("\n")}`);
264
+ }
265
+ }
266
+ return save(root, config, { ...item, stage: target, updatedAt: now.toISOString() });
267
+ }
268
+ const OUTPUT_KEPT = 4000;
269
+ const VERIFY_TIMEOUT_MS = 30 * 60 * 1000;
270
+ /** Runs a command from the project's config through the shell, as a person would type it. */
271
+ export const runCommand = (command, cwd) => new Promise((resolve) => {
272
+ let output = "";
273
+ const keep = (chunk) => {
274
+ output = (output + chunk.toString("utf8")).slice(-OUTPUT_KEPT);
275
+ };
276
+ // stdin is closed, so a command that waits for input ends instead of hanging until the
277
+ // timeout; output is piped, never inherited, because stdout carries the MCP protocol.
278
+ const child = spawn(command, { cwd, shell: true, timeout: VERIFY_TIMEOUT_MS, stdio: ["ignore", "pipe", "pipe"] });
279
+ child.stdout.on("data", keep);
280
+ child.stderr.on("data", keep);
281
+ child.on("error", (error) => {
282
+ resolve({ code: null, output: `${output}\n${error.message}`.trim() });
283
+ });
284
+ child.on("close", (code) => {
285
+ resolve({ code, output });
286
+ });
287
+ });
288
+ /** Runs commands.verify and records the result, so a pass is proven rather than claimed. */
289
+ export async function runVerify(root, config, id, now, run = runCommand) {
290
+ const loaded = loadWorkItem(root, id);
291
+ if (!loaded.ok)
292
+ return loaded;
293
+ const command = config.commands?.verify ?? undefined;
294
+ if (command === undefined) {
295
+ return failed("There is no verify command. Set commands.verify in peer-ai.config.json to the command that checks the project, such as its tests and linter.");
296
+ }
297
+ const { code, output } = await run(command, root);
298
+ const result = code === 0 ? "pass" : "fail";
299
+ const recorded = recordVerify(root, config, id, result, now());
300
+ return recorded.ok ? { ok: true, value: { item: recorded.value, command, result, output } } : recorded;
301
+ }
302
+ /** The checked-out branch, even before the first commit; undefined when detached or not in git. */
303
+ export function currentBranch(root) {
304
+ try {
305
+ const branch = execFileSync("git", ["symbolic-ref", "--short", "-q", "HEAD"], {
306
+ cwd: root,
307
+ encoding: "utf8",
308
+ stdio: ["ignore", "pipe", "ignore"],
309
+ }).trim();
310
+ return branch === "" ? undefined : branch;
311
+ }
312
+ catch {
313
+ return undefined;
314
+ }
315
+ }
316
+ /** Doctor's failures and warnings, as next_work reports them. */
317
+ export function setupProblems(root, nodeVersion, today) {
318
+ return diagnose(root, nodeVersion, today).checks.flatMap((check) => check.status === "fail" || check.status === "warn"
319
+ ? [
320
+ {
321
+ check: check.id,
322
+ status: check.status,
323
+ message: check.message,
324
+ ...(check.fix === undefined ? {} : { fix: check.fix }),
325
+ },
326
+ ]
327
+ : []);
328
+ }
329
+ export function nextWork(root, config) {
330
+ const problems = setupProblems(root);
331
+ const branch = currentBranch(root);
332
+ const open = readWorkItems(root)
333
+ .flatMap(({ item }) => (item.ok && !CLOSED.includes(item.value.stage) ? [item.value] : []))
334
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
335
+ const current = branch === undefined ? undefined : open.find((item) => item.branch === branch);
336
+ const reviews = current === undefined ? [] : reviewsToDo(current);
337
+ const shipped = new Set(readWorkItems(root).flatMap(({ item }) => item.ok && (item.value.stage === "ship" || item.value.stage === "done") ? [item.value.id] : []));
338
+ const waiting = Object.fromEntries(open
339
+ .map((item) => [item.id, (item.dependsOn ?? []).filter((id) => !shipped.has(id))])
340
+ .filter(([, unfinished]) => unfinished.length > 0));
341
+ const result = {
342
+ ...(problems.length === 0 ? {} : { setup: { problems } }),
343
+ ...(branch === undefined ? {} : { branch }),
344
+ ...(current ? { current } : {}),
345
+ ...(reviews.length > 0 ? { reviews } : {}),
346
+ ...(Object.keys(waiting).length > 0 ? { waiting } : {}),
347
+ open,
348
+ };
349
+ if (open.length > 0)
350
+ return result;
351
+ const stage = projectStage(config);
352
+ const assessment = assess(root, config, stage);
353
+ const next = NEXT_STAGE[stage];
354
+ const needed = gaps(assessment, stage);
355
+ const later = next === undefined ? [] : gaps(assessment, next).filter((id) => !needed.includes(id));
356
+ return { ...result, gaps: { stage, needed, later, useSkill: gapSkills([...needed, ...later]) } };
357
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "peer-ai",
3
+ "version": "1.0.0-next.0",
4
+ "description": "Keeps AI coding tools to a senior team's standard: planning, testing, security and compliance, with proof of every check",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "peer-ai": "./dist/cli.js"
9
+ },
10
+ "dependencies": {
11
+ "@clack/prompts": "1.8.1",
12
+ "@modelcontextprotocol/sdk": "1.30.1",
13
+ "peer-ai-skills": "1.0.0-next.0",
14
+ "peer-ai-standards": "1.0.0-next.0",
15
+ "peer-ai-workflow": "1.0.0-next.0",
16
+ "jsonc-parser": "3.3.1",
17
+ "smol-toml": "1.9.0",
18
+ "yaml": "2.9.1",
19
+ "zod": "4.6.5"
20
+ },
21
+ "engines": {
22
+ "node": ">=24"
23
+ },
24
+ "devDependencies": {
25
+ "@astral-sh/ruff-wasm-nodejs": "0.16.9"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/AbuMahir980/peer-ai.git",
36
+ "directory": "packages/peer-ai"
37
+ },
38
+ "homepage": "https://github.com/AbuMahir980/peer-ai#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/AbuMahir980/peer-ai/issues"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.build.json"
44
+ }
45
+ }