ai-project-manage-cli 3.0.13 → 3.0.15
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 +114 -66
- package/package.json +1 -1
- package/template/skills/apm-dev/SKILL.md +47 -26
package/dist/index.js
CHANGED
|
@@ -231,8 +231,105 @@ import { execSync } from "child_process";
|
|
|
231
231
|
import { randomUUID } from "crypto";
|
|
232
232
|
import WebSocket from "ws";
|
|
233
233
|
import { Agent } from "@cursor/sdk";
|
|
234
|
+
|
|
235
|
+
// src/session-utils.ts
|
|
234
236
|
import { appendFileSync } from "fs";
|
|
235
237
|
import { resolve as resolve2 } from "path";
|
|
238
|
+
var EventSession = class {
|
|
239
|
+
events = [];
|
|
240
|
+
addEvent(event) {
|
|
241
|
+
const latestEvent = this.events[this.events.length - 1];
|
|
242
|
+
const formatedEvent = this.formatEvent(event);
|
|
243
|
+
if (!formatedEvent) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (formatedEvent.type === "tool_call") {
|
|
247
|
+
const existingToolCall = this.events.find(
|
|
248
|
+
(e) => e.type === "tool_call" && e.call_id === formatedEvent.call_id
|
|
249
|
+
);
|
|
250
|
+
if (existingToolCall) {
|
|
251
|
+
existingToolCall.args = formatedEvent.args;
|
|
252
|
+
existingToolCall.result = formatedEvent.result;
|
|
253
|
+
existingToolCall.status = formatedEvent.status;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.events.push(formatedEvent);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (formatedEvent.type === "status") {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (formatedEvent.type === "request") {
|
|
263
|
+
this.events.push(formatedEvent);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (latestEvent?.type === formatedEvent.type) {
|
|
267
|
+
switch (formatedEvent.type) {
|
|
268
|
+
case "assistant":
|
|
269
|
+
latestEvent.content += formatedEvent.content;
|
|
270
|
+
break;
|
|
271
|
+
case "thinking":
|
|
272
|
+
latestEvent.content += formatedEvent.content;
|
|
273
|
+
break;
|
|
274
|
+
case "task":
|
|
275
|
+
latestEvent.status = formatedEvent.status;
|
|
276
|
+
latestEvent.text = formatedEvent.text;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.events.push(event);
|
|
282
|
+
}
|
|
283
|
+
formatEvent(event) {
|
|
284
|
+
switch (event.type) {
|
|
285
|
+
case "assistant":
|
|
286
|
+
return {
|
|
287
|
+
type: "assistant",
|
|
288
|
+
content: event.message.content[0].text || ""
|
|
289
|
+
};
|
|
290
|
+
case "thinking":
|
|
291
|
+
return { type: "thinking", content: event.text || "" };
|
|
292
|
+
case "tool_call":
|
|
293
|
+
return {
|
|
294
|
+
type: "tool_call",
|
|
295
|
+
args: event.args,
|
|
296
|
+
result: event.result,
|
|
297
|
+
status: event.status,
|
|
298
|
+
call_id: event.call_id,
|
|
299
|
+
name: event.name
|
|
300
|
+
};
|
|
301
|
+
case "task":
|
|
302
|
+
return {
|
|
303
|
+
type: "task",
|
|
304
|
+
status: event.status,
|
|
305
|
+
text: event.text
|
|
306
|
+
};
|
|
307
|
+
case "request":
|
|
308
|
+
return {
|
|
309
|
+
...event,
|
|
310
|
+
type: "request"
|
|
311
|
+
};
|
|
312
|
+
case "status":
|
|
313
|
+
return { type: "status", status: event.status, message: event.message };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
writeToFile(cwd, requirementId, agentId) {
|
|
317
|
+
const sessionsDir = resolve2(
|
|
318
|
+
cwd,
|
|
319
|
+
`.apm/workitems/${requirementId}/sessions`
|
|
320
|
+
);
|
|
321
|
+
ensureDirExists(sessionsDir);
|
|
322
|
+
const sessionFile = resolve2(sessionsDir, `${agentId}.md`);
|
|
323
|
+
this.events.forEach((event) => {
|
|
324
|
+
appendFileSync(
|
|
325
|
+
sessionFile,
|
|
326
|
+
JSON.stringify(event, null, 2) + "\n=====================================================\n"
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/commands/connect.ts
|
|
236
333
|
function runConnect(opts) {
|
|
237
334
|
void (async () => {
|
|
238
335
|
const cfg = await ensureApmConfig();
|
|
@@ -294,8 +391,7 @@ function runConnect(opts) {
|
|
|
294
391
|
status: "WORKING"
|
|
295
392
|
});
|
|
296
393
|
let IN_PROGRESS = false;
|
|
297
|
-
const
|
|
298
|
-
let data2 = {};
|
|
394
|
+
const session = new EventSession();
|
|
299
395
|
const run = await agent.send(payload.prompt);
|
|
300
396
|
for await (const event of run.stream()) {
|
|
301
397
|
if (!IN_PROGRESS) {
|
|
@@ -306,50 +402,30 @@ function runConnect(opts) {
|
|
|
306
402
|
});
|
|
307
403
|
IN_PROGRESS = true;
|
|
308
404
|
}
|
|
405
|
+
if (event.type === "status") {
|
|
406
|
+
console.log(`[Status]`, event.message || event.status);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
309
409
|
if (event.type === "assistant") {
|
|
310
410
|
const output = event.message.content[0].text;
|
|
311
411
|
console.log(`[Output]`, output);
|
|
312
|
-
|
|
313
|
-
events.push(data2);
|
|
314
|
-
data2 = {};
|
|
315
|
-
}
|
|
316
|
-
data2.type = "output";
|
|
317
|
-
if (!data2.content) {
|
|
318
|
-
data2.content = "";
|
|
319
|
-
}
|
|
320
|
-
data2.content += output;
|
|
412
|
+
session.addEvent(event);
|
|
321
413
|
continue;
|
|
322
414
|
}
|
|
323
415
|
if (event.type === "thinking") {
|
|
324
416
|
const thinking = event.text;
|
|
325
417
|
console.log(`[Thinking]`, thinking);
|
|
326
|
-
|
|
327
|
-
events.push(data2);
|
|
328
|
-
data2 = {};
|
|
329
|
-
}
|
|
330
|
-
data2.type = "thinking";
|
|
331
|
-
if (!data2.content) {
|
|
332
|
-
data2.content = "";
|
|
333
|
-
}
|
|
334
|
-
data2.content += thinking;
|
|
418
|
+
session.addEvent(event);
|
|
335
419
|
continue;
|
|
336
420
|
}
|
|
337
|
-
if (event.type === "tool_call"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
events.push(data2);
|
|
344
|
-
data2 = {};
|
|
421
|
+
if (event.type === "tool_call") {
|
|
422
|
+
if (event.status === "completed" || event.status === "error") {
|
|
423
|
+
console.log(`[ToolCall(${event.call_id}) Result]`);
|
|
424
|
+
console.log(JSON.stringify(event.args));
|
|
425
|
+
console.log(JSON.stringify(event.result, null, 2));
|
|
426
|
+
console.log("---------------\u8C03\u7528\u5B8C\u6210-----------------");
|
|
345
427
|
}
|
|
346
|
-
|
|
347
|
-
data2.call_id = event.call_id;
|
|
348
|
-
data2.args = event.args;
|
|
349
|
-
data2.result = event.result;
|
|
350
|
-
data2.status = event.status;
|
|
351
|
-
events.push(data2);
|
|
352
|
-
data2 = {};
|
|
428
|
+
session.addEvent(event);
|
|
353
429
|
continue;
|
|
354
430
|
}
|
|
355
431
|
if (event.type === "task") {
|
|
@@ -357,32 +433,15 @@ function runConnect(opts) {
|
|
|
357
433
|
console.log("--------------------------------");
|
|
358
434
|
console.log(event.text || "\u65E0");
|
|
359
435
|
console.log("--------------------------------");
|
|
360
|
-
|
|
361
|
-
events.push(data2);
|
|
362
|
-
data2 = {};
|
|
363
|
-
}
|
|
364
|
-
data2.type = "task";
|
|
365
|
-
data2.status = event.status;
|
|
366
|
-
data2.text = event.text;
|
|
367
|
-
events.push(data2);
|
|
368
|
-
data2 = {};
|
|
436
|
+
session.addEvent(event);
|
|
369
437
|
continue;
|
|
370
438
|
}
|
|
371
439
|
if (event.type === "request") {
|
|
372
440
|
console.log(JSON.stringify(event, null, 2));
|
|
373
|
-
|
|
374
|
-
events.push(data2);
|
|
375
|
-
data2 = {};
|
|
376
|
-
}
|
|
377
|
-
data2 = event;
|
|
441
|
+
session.addEvent(event);
|
|
378
442
|
continue;
|
|
379
443
|
}
|
|
380
444
|
console.log("\u672A\u77E5\u4E8B\u4EF6:", JSON.stringify(event, null, 2));
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
if (data2.type) {
|
|
384
|
-
events.push(data2);
|
|
385
|
-
data2 = {};
|
|
386
445
|
}
|
|
387
446
|
await api.cliRequirements.updateTaskStatus({
|
|
388
447
|
taskId: payload.taskId,
|
|
@@ -394,18 +453,7 @@ function runConnect(opts) {
|
|
|
394
453
|
status: "IDLE"
|
|
395
454
|
});
|
|
396
455
|
console.log("[Done]");
|
|
397
|
-
|
|
398
|
-
payload.cwd,
|
|
399
|
-
`.apm/workitems/${payload.requirementId}/sessions`
|
|
400
|
-
);
|
|
401
|
-
ensureDirExists(sessionsDir);
|
|
402
|
-
const sessionFile = resolve2(sessionsDir, `${run.agentId}.md`);
|
|
403
|
-
for (const event of events) {
|
|
404
|
-
appendFileSync(
|
|
405
|
-
sessionFile,
|
|
406
|
-
JSON.stringify(event, null, 2) + "\n=====================================================\n"
|
|
407
|
-
);
|
|
408
|
-
}
|
|
456
|
+
session.writeToFile(payload.cwd, payload.requirementId, run.agentId);
|
|
409
457
|
} catch {
|
|
410
458
|
console.error("[apm] \u65E0\u6CD5\u89E3\u6790 WebSocket \u6D88\u606F:", text);
|
|
411
459
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apm-dev
|
|
3
|
-
description: 按需求 ID
|
|
3
|
+
description: 按需求 ID 全自动开发:切分支、Read defect.xml(无/空内容→新功能;有内容→修 Bug 可短路至提交)、读 PRD、按改动规模选择 Quick 或 Spec、提交并推送、同步产物;子 Agent 承担编码与规划落地;当用户 @ 本技能、提及全自动开发或「apm-dev」时使用。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# APM 全自动开发(按需求 ID)
|
|
@@ -25,7 +25,7 @@ Spec 路径依赖的技能**仅**来自 **`.apm/skills/`**。执行前父 Agent
|
|
|
25
25
|
| **apm-propose** | `.apm/skills/apm-propose/SKILL.md` |
|
|
26
26
|
| **apm-apply-change** | `.apm/skills/apm-apply-change/SKILL.md` |
|
|
27
27
|
|
|
28
|
-
instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-instruction.md`)。若上述 `SKILL.md` **不存在或不可读**:**不得**进入 Spec 子流程;在表格中标记失败原因(例如需先 `apm init` 或同步 `.apm/skills`),并停止步骤
|
|
28
|
+
instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-instruction.md`)。若上述 `SKILL.md` **不存在或不可读**:**不得**进入 Spec 子流程;在表格中标记失败原因(例如需先 `apm init` 或同步 `.apm/skills`),并停止步骤 5。
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -34,12 +34,13 @@ instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-i
|
|
|
34
34
|
| 序号 | 步骤 | 说明 |
|
|
35
35
|
| --- | --- | --- |
|
|
36
36
|
| 1 | **切分支** | 在仓库根目录执行 `apm branch <requirementId>` |
|
|
37
|
-
| 2 |
|
|
38
|
-
| 3 | **Quick
|
|
39
|
-
| 4 | **
|
|
40
|
-
| 5 |
|
|
41
|
-
| 6 |
|
|
42
|
-
| 7 |
|
|
37
|
+
| 2 | **缺陷清单(分流)** | **Read** `defect.xml`:**无文件或无有效内容** → 跳过,进入步骤 3(**新功能**);**有内容** → **修复 Bug**:Quick 修 Bug(子 Agent)成功后**跳过步骤 3~5**,直达步骤 **6** |
|
|
38
|
+
| 3 | **读 PRD + 成本评估** | **Read** `.apm/workitems/<requirementId>/prd.md`,判定 Quick / Spec(步骤 2 未走修 Bug 短路时执行,即新功能流程) |
|
|
39
|
+
| 4 | **Quick 开发** | 仅当判定为「改动成本较小」时执行;由 **子 Agent** 写代码 |
|
|
40
|
+
| 5 | **Spec 开发** | 仅当判定为「改动成本较大」时执行;先 **apm-propose** 再 **apm-apply-change**,均由 **子 Agent** 按对应技能执行 |
|
|
41
|
+
| 6 | **提交与推送** | `git` 提交并 `push`,工作区干净 |
|
|
42
|
+
| 7 | **同步产物** | `apm upload-artifact <requirementId>`,将工作项目录内 Markdown 产物同步到平台 |
|
|
43
|
+
| 8 | **对用户回复** | **一张 Markdown 表格**汇总各步执行结果(见文末模板) |
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -55,7 +56,26 @@ instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-i
|
|
|
55
56
|
|
|
56
57
|
---
|
|
57
58
|
|
|
58
|
-
## 步骤 2
|
|
59
|
+
## 步骤 2:读取缺陷清单(defect.xml)
|
|
60
|
+
|
|
61
|
+
1. 父 Agent 使用 **Read** 工具读取:`.apm/workitems/<requirementId>/defect.xml`(缺陷列表,格式以平台同步为准)。
|
|
62
|
+
2. 若首次 **Read** 失败(无此文件等):在仓库根目录执行 **`apm pull <requirementId>`**,再 **Read** 一次。
|
|
63
|
+
3. **分流(非失败)**:
|
|
64
|
+
- **无文件、仍读不到、或文件无有效内容**(空文件、仅空白、无实质缺陷条目等;具体结构以同步格式为准):**不视为步骤失败**。表格步骤 2 填 **跳过(无 defect 内容 → 新功能)**,**直接进入步骤 3**。
|
|
65
|
+
- **文件存在且有有效内容**:视为本轮为 **修复 Bug 流程**(与步骤 4 同属 Quick 形态,对照 **defect.xml + 必要时 prd.md**,目标是消除所列缺陷):
|
|
66
|
+
- 父 Agent 已通过 **Read** 掌握 `defect.xml`;若启动子 Agent,在委派提示中写明 **`requirementId`**、工作项路径、`defect.xml` 路径,以及「逐项对照缺陷列表修复,改动范围最小化」。
|
|
67
|
+
- 使用 **Task** 工具,`subagent_type: generalPurpose`,**readonly: false**,委派子 Agent:
|
|
68
|
+
- 自行 **Read** `.apm/workitems/<requirementId>/defect.xml`(及需要时的 `prd.md`)。
|
|
69
|
+
- 按缺陷列表直接改代码;遵守本仓库构建与依赖约定(AGENTS.md)。
|
|
70
|
+
- 完成后在返回中说明:修了哪些缺陷项、改了哪些路径、本地检查结果(若有)。
|
|
71
|
+
- 父 Agent 根据子 Agent 返回填写表格步骤 2 **状态**(成功 / 失败);**失败则按 Guardrails 终止**。
|
|
72
|
+
4. **短路规则**:步骤 2 走 **修复 Bug** 且子 Agent **成功**后:**不再执行步骤 3~5**(表格中步骤 3、4、5 均填 **跳过(缺陷短路)**),直接进入 **步骤 6**(提交与推送)。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 步骤 3:读取 PRD 并评估改动成本
|
|
77
|
+
|
|
78
|
+
**条件**:仅当步骤 2 **未**以「`defect.xml` **有内容**且 Quick 修 Bug **成功** → 短路至步骤 6」结束时执行(含:`defect.xml` 缺失、拉取后仍无、或**无有效内容**而走新功能流程)。
|
|
59
79
|
|
|
60
80
|
1. **Read** 全文:`.apm/workitems/<requirementId>/prd.md`。
|
|
61
81
|
2. 若无法读取:在仓库根目录执行 **`apm pull <requirementId>`**(同步工作项),再 **Read** 一次;仍失败则**停止后续实现**,仅在表格中标记失败原因。
|
|
@@ -77,36 +97,36 @@ instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-i
|
|
|
77
97
|
- PRD 范围大、条款多,或存在明显「待确认/多方案」需先规划。
|
|
78
98
|
- 评估认为不先产出 **proposal / design / specs / tasks** 则难以保证实现与验收对齐。
|
|
79
99
|
|
|
80
|
-
在表格「步骤
|
|
100
|
+
在表格「步骤 3」中写明结论:**Quick** 或 **Spec**,以及**一行内**理由(关键词即可)。
|
|
81
101
|
|
|
82
102
|
---
|
|
83
103
|
|
|
84
|
-
## 步骤
|
|
104
|
+
## 步骤 4:Quick 开发模式(子 Agent)
|
|
85
105
|
|
|
86
|
-
**条件**:步骤
|
|
106
|
+
**条件**:步骤 3 判定为 **Quick**(且未走步骤 2 缺陷短路)。
|
|
87
107
|
|
|
88
108
|
1. 父 Agent 已通过 **Read** 掌握 `prd.md`;若启动新子 Agent,在委派提示中写明 **`requirementId`**、工作项路径、以及「实现须严格对照 PRD,改动范围最小化」。
|
|
89
109
|
2. 使用 **Task** 工具,`subagent_type: generalPurpose`,**readonly: false**,委派子 Agent:
|
|
90
110
|
- 自行 **Read** `.apm/workitems/<requirementId>/prd.md`(若会话未带全文)。
|
|
91
111
|
- 按 PRD 直接改代码;遵守本仓库构建与依赖约定(AGENTS.md)。
|
|
92
112
|
- 完成后在返回中说明:改了哪些路径、如何对照验收、是否通过本地可执行的检查(若子 Agent 跑了 `rushx build` / 测试等则写明结果)。
|
|
93
|
-
3. 父 Agent 根据子 Agent 返回在表格中填写步骤
|
|
113
|
+
3. 父 Agent 根据子 Agent 返回在表格中填写步骤 4 **状态**;本模式下步骤 5 填 **跳过**。
|
|
94
114
|
|
|
95
115
|
---
|
|
96
116
|
|
|
97
|
-
## 步骤
|
|
117
|
+
## 步骤 5:Spec 开发模式(子 Agent)
|
|
98
118
|
|
|
99
|
-
**条件**:步骤
|
|
119
|
+
**条件**:步骤 3 判定为 **Spec**(且未走步骤 2 缺陷短路)。
|
|
100
120
|
|
|
101
121
|
1. 父 Agent **Read** **apm-propose**、**apm-apply-change** 的 `SKILL.md`(**仅** `.apm/skills/` 下路径,见上节)。
|
|
102
122
|
2. **子 Agent A(规划)**:Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-propose/SKILL.md` 并完整遵循:在 `.apm/workitems/<requirementId>/` 生成 **proposal、design、specs、tasks** 等工件(顺序与依赖以 SKILL 为准)。
|
|
103
123
|
3. **子 Agent B(实现)**:待 A 成功落盘后,再 Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-apply-change/SKILL.md` 并完整遵循:按 **`tasks.md`** 驱动实现与勾选;遵守该技能中的停止条件与 commit 约定。
|
|
104
124
|
4. 若 **apm-propose** 未产出可用 **`tasks.md`**,不得强行进入 **apm-apply-change**;表格中标记阻塞原因。
|
|
105
|
-
5. 表格中步骤
|
|
125
|
+
5. 表格中步骤 4 填 **跳过**;步骤 5 分两行或合并一行写清 propose / apply 状态(见表格模板)。
|
|
106
126
|
|
|
107
127
|
---
|
|
108
128
|
|
|
109
|
-
## 步骤
|
|
129
|
+
## 步骤 6:提交并推送
|
|
110
130
|
|
|
111
131
|
在仓库根目录执行(可用一条复合命令或分步;以实际仓库远程为准):
|
|
112
132
|
|
|
@@ -119,9 +139,9 @@ instruction 子文件随该目录类推(如 `.apm/skills/apm-propose/propose-i
|
|
|
119
139
|
|
|
120
140
|
---
|
|
121
141
|
|
|
122
|
-
## 步骤
|
|
142
|
+
## 步骤 7:同步产物
|
|
123
143
|
|
|
124
|
-
在**仓库/工作区根目录**(与步骤 1、
|
|
144
|
+
在**仓库/工作区根目录**(与步骤 1、6 一致)执行:
|
|
125
145
|
|
|
126
146
|
```bash
|
|
127
147
|
apm upload-artifact <requirementId>
|
|
@@ -133,18 +153,19 @@ apm upload-artifact <requirementId>
|
|
|
133
153
|
|
|
134
154
|
---
|
|
135
155
|
|
|
136
|
-
## 步骤
|
|
156
|
+
## 步骤 8:对用户回复(表格)
|
|
137
157
|
|
|
138
|
-
对用户回复 **必须包含一张 Markdown 表格**,汇总 **步骤 1~
|
|
158
|
+
对用户回复 **必须包含一张 Markdown 表格**,汇总 **步骤 1~7**(步骤 8 为呈现表格本身,可不单独成行)。表头建议:
|
|
139
159
|
|
|
140
160
|
| 步骤 | 内容 | 结果 |
|
|
141
161
|
| --- | --- | --- |
|
|
142
162
|
| 1 | `apm branch <requirementId>` | 成功 / 失败(原因) |
|
|
143
|
-
| 2 |
|
|
144
|
-
| 3 |
|
|
145
|
-
| 4 |
|
|
146
|
-
| 5 |
|
|
147
|
-
| 6 |
|
|
163
|
+
| 2 | Read `defect.xml`:无/空→新功能;有内容→Quick 修 Bug(子 Agent) | 跳过(无内容·新功能)/ 成功 / 失败;短路时注明 |
|
|
164
|
+
| 3 | 读 PRD + 成本评估 | Quick 或 Spec;一行理由 / **跳过(缺陷短路)** |
|
|
165
|
+
| 4 | Quick 开发(子 Agent) | 成功 / 失败 / **跳过** |
|
|
166
|
+
| 5 | Spec:apm-propose → apm-apply-change(子 Agent) | 成功 / 失败 / **跳过**;可注明子步骤 |
|
|
167
|
+
| 6 | commit & push;工作区干净 | 成功 / 失败(原因) |
|
|
168
|
+
| 7 | `apm upload-artifact <requirementId>` | 成功 / 失败(原因) |
|
|
148
169
|
|
|
149
170
|
**可选**:在表格外增加**简短**一句话摘要(例如当前分支名、阻塞点);若用户此前约定「仅表格」,则可仅输出表格。
|
|
150
171
|
|