ai-project-manage-cli 5.0.7 → 5.0.9
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 +436 -62
- package/package.json +4 -1
- package/template/AGENTS.md +13 -7
- package/template/skills/apm-apply-change/SKILL.md +178 -0
- package/template/skills/apm-deploy/SKILL.md +21 -0
- package/template/skills/apm-dev/SKILL.md +28 -0
- package/template/skills/apm-propose/SKILL.md +191 -0
- package/template/skills/apm-review/SKILL.md +30 -0
- package/template/skills/apm-write-prd/SKILL.md +69 -0
package/dist/index.js
CHANGED
|
@@ -40,6 +40,12 @@ function defaultApmConfig() {
|
|
|
40
40
|
email: ""
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
function httpBaseToWsOrigin(httpBase) {
|
|
44
|
+
const u = httpBase.trim().replace(/\/+$/, "");
|
|
45
|
+
if (u.startsWith("https://")) return "wss://" + u.slice("https://".length);
|
|
46
|
+
if (u.startsWith("http://")) return "ws://" + u.slice("http://".length);
|
|
47
|
+
throw new Error("baseUrl \u5FC5\u987B\u4EE5 http:// \u6216 https:// \u5F00\u5934");
|
|
48
|
+
}
|
|
43
49
|
async function ensureApmConfig() {
|
|
44
50
|
const existing = await readApmConfig();
|
|
45
51
|
if (existing) return existing;
|
|
@@ -59,6 +65,11 @@ async function writeApmConfig(cfg) {
|
|
|
59
65
|
mode: 384
|
|
60
66
|
});
|
|
61
67
|
}
|
|
68
|
+
function buildAgentWsUrl(httpBase, token) {
|
|
69
|
+
const origin = httpBaseToWsOrigin(httpBase);
|
|
70
|
+
const q = new URLSearchParams({ token });
|
|
71
|
+
return `${origin}/ws/agent?${q.toString()}`;
|
|
72
|
+
}
|
|
62
73
|
|
|
63
74
|
// src/commands/init.ts
|
|
64
75
|
import { join as join3 } from "path";
|
|
@@ -456,33 +467,17 @@ function escapeXmlAttr(value) {
|
|
|
456
467
|
function wrapCdata(value) {
|
|
457
468
|
return `<![CDATA[${value.replace(/]]>/g, "]]]]><![CDATA[>")}]]>`;
|
|
458
469
|
}
|
|
459
|
-
function optionalAttr(name, value) {
|
|
460
|
-
if (value == null || value === "") {
|
|
461
|
-
return "";
|
|
462
|
-
}
|
|
463
|
-
return ` ${name}="${escapeXmlAttr(value)}"`;
|
|
464
|
-
}
|
|
465
470
|
function formatSessionMessagesXml(sessionId, messages) {
|
|
466
471
|
const lines = [
|
|
467
472
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
468
473
|
`<messages sessionId="${escapeXmlAttr(sessionId)}">`
|
|
469
474
|
];
|
|
470
475
|
for (const message of messages) {
|
|
471
|
-
const
|
|
472
|
-
`id="${escapeXmlAttr(message.id)}"`,
|
|
473
|
-
`senderId="${escapeXmlAttr(message.senderId)}"`,
|
|
474
|
-
message.sender ? `senderName="${escapeXmlAttr(message.sender.displayName)}"` : "",
|
|
475
|
-
message.sender ? `senderPosition="${escapeXmlAttr(message.sender.position)}"` : "",
|
|
476
|
-
message.sender ? `proxyMode="${escapeXmlAttr(message.sender.proxyMode)}"` : "",
|
|
477
|
-
`status="${escapeXmlAttr(message.status)}"`,
|
|
478
|
-
`createdAt="${escapeXmlAttr(message.createdAt)}"`,
|
|
479
|
-
`contentUpdatedAt="${escapeXmlAttr(message.contentUpdatedAt)}"`
|
|
480
|
-
].filter(Boolean).join(" ");
|
|
476
|
+
const roundAttr = message.round != null && message.round > 0 ? ` round="${message.round}"` : "";
|
|
481
477
|
lines.push(
|
|
482
|
-
` <message ${
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
)}${optionalAttr("sentAt", message.sentAt)}>`,
|
|
478
|
+
` <message id="${escapeXmlAttr(message.id)}" name="${escapeXmlAttr(
|
|
479
|
+
message.name
|
|
480
|
+
)}" position="${escapeXmlAttr(message.position)}"${roundAttr}>`,
|
|
486
481
|
` <content>${wrapCdata(message.content)}</content>`,
|
|
487
482
|
" </message>"
|
|
488
483
|
);
|
|
@@ -662,43 +657,126 @@ async function runUpdate() {
|
|
|
662
657
|
}
|
|
663
658
|
|
|
664
659
|
// src/commands/update-skills.ts
|
|
665
|
-
import { existsSync as
|
|
660
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
|
|
661
|
+
import { join as join8 } from "path";
|
|
662
|
+
|
|
663
|
+
// src/skills-sync.ts
|
|
664
|
+
import {
|
|
665
|
+
copyFileSync,
|
|
666
|
+
cpSync as cpSync2,
|
|
667
|
+
existsSync as existsSync2,
|
|
668
|
+
mkdirSync as mkdirSync3,
|
|
669
|
+
readdirSync as readdirSync2,
|
|
670
|
+
rmSync,
|
|
671
|
+
statSync as statSync2,
|
|
672
|
+
writeFileSync as writeFileSync5
|
|
673
|
+
} from "fs";
|
|
666
674
|
import { join as join7 } from "path";
|
|
667
|
-
|
|
675
|
+
var AGENTS_TEMPLATE_PATH = join7(CLI_TEMPLATE_DIR, "AGENTS.md");
|
|
676
|
+
var BASE_SKILLS_TEMPLATE_DIR = join7(CLI_TEMPLATE_DIR, "skills");
|
|
677
|
+
function sanitizeSkillDirName(name) {
|
|
668
678
|
const trimmed = name.trim();
|
|
669
679
|
if (!trimmed) return "skill";
|
|
670
680
|
return trimmed.replace(/[/\\:*?"<>|]/g, "_");
|
|
671
681
|
}
|
|
682
|
+
function listBaseSkillDirNames() {
|
|
683
|
+
if (!existsSync2(BASE_SKILLS_TEMPLATE_DIR)) return [];
|
|
684
|
+
return readdirSync2(BASE_SKILLS_TEMPLATE_DIR).filter((name) => {
|
|
685
|
+
const path8 = join7(BASE_SKILLS_TEMPLATE_DIR, name);
|
|
686
|
+
return statSync2(path8).isDirectory();
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function syncAgentsGuide(apmDir) {
|
|
690
|
+
if (!existsSync2(AGENTS_TEMPLATE_PATH)) return false;
|
|
691
|
+
mkdirSync3(apmDir, { recursive: true });
|
|
692
|
+
copyFileSync(AGENTS_TEMPLATE_PATH, join7(apmDir, "AGENTS.md"));
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
function syncBaseSkills(skillsDir) {
|
|
696
|
+
mkdirSync3(skillsDir, { recursive: true });
|
|
697
|
+
const names = listBaseSkillDirNames();
|
|
698
|
+
for (const name of names) {
|
|
699
|
+
const src = join7(BASE_SKILLS_TEMPLATE_DIR, name);
|
|
700
|
+
const dest = join7(skillsDir, name);
|
|
701
|
+
cpSync2(src, dest, { recursive: true, force: true });
|
|
702
|
+
}
|
|
703
|
+
return names;
|
|
704
|
+
}
|
|
705
|
+
function syncSupplementarySkills(skillsDir, list) {
|
|
706
|
+
const baseNames = new Set(listBaseSkillDirNames());
|
|
707
|
+
const apiDirNames = /* @__PURE__ */ new Set();
|
|
708
|
+
const written = [];
|
|
709
|
+
const skipped = [];
|
|
710
|
+
for (const skill of list) {
|
|
711
|
+
const dirName = sanitizeSkillDirName(skill.name);
|
|
712
|
+
apiDirNames.add(dirName);
|
|
713
|
+
if (baseNames.has(dirName)) {
|
|
714
|
+
skipped.push(dirName);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const skillDir = join7(skillsDir, dirName);
|
|
718
|
+
mkdirSync3(skillDir, { recursive: true });
|
|
719
|
+
writeFileSync5(join7(skillDir, "SKILL.md"), skill.content ?? "", "utf8");
|
|
720
|
+
written.push(dirName);
|
|
721
|
+
}
|
|
722
|
+
const removed = [];
|
|
723
|
+
if (!existsSync2(skillsDir)) return { written, skipped, removed };
|
|
724
|
+
for (const entry of readdirSync2(skillsDir)) {
|
|
725
|
+
const full = join7(skillsDir, entry);
|
|
726
|
+
if (!statSync2(full).isDirectory()) continue;
|
|
727
|
+
if (baseNames.has(entry)) continue;
|
|
728
|
+
if (apiDirNames.has(entry)) continue;
|
|
729
|
+
rmSync(full, { recursive: true, force: true });
|
|
730
|
+
removed.push(entry);
|
|
731
|
+
}
|
|
732
|
+
return { written, skipped, removed };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/update-skills.ts
|
|
672
736
|
async function runUpdateSkills() {
|
|
673
737
|
const apmDir = workspaceApmDir();
|
|
674
|
-
if (!
|
|
738
|
+
if (!existsSync3(apmDir)) {
|
|
675
739
|
console.error("[apm] \u672A\u627E\u5230 .apm \u76EE\u5F55\uFF0C\u8BF7\u5148\u6267\u884C apm init");
|
|
676
740
|
process.exit(1);
|
|
677
741
|
}
|
|
678
|
-
const apmStat =
|
|
742
|
+
const apmStat = statSync3(apmDir);
|
|
679
743
|
if (!apmStat.isDirectory()) {
|
|
680
744
|
throw new Error(`[apm] \u8DEF\u5F84\u5DF2\u5B58\u5728\u4F46\u4E0D\u662F\u76EE\u5F55: ${apmDir}`);
|
|
681
745
|
}
|
|
682
746
|
const cfg = await ensureLoggedConfig();
|
|
683
747
|
const api = createApmApiClient(cfg);
|
|
684
748
|
const { list } = await api.cli.listSkills({});
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
749
|
+
if (syncAgentsGuide(apmDir)) {
|
|
750
|
+
console.log("[apm] \u5DF2\u540C\u6B65 APM \u6307\u5357: .apm/AGENTS.md");
|
|
751
|
+
}
|
|
752
|
+
const skillsDir = join8(apmDir, "skills");
|
|
753
|
+
mkdirSync4(skillsDir, { recursive: true });
|
|
754
|
+
const baseNames = syncBaseSkills(skillsDir);
|
|
755
|
+
for (const name of baseNames) {
|
|
756
|
+
console.log(`[apm] \u5DF2\u540C\u6B65\u57FA\u7840\u6280\u80FD: skills/${name}/`);
|
|
757
|
+
}
|
|
758
|
+
const { written, skipped, removed } = syncSupplementarySkills(
|
|
759
|
+
skillsDir,
|
|
760
|
+
list
|
|
761
|
+
);
|
|
762
|
+
for (const name of written) {
|
|
763
|
+
console.log(`[apm] \u5DF2\u5199\u5165\u8865\u5145\u6280\u80FD: skills/${name}/SKILL.md`);
|
|
688
764
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
console.log(`[apm] \u5DF2\
|
|
765
|
+
for (const name of skipped) {
|
|
766
|
+
console.log(
|
|
767
|
+
`[apm] \u5DF2\u8DF3\u8FC7\u4E0E\u57FA\u7840\u6280\u80FD\u540C\u540D\u7684\u8865\u5145\u6280\u80FD: ${name}\uFF08\u4FDD\u7559\u6A21\u677F\u7248\u672C\uFF09`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
for (const name of removed) {
|
|
771
|
+
console.log(`[apm] \u5DF2\u79FB\u9664\u5DF2\u4E0B\u7EBF\u7684\u8865\u5145\u6280\u80FD: skills/${name}/`);
|
|
696
772
|
}
|
|
697
|
-
console.log(
|
|
773
|
+
console.log(
|
|
774
|
+
`[apm] \u6280\u80FD\u540C\u6B65\u5B8C\u6210\uFF1A${baseNames.length} \u4E2A\u57FA\u7840\u6280\u80FD\uFF0C${written.length} \u4E2A\u8865\u5145\u6280\u80FD`
|
|
775
|
+
);
|
|
698
776
|
}
|
|
699
777
|
|
|
700
778
|
// src/commands/sync-document.ts
|
|
701
|
-
import { existsSync as
|
|
779
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
702
780
|
async function runSyncDocument(sessionId, options) {
|
|
703
781
|
const trimmedSessionId = sessionId.trim();
|
|
704
782
|
if (!trimmedSessionId) {
|
|
@@ -711,7 +789,7 @@ async function runSyncDocument(sessionId, options) {
|
|
|
711
789
|
process.exit(1);
|
|
712
790
|
}
|
|
713
791
|
const absPath = resolveSessionDocumentPath(trimmedSessionId, fileArg);
|
|
714
|
-
if (!
|
|
792
|
+
if (!existsSync4(absPath)) {
|
|
715
793
|
const docsDir = sessionDocsDir(trimmedSessionId);
|
|
716
794
|
console.error(
|
|
717
795
|
`[apm] \u6587\u6863\u4E0D\u5B58\u5728: ${absPath}
|
|
@@ -775,18 +853,307 @@ async function runUpdateMessageStatus(options) {
|
|
|
775
853
|
console.log(`[apm] \u5DF2\u66F4\u65B0\u6D88\u606F\u72B6\u6001: ${messageId} \u2192 ${status}`);
|
|
776
854
|
}
|
|
777
855
|
|
|
856
|
+
// src/commands/connect.ts
|
|
857
|
+
import WebSocket from "ws";
|
|
858
|
+
|
|
859
|
+
// src/ws/protocol.ts
|
|
860
|
+
function nonEmptyString(v) {
|
|
861
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
862
|
+
}
|
|
863
|
+
function parseAgentWsMessage(raw) {
|
|
864
|
+
try {
|
|
865
|
+
return JSON.parse(raw);
|
|
866
|
+
} catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function serializeAgentWsMessage(msg) {
|
|
871
|
+
return JSON.stringify(msg);
|
|
872
|
+
}
|
|
873
|
+
function validateAgentWsMessage(value, kind) {
|
|
874
|
+
if (typeof value !== "object" || value === null) {
|
|
875
|
+
return { ok: false, reason: "\u6D88\u606F\u4F53\u4E0D\u662F JSON \u5BF9\u8C61" };
|
|
876
|
+
}
|
|
877
|
+
const o = value;
|
|
878
|
+
const type = o.type;
|
|
879
|
+
if (type !== "heartbeat" && type !== "message") {
|
|
880
|
+
return { ok: false, reason: `\u672A\u77E5 type: ${String(type)}` };
|
|
881
|
+
}
|
|
882
|
+
if (kind === "heartbeat" || type === "heartbeat") {
|
|
883
|
+
if (type !== "heartbeat") {
|
|
884
|
+
return { ok: false, reason: "\u671F\u671B heartbeat" };
|
|
885
|
+
}
|
|
886
|
+
if (!nonEmptyString(o.userId)) {
|
|
887
|
+
return { ok: false, reason: "heartbeat \u7F3A\u5C11 userId" };
|
|
888
|
+
}
|
|
889
|
+
return { ok: true, data: { type: "heartbeat", userId: o.userId.trim() } };
|
|
890
|
+
}
|
|
891
|
+
if (type !== "message") {
|
|
892
|
+
return { ok: false, reason: "\u671F\u671B message" };
|
|
893
|
+
}
|
|
894
|
+
if (!nonEmptyString(o.messageId)) {
|
|
895
|
+
return { ok: false, reason: "message \u7F3A\u5C11 messageId" };
|
|
896
|
+
}
|
|
897
|
+
if (!nonEmptyString(o.sessionId)) {
|
|
898
|
+
return { ok: false, reason: "message \u7F3A\u5C11 sessionId" };
|
|
899
|
+
}
|
|
900
|
+
if (!nonEmptyString(o.content)) {
|
|
901
|
+
return { ok: false, reason: "message \u7F3A\u5C11 content" };
|
|
902
|
+
}
|
|
903
|
+
if (!nonEmptyString(o.model)) {
|
|
904
|
+
return { ok: false, reason: "message \u7F3A\u5C11 model" };
|
|
905
|
+
}
|
|
906
|
+
if (!nonEmptyString(o.apiKey)) {
|
|
907
|
+
return { ok: false, reason: "message \u7F3A\u5C11 apiKey" };
|
|
908
|
+
}
|
|
909
|
+
if (!nonEmptyString(o.workdir)) {
|
|
910
|
+
return { ok: false, reason: "message \u7F3A\u5C11 workdir" };
|
|
911
|
+
}
|
|
912
|
+
if (!nonEmptyString(o.user)) {
|
|
913
|
+
return { ok: false, reason: "message \u7F3A\u5C11 user" };
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
ok: true,
|
|
917
|
+
data: {
|
|
918
|
+
type: "message",
|
|
919
|
+
messageId: o.messageId.trim(),
|
|
920
|
+
sessionId: o.sessionId.trim(),
|
|
921
|
+
content: o.content,
|
|
922
|
+
model: o.model.trim(),
|
|
923
|
+
apiKey: o.apiKey.trim(),
|
|
924
|
+
workdir: o.workdir.trim(),
|
|
925
|
+
user: o.user.trim()
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/commands/connect/cursor-agent.ts
|
|
931
|
+
import { resolve as resolve3 } from "path";
|
|
932
|
+
import { Agent, CursorAgentError } from "@cursor/sdk";
|
|
933
|
+
var DEFAULT_CURSOR_MODEL = "default";
|
|
934
|
+
function resolveModelId(model) {
|
|
935
|
+
const id = model.trim();
|
|
936
|
+
return id || DEFAULT_CURSOR_MODEL;
|
|
937
|
+
}
|
|
938
|
+
function buildPrompt(ctx) {
|
|
939
|
+
const lines = [
|
|
940
|
+
`\u4F60\u662F${ctx.user}`,
|
|
941
|
+
`\u4F1A\u8BDDID: ${ctx.sessionId}`,
|
|
942
|
+
`\u6D88\u606FID: ${ctx.messageId}`,
|
|
943
|
+
`\u4EFB\u52A1: ${ctx.content}`,
|
|
944
|
+
`\u8BF7\u5148\u9605\u8BFBAPM\u6307\u5357\uFF1A [AGENTS.md](.apm/AGENTS.md)\uFF0C\u4E25\u683C\u6309\u7167\u6307\u5357\u7684\u5DE5\u4F5C\u6D41\u7A0B\u5B8C\u6210\u4EFB\u52A1`
|
|
945
|
+
];
|
|
946
|
+
return lines.join("\n");
|
|
947
|
+
}
|
|
948
|
+
function extractAssistantTextFromStream(event) {
|
|
949
|
+
if (event.type !== "assistant") {
|
|
950
|
+
return "";
|
|
951
|
+
}
|
|
952
|
+
const msg = event.message;
|
|
953
|
+
if (!msg?.content) {
|
|
954
|
+
return "";
|
|
955
|
+
}
|
|
956
|
+
let text = "";
|
|
957
|
+
for (const block of msg.content) {
|
|
958
|
+
if (block.type === "text" && block.text) {
|
|
959
|
+
text += block.text;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return text;
|
|
963
|
+
}
|
|
964
|
+
async function runCursorAgent(_cfg, ctx) {
|
|
965
|
+
const apiKey = ctx.apiKey.trim();
|
|
966
|
+
if (!apiKey) {
|
|
967
|
+
throw new Error("\u7F3A\u5C11 apiKey\uFF0C\u65E0\u6CD5\u8C03\u7528 Cursor SDK");
|
|
968
|
+
}
|
|
969
|
+
const cwd = resolve3(ctx.workdir);
|
|
970
|
+
const prompt = buildPrompt(ctx);
|
|
971
|
+
console.log(
|
|
972
|
+
`[apm] Cursor Agent \u5F00\u59CB messageId=${ctx.messageId} sessionId=${ctx.sessionId} cwd=${cwd}`
|
|
973
|
+
);
|
|
974
|
+
const agent = await Agent.create({
|
|
975
|
+
apiKey,
|
|
976
|
+
model: { id: resolveModelId(ctx.model) },
|
|
977
|
+
local: {
|
|
978
|
+
cwd,
|
|
979
|
+
settingSources: []
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
try {
|
|
983
|
+
const run = await agent.send(prompt);
|
|
984
|
+
console.log(`[apm] Cursor run id=${run.id} agentId=${agent.agentId}`);
|
|
985
|
+
for await (const event of run.stream()) {
|
|
986
|
+
const chunk = extractAssistantTextFromStream(event);
|
|
987
|
+
if (chunk) {
|
|
988
|
+
process.stdout.write(chunk);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const result = await run.wait();
|
|
992
|
+
if (result.status === "error") {
|
|
993
|
+
throw new Error(`Cursor run \u5931\u8D25: ${result.id}`);
|
|
994
|
+
}
|
|
995
|
+
if (result.status === "cancelled") {
|
|
996
|
+
throw new Error(`Cursor run \u5DF2\u53D6\u6D88: ${result.id}`);
|
|
997
|
+
}
|
|
998
|
+
console.log(`[apm] Cursor Agent \u5B8C\u6210 messageId=${ctx.messageId}`);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
if (err instanceof CursorAgentError) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`Cursor \u542F\u52A8\u5931\u8D25: ${err.message}${err.isRetryable ? "\uFF08\u53EF\u91CD\u8BD5\uFF09" : ""}`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
throw err;
|
|
1006
|
+
} finally {
|
|
1007
|
+
await agent[Symbol.asyncDispose]();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/commands/connect.ts
|
|
1012
|
+
var HEARTBEAT_MS = 3e4;
|
|
1013
|
+
async function updateMessageStatus(cfg, messageId, status) {
|
|
1014
|
+
const api = createApmApiClient(cfg);
|
|
1015
|
+
await api.cli.updateMessageStatus({ id: messageId, status });
|
|
1016
|
+
console.log(`[apm] \u5DF2\u66F4\u65B0\u6D88\u606F\u72B6\u6001: ${messageId} \u2192 ${status}`);
|
|
1017
|
+
}
|
|
1018
|
+
async function appendMessageContent(cfg, messageId, content) {
|
|
1019
|
+
const api = createApmApiClient(cfg);
|
|
1020
|
+
await api.cli.appendMessageContent({ id: messageId, content });
|
|
1021
|
+
console.log(`[apm] \u5DF2\u8FFD\u52A0\u6D88\u606F\u5185\u5BB9: ${messageId}`);
|
|
1022
|
+
}
|
|
1023
|
+
async function handleInboundMessage(cfg, raw) {
|
|
1024
|
+
const parsed = parseAgentWsMessage(raw);
|
|
1025
|
+
if (parsed === null) {
|
|
1026
|
+
console.error("[apm] \u6536\u5230\u65E0\u6548 JSON");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof parsed === "object" && parsed !== null && parsed.type === "heartbeat") {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const validated = validateAgentWsMessage(parsed, "outbound");
|
|
1033
|
+
if (!validated.ok) {
|
|
1034
|
+
console.error(`[apm] \u6536\u5230\u65E0\u6548 message: ${validated.reason}`);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (validated.data.type !== "message") {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const msg = validated.data;
|
|
1041
|
+
const messageId = msg.messageId;
|
|
1042
|
+
try {
|
|
1043
|
+
await runBranch(msg.sessionId, { cwd: msg.workdir });
|
|
1044
|
+
await runPull(msg.sessionId, msg.workdir);
|
|
1045
|
+
await updateMessageStatus(cfg, messageId, "TYPING");
|
|
1046
|
+
await runCursorAgent(cfg, {
|
|
1047
|
+
messageId: msg.messageId,
|
|
1048
|
+
sessionId: msg.sessionId,
|
|
1049
|
+
content: msg.content,
|
|
1050
|
+
model: msg.model,
|
|
1051
|
+
apiKey: msg.apiKey,
|
|
1052
|
+
workdir: msg.workdir,
|
|
1053
|
+
user: msg.user
|
|
1054
|
+
});
|
|
1055
|
+
await updateMessageStatus(cfg, messageId, "SUCCESS");
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
console.error(
|
|
1058
|
+
"[apm] \u5904\u7406\u6D88\u606F\u5931\u8D25:",
|
|
1059
|
+
err instanceof Error ? err.message : err
|
|
1060
|
+
);
|
|
1061
|
+
try {
|
|
1062
|
+
await appendMessageContent(
|
|
1063
|
+
cfg,
|
|
1064
|
+
messageId,
|
|
1065
|
+
`\u5904\u7406\u6D88\u606F\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}
|
|
1066
|
+
`
|
|
1067
|
+
);
|
|
1068
|
+
await updateMessageStatus(cfg, messageId, "FAILED");
|
|
1069
|
+
} catch (statusErr) {
|
|
1070
|
+
console.error(
|
|
1071
|
+
"[apm] \u66F4\u65B0 FAILED \u72B6\u6001\u5931\u8D25:",
|
|
1072
|
+
statusErr instanceof Error ? statusErr.message : statusErr
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function startHeartbeat(ws, userId) {
|
|
1078
|
+
const send = () => {
|
|
1079
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1080
|
+
ws.send(
|
|
1081
|
+
serializeAgentWsMessage({
|
|
1082
|
+
type: "heartbeat",
|
|
1083
|
+
userId
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
send();
|
|
1089
|
+
const timer = setInterval(send, HEARTBEAT_MS);
|
|
1090
|
+
return () => clearInterval(timer);
|
|
1091
|
+
}
|
|
1092
|
+
async function runConnect(options) {
|
|
1093
|
+
const cfg = await ensureLoggedConfig();
|
|
1094
|
+
if (options.server?.trim()) {
|
|
1095
|
+
cfg.baseUrl = options.server.trim().replace(/\/+$/, "");
|
|
1096
|
+
}
|
|
1097
|
+
if (!cfg.userId?.trim()) {
|
|
1098
|
+
console.error("[apm] config \u7F3A\u5C11 userId\uFF0C\u8BF7\u91CD\u65B0 apm login");
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
const url = buildAgentWsUrl(cfg.baseUrl, cfg.token);
|
|
1102
|
+
console.log(`[apm] \u8FDE\u63A5 ${cfg.baseUrl} \u2026`);
|
|
1103
|
+
await new Promise((resolve5, reject) => {
|
|
1104
|
+
const ws = new WebSocket(url);
|
|
1105
|
+
let stopHeartbeat;
|
|
1106
|
+
let shuttingDown = false;
|
|
1107
|
+
const shutdown = (code = 0) => {
|
|
1108
|
+
if (shuttingDown) return;
|
|
1109
|
+
shuttingDown = true;
|
|
1110
|
+
stopHeartbeat?.();
|
|
1111
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
1112
|
+
ws.close();
|
|
1113
|
+
}
|
|
1114
|
+
resolve5();
|
|
1115
|
+
if (code !== 0) {
|
|
1116
|
+
process.exit(code);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
ws.on("open", () => {
|
|
1120
|
+
console.log("[apm] WebSocket \u5DF2\u8FDE\u63A5");
|
|
1121
|
+
stopHeartbeat = startHeartbeat(ws, cfg.userId);
|
|
1122
|
+
});
|
|
1123
|
+
ws.on("message", (data) => {
|
|
1124
|
+
const text = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1125
|
+
void handleInboundMessage(cfg, text);
|
|
1126
|
+
});
|
|
1127
|
+
ws.on("close", (code, reason) => {
|
|
1128
|
+
console.log(
|
|
1129
|
+
`[apm] \u8FDE\u63A5\u5DF2\u65AD\u5F00 code=${code}${reason ? ` reason=${reason.toString()}` : ""}`
|
|
1130
|
+
);
|
|
1131
|
+
shutdown();
|
|
1132
|
+
});
|
|
1133
|
+
ws.on("error", (err) => {
|
|
1134
|
+
console.error("[apm] WebSocket \u9519\u8BEF:", err.message);
|
|
1135
|
+
reject(err);
|
|
1136
|
+
});
|
|
1137
|
+
process.on("SIGINT", () => {
|
|
1138
|
+
console.log("[apm] \u6B63\u5728\u5173\u95ED\u2026");
|
|
1139
|
+
shutdown();
|
|
1140
|
+
});
|
|
1141
|
+
process.on("SIGTERM", () => shutdown());
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
778
1145
|
// src/commands/deploy/backend.ts
|
|
779
1146
|
import path5 from "node:path";
|
|
780
1147
|
|
|
781
1148
|
// src/commands/deploy/internal/apm-config.ts
|
|
782
|
-
import { existsSync as
|
|
783
|
-
import { resolve as
|
|
1149
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
1150
|
+
import { resolve as resolve4 } from "node:path";
|
|
784
1151
|
function loadApmConfig(options) {
|
|
785
|
-
const p =
|
|
1152
|
+
const p = resolve4(
|
|
786
1153
|
process.cwd(),
|
|
787
|
-
options?.configPath ??
|
|
1154
|
+
options?.configPath ?? resolve4(workspaceApmDir(), "apm.config.json")
|
|
788
1155
|
);
|
|
789
|
-
if (!
|
|
1156
|
+
if (!existsSync5(p)) {
|
|
790
1157
|
console.error(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF1A${p}`);
|
|
791
1158
|
process.exit(1);
|
|
792
1159
|
}
|
|
@@ -888,7 +1255,7 @@ import path4 from "node:path";
|
|
|
888
1255
|
import Docker from "dockerode";
|
|
889
1256
|
|
|
890
1257
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/connection-options.ts
|
|
891
|
-
import { existsSync as
|
|
1258
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
|
|
892
1259
|
import path from "node:path";
|
|
893
1260
|
function asOptionalTlsBuffer(value) {
|
|
894
1261
|
if (typeof value !== "string") {
|
|
@@ -900,7 +1267,7 @@ function asOptionalTlsBuffer(value) {
|
|
|
900
1267
|
if (normalized === "") {
|
|
901
1268
|
return void 0;
|
|
902
1269
|
}
|
|
903
|
-
if (
|
|
1270
|
+
if (existsSync6(normalized)) {
|
|
904
1271
|
return readFileSync6(normalized);
|
|
905
1272
|
}
|
|
906
1273
|
const looksLikePath = /[\\/]/.test(normalized) || normalized.endsWith(".pem");
|
|
@@ -1026,17 +1393,17 @@ var DockerodeClient = class {
|
|
|
1026
1393
|
await this.client.getImage(image).remove({ force: true });
|
|
1027
1394
|
}
|
|
1028
1395
|
async pullImage(image, auth) {
|
|
1029
|
-
const stream = await new Promise((
|
|
1396
|
+
const stream = await new Promise((resolve5, reject) => {
|
|
1030
1397
|
const pullOptions = auth ? { authconfig: auth } : void 0;
|
|
1031
1398
|
this.client.pull(image, pullOptions, (err, output) => {
|
|
1032
1399
|
if (err || !output) {
|
|
1033
1400
|
reject(err ?? new Error("docker pull \u8FD4\u56DE\u7A7A\u8F93\u51FA"));
|
|
1034
1401
|
return;
|
|
1035
1402
|
}
|
|
1036
|
-
|
|
1403
|
+
resolve5(output);
|
|
1037
1404
|
});
|
|
1038
1405
|
});
|
|
1039
|
-
await new Promise((
|
|
1406
|
+
await new Promise((resolve5, reject) => {
|
|
1040
1407
|
this.client.modem.followProgress(
|
|
1041
1408
|
stream,
|
|
1042
1409
|
(err) => {
|
|
@@ -1044,7 +1411,7 @@ var DockerodeClient = class {
|
|
|
1044
1411
|
reject(err);
|
|
1045
1412
|
return;
|
|
1046
1413
|
}
|
|
1047
|
-
|
|
1414
|
+
resolve5();
|
|
1048
1415
|
},
|
|
1049
1416
|
() => void 0
|
|
1050
1417
|
);
|
|
@@ -1111,7 +1478,7 @@ var DockerodeClient = class {
|
|
|
1111
1478
|
var createDockerodeClient = (config) => new DockerodeClient(config);
|
|
1112
1479
|
|
|
1113
1480
|
// src/commands/deploy/internal/backend-deploy/dockerode-client/env.ts
|
|
1114
|
-
import { existsSync as
|
|
1481
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
1115
1482
|
import path2 from "node:path";
|
|
1116
1483
|
function stripSurroundingQuotes(value) {
|
|
1117
1484
|
const t = value.trim();
|
|
@@ -1128,7 +1495,7 @@ function loadEnvFromFile(envFilePath) {
|
|
|
1128
1495
|
return {};
|
|
1129
1496
|
}
|
|
1130
1497
|
const targetPath = path2.resolve(envFilePath);
|
|
1131
|
-
if (!
|
|
1498
|
+
if (!existsSync7(targetPath) || !statSync4(targetPath).isFile()) {
|
|
1132
1499
|
return {};
|
|
1133
1500
|
}
|
|
1134
1501
|
const raw = readFileSync7(targetPath, "utf-8");
|
|
@@ -1302,12 +1669,12 @@ function dockerPushImage(params, cwd) {
|
|
|
1302
1669
|
}
|
|
1303
1670
|
|
|
1304
1671
|
// src/commands/deploy/internal/backend-deploy/resolve-dockerfile.ts
|
|
1305
|
-
import { existsSync as
|
|
1672
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1306
1673
|
import path3 from "node:path";
|
|
1307
1674
|
function resolveDockerBuildPaths(cwd) {
|
|
1308
1675
|
const dockerfilePath = path3.join(cwd, "Dockerfile");
|
|
1309
1676
|
Logger.info(`\u67E5\u627EDockerfile\u6587\u4EF6\uFF0C\u8DEF\u5F84: ${dockerfilePath}`);
|
|
1310
|
-
if (!
|
|
1677
|
+
if (!existsSync8(dockerfilePath)) {
|
|
1311
1678
|
throw new Error(`Dockerfile \u4E0D\u5B58\u5728\uFF1A${dockerfilePath}`);
|
|
1312
1679
|
}
|
|
1313
1680
|
Logger.info("\u2713 Dockerfile \u5B58\u5728");
|
|
@@ -1436,11 +1803,11 @@ import { copyFile, readdir as readdir2, stat } from "node:fs/promises";
|
|
|
1436
1803
|
import path7 from "node:path";
|
|
1437
1804
|
|
|
1438
1805
|
// src/commands/deploy/internal/load-apm-dotenv.ts
|
|
1439
|
-
import { existsSync as
|
|
1440
|
-
import { join as
|
|
1806
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
1807
|
+
import { join as join9 } from "node:path";
|
|
1441
1808
|
function loadApmDotEnvIfPresent() {
|
|
1442
|
-
const p =
|
|
1443
|
-
if (!
|
|
1809
|
+
const p = join9(workspaceApmDir(), ".env");
|
|
1810
|
+
if (!existsSync9(p)) {
|
|
1444
1811
|
return;
|
|
1445
1812
|
}
|
|
1446
1813
|
let text;
|
|
@@ -1470,14 +1837,14 @@ function loadApmDotEnvIfPresent() {
|
|
|
1470
1837
|
}
|
|
1471
1838
|
|
|
1472
1839
|
// src/commands/deploy/internal/minio.ts
|
|
1473
|
-
import { statSync as
|
|
1840
|
+
import { statSync as statSync5 } from "node:fs";
|
|
1474
1841
|
import { readdir, readFile } from "node:fs/promises";
|
|
1475
1842
|
import path6 from "node:path";
|
|
1476
1843
|
import * as Minio from "minio";
|
|
1477
1844
|
var DEFAULT_MAX_FILE_SIZE_MB = 50;
|
|
1478
1845
|
async function isDirectoryPath(dir) {
|
|
1479
1846
|
try {
|
|
1480
|
-
const st =
|
|
1847
|
+
const st = statSync5(dir);
|
|
1481
1848
|
return st.isDirectory();
|
|
1482
1849
|
} catch {
|
|
1483
1850
|
return false;
|
|
@@ -1507,7 +1874,7 @@ async function collectFiles(root) {
|
|
|
1507
1874
|
if (e.isDirectory()) {
|
|
1508
1875
|
await walk(abs, rel);
|
|
1509
1876
|
} else if (e.isFile()) {
|
|
1510
|
-
const st =
|
|
1877
|
+
const st = statSync5(abs);
|
|
1511
1878
|
out.push({
|
|
1512
1879
|
absPath: abs,
|
|
1513
1880
|
relativePath: rel.replace(/\\/g, "/"),
|
|
@@ -1569,14 +1936,14 @@ var MinioClient = class {
|
|
|
1569
1936
|
async deleteObjectsByPrefix(bucket, prefix) {
|
|
1570
1937
|
const objectsStream = this.inner.listObjectsV2(bucket, prefix, true);
|
|
1571
1938
|
const keys = [];
|
|
1572
|
-
await new Promise((
|
|
1939
|
+
await new Promise((resolve5, reject) => {
|
|
1573
1940
|
objectsStream.on("data", (obj) => {
|
|
1574
1941
|
if (obj.name) {
|
|
1575
1942
|
keys.push(obj.name);
|
|
1576
1943
|
}
|
|
1577
1944
|
});
|
|
1578
1945
|
objectsStream.on("error", reject);
|
|
1579
|
-
objectsStream.on("end",
|
|
1946
|
+
objectsStream.on("end", resolve5);
|
|
1580
1947
|
});
|
|
1581
1948
|
const chunkSize = 500;
|
|
1582
1949
|
for (let i = 0; i < keys.length; i += chunkSize) {
|
|
@@ -1821,7 +2188,9 @@ function buildProgram() {
|
|
|
1821
2188
|
).action(async () => {
|
|
1822
2189
|
await runUpdate();
|
|
1823
2190
|
});
|
|
1824
|
-
program.command("update-skills").description(
|
|
2191
|
+
program.command("update-skills").description(
|
|
2192
|
+
"\u540C\u6B65\u6280\u80FD\u5230 .apm/skills/\uFF1A\u57FA\u7840\u6280\u80FD\u6765\u81EA CLI \u6A21\u677F\uFF0C\u8865\u5145\u6280\u80FD\u6765\u81EA\u5E73\u53F0"
|
|
2193
|
+
).action(async () => {
|
|
1825
2194
|
await runUpdateSkills();
|
|
1826
2195
|
});
|
|
1827
2196
|
program.command("pull").description(
|
|
@@ -1841,6 +2210,11 @@ function buildProgram() {
|
|
|
1841
2210
|
program.command("update-message-status").description("\u66F4\u65B0\u5E73\u53F0\u4F1A\u8BDD\u6D88\u606F\u72B6\u6001").requiredOption("--id <messageId>", "\u6D88\u606F ID").requiredOption("--status <status>", "CREATED | TYPING | SUCCESS | FAILED").action(async (opts) => {
|
|
1842
2211
|
await runUpdateMessageStatus(opts);
|
|
1843
2212
|
});
|
|
2213
|
+
program.command("connect").description(
|
|
2214
|
+
"\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"
|
|
2215
|
+
).option("--server <url>", "API \u6839\u5730\u5740\uFF0C\u8986\u76D6 config \u4E2D\u7684 baseUrl").action(async (opts) => {
|
|
2216
|
+
await runConnect(opts);
|
|
2217
|
+
});
|
|
1844
2218
|
program.command("branch").description("\u5207\u6362\u6216\u521B\u5EFA\u4F1A\u8BDD\u5206\u652F feat/session-<sessionId>").argument("<sessionId>", "\u6C9F\u901A\u7FA4 ID").option(
|
|
1845
2219
|
"-m, --message <text>",
|
|
1846
2220
|
"\u5DF2\u5728\u76EE\u6807\u5206\u652F\u4E14\u9700\u63D0\u4EA4\u672C\u5730\u6539\u52A8\u65F6\u4F7F\u7528\u7684\u63D0\u4EA4\u8BF4\u660E"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-project-manage-cli",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.9",
|
|
4
4
|
"description": "命令行工具:后续用于调用平台后端 API 完成运维与自动化操作",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"prepublishOnly": "npm run build"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@types/ws": "~8.5.14",
|
|
21
22
|
"@types/node": "^22.0.0",
|
|
22
23
|
"typescript": "~5.6.0",
|
|
23
24
|
"esbuild": "~0.28.0",
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
"node": ">=18"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@cursor/sdk": "~1.0.17",
|
|
33
|
+
"ws": "~8.18.0",
|
|
31
34
|
"listpage-http": "~0.0.318",
|
|
32
35
|
"commander": "~14.0.3",
|
|
33
36
|
"yaml": "~2.8.4",
|
package/template/AGENTS.md
CHANGED
|
@@ -3,20 +3,26 @@
|
|
|
3
3
|
### 回复消息命令
|
|
4
4
|
|
|
5
5
|
- 命令: `apm append-message --id=<消息ID> --content=<消息内容>`
|
|
6
|
-
-
|
|
6
|
+
- 注意事项:
|
|
7
|
+
- 如果消息内容中包含文档地址,不要把前缀也写出来,直接用文档名称即可,并且文档名称要用倒引号包裹
|
|
8
|
+
- 这个是补充消息内容,只要消息没有结束,都可以用这个命令来继续补充发言。
|
|
7
9
|
|
|
8
10
|
### 创建/更新群文档命令
|
|
9
11
|
|
|
10
12
|
- 命令: `apm sync-document <会话ID> --file <文档名称>`
|
|
11
|
-
-
|
|
13
|
+
- 注意事项:`apm sync-document` 的 `--file` 为**文档名称**(如 `PRD.md`),对应磁盘路径 `.apm/sessions/<会话ID>/docs/PRD.md`(群文档在 `docs/` 下)。
|
|
12
14
|
|
|
13
15
|
## 工作流程
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
1. 读取 `.apm/sessions/<会话ID>/session.yaml`,必要时读取`messages.xml` 了解当前会话状态与历史消息
|
|
18
|
+
2. 根据你的名字从 session.yaml 中找到你对应的人设(system_persona)
|
|
19
|
+
3. 根据人设完成用户指定的任务
|
|
20
|
+
4. 任务有阶段行进展或者任务完成后必须使用 `apm append-message` 回复消息
|
|
18
21
|
|
|
19
22
|
## 目录声明
|
|
20
23
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
24
|
+
- APM 指南: `.apm/AGENTS.md`
|
|
25
|
+
- 技能: `.apm/skills/<技能名称>/SKILL.md`,当 `system_persona` 依赖相关技能时可以在这里按需查找
|
|
26
|
+
- 群文档: `.apm/sessions/<会话 ID>/docs/xxxx.md`,当需要相关上下文可以在这里查找
|
|
27
|
+
- 协作规则: `.apm/sessions/<会话 ID>/RULE.md`,在这里可以看到不同成员的协作规则
|
|
28
|
+
- 历史消息记录: `.apm/sessions/<会话 ID>/message.xml`,当需要查看历史消息记录时可以阅读这个文档
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# APM 工作项:按任务实现
|
|
2
|
+
|
|
3
|
+
根据工作项目录下的 **`tasks.md`** 驱动实现:**读规划 → 做代码 → 对账元数据 → 勾选 → 单独 commit**,直至全部完成或遇硬阻塞;上下文以 **`.apm/sessions/<sessionId>/`** 内文件为准。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 输入
|
|
8
|
+
|
|
9
|
+
若本轮未提供 `sessionId`:
|
|
10
|
+
|
|
11
|
+
1. 若对话中已能**唯一**确定工作项目录名,则用之。
|
|
12
|
+
2. 否则直接停止工作即可,**不要**调用 AskUserQuestion,**不要**停下来让用户当场选择。
|
|
13
|
+
|
|
14
|
+
## 停止执行(优先于继续推进)
|
|
15
|
+
|
|
16
|
+
若在实施过程中**发现**任一情况,**立即停止**本技能所驱动的后续实现与勾选(不再继续下一项任务),仅在会话中输出:
|
|
17
|
+
|
|
18
|
+
1. **客观事实**:已读到的文件/路径、与代码或任务条目的**具体矛盾点**(可摘引,避免主观评价)。
|
|
19
|
+
2. **当前进度**:处理到 `tasks.md` 的哪一条、仓库是否已有未提交改动(若有,说明范围)。
|
|
20
|
+
3. **需要用户提供什么**:为继续推进所**缺的信息或决策**(例如:以何者为准、是否接受某类改动、需确认的业务口径)。用**清单**列出,**一句一项**。
|
|
21
|
+
|
|
22
|
+
**禁止**:给出「建议你先改 A / 再改 B」「可选方案 1/2/3」「宜采用…」等**替用户决策**或**行动建议**;不指导用户如何改文档、不预设优先级。
|
|
23
|
+
|
|
24
|
+
**允许**:描述「若不补充某信息则无法继续」的**逻辑关系**(仍不展开成方案)。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 工作项路径与必备文件
|
|
29
|
+
|
|
30
|
+
- **根路径**:**`.apm/sessions/<sessionId>/`**
|
|
31
|
+
- **必备**:**`tasks.md`**(含 `- [ ]` / `- [x]` 待办)。若不存在或无可执行条目:**阻塞**,说明须先完成 **apm-propose**(或手工补齐 `tasks.md`)。
|
|
32
|
+
- **强烈建议读**:**`docs/PRD.md`**(`.apm/sessions/<sessionId>/docs/PRD.md`,范围与验收);实现时按需 **Read** **`proposal.md`**、**`design.md`**、**`specs/`**(与 **`tasks-instruction.md`** 中每条任务的元数据一致)。
|
|
33
|
+
- **进度**:仅由 **`tasks.md`** 中复选框统计「已完成 / 总数」。
|
|
34
|
+
|
|
35
|
+
### 单会话读取(可选减负)
|
|
36
|
+
|
|
37
|
+
同一轮连续执行多项任务时:**`docs/PRD.md` / `design.md` / 各 spec** 若已在会话内读过且无疑虑,不必每个任务重复全文 Read;以**当前任务行**及元数据指向的 spec/设计段落为准,必要时对文件 **Read(偏移)** 或再读全文。
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Steps
|
|
42
|
+
|
|
43
|
+
### 1. 锚定工作项
|
|
44
|
+
|
|
45
|
+
确定 **`sessionId`** 与工作项根路径;声明 **使用工作项:`<sessionId>`**(若依上节规则自动选定,须说明如何覆盖)。
|
|
46
|
+
|
|
47
|
+
### 2. 读取 `tasks.md` 并判断状态
|
|
48
|
+
|
|
49
|
+
- **Read** **`.apm/sessions/<sessionId>/tasks.md`**。
|
|
50
|
+
- 若文件缺失、为空、或无任何 `- [ ]`:**停止**,提示先具备可执行 `tasks`(规划阶段)。
|
|
51
|
+
- 若全部已为 `- [x]`:**可**直接汇报「已全部完成」(仅作事实陈述,不建议是否提交或发 MR)。
|
|
52
|
+
|
|
53
|
+
### 3. 读取实现上下文(按任务需要)
|
|
54
|
+
|
|
55
|
+
开始**第一个**未勾选任务前,至少 **Read**:
|
|
56
|
+
|
|
57
|
+
- **`docs/PRD.md`**(`.apm/sessions/<sessionId>/docs/PRD.md`;若存在,与验收相关条款)
|
|
58
|
+
- 与任务相关的 **`design.md`** 片段、**`specs/`** 下对应文件(见任务条目的 **需求编号** / 描述)
|
|
59
|
+
|
|
60
|
+
不要求每次任务都重读全部规划文件;以 **`tasks.md` 当前项** + **缺口再 Read** 为准。
|
|
61
|
+
|
|
62
|
+
### 4. 展示当前进度
|
|
63
|
+
|
|
64
|
+
在对话中简要展示:
|
|
65
|
+
|
|
66
|
+
- **工作项**:`sessionId`
|
|
67
|
+
- **进度**:「N/M 个任务已完成」(由 `tasks.md` 勾选统计)
|
|
68
|
+
- **当前将处理**:下一条 `- [ ]` 的摘要(含组号/编号如 `2.1`)
|
|
69
|
+
|
|
70
|
+
### 5. 实现任务(循环直至完成或阻塞)
|
|
71
|
+
|
|
72
|
+
对每一条 **未勾选** 任务(建议按 `tasks.md` 自上而下顺序):
|
|
73
|
+
|
|
74
|
+
1. **说明**正在处理哪一项(编号 + 简述)。
|
|
75
|
+
2. **实现**所需代码/配置/迁移等;改动保持**最小**、与该项描述一致。
|
|
76
|
+
3. **标记完成前**在回复或备注中收集依据(与 **`tasks-instruction.md`** 子列表字段对齐即可):
|
|
77
|
+
- **需求编号**:本实现对应 specs / tasks 中的哪条需求或场景。
|
|
78
|
+
- **预期改动路径** 与 **实际改动文件**:若有合理偏差,简要说明原因。
|
|
79
|
+
- **验证用例编号**(若任务中有)。
|
|
80
|
+
- **验证结果** / **完成标准**:如何确认本项已达成(可简短)。
|
|
81
|
+
4. 仅在依据已齐、且自洽通过后,将 **`tasks.md`** 中对应行 **`- [ ]` 改为 `- [x]`**。
|
|
82
|
+
5. **Git**:本待办对应的**实现 + 勾选**等改动,**单独 `git commit` 一次**(**一项待办 = 一个 commit**;勿把多项待办混在同一 commit)。提交信息建议包含 `requirementId` 与任务编号或简述。
|
|
83
|
+
|
|
84
|
+
**何时停止(不继续下一项)**——与上文 **「停止执行」** 一致:
|
|
85
|
+
|
|
86
|
+
- **`design` / `specs` / `tasks` 与当前代码或彼此严重不一致**,以致无法按任务字面含义在**不臆造**的前提下完成 → **按「停止执行」输出**(事实 + 需用户提供的材料),**不**建议先改哪份文档、不改哪些。
|
|
87
|
+
- 命令失败、环境错误、权限等**硬阻塞** → 说明原因、失败点、已执行步骤;**不**给排障步骤或替代命令建议(除非任务本身要求执行某命令且该命令已写在任务/文档中)。
|
|
88
|
+
- 用户中断。
|
|
89
|
+
|
|
90
|
+
**何时仍可继续**(仅当满足):任务表述略含糊,但可在 **docs/PRD.md / design / specs / tasks** 已有文字内**自洽**完成;若有**假设**,在回复中写清**假设**(仍不反问)。一旦假设会触及「以谁为准」的**产品/架构决策**,转 **「停止执行」**。
|
|
91
|
+
|
|
92
|
+
### 6. 完成或暂停时展示状态
|
|
93
|
+
|
|
94
|
+
- **本会话**完成了哪些任务(可列勾选项摘要)。
|
|
95
|
+
- **总体进度**:「N/M 个任务已完成」。
|
|
96
|
+
- 若**全部完成**:仅陈述事实(进度与勾选状态),**不**建议后续流程(MR/发布/归档等)。
|
|
97
|
+
- 若**暂停**:按 **「停止执行」** 或硬阻塞格式输出,**无**「可选后续」或方案建议。
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 实现过程中的输出(示例)
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
## 正在实施:<sessionId>
|
|
105
|
+
|
|
106
|
+
处理任务 3/7:2.1 实现导出接口
|
|
107
|
+
[...实现过程...]
|
|
108
|
+
✓ 任务完成 · commit <short-sha> <subject>
|
|
109
|
+
|
|
110
|
+
处理任务 4/7:2.2 …
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 全部完成时的输出(示例)
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
## 实现完成
|
|
119
|
+
|
|
120
|
+
**工作项:** <sessionId>
|
|
121
|
+
**进度:** 7/7 个任务已完成 ✓
|
|
122
|
+
|
|
123
|
+
### 本会话已完成
|
|
124
|
+
- [x] …
|
|
125
|
+
- [x] …
|
|
126
|
+
|
|
127
|
+
(每项待办均已对应独立 commit。)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 暂停时的输出(示例)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
## 实现已暂停
|
|
136
|
+
|
|
137
|
+
**工作项:** <sessionId>
|
|
138
|
+
**进度:** 4/7 个任务已完成
|
|
139
|
+
|
|
140
|
+
### 事实与原因
|
|
141
|
+
<客观描述:文件/行/与任务或代码的矛盾点>
|
|
142
|
+
|
|
143
|
+
### 需要用户提供(或决策)
|
|
144
|
+
- …
|
|
145
|
+
- …
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
(无方案列表、无「建议」;用户补充信息后可再次运行本技能。)
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 约束
|
|
153
|
+
|
|
154
|
+
- 持续执行待办直至完成、**停止执行**或硬阻塞。
|
|
155
|
+
- 开始前须已能对照 **`tasks.md`** 与 **`docs/PRD.md`**(及任务所需的 design/spec);**不要**在未读清当前任务依赖时盲改。
|
|
156
|
+
- 任务表述有歧义时,仅在**无需用户决策**的前提下依据 **specs / design / docs/PRD.md** 推断;一旦涉及**以谁为准**或**严重不一致**,**停止执行**(见上文)。
|
|
157
|
+
- **不**因「发现规划与代码不符」而**主动建议**修改 `design.md` / `specs/` / `tasks.md`;**停止**并说明事实与需用户提供的材料即可。
|
|
158
|
+
- 代码改动保持最小、与**当前任务**范围一致。
|
|
159
|
+
- 未完成需求/验证对账前,**不要**将任务勾为完成。
|
|
160
|
+
- 验证失败或依据不足时,保持 **`- [ ]`**,并明确写出缺口。
|
|
161
|
+
- 若任务元数据缺失(需求编号、预期改动路径、完成标准等),在可能范围内于实现前**补全或按 tasks 模板推断**,并在说明中注明。
|
|
162
|
+
- 每完成一项任务并记录依据后,**立即**更新勾选并**随即**单独 `git commit`。
|
|
163
|
+
- 遇硬阻塞时暂停并说明原因(**不**给出替代命令或排查建议,除非任务/文档已写明);
|
|
164
|
+
- 需求不清且无法自洽推断时,**停止执行**并列出需用户提供的材料;**不反问用户**。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 与规划流程的衔接
|
|
169
|
+
|
|
170
|
+
- **`tasks.md`** 须由 **apm-propose**(或等价流程)生成并符合 **`tasks-instruction.md`** 格式(`- [ ]`、元数据子列表等),否则本技能无法可靠追踪。
|
|
171
|
+
- 若执行中发现 **`design` / `specs` 与代码严重不一致**:**停止执行**,按上文输出事实与**需用户提供**的决策/信息;**不**建议先改哪类工件、不代用户排优先级。
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 流动工作流
|
|
176
|
+
|
|
177
|
+
- **可分段调用**:可在部分任务完成后结束会话,下次同一工作项继续。
|
|
178
|
+
- **规划修订**:由用户在**流程外**修订 `design.md` / `specs/` / `tasks.md` 后,再触发本技能;本技能执行中**不因发现不符**而主动改规划文件或提出修改方案。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## 工作流程
|
|
2
|
+
|
|
3
|
+
### 步骤1: 阅读部署文档
|
|
4
|
+
|
|
5
|
+
使用**Read**工具阅读 `.apm/deploy/README.md`(这个目录可能被忽略了,也可能不存在)
|
|
6
|
+
|
|
7
|
+
### 步骤2: 判断能否部署
|
|
8
|
+
|
|
9
|
+
遇到以下情况拒绝部署,直接退出即可
|
|
10
|
+
1. 没有找到部署文档,或者部署文档没有可执行的部署命令(或者说部署不可执行)
|
|
11
|
+
2. 用户没有明确部署环境
|
|
12
|
+
|
|
13
|
+
### 步骤3: 按照文档执行部署
|
|
14
|
+
|
|
15
|
+
**严格对照** README:顺序、工作目录(若文档指定 `cd`)、环境变量、所用 CLI(如 `rush`、`docker`、`kubectl` 等)均以文档为准;将步骤 3 得到的 **`env` 等**按 README 约定注入命令或环境(禁止凭猜测填值)。**默认在 `<repoRoot>` 执行**;
|
|
16
|
+
|
|
17
|
+
## Guardrails
|
|
18
|
+
|
|
19
|
+
- **gitignore 不等于不存在**:`.apm` 下文件可能不入 Git 索引;**禁止**用「搜索无结果」代替 **Read** 或磁盘校验。
|
|
20
|
+
- **不臆造**:README 未写的命令、环境、目标集群/命名空间,**不补充**;工作项 YAML **未提供**的字段**不得**编造。
|
|
21
|
+
- **不报假成功**:命令失败或未完成 README 目标时,结论必须为「否」或等价表述。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## 工作流程
|
|
2
|
+
1. 选择开发模式:**Quick/Spec**
|
|
3
|
+
|
|
4
|
+
**Quick 开发**(满足越多越适用):
|
|
5
|
+
- 影响范围局部:少量文件或单一层次(例如仅前端组件、或仅一个后端模块小改)。
|
|
6
|
+
- 无新表结构/大规模迁移/权限模型变更。
|
|
7
|
+
- PRD 验收点清晰且数量少(经验上 **≤3** 条独立验收维度)。
|
|
8
|
+
- 不需要跨多服务的架构裁定即可开工。
|
|
9
|
+
**Spec 开发**:(命中任一条即可):
|
|
10
|
+
- 前后端联动、多包改造或新公共抽象。
|
|
11
|
+
- 新数据模型、迁移、或安全/审计/权限相关。
|
|
12
|
+
- PRD 范围大、条款多,或存在明显「待确认/多方案」需先规划。
|
|
13
|
+
- 评估认为不先产出 **proposal / design / specs /
|
|
14
|
+
|
|
15
|
+
2. 如果为 **Quick 开发**(子Agent中)执行:
|
|
16
|
+
|
|
17
|
+
+ 父 Agent 已通过 **Read** 掌握 `PRD.md`;若启动新子 Agent,在委派提示中写明会话ID、消息ID、工作项路径、以及「实现须严格对照 PRD,改动范围最小化」。
|
|
18
|
+
+ 使用 **Task** 工具,`subagent_type: generalPurpose`,**readonly: false**,委派子 Agent:
|
|
19
|
+
- 按需 **Read** `.apm/sessions/<会话ID>/docs/PRD.md`
|
|
20
|
+
- 按 PRD 直接改代码;遵守本仓库构建与依赖约定(AGENTS.md)。
|
|
21
|
+
- 完成后在返回中说明:改了哪些路径、如何对照验收、是否通过本地可执行的检查(若子 Agent 跑了 `rushx build` / 测试等则写明结果)。
|
|
22
|
+
|
|
23
|
+
3. 如果为 **Spec 开发** (子Agent中)执行:
|
|
24
|
+
|
|
25
|
+
- 父 Agent **Read** **apm-propose**、**apm-apply-change** 的 `SKILL.md`(**仅** `.apm/skills/` 下路径,见上节)。
|
|
26
|
+
- **子 Agent A(规划)**:Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-propose/SKILL.md` 并完整遵循:在 `.apm/sessions/<sessionId>/` 生成 **proposal、design、specs、tasks** 等工件(顺序与依赖以 SKILL 为准)。
|
|
27
|
+
- **子 Agent B(实现)**:待 A 成功落盘后,再 Task `generalPurpose`,提示其自行 **Read** `.apm/skills/apm-apply-change/SKILL.md` 并完整遵循:按 **`tasks.md`** 驱动实现与勾选;遵守该技能中的停止条件与 commit 约定。
|
|
28
|
+
- 若 **apm-propose** 未产出可用 **`tasks.md`**,不得强行进入 **apm-apply-change**;表格中标记阻塞原因。
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# APM 工作项:规划工件
|
|
2
|
+
|
|
3
|
+
在 `.apm/sessions/<sessionId>/docs/PRD.md` 上生成 **proposal → design / specs → tasks** 四类规划工件。各工件写什么、依赖谁,见下方对应章节;**不要**把 SKILL 说明或内部推理写进产出文件。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 输入
|
|
8
|
+
|
|
9
|
+
| 字段 | 规则 |
|
|
10
|
+
| --------------- | ------------------------------------------------------ |
|
|
11
|
+
| **`sessionId`** | **必填**。与 `.apm/sessions/<sessionId>/` 目录名一致。 |
|
|
12
|
+
| **需求正文** | **唯一权威**:`.apm/sessions/<sessionId>/docs/PRD.md`。 |
|
|
13
|
+
|
|
14
|
+
用户补充仅在与 PRD 兼容时写入,并标注 **「会话补充(PRD 未载明)」**。PRD 未写明的**不反问用户**,写入后续工件的假设 / 待确认。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 流程
|
|
19
|
+
|
|
20
|
+
**顺序**:`proposal` → `design` 与 `specs`(可并行,均依赖 proposal;specs 不依赖 design)→ `tasks`(依赖 design 与 specs 均已落盘)。
|
|
21
|
+
|
|
22
|
+
用 **TodoWrite** 跟踪四工件(`ready` / `blocked` / `done`),**每完成一工件即落盘并标 `done`**;依赖未满足时**不得**写下一工件。
|
|
23
|
+
|
|
24
|
+
1. **Read `docs/PRD.md` 全文**(路径:`.apm/sessions/<sessionId>/docs/PRD.md`);必要时 **SemanticSearch** / **Read** 仓库代码,便于 tasks 中 **预期改动路径** 可落地。
|
|
25
|
+
2. 按解锁顺序撰写各工件(见下方章节)。
|
|
26
|
+
3. 全部完成后汇总:工作项路径、已创建文件、PRD 与各工件的对应关系(一两句);回复含各工件一句话用途。
|
|
27
|
+
|
|
28
|
+
### 单会话读取策略
|
|
29
|
+
|
|
30
|
+
同一会话连续跑完时,可省略重复 Read,但**不得**省略依赖关系;断点续写或新会话须按各工件「依赖」重新 Read 磁盘文件。
|
|
31
|
+
|
|
32
|
+
| 文件 | 建议 |
|
|
33
|
+
| ---------------------- | ----------------------------------------------------------------- |
|
|
34
|
+
| `docs/PRD.md` | 进入流程时至少读一次全文;后续按需再读。 |
|
|
35
|
+
| `proposal.md` | 落盘后写 design/specs 时,若会话内已有刚写入的全文可不重复 Read。 |
|
|
36
|
+
| `design.md` / `specs/` | 写 tasks 时若无可靠记忆须 Read;以落盘文件为准。 |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## proposal 工件
|
|
41
|
+
|
|
42
|
+
**路径**:`.apm/sessions/<sessionId>/proposal.md`
|
|
43
|
+
|
|
44
|
+
**依赖**:`docs/PRD.md`(全文)
|
|
45
|
+
|
|
46
|
+
以 PRD 为事实来源写变更提案(**Why**,不写实现细节)。精短即可。
|
|
47
|
+
|
|
48
|
+
**Capabilities** 是 proposal 与 specs 的契约:每一项须在后续 `specs/` 可追溯。
|
|
49
|
+
|
|
50
|
+
- **New Capabilities**:每条对应 `specs/<kebab-name>.md`(如 `user-auth`)。
|
|
51
|
+
- **Modified Capabilities**:规格层行为变更;无则写「无」。
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Why
|
|
55
|
+
|
|
56
|
+
<!-- 1~2 句:解决什么、为何是现在 -->
|
|
57
|
+
|
|
58
|
+
## What Changes
|
|
59
|
+
|
|
60
|
+
<!-- 变更要点;破坏性变更标注 BREAKING -->
|
|
61
|
+
|
|
62
|
+
## Capabilities
|
|
63
|
+
|
|
64
|
+
### New Capabilities
|
|
65
|
+
|
|
66
|
+
- `<name>`: <brief description>
|
|
67
|
+
|
|
68
|
+
### Modified Capabilities
|
|
69
|
+
|
|
70
|
+
- `<existing-name>`: <what requirement behavior changes> <!-- 无则写 无 -->
|
|
71
|
+
|
|
72
|
+
## Impact
|
|
73
|
+
|
|
74
|
+
<!-- 受影响代码、API、依赖、系统 -->
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## design 工件
|
|
80
|
+
|
|
81
|
+
**路径**:`.apm/sessions/<sessionId>/design.md`
|
|
82
|
+
|
|
83
|
+
**依赖**:`docs/PRD.md`、`proposal.md`
|
|
84
|
+
|
|
85
|
+
说明 **如何实现**(HOW):架构与决策,不写逐行代码,不替代 tasks 里的步骤枚举。
|
|
86
|
+
|
|
87
|
+
跨模块、新依赖、安全/性能/迁移复杂或方案待拍板时写完整版;否则可精简,但保留 **Context** 与 **Decisions**。
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
## Context
|
|
91
|
+
|
|
92
|
+
## Goals / Non-Goals
|
|
93
|
+
|
|
94
|
+
**Goals:**
|
|
95
|
+
**Non-Goals:**
|
|
96
|
+
|
|
97
|
+
## Decisions
|
|
98
|
+
|
|
99
|
+
<!-- 每项:备选 + 取舍理由 -->
|
|
100
|
+
|
|
101
|
+
## Risks / Trade-offs
|
|
102
|
+
|
|
103
|
+
<!-- [风险] → 缓解 -->
|
|
104
|
+
|
|
105
|
+
## Migration Plan
|
|
106
|
+
|
|
107
|
+
<!-- 无则写 无 / 不适用 -->
|
|
108
|
+
|
|
109
|
+
## Open Questions
|
|
110
|
+
|
|
111
|
+
<!-- 无则写 无 -->
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## specs 工件
|
|
117
|
+
|
|
118
|
+
**路径**:`.apm/sessions/<sessionId>/specs/`
|
|
119
|
+
|
|
120
|
+
**依赖**:`docs/PRD.md`、`proposal.md`(不依赖 design)
|
|
121
|
+
|
|
122
|
+
定义系统 **应做什么**;与 proposal **Capabilities** 逐条对齐,**不得**虚构 PRD/proposal 未出现的能力。
|
|
123
|
+
|
|
124
|
+
**文件组织**:一能力一文件 `specs/<短横线名称>.md`(或 `specs/<名称>/规格.md`,同一工作项内择一,勿混用)。
|
|
125
|
+
|
|
126
|
+
**结构**:
|
|
127
|
+
|
|
128
|
+
- 分块(`##`):**新增需求** / **变更需求** / **移除需求** / **重命名需求**(按需组合;纯新增只用「新增需求」)。
|
|
129
|
+
- 需求:`**### 需求:<名称>**`;行为用 **须、必须**。
|
|
130
|
+
- 场景:`**#### 场景:<名称>**`(固定四个 `#`);正文 `- **当** …` / `- **则** …`。
|
|
131
|
+
- **每条需求至少一个场景**;变更需求须写**完整替换段落**(从需求到全部场景),禁止只贴片段。
|
|
132
|
+
|
|
133
|
+
```markdown
|
|
134
|
+
## 新增需求
|
|
135
|
+
|
|
136
|
+
### 需求:<名称>
|
|
137
|
+
|
|
138
|
+
系统须 …
|
|
139
|
+
|
|
140
|
+
#### 场景:<名称>
|
|
141
|
+
|
|
142
|
+
- **当** …
|
|
143
|
+
- **则** …
|
|
144
|
+
|
|
145
|
+
## 变更需求
|
|
146
|
+
|
|
147
|
+
<!-- 完整替换后的需求 + 全部场景 -->
|
|
148
|
+
|
|
149
|
+
## 移除需求
|
|
150
|
+
|
|
151
|
+
### 需求:<名称>
|
|
152
|
+
|
|
153
|
+
**原因**:… **迁移说明**:…
|
|
154
|
+
|
|
155
|
+
## 重命名需求
|
|
156
|
+
|
|
157
|
+
- **原名称:** … **新名称:** …
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## tasks 工件
|
|
163
|
+
|
|
164
|
+
**路径**:`.apm/sessions/<sessionId>/tasks.md`
|
|
165
|
+
|
|
166
|
+
**依赖**:`docs/PRD.md`、`proposal.md`、`design.md`、`specs/`
|
|
167
|
+
|
|
168
|
+
拆成可勾选待办;**apm-apply-change** 依赖 `- [ ]` / `- [x]` 推进。
|
|
169
|
+
|
|
170
|
+
**格式**:
|
|
171
|
+
|
|
172
|
+
- 行首 **`- [ ]`**;分组 **`## 1. 名称`**;编号 **`- [ ] 1.1 说明`**(组号.序号)。
|
|
173
|
+
- 每项下方缩进子列表:**需求编号**(对应 specs 中需求/场景)、**预期改动路径**、**验证用例编号**(可选)、**完成标准**。
|
|
174
|
+
- **做什么**以 specs 为准,**改哪里**以 design 为准;按技术依赖排序。
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
## 1. 数据层
|
|
178
|
+
|
|
179
|
+
- [ ] 1.1 新增合同状态字段
|
|
180
|
+
- **需求编号**:需求:合同状态同步(见 specs/contract.md)
|
|
181
|
+
- **预期改动路径**:`servers/be/prisma/schema.prisma`
|
|
182
|
+
- **完成标准**:迁移可执行;已有合同默认状态正确
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Guardrails
|
|
188
|
+
|
|
189
|
+
- 后写须与 PRD 及已落盘前序文件一致。
|
|
190
|
+
- 四个工件缺一不可(specs 至少一个 `.md`);落盘非空后再进下一工件。
|
|
191
|
+
- **默认不覆盖**已有规划文件;用户明确要求「整目录覆盖重生成」时除外。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## 输入
|
|
2
|
+
|
|
3
|
+
需求文档:使用 **Read** 工具读 `.apm/sessions/<sessionId>/docs/PRD.md`;若文档存在才进行下面的需求评审流程,否则直接退出并输出:没有看到需求内容。
|
|
4
|
+
|
|
5
|
+
## 评审目标
|
|
6
|
+
|
|
7
|
+
1. 梳理需求边界:发现文档中未写清的口径
|
|
8
|
+
2. 调研实现难度:在实现难度较高时给出提示
|
|
9
|
+
3. 禁止为了凑问题而提问,当 PRD 已足够清晰且无高难度改造时,可直接说此需求没问题
|
|
10
|
+
4. 你只需要站在自己的角度去评审,禁止站在全局思考问题。结合代码进行评审即可:
|
|
11
|
+
- 前端关注 UI 交互还原,交互逻辑的各种边界,不考虑一些与后端接口之类的问题
|
|
12
|
+
- 后端则关注数据表设计,需求对应的数据接口是否合理,能否获取
|
|
13
|
+
- 全栈则关注前面两者,可以把这些问题合并起来提出来
|
|
14
|
+
|
|
15
|
+
**难度高的定义**:预估改动的文件可能比较多,超过 10 个;或者改动逻辑比较复杂;
|
|
16
|
+
|
|
17
|
+
## 表达规范
|
|
18
|
+
|
|
19
|
+
1. **产品语言优先**:页面定位用「医德考评弹窗」「行风办审批页」等说法;**禁止**用文件路径、组件名、函数名作为论据主体。
|
|
20
|
+
2. **后端可略宽**:必要时可写**表名 / 主表字段名**帮助产品理解数据口径,但仍避免贴大段代码或接口路径。
|
|
21
|
+
3. **锚定 PRD**:当需要引用到需求文档中的某一条时可以直接说行号,比如:需求原文[1-3]说 xxx;
|
|
22
|
+
4. **问题 = 文档缺口**:只写 PRD **未写清**且**影响本端理解边界**的点。已写清楚的规则不要重复质疑。
|
|
23
|
+
|
|
24
|
+
## 工作流程
|
|
25
|
+
|
|
26
|
+
### 步骤 1: 读取 PRD 内容,具体见第一节输入输入
|
|
27
|
+
|
|
28
|
+
### 步骤 2: 理解代码,评估 PRD
|
|
29
|
+
|
|
30
|
+
### 步骤 3: 回复用户
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## 输入
|
|
2
|
+
1. 口头需求要求:如果是用户口头需求要求则根据下面需求模板编写PRD
|
|
3
|
+
2. 已有PRD+用户提要求:这个场景是润色现有的PRD文档,一般是原有PRD中的一些问题被解决了
|
|
4
|
+
|
|
5
|
+
**注意**:
|
|
6
|
+
- 编写/润色PRD时禁止提出额外的问题,一切以用户要求为准。
|
|
7
|
+
- 已有PRD的地址为: `../../sessions/<需求ID>/docs/PRD.md`,可用 **Read** 工具读取。
|
|
8
|
+
- 需求要始终保持干净,模板规定以外的,比如历史沟通记录不需要
|
|
9
|
+
|
|
10
|
+
## 工作流程
|
|
11
|
+
### 步骤1: 读取PRD
|
|
12
|
+
### 步骤2: 按照下面的模板要求编写PRD
|
|
13
|
+
### 步骤3: 同步PRD到远程
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 需求模板
|
|
17
|
+
### 正文骨架
|
|
18
|
+
```markdown
|
|
19
|
+
## 背景
|
|
20
|
+
(1 ~ 3 句:业务背景、要解决的问题、预期效果,如果没有可空着,不要强行编)
|
|
21
|
+
|
|
22
|
+
## 范围
|
|
23
|
+
- **包含**:…
|
|
24
|
+
- **不包含**:…
|
|
25
|
+
|
|
26
|
+
## 需求说明
|
|
27
|
+
|
|
28
|
+
### 需求点 1:[简短名称]
|
|
29
|
+
- …
|
|
30
|
+
- …
|
|
31
|
+
|
|
32
|
+
### 需求点 2:…
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 需求点编写规范
|
|
36
|
+
|
|
37
|
+
每条 bullet 写清一个可验收点,优先覆盖:
|
|
38
|
+
|
|
39
|
+
| 类型 | 写法要点 |
|
|
40
|
+
| --------- | ------------------------------------------------------ |
|
|
41
|
+
| 展示/选项 | 展示或隐藏;保留/去掉哪些选项;新建默认值 |
|
|
42
|
+
| 文案 | 原名称 → 新名称;界面、提示、校验文案一并统一 |
|
|
43
|
+
| 按钮 | 场景 + 按钮名 + 行为(对照哪个既有按钮、是否仅改文案) |
|
|
44
|
+
| 校验 | 场景 + 字段展示条件 + 必填时机(暂存 / 提交分别怎样) |
|
|
45
|
+
| 边界 | **不考虑** …(历史数据、迁移、导出等明确排除项) |
|
|
46
|
+
|
|
47
|
+
材料中的技术名可译为业务表述,例如:`audit1` → 一级审批;保存不推进流程 → **暂存**;办理并推进流程 → **提交**。
|
|
48
|
+
|
|
49
|
+
编写示例如下:
|
|
50
|
+
|
|
51
|
+
```markdown
|
|
52
|
+
### 需求点 2:新增/编辑弹窗底部操作
|
|
53
|
+
|
|
54
|
+
- 在**新增、编辑**场景下,弹窗底部新增 **「暂存」** 按钮。
|
|
55
|
+
- **「暂存」**:沿用原 **「确定」** 按钮的业务逻辑(含校验与保存/流程行为),**仅将按钮展示文案改为「暂存」**。
|
|
56
|
+
|
|
57
|
+
### 需求点 3:「科室医德考评小组人员名单」字段
|
|
58
|
+
|
|
59
|
+
- 原字段展示名 **「科室医德考评人员名单」** 统一改为 **「科室医德考评小组人员名单」**;凡界面、提示、校验文案等涉及该名称处**一并修改**。
|
|
60
|
+
- 当流程处于 **一级审批** 且该字段**展示**时,须校验为**必填**(在展示场景下触发,而非所有保存入口一律校验)。
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 自检列表
|
|
64
|
+
- [ ] 含 **背景与目标、范围、需求说明** 三章,叙述完整
|
|
65
|
+
- [ ] 2 分钟内可通读;每个需求点 2 ~ 5 条 bullet,一层列表
|
|
66
|
+
- [ ] 场景、按钮行为、校验时机表述清楚,前后一致
|
|
67
|
+
- [ ] 「范围」与需求点中的 **「不考虑」** 口径一致
|
|
68
|
+
- [ ] 全文使用业务语言,术语统一
|
|
69
|
+
- [ ] 仅材料明确涉及时才有「非功能」「待确认」
|