ai-project-manage-cli 3.0.5 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync9 } from "fs";
5
- import { dirname as dirname2, join as join7 } from "path";
4
+ import { readFileSync as readFileSync10 } from "fs";
5
+ import { dirname as dirname2, join as join8 } from "path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { Command } from "commander";
8
8
 
@@ -115,6 +115,24 @@ var requestConfig = {
115
115
  method: "POST",
116
116
  path: "/cli/requirements/update-dev-status"
117
117
  })
118
+ },
119
+ requirementArtifact: {
120
+ list: defineEndpoint({
121
+ method: "GET",
122
+ path: "/requirement-artifacts/list"
123
+ }),
124
+ create: defineEndpoint({
125
+ method: "POST",
126
+ path: "/requirement-artifacts/create"
127
+ }),
128
+ update: defineEndpoint({
129
+ method: "POST",
130
+ path: "/requirement-artifacts/update"
131
+ }),
132
+ delete: defineEndpoint({
133
+ method: "POST",
134
+ path: "/requirement-artifacts/delete"
135
+ })
118
136
  }
119
137
  };
120
138
 
@@ -136,6 +154,9 @@ import { fileURLToPath } from "url";
136
154
  var __dirname = dirname(fileURLToPath(import.meta.url));
137
155
  var CLI_TEMPLATE_DIR = resolve(__dirname, "../template");
138
156
  var WORKSPACE_APM_DIR = resolve(process.cwd(), ".apm");
