@co0ontty/wand 4.71.2 → 4.72.0-beta.gfc56736
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/build-info.json +4 -4
- package/dist/git-quick-commit.d.ts +12 -2
- package/dist/git-quick-commit.js +80 -12
- package/dist/iteration-log.d.ts +133 -0
- package/dist/iteration-log.js +293 -0
- package/dist/milestone-scope.d.ts +19 -0
- package/dist/milestone-scope.js +22 -0
- package/dist/missions.js +14 -2
- package/dist/process-manager.js +7 -2
- package/dist/server-session-routes.js +95 -2
- package/dist/server-task-routes.js +39 -11
- package/dist/server-workspace-routes.js +23 -6
- package/dist/session-topic.d.ts +4 -0
- package/dist/session-topic.js +20 -1
- package/dist/storage.d.ts +43 -2
- package/dist/storage.js +201 -12
- package/dist/structured-session-manager.js +7 -2
- package/dist/task-types.d.ts +47 -1
- package/dist/task-types.js +6 -0
- package/dist/web-ui/content/scripts.js +260 -116
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
5
|
-
"channel": "
|
|
2
|
+
"commit": "fc5673635615d567540a2a3e636041a4a3422cad",
|
|
3
|
+
"builtAt": "2026-09-21T22:39:08.943Z",
|
|
4
|
+
"version": "4.72.0-beta.gfc56736",
|
|
5
|
+
"channel": "beta"
|
|
6
6
|
}
|
|
@@ -2,7 +2,7 @@ import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, Sessio
|
|
|
2
2
|
export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
|
|
3
3
|
/** Non-blocking status implementation used by HTTP routes. */
|
|
4
4
|
export declare function getGitStatusAsync(cwd: string): Promise<GitStatusResult>;
|
|
5
|
-
interface QuickCommitOptions {
|
|
5
|
+
interface QuickCommitOptions extends CommitInputOptions {
|
|
6
6
|
cwd: string;
|
|
7
7
|
language: string;
|
|
8
8
|
provider?: SessionProvider;
|
|
@@ -33,6 +33,16 @@ export declare class QuickCommitError extends Error {
|
|
|
33
33
|
readonly code: QuickCommitErrorCode;
|
|
34
34
|
constructor(message: string, code: QuickCommitErrorCode);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 生成 commit message / tag 的输入源。
|
|
38
|
+
* - iterationDigest:本轮迭代的提示词标题清单(默认输入,省 token、不读代码);
|
|
39
|
+
* - includeDiff:仍然附上完整 diff,用于提示词不够、需要逐行核对时;
|
|
40
|
+
* - 两个都没有时保持旧行为:直接读 diff。
|
|
41
|
+
*/
|
|
42
|
+
export interface CommitInputOptions {
|
|
43
|
+
iterationDigest?: string;
|
|
44
|
+
includeDiff?: boolean;
|
|
45
|
+
}
|
|
36
46
|
/**
|
|
37
47
|
* Run a lightweight AI request through the selected Commit source. API mode
|
|
38
48
|
* exhausts its profile chain and then falls back once to the current-session
|
|
@@ -44,7 +54,7 @@ export interface GenerateCommitMessageResult {
|
|
|
44
54
|
/** AI-suggested next tag derived from the staged diff and the latest existing tag. */
|
|
45
55
|
suggestedTag?: string;
|
|
46
56
|
}
|
|
47
|
-
export declare function generateCommitMessageOnly(cwd: string, language: string, ai?: QuickCommitAiOptions): Promise<GenerateCommitMessageResult>;
|
|
57
|
+
export declare function generateCommitMessageOnly(cwd: string, language: string, ai?: QuickCommitAiOptions, options?: CommitInputOptions): Promise<GenerateCommitMessageResult>;
|
|
48
58
|
interface TagHeadOptions {
|
|
49
59
|
cwd: string;
|
|
50
60
|
language: string;
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -23,6 +23,8 @@ const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
|
|
|
23
23
|
const DIRECT_API_PROFILE_TIMEOUT_MS = 20_000;
|
|
24
24
|
const QUICK_COMMIT_CLI_TIMEOUT_MS = 120_000;
|
|
25
25
|
const MAX_DIFF_FOR_AI = 100_000;
|
|
26
|
+
// 迭代提示词模式下只给文件清单做核对,不需要完整 diff:单次上限比 diff 小一个量级。
|
|
27
|
+
const MAX_WORKTREE_SUMMARY_FOR_AI = 4_000;
|
|
26
28
|
const GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
27
29
|
function runGitAsync(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
28
30
|
return runGitAsyncBase(args, cwd, { timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
@@ -639,10 +641,71 @@ async function collectWorkingTreeDiff(cwd) {
|
|
|
639
641
|
}
|
|
640
642
|
return diff;
|
|
641
643
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
+
/** 有迭代提示词时用的轻量改动清单:只列文件与增删行数,不读任何代码内容。 */
|
|
645
|
+
async function collectWorkingTreeSummary(cwd) {
|
|
646
|
+
const parts = [];
|
|
647
|
+
for (const args of [["diff", "HEAD", "--stat"], ["diff", "--cached", "--stat"]]) {
|
|
648
|
+
try {
|
|
649
|
+
const stat = await runGitAsync(args, cwd, 5000);
|
|
650
|
+
if (stat) {
|
|
651
|
+
parts.push(stat);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// 没有 HEAD(初始仓库)或没有 staged 内容都会走到这里,换下一个候选。
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const untracked = await runGitAllowEmptyAsync(["ls-files", "--others", "--exclude-standard"], cwd, 3000);
|
|
661
|
+
if (untracked.trim())
|
|
662
|
+
parts.push(`未跟踪文件:\n${untracked.trim()}`);
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// 可选信息,取不到就算了:提示词清单已经够模型总结。
|
|
666
|
+
}
|
|
667
|
+
if (parts.length === 0)
|
|
668
|
+
return "(没有可见的文件改动)";
|
|
669
|
+
const summary = parts.join("\n");
|
|
670
|
+
return summary.length > MAX_WORKTREE_SUMMARY_FOR_AI
|
|
671
|
+
? `${summary.slice(0, MAX_WORKTREE_SUMMARY_FOR_AI)}\n...(文件清单已截断)`
|
|
672
|
+
: summary;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* 拼出「要总结什么」的输入块。
|
|
676
|
+
* 有迭代提示词时默认只附文件清单:commit message 要的是「我让你们改了什么」,
|
|
677
|
+
* 不是逐行 diff,省下的就是真金白银的 token。
|
|
678
|
+
*/
|
|
679
|
+
async function buildCommitPromptInput(cwd, options) {
|
|
680
|
+
const digest = options.iterationDigest?.trim();
|
|
681
|
+
if (digest) {
|
|
682
|
+
const summary = await collectWorkingTreeSummary(cwd);
|
|
683
|
+
const diff = options.includeDiff ? `\ngit diff:\n${await collectWorkingTreeDiff(cwd)}` : "";
|
|
684
|
+
return {
|
|
685
|
+
usedIteration: true,
|
|
686
|
+
input: [
|
|
687
|
+
"我这一轮迭代(上次提交之后)发给编码助手的改动提示词,按时间排列:",
|
|
688
|
+
digest,
|
|
689
|
+
"",
|
|
690
|
+
"本次工作区改动清单(仅供核对,不要逐文件罗列):",
|
|
691
|
+
summary,
|
|
692
|
+
diff,
|
|
693
|
+
].join("\n"),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
return { usedIteration: false, input: `git diff:\n${await collectWorkingTreeDiff(cwd)}` };
|
|
697
|
+
}
|
|
698
|
+
/** 两种输入源共用的任务描述,只改变如何引导模型读输入。 */
|
|
699
|
+
function commitTaskLine(lang, usedIteration) {
|
|
700
|
+
const instruction = usedIteration
|
|
701
|
+
? `阅读以下迭代提示词与文件清单,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。`
|
|
702
|
+
: `阅读以下 git diff,用${lang}写一条简洁的 commit message。要求:祈使句,不超过 50 字,描述「做了什么」。只输出 message 本身,不要引号、不要 Markdown 格式、不要任何额外说明。`;
|
|
703
|
+
return instruction;
|
|
704
|
+
}
|
|
705
|
+
async function generateCommitMessage(cwd, language, ai = {}, options = {}) {
|
|
706
|
+
const { input, usedIteration } = await buildCommitPromptInput(cwd, options);
|
|
644
707
|
const lang = language.trim() || "中文";
|
|
645
|
-
const prompt =
|
|
708
|
+
const prompt = `${commitTaskLine(lang, usedIteration)}\n\n${input}`;
|
|
646
709
|
const raw = await callConfiguredAiText(prompt, cwd, language, ai);
|
|
647
710
|
const message = normalizeAiText(raw);
|
|
648
711
|
if (!message) {
|
|
@@ -677,8 +740,8 @@ function sanitizeSuggestedTag(value) {
|
|
|
677
740
|
return undefined;
|
|
678
741
|
return cleaned;
|
|
679
742
|
}
|
|
680
|
-
async function generateCommitMessageWithTag(cwd, language, ai = {}) {
|
|
681
|
-
const
|
|
743
|
+
async function generateCommitMessageWithTag(cwd, language, ai = {}, options = {}) {
|
|
744
|
+
const { input, usedIteration } = await buildCommitPromptInput(cwd, options);
|
|
682
745
|
let latestTag;
|
|
683
746
|
try {
|
|
684
747
|
latestTag = await runGitAsync(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
@@ -690,15 +753,14 @@ async function generateCommitMessageWithTag(cwd, language, ai = {}) {
|
|
|
690
753
|
const tagHint = latestTag
|
|
691
754
|
? `当前最新 tag 是 \`${latestTag}\`,请基于它给出下一个版本号(保持原有前缀风格,例如有 \`v\` 就保留 \`v\`)。`
|
|
692
755
|
: `仓库还没有任何 tag,请直接给一个起始版本号(建议 \`v0.0.1\` / \`v0.1.0\` / \`v1.0.0\` 之一,按改动幅度选择)。`;
|
|
693
|
-
const prompt =
|
|
756
|
+
const prompt = `阅读以下输入,完成两件事:
|
|
694
757
|
1. 用${lang}写一条简洁的 commit message(祈使句,不超过 50 字,描述「做了什么」)。
|
|
695
758
|
2. 根据改动幅度推荐下一个语义化版本 tag(破坏性变更 → 升 major;新增功能 → 升 minor;修复 / 文档 / 重构 / 维护 → 升 patch)。${tagHint}
|
|
696
759
|
|
|
697
760
|
请严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
|
|
698
761
|
{"message":"...","tag":"v1.2.3"}
|
|
699
762
|
|
|
700
|
-
|
|
701
|
-
${diff}`;
|
|
763
|
+
${input}`;
|
|
702
764
|
const raw = await callConfiguredAiText(prompt, cwd, language, ai);
|
|
703
765
|
const parsed = tryParseJson(raw);
|
|
704
766
|
let message;
|
|
@@ -717,12 +779,12 @@ ${diff}`;
|
|
|
717
779
|
}
|
|
718
780
|
return { message, suggestedTag };
|
|
719
781
|
}
|
|
720
|
-
export async function generateCommitMessageOnly(cwd, language, ai = {}) {
|
|
782
|
+
export async function generateCommitMessageOnly(cwd, language, ai = {}, options = {}) {
|
|
721
783
|
if (!cwd || !existsSync(cwd)) {
|
|
722
784
|
throw new QuickCommitError("工作目录不存在。", "CWD_MISSING");
|
|
723
785
|
}
|
|
724
786
|
// Read the working tree as-is. Generating a message must not stage files.
|
|
725
|
-
return generateCommitMessageWithTag(cwd, language, ai);
|
|
787
|
+
return generateCommitMessageWithTag(cwd, language, ai, options);
|
|
726
788
|
}
|
|
727
789
|
/**
|
|
728
790
|
* Ask the session provider for a single tag string. Called from `runQuickCommit` after the commit has
|
|
@@ -1123,9 +1185,12 @@ async function collectSubmodulesForPush(cwd) {
|
|
|
1123
1185
|
}
|
|
1124
1186
|
function buildFallbackPrompt(opts, priorError) {
|
|
1125
1187
|
const lang = opts.language.trim() || "中文";
|
|
1188
|
+
const digest = opts.iterationDigest?.trim();
|
|
1126
1189
|
const messageLine = opts.autoMessage === false
|
|
1127
1190
|
? `- 使用这个 commit message:${(opts.customMessage || "").trim()}`
|
|
1128
|
-
:
|
|
1191
|
+
: digest
|
|
1192
|
+
? `- 先依据下面「本轮迭代提示词」总结改动,写一条简洁的 ${lang} commit message(祈使句,不超过 50 字);不需要逐行读 diff`
|
|
1193
|
+
: `- 先根据当前 staged/unstaged diff 生成一条简洁的 ${lang} commit message(祈使句,不超过 50 字)`;
|
|
1129
1194
|
const tagLine = opts.tag?.trim()
|
|
1130
1195
|
? `- 提交后创建 tag:${opts.tag.trim()}`
|
|
1131
1196
|
: opts.autoTag
|
|
@@ -1150,6 +1215,9 @@ function buildFallbackPrompt(opts, priorError) {
|
|
|
1150
1215
|
tagLine,
|
|
1151
1216
|
pushLine,
|
|
1152
1217
|
submoduleLine,
|
|
1218
|
+
...(digest && opts.autoMessage !== false
|
|
1219
|
+
? ["", "本轮迭代提示词(按时间):", digest, ...(opts.includeDiff ? [] : ["(文件清单请自行用 git status 查看,不必逐行读 diff)"])]
|
|
1220
|
+
: []),
|
|
1153
1221
|
"",
|
|
1154
1222
|
`内置流程失败原因:${priorError}`,
|
|
1155
1223
|
"",
|
|
@@ -1341,7 +1409,7 @@ export async function runQuickCommit(opts) {
|
|
|
1341
1409
|
}
|
|
1342
1410
|
let message;
|
|
1343
1411
|
if (autoMessage) {
|
|
1344
|
-
message = await generateCommitMessage(cwd, language, ai);
|
|
1412
|
+
message = await generateCommitMessage(cwd, language, ai, opts);
|
|
1345
1413
|
}
|
|
1346
1414
|
else {
|
|
1347
1415
|
message = (customMessage || "").trim();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { WandStorage } from "./storage.js";
|
|
2
|
+
import { type WandIterationPrompt, type WandIterationPromptSource } from "./task-types.js";
|
|
3
|
+
/** commit 生成的输入模式:iteration = 只用本轮提示词(默认,省 token);diff = 读完整 diff。 */
|
|
4
|
+
export type CommitContextMode = "iteration" | "diff";
|
|
5
|
+
/** 用户在上次选择后记住的模式;两条互不影响,互相切不会被覆盖。 */
|
|
6
|
+
export declare const COMMIT_CONTEXT_MODE_PREF_KEY = "pref:commitContextMode";
|
|
7
|
+
export declare function isCommitContextMode(value: unknown): value is CommitContextMode;
|
|
8
|
+
/** 读取记住的模式;没存过 / 存坏了就用默认(提示词优先,省 token)。 */
|
|
9
|
+
export declare function readCommitContextMode(storage: WandStorage, override?: unknown): CommitContextMode;
|
|
10
|
+
/** 记录时用到的会话信息:两个 manager 的 snapshot 都满足这个结构。 */
|
|
11
|
+
export interface IterationPromptSession {
|
|
12
|
+
id: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
workspaceId?: string;
|
|
15
|
+
workspaceTaskId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface IterationPromptTarget {
|
|
18
|
+
milestoneId: string;
|
|
19
|
+
/** 这个迭代的名字;UI 上用来显示「本次迭代:默认迭代」。 */
|
|
20
|
+
milestoneName: string;
|
|
21
|
+
isDefaultIteration: boolean;
|
|
22
|
+
workspaceId: string | null;
|
|
23
|
+
taskId: string | null;
|
|
24
|
+
repoKey: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** 单次生成的提示词清单上限:够覆盖一轮迭代,又不至于把 prompt 撑爆。 */
|
|
27
|
+
export declare const MAX_ITERATION_PROMPT_ENTRIES = 120;
|
|
28
|
+
/** 提示词清单的总字符预算,超出后从最旧的开始丢。 */
|
|
29
|
+
export declare const MAX_ITERATION_PROMPT_CHARS = 12000;
|
|
30
|
+
/**
|
|
31
|
+
* 仓库身份:`git rev-parse --git-common-dir` 的绝对路径。
|
|
32
|
+
* worktree 与主仓库拿到同一个值,因此「同一项目的并行 worktree」会被算成一个仓库。
|
|
33
|
+
* 结果按目录缓存;拿不到(不是 git 仓库 / 没有 git)时缓存 null,不反复重试。
|
|
34
|
+
*/
|
|
35
|
+
export declare function repoKeyForCwd(cwd: string | undefined | null): Promise<string | null>;
|
|
36
|
+
/** 测试用:清掉目录 → 仓库身份缓存。 */
|
|
37
|
+
export declare function resetRepoKeyCache(): void;
|
|
38
|
+
/**
|
|
39
|
+
* 这条提示词应该记到哪个迭代下:
|
|
40
|
+
* 会话绑定的看板任务 → 它所属工作区的任务 → 都没绑就落到默认迭代。
|
|
41
|
+
* 只有写路径调用,所以这里会惰性把默认迭代建出来。
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveIterationPromptTarget(storage: WandStorage, session: IterationPromptSession): IterationPromptTarget;
|
|
44
|
+
/** 提示词摘要清洗:去掉控制字符、压空白;保留用户原话,不做二次总结。 */
|
|
45
|
+
export declare function cleanIterationPromptText(value: string): string;
|
|
46
|
+
/** 本地标题:取提示词第一条有效行,不调模型(省一次调用,也不阻塞输入)。 */
|
|
47
|
+
export declare function iterationPromptTitle(prompt: string): string;
|
|
48
|
+
/** 测试用:等待排队中的记录写入全部落地。 */
|
|
49
|
+
export declare function whenIterationPromptsSettled(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* 记录一条提示词。调用点都在用户提交输入的热路径上,所以它:
|
|
52
|
+
* - 同步过滤没信息量的输入(y / 单条命令 / 斜杠命令),不产生任何 IO;
|
|
53
|
+
* - 把真正的写入排进串行队列,不阻塞输入;
|
|
54
|
+
* - 永不抛错(记录失败不该影响会话)。
|
|
55
|
+
*/
|
|
56
|
+
export declare function recordIterationPrompt(storage: WandStorage, session: IterationPromptSession, prompt: string, source?: WandIterationPromptSource, options?: {
|
|
57
|
+
skipFilter?: boolean;
|
|
58
|
+
}): void;
|
|
59
|
+
/** 任务 / 派发这类没有会话输入的入口:直接用任务标题当记录。 */
|
|
60
|
+
export declare function recordIterationPromptForTask(storage: WandStorage, input: {
|
|
61
|
+
sessionId?: string | null;
|
|
62
|
+
cwd?: string;
|
|
63
|
+
workspaceId?: string | null;
|
|
64
|
+
milestoneId?: string | null;
|
|
65
|
+
taskId?: string | null;
|
|
66
|
+
title: string;
|
|
67
|
+
detail?: string;
|
|
68
|
+
source: WandIterationPromptSource;
|
|
69
|
+
}): void;
|
|
70
|
+
/**
|
|
71
|
+
* 把记录拼成给模型看的清单:按时间正序,附时间、标题与摘要;
|
|
72
|
+
* 已被上一轮提交用掉的条目会被标出来,方便模型少写重复内容。
|
|
73
|
+
*/
|
|
74
|
+
export declare function iterationPromptDigest(entries: readonly WandIterationPrompt[]): string;
|
|
75
|
+
/** 迭代为空时给模型的一句话,避免它把「没有输入」理解成「没有改动」。 */
|
|
76
|
+
export declare const EMPTY_ITERATION_DIGEST_HINT = "\uFF08\u672C\u8FED\u4EE3\u8FD8\u6CA1\u6709\u8BB0\u5F55\u5230\u63D0\u793A\u8BCD\uFF0C\u6539\u4E3A\u4F9D\u636E\u6587\u4EF6\u6539\u52A8\u5224\u65AD\u3002\uFF09";
|
|
77
|
+
/** 默认迭代的名字;UI 与文档共用,避免各处硬编码。 */
|
|
78
|
+
export declare const DEFAULT_ITERATION_LABEL = "\u9ED8\u8BA4\u8FED\u4EE3";
|
|
79
|
+
/** 面板里的一行:就是一条提示词记录 + 是否已被上一轮提交用掉。 */
|
|
80
|
+
export interface IterationCommitEntry {
|
|
81
|
+
id: string;
|
|
82
|
+
title: string;
|
|
83
|
+
detail: string;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
consumed: boolean;
|
|
86
|
+
consumedCommit: string | null;
|
|
87
|
+
source: WandIterationPromptSource;
|
|
88
|
+
}
|
|
89
|
+
export interface IterationCommitContext {
|
|
90
|
+
/** 会话当前所属迭代(未选迭代时就是默认迭代)。 */
|
|
91
|
+
iteration: {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
isDefault: boolean;
|
|
95
|
+
};
|
|
96
|
+
repoKey: string | null;
|
|
97
|
+
/** 面板可展示的条目,新的在前(从上往下看就是「最近改了什么」)。 */
|
|
98
|
+
entries: IterationCommitEntry[];
|
|
99
|
+
/** 默认勾选:还没被上一轮提交用掉的那些(= 上次提交以来的变更),时间正序。 */
|
|
100
|
+
defaultEntryIds: string[];
|
|
101
|
+
/** 允许提交的 id 全集(比 entries 宽),用于校验前端传回来的勾选。 */
|
|
102
|
+
selectableIds: string[];
|
|
103
|
+
/** 是否还有更早的历史没被取到(面板据此提示「更多历史」)。 */
|
|
104
|
+
truncated: boolean;
|
|
105
|
+
/** 默认模式下后端会用哪个输入:有未提交提示词就 iteration,否则 diff。 */
|
|
106
|
+
effectiveMode: CommitContextMode;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 组装 commit 生成面板要的上下文:
|
|
110
|
+
* - 会话 → 迭代(没选迭代就是默认迭代);
|
|
111
|
+
* - 按仓库隔离(同一仓库的 worktree 共享,不混别的项目);
|
|
112
|
+
* - 默认勾选尚未被提交消费的条目,也就是「两次迭代之间」的提示词。
|
|
113
|
+
* 纯读操作,不写库。
|
|
114
|
+
*/
|
|
115
|
+
export declare function buildIterationCommitContext(storage: WandStorage, input: {
|
|
116
|
+
session: IterationPromptSession;
|
|
117
|
+
milestoneId?: string | null;
|
|
118
|
+
limit?: number;
|
|
119
|
+
}): Promise<IterationCommitContext>;
|
|
120
|
+
/**
|
|
121
|
+
* 把用户勾选的条目(不传就是默认的「上次提交以来」)转成给模型的输入。
|
|
122
|
+
* - `entryIds`:本次算作已提交的条目(提交成功后由调用方标记,构成下一轮的窗口起点);
|
|
123
|
+
* diff 模式下也返回,因为「提交发生了」是仓库事实,不取决于 message 是谁写的。
|
|
124
|
+
* - `digest`:空表示调用方应该退回读 diff(切了 diff 模式、或没有可用提示词)。
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolveCommitContextInput(storage: WandStorage, context: IterationCommitContext, body: {
|
|
127
|
+
mode?: unknown;
|
|
128
|
+
entryIds?: unknown;
|
|
129
|
+
}): {
|
|
130
|
+
mode: CommitContextMode;
|
|
131
|
+
digest: string;
|
|
132
|
+
entryIds: string[];
|
|
133
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// 迭代提示词记录:把「我这一轮迭代到底让 Agent 改了什么」沉淀成一串提示词标题,
|
|
2
|
+
// 供 commit message / 汇报类生成直接引用,不必每次都让模型读 diff(省 token 也更快)。
|
|
3
|
+
//
|
|
4
|
+
// 两条不变式:
|
|
5
|
+
// 1. 记录一定挂在某个真实迭代上;用户没选迭代时落到全局「默认迭代」(惰性创建)。
|
|
6
|
+
// 2. 记录按仓库隔离(repoKey = git 的 common dir):同一仓库的 worktree 共享,
|
|
7
|
+
// 别的项目的历史不会混进这次 commit message。
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
10
|
+
import { runGitAsync } from "./git-utils.js";
|
|
11
|
+
import { shouldGenerateSessionTopicFromInput, summarizeSessionTitleFromInput } from "./session-topic.js";
|
|
12
|
+
import { DEFAULT_ITERATION_NAME, WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH, } from "./task-types.js";
|
|
13
|
+
/** 用户在上次选择后记住的模式;两条互不影响,互相切不会被覆盖。 */
|
|
14
|
+
export const COMMIT_CONTEXT_MODE_PREF_KEY = "pref:commitContextMode";
|
|
15
|
+
export function isCommitContextMode(value) {
|
|
16
|
+
return value === "iteration" || value === "diff";
|
|
17
|
+
}
|
|
18
|
+
/** 读取记住的模式;没存过 / 存坏了就用默认(提示词优先,省 token)。 */
|
|
19
|
+
export function readCommitContextMode(storage, override) {
|
|
20
|
+
if (isCommitContextMode(override))
|
|
21
|
+
return override;
|
|
22
|
+
const stored = storage.getPreference(COMMIT_CONTEXT_MODE_PREF_KEY, "iteration");
|
|
23
|
+
return isCommitContextMode(stored) ? stored : "iteration";
|
|
24
|
+
}
|
|
25
|
+
/** 单次生成的提示词清单上限:够覆盖一轮迭代,又不至于把 prompt 撑爆。 */
|
|
26
|
+
export const MAX_ITERATION_PROMPT_ENTRIES = 120;
|
|
27
|
+
/** 提示词清单的总字符预算,超出后从最旧的开始丢。 */
|
|
28
|
+
export const MAX_ITERATION_PROMPT_CHARS = 12_000;
|
|
29
|
+
const REPO_KEY_TIMEOUT_MS = 5_000;
|
|
30
|
+
const DUPLICATE_WINDOW_MS = 120_000;
|
|
31
|
+
const repoKeyCache = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* 仓库身份:`git rev-parse --git-common-dir` 的绝对路径。
|
|
34
|
+
* worktree 与主仓库拿到同一个值,因此「同一项目的并行 worktree」会被算成一个仓库。
|
|
35
|
+
* 结果按目录缓存;拿不到(不是 git 仓库 / 没有 git)时缓存 null,不反复重试。
|
|
36
|
+
*/
|
|
37
|
+
export async function repoKeyForCwd(cwd) {
|
|
38
|
+
const directory = path.resolve(cwd?.trim() || "");
|
|
39
|
+
if (!cwd?.trim())
|
|
40
|
+
return null;
|
|
41
|
+
const cached = repoKeyCache.get(directory);
|
|
42
|
+
if (cached !== undefined)
|
|
43
|
+
return cached;
|
|
44
|
+
let key = null;
|
|
45
|
+
try {
|
|
46
|
+
key = await runGitAsync(["rev-parse", "--path-format=absolute", "--git-common-dir"], directory, { timeout: REPO_KEY_TIMEOUT_MS }) || null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// 老版本 git 不认 --path-format:退回 --show-toplevel,至少能把「同一目录树」聚在一起。
|
|
50
|
+
try {
|
|
51
|
+
key = await runGitAsync(["rev-parse", "--show-toplevel"], directory, { timeout: REPO_KEY_TIMEOUT_MS }) || null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
key = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
repoKeyCache.set(directory, key ?? null);
|
|
58
|
+
return key ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** 测试用:清掉目录 → 仓库身份缓存。 */
|
|
61
|
+
export function resetRepoKeyCache() {
|
|
62
|
+
repoKeyCache.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 这条提示词应该记到哪个迭代下:
|
|
66
|
+
* 会话绑定的看板任务 → 它所属工作区的任务 → 都没绑就落到默认迭代。
|
|
67
|
+
* 只有写路径调用,所以这里会惰性把默认迭代建出来。
|
|
68
|
+
*/
|
|
69
|
+
export function resolveIterationPromptTarget(storage, session) {
|
|
70
|
+
const workspaceTask = session.workspaceTaskId ? storage.getWorkspaceTask(session.workspaceTaskId) : null;
|
|
71
|
+
// 会话多数是从侧栏任务起的(有 workspaceTaskId),直接按它反查卡片;没绑时才看 wand_task_sessions。
|
|
72
|
+
const task = workspaceTask
|
|
73
|
+
? storage.getWandTaskByWorkspaceTaskId(workspaceTask.id)
|
|
74
|
+
: (() => {
|
|
75
|
+
const taskId = storage.getWandTaskIdForSession(session.id);
|
|
76
|
+
return taskId ? storage.getWandTask(taskId) : null;
|
|
77
|
+
})();
|
|
78
|
+
// 任务上的迭代优先,其次看它所属侧栏任务的迭代,最后兜底默认迭代。
|
|
79
|
+
const explicitId = task?.milestoneId || workspaceTask?.milestoneId || null;
|
|
80
|
+
const explicit = explicitId ? storage.getWandMilestone(explicitId) : null;
|
|
81
|
+
// 只有写路径会走到这里,所以可以惰性把默认迭代建出来。
|
|
82
|
+
const milestone = explicit ?? storage.ensureDefaultWandMilestone();
|
|
83
|
+
return {
|
|
84
|
+
milestoneId: milestone.id,
|
|
85
|
+
milestoneName: milestone.name,
|
|
86
|
+
isDefaultIteration: milestone.isDefault,
|
|
87
|
+
workspaceId: session.workspaceId ?? task?.workspaceId ?? workspaceTask?.workspaceId ?? null,
|
|
88
|
+
taskId: task?.id ?? null,
|
|
89
|
+
repoKey: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** 提示词摘要清洗:去掉控制字符、压空白;保留用户原话,不做二次总结。 */
|
|
93
|
+
export function cleanIterationPromptText(value) {
|
|
94
|
+
return value
|
|
95
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, " ")
|
|
96
|
+
.replace(/\s+/g, " ")
|
|
97
|
+
.trim();
|
|
98
|
+
}
|
|
99
|
+
function clip(value, maxLength) {
|
|
100
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1).trimEnd()}…` : value;
|
|
101
|
+
}
|
|
102
|
+
/** 本地标题:取提示词第一条有效行,不调模型(省一次调用,也不阻塞输入)。 */
|
|
103
|
+
export function iterationPromptTitle(prompt) {
|
|
104
|
+
const cleaned = cleanIterationPromptText(prompt);
|
|
105
|
+
if (!cleaned)
|
|
106
|
+
return "";
|
|
107
|
+
return clip(summarizeSessionTitleFromInput(cleaned) || cleaned, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH);
|
|
108
|
+
}
|
|
109
|
+
let writeChain = Promise.resolve();
|
|
110
|
+
/** 测试用:等待排队中的记录写入全部落地。 */
|
|
111
|
+
export function whenIterationPromptsSettled() {
|
|
112
|
+
return writeChain;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 记录一条提示词。调用点都在用户提交输入的热路径上,所以它:
|
|
116
|
+
* - 同步过滤没信息量的输入(y / 单条命令 / 斜杠命令),不产生任何 IO;
|
|
117
|
+
* - 把真正的写入排进串行队列,不阻塞输入;
|
|
118
|
+
* - 永不抛错(记录失败不该影响会话)。
|
|
119
|
+
*/
|
|
120
|
+
export function recordIterationPrompt(storage, session, prompt, source = "session", options = {}) {
|
|
121
|
+
const raw = prompt?.trim();
|
|
122
|
+
if (!raw)
|
|
123
|
+
return;
|
|
124
|
+
// 与会话标题共用同一把尺子:太短的输入不值得进迭代记录。
|
|
125
|
+
if (!options.skipFilter && !shouldGenerateSessionTopicFromInput(raw))
|
|
126
|
+
return;
|
|
127
|
+
const run = async () => {
|
|
128
|
+
try {
|
|
129
|
+
const detail = clip(cleanIterationPromptText(raw), WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH);
|
|
130
|
+
const title = iterationPromptTitle(raw) || clip(detail, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH);
|
|
131
|
+
if (!title)
|
|
132
|
+
return;
|
|
133
|
+
const previous = storage.latestIterationPromptForSession(session.id);
|
|
134
|
+
if (previous
|
|
135
|
+
&& previous.title === title
|
|
136
|
+
&& previous.detail === detail
|
|
137
|
+
&& Date.now() - Date.parse(previous.createdAt) < DUPLICATE_WINDOW_MS) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const target = resolveIterationPromptTarget(storage, session);
|
|
141
|
+
const repoKey = await repoKeyForCwd(session.cwd);
|
|
142
|
+
storage.createIterationPrompt({
|
|
143
|
+
milestoneId: target.milestoneId,
|
|
144
|
+
workspaceId: target.workspaceId,
|
|
145
|
+
sessionId: session.id,
|
|
146
|
+
taskId: target.taskId,
|
|
147
|
+
repoKey,
|
|
148
|
+
cwd: session.cwd ?? "",
|
|
149
|
+
title,
|
|
150
|
+
detail,
|
|
151
|
+
source,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`[Iteration] Failed to record prompt for session ${session.id}:`, getErrorMessage(error));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
writeChain = writeChain.then(run, run);
|
|
159
|
+
}
|
|
160
|
+
/** 任务 / 派发这类没有会话输入的入口:直接用任务标题当记录。 */
|
|
161
|
+
export function recordIterationPromptForTask(storage, input) {
|
|
162
|
+
const raw = input.detail?.trim() || input.title.trim();
|
|
163
|
+
if (!raw)
|
|
164
|
+
return;
|
|
165
|
+
const run = async () => {
|
|
166
|
+
try {
|
|
167
|
+
const milestone = input.milestoneId
|
|
168
|
+
? storage.getWandMilestone(input.milestoneId)
|
|
169
|
+
: null;
|
|
170
|
+
const fallback = milestone ?? storage.ensureDefaultWandMilestone();
|
|
171
|
+
const repoKey = await repoKeyForCwd(input.cwd);
|
|
172
|
+
storage.createIterationPrompt({
|
|
173
|
+
milestoneId: fallback.id,
|
|
174
|
+
workspaceId: input.workspaceId ?? null,
|
|
175
|
+
sessionId: input.sessionId ?? null,
|
|
176
|
+
taskId: input.taskId ?? null,
|
|
177
|
+
repoKey,
|
|
178
|
+
cwd: input.cwd ?? "",
|
|
179
|
+
title: clip(cleanIterationPromptText(input.title), WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH),
|
|
180
|
+
detail: clip(cleanIterationPromptText(raw), WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH),
|
|
181
|
+
source: input.source,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error("[Iteration] Failed to record task prompt:", getErrorMessage(error));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
writeChain = writeChain.then(run, run);
|
|
189
|
+
}
|
|
190
|
+
/** `2026-02-14T09:05:00.000Z` → `02-14 09:05`(UTC,稳定可测)。 */
|
|
191
|
+
function shortStamp(iso) {
|
|
192
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(iso);
|
|
193
|
+
return match ? `${match[2]}-${match[3]} ${match[4]}:${match[5]}` : iso;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 把记录拼成给模型看的清单:按时间正序,附时间、标题与摘要;
|
|
197
|
+
* 已被上一轮提交用掉的条目会被标出来,方便模型少写重复内容。
|
|
198
|
+
*/
|
|
199
|
+
export function iterationPromptDigest(entries) {
|
|
200
|
+
let budget = MAX_ITERATION_PROMPT_CHARS;
|
|
201
|
+
const kept = [];
|
|
202
|
+
// 从最新的往回装:预算不够时丢掉的总是最早的历史,越新的改动越不会丢。
|
|
203
|
+
for (let index = entries.length - 1; index >= 0 && budget > 0; index -= 1) {
|
|
204
|
+
const entry = entries[index];
|
|
205
|
+
const consumed = entry.consumedAt ? "(上一轮已提交)" : "";
|
|
206
|
+
// 标题往往是详情被裁短的前缀,这时只给详情,避免同一句话重复两遍。
|
|
207
|
+
const body = entry.detail && entry.detail !== entry.title && !entry.detail.startsWith(entry.title)
|
|
208
|
+
? `${entry.title} — ${entry.detail}`
|
|
209
|
+
: entry.detail || entry.title;
|
|
210
|
+
const line = `${shortStamp(entry.createdAt)} ${body}${consumed}`;
|
|
211
|
+
if (line.length > budget && kept.length > 0)
|
|
212
|
+
break;
|
|
213
|
+
kept.unshift(line);
|
|
214
|
+
budget -= line.length + 1;
|
|
215
|
+
}
|
|
216
|
+
return kept.map((line, index) => `${index + 1}. ${line}`).join("\n");
|
|
217
|
+
}
|
|
218
|
+
/** 迭代为空时给模型的一句话,避免它把「没有输入」理解成「没有改动」。 */
|
|
219
|
+
export const EMPTY_ITERATION_DIGEST_HINT = `(本迭代还没有记录到提示词,改为依据文件改动判断。)`;
|
|
220
|
+
/** 默认迭代的名字;UI 与文档共用,避免各处硬编码。 */
|
|
221
|
+
export const DEFAULT_ITERATION_LABEL = DEFAULT_ITERATION_NAME;
|
|
222
|
+
function toCommitEntry(entry) {
|
|
223
|
+
return {
|
|
224
|
+
id: entry.id,
|
|
225
|
+
title: entry.title,
|
|
226
|
+
detail: entry.detail,
|
|
227
|
+
createdAt: entry.createdAt,
|
|
228
|
+
consumed: !!entry.consumedAt,
|
|
229
|
+
consumedCommit: entry.consumedCommit,
|
|
230
|
+
source: entry.source,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 组装 commit 生成面板要的上下文:
|
|
235
|
+
* - 会话 → 迭代(没选迭代就是默认迭代);
|
|
236
|
+
* - 按仓库隔离(同一仓库的 worktree 共享,不混别的项目);
|
|
237
|
+
* - 默认勾选尚未被提交消费的条目,也就是「两次迭代之间」的提示词。
|
|
238
|
+
* 纯读操作,不写库。
|
|
239
|
+
*/
|
|
240
|
+
export async function buildIterationCommitContext(storage, input) {
|
|
241
|
+
const explicit = input.milestoneId ? storage.getWandMilestone(input.milestoneId) : null;
|
|
242
|
+
const target = explicit
|
|
243
|
+
? { id: explicit.id, name: explicit.name, isDefault: explicit.isDefault }
|
|
244
|
+
: (() => {
|
|
245
|
+
const resolved = resolveIterationPromptTarget(storage, input.session);
|
|
246
|
+
return { id: resolved.milestoneId, name: resolved.milestoneName, isDefault: resolved.isDefaultIteration };
|
|
247
|
+
})();
|
|
248
|
+
// 拿不到仓库身份(不是 git 仓库 / 没装 git)时不按仓库过滤,宁可多给几条也不给空面板。
|
|
249
|
+
const repoKey = await repoKeyForCwd(input.session.cwd);
|
|
250
|
+
const displayLimit = Math.max(1, Math.min(200, Math.floor(input.limit ?? 60)));
|
|
251
|
+
// 取「展示窗口」和「未提交集合」两次:展示只要最近的,默认勾选则要拿全。
|
|
252
|
+
const rows = storage.listIterationPrompts({
|
|
253
|
+
milestoneId: target.id,
|
|
254
|
+
repoKey,
|
|
255
|
+
includeConsumed: true,
|
|
256
|
+
limit: 200,
|
|
257
|
+
});
|
|
258
|
+
const pending = storage.listIterationPrompts({
|
|
259
|
+
milestoneId: target.id,
|
|
260
|
+
repoKey,
|
|
261
|
+
includeConsumed: false,
|
|
262
|
+
limit: MAX_ITERATION_PROMPT_ENTRIES,
|
|
263
|
+
});
|
|
264
|
+
const defaultEntryIds = pending.map((entry) => entry.id);
|
|
265
|
+
return {
|
|
266
|
+
iteration: target,
|
|
267
|
+
repoKey,
|
|
268
|
+
entries: rows.slice(-displayLimit).reverse().map(toCommitEntry),
|
|
269
|
+
defaultEntryIds,
|
|
270
|
+
selectableIds: rows.map((entry) => entry.id),
|
|
271
|
+
truncated: rows.length >= 200 || pending.length >= MAX_ITERATION_PROMPT_ENTRIES,
|
|
272
|
+
effectiveMode: defaultEntryIds.length > 0 ? "iteration" : "diff",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 把用户勾选的条目(不传就是默认的「上次提交以来」)转成给模型的输入。
|
|
277
|
+
* - `entryIds`:本次算作已提交的条目(提交成功后由调用方标记,构成下一轮的窗口起点);
|
|
278
|
+
* diff 模式下也返回,因为「提交发生了」是仓库事实,不取决于 message 是谁写的。
|
|
279
|
+
* - `digest`:空表示调用方应该退回读 diff(切了 diff 模式、或没有可用提示词)。
|
|
280
|
+
*/
|
|
281
|
+
export function resolveCommitContextInput(storage, context, body) {
|
|
282
|
+
const mode = readCommitContextMode(storage, body.mode);
|
|
283
|
+
const selected = Array.isArray(body.entryIds)
|
|
284
|
+
? body.entryIds.filter((id) => typeof id === "string" && id.trim().length > 0)
|
|
285
|
+
: null;
|
|
286
|
+
// 只认这个迭代(而且同仓库)里的条目:前端传错 / 传了别的迭代也不会串味。
|
|
287
|
+
const allowed = new Set(context.selectableIds);
|
|
288
|
+
const ids = (selected ?? context.defaultEntryIds).filter((id) => allowed.has(id));
|
|
289
|
+
if (mode !== "iteration" || ids.length === 0)
|
|
290
|
+
return { mode, digest: "", entryIds: ids };
|
|
291
|
+
const digest = iterationPromptDigest(storage.listIterationPromptsByIds(ids));
|
|
292
|
+
return digest ? { mode, digest, entryIds: ids } : { mode, digest: "", entryIds: ids };
|
|
293
|
+
}
|
|
@@ -4,3 +4,22 @@ import type { WandStorage } from "./storage.js";
|
|
|
4
4
|
* 调用方负责先校验 id 是否存在。
|
|
5
5
|
*/
|
|
6
6
|
export declare function scopedMilestoneId(storage: WandStorage, milestoneId: string | null | undefined, workspaceId: string | null | undefined): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* 写路径的迭代兑底:用户没选(或没传)迭代就落到默认迭代,
|
|
9
|
+
* 这样「每个任务都属于一个迭代」在前端和后续 commit 总结里都成立。
|
|
10
|
+
*/
|
|
11
|
+
export declare function defaultMilestoneIdForWrite(storage: WandStorage): string;
|
|
12
|
+
export interface ResolvedTaskMilestone {
|
|
13
|
+
/** 要展示 / 回传的迭代 id;任务本身没指定时就是默认迭代的 id。 */
|
|
14
|
+
milestoneId: string | null;
|
|
15
|
+
milestone: {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
isDefault: boolean;
|
|
19
|
+
} | null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 读出任务该显示的迭代:库里仍然用 NULL 表示「没单独指定」
|
|
23
|
+
* (历史数据不动,写入时才开始落默认迭代),读的时候统一兑成默认迭代。
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolvedMilestoneFields(storage: WandStorage, milestoneId: string | null | undefined): ResolvedTaskMilestone;
|