@co0ontty/wand 4.27.1 → 4.28.0
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 +6 -6
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +11 -0
- package/dist/cli-api.js +76 -0
- package/dist/cli.js +97 -0
- package/dist/config.d.ts +4 -3
- package/dist/config.js +20 -3
- package/dist/git-quick-commit.js +47 -1
- package/dist/git-worktree.d.ts +11 -0
- package/dist/git-worktree.js +72 -3
- package/dist/mission-diff.d.ts +8 -0
- package/dist/mission-diff.js +65 -0
- package/dist/mission-types.d.ts +90 -0
- package/dist/mission-types.js +1 -0
- package/dist/missions.d.ts +32 -0
- package/dist/missions.js +344 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +10 -1
- package/dist/path-repair.js +1 -1
- package/dist/process-manager.js +14 -1
- package/dist/prompt-optimizer.d.ts +2 -2
- package/dist/prompt-optimizer.js +11 -32
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/provider-cli-updater.js +8 -0
- package/dist/resume-policy.js +5 -3
- package/dist/server-mission-routes.d.ts +3 -0
- package/dist/server-mission-routes.js +81 -0
- package/dist/server-session-routes.js +5 -3
- package/dist/server-settings-routes.js +5 -0
- package/dist/server-update-routes.js +1 -1
- package/dist/server.js +31 -5
- package/dist/session-ai-context.d.ts +3 -3
- package/dist/session-ai-context.js +14 -18
- package/dist/storage.d.ts +15 -0
- package/dist/storage.js +221 -1
- package/dist/structured-pi-adapter.d.ts +11 -0
- package/dist/structured-pi-adapter.js +135 -0
- package/dist/structured-provider-common.d.ts +1 -0
- package/dist/structured-provider-common.js +16 -1
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +21 -7
- package/dist/system-ai.js +1 -1
- package/dist/types.d.ts +9 -4
- package/dist/web-ui/content/scripts.js +146 -69
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/provider-identity.d.ts +2 -1
- package/dist/web-ui/provider-identity.js +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { runGit, runGitRaw } from "./git-utils.js";
|
|
4
|
+
const DEFAULT_MAX_PATCH_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
function appendBounded(current, next, maxBytes) {
|
|
6
|
+
const available = maxBytes - Buffer.byteLength(current);
|
|
7
|
+
if (available <= 0)
|
|
8
|
+
return { value: current, truncated: true };
|
|
9
|
+
if (Buffer.byteLength(next) <= available)
|
|
10
|
+
return { value: current + next, truncated: false };
|
|
11
|
+
return { value: current + Buffer.from(next).subarray(0, available).toString("utf8"), truncated: true };
|
|
12
|
+
}
|
|
13
|
+
function parseNameStatus(raw) {
|
|
14
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
15
|
+
const fields = line.split("\t");
|
|
16
|
+
return { status: fields[0] || "M", path: fields[fields.length - 1] || line };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function untrackedPatch(cwd, relativePath) {
|
|
20
|
+
const result = spawnSync("git", ["diff", "--no-index", "--no-color", "--", "/dev/null", relativePath], {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
maxBuffer: DEFAULT_MAX_PATCH_BYTES,
|
|
24
|
+
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
|
|
25
|
+
});
|
|
26
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
27
|
+
throw new Error(result.stderr?.trim() || `无法读取未跟踪文件 diff:${relativePath}`);
|
|
28
|
+
}
|
|
29
|
+
return result.stdout || "";
|
|
30
|
+
}
|
|
31
|
+
export function buildMissionDiff(options) {
|
|
32
|
+
if (!existsSync(options.cwd))
|
|
33
|
+
throw new Error("任务 worktree 不存在,无法生成 diff。");
|
|
34
|
+
runGit(["rev-parse", "--verify", `${options.baseRef}^{commit}`], options.cwd);
|
|
35
|
+
const maxBytes = options.maxPatchBytes ?? DEFAULT_MAX_PATCH_BYTES;
|
|
36
|
+
const trackedNames = parseNameStatus(runGitRaw(["diff", "--name-status", options.baseRef, "--", "."], options.cwd));
|
|
37
|
+
const untracked = runGitRaw(["ls-files", "--others", "--exclude-standard", "-z"], options.cwd)
|
|
38
|
+
.split("\0")
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
const files = [...trackedNames, ...untracked.map((file) => ({ path: file, status: "?" }))];
|
|
41
|
+
let patch = runGitRaw(["diff", "--no-color", "--find-renames", options.baseRef, "--", "."], options.cwd);
|
|
42
|
+
let truncated = false;
|
|
43
|
+
if (Buffer.byteLength(patch) > maxBytes) {
|
|
44
|
+
patch = Buffer.from(patch).subarray(0, maxBytes).toString("utf8");
|
|
45
|
+
truncated = true;
|
|
46
|
+
}
|
|
47
|
+
if (!truncated) {
|
|
48
|
+
for (const file of untracked) {
|
|
49
|
+
const appended = appendBounded(patch, untrackedPatch(options.cwd, file), maxBytes);
|
|
50
|
+
patch = appended.value;
|
|
51
|
+
if (appended.truncated) {
|
|
52
|
+
truncated = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
missionId: options.missionId,
|
|
59
|
+
attemptId: options.attemptId,
|
|
60
|
+
baseRef: options.baseRef,
|
|
61
|
+
files,
|
|
62
|
+
patch,
|
|
63
|
+
truncated,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { SessionProvider } from "./types.js";
|
|
2
|
+
export type AgentActivityState = "working" | "needs_input" | "needs_permission" | "done" | "failed";
|
|
3
|
+
export type MissionStatus = "dispatching" | "running" | "needs_input" | "completed" | "failed" | "archived";
|
|
4
|
+
export type MissionAttemptState = AgentActivityState | "queued";
|
|
5
|
+
export type MissionReviewStatus = "pending" | "sent" | "resolved";
|
|
6
|
+
export interface MissionWorktreeOptions {
|
|
7
|
+
baseRef?: string;
|
|
8
|
+
sharedDirectories?: string[];
|
|
9
|
+
copyPaths?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface Mission {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
prompt: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
status: MissionStatus;
|
|
17
|
+
worktree: MissionWorktreeOptions;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MissionAttempt {
|
|
22
|
+
id: string;
|
|
23
|
+
missionId: string;
|
|
24
|
+
sessionId: string | null;
|
|
25
|
+
provider: SessionProvider;
|
|
26
|
+
state: MissionAttemptState;
|
|
27
|
+
branch: string | null;
|
|
28
|
+
worktreePath: string | null;
|
|
29
|
+
baseRef: string | null;
|
|
30
|
+
summary: string | null;
|
|
31
|
+
error: string | null;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
updatedAt: string;
|
|
34
|
+
}
|
|
35
|
+
export interface MissionReviewComment {
|
|
36
|
+
id: string;
|
|
37
|
+
missionId: string;
|
|
38
|
+
attemptId: string;
|
|
39
|
+
filePath: string;
|
|
40
|
+
line: number | null;
|
|
41
|
+
side: "old" | "new";
|
|
42
|
+
body: string;
|
|
43
|
+
status: MissionReviewStatus;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
sentAt: string | null;
|
|
46
|
+
resolvedAt: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface AgentActivityItem {
|
|
49
|
+
sessionId: string;
|
|
50
|
+
missionId: string | null;
|
|
51
|
+
attemptId: string | null;
|
|
52
|
+
state: AgentActivityState;
|
|
53
|
+
title: string;
|
|
54
|
+
summary: string | null;
|
|
55
|
+
provider: SessionProvider | null;
|
|
56
|
+
cwd: string | null;
|
|
57
|
+
updatedAt: string;
|
|
58
|
+
readAt: string | null;
|
|
59
|
+
}
|
|
60
|
+
export interface MissionDetails extends Mission {
|
|
61
|
+
attempts: MissionAttempt[];
|
|
62
|
+
comments: MissionReviewComment[];
|
|
63
|
+
}
|
|
64
|
+
export interface MissionDiffFile {
|
|
65
|
+
path: string;
|
|
66
|
+
status: string;
|
|
67
|
+
}
|
|
68
|
+
export interface MissionDiff {
|
|
69
|
+
missionId: string;
|
|
70
|
+
attemptId: string;
|
|
71
|
+
baseRef: string;
|
|
72
|
+
files: MissionDiffFile[];
|
|
73
|
+
patch: string;
|
|
74
|
+
truncated: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface CreateMissionInput {
|
|
77
|
+
prompt: string;
|
|
78
|
+
title?: string;
|
|
79
|
+
cwd: string;
|
|
80
|
+
providers: SessionProvider[];
|
|
81
|
+
baseRef?: string;
|
|
82
|
+
sharedDirectories?: string[];
|
|
83
|
+
copyPaths?: string[];
|
|
84
|
+
}
|
|
85
|
+
export interface CreateReviewCommentInput {
|
|
86
|
+
filePath: string;
|
|
87
|
+
line?: number | null;
|
|
88
|
+
side?: "old" | "new";
|
|
89
|
+
body: string;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
|
|
2
|
+
import type { SessionRegistry } from "./session-registry.js";
|
|
3
|
+
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
4
|
+
import type { WandStorage } from "./storage.js";
|
|
5
|
+
import type { ProcessEvent } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Deep task-orchestration module. Callers create missions, observe one inbox,
|
|
8
|
+
* and review diffs without coordinating session/worktree/storage details.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Missions {
|
|
11
|
+
private readonly storage;
|
|
12
|
+
private readonly structured;
|
|
13
|
+
private readonly sessions;
|
|
14
|
+
constructor(storage: WandStorage, structured: StructuredSessionManager, sessions: SessionRegistry);
|
|
15
|
+
list(): MissionDetails[];
|
|
16
|
+
get(id: string): MissionDetails | null;
|
|
17
|
+
inbox(): AgentActivityItem[];
|
|
18
|
+
markInboxRead(sessionId?: string): void;
|
|
19
|
+
create(input: CreateMissionInput): MissionDetails;
|
|
20
|
+
ingest(event: ProcessEvent): AgentActivityItem | null;
|
|
21
|
+
diff(missionId: string, attemptId: string): MissionDiff;
|
|
22
|
+
addReviewComment(missionId: string, attemptId: string, input: CreateReviewCommentInput): MissionReviewComment;
|
|
23
|
+
sendReview(missionId: string, attemptId: string, commentIds?: string[]): MissionReviewComment[];
|
|
24
|
+
resolveReview(missionId: string, attemptId: string, commentIds: string[]): MissionReviewComment[];
|
|
25
|
+
archive(id: string): MissionDetails;
|
|
26
|
+
private dispatchAttempt;
|
|
27
|
+
private seedInbox;
|
|
28
|
+
private details;
|
|
29
|
+
private getIncludingArchived;
|
|
30
|
+
private requireAttempt;
|
|
31
|
+
private refreshMissionStatus;
|
|
32
|
+
}
|
package/dist/missions.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { buildMissionDiff } from "./mission-diff.js";
|
|
5
|
+
const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
6
|
+
const MAX_ATTEMPTS = 6;
|
|
7
|
+
function nowIso() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function normalizeStringList(value, field) {
|
|
11
|
+
const result = [...new Set((value ?? []).map((item) => item.trim()).filter(Boolean))];
|
|
12
|
+
if (result.length > 20)
|
|
13
|
+
throw new Error(`${field} 最多允许 20 个路径。`);
|
|
14
|
+
for (const item of result) {
|
|
15
|
+
if (item.length > 500)
|
|
16
|
+
throw new Error(`${field} 中的路径过长。`);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function firstPromptLine(prompt) {
|
|
21
|
+
const line = prompt.split("\n").map((part) => part.trim()).find(Boolean) || "新任务";
|
|
22
|
+
return line.length > 72 ? `${line.slice(0, 69)}…` : line;
|
|
23
|
+
}
|
|
24
|
+
function sessionSummary(snapshot) {
|
|
25
|
+
if (snapshot.description?.trim())
|
|
26
|
+
return snapshot.description.trim();
|
|
27
|
+
if (snapshot.currentTaskTitle?.trim())
|
|
28
|
+
return snapshot.currentTaskTitle.trim();
|
|
29
|
+
const messages = snapshot.messages ?? [];
|
|
30
|
+
for (let turnIndex = messages.length - 1; turnIndex >= 0; turnIndex--) {
|
|
31
|
+
const text = messages[turnIndex].content
|
|
32
|
+
.filter((block) => block.type === "text")
|
|
33
|
+
.map((block) => block.text.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(" ");
|
|
36
|
+
if (text)
|
|
37
|
+
return text.length > 240 ? `${text.slice(0, 237)}…` : text;
|
|
38
|
+
}
|
|
39
|
+
return snapshot.summary?.trim() || null;
|
|
40
|
+
}
|
|
41
|
+
function hasUnansweredQuestion(messages) {
|
|
42
|
+
if (!messages?.length || messages[messages.length - 1]?.role === "user")
|
|
43
|
+
return false;
|
|
44
|
+
const answered = new Set();
|
|
45
|
+
for (const turn of messages) {
|
|
46
|
+
for (const block of turn.content) {
|
|
47
|
+
if (block.type === "tool_result")
|
|
48
|
+
answered.add(block.tool_use_id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let turnIndex = messages.length - 1; turnIndex >= 0; turnIndex--) {
|
|
52
|
+
const turn = messages[turnIndex];
|
|
53
|
+
if (turn.role === "user" && turn.content.some((block) => block.type === "text"))
|
|
54
|
+
break;
|
|
55
|
+
if (turn.content.some((block) => block.type === "tool_use"
|
|
56
|
+
&& (block.name === "AskUserQuestion" || block.semantic?.kind === "question_request")
|
|
57
|
+
&& !answered.has(block.id)))
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function activityState(snapshot, event) {
|
|
63
|
+
if (snapshot.pendingEscalation || snapshot.permissionBlocked)
|
|
64
|
+
return "needs_permission";
|
|
65
|
+
if (hasUnansweredQuestion(snapshot.messages))
|
|
66
|
+
return "needs_input";
|
|
67
|
+
if (snapshot.status === "failed" || (event?.type === "ended" && snapshot.exitCode !== null && snapshot.exitCode !== 0))
|
|
68
|
+
return "failed";
|
|
69
|
+
if (snapshot.status === "running" || snapshot.structuredState?.inFlight)
|
|
70
|
+
return "working";
|
|
71
|
+
return "done";
|
|
72
|
+
}
|
|
73
|
+
function missionStatus(attempts) {
|
|
74
|
+
if (attempts.length === 0)
|
|
75
|
+
return "dispatching";
|
|
76
|
+
if (attempts.some((attempt) => attempt.state === "needs_input" || attempt.state === "needs_permission"))
|
|
77
|
+
return "needs_input";
|
|
78
|
+
if (attempts.some((attempt) => attempt.state === "working" || attempt.state === "queued"))
|
|
79
|
+
return "running";
|
|
80
|
+
if (attempts.some((attempt) => attempt.state === "done"))
|
|
81
|
+
return "completed";
|
|
82
|
+
return "failed";
|
|
83
|
+
}
|
|
84
|
+
function reviewPrompt(comments) {
|
|
85
|
+
const lines = comments.map((comment, index) => {
|
|
86
|
+
const location = comment.line === null ? comment.filePath : `${comment.filePath}:${comment.line}`;
|
|
87
|
+
return `${index + 1}. ${location} [${comment.side}] — ${comment.body}`;
|
|
88
|
+
});
|
|
89
|
+
return [
|
|
90
|
+
"Please address the following review feedback for this task.",
|
|
91
|
+
"Apply all requested changes in the current worktree, preserve unrelated work, and run focused verification before replying.",
|
|
92
|
+
"",
|
|
93
|
+
...lines,
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Deep task-orchestration module. Callers create missions, observe one inbox,
|
|
98
|
+
* and review diffs without coordinating session/worktree/storage details.
|
|
99
|
+
*/
|
|
100
|
+
export class Missions {
|
|
101
|
+
storage;
|
|
102
|
+
structured;
|
|
103
|
+
sessions;
|
|
104
|
+
constructor(storage, structured, sessions) {
|
|
105
|
+
this.storage = storage;
|
|
106
|
+
this.structured = structured;
|
|
107
|
+
this.sessions = sessions;
|
|
108
|
+
}
|
|
109
|
+
list() {
|
|
110
|
+
return this.storage.listMissions().map((mission) => this.details(mission));
|
|
111
|
+
}
|
|
112
|
+
get(id) {
|
|
113
|
+
const mission = this.storage.getMission(id);
|
|
114
|
+
return mission ? this.details(mission) : null;
|
|
115
|
+
}
|
|
116
|
+
inbox() {
|
|
117
|
+
this.seedInbox();
|
|
118
|
+
return this.storage.listAgentActivity();
|
|
119
|
+
}
|
|
120
|
+
markInboxRead(sessionId) {
|
|
121
|
+
this.storage.markAgentActivityRead(sessionId);
|
|
122
|
+
}
|
|
123
|
+
create(input) {
|
|
124
|
+
const prompt = input.prompt?.trim();
|
|
125
|
+
if (!prompt)
|
|
126
|
+
throw new Error("任务提示词不能为空。");
|
|
127
|
+
if (prompt.length > 200_000)
|
|
128
|
+
throw new Error("任务提示词不能超过 200000 个字符。");
|
|
129
|
+
const cwd = path.resolve(input.cwd || "");
|
|
130
|
+
if (!existsSync(cwd) || !statSync(cwd).isDirectory())
|
|
131
|
+
throw new Error("任务工作目录不存在。");
|
|
132
|
+
const providers = [...new Set(input.providers ?? [])];
|
|
133
|
+
if (providers.length === 0 || providers.length > MAX_ATTEMPTS || providers.some((provider) => !PROVIDERS.has(provider))) {
|
|
134
|
+
throw new Error(`请选择 1-${MAX_ATTEMPTS} 个有效 provider。`);
|
|
135
|
+
}
|
|
136
|
+
const createdAt = nowIso();
|
|
137
|
+
const mission = {
|
|
138
|
+
id: randomUUID(),
|
|
139
|
+
title: input.title?.trim().slice(0, 120) || firstPromptLine(prompt),
|
|
140
|
+
prompt,
|
|
141
|
+
cwd,
|
|
142
|
+
status: "dispatching",
|
|
143
|
+
worktree: {
|
|
144
|
+
baseRef: input.baseRef?.trim() || undefined,
|
|
145
|
+
sharedDirectories: normalizeStringList(input.sharedDirectories, "sharedDirectories"),
|
|
146
|
+
copyPaths: normalizeStringList(input.copyPaths, "copyPaths"),
|
|
147
|
+
},
|
|
148
|
+
createdAt,
|
|
149
|
+
updatedAt: createdAt,
|
|
150
|
+
};
|
|
151
|
+
this.storage.saveMission(mission);
|
|
152
|
+
for (const provider of providers)
|
|
153
|
+
this.dispatchAttempt(mission, provider);
|
|
154
|
+
this.refreshMissionStatus(mission.id);
|
|
155
|
+
return this.get(mission.id);
|
|
156
|
+
}
|
|
157
|
+
ingest(event) {
|
|
158
|
+
if (!event.sessionId || event.sessionId === "__system__")
|
|
159
|
+
return null;
|
|
160
|
+
const snapshot = this.sessions.getLatest(event.sessionId);
|
|
161
|
+
if (!snapshot)
|
|
162
|
+
return null;
|
|
163
|
+
const attempt = this.storage.getMissionAttemptBySession(event.sessionId);
|
|
164
|
+
const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
|
|
165
|
+
const state = activityState(snapshot, event);
|
|
166
|
+
const updatedAt = nowIso();
|
|
167
|
+
const item = {
|
|
168
|
+
sessionId: snapshot.id,
|
|
169
|
+
missionId: attempt?.missionId ?? null,
|
|
170
|
+
attemptId: attempt?.id ?? null,
|
|
171
|
+
state,
|
|
172
|
+
title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
|
|
173
|
+
summary: sessionSummary(snapshot),
|
|
174
|
+
provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
|
|
175
|
+
cwd: snapshot.cwd || null,
|
|
176
|
+
updatedAt,
|
|
177
|
+
readAt: null,
|
|
178
|
+
};
|
|
179
|
+
this.storage.upsertAgentActivity(item);
|
|
180
|
+
if (attempt) {
|
|
181
|
+
this.storage.saveMissionAttempt({
|
|
182
|
+
...attempt,
|
|
183
|
+
state: state,
|
|
184
|
+
summary: item.summary,
|
|
185
|
+
error: state === "failed" ? snapshot.structuredState?.lastError ?? "任务执行失败" : null,
|
|
186
|
+
updatedAt,
|
|
187
|
+
});
|
|
188
|
+
this.refreshMissionStatus(attempt.missionId);
|
|
189
|
+
}
|
|
190
|
+
return item;
|
|
191
|
+
}
|
|
192
|
+
diff(missionId, attemptId) {
|
|
193
|
+
const attempt = this.requireAttempt(missionId, attemptId);
|
|
194
|
+
if (!attempt.worktreePath || !attempt.baseRef)
|
|
195
|
+
throw new Error("这个 attempt 没有可审查的 worktree。");
|
|
196
|
+
return buildMissionDiff({ missionId, attemptId, cwd: attempt.worktreePath, baseRef: attempt.baseRef });
|
|
197
|
+
}
|
|
198
|
+
addReviewComment(missionId, attemptId, input) {
|
|
199
|
+
this.requireAttempt(missionId, attemptId);
|
|
200
|
+
const body = input.body?.trim();
|
|
201
|
+
const filePath = input.filePath?.trim();
|
|
202
|
+
if (!body || !filePath)
|
|
203
|
+
throw new Error("文件和 review 内容不能为空。");
|
|
204
|
+
const createdAt = nowIso();
|
|
205
|
+
const comment = {
|
|
206
|
+
id: randomUUID(), missionId, attemptId, filePath,
|
|
207
|
+
line: typeof input.line === "number" && Number.isFinite(input.line) ? Math.max(1, Math.floor(input.line)) : null,
|
|
208
|
+
side: input.side === "old" ? "old" : "new",
|
|
209
|
+
body, status: "pending", createdAt, sentAt: null, resolvedAt: null,
|
|
210
|
+
};
|
|
211
|
+
this.storage.saveMissionReviewComment(comment);
|
|
212
|
+
return comment;
|
|
213
|
+
}
|
|
214
|
+
sendReview(missionId, attemptId, commentIds) {
|
|
215
|
+
const attempt = this.requireAttempt(missionId, attemptId);
|
|
216
|
+
if (!attempt.sessionId)
|
|
217
|
+
throw new Error("这个 attempt 没有关联会话。");
|
|
218
|
+
const selected = this.storage.listMissionReviewComments(missionId, attemptId)
|
|
219
|
+
.filter((comment) => comment.status === "pending" && (!commentIds?.length || commentIds.includes(comment.id)));
|
|
220
|
+
if (selected.length === 0)
|
|
221
|
+
throw new Error("没有待发送的 review 意见。");
|
|
222
|
+
const session = this.structured.get(attempt.sessionId);
|
|
223
|
+
if (!session)
|
|
224
|
+
throw new Error("任务会话当前不可用。");
|
|
225
|
+
const completion = this.structured.sendMessage(attempt.sessionId, reviewPrompt(selected));
|
|
226
|
+
completion.catch((error) => {
|
|
227
|
+
console.error(`[Missions] Review dispatch failed for ${attempt.id}:`, error);
|
|
228
|
+
});
|
|
229
|
+
this.storage.updateMissionReviewStatus(selected.map((comment) => comment.id), "sent");
|
|
230
|
+
this.storage.saveMissionAttempt({ ...attempt, state: "working", updatedAt: nowIso() });
|
|
231
|
+
this.refreshMissionStatus(missionId);
|
|
232
|
+
return this.storage.listMissionReviewComments(missionId, attemptId);
|
|
233
|
+
}
|
|
234
|
+
resolveReview(missionId, attemptId, commentIds) {
|
|
235
|
+
this.requireAttempt(missionId, attemptId);
|
|
236
|
+
this.storage.updateMissionReviewStatus(commentIds, "resolved");
|
|
237
|
+
return this.storage.listMissionReviewComments(missionId, attemptId);
|
|
238
|
+
}
|
|
239
|
+
archive(id) {
|
|
240
|
+
const mission = this.storage.getMission(id);
|
|
241
|
+
if (!mission)
|
|
242
|
+
throw new Error("任务不存在。");
|
|
243
|
+
this.storage.updateMissionStatus(id, "archived");
|
|
244
|
+
return this.getIncludingArchived(id);
|
|
245
|
+
}
|
|
246
|
+
dispatchAttempt(mission, provider) {
|
|
247
|
+
const attemptId = randomUUID();
|
|
248
|
+
const createdAt = nowIso();
|
|
249
|
+
let attempt = {
|
|
250
|
+
id: attemptId, missionId: mission.id, sessionId: null, provider, state: "queued",
|
|
251
|
+
branch: null, worktreePath: null, baseRef: mission.worktree.baseRef ?? null,
|
|
252
|
+
summary: null, error: null, createdAt, updatedAt: createdAt,
|
|
253
|
+
};
|
|
254
|
+
this.storage.saveMissionAttempt(attempt);
|
|
255
|
+
try {
|
|
256
|
+
const session = this.structured.createSession({
|
|
257
|
+
cwd: mission.cwd,
|
|
258
|
+
mode: "agent",
|
|
259
|
+
provider,
|
|
260
|
+
worktreeEnabled: true,
|
|
261
|
+
worktreeSpec: {
|
|
262
|
+
baseRef: mission.worktree.baseRef,
|
|
263
|
+
taskName: `${mission.title}-${provider}`,
|
|
264
|
+
sharedDirectories: mission.worktree.sharedDirectories,
|
|
265
|
+
copyPaths: mission.worktree.copyPaths,
|
|
266
|
+
},
|
|
267
|
+
sessionSource: "automation",
|
|
268
|
+
automationId: mission.id,
|
|
269
|
+
});
|
|
270
|
+
attempt = {
|
|
271
|
+
...attempt,
|
|
272
|
+
sessionId: session.id,
|
|
273
|
+
state: "working",
|
|
274
|
+
branch: session.worktree?.branch ?? null,
|
|
275
|
+
worktreePath: session.worktree?.path ?? null,
|
|
276
|
+
baseRef: session.worktree?.baseRef ?? mission.worktree.baseRef ?? null,
|
|
277
|
+
updatedAt: nowIso(),
|
|
278
|
+
};
|
|
279
|
+
this.storage.saveMissionAttempt(attempt);
|
|
280
|
+
this.storage.upsertAgentActivity({
|
|
281
|
+
sessionId: session.id, missionId: mission.id, attemptId, state: "working",
|
|
282
|
+
title: mission.title, summary: `已分派给 ${provider}`, provider, cwd: session.cwd,
|
|
283
|
+
updatedAt: attempt.updatedAt, readAt: null,
|
|
284
|
+
});
|
|
285
|
+
const completion = this.structured.sendMessage(session.id, mission.prompt);
|
|
286
|
+
completion.catch((error) => {
|
|
287
|
+
console.error(`[Missions] Attempt ${attemptId} failed after dispatch:`, error);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.storage.saveMissionAttempt({
|
|
292
|
+
...attempt,
|
|
293
|
+
state: "failed",
|
|
294
|
+
error: error instanceof Error ? error.message : String(error),
|
|
295
|
+
updatedAt: nowIso(),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
seedInbox() {
|
|
300
|
+
const existing = new Set(this.storage.listAgentActivity().map((item) => item.sessionId));
|
|
301
|
+
for (const snapshot of this.sessions.listSlim()) {
|
|
302
|
+
if (existing.has(snapshot.id))
|
|
303
|
+
continue;
|
|
304
|
+
const attempt = this.storage.getMissionAttemptBySession(snapshot.id);
|
|
305
|
+
const mission = attempt ? this.storage.getMission(attempt.missionId) : null;
|
|
306
|
+
const at = nowIso();
|
|
307
|
+
this.storage.upsertAgentActivity({
|
|
308
|
+
sessionId: snapshot.id,
|
|
309
|
+
missionId: attempt?.missionId ?? null,
|
|
310
|
+
attemptId: attempt?.id ?? null,
|
|
311
|
+
state: activityState(snapshot),
|
|
312
|
+
title: mission?.title || snapshot.title || `${snapshot.provider ?? "agent"} 会话`,
|
|
313
|
+
summary: sessionSummary(snapshot),
|
|
314
|
+
provider: snapshot.provider ?? snapshot.structuredState?.provider ?? null,
|
|
315
|
+
cwd: snapshot.cwd || null,
|
|
316
|
+
updatedAt: snapshot.endedAt || snapshot.startedAt || at,
|
|
317
|
+
readAt: at,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
details(mission) {
|
|
322
|
+
return {
|
|
323
|
+
...mission,
|
|
324
|
+
attempts: this.storage.listMissionAttempts(mission.id),
|
|
325
|
+
comments: this.storage.listMissionReviewComments(mission.id),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
getIncludingArchived(id) {
|
|
329
|
+
const mission = this.storage.getMission(id);
|
|
330
|
+
return mission ? this.details(mission) : null;
|
|
331
|
+
}
|
|
332
|
+
requireAttempt(missionId, attemptId) {
|
|
333
|
+
const attempt = this.storage.getMissionAttempt(attemptId);
|
|
334
|
+
if (!attempt || attempt.missionId !== missionId)
|
|
335
|
+
throw new Error("任务 attempt 不存在。");
|
|
336
|
+
return attempt;
|
|
337
|
+
}
|
|
338
|
+
refreshMissionStatus(missionId) {
|
|
339
|
+
const mission = this.storage.getMission(missionId);
|
|
340
|
+
if (!mission || mission.status === "archived")
|
|
341
|
+
return;
|
|
342
|
+
this.storage.updateMissionStatus(missionId, missionStatus(this.storage.listMissionAttempts(missionId)));
|
|
343
|
+
}
|
|
344
|
+
}
|
package/dist/models.d.ts
CHANGED
package/dist/models.js
CHANGED
|
@@ -53,6 +53,9 @@ const QODER_FALLBACK_MODELS = [
|
|
|
53
53
|
{ id: "performance", label: "Performance" },
|
|
54
54
|
{ id: "ultimate", label: "Ultimate" },
|
|
55
55
|
];
|
|
56
|
+
const PI_FALLBACK_MODELS = [
|
|
57
|
+
{ id: "default", label: "跟随 Pi 默认", alias: true },
|
|
58
|
+
];
|
|
56
59
|
function cloneModels(models) {
|
|
57
60
|
return models.map((model) => ({
|
|
58
61
|
...model,
|
|
@@ -68,6 +71,7 @@ function cloneCache(cache) {
|
|
|
68
71
|
opencodeModels: cloneModels(cache.opencodeModels),
|
|
69
72
|
grokModels: cloneModels(cache.grokModels),
|
|
70
73
|
qoderModels: cloneModels(cache.qoderModels),
|
|
74
|
+
piModels: cloneModels(cache.piModels),
|
|
71
75
|
claudeVersion: cache.claudeVersion,
|
|
72
76
|
opencodeVersion: cache.opencodeVersion,
|
|
73
77
|
refreshedAt: cache.refreshedAt,
|
|
@@ -235,6 +239,7 @@ function createInitialCache(options) {
|
|
|
235
239
|
opencodeModels: cloneModels(OPENCODE_FALLBACK_MODELS),
|
|
236
240
|
grokModels: cloneModels(GROK_FALLBACK_MODELS),
|
|
237
241
|
qoderModels: cloneModels(QODER_FALLBACK_MODELS),
|
|
242
|
+
piModels: cloneModels(PI_FALLBACK_MODELS),
|
|
238
243
|
claudeVersion: null,
|
|
239
244
|
opencodeVersion: null,
|
|
240
245
|
refreshedAt: now.toISOString(),
|
|
@@ -523,6 +528,7 @@ function catalogRevision(cache) {
|
|
|
523
528
|
opencodeModels: cache.opencodeModels,
|
|
524
529
|
grokModels: cache.grokModels,
|
|
525
530
|
qoderModels: cache.qoderModels,
|
|
531
|
+
piModels: cache.piModels,
|
|
526
532
|
claudeVersion: cache.claudeVersion,
|
|
527
533
|
opencodeVersion: cache.opencodeVersion,
|
|
528
534
|
});
|
|
@@ -603,8 +609,9 @@ function parsePersistedModelCatalog(value) {
|
|
|
603
609
|
const opencodeModels = parsePersistedModelList(catalog.opencodeModels);
|
|
604
610
|
const grokModels = parsePersistedModelList(catalog.grokModels);
|
|
605
611
|
const qoderModels = parsePersistedModelList(catalog.qoderModels);
|
|
612
|
+
const piModels = parsePersistedModelList(catalog.piModels);
|
|
606
613
|
const refreshedAt = safePersistedString(catalog.refreshedAt, 64);
|
|
607
|
-
if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels
|
|
614
|
+
if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels || !piModels
|
|
608
615
|
|| !refreshedAt || Number.isNaN(Date.parse(refreshedAt))) {
|
|
609
616
|
return null;
|
|
610
617
|
}
|
|
@@ -619,6 +626,7 @@ function parsePersistedModelCatalog(value) {
|
|
|
619
626
|
opencodeModels,
|
|
620
627
|
grokModels,
|
|
621
628
|
qoderModels,
|
|
629
|
+
piModels,
|
|
622
630
|
claudeVersion,
|
|
623
631
|
opencodeVersion,
|
|
624
632
|
refreshedAt,
|
|
@@ -696,6 +704,7 @@ async function discoverModelCache(options, previous) {
|
|
|
696
704
|
opencodeModels: opencodeProbe.models.ok ? opencodeProbe.models.value : cloneModels(previous.opencodeModels),
|
|
697
705
|
grokModels: grokProbe.ok ? grokProbe.value : cloneModels(previous.grokModels),
|
|
698
706
|
qoderModels: qoderProbe.ok ? qoderProbe.value : cloneModels(previous.qoderModels),
|
|
707
|
+
piModels: cloneModels(previous.piModels),
|
|
699
708
|
claudeVersion,
|
|
700
709
|
opencodeVersion: opencodeProbe.version.ok ? opencodeProbe.version.value : previous.opencodeVersion,
|
|
701
710
|
refreshedAt: now.toISOString(),
|
package/dist/path-repair.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
/** 关键的 CLI 工具,会被诊断输出。 */
|
|
9
|
-
const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli"];
|
|
9
|
+
const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli", "pi"];
|
|
10
10
|
const DEEP_PROBE_TIMEOUT_MS = 4000;
|
|
11
11
|
/**
|
|
12
12
|
* 构造候选 bin 目录列表(按优先级,前面的更可信)。
|
package/dist/process-manager.js
CHANGED
|
@@ -15,7 +15,7 @@ import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
|
|
|
15
15
|
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
16
16
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
17
17
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
18
|
-
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
|
|
18
|
+
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
19
19
|
import { SessionTopicCoordinator } from "./session-topic.js";
|
|
20
20
|
import { getErrorMessage } from "./error-utils.js";
|
|
21
21
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
@@ -28,6 +28,8 @@ function resolveProviderFromCommand(command) {
|
|
|
28
28
|
return "opencode";
|
|
29
29
|
if (/^grok\b/.test(command.trim()))
|
|
30
30
|
return "grok";
|
|
31
|
+
if (/^pi\b/.test(command.trim()))
|
|
32
|
+
return "pi";
|
|
31
33
|
return /^qodercli\b/.test(command.trim()) ? "qoder" : "claude";
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
@@ -2210,6 +2212,17 @@ export class ProcessManager extends EventEmitter {
|
|
|
2210
2212
|
}
|
|
2211
2213
|
return result;
|
|
2212
2214
|
}
|
|
2215
|
+
if (provider === "pi") {
|
|
2216
|
+
let result = command;
|
|
2217
|
+
const trimmedModel = model?.trim();
|
|
2218
|
+
if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result)) {
|
|
2219
|
+
result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
|
|
2220
|
+
}
|
|
2221
|
+
const level = thinkingEffortToPiLevel(thinkingEffort ?? null);
|
|
2222
|
+
if (level && !/--thinking(?:\s|=)/.test(result))
|
|
2223
|
+
result += ` --thinking '${level}'`;
|
|
2224
|
+
return result;
|
|
2225
|
+
}
|
|
2213
2226
|
const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
|
|
2214
2227
|
if (!isClaudeCmd)
|
|
2215
2228
|
return command;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type QuickCommitAiOptions } from "./git-quick-commit.js";
|
|
2
2
|
export declare class PromptOptimizeError extends Error {
|
|
3
3
|
readonly code: string;
|
|
4
4
|
constructor(message: string, code: string);
|
|
5
5
|
}
|
|
6
|
-
export declare function optimizePrompt(rawText: string, language: string, cwd?: string,
|
|
6
|
+
export declare function optimizePrompt(rawText: string, language: string, cwd?: string, ai?: QuickCommitAiOptions): Promise<string>;
|