ai-project-manage-cli 6.0.94 → 6.0.95
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 +265 -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,69 @@ 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 findGlobalPm2() {
|
|
3882
|
+
const whichResult = spawnSync2(useNpmShell2 ? "where" : "which", ["pm2"], {
|
|
3883
|
+
encoding: "utf8",
|
|
3884
|
+
shell: useNpmShell2,
|
|
3885
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3886
|
+
});
|
|
3887
|
+
if (whichResult.status === 0) {
|
|
3888
|
+
const lines = whichResult.stdout?.toString().trim().split(/\r?\n/) ?? [];
|
|
3889
|
+
for (const line of lines) {
|
|
3890
|
+
const candidate = line.trim();
|
|
3891
|
+
if (candidate && existsSync14(candidate)) {
|
|
3892
|
+
return candidate;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
const binResult = runNpm2(["bin", "-g"], {
|
|
3897
|
+
encoding: "utf8",
|
|
3898
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3899
|
+
});
|
|
3900
|
+
if (binResult.status === 0) {
|
|
3901
|
+
const globalBin = binResult.stdout?.toString().trim();
|
|
3902
|
+
if (globalBin) {
|
|
3903
|
+
const candidate = join15(globalBin, useNpmShell2 ? "pm2.cmd" : "pm2");
|
|
3904
|
+
if (existsSync14(candidate)) {
|
|
3905
|
+
return candidate;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
return null;
|
|
3910
|
+
}
|
|
3911
|
+
function installGlobalPm2() {
|
|
3912
|
+
console.log("[apm] \u672A\u68C0\u6D4B\u5230\u5168\u5C40 pm2\uFF0C\u6B63\u5728\u5B89\u88C5 npm install -g pm2 \u2026");
|
|
3913
|
+
const result = runNpm2(["install", "-g", "pm2"], {
|
|
3914
|
+
encoding: "utf8",
|
|
3915
|
+
stdio: "inherit"
|
|
3916
|
+
});
|
|
3917
|
+
if (result.status !== 0) {
|
|
3918
|
+
console.error("[apm] \u5B89\u88C5 pm2 \u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: npm install -g pm2");
|
|
3919
|
+
process.exit(result.status ?? 1);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
function ensureGlobalPm2() {
|
|
3923
|
+
const existing = findGlobalPm2();
|
|
3924
|
+
if (existing) {
|
|
3925
|
+
return existing;
|
|
3926
|
+
}
|
|
3927
|
+
installGlobalPm2();
|
|
3928
|
+
const installed2 = findGlobalPm2();
|
|
3929
|
+
if (!installed2) {
|
|
3930
|
+
console.error(
|
|
3931
|
+
"[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"
|
|
3932
|
+
);
|
|
3933
|
+
process.exit(1);
|
|
3934
|
+
}
|
|
3935
|
+
return installed2;
|
|
3936
|
+
}
|
|
3707
3937
|
function resolveApmEntryPath(entryArg = process.argv[1]) {
|
|
3708
3938
|
const fromArgv = entryArg?.trim();
|
|
3709
3939
|
if (fromArgv && existsSync14(fromArgv)) {
|
|
@@ -3733,20 +3963,13 @@ function isRunningUnderPm2() {
|
|
|
3733
3963
|
return process.env.name === PM2_APP_NAME && process.env.pm_id !== void 0;
|
|
3734
3964
|
}
|
|
3735
3965
|
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
|
-
}
|
|
3966
|
+
const pm2Bin = ensureGlobalPm2();
|
|
3745
3967
|
const spawnOptions = {
|
|
3746
3968
|
encoding: "utf8",
|
|
3747
|
-
stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"]
|
|
3969
|
+
stdio: options?.inherit ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
3970
|
+
shell: useNpmShell2
|
|
3748
3971
|
};
|
|
3749
|
-
const result = spawnSync2(
|
|
3972
|
+
const result = spawnSync2(pm2Bin, args, spawnOptions);
|
|
3750
3973
|
if (result.error) {
|
|
3751
3974
|
console.error("[apm] \u6267\u884C pm2 \u5931\u8D25:", result.error.message);
|
|
3752
3975
|
process.exit(1);
|
|
@@ -3760,15 +3983,14 @@ function runPm2(args, options) {
|
|
|
3760
3983
|
}
|
|
3761
3984
|
}
|
|
3762
3985
|
function runPm2Json(args) {
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
pm2Bin = resolvePm2Bin();
|
|
3766
|
-
} catch {
|
|
3986
|
+
const pm2Bin = findGlobalPm2();
|
|
3987
|
+
if (!pm2Bin) {
|
|
3767
3988
|
return "[]";
|
|
3768
3989
|
}
|
|
3769
|
-
const result = spawnSync2(
|
|
3990
|
+
const result = spawnSync2(pm2Bin, args, {
|
|
3770
3991
|
encoding: "utf8",
|
|
3771
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3992
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3993
|
+
shell: useNpmShell2
|
|
3772
3994
|
});
|
|
3773
3995
|
if (result.status !== 0) return "[]";
|
|
3774
3996
|
return result.stdout?.toString() ?? "[]";
|
|
@@ -3864,6 +4086,7 @@ async function prepareEcosystem(server) {
|
|
|
3864
4086
|
}
|
|
3865
4087
|
async function runDaemonStart(options) {
|
|
3866
4088
|
assertConnectNotRunning();
|
|
4089
|
+
ensureGlobalPm2();
|
|
3867
4090
|
await runUpdate();
|
|
3868
4091
|
const cfg = await prepareEcosystem(options.server);
|
|
3869
4092
|
runPm2(["start", PM2_ECOSYSTEM_PATH, "--update-env"], { inherit: true });
|
|
@@ -4534,7 +4757,7 @@ import { spawnSync as spawnSync5 } from "node:child_process";
|
|
|
4534
4757
|
import path5 from "node:path";
|
|
4535
4758
|
|
|
4536
4759
|
// src/commands/deploy/internal/apm-config.ts
|
|
4537
|
-
import { existsSync as existsSync15, readFileSync as
|
|
4760
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
4538
4761
|
import { homedir as homedir2 } from "node:os";
|
|
4539
4762
|
import { join as join16, resolve as resolve4 } from "node:path";
|
|
4540
4763
|
function loadApmConfig(options) {
|
|
@@ -4547,7 +4770,7 @@ function loadApmConfig(options) {
|
|
|
4547
4770
|
process.exit(1);
|
|
4548
4771
|
}
|
|
4549
4772
|
try {
|
|
4550
|
-
const raw =
|
|
4773
|
+
const raw = readFileSync13(p, "utf8");
|
|
4551
4774
|
return JSON.parse(raw);
|
|
4552
4775
|
} catch (e) {
|
|
4553
4776
|
console.error(`\u65E0\u6CD5\u89E3\u6790 apm.config.json\uFF1A${p}`, e);
|
|
@@ -4736,7 +4959,7 @@ function readMavenLocalRepoFromSettings() {
|
|
|
4736
4959
|
return null;
|
|
4737
4960
|
}
|
|
4738
4961
|
try {
|
|
4739
|
-
const xml =
|
|
4962
|
+
const xml = readFileSync13(settingsPath, "utf8");
|
|
4740
4963
|
const match = xml.match(
|
|
4741
4964
|
/<localRepository>\s*([^<]+?)\s*<\/localRepository>/
|
|
4742
4965
|
);
|
|
@@ -4878,7 +5101,7 @@ var DeployExecutionError = class extends Error {
|
|
|
4878
5101
|
};
|
|
4879
5102
|
|
|
4880
5103
|
// src/commands/deploy/internal/wisdom-auto-deploy.ts
|
|
4881
|
-
import { existsSync as existsSync17, readFileSync as
|
|
5104
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15, statSync as statSync7 } from "node:fs";
|
|
4882
5105
|
import path4 from "node:path";
|
|
4883
5106
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
4884
5107
|
|
|
@@ -4887,7 +5110,7 @@ import {
|
|
|
4887
5110
|
existsSync as existsSync16,
|
|
4888
5111
|
mkdirSync as mkdirSync8,
|
|
4889
5112
|
readdirSync as readdirSync5,
|
|
4890
|
-
readFileSync as
|
|
5113
|
+
readFileSync as readFileSync14,
|
|
4891
5114
|
statSync as statSync6,
|
|
4892
5115
|
writeFileSync as writeFileSync14
|
|
4893
5116
|
} from "node:fs";
|
|
@@ -5157,7 +5380,7 @@ function loadManifest4() {
|
|
|
5157
5380
|
if (!existsSync16(manifestPath2)) {
|
|
5158
5381
|
return {};
|
|
5159
5382
|
}
|
|
5160
|
-
return JSON.parse(
|
|
5383
|
+
return JSON.parse(readFileSync14(manifestPath2, "utf8"));
|
|
5161
5384
|
}
|
|
5162
5385
|
function saveManifest4(manifest) {
|
|
5163
5386
|
const dir = deployCacheDir();
|
|
@@ -5223,7 +5446,7 @@ async function createUpdatePackage(entries, packageName) {
|
|
|
5223
5446
|
log(`\u521B\u5EFA\u66F4\u65B0\u5305: ${path3.basename(zipPath)}\uFF08${entries.length} \u4E2A\u6587\u4EF6\uFF09`);
|
|
5224
5447
|
const zip = new JSZip2();
|
|
5225
5448
|
for (const entry of entries) {
|
|
5226
|
-
const content =
|
|
5449
|
+
const content = readFileSync14(entry.path);
|
|
5227
5450
|
zip.file(entry.arcname, content);
|
|
5228
5451
|
log(` \u6253\u5305: ${entry.arcname} (${entry.reason})`);
|
|
5229
5452
|
}
|
|
@@ -5789,7 +6012,7 @@ function readPackageScripts(cwd) {
|
|
|
5789
6012
|
return {};
|
|
5790
6013
|
}
|
|
5791
6014
|
try {
|
|
5792
|
-
const raw =
|
|
6015
|
+
const raw = readFileSync15(pkgPath, "utf8");
|
|
5793
6016
|
const parsed = JSON.parse(raw);
|
|
5794
6017
|
return parsed.scripts ?? {};
|
|
5795
6018
|
} catch {
|
|
@@ -6313,7 +6536,7 @@ import path9 from "node:path";
|
|
|
6313
6536
|
import Docker from "dockerode";
|
|
6314
6537
|
|
|
6315
6538
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
|
|
6316
|
-
import { existsSync as existsSync18, readFileSync as
|
|
6539
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "node:fs";
|
|
6317
6540
|
import path6 from "node:path";
|
|
6318
6541
|
function asOptionalTlsBuffer(value) {
|
|
6319
6542
|
if (typeof value !== "string") {
|
|
@@ -6326,7 +6549,7 @@ function asOptionalTlsBuffer(value) {
|
|
|
6326
6549
|
return void 0;
|
|
6327
6550
|
}
|
|
6328
6551
|
if (existsSync18(normalized)) {
|
|
6329
|
-
return
|
|
6552
|
+
return readFileSync16(normalized);
|
|
6330
6553
|
}
|
|
6331
6554
|
const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
|
|
6332
6555
|
if (looksLikePath) {
|
|
@@ -6536,7 +6759,7 @@ var DockerodeClient = class {
|
|
|
6536
6759
|
var createDockerodeClient = (config) => new DockerodeClient(config);
|
|
6537
6760
|
|
|
6538
6761
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
|
|
6539
|
-
import { existsSync as existsSync19, readFileSync as
|
|
6762
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, statSync as statSync8 } from "node:fs";
|
|
6540
6763
|
import path7 from "node:path";
|
|
6541
6764
|
function stripSurroundingQuotes(value) {
|
|
6542
6765
|
const t = value.trim();
|
|
@@ -6556,7 +6779,7 @@ function loadEnvFromFile(envFilePath) {
|
|
|
6556
6779
|
if (!existsSync19(targetPath) || !statSync8(targetPath).isFile()) {
|
|
6557
6780
|
return {};
|
|
6558
6781
|
}
|
|
6559
|
-
const raw =
|
|
6782
|
+
const raw = readFileSync17(targetPath, "utf-8");
|
|
6560
6783
|
const result = {};
|
|
6561
6784
|
for (const line of raw.split(/\r?\n/)) {
|
|
6562
6785
|
const normalized = line.trim();
|
|
@@ -7217,7 +7440,7 @@ function buildProgram() {
|
|
|
7217
7440
|
"\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
7441
|
).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").option(
|
|
7219
7442
|
"--daemon",
|
|
7220
|
-
"\u540E\u53F0\u5B88\u62A4\u8FD0\u884C\uFF08\
|
|
7443
|
+
"\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
7444
|
).option("-f, --follow", "\u4E0E --daemon \u5408\u7528\uFF1A\u542F\u52A8\u540E\u5728\u5F53\u524D\u7EC8\u7AEF\u5B9E\u65F6\u8DDF\u8E2A\u65E5\u5FD7").action(
|
|
7222
7445
|
async (opts) => {
|
|
7223
7446
|
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.95",
|
|
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`** 同步。
|