ai-project-manage-cli 6.0.94 → 6.0.96
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 +1 -1
- package/dist/index.js +306 -42
- package/package.json +2 -3
- package/template/skills/apm-write-assumptions/SKILL.md +78 -0
- package/template/skills/{apm-write-plan → apm-write-assumptions}/assumptions-template.md +3 -1
- package/template/skills/apm-write-plan/SKILL.md +11 -20
- package/template/skills/apm-review/SKILL.md +0 -26
- package/template/skills/apm-rewrite-assumptions/SKILL.md +0 -12
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ apm daemon start
|
|
|
55
55
|
apm daemon start -f
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
PM2
|
|
58
|
+
使用全局 PM2(未安装时会自动执行 `npm install -g pm2`);同一时刻仅允许一个 connect。启动时会自动检测 CLI 更新并刷新守护配置。
|
|
59
59
|
|
|
60
60
|
`apm pull` 会自动将平台登记的**仓库项目文档**同步到 `.apm/project/`(含 `manifest.json`)。Agent 修改 `.apm/project/` 下文件后,`apm connect` 处理完消息会自动推回平台。
|
|
61
61
|
|
package/dist/index.js
CHANGED
|
@@ -2029,9 +2029,169 @@ async function runSyncProjectDocuments(options) {
|
|
|
2029
2029
|
}
|
|
2030
2030
|
|
|
2031
2031
|
// src/commands/sync-document.ts
|
|
2032
|
-
import { existsSync as existsSync10 } from "fs";
|
|
2032
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
2033
2033
|
import { basename as basename3 } from "path";
|
|
2034
2034
|
|
|
2035
|
+
// src/assumptions/local-validate.ts
|
|
2036
|
+
var NO_ASSUMPTIONS_RE = /无[,,]?\s*口径均有依据/;
|
|
2037
|
+
var BLOCK_HEADER_RE = /^####\s+(A\d+)\s*$/im;
|
|
2038
|
+
var DEV_PATTERNS = [
|
|
2039
|
+
{ pattern: /status\s*=/i, message: "\u5305\u542B status= \u7B49\u6280\u672F\u679A\u4E3E" },
|
|
2040
|
+
{ pattern: /\.(java|vue|ts|tsx|jsx|sql)\b/i, message: "\u5305\u542B\u6587\u4EF6\u8DEF\u5F84" },
|
|
2041
|
+
{ pattern: /`[^`]+`/i, message: "\u5305\u542B\u4EE3\u7801\u53CD\u5F15\u53F7" },
|
|
2042
|
+
{ pattern: /\bSELECT\b|\bINSERT\b|\bALTER\b/i, message: "\u5305\u542B SQL \u5173\u952E\u5B57" },
|
|
2043
|
+
{ pattern: /\w+\.\w+/i, message: "\u5305\u542B\u7C7B\u540D\u6216\u8868\u5B57\u6BB5\u70B9\u53F7\u5199\u6CD5" },
|
|
2044
|
+
{ pattern: /\b[a-z]+_[a-z0-9_]+\b/i, message: "\u5305\u542B snake_case \u5B57\u6BB5\u540D" }
|
|
2045
|
+
];
|
|
2046
|
+
var BUSINESS_VERBS = /提交|展示|校验|驳回|审批|允许|禁止|筛选|列表|详情|暂存|删除|新增|修改|导出|导入/;
|
|
2047
|
+
function parseOptionsBlock(text) {
|
|
2048
|
+
const options = [];
|
|
2049
|
+
for (const line of text.split("\n")) {
|
|
2050
|
+
const trimmed = line.trim();
|
|
2051
|
+
const match = trimmed.match(/^-\s*([a-zA-Z0-9_]+)[::]\s*(.+)$/);
|
|
2052
|
+
if (match) {
|
|
2053
|
+
options.push({ id: match[1], label: match[2].trim() });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return options;
|
|
2057
|
+
}
|
|
2058
|
+
function parseField(block, label) {
|
|
2059
|
+
const re = new RegExp(
|
|
2060
|
+
`-\\s*\\*\\*${label}\\*\\*(?:\\\uFF08[^\uFF09]*\\\uFF09)?[:\uFF1A]\\s*(.*)$`,
|
|
2061
|
+
"im"
|
|
2062
|
+
);
|
|
2063
|
+
const match = block.match(re);
|
|
2064
|
+
const value = match?.[1]?.trim();
|
|
2065
|
+
return value || void 0;
|
|
2066
|
+
}
|
|
2067
|
+
function parseStructuredBlock(code, block) {
|
|
2068
|
+
const pmQuestion = parseField(block, "\u95EE") ?? parseField(block, "prompt");
|
|
2069
|
+
if (!pmQuestion) return null;
|
|
2070
|
+
const optionsSection = block.match(
|
|
2071
|
+
/-\s*\*\*选项\*\*[::]?\s*\n([\s\S]*?)(?=\n-\s*\*\*|$)/i
|
|
2072
|
+
);
|
|
2073
|
+
const options = optionsSection ? parseOptionsBlock(optionsSection[1]) : [];
|
|
2074
|
+
return {
|
|
2075
|
+
code,
|
|
2076
|
+
questionId: code,
|
|
2077
|
+
pmQuestion,
|
|
2078
|
+
context: parseField(block, "\u573A\u666F"),
|
|
2079
|
+
options,
|
|
2080
|
+
technicalNote: parseField(block, "\u7814\u53D1\u5907\u6CE8"),
|
|
2081
|
+
reason: parseField(block, "\u539F\u56E0")
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function hasNoAssumptionsMarker(markdown) {
|
|
2085
|
+
return NO_ASSUMPTIONS_RE.test(markdown.replace(/\r\n/g, "\n"));
|
|
2086
|
+
}
|
|
2087
|
+
function countAssumptionMarkers(markdown) {
|
|
2088
|
+
const normalized = markdown.replace(/\r\n/g, "\n").trim();
|
|
2089
|
+
if (NO_ASSUMPTIONS_RE.test(normalized)) return 0;
|
|
2090
|
+
const codes = /* @__PURE__ */ new Set();
|
|
2091
|
+
for (const line of normalized.split("\n")) {
|
|
2092
|
+
const match = line.trim().match(/^####\s+(A\d+)\s*$/);
|
|
2093
|
+
if (match) codes.add(match[1]);
|
|
2094
|
+
}
|
|
2095
|
+
return codes.size;
|
|
2096
|
+
}
|
|
2097
|
+
function parseAssumptionsDocument(markdown, _source) {
|
|
2098
|
+
const normalized = markdown.replace(/\r\n/g, "\n").trim();
|
|
2099
|
+
if (!normalized || NO_ASSUMPTIONS_RE.test(normalized)) {
|
|
2100
|
+
return [];
|
|
2101
|
+
}
|
|
2102
|
+
const results = [];
|
|
2103
|
+
const blocks = normalized.split(/(?=^####\s+A\d+\s*$)/im);
|
|
2104
|
+
for (const block of blocks) {
|
|
2105
|
+
const headerMatch = block.match(BLOCK_HEADER_RE);
|
|
2106
|
+
if (!headerMatch) continue;
|
|
2107
|
+
const parsed = parseStructuredBlock(headerMatch[1], block);
|
|
2108
|
+
if (parsed) results.push(parsed);
|
|
2109
|
+
}
|
|
2110
|
+
return results;
|
|
2111
|
+
}
|
|
2112
|
+
function validateAssumptionForPm(assumption) {
|
|
2113
|
+
const issues = [];
|
|
2114
|
+
const questionForCheck = assumption.pmQuestion.replace(/`[^`]*`/g, "").trim();
|
|
2115
|
+
if (!assumption.pmQuestion?.trim()) {
|
|
2116
|
+
issues.push("\u7F3A\u5C11\u4E1A\u52A1\u95EE\u53E5");
|
|
2117
|
+
} else if (questionForCheck.length < 6) {
|
|
2118
|
+
issues.push("\u95EE\u53E5\u8FC7\u77ED");
|
|
2119
|
+
} else if (!BUSINESS_VERBS.test(questionForCheck)) {
|
|
2120
|
+
issues.push("\u95EE\u53E5\u7F3A\u5C11\u53EF\u7406\u89E3\u7684\u4E1A\u52A1\u52A8\u8BCD");
|
|
2121
|
+
}
|
|
2122
|
+
for (const { pattern, message } of DEV_PATTERNS) {
|
|
2123
|
+
if (pattern.test(questionForCheck)) {
|
|
2124
|
+
issues.push(message);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (!assumption.context?.trim()) {
|
|
2128
|
+
issues.push("\u7F3A\u5C11\u573A\u666F\u8BF4\u660E");
|
|
2129
|
+
}
|
|
2130
|
+
if (!assumption.options || assumption.options.length < 2) {
|
|
2131
|
+
issues.push("\u9009\u9879\u5C11\u4E8E 2 \u9879");
|
|
2132
|
+
}
|
|
2133
|
+
return { ok: issues.length === 0, issues };
|
|
2134
|
+
}
|
|
2135
|
+
function printAssumptionValidationResult(result) {
|
|
2136
|
+
for (const warning of result.warnings) {
|
|
2137
|
+
console.warn(`[apm] \u5047\u8BBE\u6821\u9A8C\u544A\u8B66: ${warning}`);
|
|
2138
|
+
}
|
|
2139
|
+
for (const item of result.issues) {
|
|
2140
|
+
console.error(
|
|
2141
|
+
`[apm] \u5047\u8BBE ${item.code} \u672A\u901A\u8FC7 PM \u6821\u9A8C: ${item.issues.join("\uFF1B")}`
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
function resolveAssumptionSourceFromFileName(fileName) {
|
|
2146
|
+
const key = fileName.replace(/\.md$/i, "").toUpperCase();
|
|
2147
|
+
if (key === "FRONTEND-ASSUMPTIONS") {
|
|
2148
|
+
return "FRONTEND_PLAN" /* FRONTEND_PLAN */;
|
|
2149
|
+
}
|
|
2150
|
+
if (key === "BACKEND-ASSUMPTIONS") {
|
|
2151
|
+
return "BACKEND_PLAN" /* BACKEND_PLAN */;
|
|
2152
|
+
}
|
|
2153
|
+
return null;
|
|
2154
|
+
}
|
|
2155
|
+
function validateAssumptionsMarkdown(markdown, source) {
|
|
2156
|
+
const markerCount = countAssumptionMarkers(markdown);
|
|
2157
|
+
const explicitEmpty = hasNoAssumptionsMarker(markdown);
|
|
2158
|
+
if (explicitEmpty && markerCount === 0) {
|
|
2159
|
+
return {
|
|
2160
|
+
ok: true,
|
|
2161
|
+
markerCount: 0,
|
|
2162
|
+
parsedCount: 0,
|
|
2163
|
+
issues: [],
|
|
2164
|
+
warnings: []
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
const parsed = parseAssumptionsDocument(markdown, source);
|
|
2168
|
+
const issues = [];
|
|
2169
|
+
const warnings = [];
|
|
2170
|
+
for (const item of parsed) {
|
|
2171
|
+
const result = validateAssumptionForPm(item);
|
|
2172
|
+
if (!result.ok) {
|
|
2173
|
+
issues.push({ code: item.code, issues: result.issues });
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (markerCount > 0 && parsed.length === 0) {
|
|
2177
|
+
warnings.push(
|
|
2178
|
+
`\u68C0\u6D4B\u5230 ${markerCount} \u6761\u5047\u8BBE\u6807\u8BB0\uFF0C\u4F46\u683C\u5F0F\u65E0\u6CD5\u89E3\u6790\u3002\u8BF7\u4F7F\u7528 #### A1 + \u95EE/\u573A\u666F/\u9009\u9879 \u7ED3\u6784\u3002`
|
|
2179
|
+
);
|
|
2180
|
+
} else if (markerCount > parsed.length) {
|
|
2181
|
+
warnings.push(
|
|
2182
|
+
`\u68C0\u6D4B\u5230 ${markerCount} \u6761\u5047\u8BBE\u6807\u8BB0\uFF0C\u4EC5\u89E3\u6790 ${parsed.length} \u6761\u3002`
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
const ok = issues.length === 0 && warnings.length === 0 && (markerCount === 0 || parsed.length > 0);
|
|
2186
|
+
return {
|
|
2187
|
+
ok,
|
|
2188
|
+
markerCount,
|
|
2189
|
+
parsedCount: parsed.length,
|
|
2190
|
+
issues,
|
|
2191
|
+
warnings
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2035
2195
|
// src/commands/sync-session-documents.ts
|
|
2036
2196
|
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync8 } from "fs";
|
|
2037
2197
|
import { join as join12 } from "path";
|
|
@@ -2116,6 +2276,19 @@ async function runSyncDocument(sessionId, options) {
|
|
|
2116
2276
|
);
|
|
2117
2277
|
process.exit(1);
|
|
2118
2278
|
}
|
|
2279
|
+
const fileName = basename3(absPath);
|
|
2280
|
+
const assumptionSource = resolveAssumptionSourceFromFileName(fileName);
|
|
2281
|
+
if (assumptionSource) {
|
|
2282
|
+
const content = readFileSync9(absPath, "utf8");
|
|
2283
|
+
const validation = validateAssumptionsMarkdown(content, assumptionSource);
|
|
2284
|
+
if (!validation.ok) {
|
|
2285
|
+
printAssumptionValidationResult(validation);
|
|
2286
|
+
console.error(
|
|
2287
|
+
"[apm] \u5047\u8BBE sync \u672A\u901A\u8FC7\uFF0C\u5DF2\u4E2D\u6B62\u4E0A\u4F20\u3002\u8BF7\u6309 apm-write-assumptions \u6280\u80FD\u4FEE\u6B63\u540E\u91CD\u8BD5"
|
|
2288
|
+
);
|
|
2289
|
+
process.exit(1);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2119
2292
|
const cfg = await ensureLoggedConfig();
|
|
2120
2293
|
const api = createApmApiClient(cfg);
|
|
2121
2294
|
const docsDir = sessionDocsDir(trimmedSessionId);
|
|
@@ -3005,7 +3178,7 @@ ${JSON.stringify(event, null, 2)}
|
|
|
3005
3178
|
}
|
|
3006
3179
|
|
|
3007
3180
|
// src/commands/connect/agent-session-registry.ts
|
|
3008
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as
|
|
3181
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "node:fs";
|
|
3009
3182
|
import { dirname as dirname4, resolve as resolve3 } from "node:path";
|
|
3010
3183
|
function registryPath(workdir, sessionId) {
|
|
3011
3184
|
return resolve3(workdir, ".apm", "sessions", sessionId, "cursor-agents.json");
|
|
@@ -3015,7 +3188,7 @@ function readRegistry(path13) {
|
|
|
3015
3188
|
return {};
|
|
3016
3189
|
}
|
|
3017
3190
|
try {
|
|
3018
|
-
const parsed = JSON.parse(
|
|
3191
|
+
const parsed = JSON.parse(readFileSync10(path13, "utf8"));
|
|
3019
3192
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3020
3193
|
const result = {};
|
|
3021
3194
|
for (const [key, value] of Object.entries(
|
|
@@ -3483,7 +3656,7 @@ async function ensureMessageHasReply(cfg, sessionId, messageId, fallback) {
|
|
|
3483
3656
|
}
|
|
3484
3657
|
|
|
3485
3658
|
// src/commands/connect/cli-version-sync.ts
|
|
3486
|
-
import { existsSync as existsSync12, readFileSync as
|
|
3659
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
3487
3660
|
import { join as join13 } from "path";
|
|
3488
3661
|
var CLI_VERSION_FILE = ".cli-version.json";
|
|
3489
3662
|
function manifestPath(apmDir) {
|
|
@@ -3496,7 +3669,7 @@ function loadManifest3(apmDir) {
|
|
|
3496
3669
|
}
|
|
3497
3670
|
try {
|
|
3498
3671
|
const parsed = JSON.parse(
|
|
3499
|
-
|
|
3672
|
+
readFileSync11(path13, "utf8")
|
|
3500
3673
|
);
|
|
3501
3674
|
if (parsed?.version === 1 && typeof parsed.cliVersion === "string" && parsed.cliVersion.trim()) {
|
|
3502
3675
|
return parsed;
|
|
@@ -3585,7 +3758,7 @@ function createRunSlotPool(maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
|
3585
3758
|
}
|
|
3586
3759
|
|
|
3587
3760
|
// src/commands/connect-lock.ts
|
|
3588
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as
|
|
3761
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync12 } from "fs";
|
|
3589
3762
|
import { join as join14 } from "path";
|
|
3590
3763
|
var CONNECT_LOCK_PATH = join14(APM_CONFIG_DIR, "connect.lock");
|
|
3591
3764
|
function isProcessAlive(pid) {
|
|
@@ -3600,7 +3773,7 @@ function isProcessAlive(pid) {
|
|
|
3600
3773
|
function readConnectLock() {
|
|
3601
3774
|
if (!existsSync13(CONNECT_LOCK_PATH)) return null;
|
|
3602
3775
|
try {
|
|
3603
|
-
const raw =
|
|
3776
|
+
const raw = readFileSync12(CONNECT_LOCK_PATH, "utf8");
|
|
3604
3777
|
const parsed = JSON.parse(raw);
|
|
3605
3778
|
if (typeof parsed.pid !== "number" || parsed.mode !== "foreground" && parsed.mode !== "pm2" || typeof parsed.startedAt !== "string") {
|
|
3606
3779
|
return null;
|
|
@@ -3656,7 +3829,6 @@ function forceReleaseConnectLock() {
|
|
|
3656
3829
|
// src/commands/daemon.ts
|
|
3657
3830
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3658
3831
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3659
|
-
import { createRequire } from "node:module";
|
|
3660
3832
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
3661
3833
|
import { join as join15 } from "path";
|
|
3662
3834
|
var PM2_APP_NAME = "apm-connect";
|
|
@@ -3699,11 +3871,110 @@ function formatConnectPm2EcosystemFile(ecosystem) {
|
|
|
3699
3871
|
return `module.exports = ${JSON.stringify(ecosystem, null, 2)};
|
|
3700
3872
|
`;
|
|
3701
3873
|
}
|
|
3702
|
-
function resolvePm2Bin() {
|
|
3703
|
-
const require2 = createRequire(import.meta.url);
|
|
3704
|
-
return require2.resolve("pm2/bin/pm2");
|
|
3705
|
-
}
|
|
3706
3874
|
var useNpmShell2 = process.platform === "win32";
|
|
3875
|
+
function runNpm2(args, options = {}) {
|
|
3876
|
+
return spawnSync2(useNpmShell2 ? "npm.cmd" : "npm", args, {
|
|
3877
|
+
...options,
|
|
3878
|
+
shell: useNpmShell2
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
function verifyPm2Bin(pm2Bin) {
|
|
3882
|
+
if (!pm2Bin.trim() || !existsSync14(pm2Bin)) {
|
|
3883
|
+
return false;
|
|
3884
|
+
}
|
|
3885
|
+
const result = spawnSync2(pm2Bin, ["--version"], {
|
|
3886
|
+
encoding: "utf8",
|
|
3887
|
+
shell: useNpmShell2,
|
|
3888
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3889
|
+
timeout: 15e3
|
|
3890
|
+
});
|
|
3891
|
+
return !result.error && result.status === 0;
|
|
3892
|
+
}
|
|
3893
|
+
function findGlobalPm2() {
|
|
3894
|
+
const candidates = [];
|
|
3895
|
+
const whichResult = spawnSync2(useNpmShell2 ? "where" : "which", ["pm2"], {
|
|
3896
|
+
encoding: "utf8",
|
|
3897
|
+
shell: useNpmShell2,
|
|
3898
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3899
|
+
});
|
|
3900
|
+
if (whichResult.status === 0) {
|
|
3901
|
+
const lines = whichResult.stdout?.toString().trim().split(/\r?\n/) ?? [];
|
|
3902
|
+
for (const line of lines) {
|
|
3903
|
+
const candidate = line.trim();
|
|
3904
|
+
if (candidate) {
|
|
3905
|
+
candidates.push(candidate);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
const binResult = runNpm2(["bin", "-g"], {
|
|
3910
|
+
encoding: "utf8",
|
|
3911
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3912
|
+
});
|
|
3913
|
+
if (binResult.status === 0) {
|
|
3914
|
+
const globalBin = binResult.stdout?.toString().trim();
|
|
3915
|
+
if (globalBin) {
|
|
3916
|
+
candidates.push(join15(globalBin, useNpmShell2 ? "pm2.cmd" : "pm2"));
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3920
|
+
for (const candidate of candidates) {
|
|
3921
|
+
if (seen.has(candidate)) continue;
|
|
3922
|
+
seen.add(candidate);
|
|
3923
|
+
if (verifyPm2Bin(candidate)) {
|
|
3924
|
+
return candidate;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
return null;
|
|
3928
|
+
}
|
|
3929
|
+
function installGlobalPm2() {
|
|
3930
|
+
console.log("[apm] \u672A\u68C0\u6D4B\u5230\u5168\u5C40 pm2\uFF0C\u6B63\u5728\u5B89\u88C5 npm install -g pm2 \u2026");
|
|
3931
|
+
const result = runNpm2(["install", "-g", "pm2"], {
|
|
3932
|
+
encoding: "utf8",
|
|
3933
|
+
stdio: "inherit"
|
|
3934
|
+
});
|
|
3935
|
+
if (result.status !== 0) {
|
|
3936
|
+
console.error("[apm] \u5B89\u88C5 pm2 \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: npm install -g pm2");
|
|
3937
|
+
process.exit(result.status ?? 1);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
function readPm2Version(pm2Bin) {
|
|
3941
|
+
const result = spawnSync2(pm2Bin, ["--version"], {
|
|
3942
|
+
encoding: "utf8",
|
|
3943
|
+
shell: useNpmShell2,
|
|
3944
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3945
|
+
timeout: 15e3
|
|
3946
|
+
});
|
|
3947
|
+
if (result.error || result.status !== 0) {
|
|
3948
|
+
return null;
|
|
3949
|
+
}
|
|
3950
|
+
return result.stdout?.toString().trim() || null;
|
|
3951
|
+
}
|
|
3952
|
+
function logPm2Ready(pm2Bin, installed2) {
|
|
3953
|
+
const version = readPm2Version(pm2Bin);
|
|
3954
|
+
const versionSuffix = version ? ` ${version}` : "";
|
|
3955
|
+
if (installed2) {
|
|
3956
|
+
console.log(`[apm] \u5168\u5C40 pm2 \u5DF2\u5B89\u88C5${versionSuffix}: ${pm2Bin}`);
|
|
3957
|
+
} else {
|
|
3958
|
+
console.log(`[apm] \u5DF2\u68C0\u6D4B\u5230\u5168\u5C40 pm2${versionSuffix}: ${pm2Bin}`);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
function ensureGlobalPm2() {
|
|
3962
|
+
const existing = findGlobalPm2();
|
|
3963
|
+
if (existing) {
|
|
3964
|
+
logPm2Ready(existing, false);
|
|
3965
|
+
return existing;
|
|
3966
|
+
}
|
|
3967
|
+
installGlobalPm2();
|
|
3968
|
+
const installed2 = findGlobalPm2();
|
|
3969
|
+
if (!installed2) {
|
|
3970
|
+
console.error(
|
|
3971
|
+
"[apm] \u5B89\u88C5 pm2 \u540E\u4ECD\u65E0\u6CD5\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: npm install -g pm2"
|
|
3972
|
+
);
|
|
3973
|
+
process.exit(1);
|
|
3974
|
+
}
|
|
3975
|
+
logPm2Ready(installed2, true);
|
|
3976
|
+
return installed2;
|
|
3977
|
+
}
|
|
3707
3978
|
function resolveApmEntryPath(entryArg = process.argv[1]) {
|
|
3708
3979
|
const fromArgv = entryArg?.trim();
|
|
3709
3980
|
if (fromArgv && existsSync14(fromArgv)) {
|
|
@@ -3733,20 +4004,13 @@ function isRunningUnderPm2() {
|
|
|
3733
4004
|
return process.env.name === PM2_APP_NAME && process.env.pm_id !== void 0;
|
|
3734
4005
|
}
|
|
3735
4006
|
function runPm2(args, options) {
|
|
3736
|
-
|
|
3737
|
-
try {
|
|
3738
|
-
pm2Bin = resolvePm2Bin();
|
|
3739
|
-
} catch {
|
|
3740
|
-
console.error(
|
|
3741
|
-
"[apm] \u672A\u627E\u5230\u5185\u7F6E pm2\uFF0C\u8BF7\u91CD\u65B0\u5B89\u88C5 apm CLI\uFF1Anpm install -g ai-project-manage-cli@latest"
|
|
3742
|
-
);
|
|
3743
|
-
process.exit(1);
|
|
3744
|
-
}
|
|
4007
|
+
const pm2Bin = ensureGlobalPm2();
|
|
3745
4008
|
const spawnOptions = {
|
|
3746
4009
|
encoding: "utf8",
|
|
3747
|
-
stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
4010
|
+
stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
4011
|
+
shell: useNpmShell2
|
|
3748
4012
|
};
|
|
3749
|
-
const result = spawnSync2(
|
|
4013
|
+
const result = spawnSync2(pm2Bin, args, spawnOptions);
|
|
3750
4014
|
if (result.error) {
|
|
3751
4015
|
console.error("[apm] \u6267\u884C pm2 \u5931\u8D25:", result.error.message);
|
|
3752
4016
|
process.exit(1);
|
|
@@ -3760,15 +4024,14 @@ function runPm2(args, options) {
|
|
|
3760
4024
|
}
|
|
3761
4025
|
}
|
|
3762
4026
|
function runPm2Json(args) {
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
pm2Bin = resolvePm2Bin();
|
|
3766
|
-
} catch {
|
|
4027
|
+
const pm2Bin = findGlobalPm2();
|
|
4028
|
+
if (!pm2Bin) {
|
|
3767
4029
|
return "[]";
|
|
3768
4030
|
}
|
|
3769
|
-
const result = spawnSync2(
|
|
4031
|
+
const result = spawnSync2(pm2Bin, args, {
|
|
3770
4032
|
encoding: "utf8",
|
|
3771
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4033
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4034
|
+
shell: useNpmShell2
|
|
3772
4035
|
});
|
|
3773
4036
|
if (result.status !== 0) return "[]";
|
|
3774
4037
|
return result.stdout?.toString() ?? "[]";
|
|
@@ -3865,6 +4128,7 @@ async function prepareEcosystem(server) {
|
|
|
3865
4128
|
async function runDaemonStart(options) {
|
|
3866
4129
|
assertConnectNotRunning();
|
|
3867
4130
|
await runUpdate();
|
|
4131
|
+
ensureGlobalPm2();
|
|
3868
4132
|
const cfg = await prepareEcosystem(options.server);
|
|
3869
4133
|
runPm2(["start", PM2_ECOSYSTEM_PATH, "--update-env"], { inherit: true });
|
|
3870
4134
|
console.log(
|
|
@@ -4534,7 +4798,7 @@ import { spawnSync as spawnSync5 } from "node:child_process";
|
|
|
4534
4798
|
import path5 from "node:path";
|
|
4535
4799
|
|
|
4536
4800
|
// src/commands/deploy/internal/apm-config.ts
|
|
4537
|
-
import { existsSync as existsSync15, readFileSync as
|
|
4801
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
4538
4802
|
import { homedir as homedir2 } from "node:os";
|
|
4539
4803
|
import { join as join16, resolve as resolve4 } from "node:path";
|
|
4540
4804
|
function loadApmConfig(options) {
|
|
@@ -4547,7 +4811,7 @@ function loadApmConfig(options) {
|
|
|
4547
4811
|
process.exit(1);
|
|
4548
4812
|
}
|
|
4549
4813
|
try {
|
|
4550
|
-
const raw =
|
|
4814
|
+
const raw = readFileSync13(p, "utf8");
|
|
4551
4815
|
return JSON.parse(raw);
|
|
4552
4816
|
} catch (e) {
|
|
4553
4817
|
console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
|
|
@@ -4736,7 +5000,7 @@ function readMavenLocalRepoFromSettings() {
|
|
|
4736
5000
|
return null;
|
|
4737
5001
|
}
|
|
4738
5002
|
try {
|
|
4739
|
-
const xml =
|
|
5003
|
+
const xml = readFileSync13(settingsPath, "utf8");
|
|
4740
5004
|
const match = xml.match(
|
|
4741
5005
|
/<localRepository>\s*([^<]+?)\s*<\/localRepository>/
|
|
4742
5006
|
);
|
|
@@ -4878,7 +5142,7 @@ var DeployExecutionError = class extends Error {
|
|
|
4878
5142
|
};
|
|
4879
5143
|
|
|
4880
5144
|
// src/commands/deploy/internal/wisdom-auto-deploy.ts
|
|
4881
|
-
import { existsSync as existsSync17, readFileSync as
|
|
5145
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15, statSync as statSync7 } from "node:fs";
|
|
4882
5146
|
import path4 from "node:path";
|
|
4883
5147
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4884
5148
|
|
|
@@ -4887,7 +5151,7 @@ import {
|
|
|
4887
5151
|
existsSync as existsSync16,
|
|
4888
5152
|
mkdirSync as mkdirSync8,
|
|
4889
5153
|
readdirSync as readdirSync5,
|
|
4890
|
-
readFileSync as
|
|
5154
|
+
readFileSync as readFileSync14,
|
|
4891
5155
|
statSync as statSync6,
|
|
4892
5156
|
writeFileSync as writeFileSync14
|
|
4893
5157
|
} from "node:fs";
|
|
@@ -5157,7 +5421,7 @@ function loadManifest4() {
|
|
|
5157
5421
|
if (!existsSync16(manifestPath2)) {
|
|
5158
5422
|
return {};
|
|
5159
5423
|
}
|
|
5160
|
-
return JSON.parse(
|
|
5424
|
+
return JSON.parse(readFileSync14(manifestPath2, "utf8"));
|
|
5161
5425
|
}
|
|
5162
5426
|
function saveManifest4(manifest) {
|
|
5163
5427
|
const dir = deployCacheDir();
|
|
@@ -5223,7 +5487,7 @@ async function createUpdatePackage(entries, packageName) {
|
|
|
5223
5487
|
log(`\u521B\u5EFA\u66F4\u65B0\u5305: ${path3.basename(zipPath)}\uFF08${entries.length} \u4E2A\u6587\u4EF6\uFF09`);
|
|
5224
5488
|
const zip = new JSZip2();
|
|
5225
5489
|
for (const entry of entries) {
|
|
5226
|
-
const content =
|
|
5490
|
+
const content = readFileSync14(entry.path);
|
|
5227
5491
|
zip.file(entry.arcname, content);
|
|
5228
5492
|
log(` \u6253\u5305: ${entry.arcname} (${entry.reason})`);
|
|
5229
5493
|
}
|
|
@@ -5789,7 +6053,7 @@ function readPackageScripts(cwd) {
|
|
|
5789
6053
|
return {};
|
|
5790
6054
|
}
|
|
5791
6055
|
try {
|
|
5792
|
-
const raw =
|
|
6056
|
+
const raw = readFileSync15(pkgPath, "utf8");
|
|
5793
6057
|
const parsed = JSON.parse(raw);
|
|
5794
6058
|
return parsed.scripts ?? {};
|
|
5795
6059
|
} catch {
|
|
@@ -6313,7 +6577,7 @@ import path9 from "node:path";
|
|
|
6313
6577
|
import Docker from "dockerode";
|
|
6314
6578
|
|
|
6315
6579
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
|
|
6316
|
-
import { existsSync as existsSync18, readFileSync as
|
|
6580
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
|
|
6317
6581
|
import path6 from "node:path";
|
|
6318
6582
|
function asOptionalTlsBuffer(value) {
|
|
6319
6583
|
if (typeof value !== "string") {
|
|
@@ -6326,7 +6590,7 @@ function asOptionalTlsBuffer(value) {
|
|
|
6326
6590
|
return void 0;
|
|
6327
6591
|
}
|
|
6328
6592
|
if (existsSync18(normalized)) {
|
|
6329
|
-
return
|
|
6593
|
+
return readFileSync16(normalized);
|
|
6330
6594
|
}
|
|
6331
6595
|
const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
|
|
6332
6596
|
if (looksLikePath) {
|
|
@@ -6536,7 +6800,7 @@ var DockerodeClient = class {
|
|
|
6536
6800
|
var createDockerodeClient = (config) => new DockerodeClient(config);
|
|
6537
6801
|
|
|
6538
6802
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
|
|
6539
|
-
import { existsSync as existsSync19, readFileSync as
|
|
6803
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
|
|
6540
6804
|
import path7 from "node:path";
|
|
6541
6805
|
function stripSurroundingQuotes(value) {
|
|
6542
6806
|
const t = value.trim();
|
|
@@ -6556,7 +6820,7 @@ function loadEnvFromFile(envFilePath) {
|
|
|
6556
6820
|
if (!existsSync19(targetPath) || !statSync8(targetPath).isFile()) {
|
|
6557
6821
|
return {};
|
|
6558
6822
|
}
|
|
6559
|
-
const raw =
|
|
6823
|
+
const raw = readFileSync17(targetPath, "utf-8");
|
|
6560
6824
|
const result = {};
|
|
6561
6825
|
for (const line of raw.split(/\r?\n/)) {
|
|
6562
6826
|
const normalized = line.trim();
|
|
@@ -7217,7 +7481,7 @@ function buildProgram() {
|
|
|
7217
7481
|
"\u8FDE\u63A5\u5E73\u53F0 WebSocket\uFF08/ws/agent\uFF09\uFF0C\u7EF4\u6301\u5FC3\u8DF3\u5E76\u5904\u7406\u4E0B\u884C message\uFF08TYPING \u2192 Cursor \u2192 SUCCESS/FAILED\uFF09\uFF1B\u542F\u52A8\u524D\u81EA\u52A8 apm update \u5230\u5F53\u524D\u5927\u7248\u672C\u6700\u65B0\u7248"
|
|
7218
7482
|
).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").option(
|
|
7219
7483
|
"--daemon",
|
|
7220
|
-
"\u540E\u53F0\u5B88\u62A4\u8FD0\u884C\uFF08\
|
|
7484
|
+
"\u540E\u53F0\u5B88\u62A4\u8FD0\u884C\uFF08\u5168\u5C40 PM2\uFF0C\u65AD\u7EBF\u6216\u9000\u51FA\u540E\u81EA\u52A8\u91CD\u542F\uFF1B\u63A8\u8350\u7528\u4E8E\u957F\u671F\u5728\u7EBF\uFF09"
|
|
7221
7485
|
).option("-f, --follow", "\u4E0E --daemon \u5408\u7528\uFF1A\u542F\u52A8\u540E\u5728\u5F53\u524D\u7EC8\u7AEF\u5B9E\u65F6\u8DDF\u8E2A\u65E5\u5FD7").action(
|
|
7222
7486
|
async (opts) => {
|
|
7223
7487
|
if (opts.follow && !opts.daemon) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-project-manage-cli",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.96",
|
|
4
4
|
"description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"dockerode": "~5.0.0",
|
|
45
45
|
"ssh2": "~1.16.0",
|
|
46
46
|
"ssh2-sftp-client": "~12.0.1",
|
|
47
|
-
"jszip": "~3.10.1"
|
|
48
|
-
"pm2": "~5.4.3"
|
|
47
|
+
"jszip": "~3.10.1"
|
|
49
48
|
}
|
|
50
49
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# apm-write-assumptions:编写并同步待确认口径
|
|
2
|
+
|
|
3
|
+
## 何时使用
|
|
4
|
+
|
|
5
|
+
`apm-write-plan` 步骤 3 编写 `*-ASSUMPTIONS.md` 时;或需重写/补写假设文档时。
|
|
6
|
+
|
|
7
|
+
| 角色 | 产出 |
|
|
8
|
+
| ---- | ------------------------------ |
|
|
9
|
+
| 前端 | `docs/FRONTEND-ASSUMPTIONS.md` |
|
|
10
|
+
| 后端 | `docs/BACKEND-ASSUMPTIONS.md` |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 工作流程
|
|
15
|
+
|
|
16
|
+
### 1. 按模板编写
|
|
17
|
+
|
|
18
|
+
1. **Read** `.apm/skills/apm-write-assumptions/assumptions-template.md`
|
|
19
|
+
2. **Write** 本端 `docs/*-ASSUMPTIONS.md`(写作时对照下方 **PM 校验规则**)
|
|
20
|
+
3. 无假设时正文仅一行:`无,口径均有依据。`
|
|
21
|
+
|
|
22
|
+
### 2. 直接 sync
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
apm sync-document <sessionId> --file FRONTEND-ASSUMPTIONS.md
|
|
26
|
+
# 或 BACKEND-ASSUMPTIONS.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**成功**须看到:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
[apm] 假设解析: 标记 N 条,入库 N 条(PM 可确认 N 条)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
无假设时无 `假设解析` 行,属正常。
|
|
36
|
+
|
|
37
|
+
### 3. 报错则改完重 sync
|
|
38
|
+
|
|
39
|
+
`sync-document` 失败时会打印具体假设编号与原因(如 `假设 A2 未通过 PM 校验: …`)。
|
|
40
|
+
|
|
41
|
+
1. 按报错修正对应 `A*` 的 **问 / 场景 / 选项**(规则见下)
|
|
42
|
+
2. 再次执行步骤 2
|
|
43
|
+
3. **禁止**在未成功 sync 前 @ 项目经理、sync PLAN、或声称「请在 Web 待确认」
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## PM 校验规则(与服务端一致)
|
|
48
|
+
|
|
49
|
+
每条假设(`#### A1` …)必须包含:
|
|
50
|
+
|
|
51
|
+
| 字段 | 要求 |
|
|
52
|
+
| ------------ | --------------------------------------------------------------- |
|
|
53
|
+
| **问** | 业务语言;≥6 字;含下列业务动词之一;禁止字段名/路径/SQL/反引号 |
|
|
54
|
+
| **场景** | 非空 |
|
|
55
|
+
| **选项** | ≥2 项,格式 `- opt_id:说明` |
|
|
56
|
+
| **研发备注** | 可选;技术细节放这里,**不要**写进「问」 |
|
|
57
|
+
|
|
58
|
+
**问句须含以下动词之一**:
|
|
59
|
+
|
|
60
|
+
`提交` `展示` `校验` `驳回` `审批` `允许` `禁止` `筛选` `列表` `详情` `暂存` `删除` `新增` `修改` `导出` `导入`
|
|
61
|
+
|
|
62
|
+
**问句禁止**:`status=`、文件扩展名、反引号代码、SQL、`类.字段`、snake_case 字段名。
|
|
63
|
+
|
|
64
|
+
### 正反例
|
|
65
|
+
|
|
66
|
+
| 假设 | 问句 | 结果 |
|
|
67
|
+
| ------- | ---------------------------------------------- | ---------------------- |
|
|
68
|
+
| A2 | 编辑已有记录时,「落实人员」应如何处理? | **失败**(无业务动词) |
|
|
69
|
+
| A2 修正 | 编辑已有记录时,是否**允许修改**「落实人员」? | 通过 |
|
|
70
|
+
| A3 | 「落实人员」可选择的范围是? | **失败** |
|
|
71
|
+
| A3 修正 | 「落实人员」下拉**列表**应**展示**哪些人员? | 通过 |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 与其他技能
|
|
76
|
+
|
|
77
|
+
- 由 **`apm-write-plan`** 在 sync PLAN 前调用本技能
|
|
78
|
+
- PM 确认后由 **`apm-update-plan`** 更新 PLAN
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> 本文件由 apm sync-document 写入;PM 确认后服务端回写「状态/确认结果」。
|
|
4
4
|
> **禁止**手动编辑;已确认条目 AI 禁止修改。
|
|
5
|
+
> 编写与校验规则见 `.apm/skills/apm-write-assumptions/SKILL.md`。
|
|
5
6
|
|
|
6
7
|
#### A1
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
- **问**:(业务语言问句;须含:提交/展示/校验/驳回/审批/允许/禁止/筛选/列表/详情/暂存/删除/新增/修改/导出/导入 之一)
|
|
8
10
|
- **场景**:(需求未说明的背景)
|
|
9
11
|
- **选项**:
|
|
10
12
|
- opt_a:(选项说明)
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
前端 / 后端工程师在任务启动后**直接读原始需求写实现计划**,不经过 PRD。
|
|
6
6
|
|
|
7
|
-
| 角色 | 产出文档
|
|
8
|
-
| ---- |
|
|
9
|
-
| 后端 | `BACKEND-PLAN.md` + `API.md`
|
|
7
|
+
| 角色 | 产出文档 | 路径 |
|
|
8
|
+
| ---- | ---------------------------------------------- | ------------------------------------------------------- |
|
|
9
|
+
| 后端 | `BACKEND-PLAN.md` + `API.md` | `docs/BACKEND-PLAN.md`、`docs/API.md` |
|
|
10
10
|
| 前端 | `FRONTEND-PLAN.md` + `FRONTEND-ASSUMPTIONS.md` | `docs/FRONTEND-PLAN.md`、`docs/FRONTEND-ASSUMPTIONS.md` |
|
|
11
|
-
| 后端 | 同上
|
|
11
|
+
| 后端 | 同上 | `docs/BACKEND-ASSUMPTIONS.md` |
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -27,27 +27,18 @@
|
|
|
27
27
|
2. 代码调研预算:**最多 15 个文件**。
|
|
28
28
|
3. 口径不清 → 记入 **ASSUMPTIONS 文件**,禁止编造。
|
|
29
29
|
|
|
30
|
-
### 步骤 3
|
|
30
|
+
### 步骤 3:写计划与假设
|
|
31
31
|
|
|
32
32
|
1. **Read** `.apm/skills/apm-write-plan/plan-template.md`,**Write** 本端 `*-PLAN.md`(**不含**假设章节)。
|
|
33
|
-
2. **Read**
|
|
34
|
-
3. 无假设时在 ASSUMPTIONS 中写「无,口径均有依据」。
|
|
33
|
+
2. **Read** 并完整执行 **`.apm/skills/apm-write-assumptions/SKILL.md`**(编写 `*-ASSUMPTIONS.md` → sync;报错则改完重 sync)。
|
|
35
34
|
|
|
36
|
-
### 步骤
|
|
35
|
+
### 步骤 4:同步 PLAN 与回复
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
1. `apm sync-document --file FRONTEND-PLAN.md`(或 BACKEND)。
|
|
38
|
+
2. **有假设且 ASSUMPTIONS 已成功 sync**:@项目经理 请其在 Web **「待您确认」** Panel 点选;说明「以上假设确认前不开始开发」。
|
|
39
|
+
3. **无假设**:`append_message` 发送「本端需求理解」摘要并 @项目经理 请确认。
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
- **禁止多选**;复合口径由 PM 在 Web「其他」中填写。
|
|
42
|
-
- 能自己查清的代码含义 → 移入 PLAN「依据」,删除假设。
|
|
43
|
-
- 技术发现 → 写进 **研发备注**,问句用业务语言。
|
|
44
|
-
|
|
45
|
-
### 步骤 4:同步与回复
|
|
46
|
-
|
|
47
|
-
1. `apm sync-document --file FRONTEND-ASSUMPTIONS.md`(或 BACKEND)**先于** PLAN。
|
|
48
|
-
2. `apm sync-document --file FRONTEND-PLAN.md`(或 BACKEND)。
|
|
49
|
-
3. **有假设**:@项目经理 请其在 Web **「待您确认」** Panel 点选;说明「以上假设确认前不开始开发」。
|
|
50
|
-
4. **无假设**:`append_message` 发送「本端需求理解」摘要并 @项目经理 请确认。
|
|
41
|
+
> **禁止**在 ASSUMPTIONS sync 未成功时 @ 项目经理确认假设;PLAN 中「待确认 N 项」由服务端 sync ASSUMPTIONS 后自动更新,AI 不要手写具体数字。
|
|
51
42
|
|
|
52
43
|
### 假设确认后更新计划
|
|
53
44
|
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
## 工作流程
|
|
2
|
-
|
|
3
|
-
### 步骤 1: 获取当前版本 PRD 的内容
|
|
4
|
-
|
|
5
|
-
先用 **Read** 工具阅读 `.apm/sessions/<会话ID>/docs/PRD.md`,如果存在则这个目录下的内容为需求文档,否则用 **Read** 工具阅读 `.apm/sessions/<会话ID>/TASK.md`,并把这个文件里面的内容视为需求文档
|
|
6
|
-
|
|
7
|
-
### 步骤 2: 理解代码,给出评审意见,具体要求如下:
|
|
8
|
-
|
|
9
|
-
#### 评审目标如下:
|
|
10
|
-
|
|
11
|
-
1. 梳理需求边界:发现文档中未写清的口径
|
|
12
|
-
2. 调研实现难度:在实现难度较高时给出提示
|
|
13
|
-
3. 禁止为了凑问题而提问,当 PRD 已足够清晰且无高难度改造时,可直接说此任务没问题
|
|
14
|
-
4. 你只需要站在自己的角度去评审,禁止站在全局思考问题。结合代码进行评审即可:
|
|
15
|
-
- 前端关注 UI 交互还原,交互逻辑的各种边界,不考虑一些与后端接口之类的问题
|
|
16
|
-
- 后端则关注数据表设计,需求对应的数据接口是否合理,能否获取
|
|
17
|
-
- 全栈则关注前面两者,可以把这些问题合并起来提出来
|
|
18
|
-
|
|
19
|
-
**难度高的定义**:预估改动的文件可能比较多,超过 10 个;或者改动逻辑比较复杂;
|
|
20
|
-
|
|
21
|
-
#### 表达规范如下
|
|
22
|
-
|
|
23
|
-
1. **产品语言优先**:页面定位用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体。
|
|
24
|
-
2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
|
|
25
|
-
3. **锚定 PRD**:当需要引用到任务文档中的某一条时可以直接说行号,比如:需求原文[1-3]说 xxx;
|
|
26
|
-
4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点。已写清楚的规则不要重复质疑。
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# apm-rewrite-assumptions:重写 PM 不友好的假设问法
|
|
2
|
-
|
|
3
|
-
## 何时使用
|
|
4
|
-
|
|
5
|
-
`apm sync-document` 同步 ASSUMPTIONS 失败(问法校验不通过),或计划被退回需重写「问 / 场景 / 选项」。
|
|
6
|
-
|
|
7
|
-
## 工作流程
|
|
8
|
-
|
|
9
|
-
1. **Read** 本端 `docs/FRONTEND-ASSUMPTIONS.md` 或 `docs/BACKEND-ASSUMPTIONS.md`(及 sync 报错中的 qualityIssues)。
|
|
10
|
-
2. 将研发术语移入 **研发备注**;**问** 改为 PM 可理解的业务句子;补全 **场景** 与 **≥2 个选项**(含 id + label)。
|
|
11
|
-
3. **禁止多选**;复合口径说明写在 Other 指引中。
|
|
12
|
-
4. 重新 **`apm sync-document --file *-ASSUMPTIONS.md`** 同步。
|