157
+ function requirementWorkitemsDir(requirementId) {
158
+ return join2(WORKSPACE_APM_DIR, "workitems", requirementId);
159
+ }
139
160
  async function ensureLoggedConfig() {
140
161
  const cfg = await ensureApmConfig();
141
162
  if (!cfg.token) {
@@ -499,7 +520,7 @@ async function runPull(requirementId) {
499
520
  const cfg = await ensureLoggedConfig();
500
521
  const api = createApmApiClient(cfg);
501
522
  const data = await api.cliRequirements.pull({ requirementId });
502
- const WORKITEMS_DIR = join4(WORKSPACE_APM_DIR, "workitems", requirementId);
523
+ const WORKITEMS_DIR = requirementWorkitemsDir(requirementId);
503
524
  await ensureDirExists(WORKITEMS_DIR);
504
525
  const req2 = data.requirement;
505
526
  const statusYaml = yamlStringify(
@@ -584,23 +605,118 @@ async function runUpdateStatus(requirementId, status) {
584
605
  console.log(JSON.stringify(data, null, 2));
585
606
  }
586
607
 
608
+ // src/commands/upload-artifact.ts
609
+ import { existsSync as existsSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
610
+ import { join as join6, relative, sep } from "path";
611
+ var EXCLUDED_RELATIVE_PATHS = /* @__PURE__ */ new Set([
612
+ "defect.xml",
613
+ "prd.md",
614
+ "requirement-status.yaml",
615
+ "reviews.xml",
616
+ "testcase.xml"
617
+ ]);
618
+ function toPosixRelative(root, absoluteFile) {
619
+ return relative(root, absoluteFile).split(sep).join("/");
620
+ }
621
+ function artifactTagFromRelPath(relPosix) {
622
+ const parts = relPosix.split("/");
623
+ const fileName = parts[parts.length - 1] ?? relPosix;
624
+ if (parts.length > 1) {
625
+ return parts[0] ?? fileName;
626
+ }
627
+ const dot = fileName.lastIndexOf(".");
628
+ return dot > 0 ? fileName.slice(0, dot) : fileName;
629
+ }
630
+ function* walkMarkdownFiles(dir) {
631
+ const names = readdirSync2(dir);
632
+ for (const name of names) {
633
+ if (name.startsWith(".")) continue;
634
+ const full = join6(dir, name);
635
+ const st = statSync2(full);
636
+ if (st.isDirectory()) {
637
+ yield* walkMarkdownFiles(full);
638
+ continue;
639
+ }
640
+ if (!st.isFile()) continue;
641
+ if (!name.toLowerCase().endsWith(".md")) continue;
642
+ yield full;
643
+ }
644
+ }
645
+ async function deleteAllArtifactsForRequirement(api, requirementId) {
646
+ const pageSize = 100;
647
+ let page = 1;
648
+ const rows = [];
649
+ while (true) {
650
+ const batch = await api.requirementArtifact.list({
651
+ requirementId,
652
+ page,
653
+ pageSize
654
+ });
655
+ rows.push(...batch.items);
656
+ if (batch.total === 0 || rows.length >= batch.total) break;
657
+ page += 1;
658
+ }
659
+ for (const row of rows) {
660
+ await api.requirementArtifact.delete({ artifactId: row.id });
661
+ }
662
+ return rows.length;
663
+ }
664
+ async function runUploadArtifact(requirementId) {
665
+ const cfg = await ensureLoggedConfig();
666
+ const api = createApmApiClient(cfg);
667
+ const root = requirementWorkitemsDir(requirementId);
668
+ if (!existsSync2(root)) {
669
+ console.error(
670
+ `[apm] \u76EE\u5F55\u4E0D\u5B58\u5728: ${root}
671
+ \u8BF7\u5148\u6267\u884C: apm pull ${requirementId}`
672
+ );
673
+ process.exit(1);
674
+ }
675
+ const deleted = await deleteAllArtifactsForRequirement(api, requirementId);
676
+ console.log(`[apm] \u5DF2\u6E05\u7A7A\u9700\u6C42\u4EA7\u7269\u6587\u6863 ${deleted} \u6761`);
677
+ const paths = [...walkMarkdownFiles(root)];
678
+ let created = 0;
679
+ let skipped = 0;
680
+ for (const abs of paths) {
681
+ const relPosix = toPosixRelative(root, abs);
682
+ if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
683
+ skipped += 1;
684
+ console.log(`[apm] \u8DF3\u8FC7\uFF08\u6392\u9664\u5217\u8868\uFF09: ${relPosix}`);
685
+ continue;
686
+ }
687
+ const content = readFileSync5(abs, "utf8");
688
+ const tag = artifactTagFromRelPath(relPosix);
689
+ await api.requirementArtifact.create({
690
+ requirementId,
691
+ tag,
692
+ fileName: relPosix,
693
+ content
694
+ });
695
+ created += 1;
696
+ console.log(`[apm] \u5DF2\u4E0A\u4F20\u4EA7\u7269: ${relPosix} (tag=${tag})`);
697
+ }
698
+ console.log(
699
+ `[apm] \u5B8C\u6210\uFF1A\u5220\u9664 ${deleted}\uFF0C\u65B0\u5EFA ${created}\uFF0C\u8DF3\u8FC7\uFF08\u6392\u9664\uFF09 ${skipped}\uFF0C\u5171\u626B\u63CF ${paths.length} \u4E2A Markdown \u6587\u4EF6`
700
+ );
701
+ }
702
+
587
703
  // src/commands/deploy/backend.ts
588
704
  import path5 from "node:path";
589
705
 
590
706
  // src/commands/deploy/lib/apm-config.ts
591
- import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
707
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
592
708
  import { resolve as resolve3 } from "node:path";
593
709
  function loadApmConfig(options) {
594
710
  const p = resolve3(
595
711
  process.cwd(),
596
712
  options?.configPath ?? resolve3(WORKSPACE_APM_DIR, "apm.config.json")
597
713
  );
598
- if (!existsSync2(p)) {
714
+ if (!existsSync3(p)) {
599
715
  console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
600
716
  process.exit(1);
601
717
  }
602
718
  try {
603
- const raw = readFileSync5(p, "utf8");
719
+ const raw = readFileSync6(p, "utf8");
604
720
  return JSON.parse(raw);
605
721
  } catch (e) {
606
722
  console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
@@ -691,7 +807,7 @@ import path4 from "node:path";
691
807
  import Docker from "dockerode";
692
808
 
693
809
  // src/commands/deploy/lib/backend-deploy/dockerode-client/connection-options.ts
694
- import { existsSync as existsSync3, readFileSync as readFileSync6 } from "node:fs";
810
+ import { existsSync as existsSync4, readFileSync as readFileSync7 } from "node:fs";
695
811
  import path from "node:path";
696
812
  function asOptionalTlsBuffer(value) {
697
813
  if (typeof value !== "string") {
@@ -703,8 +819,8 @@ function asOptionalTlsBuffer(value) {
703
819
  if (normalized === "") {
704
820
  return void 0;
705
821
  }
706
- if (existsSync3(normalized)) {
707
- return readFileSync6(normalized);
822
+ if (existsSync4(normalized)) {
823
+ return readFileSync7(normalized);
708
824
  }
709
825
  const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
710
826
  if (looksLikePath) {
@@ -914,17 +1030,17 @@ var DockerodeClient = class {
914
1030
  var createDockerodeClient = (config) => new DockerodeClient(config);
915
1031
 
916
1032
  // src/commands/deploy/lib/backend-deploy/dockerode-client/env.ts
917
- import { existsSync as existsSync4, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
1033
+ import { existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
918
1034
  import path2 from "node:path";
919
1035
  function loadEnvFromFile(envFilePath) {
920
1036
  if (!envFilePath) {
921
1037
  return {};
922
1038
  }
923
1039
  const targetPath = path2.resolve(envFilePath);
924
- if (!existsSync4(targetPath) || !statSync2(targetPath).isFile()) {
1040
+ if (!existsSync5(targetPath) || !statSync3(targetPath).isFile()) {
925
1041
  return {};
926
1042
  }
927
- const raw = readFileSync7(targetPath, "utf-8");
1043
+ const raw = readFileSync8(targetPath, "utf-8");
928
1044
  const result = {};
929
1045
  for (const line of raw.split(/\r?\n/)) {
930
1046
  const normalized = line.trim();
@@ -1095,12 +1211,12 @@ function dockerPushImage(params, cwd) {
1095
1211
  }
1096
1212
 
1097
1213
  // src/commands/deploy/lib/backend-deploy/resolve-dockerfile.ts
1098
- import { existsSync as existsSync5 } from "node:fs";
1214
+ import { existsSync as existsSync6 } from "node:fs";
1099
1215
  import path3 from "node:path";
1100
1216
  function resolveDockerBuildPaths(cwd) {
1101
1217
  const dockerfilePath = path3.join(cwd, "Dockerfile");
1102
1218
  Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
1103
- if (!existsSync5(dockerfilePath)) {
1219
+ if (!existsSync6(dockerfilePath)) {
1104
1220
  throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
1105
1221
  }
1106
1222
  Logger.info("\u2713 Dockerfile \u5B58\u5728");
@@ -1229,16 +1345,16 @@ import { copyFile, readdir as readdir2, stat } from "node:fs/promises";
1229
1345
  import path7 from "node:path";
1230
1346
 
1231
1347
  // src/commands/deploy/lib/load-apm-dotenv.ts
1232
- import { existsSync as existsSync6, readFileSync as readFileSync8 } from "node:fs";
1233
- import { join as join6 } from "node:path";
1348
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "node:fs";
1349
+ import { join as join7 } from "node:path";
1234
1350
  function loadApmDotEnvIfPresent() {
1235
- const p = join6(WORKSPACE_APM_DIR, ".env");
1236
- if (!existsSync6(p)) {
1351
+ const p = join7(WORKSPACE_APM_DIR, ".env");
1352
+ if (!existsSync7(p)) {
1237
1353
  return;
1238
1354
  }
1239
1355
  let text;
1240
1356
  try {
1241
- text = readFileSync8(p, "utf8");
1357
+ text = readFileSync9(p, "utf8");
1242
1358
  } catch {
1243
1359
  return;
1244
1360
  }
@@ -1263,14 +1379,14 @@ function loadApmDotEnvIfPresent() {
1263
1379
  }
1264
1380
 
1265
1381
  // src/commands/deploy/lib/minio.ts
1266
- import { statSync as statSync3 } from "node:fs";
1382
+ import { statSync as statSync4 } from "node:fs";
1267
1383
  import { readdir } from "node:fs/promises";
1268
1384
  import path6 from "node:path";
1269
1385
  import * as Minio from "minio";
1270
1386
  var DEFAULT_MAX_FILE_SIZE_MB = 50;
1271
1387
  async function isDirectoryPath(dir) {
1272
1388
  try {
1273
- const st = statSync3(dir);
1389
+ const st = statSync4(dir);
1274
1390
  return st.isDirectory();
1275
1391
  } catch {
1276
1392
  return false;
@@ -1300,7 +1416,7 @@ async function collectFiles(root) {
1300
1416
  if (e.isDirectory()) {
1301
1417
  await walk(abs, rel);
1302
1418
  } else if (e.isFile()) {
1303
- const st = statSync3(abs);
1419
+ const st = statSync4(abs);
1304
1420
  out.push({
1305
1421
  absPath: abs,
1306
1422
  relativePath: rel.replace(/\\/g, "/"),
@@ -1597,8 +1713,8 @@ function registerDeployCommands(program) {
1597
1713
  function readCliVersion() {
1598
1714
  try {
1599
1715
  const dir = dirname2(fileURLToPath2(import.meta.url));
1600
- const pkgPath = join7(dir, "..", "package.json");
1601
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
1716
+ const pkgPath = join8(dir, "..", "package.json");
1717
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
1602
1718
  return pkg.version ?? "0.0.0";
1603
1719
  } catch {
1604
1720
  return "0.0.0";
@@ -1626,6 +1742,11 @@ function buildProgram() {
1626
1742
  program.command("pull").description("GET /api/cli/requirements/pull\uFF0C\u540C\u6B65\u6570\u636E\u5230 .apm \u76EE\u5F55").argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
1627
1743
  await runPull(requirementId);
1628
1744
  });
1745
+ program.command("upload-artifact").description(
1746
+ "\u5148\u6E05\u7A7A\u8BE5\u9700\u6C42\u5728\u5E73\u53F0\u4E0A\u7684\u4EA7\u7269\u6587\u6863\uFF0C\u518D\u5C06 .apm/workitems/<\u9700\u6C42ID> \u4E0B Markdown \u540C\u6B65\u4E0A\u53BB\uFF08\u6392\u9664 pull \u7CFB\u7EDF\u6587\u4EF6\uFF09"
1747
+ ).argument("<requirementId>", "\u9700\u6C42 ID").action(async (requirementId) => {
1748
+ await runUploadArtifact(requirementId);
1749
+ });
1629
1750
  program.command("branch").description(
1630
1751
  "\u5207\u6362\u6216\u521B\u5EFA\u9700\u6C42\u5206\u652F feat/req-<ID>\uFF1A\u8FDC\u7AEF\u5B58\u5728\u5219\u62C9\u53D6\u6700\u65B0\uFF1B\u8FDC\u7AEF\u5C1A\u65E0\u8BE5\u5206\u652F\u4E14\u672C\u5730\u4E5F\u65E0\u540C\u540D\u5206\u652F\u65F6\uFF0C\u9700\u5DF2 login\uFF0C\u5E76\u7531\u5E73\u53F0\u6839\u636E\u5F53\u524D\u76EE\u5F55\u8DEF\u5F84\u89E3\u6790\u4ED3\u5E93\u57FA\u7EBF\u5206\u652F\u540E\u4ECE origin \u68C0\u51FA\u518D\u63A8\u9001\uFF1B\u6709\u672C\u5730\u672A\u63D0\u4EA4\u6539\u52A8\u65F6\u5728\u975E\u76EE\u6807\u5206\u652F\u5148 stash\uFF08\u4E0D\u81EA\u52A8\u6062\u590D\uFF09\uFF0C\u5728\u76EE\u6807\u5206\u652F\u5219\u5148 commit"
1631
1752
  ).argument("<requirementId>", "\u9700\u6C42 ID").option(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-project-manage-cli",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: apm-review
3
- description: 根据需求 ID 读取工作项 prd.md,对照代码做需求评审;评审立场依可见代码而定(仅前台 / 仅后台 / 全栈);提交到评论的正文用 Markdown 拉出层次与重点,业务白话、避免代码与工程术语;结合代码交付面过滤与现状无关的空头边界质疑,当用户在对话中 @ 本技能时使用。
3
+ description: 根据需求 ID 读取工作项 prd.md,对照代码做需求评审;评审立场依可见代码而定(仅前台 / 仅后台 / 全栈);正文须结论先行、短句拆分,易懂且不整段复述 PRD(见 apm-review-reference.md);业务白话、避免代码与工程术语;结合代码交付面过滤与现状无关的空头边界质疑,当用户在对话中 @ 本技能时使用。
4
4
  ---
5
5
 
6
6
  # APM 需求评审(对照代码)
7
7
 
8
8
  用户仅提供 **需求 ID**(workitem id)。缺 ID 时索要,不猜测。
9
9
 
10
- **评审规范与评审模板**(受众与用语、立场与范围、书写约束、正文示例等)见同目录 **[apm-review-reference.md](./apm-review-reference.md)**;执行本技能时须按该文件撰写评审正文。
10
+ **评审规范与评审模板**(受众与用语、**易懂且不冗余的正文结构**、立场与范围、书写约束、待确认类问题的写法、正文示例等)见同目录 **[apm-review-reference.md](./apm-review-reference.md)**;执行本技能时须按该文件撰写评审正文,**每条缺陷均须结论先行、要点用短列表拆分,禁止大段复述 `prd.md`**。凡正文含须产品拍板的条目,待确认问题宜 **一句一点、并给出具体选项**(见 reference 中「待确认 / 歧义项:好问题的写法」)。
11
11
 
12
12
  ## 强制执行顺序(三步)
13
13
 
@@ -18,7 +18,7 @@ description: 根据需求 ID 读取工作项 prd.md,对照代码做需求评
18
18
 
19
19
  ### 步骤 2:撰写评审、落盘、推送、清理
20
20
 
21
- 1. 使用 **Read** 读取 [apm-review-reference.md](./apm-review-reference.md)(与 `SKILL.md` 同目录),并依其中规范对照代码撰写 **Markdown 评审正文**:**只写确实存在的问题**;用「需求背景 / 需求范围 / 交互与功能要求第 X 节 / 非目标」等文档自有结构**点名条款**,不先单独铺一节「对用户意图的理解」。撰写前须完成代码检索并锁定本轮**评审立场**(见 reference 中「评审立场」),正文内容与措辞须与该立场一致,**不得超越可见范围下断定**。
21
+ 1. 使用 **Read** 读取 [apm-review-reference.md](./apm-review-reference.md)(与 `SKILL.md` 同目录),并依其中规范对照代码撰写 **Markdown 评审正文**:**只写确实存在的问题**;用「需求背景 / 需求范围 / 交互与功能要求第 X 节 / 非目标」等文档自有结构**点名条款**(半句锚定即可),不先单独铺一节「对用户意图的理解」,**不写与 `prd-review` 类似的整条「需求描述」复述**。撰写前须完成代码检索并锁定本轮**评审立场**(见 reference 中「评审立场」),正文内容与措辞须与该立场一致,**不得超越可见范围下断定**。
22
22
  2. 使用 **Write** 将正文写入**临时文件**,路径建议使用**绝对路径**,例如 `/tmp/apm-review-<需求ID>.md`(避免与相对 cwd 混淆)。
23
23
  3. 在项目根目录下执行:`apm comment <需求ID> --file=<临时文件绝对路径> --model=<评论使用的模型名称>`。
24
24
  4. 命令结束后 **删除临时文件**(**Delete** 工具或 `rm`),无论命令成功或失败都尽量清理(失败时保留文件仅供用户排错——技能默认仍删除,若需保留应在表格备注中说明)。
@@ -35,4 +35,4 @@ description: 根据需求 ID 读取工作项 prd.md,对照代码做需求评
35
35
  | 2. 评审与 comment | 成功 / 失败 | 临时文件路径(已删可写「已清理」);`apm comment` 退出情况或 API 返回摘要;**建议**注明本轮评审立场(仅前台 / 仅后台 / 全栈) |
36
36
  | 3. 清理临时文件 | 成功 / 失败 | — |
37
37
 
38
- 内部编排:**通读 prd → 检索并对照与需求相关的代码 → 判定本轮可见范围(仅前台 / 仅后台 / 全栈)并选定评审立场 → 将技术发现改写为业务白话 → 只输出有问题之处**;唯**对用户输出**遵守上表约束。
38
+ 内部编排:**通读 prd → 检索并对照与需求相关的代码 → 判定本轮可见范围(仅前台 / 仅后台 / 全栈)并选定评审立场 → 将技术发现改写为业务白话 → 按 reference「易懂且不冗余」组织每条缺陷(结论先行、短列表)→ 只输出有问题之处**;唯**对用户输出**遵守上表约束。
@@ -15,7 +15,31 @@
15
15
  - **像真人写的**:自然段落或简短条目均可;**禁止**行政腔、教程腔套话,例如「请先确认」「建议补一句」「会上拍板」「建议各方对齐」等指向「写作动作」的提示——只陈述**文档里哪里不顺、会导致什么后果**。
16
16
  - **直入问题**:不写「对用户意图的理解」这类总起段;意图若与某条缺陷强相关,**并入该条一句带过**即可。
17
17
  - **只写有内容的条目**:某类问题不存在则**整段不写**;**禁止**用「未发现矛盾」「未见明显问题」「在已对照范围内无……」等否定句凑篇幅。若通读后没有可写问题:正文可仅为一两句说明「按当前正文暂无新增评审意见」,**仍不写**空洞分类标题。
18
- - **Markdown 结构与可读性(写入评论的正文)**:平台讨论区按 Markdown 渲染。正文宜用 **`##` 二级标题**按条拆分(标题里点明文档位置),条内可用 **「要点 / 后果」** 或等价两项列表;**关键短语加粗**,必要时用 `---` 分隔大段,避免「一整块纯叙述」难以扫读。结构服从内容:**没有问题则不硬凑条目**。
18
+ - **Markdown 结构与可读性(写入评论的正文)**:平台讨论区按 Markdown 渲染。正文宜用 **`##` 二级标题**按条拆分(标题里点明文档位置),条内结构须遵守下文 **「易懂且不冗余:每条缺陷怎么写(强制执行)」**;**关键短语加粗**,必要时用 `---` 分隔大段。结构服从内容:**没有问题则不硬凑条目**。
19
+
20
+ ## 易懂且不冗余:每条缺陷怎么写(强制执行)
21
+
22
+ 目标:**产品与业务方一眼能懂**,读起来不累;同时**不像 `prd-review` 那样为每条重复「需求描述」**,不把 PRD 整段抄进评论。
23
+
24
+ ### 必须遵守的顺序(每个 `##` 缺陷块内)
25
+
26
+ 1. **`##` 标题**:简短;带上文档锚点(章节名、小节编号或「第 X 条」)+ 问题关键词即可。
27
+ 2. **结论先行(单独一行)**:紧接着标题,**单独一行**写 **一句完整人话**,说明「哪里不对劲 / 和文档期望差什么」。读者只读这一句也应大致明白。**禁止**把结论藏在「要点」末尾或挤在长句宾语里。
28
+ 3. **文档锚点(半句,可并入结论或单独一行)**:用 **半句** 指向 PRD(例如「对应 … 第 X 条」),**不写**需求全文复述,**不**单独起一节「需求原文」「需求点摘要」。
29
+ 4. **要点**:用 **嵌套列表** 拆成多条短句(建议 2~4 条),**一条只说一件事**;说明「页面上 / 流程里实际怎样」「相对文档缺了什么」。单条避免多个「;」连环转折。
30
+ 5. **后果**:**优先一行**写完(谁会误判、验收不好勾、有何业务风险);确有需要再补第二句。**禁止**与结论重复同一句话换说法凑字数。
31
+ 6. **待产品拍板**(仅当需要决策时):紧跟该缺陷块,用已有规范 **一句一点 + 具体选项**(见「待确认 / 歧义项」);不写「建议对齐」类空话。
32
+
33
+ ### 篇幅与句式(写入评论前自检)
34
+
35
+ - **结论**:控制在 **约 35 字以内为宜**(复杂项可到一句半),能用「谁 / 在哪 / 缺什么」就不要用「未见……谈不上……」一串评审腔。
36
+ - **每条缺陷块**:除「待产品拍板」外,**整块以偏短为宜**;宁可多拆一条 `##`,也不要单条塞成一长段散文。
37
+ - **禁止**:以复述 PRD 段落代替分析;以「原则性表述」「无法闭环」等抽象收尾代替具体后果。
38
+
39
+ ### 与 `prd-review` 类模板的区别(避免冗余)
40
+
41
+ - **不要**:为每条缺陷先写一大段「需求描述」再写评论(那是冗余来源)。
42
+ - **只要**:标题或结论里的 **半句锚点** + **要点**里的针对性事实,让读者需要时可回去翻 `prd.md`。
19
43
 
20
44
  ## 评审立场(依可见代码)
21
45
 
@@ -34,6 +58,7 @@
34
58
  ## 立场:专业评审,而非复述原文
35
59
 
36
60
  - `prd.md` 可能口语化、不完整;正文用**清晰、可决策**的语言(业务名词与文档对齐),**对准具体条款**写缺口或风险。
61
+ - **对准条款 ≠ 复述条款**:用章节名、「第 X 条」或半句转述定位即可;**禁止**把 PRD 某节全文或大段复制进评论后再点评(易与 `prd-review` 式「需求描述」同级冗余)。
37
62
  - **禁止**空洞表态(例如「技术上都能做」);谈可行性须**建立在已读相关代码与模块边界之上**(且不超过上文「评审立场」准许的范围),说明与**现有能力划分、数据含义、产品约定**的关系及**后续改版成本**,用语落在「需求若坚持某种表述会带来何种**产品规则或协作上的代价**」,而非教人怎么写代码。
38
63
 
39
64
  ## 评审范围
@@ -59,6 +84,19 @@
59
84
 
60
85
  ## 书写约束(针对写入文件的评审正文)
61
86
 
87
+ ### 待确认 / 歧义项:好问题的写法
88
+
89
+ 当正文需要列出「须产品拍板」的条目时,优先写成**可一句决策**的问题,避免开放式空话。可参考下列特点(与具体业务无关,重结构与粒度):
90
+
91
+ | 特点 | 说明 |
92
+ | --- | --- |
93
+ | **一句一点** | 每条只对应一个决策主题;需要并列澄清时在一条内用分号串起同一主题下的子维度(入口形态;失败提示),不把多件不相干的事塞进同一句。 |
94
+ | **选项具体** | 用「是 A、B 还是 C」或并列短语给出可选方案,让读者能勾选一个答案或组合答复;避免单独出现「需明确交互形态」而无备选答案。 |
95
+ | **对齐文档缺口** | 指向 PRD 尚未写死的规则(入口、类型口径、端侧兼容、异常反馈等),便于对方按条回复,减少来回追问。 |
96
+
97
+ **反例**:「预览相关交互建议再和产品对齐一下。」(无选项、无锚点)
98
+ **正例**:「预览入口:文件列表单独『预览』按钮,还是点击文件名即预览;展示载体:弹窗、抽屉或新标签页,需定一种默认。」(一句内同一主题,且给出可选集合)
99
+
62
100
  - 每条问题须说清「指向文档哪一句 / 哪一节」+「会卡在哪」。篇幅上**优先简洁**:段落式宜控制在**两三句话量级**;若采用 **Markdown 分条**,每条下的「要点 / 后果」也各自保持简短,**禁止**为套模板而重复空话。
63
101
  - 引用需求文档用章节名、小节编号或口语转述条款;**不写**代码或工程标识符,技术边界用「评论受众与用语」中的白话改写。
64
102
  - **简洁优先**:宁可少写几条,也不要为显得「全面」而重复或空话。
@@ -68,22 +106,32 @@
68
106
 
69
107
  ## 评审模板(写入临时文件的正文示例,结构仅供参考)
70
108
 
71
- 按问题组织,**每条尽量点明文档位置**(章节名、小节编号、列表要点均可)。可读性要求高时优先采用 **Markdown 分条 + 要点/后果**(见上文「正文形态与语气」)。
109
+ 按问题组织;**每条缺陷**采用「结论先行 + 短要点 + 短后果」(见上文「易懂且不冗余」)。以下为推荐骨架。
72
110
 
73
- **示例(标题 + 要点/后果 + 加粗):**
111
+ **示例(结论先行 + 嵌套要点):**
74
112
 
75
113
  ```markdown
76
- ## 「需求范围」第三条:主链路落在哪一屏没说死
114
+ ## 「交互与功能要求」第 2 条:单笔回款与批量核销上限不一致
77
115
 
78
- - **要点**:……
79
- - **后果**:……
116
+ **结论**:单笔回款入账时,没有看到和批量核销一致的「不得超过节点剩余应收」校验,两条路可能一个能超额、一个不能。
117
+
118
+ - **要点**
119
+ - 批量核销(含预览)里,分配是按履约期次和节点的剩余应收封顶的。
120
+ - 单笔新增回款在界面上校的是日期等规则,没有像批量那样按节点剩余应收封顶。
121
+ - **后果**:同一笔钱走不同入口,合规边界可能不一致,验收「同一套上限规则」时不好勾选。
122
+
123
+ **待产品拍板**:若允许超额,需约定是统一禁止、单独权限还是二次确认(文中未写死)。
80
124
 
81
125
  ---
82
126
 
83
- ## 「交互与功能要求」第 4 节:类型认定规则缺失
127
+ ## 「需求范围」第三条:主链路落在哪一屏没说死
128
+
129
+ **结论**:文档没说清主链路默认从哪一屏进入,研发和验收可能对「做到哪算完成」各有一套理解。
84
130
 
85
- - **要点**:……
131
+ - **要点**
132
+ - …
133
+ - …
86
134
  - **后果**:……
87
135
  ```
88
136
 
89
- (以上为示例:实际只保留**本轮确有依据**的条目;没有问题则不硬写。)
137
+ (以上为示例:实际只保留**本轮确有依据**的条目;没有问题则不硬写;不需要决策时可删「待产品拍板」整段。)
@@ -1,142 +0,0 @@
1
- ---
2
- name: apm-dev
3
- description: 按需求 ID 全自动开发:切分支、读 PRD、按改动规模选择 Quick 或 Spec 路径实现代码、提交并推送;子 Agent 承担编码与规划落地;当用户 @ 本技能、提及全自动开发或「apm-dev」时使用。
4
- ---
5
-
6
- # APM 全自动开发(按需求 ID)
7
-
8
- 用户给出 **需求 ID**(`requirementId`,与工作项目录名一致)。**缺 ID 时索要,不猜测。**
9
- ---
10
-
11
- ## 输入
12
-
13
- | 字段 | 规则 |
14
- | --- | --- |
15
- | **`requirementId`** | **必填**。与 `.apm/workitems/<requirementId>/` 目录名一致。 |
16
-
17
- ---
18
-
19
- ## 技能文件定位(apm-propose / apm-apply-change)
20
-
21
- Spec 路径依赖的技能**仅**来自 **`.apm/skills/`**。执行前父 Agent **须 Read**:
22
-
23
- | 技能 | 路径 |
24
- | --- | --- |
25
- | **apm-propose** | `.apm/skills/apm-propose/SKILL.md` |
26
- | **apm-apply-change** | `.apm/skills/apm-apply-change/SKILL.md` |
27
-
28
- instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-instruction.md`)。若上述 `SKILL.md` **不存在或不可读**:**不得**进入 Spec 子流程;在表格中标记失败原因(例如需先 `apm init` 或同步 `.apm/skills`),并停止步骤 4。
29
-
30
- ---
31
-
32
- ## 流程总览
33
-
34
- | 序号 | 步骤 | 说明 |
35
- | --- | --- | --- |
36
- | 1 | **切分支** | 在仓库根目录执行 `apm branch <requirementId>` |
37
- | 2 | **读 PRD + 成本评估** | **Read** `.apm/workitems/<requirementId>/prd.md`,判定 Quick / Spec |
38
- | 3 | **Quick 开发** | 仅当判定为「改动成本较小」时执行;由 **子 Agent** 写代码 |
39
- | 4 | **Spec 开发** | 仅当判定为「改动成本较大」时执行;先 **apm-propose** 再 **apm-apply-change**,均由 **子 Agent** 按对应技能执行 |
40
- | 5 | **提交与推送** | `git` 提交并 `push`,工作区干净 |
41
- | 6 | **对用户回复** | **一张 Markdown 表格**汇总各步执行结果(见文末模板) |
42
-
43
- ---
44
-
45
- ## 步骤 1:`apm branch`
46
-
47
- 1. 在**仓库/工作区根目录**执行:
48
-
49
- ```bash
50
- apm branch <requirementId>
51
- ```
52
-
53
- 2. 记录:命令是否成功、简要输出或错误信息(写入最终表格)。**失败则按「Guardrails → 失败即终止」处理**,不为此命令做多轮重试(除非属 Agent 用错目录等可纠正失误)。
54
-
55
- ---
56
-
57
- ## 步骤 2:读取 PRD 并评估改动成本
58
-
59
- 1. **Read** 全文:`.apm/workitems/<requirementId>/prd.md`。
60
- 2. 若无法读取:在仓库根目录执行 **`apm pull <requirementId>`**(同步工作项),再 **Read** 一次;仍失败则**停止后续实现**,仅在表格中标记失败原因。
61
- 3. **不**为评估向用户发起追问;信息不足时倾向 **Spec**(保守)。
62
-
63
- ### Quick(较小)与 Spec(较大)判定参考
64
-
65
- **倾向 Quick**(满足越多越适用):
66
-
67
- - 影响范围局部:少量文件或单一层次(例如仅前端组件、或仅一个后端模块小改)。
68
- - 无新表结构/大规模迁移/权限模型变更。
69
- - PRD 验收点清晰且数量少(经验上 **≤3** 条独立验收维度)。
70
- - 不需要跨多服务的架构裁定即可开工。
71
-
72
- **倾向 Spec**(命中任一条即可):
73
-
74
- - 前后端联动、多包改造或新公共抽象。
75
- - 新数据模型、迁移、或安全/审计/权限相关。
76
- - PRD 范围大、条款多,或存在明显「待确认/多方案」需先规划。
77
- - 评估认为不先产出 **proposal / design / specs / tasks** 则难以保证实现与验收对齐。
78
-
79
- 在表格「步骤 2」中写明结论:**Quick** 或 **Spec**,以及**一行内**理由(关键词即可)。
80
-
81
- ---
82
-
83
- ## 步骤 3:Quick 开发模式(子 Agent)
84
-
85
- **条件**:步骤 2 判定为 **Quick**。
86
-
87
- 1. 父 Agent 已通过 **Read** 掌握 `prd.md`;若启动新子 Agent,在委派提示中写明 **`requirementId`**、工作项路径、以及「实现须严格对照 PRD,改动范围最小化」。
88
- 2. 使用 **Task** 工具,`subagent_type: generalPurpose`,**readonly: false**,委派子 Agent:
89
- - 自行 **Read** `.apm/workitems/<requirementId>/prd.md`(若会话未带全文)。
90
- - 按 PRD 直接改代码;遵守本仓库构建与依赖约定(AGENTS.md)。
91
- - 完成后在返回中说明:改了哪些路径、如何对照验收、是否通过本地可执行的检查(若子 Agent 跑了 `rushx build` / 测试等则写明结果)。
92
- 3. 父 Agent 根据子 Agent 返回在表格中填写步骤 3 **状态**;本模式下步骤 4 填 **跳过**。
93
-
94
- ---
95
-
96
- ## 步骤 4:Spec 开发模式(子 Agent)
97
-
98
- **条件**:步骤 2 判定为 **Spec**。
99
-
100
- 1. 父 Agent **Read** **apm-propose**、**apm-apply-change** 的 `SKILL.md`(**仅** `.apm/skills/` 下路径,见上节)。
101
- 2. **子 Agent A(规划)**:Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-propose/SKILL.md` 并完整遵循:在 `.apm/workitems/<requirementId>/` 生成 **proposal、design、specs、tasks** 等工件(顺序与依赖以 SKILL 为准)。
102
- 3. **子 Agent B(实现)**:待 A 成功落盘后,再 Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-apply-change/SKILL.md` 并完整遵循:按 **`tasks.md`** 驱动实现与勾选;遵守该技能中的停止条件与 commit 约定。
103
- 4. 若 **apm-propose** 未产出可用 **`tasks.md`**,不得强行进入 **apm-apply-change**;表格中标记阻塞原因。
104
- 5. 表格中步骤 3 填 **跳过**;步骤 4 分两行或合并一行写清 propose / apply 状态(见表格模板)。
105
-
106
- ---
107
-
108
- ## 步骤 5:提交并推送
109
-
110
- 在仓库根目录执行(可用一条复合命令或分步;以实际仓库远程为准):
111
-
112
- 1. `git status`:确认变更范围。
113
- 2. 若有未提交变更:`git add -A`,然后 `git commit -m "feat(req-<requirementId>): <简短说明>"`(说明应概括本轮需求实现;若子 Agent 已按任务多次 commit,则可能无需新 commit,以 `status` 为准)。
114
- 3. `git push`:推送到当前分支对应远程(首次必要时 `-u origin <branch>`)。
115
- 4. 再次 `git status`:**须为干净工作区**(无未提交、未跟踪的重要残留;若有应记录为失败或说明例外)。
116
-
117
- 将命令结果、最终分支名、是否已 push、工作区是否干净写入表格。
118
-
119
- ---
120
-
121
- ## 步骤 6:对用户回复(表格)
122
-
123
- 对用户回复 **必须包含一张 Markdown 表格**,汇总 **步骤 1~5**(步骤 6 为呈现表格本身,可不单独成行)。表头建议:
124
-
125
- | 步骤 | 内容 | 结果 |
126
- | --- | --- | --- |
127
- | 1 | `apm branch <requirementId>` | 成功 / 失败(原因) |
128
- | 2 | 读 PRD + 成本评估 | Quick 或 Spec;一行理由 |
129
- | 3 | Quick 开发(子 Agent) | 成功 / 失败 / **跳过** |
130
- | 4 | Spec:apm-propose → apm-apply-change(子 Agent) | 成功 / 失败 / **跳过**;可注明子步骤 |
131
- | 5 | commit & push;工作区干净 | 成功 / 失败(原因) |
132
-
133
- **可选**:在表格外增加**简短**一句话摘要(例如当前分支名、阻塞点);若用户此前约定「仅表格」,则可仅输出表格。
134
-
135
- ---
136
-
137
- ## Guardrails
138
-
139
- - **失败即终止**:在用户提供的 **`requirementId`** 等参数合法、命令与路径按本技能书写的前提下,任一步骤(含 `apm branch`、`apm pull`、读文件、子 Agent、`git`)**一旦失败**:**停止后续所有步骤**,仅在表格中记录失败步骤与原因;**不要**为登录、依赖、网络、命令结果等做**多次**或「轮番」重试。
140
- - **例外(Agent 自身失误)**:若失败明显由执行 Agent **用错工作目录、读错/漏写路径** 等导致,**允许**纠正 `cwd` 或路径后**仅对该失败步骤再执行一次**;纠正后仍失败则**立即终止**,不再扩展尝试。
141
- - **不要**在无 `prd.md`(且 `apm pull` 后仍无)的情况下编造需求实现。
142
- - **子 Agent** 提示中须带 **`requirementId`** 与仓库根路径意识,避免改错工作树。