ai-project-manage-cli 8.1.21 → 8.1.22
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/README.md +0 -4
- package/dist/index.js +622 -656
- package/dist/scripts/branch_has_changes.sh +53 -0
- package/dist/scripts/commit_if_dirty.sh +21 -0
- package/dist/scripts/ensure_baseline.sh +15 -0
- package/dist/scripts/ensure_feature_branch.sh +55 -0
- package/dist/scripts/push_code.sh +21 -0
- package/dist/worker-script.js +2584 -3120
- package/package.json +2 -2
- package/template/README.md +13 -0
- package/template/hooks/after-agent-commit-repos.cjs +112 -0
- package/template/hooks/before-prompt-prepare.cjs +206 -0
- package/template/hooks.json +18 -0
- package/template/mcp.json +18 -0
- package/template/skills/webide-run-retro/SKILL.md +0 -103
- package/template/skills/webide-run-retro/attribution.md +0 -26
- package/template/skills/webide-run-retro/scripts/inspect-run.mjs +0 -335
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-project-manage-cli",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.22",
|
|
4
4
|
"description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"template"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.js && esbuild src/commands/connect/core/worker/worker-script.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/worker-script.js && tsc --noEmit -p tsconfig.json",
|
|
17
|
+
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.js && esbuild src/commands/connect/core/worker/worker-script.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/worker-script.js && mkdir -p dist/scripts && cp src/scripts/*.sh dist/scripts/ && chmod +x dist/scripts/*.sh && tsc --noEmit -p tsconfig.json",
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# CLI template(新版星图 / ACP)
|
|
2
|
+
|
|
3
|
+
本目录供**新版** Cursor ACP / 星图流程使用,与历史 WebIDE `apm connect` 路径**解耦**:
|
|
4
|
+
|
|
5
|
+
| 内容 | 用途 |
|
|
6
|
+
|------|------|
|
|
7
|
+
| `mcp.json` | 远程 MCP(`x-task-id` / `x-project-id`) |
|
|
8
|
+
| `hooks/` | ACP hooks:开对话切分支、结束 commit/push |
|
|
9
|
+
| `skills/` | 星图等新版 skills(如 `starmap-requirement-align`) |
|
|
10
|
+
|
|
11
|
+
历史 WebIDE 指南与规则仍由平台 `agent-config` 灌库,经 `apm update-skills` 写入 `.apm/`,**不读**本 template。
|
|
12
|
+
|
|
13
|
+
请勿把本目录 hooks/mcp 默认拷进历史 WebIDE 工作区,以免与旧流程混用。
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor stop(每次 Agent 执行结束一次):
|
|
4
|
+
* 对 `/home/repository.json` 各仓 commit-if-dirty + push。
|
|
5
|
+
* 无变更不空 commit;单仓失败继续其它仓;本 hook 始终 exit 0。
|
|
6
|
+
*
|
|
7
|
+
* ACP 若不触发 stop:由启动方在进程结束后调用
|
|
8
|
+
* apm commit-task-repos --cwd <workdir>
|
|
9
|
+
*/
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const { spawnSync } = require("node:child_process");
|
|
13
|
+
const { existsSync, readFileSync } = require("node:fs");
|
|
14
|
+
const { join, resolve } = require("node:path");
|
|
15
|
+
|
|
16
|
+
function respond(obj) {
|
|
17
|
+
process.stdout.write(JSON.stringify(obj ?? {}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readStdin() {
|
|
21
|
+
try {
|
|
22
|
+
return readFileSync(0, "utf8");
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveWorkdir(payload) {
|
|
29
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd.trim() : "";
|
|
30
|
+
if (cwd) return resolve(cwd);
|
|
31
|
+
const roots = Array.isArray(payload.workspace_roots)
|
|
32
|
+
? payload.workspace_roots
|
|
33
|
+
: [];
|
|
34
|
+
for (const root of roots) {
|
|
35
|
+
if (typeof root === "string" && root.trim()) return resolve(root.trim());
|
|
36
|
+
}
|
|
37
|
+
return process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** APM 工作区根 = ACP 启动目录,其下必须有 `.apm`(不向上查找) */
|
|
41
|
+
function requireApmWorkdir(start) {
|
|
42
|
+
const workdir = resolve(start);
|
|
43
|
+
if (!existsSync(join(workdir, ".apm"))) return null;
|
|
44
|
+
return workdir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function whichApm() {
|
|
48
|
+
const r = spawnSync("bash", ["-lc", "command -v apm"], {
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
});
|
|
51
|
+
if (r.status !== 0) return "";
|
|
52
|
+
return (r.stdout || "").trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasTaskContext() {
|
|
56
|
+
return Boolean(
|
|
57
|
+
(process.env.APM_TASK_ID || "").trim() ||
|
|
58
|
+
(process.env.APM_PROJECT_ID || "").trim()
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
let input = {};
|
|
64
|
+
try {
|
|
65
|
+
const raw = readStdin();
|
|
66
|
+
if (raw.trim()) input = JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
/* 收尾:输入坏了也尽量继续 */
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workdirHint = resolveWorkdir(input);
|
|
72
|
+
const workdir = requireApmWorkdir(workdirHint);
|
|
73
|
+
if (!workdir) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`[apm] ACP 启动目录下未找到 .apm,跳过收尾: ${resolve(workdirHint)}\n`
|
|
76
|
+
);
|
|
77
|
+
respond({});
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!hasTaskContext() && !existsSync("/home/repository.json")) {
|
|
82
|
+
respond({});
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const apm = whichApm();
|
|
87
|
+
if (!apm) {
|
|
88
|
+
process.stderr.write("[apm] commit-task-repos:未找到 apm,跳过收尾\n");
|
|
89
|
+
respond({});
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = spawnSync(apm, ["commit-task-repos", "--cwd", workdir], {
|
|
94
|
+
encoding: "utf8",
|
|
95
|
+
env: process.env,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (result.stdout?.trim()) process.stderr.write(result.stdout);
|
|
99
|
+
if (result.stderr?.trim()) process.stderr.write(result.stderr);
|
|
100
|
+
|
|
101
|
+
respond({});
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
main();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
109
|
+
process.stderr.write(`[apm] after-agent-commit-repos 异常: ${detail}\n`);
|
|
110
|
+
respond({});
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor beforeSubmitPrompt(对话 / 本轮用户提问开始之前):
|
|
4
|
+
* 1. 缓存用户问题(供结束时 commit message)
|
|
5
|
+
* 2. 确保 /home/repository.json 内各仓位于 feat/task-<APM_TASK_ID>
|
|
6
|
+
*
|
|
7
|
+
* 失败则 continue:false,本轮不进入 Agent。
|
|
8
|
+
* ACP 若不触发本 hook:启动对话前由宿主执行
|
|
9
|
+
* apm ensure-task-branch && 写入 user-prompt 缓存文件
|
|
10
|
+
*/
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { createHash } = require("node:crypto");
|
|
14
|
+
const { spawnSync } = require("node:child_process");
|
|
15
|
+
const {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} = require("node:fs");
|
|
21
|
+
const { tmpdir } = require("node:os");
|
|
22
|
+
const { join, resolve } = require("node:path");
|
|
23
|
+
|
|
24
|
+
function respond(obj) {
|
|
25
|
+
process.stdout.write(JSON.stringify(obj));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function allow() {
|
|
29
|
+
respond({ continue: true });
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deny(userMessage) {
|
|
34
|
+
respond({
|
|
35
|
+
continue: false,
|
|
36
|
+
user_message: userMessage,
|
|
37
|
+
});
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStdin() {
|
|
42
|
+
return readFileSync(0, "utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveWorkdir(payload) {
|
|
46
|
+
const roots = Array.isArray(payload.workspace_roots)
|
|
47
|
+
? payload.workspace_roots
|
|
48
|
+
: [];
|
|
49
|
+
for (const root of roots) {
|
|
50
|
+
if (typeof root === "string" && root.trim()) return resolve(root.trim());
|
|
51
|
+
}
|
|
52
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd.trim() : "";
|
|
53
|
+
if (cwd) return resolve(cwd);
|
|
54
|
+
return process.cwd();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* APM 工作区根 = Cursor ACP 启动目录,其下必须有 `.apm`(不向上查找)。
|
|
59
|
+
* @returns {string | null}
|
|
60
|
+
*/
|
|
61
|
+
function requireApmWorkdir(start) {
|
|
62
|
+
const workdir = resolve(start);
|
|
63
|
+
if (!existsSync(join(workdir, ".apm"))) return null;
|
|
64
|
+
return workdir;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function promptCachePath(workdir) {
|
|
68
|
+
const key = createHash("sha256").update(workdir).digest("hex").slice(0, 16);
|
|
69
|
+
return join(tmpdir(), "apm-hooks", key, "user-prompt.txt");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cacheUserPrompt(workdir, prompt) {
|
|
73
|
+
const text = typeof prompt === "string" ? prompt.trim() : "";
|
|
74
|
+
if (!text) return;
|
|
75
|
+
const path = promptCachePath(workdir);
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
78
|
+
writeFileSync(path, `${text}\n`, "utf8");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
81
|
+
process.stderr.write(`[apm] 缓存用户问题失败: ${detail}\n`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function expectedBranch(taskId) {
|
|
86
|
+
return `feat/task-${taskId}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function currentBranch(repoRoot) {
|
|
90
|
+
const r = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
91
|
+
cwd: repoRoot,
|
|
92
|
+
encoding: "utf8",
|
|
93
|
+
});
|
|
94
|
+
if (r.status !== 0) return null;
|
|
95
|
+
return (r.stdout || "").trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const HOME_REPOSITORY_JSON = "/home/repository.json";
|
|
99
|
+
|
|
100
|
+
function listReposNotOnBranch(workdir, branch) {
|
|
101
|
+
if (!existsSync(HOME_REPOSITORY_JSON)) return null;
|
|
102
|
+
let map;
|
|
103
|
+
try {
|
|
104
|
+
map = JSON.parse(readFileSync(HOME_REPOSITORY_JSON, "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return null;
|
|
109
|
+
const keys = Object.keys(map)
|
|
110
|
+
.map((k) => String(k).trim())
|
|
111
|
+
.filter((k) => k && k !== "." && !k.includes("/") && !k.includes("\\"));
|
|
112
|
+
if (keys.length === 0) return null;
|
|
113
|
+
const off = [];
|
|
114
|
+
for (const rel of keys) {
|
|
115
|
+
if (currentBranch(join(workdir, rel)) !== branch) off.push(rel);
|
|
116
|
+
}
|
|
117
|
+
return off;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function whichApm() {
|
|
121
|
+
const r = spawnSync("bash", ["-lc", "command -v apm"], {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
});
|
|
124
|
+
if (r.status !== 0) return "";
|
|
125
|
+
return (r.stdout || "").trim();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function main() {
|
|
129
|
+
let input;
|
|
130
|
+
try {
|
|
131
|
+
input = JSON.parse(readStdin() || "{}");
|
|
132
|
+
} catch {
|
|
133
|
+
deny("[apm] beforeSubmitPrompt 输入不是合法 JSON");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const workdirHint = resolveWorkdir(input);
|
|
137
|
+
const workdir = requireApmWorkdir(workdirHint);
|
|
138
|
+
if (!workdir) {
|
|
139
|
+
deny(
|
|
140
|
+
`[apm] ACP 启动目录下未找到 .apm:${resolve(workdirHint)}(必须为启动目录/.apm,不向上查找)`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
cacheUserPrompt(workdir, input.prompt);
|
|
145
|
+
|
|
146
|
+
const taskId = (process.env.APM_TASK_ID || "").trim();
|
|
147
|
+
const projectId = (process.env.APM_PROJECT_ID || "").trim();
|
|
148
|
+
|
|
149
|
+
// 非 APM 任务:只缓存问题(若有),放行
|
|
150
|
+
if (!taskId && !projectId) {
|
|
151
|
+
allow();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!taskId || !projectId) {
|
|
155
|
+
deny(
|
|
156
|
+
"[apm] 缺少 APM_TASK_ID 或 APM_PROJECT_ID,无法在对话开始前确认特性分支"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const branch = expectedBranch(taskId);
|
|
161
|
+
const offBranch = listReposNotOnBranch(workdir, branch);
|
|
162
|
+
if (Array.isArray(offBranch) && offBranch.length === 0) {
|
|
163
|
+
allow();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const apm = whichApm();
|
|
167
|
+
if (!apm) {
|
|
168
|
+
deny("[apm] 未找到 apm CLI,无法确认特性分支");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (Array.isArray(offBranch) && offBranch.length > 0) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`[apm] 对话开始前确认分支 ${branch},未就绪: ${offBranch.join(", ")}\n`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = spawnSync(
|
|
178
|
+
apm,
|
|
179
|
+
["ensure-task-branch", taskId, projectId, "--cwd", workdir],
|
|
180
|
+
{
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
env: process.env,
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (result.stdout?.trim()) process.stderr.write(result.stdout);
|
|
187
|
+
if (result.stderr?.trim()) process.stderr.write(result.stderr);
|
|
188
|
+
|
|
189
|
+
if (result.status === 0) {
|
|
190
|
+
allow();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const detail = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
194
|
+
deny(
|
|
195
|
+
detail
|
|
196
|
+
? `[apm] 对话开始前确认特性分支失败\n${detail}`
|
|
197
|
+
: `[apm] 对话开始前确认特性分支失败(exit=${result.status ?? "unknown"})`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
main();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
205
|
+
deny(`[apm] before-prompt-prepare 异常: ${detail}`);
|
|
206
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"beforeSubmitPrompt": [
|
|
5
|
+
{
|
|
6
|
+
"command": "./hooks/before-prompt-prepare.cjs",
|
|
7
|
+
"timeout": 180,
|
|
8
|
+
"failClosed": true
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"stop": [
|
|
12
|
+
{
|
|
13
|
+
"command": "./hooks/after-agent-commit-repos.cjs",
|
|
14
|
+
"timeout": 180
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"apm": {
|
|
4
|
+
"url": "${env:APM_SERVER}/api/v1/mcp",
|
|
5
|
+
"headers": {
|
|
6
|
+
"Authorization": "Bearer ${env:APM_API_KEY}",
|
|
7
|
+
"x-task-id": "${env:APM_TASK_ID}",
|
|
8
|
+
"x-project-id": "${env:APM_PROJECT_ID}"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"billion-skill": {
|
|
12
|
+
"url": "https://wdxnhqgjbvib.sealosgzg.site/api/mcp",
|
|
13
|
+
"headers": {
|
|
14
|
+
"Authorization": "Bearer sk_ImOQHJZSGncproCHg6Q7ZnE_vGmylWtdYkG6TPkH2Jg"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: webide-run-retro
|
|
3
|
-
description: >-
|
|
4
|
-
Resolve a Cursor WebIDE run-id or agent-id to the local conversation
|
|
5
|
-
transcript, then produce two documents: conversation replay and defect
|
|
6
|
-
retrospective attribution (unclear requirements vs AI vs environment). Use
|
|
7
|
-
when the user pastes a run-id (run-<uuid>), agent-id (agent-<uuid>), asks to
|
|
8
|
-
查找对话, 对话回放, 复盘, 缺陷归因, or reviews WebIDE report-defect rounds.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# WebIDE Run / Agent 复盘
|
|
12
|
-
|
|
13
|
-
用户给出 `run-id` 或 `agent-id` 时:先还原对话,再只对 **report-defect**(及等价的用户缺陷反馈)做归因。回复用中文。
|
|
14
|
-
|
|
15
|
-
产出必须是 **两份独立文档**(两份 Canvas),缺一不可:
|
|
16
|
-
|
|
17
|
-
1. **对话回放** — 整段 agent 对话按轮次可浏览
|
|
18
|
-
2. **复盘归因** — 对用户提出的缺陷做主因判定与改进建议
|
|
19
|
-
|
|
20
|
-
## 1. 解析 ID
|
|
21
|
-
|
|
22
|
-
执行(不要手写等价逻辑):
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
node ~/.cursor/skills/webide-run-retro/scripts/inspect-run.mjs --id '<用户给的ID>'
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
脚本会:
|
|
29
|
-
|
|
30
|
-
- 读 `AGENT_TRANSCRIPTS` 与 `~/.cursor/projects/*/agent-transcripts`
|
|
31
|
-
- 查 SDK store `index.db`(活表 + 已删除页的字符串残留,因为 WebIDE 常 `purge` store)
|
|
32
|
-
- 读 `.apm/webide/<taskId>/cursor-agent.json`
|
|
33
|
-
- 打印 `agentId`、`matchedRunId`、`transcriptPath`、按轮次的 `conversation`
|
|
34
|
-
|
|
35
|
-
`run-<uuid>` 是一次 `send()`;`agent-<uuid>` 是整段多轮会话。有 run-id 时仍要拉**整个 agent** 对话,否则看不到原需求。
|
|
36
|
-
|
|
37
|
-
找不到 transcript 就停,说明缺了哪一层(store / transcripts / 已清理),不要编对话。
|
|
38
|
-
|
|
39
|
-
## 2. 读对话
|
|
40
|
-
|
|
41
|
-
用脚本输出的 `transcriptPath` 做补充阅读:
|
|
42
|
-
|
|
43
|
-
- 第一轮 `action=start`(或首条用户需求)= **原需求原文**
|
|
44
|
-
- `action=write-plan` / `start-develop` = AI 理解与实现范围
|
|
45
|
-
- `action=report-defect` = 待归因缺陷(`【现状】` / `【预期】`)
|
|
46
|
-
- 助手收尾句与 git 提交(`feat:` / `fix:`)用来核对手改了什么
|
|
47
|
-
|
|
48
|
-
需要核对代码时,对比特性分支相对基线的首次实现提交 vs 后续 `fix:` 提交,避免把基线旧逻辑算成 AI 新引入。
|
|
49
|
-
|
|
50
|
-
## 3. 归因(必做)
|
|
51
|
-
|
|
52
|
-
细则见 [attribution.md](attribution.md)。对每条缺陷标**一个主因**:
|
|
53
|
-
|
|
54
|
-
1. **需求没写清** — 原文没有该行为/边界/状态机
|
|
55
|
-
2. **AI 理解或实现问题** — 原文或缺陷期望已写清,但理解偏了、修不彻底、工作流做半截
|
|
56
|
-
3. **环境 / 工程** — 启动、构建、端口,与业务无关
|
|
57
|
-
|
|
58
|
-
总判断先写:**原需求有没有被理解对**。后面的缺陷经常是测试挖出未写进需求的业务,不要误判成「字段理解反了」。
|
|
59
|
-
|
|
60
|
-
给出两个动作建议:
|
|
61
|
-
|
|
62
|
-
- 需求没写清 → 培训用户(过滤包含/排除、空值样例、暂存/提交状态机、字段可空、验收反例)
|
|
63
|
-
- AI 理解/实现问题 → 补业务文档(组件默认过滤、审批暂存/startFlow、deploy 启动命令)
|
|
64
|
-
|
|
65
|
-
## 4. 输出
|
|
66
|
-
|
|
67
|
-
聊天里用 3–8 句中文给出:agent-id、任务名、回放轮次数、缺陷条数、总判断、两条动作建议。然后给出两份 Canvas 的 markdown 链接。不要用大 markdown 表代替 Canvas。
|
|
68
|
-
|
|
69
|
-
先 Read `~/.cursor/skills-cursor/canvas/SKILL.md`。两份都写在 `~/.cursor/projects/<workspace>/canvases/`,文件名 kebab-case。
|
|
70
|
-
|
|
71
|
-
### 文档 1:对话回放
|
|
72
|
-
|
|
73
|
-
文件:`<agent-short-id>-conversation.canvas.tsx`
|
|
74
|
-
|
|
75
|
-
必须包含:
|
|
76
|
-
|
|
77
|
-
- ID 映射:run-id、agent-id、task-id
|
|
78
|
-
- 任务标题与时间范围
|
|
79
|
-
- 轮次一览表:序号、时间、动作、用户意图、工具次数
|
|
80
|
-
- 对话正文:每轮可展开,用户请求摘要 + 助手收尾回复(不要堆工具调用细节)
|
|
81
|
-
- 中断后重试的同一缺陷合并说明(若有)
|
|
82
|
-
|
|
83
|
-
### 文档 2:复盘归因
|
|
84
|
-
|
|
85
|
-
文件:`<agent-short-id>-retro.canvas.tsx`
|
|
86
|
-
|
|
87
|
-
必须包含:
|
|
88
|
-
|
|
89
|
-
- 总判断:原需求有没有被理解对
|
|
90
|
-
- 主因分布(饼图或统计):需求没写清 / AI 理解或实现 / 环境
|
|
91
|
-
- 原需求实际覆盖了什么(对照评估与首轮实现)
|
|
92
|
-
- 缺陷连锁关系(若有)
|
|
93
|
-
- 逐条归因表:缺陷、主因、原需求是否覆盖、原因
|
|
94
|
-
- 两块建议:培训用户写需求 vs 补 AI 业务文档
|
|
95
|
-
|
|
96
|
-
无缺陷时:仍产出文档 2,写明「无 report-defect 轮次,不归因」,不要编缺陷。
|
|
97
|
-
|
|
98
|
-
## 5. 禁止
|
|
99
|
-
|
|
100
|
-
- 不要把对话回放和复盘归因合成一份 Canvas
|
|
101
|
-
- 不要把缺陷单里新补的期望,回溯成「原需求 AI 理解错」
|
|
102
|
-
- 不要只复述工具调用;归因要对照原文、评估结论、代码/提交
|
|
103
|
-
- 不要输出密钥、API Key、MinIO 凭证
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# 缺陷归因细则
|
|
2
|
-
|
|
3
|
-
主因只标一个。对照物永远是**该 agent 第一轮用户需求原文**,不是后续缺陷单里补充的期望。
|
|
4
|
-
|
|
5
|
-
## 三类主因
|
|
6
|
-
|
|
7
|
-
| 主因 | 何时用 | 对应动作 |
|
|
8
|
-
|------|--------|----------|
|
|
9
|
-
| 需求没写清 | 缺陷所要的行为不在原文(含空值、过滤白名单、按钮/状态机、验收反例) | 培训用户把需求写完整 |
|
|
10
|
-
| AI 理解或实现问题 | 原文已覆盖,或缺陷单已写清期望,但 AI 理解偏了、修不彻底、把工作流做半截 | 补业务/组件/工作流文档 |
|
|
11
|
-
| 环境 / 工程 | 启动失败、端口、构建命令、与业务无关 | 补 deploy/启动文档 |
|
|
12
|
-
|
|
13
|
-
不要把「测试中途新提出的暂存/提交」算成「AI 把原需求理解错」。原需求理解对、范围外行为在测试里才出现 → 需求缺口。
|
|
14
|
-
|
|
15
|
-
## 连锁缺陷
|
|
16
|
-
|
|
17
|
-
同一缺口连续爆出来时,在表里分开写,但复盘正文要标明因果链。例如:未写「无级别术式可申请」→ 选中后必填失败 → 库非空失败 → 改成暂存 → 列表/工作流半截。
|
|
18
|
-
|
|
19
|
-
只把链尾的「做半截」标成 AI 实现问题;链头标需求缺口。
|
|
20
|
-
|
|
21
|
-
## 评估阶段检查
|
|
22
|
-
|
|
23
|
-
若第一轮评估写了「无开放问题」,但测试缺陷正好落在未提问的边界上,复盘里必须点出本该问的问题,并同时改进:
|
|
24
|
-
|
|
25
|
-
- 用户需求模板(过滤包含/排除、空值样例、按钮状态机、字段可空、验收反例)
|
|
26
|
-
- AI 评估检查单(空值、默认过滤、暂存 vs 提交、流程何时启动)
|