paseo-req-kit 1.8.0 → 1.9.0-beta.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/CHANGELOG.md +38 -0
- package/README.md +2 -0
- package/bin/req-plugin.mjs +226 -0
- package/bin/req.mjs +27 -0
- package/contracts/README.md +2 -0
- package/contracts/cli-json.md +66 -0
- package/contracts/usage-record.md +48 -0
- package/docs/adr/0026-machine-records-and-api.md +23 -0
- package/docs/adr/0027-paseo-plugin.md +23 -0
- package/docs/adr/README.md +2 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +2 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +26 -0
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +3 -1
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +10 -0
- package/engine/src/api.ts +397 -0
- package/engine/src/control.ts +2 -2
- package/engine/src/eval.ts +2 -0
- package/engine/src/index-cmd.ts +33 -0
- package/engine/src/main.ts +26 -0
- package/engine/src/records.ts +271 -0
- package/engine/src/state.ts +3 -0
- package/engine/src/stats.ts +479 -0
- package/install.mjs +3 -0
- package/package.json +3 -2
- package/paseo-plugin/OVERVIEW.md +7 -0
- package/paseo-plugin/client/data.ts +55 -0
- package/paseo-plugin/client/detail.tsx +277 -0
- package/paseo-plugin/client/format.ts +101 -0
- package/paseo-plugin/client/live.ts +161 -0
- package/paseo-plugin/client/overview.tsx +89 -0
- package/paseo-plugin/client/panel.tsx +56 -0
- package/paseo-plugin/client/row.tsx +40 -0
- package/paseo-plugin/client/settings.tsx +90 -0
- package/paseo-plugin/client/sidebar.tsx +27 -0
- package/paseo-plugin/client/stats.tsx +80 -0
- package/paseo-plugin/client/ui.tsx +111 -0
- package/paseo-plugin/index.client.tsx +52 -0
- package/paseo-plugin/index.server.ts +41 -0
- package/paseo-plugin/package.json +22 -0
- package/paseo-plugin/paseo-plugin.json +6 -0
- package/paseo-plugin/server/handlers.ts +45 -0
- package/paseo-plugin/server/req.ts +100 -0
- package/paseo-plugin/shared/contracts.ts +165 -0
- package/paseo-plugin/tsconfig.json +16 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// `req api`: machine-readable output for programs (the Paseo plugin, scripts), 1.9.
|
|
2
|
+
// The format is a contract (contracts/cli-json.md): every command prints one JSON object,
|
|
3
|
+
// { "schema": 1, "kitVersion": "…", "data": … } exit 0
|
|
4
|
+
// { "schema": 1, "kitVersion": "…", "error": { "code", "message" } } exit 1
|
|
5
|
+
//
|
|
6
|
+
// req api version
|
|
7
|
+
// req api list [--all] [--repo <dir>]
|
|
8
|
+
// req api status <需求> [--repo <dir>]
|
|
9
|
+
// req api file <需求> <文件> [--repo <dir>]
|
|
10
|
+
// req api stats [--since 30d] [--month 2026-10]
|
|
11
|
+
// req api index [--rebuild <dir>...] [--prune]
|
|
12
|
+
// req api run <approve|stop|resume|verify> <需求> [--repo <dir>] [其他选项]
|
|
13
|
+
//
|
|
14
|
+
// Reading never changes anything. `run` performs the same command as `req <command>` in that
|
|
15
|
+
// directory, so the engine stays the only one that changes a requirement's state.
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { resultText, stepName } from "./control.js";
|
|
21
|
+
import { FEATURES_DIR, findWorktreeRun, listFeatures, resolveFeatureArg, summarize } from "./features.js";
|
|
22
|
+
import { KIT_ROOT, kitVersion } from "./locations.js";
|
|
23
|
+
import { pruneIndex, readIndex, registerRequirement, requirementKey } from "./records.js";
|
|
24
|
+
import { isAlive, type PipelineState, readState, statePath, type StepRecord } from "./state.js";
|
|
25
|
+
import { parsePeriod, summarizeRuns } from "./stats.js";
|
|
26
|
+
import { readRuns, runsFile } from "./records.js";
|
|
27
|
+
import { totals } from "./usage.js";
|
|
28
|
+
|
|
29
|
+
export const API_SCHEMA = 1;
|
|
30
|
+
|
|
31
|
+
export class ApiError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
readonly code: string,
|
|
34
|
+
message: string,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Documents a program may read from a feature directory (`req api file`). */
|
|
41
|
+
export const READABLE_FILES = [
|
|
42
|
+
"spec.md",
|
|
43
|
+
"alignment.md",
|
|
44
|
+
"spec-review.md",
|
|
45
|
+
"changes.md",
|
|
46
|
+
"decisions.tsv",
|
|
47
|
+
"design.md",
|
|
48
|
+
"design-review.md",
|
|
49
|
+
"preview.md",
|
|
50
|
+
"contract.md",
|
|
51
|
+
"diagnosis.md",
|
|
52
|
+
"assessment.md",
|
|
53
|
+
"implementation.md",
|
|
54
|
+
"progress.md",
|
|
55
|
+
"code-review.md",
|
|
56
|
+
"handoff.md",
|
|
57
|
+
"service-start.md",
|
|
58
|
+
"verification.md",
|
|
59
|
+
"report.md",
|
|
60
|
+
"acceptance.md",
|
|
61
|
+
"lessons.tsv",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
65
|
+
|
|
66
|
+
export type Attention = "approval" | "business" | "interrupted" | "not_completed" | null;
|
|
67
|
+
|
|
68
|
+
/** What a person needs to do about a requirement, if anything. */
|
|
69
|
+
export function attentionOf(state: Pick<PipelineState, "status" | "pid" | "outcome"> | null): Attention {
|
|
70
|
+
if (!state) return null;
|
|
71
|
+
if (state.status === "waiting_for_approval") return "approval";
|
|
72
|
+
if (state.status === "waiting_for_business") return "business";
|
|
73
|
+
if (state.status === "failed" || (state.status === "running" && !isAlive(state.pid))) return "interrupted";
|
|
74
|
+
if (state.status === "completed" && state.outcome === "not_completed") return "not_completed";
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function usageOf(state: PipelineState | null) {
|
|
79
|
+
if (!state || state.history.length === 0) return null;
|
|
80
|
+
const t = totals(state.history);
|
|
81
|
+
return { steps: t.steps, agentMinutes: Math.round(t.agentMinutes * 10) / 10, inputTokens: t.inputTokens, outputTokens: t.outputTokens, costUsd: t.costUsd };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readStateSafe(repo: string, featureDir: string): PipelineState | null {
|
|
85
|
+
try {
|
|
86
|
+
return readState(statePath(repo, featureDir));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function currentSteps(state: PipelineState | null) {
|
|
93
|
+
if (!state || state.status !== "running" || !isAlive(state.pid)) return [];
|
|
94
|
+
return state.history.filter((h) => h.outcome === "running").map((h) => ({ key: h.key, title: stepName(h, state), agentId: h.agentId }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ListItem {
|
|
98
|
+
key: string;
|
|
99
|
+
project: string;
|
|
100
|
+
repo: string;
|
|
101
|
+
sourceRepo: string | null;
|
|
102
|
+
workspaceId: string | null;
|
|
103
|
+
featureDir: string;
|
|
104
|
+
name: string;
|
|
105
|
+
title: string;
|
|
106
|
+
type: string | null;
|
|
107
|
+
profile: string | null;
|
|
108
|
+
branch: string | null;
|
|
109
|
+
/** Plain-language stage, as `req list` shows it. */
|
|
110
|
+
stage: string;
|
|
111
|
+
/** The state file's status; null before the first run. */
|
|
112
|
+
status: string | null;
|
|
113
|
+
running: boolean;
|
|
114
|
+
phase: string | null;
|
|
115
|
+
attention: Attention;
|
|
116
|
+
current: Array<{ key: string; title: string; agentId: string }>;
|
|
117
|
+
updatedAt: string;
|
|
118
|
+
usage: ReturnType<typeof usageOf>;
|
|
119
|
+
/** False when the index still lists a requirement whose directory is gone. */
|
|
120
|
+
present: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function item(repo: string, branch: string | null, featureDir: string, extra: { sourceRepo: string | null; workspaceId: string | null; project: string }): ListItem | null {
|
|
124
|
+
const name = path.basename(featureDir);
|
|
125
|
+
const summary = summarize({ path: repo, branch }, name);
|
|
126
|
+
if (!summary) return null;
|
|
127
|
+
const state = readStateSafe(repo, featureDir);
|
|
128
|
+
return {
|
|
129
|
+
key: requirementKey(extra.sourceRepo ?? repo, featureDir),
|
|
130
|
+
project: extra.project,
|
|
131
|
+
repo,
|
|
132
|
+
sourceRepo: extra.sourceRepo,
|
|
133
|
+
workspaceId: extra.workspaceId ?? state?.worktree?.workspaceId ?? null,
|
|
134
|
+
featureDir: summary.featureDir,
|
|
135
|
+
name,
|
|
136
|
+
title: summary.title,
|
|
137
|
+
type: state?.type ?? null,
|
|
138
|
+
profile: summary.profile ?? state?.profile ?? null,
|
|
139
|
+
branch: state?.branch ?? summary.branch,
|
|
140
|
+
stage: summary.stage,
|
|
141
|
+
status: state?.status ?? null,
|
|
142
|
+
running: !!state && state.status === "running" && isAlive(state.pid),
|
|
143
|
+
phase: state?.phase ?? null,
|
|
144
|
+
attention: attentionOf(state),
|
|
145
|
+
current: currentSteps(state),
|
|
146
|
+
updatedAt: summary.updatedAt,
|
|
147
|
+
usage: usageOf(state),
|
|
148
|
+
present: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Requirements of the repository at `repo` (all its worktrees), or of the whole machine. */
|
|
153
|
+
export function apiList(input: { repo: string; all: boolean }): ListItem[] {
|
|
154
|
+
if (!input.all) {
|
|
155
|
+
const repo = fs.realpathSync(input.repo);
|
|
156
|
+
return listFeatures(repo)
|
|
157
|
+
.map((f) => {
|
|
158
|
+
const state = readStateSafe(f.worktree, f.featureDir);
|
|
159
|
+
const source = state?.worktree?.sourceRepo ?? null;
|
|
160
|
+
return item(f.worktree, f.branch, f.featureDir, { sourceRepo: source, workspaceId: state?.worktree?.workspaceId ?? null, project: path.basename(source ?? repo) });
|
|
161
|
+
})
|
|
162
|
+
.filter((x): x is ListItem => x !== null);
|
|
163
|
+
}
|
|
164
|
+
const out: ListItem[] = [];
|
|
165
|
+
for (const e of readIndex()) {
|
|
166
|
+
const found = e.present ? item(e.repo, null, e.featureDir, { sourceRepo: e.sourceRepo, workspaceId: e.workspaceId, project: e.project }) : null;
|
|
167
|
+
out.push(
|
|
168
|
+
found ?? {
|
|
169
|
+
key: e.key,
|
|
170
|
+
project: e.project,
|
|
171
|
+
repo: e.repo,
|
|
172
|
+
sourceRepo: e.sourceRepo,
|
|
173
|
+
workspaceId: e.workspaceId,
|
|
174
|
+
featureDir: e.featureDir,
|
|
175
|
+
name: path.basename(e.featureDir),
|
|
176
|
+
title: path.basename(e.featureDir),
|
|
177
|
+
type: null,
|
|
178
|
+
profile: null,
|
|
179
|
+
branch: null,
|
|
180
|
+
stage: "已移除(目录不在了)",
|
|
181
|
+
status: null,
|
|
182
|
+
running: false,
|
|
183
|
+
phase: null,
|
|
184
|
+
attention: null,
|
|
185
|
+
current: [],
|
|
186
|
+
updatedAt: e.registeredAt,
|
|
187
|
+
usage: null,
|
|
188
|
+
present: false,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The repository and feature directory a requirement argument names, as `req status` resolves it. */
|
|
196
|
+
export function locate(repoArg: string, feature: string): { repo: string; featureDir: string; sourceRepo: string | null } {
|
|
197
|
+
if (!fs.existsSync(repoArg)) throw new ApiError("not_found", `目录不存在:${repoArg}`);
|
|
198
|
+
const repo = fs.realpathSync(repoArg);
|
|
199
|
+
let here: string | null = null;
|
|
200
|
+
try {
|
|
201
|
+
here = resolveFeatureArg(repo, feature);
|
|
202
|
+
} catch {
|
|
203
|
+
here = null;
|
|
204
|
+
}
|
|
205
|
+
if (here && fs.existsSync(statePath(repo, here))) return { repo, featureDir: here, sourceRepo: null };
|
|
206
|
+
const away = findWorktreeRun(repo, here ?? feature);
|
|
207
|
+
if (away) return { repo: away.repo, featureDir: away.featureDir, sourceRepo: repo };
|
|
208
|
+
if (here) return { repo, featureDir: here, sourceRepo: null };
|
|
209
|
+
throw new ApiError("not_found", `没有找到需求"${feature}"(${repo})`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stepJson(h: StepRecord, state: PipelineState) {
|
|
213
|
+
const minutes = Math.max(0, ((h.endedAt ? new Date(h.endedAt) : new Date()).getTime() - new Date(h.startedAt).getTime()) / 60_000);
|
|
214
|
+
return {
|
|
215
|
+
key: h.key,
|
|
216
|
+
stepId: h.stepId,
|
|
217
|
+
title: stepName(h, state),
|
|
218
|
+
phase: h.phase ?? null,
|
|
219
|
+
attempt: h.attempt,
|
|
220
|
+
round: h.round,
|
|
221
|
+
agentId: h.agentId,
|
|
222
|
+
startedAt: h.startedAt,
|
|
223
|
+
endedAt: h.endedAt,
|
|
224
|
+
minutes: Math.round(minutes * 10) / 10,
|
|
225
|
+
outcome: h.outcome,
|
|
226
|
+
result: h.outcome === "completed" ? resultText(h) : null,
|
|
227
|
+
error: h.error,
|
|
228
|
+
usage: h.usage ?? null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function apiStatus(repoArg: string, feature: string) {
|
|
233
|
+
const { repo, featureDir, sourceRepo } = locate(repoArg, feature);
|
|
234
|
+
const state = readStateSafe(repo, featureDir);
|
|
235
|
+
const source = sourceRepo ?? state?.worktree?.sourceRepo ?? null;
|
|
236
|
+
const base = item(repo, state?.branch ?? null, featureDir, { sourceRepo: source, workspaceId: state?.worktree?.workspaceId ?? null, project: path.basename(source ?? repo) });
|
|
237
|
+
if (!base) throw new ApiError("not_found", `${featureDir} 中没有 spec.md`);
|
|
238
|
+
const dir = path.join(repo, featureDir);
|
|
239
|
+
return {
|
|
240
|
+
...base,
|
|
241
|
+
files: READABLE_FILES.filter((f) => fs.existsSync(path.join(dir, f))),
|
|
242
|
+
run: state
|
|
243
|
+
? {
|
|
244
|
+
attempt: state.attempt,
|
|
245
|
+
startedAt: state.startedAt,
|
|
246
|
+
updatedAt: state.updatedAt,
|
|
247
|
+
pid: state.pid,
|
|
248
|
+
phaseMode: state.phases?.mode ?? null,
|
|
249
|
+
stopReason: state.stopReason,
|
|
250
|
+
outcome: state.outcome,
|
|
251
|
+
queuedSince: state.queuedSince ?? null,
|
|
252
|
+
verifyAt: state.verifyAt ?? null,
|
|
253
|
+
approval: state.approval ?? null,
|
|
254
|
+
originAgentId: state.originAgentId,
|
|
255
|
+
}
|
|
256
|
+
: null,
|
|
257
|
+
steps: state ? state.history.map((h) => stepJson(h, state)) : [],
|
|
258
|
+
gate: state?.gate ?? null,
|
|
259
|
+
joint: (state?.joint ?? []).map((j) => ({ name: j.name, slug: j.slug, repo: j.repo, sourceRepo: j.sourceRepo, branch: j.branch, workspaceId: j.workspaceId, gate: j.gate ?? null })),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function apiFile(repoArg: string, feature: string, file: string) {
|
|
264
|
+
if (!(READABLE_FILES as readonly string[]).includes(file)) {
|
|
265
|
+
throw new ApiError("not_allowed", `只能读取这些文件:${READABLE_FILES.join("、")}`);
|
|
266
|
+
}
|
|
267
|
+
const { repo, featureDir } = locate(repoArg, feature);
|
|
268
|
+
const full = path.join(repo, featureDir, file);
|
|
269
|
+
if (!fs.existsSync(full)) throw new ApiError("not_found", `${featureDir} 中没有 ${file}`);
|
|
270
|
+
const size = fs.statSync(full).size;
|
|
271
|
+
const buf = fs.readFileSync(full);
|
|
272
|
+
return { featureDir, file, size, truncated: size > MAX_FILE_BYTES, content: buf.subarray(0, MAX_FILE_BYTES).toString("utf8") };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Registers the requirements that have run in these directories' repositories (all worktrees). */
|
|
276
|
+
export function apiIndex(input: { rebuild: string[]; prune: boolean }) {
|
|
277
|
+
let registered = 0;
|
|
278
|
+
const dirs = new Set(input.rebuild.map((d) => path.resolve(d)));
|
|
279
|
+
for (const e of readIndex()) if (input.rebuild.length > 0 && e.present) dirs.add(e.sourceRepo ?? e.repo);
|
|
280
|
+
for (const dir of dirs) {
|
|
281
|
+
if (!fs.existsSync(dir)) continue;
|
|
282
|
+
for (const f of listFeatures(dir)) {
|
|
283
|
+
const state = readStateSafe(f.worktree, f.featureDir);
|
|
284
|
+
if (!state) continue;
|
|
285
|
+
registerRequirement({ repo: f.worktree, featureDir: f.featureDir, worktree: state.worktree ?? null });
|
|
286
|
+
registered++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const pruned = input.prune ? pruneIndex() : 0;
|
|
290
|
+
return { scanned: [...dirs], registered, pruned, total: readIndex().length };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const RUNNABLE = ["approve", "stop", "resume", "verify"] as const;
|
|
294
|
+
|
|
295
|
+
/** Runs `req <command> <需求>` where people would, and reports what it said. */
|
|
296
|
+
export function apiRun(command: string, repoArg: string, feature: string, extra: string[]) {
|
|
297
|
+
if (!(RUNNABLE as readonly string[]).includes(command)) throw new ApiError("not_allowed", `只能执行:${RUNNABLE.join("、")}`);
|
|
298
|
+
if (!fs.existsSync(repoArg)) throw new ApiError("not_found", `目录不存在:${repoArg}`);
|
|
299
|
+
const cwd = fs.realpathSync(repoArg);
|
|
300
|
+
// Not from a chat: the run keeps reporting to the chat that started it.
|
|
301
|
+
const env = { ...process.env };
|
|
302
|
+
delete env.PASEO_AGENT_ID;
|
|
303
|
+
delete env.PASEO_AGENT_CWD;
|
|
304
|
+
const r = spawnSync(process.execPath, [path.join(KIT_ROOT, "bin", "req.mjs"), command, feature, ...extra], {
|
|
305
|
+
cwd,
|
|
306
|
+
env,
|
|
307
|
+
encoding: "utf8",
|
|
308
|
+
timeout: 180_000,
|
|
309
|
+
windowsHide: true,
|
|
310
|
+
});
|
|
311
|
+
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
|
|
312
|
+
return { command, featureDir: feature, ok: r.status === 0, exitCode: r.status, message: r.error ? r.error.message : output };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// CLI
|
|
317
|
+
|
|
318
|
+
function option(argv: string[], name: string): string | null {
|
|
319
|
+
const i = argv.indexOf(name);
|
|
320
|
+
if (i < 0) return null;
|
|
321
|
+
const v = argv[i + 1];
|
|
322
|
+
if (v === undefined || v.startsWith("--")) throw new ApiError("bad_arguments", `${name} 后面要写值`);
|
|
323
|
+
argv.splice(i, 2);
|
|
324
|
+
return v;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function flag(argv: string[], name: string): boolean {
|
|
328
|
+
const i = argv.indexOf(name);
|
|
329
|
+
if (i < 0) return false;
|
|
330
|
+
argv.splice(i, 1);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function apiMain(argvIn: string[], cwd = process.cwd()): unknown {
|
|
335
|
+
const argv = [...argvIn];
|
|
336
|
+
const command = argv.shift();
|
|
337
|
+
const repo = option(argv, "--repo") ?? cwd;
|
|
338
|
+
const need = (what: string) => {
|
|
339
|
+
const v = argv.shift();
|
|
340
|
+
if (!v || v.startsWith("--")) throw new ApiError("bad_arguments", `缺少${what}`);
|
|
341
|
+
return v;
|
|
342
|
+
};
|
|
343
|
+
switch (command) {
|
|
344
|
+
case "version":
|
|
345
|
+
return {
|
|
346
|
+
kitVersion: kitVersion(),
|
|
347
|
+
kitRoot: KIT_ROOT,
|
|
348
|
+
apiSchema: API_SCHEMA,
|
|
349
|
+
paseo: (JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "package.json"), "utf8")) as { reqKit?: { paseo?: string } }).reqKit?.paseo ?? null,
|
|
350
|
+
featuresDir: FEATURES_DIR.split(path.sep).join("/"),
|
|
351
|
+
};
|
|
352
|
+
case "list":
|
|
353
|
+
return apiList({ repo, all: flag(argv, "--all") });
|
|
354
|
+
case "status":
|
|
355
|
+
return apiStatus(repo, need("需求"));
|
|
356
|
+
case "file": {
|
|
357
|
+
const feature = need("需求");
|
|
358
|
+
return apiFile(repo, feature, need("文件名"));
|
|
359
|
+
}
|
|
360
|
+
case "stats": {
|
|
361
|
+
const since = option(argv, "--since");
|
|
362
|
+
const month = option(argv, "--month");
|
|
363
|
+
return summarizeRuns(readRuns(runsFile()), parsePeriod({ since, month }));
|
|
364
|
+
}
|
|
365
|
+
case "index": {
|
|
366
|
+
const prune = flag(argv, "--prune");
|
|
367
|
+
const rebuild: string[] = [];
|
|
368
|
+
if (flag(argv, "--rebuild")) {
|
|
369
|
+
while (argv[0] && !argv[0].startsWith("--")) rebuild.push(argv.shift()!);
|
|
370
|
+
if (rebuild.length === 0) rebuild.push(cwd);
|
|
371
|
+
}
|
|
372
|
+
return rebuild.length > 0 || prune ? apiIndex({ rebuild, prune }) : readIndex();
|
|
373
|
+
}
|
|
374
|
+
case "run": {
|
|
375
|
+
const what = need("命令(approve、stop、resume、verify)");
|
|
376
|
+
return apiRun(what, repo, need("需求"), argv.splice(0));
|
|
377
|
+
}
|
|
378
|
+
default:
|
|
379
|
+
throw new ApiError("bad_arguments", `没有 "${command ?? ""}" 这个 api 命令。可用:version、list、status、file、stats、index、run`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
|
384
|
+
let code = 0;
|
|
385
|
+
let out: Record<string, unknown>;
|
|
386
|
+
try {
|
|
387
|
+
out = { schema: API_SCHEMA, kitVersion: kitVersion(), data: apiMain(process.argv.slice(2)) };
|
|
388
|
+
const data = out.data as { ok?: boolean } | null;
|
|
389
|
+
if (data && data.ok === false) code = 1;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
const e = error as Error & { code?: string };
|
|
392
|
+
out = { schema: API_SCHEMA, kitVersion: kitVersion(), error: { code: error instanceof ApiError ? e.code : "failed", message: e.message } };
|
|
393
|
+
code = 1;
|
|
394
|
+
}
|
|
395
|
+
process.stdout.write(`${JSON.stringify(out)}\n`);
|
|
396
|
+
process.exit(code);
|
|
397
|
+
}
|
package/engine/src/control.ts
CHANGED
|
@@ -31,7 +31,7 @@ const RESULT_TEXT: Record<string, string> = {
|
|
|
31
31
|
not_completed: "未完成",
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
function resultText(record: StepRecord): string {
|
|
34
|
+
export function resultText(record: StepRecord): string {
|
|
35
35
|
const r = record.result ?? {};
|
|
36
36
|
if (typeof r.has_runnable_service === "boolean") return r.has_runnable_service ? "完成(项目有常驻服务)" : "完成";
|
|
37
37
|
if (typeof r.started === "boolean") return r.started ? "服务启动成功" : "服务启动失败";
|
|
@@ -42,7 +42,7 @@ function resultText(record: StepRecord): string {
|
|
|
42
42
|
return "完成";
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function stepName(record: StepRecord, state: Pick<PipelineState, "type" | "joint"> | null = null): string {
|
|
45
|
+
export function stepName(record: StepRecord, state: Pick<PipelineState, "type" | "joint"> | null = null): string {
|
|
46
46
|
const [base, slug] = record.key.split("@");
|
|
47
47
|
const repo = slug ? (state?.joint ?? []).find((j) => j.slug === slug)?.name ?? slug : null;
|
|
48
48
|
const title = `${stepTitle(state?.type, record.stepId)}${repo ? `·${repo}` : ""}`;
|
package/engine/src/eval.ts
CHANGED
|
@@ -104,6 +104,8 @@ function runPipeline(repo: string, featureDir: string, c: EvalCase, configPath:
|
|
|
104
104
|
const env = { ...process.env };
|
|
105
105
|
delete env.PASEO_AGENT_ID;
|
|
106
106
|
delete env.PASEO_AGENT_CWD;
|
|
107
|
+
// Evaluation runs are not real requirements: keep them out of the machine's index and usage records.
|
|
108
|
+
env.REQ_KIT_NO_RECORD = "1";
|
|
107
109
|
const run = spawnSync(process.execPath, args, { cwd: repo, env, encoding: "utf8", timeout: timeoutMs, windowsHide: true });
|
|
108
110
|
fs.appendFileSync(path.join(repo, featureDir, ".pipeline", "eval-run.log"), `${run.stdout ?? ""}${run.stderr ?? ""}`);
|
|
109
111
|
if (run.error) return `流水线进程出错:${run.error.message}`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `req index`: the machine's list of requirements (records.ts), for people.
|
|
2
|
+
// req index what is recorded
|
|
3
|
+
// req index --rebuild [<dir>...] record the requirements that have run in these repositories
|
|
4
|
+
// (default: here) and in those already recorded
|
|
5
|
+
// req index --prune forget the requirements whose directory is gone
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { apiIndex } from "./api.js";
|
|
9
|
+
import { indexDir, readIndex } from "./records.js";
|
|
10
|
+
|
|
11
|
+
const argv = process.argv.slice(2);
|
|
12
|
+
const prune = argv.includes("--prune");
|
|
13
|
+
const rebuildAt = argv.indexOf("--rebuild");
|
|
14
|
+
const dirs = rebuildAt < 0 ? [] : argv.slice(rebuildAt + 1).filter((a) => !a.startsWith("--"));
|
|
15
|
+
if (rebuildAt >= 0 && dirs.length === 0) dirs.push(process.cwd());
|
|
16
|
+
|
|
17
|
+
if (rebuildAt >= 0 || prune) {
|
|
18
|
+
const r = apiIndex({ rebuild: dirs, prune });
|
|
19
|
+
if (rebuildAt >= 0) console.log(`扫描了 ${r.scanned.length} 个仓库,登记 ${r.registered} 个运行过的需求。`);
|
|
20
|
+
if (prune) console.log(`移除了 ${r.pruned} 个目录已不在的需求。`);
|
|
21
|
+
console.log(`本机共记录 ${r.total} 个需求(${indexDir()})。`);
|
|
22
|
+
} else {
|
|
23
|
+
const entries = readIndex();
|
|
24
|
+
if (entries.length === 0) {
|
|
25
|
+
console.log("本机还没有记录任何需求。1.9 之前运行过的需求:在项目目录运行 req index --rebuild。");
|
|
26
|
+
} else {
|
|
27
|
+
console.log(`本机记录的需求(${entries.length} 个;查看进度用 req api list --all 或 Paseo 插件):`);
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
console.log(`- ${e.project}:${path.basename(e.featureDir)}${e.present ? "" : "(目录已不在,req index --prune 可以移除)"}`);
|
|
30
|
+
console.log(` ${path.join(e.repo, e.featureDir)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/engine/src/main.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// from the chat agent's shell; can also be run in the foreground for debugging.
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { approveCommand, LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
|
|
7
8
|
import { dequeuedMessage, queuedMessage, watchdogPrompt } from "./messages.js";
|
|
@@ -13,6 +14,7 @@ import { satisfies } from "./daemon-check.js";
|
|
|
13
14
|
import { describeAgentProfile, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
14
15
|
import { jointProblem } from "./joint.js";
|
|
15
16
|
import { runPipeline } from "./pipeline.js";
|
|
17
|
+
import { appendRun, runRecord } from "./records.js";
|
|
16
18
|
import { preflight, verifyPhaseProblem } from "./preflight.js";
|
|
17
19
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
18
20
|
import {
|
|
@@ -243,6 +245,28 @@ async function main(): Promise<number> {
|
|
|
243
245
|
writeState(stateFile, state);
|
|
244
246
|
}
|
|
245
247
|
|
|
248
|
+
// This run's usage record (1.9): written once when the process ends, however it ends.
|
|
249
|
+
let recorded = false;
|
|
250
|
+
const recordRun = () => {
|
|
251
|
+
if (recorded) return;
|
|
252
|
+
recorded = true;
|
|
253
|
+
const title = (() => {
|
|
254
|
+
try {
|
|
255
|
+
return (/^#\s+(.+)$/m.exec(fs.readFileSync(checked.specPath, "utf8"))?.[1] ?? path.basename(featureDir)).trim();
|
|
256
|
+
} catch {
|
|
257
|
+
return path.basename(featureDir);
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
appendRun(
|
|
261
|
+
runRecord({
|
|
262
|
+
state,
|
|
263
|
+
runStartedAt: now,
|
|
264
|
+
title,
|
|
265
|
+
agents: { builder: describeAgentProfile(config.agents.builder), reviewer: describeAgentProfile(config.agents.reviewer) },
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
246
270
|
let releaseSlot = () => {};
|
|
247
271
|
const shutdown = (signal: string) => {
|
|
248
272
|
log(`收到 ${signal},流水线停止`);
|
|
@@ -250,6 +274,7 @@ async function main(): Promise<number> {
|
|
|
250
274
|
state.stopReason = `流水线进程被 ${signal} 终止`;
|
|
251
275
|
state.queuedSince = null;
|
|
252
276
|
writeState(stateFile, state);
|
|
277
|
+
recordRun();
|
|
253
278
|
releaseSlot();
|
|
254
279
|
release();
|
|
255
280
|
process.exit(1);
|
|
@@ -331,6 +356,7 @@ async function main(): Promise<number> {
|
|
|
331
356
|
writeState(stateFile, state);
|
|
332
357
|
}
|
|
333
358
|
await runtime.close().catch(() => undefined);
|
|
359
|
+
recordRun();
|
|
334
360
|
releaseSlot();
|
|
335
361
|
release();
|
|
336
362
|
}
|