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.
- package/LICENSE +21 -0
- package/README.md +403 -0
- package/dist/assess.d.ts +102 -0
- package/dist/assess.js +545 -0
- package/dist/check.d.ts +30 -0
- package/dist/check.js +253 -0
- package/dist/checks.d.ts +15 -0
- package/dist/checks.js +15 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +231 -0
- package/dist/detect.d.ts +42 -0
- package/dist/detect.js +459 -0
- package/dist/doctor.d.ts +36 -0
- package/dist/doctor.js +297 -0
- package/dist/document.d.ts +30 -0
- package/dist/document.js +72 -0
- package/dist/enforcers.d.ts +6 -0
- package/dist/enforcers.js +307 -0
- package/dist/feedback.d.ts +67 -0
- package/dist/feedback.js +209 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.js +72 -0
- package/dist/init.d.ts +31 -0
- package/dist/init.js +158 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.js +247 -0
- package/dist/package-info.d.ts +4 -0
- package/dist/package-info.js +6 -0
- package/dist/pipeline.d.ts +82 -0
- package/dist/pipeline.js +265 -0
- package/dist/prompter.d.ts +23 -0
- package/dist/prompter.js +56 -0
- package/dist/render.d.ts +58 -0
- package/dist/render.js +557 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +93 -0
- package/dist/routing.d.ts +24 -0
- package/dist/routing.js +121 -0
- package/dist/ruff.d.ts +19 -0
- package/dist/ruff.js +64 -0
- package/dist/standards.d.ts +46 -0
- package/dist/standards.js +130 -0
- package/dist/state.d.ts +22 -0
- package/dist/state.js +56 -0
- package/dist/test-helpers.d.ts +16 -0
- package/dist/test-helpers.js +62 -0
- package/dist/work.d.ts +147 -0
- package/dist/work.js +357 -0
- package/package.json +45 -0
package/dist/check.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// peer-ai check: the gate CI runs. It fails when the setup is broken, or when a work item claims
|
|
2
|
+
// more than its record shows: an item at ship or done without a passing verify or reviews, or a
|
|
3
|
+
// gap marked done that a fresh assessment still finds. Gaps on the map never fail it; they are
|
|
4
|
+
// reported, so they become work items instead of blockers. The project's stage sets how strict
|
|
5
|
+
// it is.
|
|
6
|
+
import { renderedName } from "peer-ai-skills";
|
|
7
|
+
import { MAP_ITEM_IDS } from "peer-ai-workflow";
|
|
8
|
+
import { MAP_FILE, assess, gaps, loadConfig } from "./assess.js";
|
|
9
|
+
import { count, fail, formatChecks, ok, plural, warn } from "./checks.js";
|
|
10
|
+
import { CONFIG_FILE } from "./detect.js";
|
|
11
|
+
import { checkTracks, checkWorkItems, diagnose } from "./doctor.js";
|
|
12
|
+
import { mapChanges, readMap, readWorkItems } from "./state.js";
|
|
13
|
+
/** Doctor's checks that check already makes itself. */
|
|
14
|
+
const CHECKED_HERE = new Set(["config", "tracks", "map", "work-items"]);
|
|
15
|
+
/**
|
|
16
|
+
* The setup checks doctor makes: every failure, and one line counting the warnings. The skills
|
|
17
|
+
* are left out, since CI never has them.
|
|
18
|
+
*/
|
|
19
|
+
export function setupChecks(root, nodeVersion, today) {
|
|
20
|
+
const diagnosis = diagnose(root, nodeVersion, today, { skills: false });
|
|
21
|
+
const others = diagnosis.checks.filter((check) => !CHECKED_HERE.has(check.id));
|
|
22
|
+
const warnings = count(others, "warn");
|
|
23
|
+
return [
|
|
24
|
+
...others.filter((check) => check.status === "fail"),
|
|
25
|
+
...(warnings === 0
|
|
26
|
+
? []
|
|
27
|
+
: [warn("setup", `${plural(warnings, "setup warning")}.`, "Run npx peer-ai doctor for the details.")]),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
/** Work at these stages says it has been verified and reviewed. */
|
|
31
|
+
const CLAIMS_VERIFIED = ["ship", "done"];
|
|
32
|
+
const OPEN = ["prepare", "build", "verify", "ship"];
|
|
33
|
+
const COMMIT_MAP = `Run peer-ai assess, and commit ${MAP_FILE}.`;
|
|
34
|
+
const isKnownItem = (id) => MAP_ITEM_IDS.includes(id);
|
|
35
|
+
/** The most recent review from each skill: a later pass supersedes an earlier failure. */
|
|
36
|
+
function latestReviews(item) {
|
|
37
|
+
const latest = new Map();
|
|
38
|
+
for (const review of item.reviews ?? []) {
|
|
39
|
+
const seen = latest.get(review.skill);
|
|
40
|
+
if (seen === undefined || Date.parse(review.at) >= Date.parse(seen.at))
|
|
41
|
+
latest.set(review.skill, review);
|
|
42
|
+
}
|
|
43
|
+
return [...latest.values()];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a work item may be at ship or done. `items` are the project's other work items, for its
|
|
47
|
+
* dependencies (RFC 0005): an item can't ship before the items it depends on have.
|
|
48
|
+
*/
|
|
49
|
+
export function gateWorkItem(item, config, stage, assessment, items = []) {
|
|
50
|
+
const checks = [];
|
|
51
|
+
const claim = `${item.id} is at ${item.stage}`;
|
|
52
|
+
const backToBuild = "or move the item back to build.";
|
|
53
|
+
const verifyCommand = config.commands?.verify ?? undefined;
|
|
54
|
+
for (const id of item.dependsOn ?? []) {
|
|
55
|
+
const dependency = items.find((other) => other.id === id);
|
|
56
|
+
if (dependency !== undefined && CLAIMS_VERIFIED.includes(dependency.stage))
|
|
57
|
+
continue;
|
|
58
|
+
checks.push(fail("gates", dependency === undefined
|
|
59
|
+
? `${claim}, but it depends on ${id}, which isn't a work item.`
|
|
60
|
+
: dependency.stage === "cancelled"
|
|
61
|
+
? `${claim}, but it depends on ${id}, which was cancelled.`
|
|
62
|
+
: `${claim}, but it depends on ${id}, which is only at ${dependency.stage}.`, dependency?.stage === "cancelled" || dependency === undefined
|
|
63
|
+
? `Remove ${id} from its dependsOn, ${backToBuild}`
|
|
64
|
+
: `Ship ${id} first, ${backToBuild}`));
|
|
65
|
+
}
|
|
66
|
+
if (item.lastVerify?.result === "fail") {
|
|
67
|
+
checks.push(fail("gates", `${claim}, but its last verify failed.`, `Fix what failed and verify again, ${backToBuild}`));
|
|
68
|
+
}
|
|
69
|
+
else if (item.lastVerify === undefined && verifyCommand !== undefined) {
|
|
70
|
+
checks.push(fail("gates", `${claim}, but it has no recorded verify.`, `Run ${verifyCommand} and record the result on the work item, ${backToBuild}`));
|
|
71
|
+
}
|
|
72
|
+
// The reviews the change needs (RFC 0004): a warning for an MVP, a failure in production. A
|
|
73
|
+
// prototype is only told, by next_work.
|
|
74
|
+
const recorded = new Set((item.reviews ?? []).map((review) => review.skill));
|
|
75
|
+
for (const required of item.requiredReviews ?? []) {
|
|
76
|
+
if (recorded.has(required.skill) || stage === "prototype")
|
|
77
|
+
continue;
|
|
78
|
+
const message = `${claim}, but it has no ${required.skill}, which it needs because ${required.reason}.`;
|
|
79
|
+
const fix = `Use the ${renderedName(required.skill)} skill and record its review, ${backToBuild}`;
|
|
80
|
+
checks.push(stage === "production" ? fail("gates", message, fix) : warn("gates", message, fix));
|
|
81
|
+
}
|
|
82
|
+
for (const review of latestReviews(item)) {
|
|
83
|
+
// A prototype accepts a review that didn't cover every rule; a failed one still stops it.
|
|
84
|
+
const allowed = review.result === "pass" || (review.result === "incomplete" && stage === "prototype");
|
|
85
|
+
if (!allowed) {
|
|
86
|
+
checks.push(review.result === "fail"
|
|
87
|
+
? fail("gates", `${claim}, but its latest ${review.skill} failed.`, `Fix the findings and review again, ${backToBuild}`)
|
|
88
|
+
: fail("gates", `${claim}, but its latest ${review.skill} is incomplete: it didn't check every rule.`, `Review again until every rule is checked, ${backToBuild}`));
|
|
89
|
+
}
|
|
90
|
+
else if (review.report === undefined && stage !== "prototype") {
|
|
91
|
+
// Without a report, the result is the agent's word (RFC 0002): a warning for an MVP, a
|
|
92
|
+
// failure in production.
|
|
93
|
+
const message = `${claim}, but its latest ${review.skill} has no report, so its result is unproven.`;
|
|
94
|
+
const fix = "Write the review's report, and record the review again with it.";
|
|
95
|
+
checks.push(stage === "production" ? fail("gates", message, fix) : warn("gates", message, fix));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (item.kind === "gap" && item.stage === "done" && item.gap !== undefined && isKnownItem(item.gap)) {
|
|
99
|
+
const status = assessment.items[item.gap].status;
|
|
100
|
+
if (status !== "present" && status !== "not-applicable") {
|
|
101
|
+
checks.push(fail("gates", `${item.id} says the ${item.gap} gap is done, but a fresh assessment finds it ${status}.`, `Finish the work, ${backToBuild}`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return checks;
|
|
105
|
+
}
|
|
106
|
+
function checkGates(items, config, stage, assessment) {
|
|
107
|
+
const claiming = items.filter((item) => CLAIMS_VERIFIED.includes(item.stage));
|
|
108
|
+
const results = claiming.flatMap((item) => gateWorkItem(item, config, stage, assessment, items));
|
|
109
|
+
const failures = results.filter((check) => check.status === "fail");
|
|
110
|
+
const warnings = results.filter((check) => check.status === "warn");
|
|
111
|
+
if (failures.length > 0)
|
|
112
|
+
return [...failures, ...warnings];
|
|
113
|
+
if (claiming.length === 0)
|
|
114
|
+
return [ok("gates", "No work items at ship or done yet")];
|
|
115
|
+
return [
|
|
116
|
+
ok("gates", `${plural(claiming.length, "work item")} at ship or done, each verified and reviewed`),
|
|
117
|
+
...warnings,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
/** Every dependency names a work item that exists, and no items wait on each other in a loop (RFC 0005). */
|
|
121
|
+
function checkDependencies(items) {
|
|
122
|
+
const ids = new Set(items.map((item) => item.id));
|
|
123
|
+
const problems = [];
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
for (const id of item.dependsOn ?? []) {
|
|
126
|
+
if (!ids.has(id)) {
|
|
127
|
+
problems.push(fail("plans", `${item.id} depends on ${id}, which isn't a work item.`, `Remove ${id} from its dependsOn.`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
132
|
+
const reported = new Set();
|
|
133
|
+
for (const start of items) {
|
|
134
|
+
// Follow the dependencies depth first; meeting an item already on the path is a loop.
|
|
135
|
+
const path = [];
|
|
136
|
+
const visit = (id) => {
|
|
137
|
+
if (path.includes(id))
|
|
138
|
+
return [...path.slice(path.indexOf(id)), id];
|
|
139
|
+
if (reported.has(id))
|
|
140
|
+
return undefined;
|
|
141
|
+
path.push(id);
|
|
142
|
+
for (const next of byId.get(id)?.dependsOn ?? []) {
|
|
143
|
+
const loop = visit(next);
|
|
144
|
+
if (loop !== undefined)
|
|
145
|
+
return loop;
|
|
146
|
+
}
|
|
147
|
+
path.pop();
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
150
|
+
const loop = visit(start.id);
|
|
151
|
+
if (loop !== undefined && !loop.some((id) => reported.has(id))) {
|
|
152
|
+
for (const id of loop)
|
|
153
|
+
reported.add(id);
|
|
154
|
+
problems.push(fail("plans", `Work items wait on each other in a loop: ${loop.join(" → ")}.`, "Remove one of the dependencies."));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (problems.length > 0)
|
|
158
|
+
return problems;
|
|
159
|
+
const planned = items.filter((item) => (item.dependsOn ?? []).length > 0).length;
|
|
160
|
+
return planned === 0 ? [] : [ok("plans", `${plural(planned, "work item")} with dependencies, each on a real item`)];
|
|
161
|
+
}
|
|
162
|
+
function checkVerifyCommand(config, stage) {
|
|
163
|
+
if (stage === "prototype" || (config.commands?.verify ?? undefined) !== undefined)
|
|
164
|
+
return [];
|
|
165
|
+
return [
|
|
166
|
+
warn("verify", "No verify command is set, so nothing is verified before work is called done.", `Set commands.verify in ${CONFIG_FILE}, for example "npm test" or "make check".`),
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
function checkMap(root, assessment) {
|
|
170
|
+
const read = readMap(root);
|
|
171
|
+
if (read === undefined)
|
|
172
|
+
return warn("map", "There is no project map yet.", COMMIT_MAP);
|
|
173
|
+
if (!read.ok)
|
|
174
|
+
return fail("map", `${MAP_FILE} ${read.error}`, COMMIT_MAP);
|
|
175
|
+
const changed = mapChanges(read.value, assessment);
|
|
176
|
+
if (changed.length > 0)
|
|
177
|
+
return warn("map", `The project map is out of date: ${changed.join(", ")}.`, COMMIT_MAP);
|
|
178
|
+
return ok("map", "The project map is up to date");
|
|
179
|
+
}
|
|
180
|
+
/** What the stage needs and is missing should each have an open gap work item. */
|
|
181
|
+
function checkGapsTracked(assessment, stage, items) {
|
|
182
|
+
if (stage === "prototype")
|
|
183
|
+
return ok("gaps", "Nothing on the map is required at the prototype stage");
|
|
184
|
+
const needed = gaps(assessment, stage);
|
|
185
|
+
if (needed.length === 0)
|
|
186
|
+
return ok("gaps", `Everything the ${stage} stage needs is in place`);
|
|
187
|
+
const tracked = new Set(items.filter((item) => item.kind === "gap" && OPEN.includes(item.stage)).map((i) => i.gap));
|
|
188
|
+
const untracked = needed.filter((id) => !tracked.has(id));
|
|
189
|
+
if (untracked.length === 0)
|
|
190
|
+
return ok("gaps", `${plural(needed.length, "gap")} for ${stage}, each with a work item`);
|
|
191
|
+
return warn("gaps", `${plural(untracked.length, "gap")} for ${stage} with no work item: ${untracked.join(", ")}.`, "Add a gap work item for each, so they are planned rather than forgotten.");
|
|
192
|
+
}
|
|
193
|
+
export function evaluate(root, config) {
|
|
194
|
+
const stage = config.project.stage ?? "mvp";
|
|
195
|
+
const assessment = assess(root, config, stage);
|
|
196
|
+
const trackFailures = checkTracks(root, config).filter((check) => check.status === "fail");
|
|
197
|
+
const items = readWorkItems(root).flatMap(({ item }) => (item.ok ? [item.value] : []));
|
|
198
|
+
const checks = [
|
|
199
|
+
ok("config", `${CONFIG_FILE} is valid`),
|
|
200
|
+
...(trackFailures.length > 0 ? trackFailures : [ok("tracks", "Every track's folder exists")]),
|
|
201
|
+
checkMap(root, assessment),
|
|
202
|
+
...checkWorkItems(root, config),
|
|
203
|
+
...checkDependencies(items),
|
|
204
|
+
...checkGates(items, config, stage, assessment),
|
|
205
|
+
...checkVerifyCommand(config, stage),
|
|
206
|
+
checkGapsTracked(assessment, stage, items),
|
|
207
|
+
];
|
|
208
|
+
const setup = setupChecks(root);
|
|
209
|
+
return {
|
|
210
|
+
name: config.project.name,
|
|
211
|
+
stage,
|
|
212
|
+
ok: count(checks, "fail") + count(setup, "fail") === 0,
|
|
213
|
+
checks,
|
|
214
|
+
setup,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function formatVerdict(verdict) {
|
|
218
|
+
const all = [...verdict.checks, ...verdict.setup];
|
|
219
|
+
const failures = count(all, "fail");
|
|
220
|
+
const warnings = count(all, "warn");
|
|
221
|
+
const andWarnings = warnings === 0 ? "" : `, and ${plural(warnings, "warning")}`;
|
|
222
|
+
return [
|
|
223
|
+
`Peer AI check: ${verdict.name} (stage: ${verdict.stage})`,
|
|
224
|
+
"",
|
|
225
|
+
...formatChecks(verdict.checks),
|
|
226
|
+
...(verdict.setup.length === 0 ? [] : ["", "Setup, as peer-ai doctor checks it:", ...formatChecks(verdict.setup)]),
|
|
227
|
+
"",
|
|
228
|
+
failures > 0
|
|
229
|
+
? `Failed: ${plural(failures, "problem")}${andWarnings}.`
|
|
230
|
+
: `Passed${warnings === 0 ? "" : `, with ${plural(warnings, "warning")}`}.`,
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
/** Exit code 0 when it passes, even with warnings; 1 when it fails; 2 without a valid config. */
|
|
234
|
+
export function runCheck(options, out) {
|
|
235
|
+
const { config, errors } = loadConfig(options.cwd);
|
|
236
|
+
if (config === undefined) {
|
|
237
|
+
if (errors === undefined)
|
|
238
|
+
out.error(`There is no ${CONFIG_FILE}. Run peer-ai init first.`);
|
|
239
|
+
else {
|
|
240
|
+
out.error(`${CONFIG_FILE} is not valid:`);
|
|
241
|
+
for (const error of errors)
|
|
242
|
+
out.error(` ${error}`);
|
|
243
|
+
}
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
const verdict = evaluate(options.cwd, config);
|
|
247
|
+
if (options.json)
|
|
248
|
+
out.log(JSON.stringify(verdict, null, 2));
|
|
249
|
+
else
|
|
250
|
+
for (const line of formatVerdict(verdict))
|
|
251
|
+
out.log(line);
|
|
252
|
+
return verdict.ok ? 0 : 1;
|
|
253
|
+
}
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type CheckStatus = "ok" | "warn" | "fail" | "skip";
|
|
2
|
+
export interface Check {
|
|
3
|
+
id: string;
|
|
4
|
+
status: CheckStatus;
|
|
5
|
+
message: string;
|
|
6
|
+
fix?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const ok: (id: string, message: string) => Check;
|
|
9
|
+
export declare const skip: (id: string, message: string) => Check;
|
|
10
|
+
export declare const warn: (id: string, message: string, fix: string) => Check;
|
|
11
|
+
export declare const fail: (id: string, message: string, fix: string) => Check;
|
|
12
|
+
export declare const plural: (count: number, word: string) => string;
|
|
13
|
+
export declare const count: (checks: Check[], status: CheckStatus) => number;
|
|
14
|
+
/** One line per check, with its fix indented beneath it. */
|
|
15
|
+
export declare function formatChecks(checks: Check[]): string[];
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The results `peer-ai doctor` and `peer-ai check` report, and how they are printed.
|
|
2
|
+
export const ok = (id, message) => ({ id, status: "ok", message });
|
|
3
|
+
export const skip = (id, message) => ({ id, status: "skip", message });
|
|
4
|
+
export const warn = (id, message, fix) => ({ id, status: "warn", message, fix });
|
|
5
|
+
export const fail = (id, message, fix) => ({ id, status: "fail", message, fix });
|
|
6
|
+
export const plural = (count, word) => `${String(count)} ${word}${count === 1 ? "" : "s"}`;
|
|
7
|
+
export const count = (checks, status) => checks.filter((check) => check.status === status).length;
|
|
8
|
+
const SYMBOL = { ok: "✓", warn: "!", fail: "✗", skip: "–" };
|
|
9
|
+
/** One line per check, with its fix indented beneath it. */
|
|
10
|
+
export function formatChecks(checks) {
|
|
11
|
+
return checks.flatMap((check) => [
|
|
12
|
+
` ${SYMBOL[check.status]} ${check.message}`,
|
|
13
|
+
...(check.fix === undefined ? [] : [` ${check.fix}`]),
|
|
14
|
+
]);
|
|
15
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { type Output } from "./init.ts";
|
|
3
|
+
import { type Prompter } from "./prompter.ts";
|
|
4
|
+
export interface Io {
|
|
5
|
+
cwd: string;
|
|
6
|
+
out: Output;
|
|
7
|
+
/** Present when questions can be asked, which means a terminal is attached. */
|
|
8
|
+
prompter?: Prompter;
|
|
9
|
+
}
|
|
10
|
+
export declare function main(argv: string[], io: Io): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The peer-ai command. Exit codes: 0 success, 1 refused, cancelled or problems found, 2 usage or
|
|
3
|
+
// config error.
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { CLI_COMMAND_IDS, TOOL_IDS } from "peer-ai-workflow";
|
|
9
|
+
import { runAssess } from "./assess.js";
|
|
10
|
+
import { runCheck } from "./check.js";
|
|
11
|
+
import { runCheckDocument } from "./document.js";
|
|
12
|
+
import { runDoctor } from "./doctor.js";
|
|
13
|
+
import { runFeedback } from "./feedback.js";
|
|
14
|
+
import { serveStdio } from "./mcp.js";
|
|
15
|
+
import { runInit } from "./init.js";
|
|
16
|
+
import { VERSION } from "./package-info.js";
|
|
17
|
+
import { createTerminalPrompter } from "./prompter.js";
|
|
18
|
+
import { formatReport } from "./report.js";
|
|
19
|
+
import { runRender } from "./render.js";
|
|
20
|
+
import { runCheckReport } from "./work.js";
|
|
21
|
+
const STAGES = ["prototype", "mvp", "production"];
|
|
22
|
+
const HELP = `peer-ai: from a brief to a shipped product, with any AI tool
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
peer-ai init [options] Set up Peer AI in this repository
|
|
26
|
+
peer-ai assess [options] Map what the project has and what its stage still needs
|
|
27
|
+
peer-ai render [options] Set up each AI tool in the config: instructions and the MCP server
|
|
28
|
+
peer-ai doctor [options] Check that Peer AI is set up correctly, and how to fix it
|
|
29
|
+
peer-ai check [options] The CI gate: fail when work claims more than its record shows
|
|
30
|
+
peer-ai check-report <file> Check a review's report, as the record_review tool does
|
|
31
|
+
peer-ai check-document <file> --skill <skill>
|
|
32
|
+
Check a document against its skill's template, as the
|
|
33
|
+
check_document tool does
|
|
34
|
+
peer-ai feedback List the feedback drafts your AI tool wrote about Peer AI
|
|
35
|
+
peer-ai feedback send <draft>
|
|
36
|
+
Send a draft as an issue on Peer AI's repository
|
|
37
|
+
peer-ai feedback drop <draft>
|
|
38
|
+
Delete a draft
|
|
39
|
+
peer-ai mcp Start the MCP server that AI tools connect to, over stdio
|
|
40
|
+
|
|
41
|
+
Options for init:
|
|
42
|
+
-y, --yes Accept what init detects instead of asking
|
|
43
|
+
--dry-run Print the config instead of writing it
|
|
44
|
+
--name <name> The project's name
|
|
45
|
+
--stage <stage> prototype, mvp or production
|
|
46
|
+
--team <team> solo or team
|
|
47
|
+
--tool <tool> An AI tool you use; repeat for several
|
|
48
|
+
(${TOOL_IDS.join(", ")})
|
|
49
|
+
|
|
50
|
+
Options for assess:
|
|
51
|
+
--target <stage> Assess against a stage other than the project's own,
|
|
52
|
+
for example production before a launch
|
|
53
|
+
--json Print the project map as JSON instead of the report
|
|
54
|
+
--dry-run Don't write .peer-ai/map.json
|
|
55
|
+
|
|
56
|
+
Options for render:
|
|
57
|
+
--check Change nothing; fail when a file is out of date (for CI)
|
|
58
|
+
--skills Write only the skills (what each tool's setup step runs)
|
|
59
|
+
--quiet Print nothing unless something fails
|
|
60
|
+
|
|
61
|
+
Options for check-report:
|
|
62
|
+
--skill <skill> The skill the report is for (default: the one it names)
|
|
63
|
+
--work-item <id> The work item it's for, when there is one
|
|
64
|
+
--json Print the result as JSON
|
|
65
|
+
|
|
66
|
+
Options for check-document:
|
|
67
|
+
--skill <skill> The document skill that wrote it, such as requirements-analysis
|
|
68
|
+
--template <name> Which of the skill's templates it follows (default: the main one)
|
|
69
|
+
--json Print the result as JSON
|
|
70
|
+
|
|
71
|
+
Options for doctor and check:
|
|
72
|
+
--json Print the checks as JSON
|
|
73
|
+
|
|
74
|
+
Other:
|
|
75
|
+
-h, --help Show this help
|
|
76
|
+
-v, --version Show the version`;
|
|
77
|
+
function oneOf(value, allowed, flag) {
|
|
78
|
+
if (allowed.includes(value))
|
|
79
|
+
return value;
|
|
80
|
+
throw new Error(`${flag} must be one of: ${allowed.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
async function init(args, io) {
|
|
83
|
+
const { values } = parseArgs({
|
|
84
|
+
args,
|
|
85
|
+
strict: true,
|
|
86
|
+
options: {
|
|
87
|
+
yes: { type: "boolean", short: "y" },
|
|
88
|
+
"dry-run": { type: "boolean" },
|
|
89
|
+
name: { type: "string" },
|
|
90
|
+
stage: { type: "string" },
|
|
91
|
+
team: { type: "string" },
|
|
92
|
+
tool: { type: "string", multiple: true },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const stage = values.stage === undefined ? undefined : oneOf(values.stage, STAGES, "--stage");
|
|
96
|
+
const team = values.team === undefined ? undefined : oneOf(values.team, ["solo", "team"], "--team");
|
|
97
|
+
const tools = values.tool?.map((tool) => oneOf(tool, TOOL_IDS, "--tool"));
|
|
98
|
+
return runInit({
|
|
99
|
+
cwd: io.cwd,
|
|
100
|
+
yes: values.yes === true,
|
|
101
|
+
dryRun: values["dry-run"] === true,
|
|
102
|
+
...(values.name === undefined ? {} : { name: values.name }),
|
|
103
|
+
...(stage === undefined ? {} : { stage }),
|
|
104
|
+
...(team === undefined ? {} : { team }),
|
|
105
|
+
...(tools === undefined ? {} : { tools }),
|
|
106
|
+
}, io.prompter, io.out);
|
|
107
|
+
}
|
|
108
|
+
function assess(args, io) {
|
|
109
|
+
const { values } = parseArgs({
|
|
110
|
+
args,
|
|
111
|
+
strict: true,
|
|
112
|
+
options: {
|
|
113
|
+
target: { type: "string" },
|
|
114
|
+
json: { type: "boolean" },
|
|
115
|
+
"dry-run": { type: "boolean" },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const target = values.target === undefined ? undefined : oneOf(values.target, STAGES, "--target");
|
|
119
|
+
return runAssess({
|
|
120
|
+
cwd: io.cwd,
|
|
121
|
+
json: values.json === true,
|
|
122
|
+
dryRun: values["dry-run"] === true,
|
|
123
|
+
...(target === undefined ? {} : { target }),
|
|
124
|
+
}, io.out, formatReport);
|
|
125
|
+
}
|
|
126
|
+
function doctor(args, io) {
|
|
127
|
+
const { values } = parseArgs({ args, strict: true, options: { json: { type: "boolean" } } });
|
|
128
|
+
return runDoctor({ cwd: io.cwd, json: values.json === true }, io.out);
|
|
129
|
+
}
|
|
130
|
+
function check(args, io) {
|
|
131
|
+
const { values } = parseArgs({ args, strict: true, options: { json: { type: "boolean" } } });
|
|
132
|
+
return runCheck({ cwd: io.cwd, json: values.json === true }, io.out);
|
|
133
|
+
}
|
|
134
|
+
function checkReportCommand(args, io) {
|
|
135
|
+
const { values, positionals } = parseArgs({
|
|
136
|
+
args,
|
|
137
|
+
strict: true,
|
|
138
|
+
allowPositionals: true,
|
|
139
|
+
options: { skill: { type: "string" }, "work-item": { type: "string" }, json: { type: "boolean" } },
|
|
140
|
+
});
|
|
141
|
+
const [report] = positionals;
|
|
142
|
+
if (report === undefined)
|
|
143
|
+
throw new TypeError("Give the report's path, such as peer-ai check-report .peer-ai/reports/project/security-review.json");
|
|
144
|
+
return runCheckReport({ cwd: io.cwd, report, skill: values.skill, workItem: values["work-item"], json: values.json === true }, io.out);
|
|
145
|
+
}
|
|
146
|
+
function checkDocumentCommand(args, io) {
|
|
147
|
+
const { values, positionals } = parseArgs({
|
|
148
|
+
args,
|
|
149
|
+
strict: true,
|
|
150
|
+
allowPositionals: true,
|
|
151
|
+
options: { skill: { type: "string" }, template: { type: "string" }, json: { type: "boolean" } },
|
|
152
|
+
});
|
|
153
|
+
const [path] = positionals;
|
|
154
|
+
if (path === undefined || values.skill === undefined)
|
|
155
|
+
throw new TypeError("Give the document's path and its skill, such as peer-ai check-document docs/requirements.md --skill requirements-analysis");
|
|
156
|
+
return runCheckDocument({ cwd: io.cwd, path, skill: values.skill, template: values.template, json: values.json === true }, io.out);
|
|
157
|
+
}
|
|
158
|
+
function render(args, io) {
|
|
159
|
+
const { values } = parseArgs({
|
|
160
|
+
args,
|
|
161
|
+
strict: true,
|
|
162
|
+
options: { check: { type: "boolean" }, skills: { type: "boolean" }, quiet: { type: "boolean" } },
|
|
163
|
+
});
|
|
164
|
+
return runRender({ cwd: io.cwd, check: values.check === true, skills: values.skills === true, quiet: values.quiet === true }, io.out);
|
|
165
|
+
}
|
|
166
|
+
function feedback(args, io) {
|
|
167
|
+
const { positionals } = parseArgs({ args, strict: true, allowPositionals: true, options: {} });
|
|
168
|
+
const [action, draft] = positionals;
|
|
169
|
+
return runFeedback({ cwd: io.cwd, action, draft }, io.out);
|
|
170
|
+
}
|
|
171
|
+
async function mcp(args, io) {
|
|
172
|
+
parseArgs({ args, strict: true, options: {} });
|
|
173
|
+
await serveStdio(io.cwd);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
const COMMANDS = {
|
|
177
|
+
init,
|
|
178
|
+
assess,
|
|
179
|
+
render,
|
|
180
|
+
doctor,
|
|
181
|
+
check,
|
|
182
|
+
"check-report": checkReportCommand,
|
|
183
|
+
"check-document": checkDocumentCommand,
|
|
184
|
+
feedback,
|
|
185
|
+
mcp,
|
|
186
|
+
};
|
|
187
|
+
const isCommand = (value) => CLI_COMMAND_IDS.includes(value);
|
|
188
|
+
export async function main(argv, io) {
|
|
189
|
+
const [command, ...rest] = argv;
|
|
190
|
+
if (command === undefined || command === "-h" || command === "--help" || command === "help") {
|
|
191
|
+
io.out.log(HELP);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
if (command === "-v" || command === "--version") {
|
|
195
|
+
io.out.log(VERSION);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
const run = isCommand(command) ? COMMANDS[command] : undefined;
|
|
199
|
+
if (run === undefined) {
|
|
200
|
+
io.out.error(`Unknown command "${command}". Run peer-ai --help to see the commands.`);
|
|
201
|
+
return 2;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
return await run(rest, io);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof TypeError || (error instanceof Error && error.message.startsWith("--"))) {
|
|
208
|
+
io.out.error(`${error.message}. Run peer-ai --help for usage.`);
|
|
209
|
+
return 2;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// An installed peer-ai runs through a link in node_modules/.bin, so compare the file the link points to.
|
|
215
|
+
const invokedDirectly = process.argv[1] !== undefined &&
|
|
216
|
+
realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url));
|
|
217
|
+
if (invokedDirectly) {
|
|
218
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
219
|
+
process.exitCode = await main(process.argv.slice(2), {
|
|
220
|
+
cwd: process.cwd(),
|
|
221
|
+
out: {
|
|
222
|
+
log: (line) => {
|
|
223
|
+
console.log(line);
|
|
224
|
+
},
|
|
225
|
+
error: (line) => {
|
|
226
|
+
console.error(line);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
...(interactive ? { prompter: createTerminalPrompter() } : {}),
|
|
230
|
+
});
|
|
231
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ToolId } from "peer-ai-workflow";
|
|
2
|
+
import { CONFIG_FILE } from "peer-ai-workflow";
|
|
3
|
+
export { CONFIG_FILE };
|
|
4
|
+
export type TrackKind = "web" | "mobile" | "desktop" | "backend" | "infrastructure" | "library" | "cli" | "other";
|
|
5
|
+
export type RepoHost = "github" | "gitlab" | "bitbucket" | "azure-devops" | "other";
|
|
6
|
+
export interface DetectedTrack {
|
|
7
|
+
id: string;
|
|
8
|
+
kind: TrackKind;
|
|
9
|
+
path?: string;
|
|
10
|
+
stack: string[];
|
|
11
|
+
/** Where it deploys, when a platform's config file says so. */
|
|
12
|
+
deploy?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface Detected {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
origin: "new" | "existing";
|
|
18
|
+
tools: ToolId[];
|
|
19
|
+
repo: {
|
|
20
|
+
host?: RepoHost;
|
|
21
|
+
remote: string | null;
|
|
22
|
+
};
|
|
23
|
+
/** Present when a CI pipeline already exists, so Peer AI extends it instead of adding another. */
|
|
24
|
+
delivery?: {
|
|
25
|
+
ci: "existing";
|
|
26
|
+
pipeline: string;
|
|
27
|
+
};
|
|
28
|
+
tracks: DetectedTrack[];
|
|
29
|
+
hasConfig: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function slugify(text: string): string;
|
|
32
|
+
export declare function detectDeploy(dir: string): string | undefined;
|
|
33
|
+
/** The project's name from its own project file, in whichever language it's written. */
|
|
34
|
+
export declare function detectName(root: string): string;
|
|
35
|
+
export declare function detectTracks(root: string, projectName: string): DetectedTrack[];
|
|
36
|
+
export declare function detectDelivery(root: string): Detected["delivery"];
|
|
37
|
+
export declare function detectTools(root: string): ToolId[];
|
|
38
|
+
/** The host name of a git remote, from an https, ssh:// or scp-style (git@host:path) URL. */
|
|
39
|
+
export declare function remoteHostname(url: string): string | undefined;
|
|
40
|
+
export declare function classifyHost(hostname: string | undefined): RepoHost;
|
|
41
|
+
export declare function detectRepo(root: string): Detected["repo"];
|
|
42
|
+
export declare function detect(root: string): Detected;
|