kcode-pi 0.1.24 → 0.1.30
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 +3 -0
- package/docs/CHANGELOG.md +82 -0
- package/docs/COMMAND_REFERENCE.md +55 -0
- package/docs/HARNESS_WORKFLOW.md +33 -0
- package/extensions/kingdee-harness.ts +127 -5
- package/extensions/kingdee-header.ts +14 -1
- package/extensions/kingdee-subagents.ts +430 -0
- package/extensions/kingdee-tools.ts +19 -5
- package/package.json +2 -1
- package/prompts/kd-verify.md +1 -1
- package/skills/kd-verify/SKILL.md +2 -2
- package/src/harness/artifacts.ts +11 -3
- package/src/harness/delegation.ts +297 -0
- package/src/harness/evidence.ts +16 -5
- package/src/harness/gates.ts +10 -4
- package/src/harness/prompt.ts +13 -1
- package/src/harness/repair.ts +224 -0
- package/src/harness/state.ts +97 -11
- package/src/harness/types.ts +10 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { Type, type Message } from "@earendil-works/pi-ai";
|
|
6
|
+
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
buildChainedDelegationRequest,
|
|
9
|
+
buildDelegationCommandPrompt,
|
|
10
|
+
buildDelegationPrompt,
|
|
11
|
+
CHILD_AGENT_USER_TASK,
|
|
12
|
+
DEFAULT_REVIEW_TASK,
|
|
13
|
+
delegationBlockReason,
|
|
14
|
+
formatDelegationPreview,
|
|
15
|
+
isDelegationRole,
|
|
16
|
+
KD_DELEGATE_USAGE,
|
|
17
|
+
KD_DELEGATE_COMMAND_DESCRIPTION,
|
|
18
|
+
KD_REVIEW_COMMAND_DESCRIPTION,
|
|
19
|
+
KD_SUBAGENT_INVALID_PARAMS,
|
|
20
|
+
KD_SUBAGENT_PARALLEL_ROLE_ERROR,
|
|
21
|
+
KD_SUBAGENT_SCHEMA_DESCRIPTIONS,
|
|
22
|
+
KD_SUBAGENT_TOOL_DESCRIPTION,
|
|
23
|
+
isReadOnlyDelegationRole,
|
|
24
|
+
isSubagentChild,
|
|
25
|
+
parallelDelegationBlockReason,
|
|
26
|
+
parseDelegationArgs,
|
|
27
|
+
subagentAllowedTools,
|
|
28
|
+
type DelegationRequest,
|
|
29
|
+
type DelegationRole,
|
|
30
|
+
} from "../src/harness/delegation.ts";
|
|
31
|
+
import { readActiveRun } from "../src/harness/state.ts";
|
|
32
|
+
|
|
33
|
+
interface ChildAgentResult {
|
|
34
|
+
role: DelegationRole;
|
|
35
|
+
task: string;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
output: string;
|
|
38
|
+
stderr: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
turns: number;
|
|
41
|
+
step?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type RawChildAgentResult = Omit<ChildAgentResult, "role" | "task" | "step">;
|
|
45
|
+
|
|
46
|
+
const DEFAULT_OUTPUT_LIMIT = 30_000;
|
|
47
|
+
const MAX_PARALLEL_TASKS = 8;
|
|
48
|
+
const MAX_CONCURRENCY = 4;
|
|
49
|
+
const MAX_CHAIN_TASKS = 8;
|
|
50
|
+
|
|
51
|
+
const kdSubagentTool = defineTool({
|
|
52
|
+
name: "kd_subagent",
|
|
53
|
+
label: "KD 子Agent",
|
|
54
|
+
description: KD_SUBAGENT_TOOL_DESCRIPTION,
|
|
55
|
+
parameters: Type.Object({
|
|
56
|
+
role: Type.Optional(Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.role })),
|
|
57
|
+
task: Type.Optional(Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.task })),
|
|
58
|
+
tasks: Type.Optional(
|
|
59
|
+
Type.Array(
|
|
60
|
+
Type.Object({
|
|
61
|
+
role: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemRole }),
|
|
62
|
+
task: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemTask }),
|
|
63
|
+
}),
|
|
64
|
+
{ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.tasks },
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
chain: Type.Optional(
|
|
68
|
+
Type.Array(
|
|
69
|
+
Type.Object({
|
|
70
|
+
role: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemRole }),
|
|
71
|
+
task: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemTask }),
|
|
72
|
+
}),
|
|
73
|
+
{ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.chain },
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
dryRun: Type.Optional(Type.Boolean({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.dryRun })),
|
|
77
|
+
maxOutputChars: Type.Optional(Type.Number({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.maxOutputChars })),
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
81
|
+
const mode = resolveMode(params);
|
|
82
|
+
if (!mode) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: KD_SUBAGENT_INVALID_PARAMS }],
|
|
85
|
+
details: { error: "invalid-params" },
|
|
86
|
+
isError: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const run = readActiveRun(ctx.cwd);
|
|
91
|
+
if (params.dryRun) {
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: formatModePreview(ctx.cwd, run, mode.requests) }],
|
|
94
|
+
details: { mode: mode.kind, dryRun: true, requests: mode.requests },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const modeBlockReason = mode.kind === "parallel" ? parallelDelegationBlockReason(mode.requests) : undefined;
|
|
99
|
+
const blockReason = modeBlockReason ?? mode.requests.map((request) => delegationBlockReason(request.role, run)).find(Boolean);
|
|
100
|
+
if (blockReason) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: blockReason }],
|
|
103
|
+
details: { error: "blocked", mode: mode.kind, run },
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const limit = normalizeOutputLimit(params.maxOutputChars);
|
|
109
|
+
const results =
|
|
110
|
+
mode.kind === "parallel"
|
|
111
|
+
? await runParallelAgents(ctx, run, mode.requests, signal)
|
|
112
|
+
: mode.kind === "chain"
|
|
113
|
+
? await runChainedAgents(ctx, run, mode.requests, signal)
|
|
114
|
+
: [await runDelegation(ctx, run, mode.requests[0], signal)];
|
|
115
|
+
const text = formatModeResult(mode.kind, results, limit);
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text }],
|
|
118
|
+
details: { mode: mode.kind, results },
|
|
119
|
+
isError: results.some((result) => result.exitCode !== 0),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export default function (pi: ExtensionAPI) {
|
|
125
|
+
if (isSubagentChild()) return;
|
|
126
|
+
|
|
127
|
+
pi.registerTool(kdSubagentTool);
|
|
128
|
+
|
|
129
|
+
pi.registerCommand("kd-delegate", {
|
|
130
|
+
description: KD_DELEGATE_COMMAND_DESCRIPTION,
|
|
131
|
+
handler: async (args, ctx) => {
|
|
132
|
+
const parsed = parseDelegationArgs(args);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
ctx.ui.notify(KD_DELEGATE_USAGE, "error");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
sendDelegationPrompt(pi, ctx, parsed.role, parsed.task, parsed.dryRun);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.registerCommand("kd-review", {
|
|
142
|
+
description: KD_REVIEW_COMMAND_DESCRIPTION,
|
|
143
|
+
handler: async (args, ctx) => {
|
|
144
|
+
const task = args.trim() || DEFAULT_REVIEW_TASK;
|
|
145
|
+
sendDelegationPrompt(pi, ctx, "review", task, false);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sendDelegationPrompt(pi: ExtensionAPI, ctx: ExtensionContext, role: DelegationRole, task: string, dryRun: boolean): void {
|
|
151
|
+
const message = buildDelegationCommandPrompt({ role, task }, dryRun);
|
|
152
|
+
if (ctx.isIdle()) pi.sendUserMessage(message);
|
|
153
|
+
else pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveMode(params: {
|
|
157
|
+
role?: unknown;
|
|
158
|
+
task?: unknown;
|
|
159
|
+
tasks?: unknown;
|
|
160
|
+
chain?: unknown;
|
|
161
|
+
}): { kind: "single" | "parallel" | "chain"; requests: DelegationRequest[] } | undefined {
|
|
162
|
+
const single = normalizeRequest(params.role, params.task);
|
|
163
|
+
const tasks = Array.isArray(params.tasks) ? normalizeTaskList(params.tasks) : [];
|
|
164
|
+
const chain = Array.isArray(params.chain) ? normalizeTaskList(params.chain) : [];
|
|
165
|
+
if (tasks === undefined || chain === undefined) return undefined;
|
|
166
|
+
const modeCount = Number(Boolean(single)) + Number(tasks.length > 0) + Number(chain.length > 0);
|
|
167
|
+
if (modeCount !== 1) return undefined;
|
|
168
|
+
if (single) return { kind: "single", requests: [single] };
|
|
169
|
+
if (tasks.length > 0 && tasks.length <= MAX_PARALLEL_TASKS) return { kind: "parallel", requests: tasks };
|
|
170
|
+
if (chain.length > 0 && chain.length <= MAX_CHAIN_TASKS) return { kind: "chain", requests: chain };
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeTaskList(values: unknown[]): DelegationRequest[] | undefined {
|
|
175
|
+
const requests = values.map(normalizeTaskItem);
|
|
176
|
+
if (requests.some((request) => !request)) return undefined;
|
|
177
|
+
return requests as DelegationRequest[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeTaskItem(value: unknown): DelegationRequest | undefined {
|
|
181
|
+
if (!value || typeof value !== "object") return undefined;
|
|
182
|
+
const item = value as { role?: unknown; task?: unknown };
|
|
183
|
+
return normalizeRequest(item.role, item.task);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeRequest(roleValue: unknown, taskValue: unknown): DelegationRequest | undefined {
|
|
187
|
+
const role = typeof roleValue === "string" ? roleValue.toLowerCase() : "";
|
|
188
|
+
const task = typeof taskValue === "string" ? taskValue.trim() : "";
|
|
189
|
+
if (!isDelegationRole(role) || !task) return undefined;
|
|
190
|
+
return { role, task };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatModePreview(cwd: string, run: ReturnType<typeof readActiveRun>, requests: DelegationRequest[]): string {
|
|
194
|
+
return requests.map((request, index) => [`## Task ${index + 1}`, formatDelegationPreview(cwd, run, request)].join("\n\n")).join("\n\n---\n\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function runDelegation(
|
|
198
|
+
ctx: ExtensionContext,
|
|
199
|
+
run: ReturnType<typeof readActiveRun>,
|
|
200
|
+
request: DelegationRequest,
|
|
201
|
+
signal: AbortSignal | undefined,
|
|
202
|
+
step?: number,
|
|
203
|
+
): Promise<ChildAgentResult> {
|
|
204
|
+
const prompt = buildDelegationPrompt(ctx.cwd, run, request);
|
|
205
|
+
return await runChildAgent(ctx, request, prompt, signal, step);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function runParallelAgents(
|
|
209
|
+
ctx: ExtensionContext,
|
|
210
|
+
run: ReturnType<typeof readActiveRun>,
|
|
211
|
+
requests: DelegationRequest[],
|
|
212
|
+
signal: AbortSignal | undefined,
|
|
213
|
+
): Promise<ChildAgentResult[]> {
|
|
214
|
+
const results: ChildAgentResult[] = new Array(requests.length);
|
|
215
|
+
let next = 0;
|
|
216
|
+
const workerCount = Math.min(MAX_CONCURRENCY, requests.length);
|
|
217
|
+
await Promise.all(
|
|
218
|
+
Array.from({ length: workerCount }, async () => {
|
|
219
|
+
while (true) {
|
|
220
|
+
const index = next++;
|
|
221
|
+
if (index >= requests.length) return;
|
|
222
|
+
results[index] = await runDelegation(ctx, run, requests[index], signal, index + 1);
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function runChainedAgents(
|
|
230
|
+
ctx: ExtensionContext,
|
|
231
|
+
run: ReturnType<typeof readActiveRun>,
|
|
232
|
+
requests: DelegationRequest[],
|
|
233
|
+
signal: AbortSignal | undefined,
|
|
234
|
+
): Promise<ChildAgentResult[]> {
|
|
235
|
+
const results: ChildAgentResult[] = [];
|
|
236
|
+
let previousOutput = "";
|
|
237
|
+
for (let i = 0; i < requests.length; i++) {
|
|
238
|
+
const request = buildChainedDelegationRequest(requests[i], previousOutput);
|
|
239
|
+
const result = await runDelegation(ctx, run, request, signal, i + 1);
|
|
240
|
+
results.push(result);
|
|
241
|
+
previousOutput = result.output;
|
|
242
|
+
if (result.exitCode !== 0) break;
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatModeResult(mode: "single" | "parallel" | "chain", results: ChildAgentResult[], limit: number): string {
|
|
248
|
+
const succeeded = results.filter((result) => result.exitCode === 0).length;
|
|
249
|
+
const header = `子 agent ${mode} 完成:${succeeded}/${results.length} succeeded`;
|
|
250
|
+
const sections = results.map((result, index) => {
|
|
251
|
+
const output = truncateOutput(result.output || result.stderr || "(子 agent 无输出)", limit);
|
|
252
|
+
return [
|
|
253
|
+
`## ${result.step ?? index + 1}. ${result.role}`,
|
|
254
|
+
`Exit:${result.exitCode}`,
|
|
255
|
+
result.model ? `Model:${result.model}` : undefined,
|
|
256
|
+
`Turns:${result.turns}`,
|
|
257
|
+
"",
|
|
258
|
+
output,
|
|
259
|
+
result.stderr.trim() ? `\nSTDERR:\n${truncateOutput(result.stderr.trim(), 4000)}` : undefined,
|
|
260
|
+
]
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.join("\n");
|
|
263
|
+
});
|
|
264
|
+
return [header, "", ...sections].join("\n");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeOutputLimit(value: unknown): number {
|
|
268
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OUTPUT_LIMIT;
|
|
269
|
+
return Math.max(1000, Math.min(100_000, Math.trunc(value)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function runChildAgent(
|
|
273
|
+
ctx: ExtensionContext,
|
|
274
|
+
request: DelegationRequest,
|
|
275
|
+
prompt: string,
|
|
276
|
+
signal: AbortSignal | undefined,
|
|
277
|
+
step?: number,
|
|
278
|
+
): Promise<ChildAgentResult> {
|
|
279
|
+
const temp = writePromptFile(request.role, prompt);
|
|
280
|
+
const args = [
|
|
281
|
+
"--mode",
|
|
282
|
+
"json",
|
|
283
|
+
"-p",
|
|
284
|
+
"--no-session",
|
|
285
|
+
"--tools",
|
|
286
|
+
subagentAllowedTools(request.role).join(","),
|
|
287
|
+
"--append-system-prompt",
|
|
288
|
+
temp.file,
|
|
289
|
+
CHILD_AGENT_USER_TASK,
|
|
290
|
+
];
|
|
291
|
+
const invocation = getPiInvocation(args);
|
|
292
|
+
try {
|
|
293
|
+
const result = await spawnJsonAgent(invocation.command, invocation.args, ctx.cwd, request.role, signal);
|
|
294
|
+
return { ...result, role: request.role, task: request.task, step };
|
|
295
|
+
} finally {
|
|
296
|
+
rmSync(temp.file, { force: true });
|
|
297
|
+
rmSync(temp.dir, { recursive: true, force: true });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function writePromptFile(role: DelegationRole, prompt: string): { dir: string; file: string } {
|
|
302
|
+
const dir = join(tmpdir(), `kcode-subagent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
303
|
+
mkdirSync(dir, { recursive: true });
|
|
304
|
+
const file = join(dir, `${role}.md`);
|
|
305
|
+
writeFileSync(file, prompt, { encoding: "utf8", mode: 0o600 });
|
|
306
|
+
return { dir, file };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
310
|
+
const currentScript = process.argv[1];
|
|
311
|
+
if (currentScript && existsSync(currentScript)) {
|
|
312
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const execName = basename(process.execPath).toLowerCase();
|
|
316
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
317
|
+
if (!isGenericRuntime) return { command: process.execPath, args };
|
|
318
|
+
return { command: "pi", args };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function spawnJsonAgent(
|
|
322
|
+
command: string,
|
|
323
|
+
args: string[],
|
|
324
|
+
cwd: string,
|
|
325
|
+
role: DelegationRole,
|
|
326
|
+
signal: AbortSignal | undefined,
|
|
327
|
+
): Promise<RawChildAgentResult> {
|
|
328
|
+
return await new Promise((resolve) => {
|
|
329
|
+
const messages: Message[] = [];
|
|
330
|
+
let stderr = "";
|
|
331
|
+
let buffer = "";
|
|
332
|
+
let turns = 0;
|
|
333
|
+
let model: string | undefined;
|
|
334
|
+
let completed = false;
|
|
335
|
+
|
|
336
|
+
const proc = spawn(command, args, {
|
|
337
|
+
cwd,
|
|
338
|
+
shell: false,
|
|
339
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
340
|
+
env: {
|
|
341
|
+
...process.env,
|
|
342
|
+
KCODE_SUBAGENT_CHILD: "1",
|
|
343
|
+
KCODE_SUBAGENT_ROLE: role,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const finish = (exitCode: number) => {
|
|
348
|
+
if (completed) return;
|
|
349
|
+
completed = true;
|
|
350
|
+
resolve({
|
|
351
|
+
exitCode,
|
|
352
|
+
output: finalAssistantText(messages),
|
|
353
|
+
stderr,
|
|
354
|
+
model,
|
|
355
|
+
turns,
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const processLine = (line: string) => {
|
|
360
|
+
if (!line.trim()) return;
|
|
361
|
+
let event: unknown;
|
|
362
|
+
try {
|
|
363
|
+
event = JSON.parse(line);
|
|
364
|
+
} catch {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!isJsonEvent(event)) return;
|
|
368
|
+
if (event.type === "message_end" && event.message) {
|
|
369
|
+
messages.push(event.message);
|
|
370
|
+
if (event.message.role === "assistant") {
|
|
371
|
+
turns++;
|
|
372
|
+
if (event.message.model) model = event.message.model;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (event.type === "tool_result_end" && event.message) messages.push(event.message);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
proc.stdout.on("data", (chunk) => {
|
|
379
|
+
buffer += chunk.toString();
|
|
380
|
+
const lines = buffer.split(/\r?\n/);
|
|
381
|
+
buffer = lines.pop() ?? "";
|
|
382
|
+
for (const line of lines) processLine(line);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
proc.stderr.on("data", (chunk) => {
|
|
386
|
+
stderr += chunk.toString();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
proc.on("close", (code) => {
|
|
390
|
+
if (buffer.trim()) processLine(buffer);
|
|
391
|
+
finish(code ?? 0);
|
|
392
|
+
});
|
|
393
|
+
proc.on("error", (error) => {
|
|
394
|
+
stderr += error.message;
|
|
395
|
+
finish(1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (signal) {
|
|
399
|
+
const abort = () => {
|
|
400
|
+
stderr += "\n子 agent 已被中断。";
|
|
401
|
+
proc.kill("SIGTERM");
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
404
|
+
}, 5000);
|
|
405
|
+
};
|
|
406
|
+
if (signal.aborted) abort();
|
|
407
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isJsonEvent(value: unknown): value is { type: string; message?: Message } {
|
|
413
|
+
return Boolean(value) && typeof value === "object" && typeof (value as { type?: unknown }).type === "string";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function finalAssistantText(messages: Message[]): string {
|
|
417
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
418
|
+
const message = messages[i];
|
|
419
|
+
if (message.role !== "assistant") continue;
|
|
420
|
+
for (const part of message.content) {
|
|
421
|
+
if (part.type === "text" && part.text.trim()) return part.text.trim();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return "";
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function truncateOutput(output: string, maxChars: number): string {
|
|
428
|
+
if (output.length <= maxChars) return output;
|
|
429
|
+
return `${output.slice(0, maxChars)}\n\n[子 agent 输出已截断:剩余 ${output.length - maxChars} 字符]`;
|
|
430
|
+
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
writeOfficialEvidence,
|
|
23
23
|
} from "../src/official/kingdee-skills.ts";
|
|
24
24
|
import { resolveWorkspacePath } from "../src/platform/path.ts";
|
|
25
|
-
import { readActiveRun } from "../src/harness/state.ts";
|
|
25
|
+
import { advanceRunIfReady, readActiveRun } from "../src/harness/state.ts";
|
|
26
26
|
import { writeEvidenceFile } from "../src/harness/evidence.ts";
|
|
27
27
|
import { SDK_SIGNATURE_EVIDENCE } from "../src/harness/sdk-policy.ts";
|
|
28
28
|
|
|
@@ -97,12 +97,25 @@ async function runOrDryRun(
|
|
|
97
97
|
|
|
98
98
|
const result = await runOfficialCommand(command);
|
|
99
99
|
const evidencePath = evidenceFile ? writeOfficialEvidence(ctx.cwd, evidenceFile, result) : undefined;
|
|
100
|
+
const autoAdvance = evidencePath ? autoAdvanceAfterEvidence(ctx.cwd) : undefined;
|
|
100
101
|
return {
|
|
101
|
-
content: [{ type: "text" as const, text: formatCommandResult(result) }],
|
|
102
|
-
details: { command: result.command, exitCode: result.exitCode, evidencePath },
|
|
102
|
+
content: [{ type: "text" as const, text: [formatCommandResult(result), evidencePath ? `已写入证据:${evidencePath}` : undefined, autoAdvance?.text].filter(Boolean).join("\n\n") }],
|
|
103
|
+
details: { command: result.command, exitCode: result.exitCode, evidencePath, autoAdvance: autoAdvance?.details },
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
function autoAdvanceAfterEvidence(cwd: string): { text: string; details: ReturnType<typeof advanceRunIfReady> } | undefined {
|
|
108
|
+
const run = readActiveRun(cwd);
|
|
109
|
+
if (!run) return undefined;
|
|
110
|
+
|
|
111
|
+
const result = advanceRunIfReady(cwd, run);
|
|
112
|
+
if (result.advanced) {
|
|
113
|
+
return { text: result.message, details: result };
|
|
114
|
+
}
|
|
115
|
+
if (result.message.includes("最终阶段")) return undefined;
|
|
116
|
+
return { text: `门禁仍阻塞:${result.message}`, details: result };
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
const kdSearchTool = defineTool({
|
|
107
120
|
name: "kd_search",
|
|
108
121
|
label: "KD 搜索",
|
|
@@ -334,9 +347,10 @@ const kdSdkSignatureTool = defineTool({
|
|
|
334
347
|
});
|
|
335
348
|
const text = formatSdkSignatureResult(result);
|
|
336
349
|
const evidencePath = result.exitCode === 0 ? writeSdkSignatureEvidence(ctx.cwd, text) : undefined;
|
|
350
|
+
const autoAdvance = evidencePath ? autoAdvanceAfterEvidence(ctx.cwd) : undefined;
|
|
337
351
|
return {
|
|
338
|
-
content: [{ type: "text", text: evidencePath ?
|
|
339
|
-
details: { product: profile.product, evidencePath, ...result },
|
|
352
|
+
content: [{ type: "text", text: [text, evidencePath ? `已写入 SDK 签名证据:${evidencePath}` : undefined, autoAdvance?.text].filter(Boolean).join("\n\n") }],
|
|
353
|
+
details: { product: profile.product, evidencePath, autoAdvance: autoAdvance?.details, ...result },
|
|
340
354
|
};
|
|
341
355
|
},
|
|
342
356
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kcode-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"extensions": [
|
|
33
33
|
"./extensions/kingdee-header.ts",
|
|
34
34
|
"./extensions/kingdee-harness.ts",
|
|
35
|
+
"./extensions/kingdee-subagents.ts",
|
|
35
36
|
"./extensions/kingdee-tools.ts"
|
|
36
37
|
],
|
|
37
38
|
"skills": [
|
package/prompts/kd-verify.md
CHANGED
|
@@ -4,7 +4,7 @@ description: 验证当前金蝶实现并收集证据。
|
|
|
4
4
|
|
|
5
5
|
使用 `kd-verify` skill。
|
|
6
6
|
|
|
7
|
-
执行 `PLAN.md`
|
|
7
|
+
执行 `PLAN.md` 中的验证命令。验证命令完成后必须调用 `kd_verify_result` 记录 `command`、`exitCode` 和关键输出;不要手工绕过 Harness 的验证结果记录。
|
|
8
8
|
|
|
9
9
|
用户补充说明:
|
|
10
10
|
|
|
@@ -13,12 +13,12 @@ Goal:
|
|
|
13
13
|
- For Cangqiong/Xinghan/Flagship Java projects, run the planned Gradle command to catch syntax/compile errors, for example `.\gradlew.bat build`, `./gradlew build`, or a narrow `:module:build` task.
|
|
14
14
|
- For C#/Enterprise projects, run `dotnet build` or `dotnet build <.sln/.csproj>` to catch syntax/compile errors.
|
|
15
15
|
- Run `kd_check` when code is available.
|
|
16
|
-
-
|
|
16
|
+
- Record the completed validation command through `kd_verify_result` with the real command, exit code, and useful output summary.
|
|
17
17
|
- Record failures, fixes, skipped checks, and residual risk.
|
|
18
18
|
|
|
19
19
|
Rules:
|
|
20
20
|
|
|
21
21
|
- Passing unit tests is not enough if acceptance criteria require workflow behavior.
|
|
22
22
|
- If validation cannot run, state the exact blocker.
|
|
23
|
-
-
|
|
23
|
+
- Do not hand-edit `VERIFY.md` as the primary result path; `kd_verify_result` owns pass/failure evidence and the repair loop.
|
|
24
24
|
- Do not ship while verification evidence is missing.
|
package/src/harness/artifacts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import type { ActiveRun, KdPhase } from "./types.ts";
|
|
3
3
|
import { PHASE_ARTIFACTS } from "./types.ts";
|
|
4
4
|
import { runArtifactPath, runRoot } from "./paths.ts";
|
|
@@ -10,7 +10,11 @@ export function ensureRunDirectories(cwd: string, run: ActiveRun): void {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function artifactExists(cwd: string, run: ActiveRun, artifactName: string): boolean {
|
|
13
|
-
|
|
13
|
+
try {
|
|
14
|
+
return statSync(runArtifactPath(cwd, run, artifactName)).isFile();
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export function phaseArtifactPath(cwd: string, run: ActiveRun, phase: KdPhase): string {
|
|
@@ -19,7 +23,11 @@ export function phaseArtifactPath(cwd: string, run: ActiveRun, phase: KdPhase):
|
|
|
19
23
|
|
|
20
24
|
export function readArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined {
|
|
21
25
|
const path = phaseArtifactPath(cwd, run, phase);
|
|
22
|
-
|
|
26
|
+
try {
|
|
27
|
+
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export function writeArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string {
|