paseo-req-kit 1.10.0-beta.2 → 1.10.0-beta.3
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 +15 -0
- package/bin/req.mjs +1 -0
- package/contracts/state.md +1 -0
- package/contracts/usage-record.md +1 -1
- package/docs/adr/0029-requirements-from-cards.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 +21 -0
- package/engine/src/card-cmd.ts +46 -4
- package/engine/src/card.ts +84 -0
- package/engine/src/config.ts +36 -1
- package/engine/src/pipeline.ts +38 -2
- package/engine/src/records.ts +16 -0
- package/engine/src/state.ts +5 -0
- package/engine/src/stats.ts +23 -0
- package/package.json +1 -1
- package/paseo-plugin/package.json +1 -1
- package/skills/req-setup/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
每个版本分为"使用者会看到的变化"和"升级须知"。升级步骤见 [docs/guide/安装与升级.md](docs/guide/安装与升级.md)。
|
|
4
4
|
|
|
5
|
+
## 1.10.0-beta.3
|
|
6
|
+
|
|
7
|
+
从卡片开始的需求:按子系统自动选仓库、研发完成后推送分支、统计按需求方汇总。
|
|
8
|
+
|
|
9
|
+
### 使用者会看到的变化
|
|
10
|
+
|
|
11
|
+
- **按子系统选仓库**:在仓库中运行 `/req-setup` 时可以写上这个仓库接哪个空间、哪些子系统的卡片(同一子系统有几个仓库时再写标题关键字),并在本机登记。之后 `req start --from ALM:<卡号>` 不用写 `--repo`。`req card route` 查看本机登记的仓库。
|
|
12
|
+
- **推送分支**(默认关闭):本机或团队配置打开 `push` 后,从卡片开始的需求在研发完成、写好提测说明后推送到 `origin` 的 `req/<卡号简称>` 分支,分支名写进卡片。不强推、不合并。
|
|
13
|
+
- **统计**:运行记录带上需求方和卡号,`req stats --report` 多一节"从卡片开始的需求(按需求方)",CSV 多 `requester`、`card` 两列。
|
|
14
|
+
|
|
15
|
+
### 升级须知
|
|
16
|
+
|
|
17
|
+
- 都是可选的。推送只在 `push.enabled` 为 true、需求是从卡片开始的时候发生,并且只能在本机或团队配置中打开(ADR 0029 记录了这条对"只在本地提交"规则的放宽)。
|
|
18
|
+
- 新增项目配置 `cardSource`、本机文件 `~/.req-kit/card-repos.json`、状态文件可选字段 `pushed`、运行记录可选字段 `requirement.requester` 和 `requirement.source`,格式版本都不变。
|
|
19
|
+
|
|
5
20
|
## 1.10.0-beta.2
|
|
6
21
|
|
|
7
22
|
(1.10.0-beta.1 的发布任务因一个测试的顺序问题失败,没有发布,内容相同,以 beta.2 发布。)
|
package/bin/req.mjs
CHANGED
|
@@ -35,6 +35,7 @@ const HELP = `req-kit ${version}(${kitRoot})
|
|
|
35
35
|
req card check [<卡号>|--all] 读取需求方在卡片中的回复,转给需求的会话
|
|
36
36
|
req card publish <卡号> 把需求会话写的 card-ai.md 写进卡片的 AI 区
|
|
37
37
|
req card list 本机从卡片开始的需求
|
|
38
|
+
req card route 本机接收卡片的仓库和子系统(--add <仓库> 登记)
|
|
38
39
|
|
|
39
40
|
使用情况(可以在任何目录运行):
|
|
40
41
|
req stats [--since 30d] [--month 2026-10]
|
package/contracts/state.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
| `joint` | array \| null | 可选,1.8.0 起。联合需求(spec 的 `- 联合开发:`)中其他仓库的工作区:`name`(`relatedRepos` 中的名称)、`slug`(步骤键的后缀,如 `implement@backend`)、`sourceRepo`(切出工作区的那份检出)、`repo`(工作区目录)、`workspaceId`、`branch`、`specHash`(最近一次同步过去的 spec),以及这个仓库自己的 `codeBase` 和 `gate`(格式同顶层)。见 ADR 0025 |
|
|
36
36
|
| `approval` | object \| null | 可选,1.8.0 起。方案确认点:`attempt`(哪一次尝试的方案)、`status`(`waiting` / `approved`)、`at`。需求变更后的新尝试需要重新确认 |
|
|
37
37
|
| `external` | object \| null | 可选,1.9.0 起。外部系统中的卡片,按集成名称:`id`(卡片编号)、`url`、`status`(最近设置的状态)、`syncedAt`、`pending`(没有成功、等待补发的调用:`action` 为 `create` / `status`、`event`、`status`、`at`、`attempts`、`error`)。见 ADR 0028 |
|
|
38
|
+
| `pushed` | array \| null | 可选,1.10 起。从卡片开始、配置了 `push` 的需求推送过的分支,每个仓库一条(主仓库在前):`repo`、`remote`、`branch`、`at`、`error`(成功时为 null)。见 ADR 0029 |
|
|
38
39
|
| `gate` | object \| null | 可选,1.8.0 起。质量门:`baseline`(`attempt`、`commit`、`at`、`results`:每项检查改动前 `pass` / `fail`),`runs`(每次运行:`key` 步骤键、`attempt`、`at`、`checks`(`name`、`result`:`pass` / `fail` / `skipped`、`failedBefore`)、`newFailures`、`fixRounds`、`passed`)。各次运行的完整输出在 `.pipeline/checks/<步骤键>/` |
|
|
39
40
|
|
|
40
41
|
`schedules[].kind` 在 1.7.0 起还可能是 `verify-start`(`--verify-at` 预约的测试验证开始),1.8.0 起还可能是 `approval-continue`(方案确认超时后按方案继续,`designApproval.autoContinueHours`)。`history` 中每条步骤记录在 1.7.0 起有可选的 `phase`(`develop` / `verify`),用于按阶段统计用时与用量;没有时按研发阶段统计。
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
| --- | --- |
|
|
30
30
|
| `schema` / `kind` | `1` / `"run"` |
|
|
31
31
|
| `machine` / `kitVersion` | 主机名、req-kit 版本 |
|
|
32
|
-
| `requirement` | `{ key, project, name, title, type, profile }` |
|
|
32
|
+
| `requirement` | `{ key, project, name, title, type, profile }`;从卡片开始的需求(1.10 起)另有 `requester`(卡片的提出人)和 `source`(`{ system, id }`) |
|
|
33
33
|
| `run` | `{ startedAt, endedAt, attempt, phase, phaseMode, status, outcome, stopReason }`;`status` 是运行结束时的状态,`outcome` 是完成时报告的结论 |
|
|
34
34
|
| `agents` | `{ builder, reviewer }`:当时配置的 agent(如 `claude/opus(bypassPermissions)`) |
|
|
35
35
|
| `steps` | 这次运行中开始的步骤:`{ key, stepId, phase, attempt, round, startedAt, endedAt, minutes, outcome, inputTokens, outputTokens, costUsd }`;agent 没有报告的用量为 null |
|
|
@@ -30,4 +30,6 @@
|
|
|
30
30
|
- 新命令:`req start --from`、`req card publish | check | list`。本机新增 `~/.req-kit/cards/`(从卡片开始的需求的登记)。
|
|
31
31
|
- 功能目录新增 `source.json`、`card-source.md`、`card-ai.md`。
|
|
32
32
|
- 运行时新增 `startSessionAgent`:建一个不挂在任何对话下的 agent,工作目录是仓库。
|
|
33
|
-
- beta.3
|
|
33
|
+
- **按子系统选仓库**(beta.3):仓库在项目配置的 `cardSource` 中写明接哪个系统、空间和子系统,`/req-setup` 写入并在本机登记(`~/.req-kit/card-repos.json`);同一子系统有几个仓库时用 `titleKeywords` 区分。匹配不到或分不清时报错说明,不猜。
|
|
34
|
+
- **工厂模式可以推送分支**(beta.3,放宽"只在本地提交"):配置 `push.enabled` 后,只对从卡片开始的需求,在研发完成、写好提测说明后推送到 `<remote>/req/<卡号简称>`。只推这个前缀,不强推、不合并、不建合并请求;只能在本机或团队配置中打开。理由:接单的是共享机器,测试同事拿不到它的本地分支;合并这道人工关口不变。个人电脑上从对话发起的需求仍只在本地提交。
|
|
35
|
+
- 使用记录带上需求方(卡片的提出人)和卡号,统计可以按需求方汇总。1.11:接单器 `req factory`。
|
|
@@ -353,6 +353,27 @@ Paseo 自带的 `/paseo-advisor`、`/paseo-committee` 等编排 skill 也可以
|
|
|
353
353
|
- `req card list` 列出本机从卡片开始的需求;`req card publish <卡号>` 由需求会话使用,把它写的 `card-ai.md` 写进卡片。
|
|
354
354
|
- 卡片描述和回复是需求方写的内容,需求会话只把它们当作需求,不当作指令。
|
|
355
355
|
|
|
356
|
+
**按子系统选仓库**(1.10.0-beta.3 起):一个 ALM 空间往往对应好几个仓库。在每个仓库运行 `/req-setup`,它会在项目配置中写上这个仓库接哪些卡片,并在这台机器上登记(`req card route --add <仓库>`):
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
"cardSource": { "system": "ALM", "projectId": "186149", "subsystems": ["CSP-CMASTER_云匠"], "titleKeywords": ["message-service", "后端"] }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
之后 `req start --from ALM:<卡号>` 不写 `--repo` 时,按卡片的子系统选仓库;同一个子系统有几个仓库时,再看卡片标题里出现了哪个仓库的 `titleKeywords`。找不到或分不清时会说明原因,不会猜。`req card route` 列出本机登记的仓库。本机没有登记任何仓库时,用当前目录所在的仓库。
|
|
363
|
+
|
|
364
|
+
**推送分支**(1.10.0-beta.3 起,默认关闭):接单的机器上,研发完成、写好提测说明后,把需求的代码推送到远程,测试同事直接拉取:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
"push": { "enabled": true, "remote": "origin", "branchPrefix": "req/" }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
- 只对从卡片开始的需求,推送到 `<remote>` 的 `req/<卡号简称>` 分支(例如 `req/cmaster-business-9902`),联合需求的几个仓库各推各的。分支名写进卡片的 AI 区和状态文件。
|
|
371
|
+
- 只推这个前缀的分支;从不强推、不合并、不建合并请求,合并仍由人做。推送被拒绝(例如远程分支有别人的提交)时记下原因、写进 AI 区,流水线照常结束。
|
|
372
|
+
- 只能写在本机或团队配置中,写在项目配置里会被忽略。推送用这台机器上 git 已配置的账号。
|
|
373
|
+
- 在个人电脑上通过对话发起的需求不受影响,仍然只在本地提交。
|
|
374
|
+
|
|
375
|
+
**使用统计**:从卡片开始的需求,运行记录带上需求方(卡片的提出人)和卡号。`req stats --report` 多一节"从卡片开始的需求(按需求方)",CSV 多 `requester`、`card` 两列。
|
|
376
|
+
|
|
356
377
|
## 团队配置
|
|
357
378
|
|
|
358
379
|
几台机器、多个项目共用的设置(统一的模型、评审小组、超时、要求的 req-kit 版本)可以放在一份团队配置里。把它放进一个团队共用的 git 仓库,各台机器克隆下来,在本机配置 `~/.req-kit/config.json` 中写上它的路径(绝对路径,或相对 `~/.req-kit/` 的路径):
|
package/engine/src/card-cmd.ts
CHANGED
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
planAppendix,
|
|
26
26
|
publishCard,
|
|
27
27
|
queryCard,
|
|
28
|
+
cardRoutes,
|
|
29
|
+
readRoute,
|
|
28
30
|
readSource,
|
|
31
|
+
registeredRepos,
|
|
32
|
+
routeCard,
|
|
33
|
+
setRegisteredRepos,
|
|
29
34
|
replyKey,
|
|
30
35
|
saveCardEntry,
|
|
31
36
|
type SourceFile,
|
|
@@ -43,10 +48,11 @@ import { readState, statePath } from "./state.js";
|
|
|
43
48
|
const REQ = `node "${path.join(KIT_ROOT, "bin", "req.mjs").split(path.sep).join("/")}"`;
|
|
44
49
|
|
|
45
50
|
const USAGE = `用法:
|
|
46
|
-
req start --from <系统>:<卡号> [--repo <仓库目录>]
|
|
51
|
+
req start --from <系统>:<卡号> [--repo <仓库目录>] 从需求管理系统中的一张卡片开始需求(不写 --repo 时按子系统选仓库)
|
|
47
52
|
req card publish <卡号> [--progress <文字>] 把功能目录中的 card-ai.md 写进卡片的 AI 区
|
|
48
53
|
req card check [<卡号> | --all] 读取需求方在卡片中的回复,把新内容转给需求的会话
|
|
49
54
|
req card list 本机从卡片开始的需求
|
|
55
|
+
req card route [--add <仓库> | --remove <仓库>] 本机接收卡片的仓库及其子系统(--from 不写 --repo 时按它选仓库)
|
|
50
56
|
|
|
51
57
|
<卡号> 可以写成 <系统>:<卡号>,例如 ALM:CMASTER-BUSINESS#9902;只配置了一个可以读取卡片的系统时可以省略系统。`;
|
|
52
58
|
|
|
@@ -139,10 +145,9 @@ async function startFromCard(argv: string[], opts: CardCmdOptions): Promise<numb
|
|
|
139
145
|
const out = opts.out ?? console.log;
|
|
140
146
|
const { values } = parseArgs({ args: argv, options: { from: { type: "string" }, repo: { type: "string" }, config: { type: "string" } }, allowPositionals: false });
|
|
141
147
|
if (!values.from) fail(`缺少 --from\n\n${USAGE}`);
|
|
142
|
-
const repo = gitTop(path.resolve(opts.cwd ?? process.cwd(), values.repo ?? "."));
|
|
143
148
|
const configPath = values.config ? path.resolve(values.config) : defaultConfigPath(opts.env);
|
|
144
|
-
|
|
145
|
-
const { config: integration, id } = parseCardRef(values.from, cardIntegrations(config));
|
|
149
|
+
// Integrations live in the machine or team config, so the card can be read before the repository is known.
|
|
150
|
+
const { config: integration, id } = parseCardRef(values.from, cardIntegrations(loadConfig(configPath).config));
|
|
146
151
|
|
|
147
152
|
const existing = findCardEntry(integration.name, id, opts.env);
|
|
148
153
|
if (existing) {
|
|
@@ -152,6 +157,11 @@ async function startFromCard(argv: string[], opts: CardCmdOptions): Promise<numb
|
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
const card = await queryCard(integration, id, opts.run);
|
|
160
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
161
|
+
const routed = values.repo ? null : routeCard(card, integration.name, opts.env);
|
|
162
|
+
const repo = gitTop(values.repo ? path.resolve(cwd, values.repo) : (routed ?? cwd));
|
|
163
|
+
if (routed) out(`按子系统「${card.subsystem}」选择仓库:${repo}`);
|
|
164
|
+
const config = loadConfig(configPath, repo).config;
|
|
155
165
|
const parts = splitDescription(card.body);
|
|
156
166
|
const featureDir = `docs/features/${today()}-${cardSlug(id)}`;
|
|
157
167
|
fs.mkdirSync(path.join(repo, featureDir), { recursive: true });
|
|
@@ -330,6 +340,36 @@ async function check(argv: string[], opts: CardCmdOptions): Promise<number> {
|
|
|
330
340
|
return failed > 0 ? 1 : 0;
|
|
331
341
|
}
|
|
332
342
|
|
|
343
|
+
function route(argv: string[], opts: CardCmdOptions): number {
|
|
344
|
+
const out = opts.out ?? console.log;
|
|
345
|
+
const { values } = parseArgs({ args: argv, options: { add: { type: "string" }, remove: { type: "string" }, config: { type: "string" } }, allowPositionals: false });
|
|
346
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
347
|
+
const configPath = values.config ? path.resolve(values.config) : defaultConfigPath(opts.env);
|
|
348
|
+
const systems = fs.existsSync(configPath) ? cardIntegrations(loadConfig(configPath).config).map((i) => i.name) : [];
|
|
349
|
+
if (values.add) {
|
|
350
|
+
const repo = gitTop(path.resolve(cwd, values.add));
|
|
351
|
+
const r = readRoute(repo);
|
|
352
|
+
setRegisteredRepos([...registeredRepos(opts.env), repo], opts.env);
|
|
353
|
+
out(r ? `已登记 ${repo}:${r.system} 空间 ${r.projectId ?? "(未写)"},子系统 ${r.subsystems.join("、") || "(未写)"}` : `已登记 ${repo},但它的项目配置还没有 cardSource(运行 /req-setup 写上)`);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
if (values.remove) {
|
|
357
|
+
const repo = gitTop(path.resolve(cwd, values.remove));
|
|
358
|
+
setRegisteredRepos(registeredRepos(opts.env).filter((r) => r !== repo), opts.env);
|
|
359
|
+
out(`已取消登记 ${repo}`);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
if (systems.length === 0) out("没有配置可以读取卡片的外部系统(本机或团队配置的 integrations 中没有写 query)。");
|
|
363
|
+
const { routes, missing } = cardRoutes(opts.env);
|
|
364
|
+
if (routes.length === 0 && missing.length === 0) {
|
|
365
|
+
out("本机还没有登记接收卡片的仓库:在仓库中运行 /req-setup(或 req card route --add <仓库目录>)。");
|
|
366
|
+
return 0;
|
|
367
|
+
}
|
|
368
|
+
for (const r of routes) out(`${r.system} 空间 ${r.projectId ?? "-"} 子系统 ${r.subsystems.join("、") || "-"} 标题关键字 ${r.titleKeywords.join("、") || "-"} ${r.repo}`);
|
|
369
|
+
for (const m of missing) out(`(没有 cardSource 或目录不存在)${m}`);
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
333
373
|
function list(opts: CardCmdOptions): number {
|
|
334
374
|
const out = opts.out ?? console.log;
|
|
335
375
|
const entries = listCardEntries(opts.env);
|
|
@@ -360,6 +400,8 @@ export async function main(argv: string[], opts: CardCmdOptions = {}): Promise<n
|
|
|
360
400
|
case "list":
|
|
361
401
|
case "ls":
|
|
362
402
|
return list(opts);
|
|
403
|
+
case "route":
|
|
404
|
+
return route(rest, opts);
|
|
363
405
|
default:
|
|
364
406
|
console.log(USAGE);
|
|
365
407
|
return command === undefined || command === "help" || command === "--help" ? 0 : 2;
|
package/engine/src/card.ts
CHANGED
|
@@ -395,3 +395,87 @@ export function planAppendix(repo: string, featureDir: string): string | null {
|
|
|
395
395
|
const shown = preview.length > 4000 ? `${preview.slice(0, 4000)}\n\n……(完整内容见功能目录中的 preview.md)` : preview;
|
|
396
396
|
return `### 待确认的方案\n\n${shown.replace(/^(#{1,4})\s/gm, "$1## ")}\n\n确认没问题就在回复区写“方案确认:同意”;要改就在那一行后写明要改什么。`;
|
|
397
397
|
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Which repository a card belongs to (1.10): each repository says in its project file
|
|
401
|
+
// (cardSource, written by /req-setup) which system, space and subsystems it takes; the
|
|
402
|
+
// machine keeps a list of repositories that take cards (~/.req-kit/card-repos.json).
|
|
403
|
+
|
|
404
|
+
export interface CardRoute {
|
|
405
|
+
repo: string;
|
|
406
|
+
system: string;
|
|
407
|
+
projectId: string | null;
|
|
408
|
+
subsystems: string[];
|
|
409
|
+
titleKeywords: string[];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function cardReposFile(env: NodeJS.ProcessEnv): string {
|
|
413
|
+
return path.join(kitHome(env), "card-repos.json");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function registeredRepos(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
417
|
+
try {
|
|
418
|
+
const list = JSON.parse(fs.readFileSync(cardReposFile(env), "utf8")) as unknown;
|
|
419
|
+
return Array.isArray(list) ? list.filter((r): r is string => typeof r === "string") : [];
|
|
420
|
+
} catch {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function setRegisteredRepos(repos: string[], env: NodeJS.ProcessEnv = process.env): void {
|
|
426
|
+
fs.mkdirSync(path.dirname(cardReposFile(env)), { recursive: true });
|
|
427
|
+
fs.writeFileSync(cardReposFile(env), `${JSON.stringify([...new Set(repos)].sort(), null, 2)}\n`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** The repository's cardSource, read from its project file (not merged with other layers). */
|
|
431
|
+
export function readRoute(repo: string): CardRoute | null {
|
|
432
|
+
try {
|
|
433
|
+
const raw = JSON.parse(fs.readFileSync(path.join(repo, ".requirement-pipeline.json"), "utf8")) as { cardSource?: Record<string, unknown> };
|
|
434
|
+
const c = raw.cardSource;
|
|
435
|
+
if (!c || typeof c.system !== "string") return null;
|
|
436
|
+
const strings = (v: unknown) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string" && x.trim() !== "") : []);
|
|
437
|
+
return { repo, system: c.system, projectId: c.projectId === undefined || c.projectId === null ? null : String(c.projectId), subsystems: strings(c.subsystems), titleKeywords: strings(c.titleKeywords) };
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Routes of the repositories this machine knows: registered ones, and those cards started in. */
|
|
444
|
+
export function cardRoutes(env: NodeJS.ProcessEnv = process.env): { routes: CardRoute[]; missing: string[] } {
|
|
445
|
+
const repos = [...new Set([...registeredRepos(env), ...listCardEntries(env).map((e) => e.sourceRepo)])];
|
|
446
|
+
const routes: CardRoute[] = [];
|
|
447
|
+
const missing: string[] = [];
|
|
448
|
+
for (const repo of repos) {
|
|
449
|
+
const route = fs.existsSync(repo) ? readRoute(repo) : null;
|
|
450
|
+
if (route) routes.push(route);
|
|
451
|
+
else missing.push(repo);
|
|
452
|
+
}
|
|
453
|
+
return { routes, missing };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const same = (a: string, b: string) => a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The repository for a card: the one whose cardSource names the card's system and subsystem;
|
|
460
|
+
* when several share the subsystem, the one whose titleKeywords appear in the card's title.
|
|
461
|
+
* Null when no repository on this machine takes cards (then the caller uses --repo or the
|
|
462
|
+
* current directory). Throws when repositories take cards but none or several match.
|
|
463
|
+
*/
|
|
464
|
+
export function routeCard(card: Pick<CardData, "id" | "title" | "subsystem">, system: string, env: NodeJS.ProcessEnv = process.env): string | null {
|
|
465
|
+
const { routes } = cardRoutes(env);
|
|
466
|
+
const ofSystem = routes.filter((r) => same(r.system, system));
|
|
467
|
+
if (ofSystem.length === 0) return null;
|
|
468
|
+
const bySubsystem = card.subsystem ? ofSystem.filter((r) => r.subsystems.some((s) => same(s, card.subsystem!))) : [];
|
|
469
|
+
if (bySubsystem.length === 0) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`卡片 ${card.id} 的子系统「${card.subsystem ?? "(没有)"}」没有对应的仓库。本机登记的仓库:${ofSystem.map((r) => `${r.repo}(${r.subsystems.join("、") || "未写子系统"})`).join(";")}。请在对应仓库运行 /req-setup 写上这个子系统,或用 --repo 指定。`,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (bySubsystem.length === 1) return bySubsystem[0].repo;
|
|
475
|
+
const title = card.title.toLowerCase();
|
|
476
|
+
const byKeyword = bySubsystem.filter((r) => r.titleKeywords.some((k) => title.includes(k.toLowerCase())));
|
|
477
|
+
if (byKeyword.length === 1) return byKeyword[0].repo;
|
|
478
|
+
throw new Error(
|
|
479
|
+
`卡片 ${card.id}「${card.title}」可能属于几个仓库:${(byKeyword.length ? byKeyword : bySubsystem).map((r) => `${r.repo}(标题关键字:${r.titleKeywords.join("、") || "未写"})`).join(";")}。请在各仓库的 cardSource.titleKeywords 中写上能区分它们的词(例如服务名、"前端""后端"),或用 --repo 指定。`,
|
|
480
|
+
);
|
|
481
|
+
}
|
package/engine/src/config.ts
CHANGED
|
@@ -180,6 +180,18 @@ export interface PipelineConfig {
|
|
|
180
180
|
integrations: IntegrationConfig[];
|
|
181
181
|
/** Per integration name, values for its {field.*} placeholders; a project's file may set these (e.g. its project id). */
|
|
182
182
|
integrationDefaults: Record<string, Record<string, string>>;
|
|
183
|
+
/**
|
|
184
|
+
* Pushing the branch of a requirement that started from a card, once development is done and
|
|
185
|
+
* the handoff is written (1.10). Off by default; machine or team config only. Never forced,
|
|
186
|
+
* only to branches under `branchPrefix`, never merged.
|
|
187
|
+
*/
|
|
188
|
+
push: { enabled: boolean; remote: string; branchPrefix: string };
|
|
189
|
+
/**
|
|
190
|
+
* Which cards this repository takes (1.10, written by /req-setup in the project's file): the
|
|
191
|
+
* system, its space (project id) and subsystems; `titleKeywords` tells apart repositories
|
|
192
|
+
* that share a subsystem (e.g. "message-service", "后端").
|
|
193
|
+
*/
|
|
194
|
+
cardSource: { system: string; projectId: string | null; subsystems: string[]; titleKeywords: string[] } | null;
|
|
183
195
|
/** Automatic retries of a step after a transient error (rate limit, dropped connection). */
|
|
184
196
|
retry: { attempts: number; backoffSeconds: number[] };
|
|
185
197
|
/**
|
|
@@ -284,6 +296,8 @@ export const DEFAULT_CONFIG: PipelineConfig = {
|
|
|
284
296
|
notifiers: [],
|
|
285
297
|
integrations: [],
|
|
286
298
|
integrationDefaults: {},
|
|
299
|
+
push: { enabled: false, remote: "origin", branchPrefix: "req/" },
|
|
300
|
+
cardSource: null,
|
|
287
301
|
retry: { attempts: 2, backoffSeconds: [60, 300] },
|
|
288
302
|
paseoServices: "auto",
|
|
289
303
|
serviceReadySeconds: 120,
|
|
@@ -347,6 +361,16 @@ function merge(base: PipelineConfig, raw: Record<string, unknown>): PipelineConf
|
|
|
347
361
|
}
|
|
348
362
|
merged.integrationDefaults = combined;
|
|
349
363
|
}
|
|
364
|
+
merged.push = { ...base.push, ...(isRecord(raw.push) ? raw.push : {}) } as PipelineConfig["push"];
|
|
365
|
+
if (isRecord(raw.cardSource)) {
|
|
366
|
+
const c = raw.cardSource;
|
|
367
|
+
merged.cardSource = {
|
|
368
|
+
system: c.system as string,
|
|
369
|
+
projectId: c.projectId === undefined || c.projectId === null ? null : String(c.projectId),
|
|
370
|
+
subsystems: (c.subsystems ?? []) as string[],
|
|
371
|
+
titleKeywords: (c.titleKeywords ?? []) as string[],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
350
374
|
merged.stepTimeoutMinutes = {
|
|
351
375
|
...base.stepTimeoutMinutes,
|
|
352
376
|
...(isRecord(raw.stepTimeoutMinutes) ? raw.stepTimeoutMinutes : {}),
|
|
@@ -535,6 +559,17 @@ function validate(config: PipelineConfig): void {
|
|
|
535
559
|
if (!isRecord(config.integrationDefaults) || !Object.values(config.integrationDefaults).every((m) => isRecord(m) && Object.values(m).every((v) => typeof v === "string"))) {
|
|
536
560
|
throw new ConfigError('integrationDefaults 必须是 { "<集成名称>": { "<字段>": "<值>" } }');
|
|
537
561
|
}
|
|
562
|
+
const push = config.push;
|
|
563
|
+
if (typeof push.enabled !== "boolean") throw new ConfigError("push.enabled 必须是 true 或 false");
|
|
564
|
+
if (typeof push.remote !== "string" || !/^[\w.-]+$/.test(push.remote)) throw new ConfigError('push.remote 必须是远程仓库名,例如 "origin"');
|
|
565
|
+
if (typeof push.branchPrefix !== "string" || !/^[A-Za-z0-9._-]+\/$/.test(push.branchPrefix)) throw new ConfigError('push.branchPrefix 必须是以 / 结尾的分支前缀,例如 "req/"');
|
|
566
|
+
const cs = config.cardSource;
|
|
567
|
+
if (cs !== null) {
|
|
568
|
+
if (typeof cs.system !== "string" || !cs.system.trim()) throw new ConfigError('cardSource.system 必须是对接系统的名称,例如 "ALM"');
|
|
569
|
+
for (const k of ["subsystems", "titleKeywords"] as const) {
|
|
570
|
+
if (!Array.isArray(cs[k]) || !cs[k].every((v) => typeof v === "string" && v.trim())) throw new ConfigError(`cardSource.${k} 必须是字符串数组`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
538
573
|
if (config.requiredVersion !== null && typeof config.requiredVersion !== "string") {
|
|
539
574
|
throw new ConfigError('requiredVersion 必须是版本范围字符串,例如 ">=1.2.0 <2"');
|
|
540
575
|
}
|
|
@@ -570,7 +605,7 @@ export interface LoadedConfig {
|
|
|
570
605
|
}
|
|
571
606
|
|
|
572
607
|
/** Keys a project file may not set: they run code or reach outside the repository. */
|
|
573
|
-
const NOT_IN_PROJECT_KEYS = ["notifiers", "integrations"] as const;
|
|
608
|
+
const NOT_IN_PROJECT_KEYS = ["notifiers", "integrations", "push"] as const;
|
|
574
609
|
|
|
575
610
|
/** Notifier module paths are relative to the file that names them; "~/" is the home directory. */
|
|
576
611
|
function resolveNotifiers(raw: Record<string, unknown>, file: string): Record<string, unknown> {
|
package/engine/src/pipeline.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { kitVersion } from "./locations.js";
|
|
|
34
34
|
import { type AgentUsage, describeTotals, totals } from "./usage.js";
|
|
35
35
|
import { dispatchNotifiers, type NotifierEventType } from "./notifiers.js";
|
|
36
36
|
import { describeExternal, type IntegrationEvent, type Runner, syncIntegration } from "./integrations.js";
|
|
37
|
-
import { planAppendix, publishCard, readSource } from "./card.js";
|
|
37
|
+
import { cardSlug, planAppendix, publishCard, readSource } from "./card.js";
|
|
38
38
|
import { principlesSection, projectAdditions, type PromptLocations, readToolPrompt } from "./prompts.js";
|
|
39
39
|
import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent } from "./runtime.js";
|
|
40
40
|
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type RunPhases, type StepRecord, writeState } from "./state.js";
|
|
@@ -414,6 +414,40 @@ const CARD_PROGRESS: Record<IntegrationEvent, string> = {
|
|
|
414
414
|
failed: "流水线中断,正在处理",
|
|
415
415
|
};
|
|
416
416
|
|
|
417
|
+
/**
|
|
418
|
+
* A requirement that started from a card (1.10), with `push` on: once development is done and the
|
|
419
|
+
* handoff is written, pushes each repository's branch to <remote>/<prefix><card>. Never forced
|
|
420
|
+
* (a rejected push is reported, not retried with force), never merged. Never throws.
|
|
421
|
+
*/
|
|
422
|
+
function pushBranch(ctx: PipelineContext): void {
|
|
423
|
+
const push = ctx.config.push;
|
|
424
|
+
if (!push.enabled) return;
|
|
425
|
+
const source = readSource(ctx.repo, ctx.featureDir);
|
|
426
|
+
if (!source) return;
|
|
427
|
+
const branch = `${push.branchPrefix}${cardSlug(source.id)}`;
|
|
428
|
+
const repos = [ctx.repo, ...(ctx.state.joint ?? []).map((j) => j.repo)];
|
|
429
|
+
const at = new Date().toISOString();
|
|
430
|
+
for (const repo of repos) {
|
|
431
|
+
let error: string | null = null;
|
|
432
|
+
try {
|
|
433
|
+
execFileSync("git", ["push", push.remote, `HEAD:refs/heads/${branch}`], { cwd: repo, stdio: ["ignore", "pipe", "pipe"], timeout: 180_000, windowsHide: true });
|
|
434
|
+
} catch (e) {
|
|
435
|
+
const stderr = (e as { stderr?: Buffer | string }).stderr?.toString().trim();
|
|
436
|
+
error = (stderr || (e as Error).message).split("\n").filter(Boolean).slice(-2).join(" ");
|
|
437
|
+
}
|
|
438
|
+
ctx.state.pushed = [...(ctx.state.pushed ?? []).filter((p) => p.repo !== repo), { repo, remote: push.remote, branch, at, error }];
|
|
439
|
+
ctx.log(error ? `推送 ${repo} 的分支到 ${push.remote}/${branch} 失败:${error}` : `已推送 ${repo} 的分支到 ${push.remote}/${branch}`);
|
|
440
|
+
}
|
|
441
|
+
save(ctx);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Where the requirement's code is, for the card: the pushed branch, or the local one. */
|
|
445
|
+
function branchText(ctx: PipelineContext): string | null {
|
|
446
|
+
const mine = ctx.state.pushed?.find((p) => p.repo === ctx.repo);
|
|
447
|
+
if (mine) return mine.error ? `分支 ${ctx.state.branch}(推送到 ${mine.remote}/${mine.branch} 失败:${mine.error})` : `代码已推送到 ${mine.remote} 的分支 ${mine.branch}`;
|
|
448
|
+
return ctx.state.branch ? `分支 ${ctx.state.branch}` : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
417
451
|
/** A requirement that started from a card (1.10): its AI section follows the pipeline. Never throws. */
|
|
418
452
|
async function updateCard(ctx: PipelineContext, event: IntegrationEvent): Promise<void> {
|
|
419
453
|
const source = readSource(ctx.repo, ctx.featureDir);
|
|
@@ -422,7 +456,7 @@ async function updateCard(ctx: PipelineContext, event: IntegrationEvent): Promis
|
|
|
422
456
|
if (!config?.describe || !config.query) return;
|
|
423
457
|
const approval = event === "approval";
|
|
424
458
|
let progress = CARD_PROGRESS[event];
|
|
425
|
-
if (event === "phase_done" && ctx
|
|
459
|
+
if (event === "phase_done" && branchText(ctx)) progress += `,${branchText(ctx)}`;
|
|
426
460
|
if (event === "completed" && ctx.state.outcome && ctx.state.outcome !== "completed") progress = `已结束(${ctx.state.outcome === "completed_with_issues" ? "有遗留问题,见完成报告" : "没有完成,见完成报告"})`;
|
|
427
461
|
try {
|
|
428
462
|
await publishCard({
|
|
@@ -1795,6 +1829,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1795
1829
|
? `研发已完成(只做研发):${outcomeText[report.outcome as string] ?? report.outcome},请按提测说明人工测试。`
|
|
1796
1830
|
: `开发流水线已结束:${outcomeText[report.outcome as string] ?? report.outcome}。`;
|
|
1797
1831
|
// Develop-only ends where testing by people begins: for the card that is "研发完成,待测试".
|
|
1832
|
+
if (developOnly && report.outcome !== "not_completed") pushBranch(ctx);
|
|
1798
1833
|
await integrate(ctx, developOnly && report.outcome !== "not_completed" ? "phase_done" : "completed");
|
|
1799
1834
|
await notifyExternal(ctx, "completed", text, {
|
|
1800
1835
|
outcome: report.outcome,
|
|
@@ -1823,6 +1858,7 @@ export async function runPipeline(ctx: PipelineContext): Promise<PipelineOutcome
|
|
|
1823
1858
|
handbacks: ctx.state.handbacks ?? 0,
|
|
1824
1859
|
}),
|
|
1825
1860
|
);
|
|
1861
|
+
pushBranch(ctx);
|
|
1826
1862
|
await integrate(ctx, "phase_done");
|
|
1827
1863
|
await notifyExternal(ctx, "phase_done", `研发已完成,可以提测:提测说明在 ${ctx.featureDir}/handoff.md。${verifyAt ? `已预约在 ${verifyAt} 开始测试验证。` : ""}`, {
|
|
1828
1864
|
handoffPath: `${ctx.featureDir}/handoff.md`,
|
package/engine/src/records.ts
CHANGED
|
@@ -149,6 +149,10 @@ export interface RunRecord {
|
|
|
149
149
|
title: string;
|
|
150
150
|
type: string | null;
|
|
151
151
|
profile: string;
|
|
152
|
+
/** Who asked for it: the card's author for a requirement that started from a card (1.10). */
|
|
153
|
+
requester?: string | null;
|
|
154
|
+
/** The card it started from, e.g. { system: "ALM", id: "…" } (1.10). */
|
|
155
|
+
source?: { system: string; id: string } | null;
|
|
152
156
|
};
|
|
153
157
|
run: {
|
|
154
158
|
startedAt: string;
|
|
@@ -226,6 +230,7 @@ export function runRecord(input: {
|
|
|
226
230
|
title: input.title,
|
|
227
231
|
type: state.type ?? null,
|
|
228
232
|
profile: state.profile,
|
|
233
|
+
...cardOrigin(state.repo, state.featureDir),
|
|
229
234
|
},
|
|
230
235
|
run: {
|
|
231
236
|
startedAt: input.runStartedAt,
|
|
@@ -243,6 +248,17 @@ export function runRecord(input: {
|
|
|
243
248
|
};
|
|
244
249
|
}
|
|
245
250
|
|
|
251
|
+
/** The card a requirement started from (source.json), for the record. */
|
|
252
|
+
function cardOrigin(repo: string, featureDir: string): { requester: string | null; source: { system: string; id: string } } | Record<string, never> {
|
|
253
|
+
try {
|
|
254
|
+
const raw = JSON.parse(fs.readFileSync(path.join(repo, featureDir, "source.json"), "utf8")) as { system?: unknown; id?: unknown; author?: unknown };
|
|
255
|
+
if (typeof raw.system !== "string" || typeof raw.id !== "string") return {};
|
|
256
|
+
return { requester: typeof raw.author === "string" ? raw.author : null, source: { system: raw.system, id: raw.id } };
|
|
257
|
+
} catch {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
246
262
|
/** Appends a run record. Never throws. */
|
|
247
263
|
export function appendRun(record: RunRecord, env: NodeJS.ProcessEnv = process.env): void {
|
|
248
264
|
if (recordingOff(env)) return;
|
package/engine/src/state.ts
CHANGED
|
@@ -109,6 +109,11 @@ export interface PipelineState {
|
|
|
109
109
|
gate?: { baseline: GateBaseline | null; runs: GateRun[] } | null;
|
|
110
110
|
/** External systems (1.9): the requirement's card in each, by integration name. */
|
|
111
111
|
external?: Record<string, ExternalRecord> | null;
|
|
112
|
+
/**
|
|
113
|
+
* Branches pushed for a requirement that started from a card (1.10, `push` config): one entry
|
|
114
|
+
* per repository (the main one, then joint ones), the latest attempt. `error` is null when it worked.
|
|
115
|
+
*/
|
|
116
|
+
pushed?: Array<{ repo: string; remote: string; branch: string; at: string; error: string | null }> | null;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
export interface JointRepo {
|
package/engine/src/stats.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface RequirementStats {
|
|
|
87
87
|
waits: Record<WaitKind, number>;
|
|
88
88
|
builder: string | null;
|
|
89
89
|
reviewer: string | null;
|
|
90
|
+
/** The card's author, for requirements that started from a card (1.10). */
|
|
91
|
+
requester: string | null;
|
|
92
|
+
/** "<system>:<id>" of that card. */
|
|
93
|
+
card: string | null;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
function add(a: number | null, b: number | null): number | null {
|
|
@@ -162,6 +166,8 @@ export function byRequirement(runs: RunRecord[]): RequirementStats[] {
|
|
|
162
166
|
waits,
|
|
163
167
|
builder: last.agents.builder,
|
|
164
168
|
reviewer: last.agents.reviewer,
|
|
169
|
+
requester: last.requirement.requester ?? null,
|
|
170
|
+
card: last.requirement.source ? `${last.requirement.source.system}:${last.requirement.source.id}` : null,
|
|
165
171
|
});
|
|
166
172
|
}
|
|
167
173
|
return out.sort((a, b) => b.lastEndedAt.localeCompare(a.lastEndedAt));
|
|
@@ -202,6 +208,8 @@ export interface StatsSummary {
|
|
|
202
208
|
waits: Array<{ kind: WaitKind; label: string; requirements: number; averageMinutes: number }>;
|
|
203
209
|
usage: { agentMinutes: number; inputTokens: number | null; outputTokens: number | null; costUsd: number | null };
|
|
204
210
|
byBuilder: Array<{ builder: string; requirements: number; averageReviewRounds: number | null; averageCostUsd: number | null }>;
|
|
211
|
+
/** Requirements that started from cards, by who asked for them (1.10); empty when there are none. */
|
|
212
|
+
byRequester: Array<{ requester: string; requirements: number; completed: number; medianCycleHours: number | null }>;
|
|
205
213
|
requirements: RequirementStats[];
|
|
206
214
|
}
|
|
207
215
|
|
|
@@ -246,6 +254,9 @@ export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
|
246
254
|
const builders = new Map<string, RequirementStats[]>();
|
|
247
255
|
for (const q of done) builders.set(q.builder ?? "(未记录)", [...(builders.get(q.builder ?? "(未记录)") ?? []), q]);
|
|
248
256
|
|
|
257
|
+
const requesters = new Map<string, RequirementStats[]>();
|
|
258
|
+
for (const q of active) if (q.card) requesters.set(q.requester ?? "(未记录)", [...(requesters.get(q.requester ?? "(未记录)") ?? []), q]);
|
|
259
|
+
|
|
249
260
|
return {
|
|
250
261
|
schema: 1,
|
|
251
262
|
period: { from: period.from?.toISOString() ?? null, to: period.to?.toISOString() ?? null, label: period.label },
|
|
@@ -276,6 +287,12 @@ export function summarizeRuns(all: RunRecord[], period: Period): StatsSummary {
|
|
|
276
287
|
averageReviewRounds: avg(qs.map((q) => q.reviewRounds)),
|
|
277
288
|
averageCostUsd: avg(qs.flatMap((q) => (q.costUsd === null ? [] : [q.costUsd]))),
|
|
278
289
|
})),
|
|
290
|
+
byRequester: [...requesters.entries()]
|
|
291
|
+
.map(([requester, qs]) => {
|
|
292
|
+
const finished = qs.filter((q) => done.includes(q));
|
|
293
|
+
return { requester, requirements: qs.length, completed: finished.length, medianCycleHours: median(finished.flatMap((q) => (q.cycleHours === null ? [] : [q.cycleHours]))) };
|
|
294
|
+
})
|
|
295
|
+
.sort((a, b) => b.requirements - a.requirements || a.requester.localeCompare(b.requester)),
|
|
279
296
|
requirements: active,
|
|
280
297
|
};
|
|
281
298
|
}
|
|
@@ -342,6 +359,10 @@ export function describeStats(s: StatsSummary): string {
|
|
|
342
359
|
lines.push("", "## 按 builder 比较(已完成的需求)", "", "| builder | 需求数 | 平均评审轮数 | 平均费用 |", "| --- | --- | --- | --- |");
|
|
343
360
|
for (const b of s.byBuilder) lines.push(`| ${b.builder} | ${b.requirements} | ${b.averageReviewRounds ?? "-"} | ${b.averageCostUsd === null ? "-" : `$${b.averageCostUsd.toFixed(2)}`} |`);
|
|
344
361
|
}
|
|
362
|
+
if (s.byRequester.length > 0) {
|
|
363
|
+
lines.push("", "## 从卡片开始的需求(按需求方)", "", "| 需求方 | 需求数 | 已完成 | 周期中位数 |", "| --- | --- | --- | --- |");
|
|
364
|
+
for (const r of s.byRequester) lines.push(`| ${r.requester} | ${r.requirements} | ${r.completed} | ${r.medianCycleHours === null ? "-" : `${r.medianCycleHours} 小时`} |`);
|
|
365
|
+
}
|
|
345
366
|
lines.push("", "## 需求", "", "| 项目 | 需求 | 类型 | 状态 | 周期 | agent 用时 | 评审轮数 | 缺陷轮数 |", "| --- | --- | --- | --- | --- | --- | --- | --- |");
|
|
346
367
|
for (const q of s.requirements) {
|
|
347
368
|
const status = q.status === "completed" ? (OUTCOME_LABELS[q.outcome ?? "unknown"] ?? "已完成") : (STATUS_LABELS[q.status] ?? q.status);
|
|
@@ -378,6 +399,8 @@ const CSV_COLUMNS: Array<[string, (q: RequirementStats) => unknown]> = [
|
|
|
378
399
|
["wait_verify_minutes", (q) => q.waits.verify],
|
|
379
400
|
["builder", (q) => q.builder ?? ""],
|
|
380
401
|
["reviewer", (q) => q.reviewer ?? ""],
|
|
402
|
+
["requester", (q) => q.requester ?? ""],
|
|
403
|
+
["card", (q) => q.card ?? ""],
|
|
381
404
|
];
|
|
382
405
|
|
|
383
406
|
function csvCell(v: unknown): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-req-kit",
|
|
3
|
-
"version": "1.10.0-beta.
|
|
3
|
+
"version": "1.10.0-beta.3",
|
|
4
4
|
"description": "req-kit: a requirement-development workflow for Paseo. /req aligns a requirement in chat; the engine runs design, implementation, review and verification as Paseo agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,6 +60,7 @@ metadata:
|
|
|
60
60
|
- 有没有相关的其他仓库(例如这是前端,接口在另一个后端仓库):在这台机器上的位置、它提供什么。登记后流水线的步骤可以只读参考它,不会改动它
|
|
61
61
|
- 多模块的仓库(例如 `web/` 和 `server/` 各有自己的测试):确认各模块的检查命令,第 5 步按模块写进 `checks`
|
|
62
62
|
- 项目已经有 `paseo.json` 时,要不要由你补充;没有时,要不要生成(推荐生成,见第 5 步)
|
|
63
|
+
- **从卡片接需求**(团队对接了需求管理系统时):先执行 `node "{{REQ_KIT_ROOT}}/bin/req.mjs" card route`。输出"没有配置可以读取卡片的外部系统"或团队不用卡片开始需求时跳过;否则问:这个仓库属于需求管理系统中的哪个空间(项目编号,如 ALM 的 projectId)、接哪些子系统的卡片(卡片上显示的子系统名称,可以多个);同一个子系统还有别的仓库时(例如前端和后端各一个),卡片标题中用什么词区分(例如服务名"message-service"、"后端")。第 5 步写进 `cardSource`
|
|
63
64
|
|
|
64
65
|
## 4. 实际运行验证命令
|
|
65
66
|
|
|
@@ -106,6 +107,7 @@ metadata:
|
|
|
106
107
|
}
|
|
107
108
|
```
|
|
108
109
|
- `relatedRepos`(有关联仓库时):`[{ "name": "后端", "path": "../backend", "role": "提供登录和订单接口" }]`。`path` 写相对项目目录的路径(团队约定把相关仓库放在同一个上级目录最方便),也可以写 `~/` 开头的路径。平时流水线的步骤只读不改;需求说明写了"联合开发"时,流水线会为这个需求在它那里新建工作区,一起开发。关联仓库最好也运行过 `/req-setup`,它自己的检查命令、验证方法和项目说明会用在它的步骤里。
|
|
110
|
+
- `cardSource`(第 3 步确认了从卡片接需求时):`{ "system": "ALM", "projectId": "186149", "subsystems": ["CSP-CMASTER_云匠"], "titleKeywords": ["message-service"] }`。`system` 是团队对接配置中的系统名称;`titleKeywords` 只在同一子系统有几个仓库时写。之后 `req start --from` 不写仓库时,按卡片的子系统和标题选到这个仓库
|
|
109
111
|
- 其他设置只在需要和本机配置不同时才写,例如 `"defaultProfile": "quick"`。不要写 `daemonUrl`、`daemonPasswordEnv`、`teamConfig`,写了也会被忽略。
|
|
110
112
|
- `.gitignore`:确认包含 `docs/features/*/.pipeline/`(流水线的运行状态),没有就建议加上。
|
|
111
113
|
|
|
@@ -115,4 +117,6 @@ metadata:
|
|
|
115
117
|
|
|
116
118
|
用户确认后,只提交这几个文件(以及 `GLOSSARY.md`、`scripts/paseo/` 和 `.gitignore` 的改动)。`paseo.json` 必须提交:Paseo 新建 worktree 时读取的是基准分支上已提交的版本,提交信息例如 `chore(req-kit): 项目接入说明与验证方法`。不 push。
|
|
117
119
|
|
|
120
|
+
写了 `cardSource` 的,提交后执行 `node "{{REQ_KIT_ROOT}}/bin/req.mjs" card route --add .`,在这台机器上登记这个仓库接收卡片(每台接单的机器各登记一次)。
|
|
121
|
+
|
|
118
122
|
最后告诉用户:之后项目的构建、测试或启动方式有变化时,再运行一次 `/req-setup`;也可以直接编辑这些文件。可以运行 `/req-doctor` 确认项目检查全部正常。
|