patchwarden 1.1.0 → 1.5.1
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.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal 最终报告导出 — 将 Goal 完成情况渲染为人类可读 Markdown 和机器可读 JSON。
|
|
3
|
+
*
|
|
4
|
+
* exportGoalReport 读取 GoalStatus 和进度统计,关联 Evidence Pack,
|
|
5
|
+
* 生成 REPORT.md 和 report.json 到 {workspaceRoot}/.patchwarden/goals/{goalId}/report/。
|
|
6
|
+
* 所有写入操作使用原子写(.tmp + rename),所有字符串内容经 redactSensitiveValue 脱敏。
|
|
7
|
+
*/
|
|
8
|
+
import { type GoalProgressSummary } from "./goalProgress.js";
|
|
9
|
+
export interface SubgoalReportEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
status: string;
|
|
13
|
+
task_ids: string[];
|
|
14
|
+
evidence_packs: string[];
|
|
15
|
+
accepted_at?: string;
|
|
16
|
+
rejected_reason?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SafeGoalReport {
|
|
19
|
+
goal_id: string;
|
|
20
|
+
generated_at: string;
|
|
21
|
+
path: string;
|
|
22
|
+
completion_rate: number;
|
|
23
|
+
files: {
|
|
24
|
+
report_md: string;
|
|
25
|
+
report_json: string;
|
|
26
|
+
};
|
|
27
|
+
summary: GoalProgressSummary;
|
|
28
|
+
subgoals: SubgoalReportEntry[];
|
|
29
|
+
risks: string[];
|
|
30
|
+
timeline: {
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
status: string;
|
|
34
|
+
};
|
|
35
|
+
bounded: true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 导出 Goal 最终报告,生成 REPORT.md 和 report.json。
|
|
39
|
+
*
|
|
40
|
+
* 流程:
|
|
41
|
+
* 1. 读取 GoalStatus(readGoalStatus)
|
|
42
|
+
* 2. 调用 summarizeGoalProgress 获取完成度统计
|
|
43
|
+
* 3. 遍历 subgoals,构建 SubgoalReportEntry,关联 Evidence Pack
|
|
44
|
+
* 4. 生成 REPORT.md(人类可读)和 report.json(机器可读)
|
|
45
|
+
* 5. 原子写到 {workspaceRoot}/.patchwarden/goals/{goalId}/report/
|
|
46
|
+
* 6. 所有字符串内容经 redactSensitiveValue 脱敏
|
|
47
|
+
*
|
|
48
|
+
* @param goalId Goal 标识
|
|
49
|
+
* @param options 可选配置(workspaceRoot:工作区根目录,默认从 getConfig 获取)
|
|
50
|
+
* @returns SafeGoalReport(脱敏后的报告对象)
|
|
51
|
+
*/
|
|
52
|
+
export declare function exportGoalReport(goalId: string, options?: {
|
|
53
|
+
workspaceRoot?: string;
|
|
54
|
+
}): SafeGoalReport;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal 最终报告导出 — 将 Goal 完成情况渲染为人类可读 Markdown 和机器可读 JSON。
|
|
3
|
+
*
|
|
4
|
+
* exportGoalReport 读取 GoalStatus 和进度统计,关联 Evidence Pack,
|
|
5
|
+
* 生成 REPORT.md 和 report.json 到 {workspaceRoot}/.patchwarden/goals/{goalId}/report/。
|
|
6
|
+
* 所有写入操作使用原子写(.tmp + rename),所有字符串内容经 redactSensitiveValue 脱敏。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { getConfig } from "../config.js";
|
|
11
|
+
import { redactSensitiveValue } from "../security/contentRedaction.js";
|
|
12
|
+
import { listEvidencePacks } from "../tools/evidencePack.js";
|
|
13
|
+
import { summarizeGoalProgress } from "./goalProgress.js";
|
|
14
|
+
import { readGoalStatus } from "./goalStore.js";
|
|
15
|
+
// ── 辅助函数 ──────────────────────────────────────────────────────
|
|
16
|
+
function resolveWorkspaceRoot(workspaceRoot) {
|
|
17
|
+
return workspaceRoot ?? getConfig().workspaceRoot;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 原子写文件:先写到 .tmp 文件,再 renameSync 到目标路径。
|
|
21
|
+
*/
|
|
22
|
+
function atomicWriteFile(filePath, content) {
|
|
23
|
+
const tmpPath = filePath + ".tmp";
|
|
24
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
25
|
+
renameSync(tmpPath, filePath);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 将百分比字符串(如 "100%")解析为数字。
|
|
29
|
+
*/
|
|
30
|
+
function parseCompletionRate(value) {
|
|
31
|
+
const n = parseInt(value, 10);
|
|
32
|
+
return Number.isFinite(n) ? n : 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 查找与 task_ids 关联的 Evidence Pack 路径。
|
|
36
|
+
* 通过 listEvidencePacks 列出所有 pack,匹配 lineage_id 在 task_ids 中的 pack。
|
|
37
|
+
*/
|
|
38
|
+
function collectEvidencePacks(taskIds) {
|
|
39
|
+
if (taskIds.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
const packs = listEvidencePacks({ max_items: 50 });
|
|
42
|
+
const taskIdSet = new Set(taskIds);
|
|
43
|
+
const paths = [];
|
|
44
|
+
for (const pack of packs.evidence_packs) {
|
|
45
|
+
if (taskIdSet.has(pack.lineage_id)) {
|
|
46
|
+
paths.push(pack.path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return paths;
|
|
50
|
+
}
|
|
51
|
+
// ── Markdown 生成 ─────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* 从脱敏后的报告对象生成人类可读 Markdown。
|
|
54
|
+
*/
|
|
55
|
+
function buildReportMarkdown(report, flags) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
// 标题
|
|
58
|
+
lines.push("# Goal Report: " + report.summary.title);
|
|
59
|
+
lines.push("");
|
|
60
|
+
// 元信息
|
|
61
|
+
lines.push("## 元信息");
|
|
62
|
+
lines.push("");
|
|
63
|
+
lines.push("- **goal_id**: " + report.goal_id);
|
|
64
|
+
lines.push("- **generated_at**: " + report.generated_at);
|
|
65
|
+
lines.push("- **status**: " + report.timeline.status);
|
|
66
|
+
lines.push("- **completion_rate**: " + report.completion_rate + "%");
|
|
67
|
+
if (flags.noSubgoals) {
|
|
68
|
+
lines.push("- **备注**: 无子目标");
|
|
69
|
+
}
|
|
70
|
+
if (flags.incomplete) {
|
|
71
|
+
lines.push("- **备注**: 未完成");
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
// 完成度统计表
|
|
75
|
+
lines.push("## 完成度统计");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("| 指标 | 数值 |");
|
|
78
|
+
lines.push("| --- | --- |");
|
|
79
|
+
lines.push("| total | " + report.summary.total + " |");
|
|
80
|
+
lines.push("| accepted | " + report.summary.accepted + " |");
|
|
81
|
+
lines.push("| rejected | " + report.summary.rejected + " |");
|
|
82
|
+
lines.push("| running | " + report.summary.running + " |");
|
|
83
|
+
lines.push("| ready | " + report.summary.ready + " |");
|
|
84
|
+
lines.push("| needs_fix | " + report.summary.needs_fix + " |");
|
|
85
|
+
lines.push("");
|
|
86
|
+
// 子目标清单
|
|
87
|
+
lines.push("## 子目标清单");
|
|
88
|
+
lines.push("");
|
|
89
|
+
if (report.subgoals.length === 0) {
|
|
90
|
+
lines.push("无子目标");
|
|
91
|
+
lines.push("");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
for (const sg of report.subgoals) {
|
|
95
|
+
lines.push("### " + sg.id + ": " + sg.title);
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("- **status**: " + sg.status);
|
|
98
|
+
lines.push("- **task_ids**: " + (sg.task_ids.length > 0 ? sg.task_ids.join(", ") : "无"));
|
|
99
|
+
lines.push("- **evidence_packs**: " +
|
|
100
|
+
(sg.evidence_packs.length > 0 ? sg.evidence_packs.join(", ") : "无"));
|
|
101
|
+
if (sg.accepted_at) {
|
|
102
|
+
lines.push("- **accepted_at**: " + sg.accepted_at);
|
|
103
|
+
}
|
|
104
|
+
if (sg.rejected_reason) {
|
|
105
|
+
lines.push("- **rejected_reason**: " + sg.rejected_reason);
|
|
106
|
+
}
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 风险汇总
|
|
111
|
+
lines.push("## 风险汇总");
|
|
112
|
+
lines.push("");
|
|
113
|
+
if (report.risks.length === 0) {
|
|
114
|
+
lines.push("无");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
for (const r of report.risks) {
|
|
118
|
+
lines.push("- " + r);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push("");
|
|
122
|
+
// 时间线
|
|
123
|
+
lines.push("## 时间线");
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("- **created_at**: " + report.timeline.created_at);
|
|
126
|
+
lines.push("- **updated_at**: " + report.timeline.updated_at);
|
|
127
|
+
lines.push("");
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* 导出 Goal 最终报告,生成 REPORT.md 和 report.json。
|
|
133
|
+
*
|
|
134
|
+
* 流程:
|
|
135
|
+
* 1. 读取 GoalStatus(readGoalStatus)
|
|
136
|
+
* 2. 调用 summarizeGoalProgress 获取完成度统计
|
|
137
|
+
* 3. 遍历 subgoals,构建 SubgoalReportEntry,关联 Evidence Pack
|
|
138
|
+
* 4. 生成 REPORT.md(人类可读)和 report.json(机器可读)
|
|
139
|
+
* 5. 原子写到 {workspaceRoot}/.patchwarden/goals/{goalId}/report/
|
|
140
|
+
* 6. 所有字符串内容经 redactSensitiveValue 脱敏
|
|
141
|
+
*
|
|
142
|
+
* @param goalId Goal 标识
|
|
143
|
+
* @param options 可选配置(workspaceRoot:工作区根目录,默认从 getConfig 获取)
|
|
144
|
+
* @returns SafeGoalReport(脱敏后的报告对象)
|
|
145
|
+
*/
|
|
146
|
+
export function exportGoalReport(goalId, options) {
|
|
147
|
+
const workspaceRoot = resolveWorkspaceRoot(options?.workspaceRoot);
|
|
148
|
+
const goalStatus = readGoalStatus(goalId, workspaceRoot);
|
|
149
|
+
const summary = summarizeGoalProgress(goalId, workspaceRoot);
|
|
150
|
+
const generatedAt = new Date().toISOString();
|
|
151
|
+
const noSubgoals = goalStatus.subgoals.length === 0;
|
|
152
|
+
const hasIncomplete = goalStatus.subgoals.some((s) => s.status === "running" || s.status === "ready");
|
|
153
|
+
const incomplete = goalStatus.status === "active" && hasIncomplete;
|
|
154
|
+
// 构建 subgoal 报告条目
|
|
155
|
+
const subgoalEntries = goalStatus.subgoals.map((sg) => {
|
|
156
|
+
const entry = {
|
|
157
|
+
id: sg.id,
|
|
158
|
+
title: sg.title,
|
|
159
|
+
status: sg.status,
|
|
160
|
+
task_ids: [...sg.task_ids],
|
|
161
|
+
evidence_packs: collectEvidencePacks(sg.task_ids),
|
|
162
|
+
};
|
|
163
|
+
if (sg.accepted_at)
|
|
164
|
+
entry.accepted_at = sg.accepted_at;
|
|
165
|
+
if (sg.rejected_reason)
|
|
166
|
+
entry.rejected_reason = sg.rejected_reason;
|
|
167
|
+
return entry;
|
|
168
|
+
});
|
|
169
|
+
// 风险汇总
|
|
170
|
+
const risks = summary.risks.map((r) => r.subgoal_id + " (" + r.status + "): " + r.reason);
|
|
171
|
+
if (incomplete) {
|
|
172
|
+
risks.push("未完成:Goal 仍有 running/ready 子目标");
|
|
173
|
+
}
|
|
174
|
+
// 报告目录
|
|
175
|
+
const reportDir = resolve(workspaceRoot, ".patchwarden", "goals", goalId, "report");
|
|
176
|
+
mkdirSync(reportDir, { recursive: true });
|
|
177
|
+
const reportMdPath = join(reportDir, "REPORT.md");
|
|
178
|
+
const reportJsonPath = join(reportDir, "report.json");
|
|
179
|
+
// 构建原始报告对象
|
|
180
|
+
const rawReport = {
|
|
181
|
+
goal_id: goalStatus.goal_id,
|
|
182
|
+
generated_at: generatedAt,
|
|
183
|
+
path: reportDir,
|
|
184
|
+
completion_rate: noSubgoals ? 0 : parseCompletionRate(summary.completion_rate),
|
|
185
|
+
files: { report_md: reportMdPath, report_json: reportJsonPath },
|
|
186
|
+
summary,
|
|
187
|
+
subgoals: subgoalEntries,
|
|
188
|
+
risks,
|
|
189
|
+
timeline: {
|
|
190
|
+
created_at: goalStatus.created_at,
|
|
191
|
+
updated_at: goalStatus.updated_at,
|
|
192
|
+
status: goalStatus.status,
|
|
193
|
+
},
|
|
194
|
+
bounded: true,
|
|
195
|
+
};
|
|
196
|
+
// 脱敏所有字符串内容
|
|
197
|
+
const safeReport = redactSensitiveValue(rawReport).value;
|
|
198
|
+
// 原子写 JSON(机器可读)
|
|
199
|
+
atomicWriteFile(reportJsonPath, JSON.stringify(safeReport, null, 2) + "\n");
|
|
200
|
+
// 原子写 Markdown(人类可读,从脱敏后的对象生成)
|
|
201
|
+
const markdown = buildReportMarkdown(safeReport, { noSubgoals, incomplete });
|
|
202
|
+
atomicWriteFile(reportMdPath, markdown);
|
|
203
|
+
return safeReport;
|
|
204
|
+
}
|
|
@@ -19,6 +19,10 @@ export interface Subgoal {
|
|
|
19
19
|
task_ids: string[];
|
|
20
20
|
accepted_at?: string;
|
|
21
21
|
rejected_reason?: string;
|
|
22
|
+
/** Spec Kit task id 回溯(用于幂等去重) */
|
|
23
|
+
external_ref?: string;
|
|
24
|
+
/** Spec Kit task.files 提示(作用域文件路径列表) */
|
|
25
|
+
scope_hints?: string[];
|
|
22
26
|
}
|
|
23
27
|
export interface GoalStatus {
|
|
24
28
|
goal_id: string;
|
|
@@ -28,6 +32,8 @@ export interface GoalStatus {
|
|
|
28
32
|
created_at: string;
|
|
29
33
|
updated_at: string;
|
|
30
34
|
subgoals: Subgoal[];
|
|
35
|
+
/** Spec Kit 验收标准列表 */
|
|
36
|
+
acceptance_criteria?: string[];
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* 创建初始 GoalStatus,用于新 goal。
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.0: Spec Kit tasks 导入 — 将 Spec Kit 的 spec/tasks 结构转换为 Goal Session 子目标。
|
|
3
|
+
*
|
|
4
|
+
* - 解析 Spec Kit JSON(parseSpecKitJson)
|
|
5
|
+
* - 幂等导入:按 external_ref 去重,重复导入不产生重复 subgoal
|
|
6
|
+
* - 依赖解析:将 task.depends_on(external_ref 引用)解析为本地 subgoal id
|
|
7
|
+
* - 验收标准映射:input.acceptance → GoalStatus.acceptance_criteria
|
|
8
|
+
*
|
|
9
|
+
* 不引入第三方依赖,仅用 Node.js 内置模块。
|
|
10
|
+
*/
|
|
11
|
+
export interface SpecKitTask {
|
|
12
|
+
id: string;
|
|
13
|
+
desc: string;
|
|
14
|
+
files?: string[];
|
|
15
|
+
/** 引用其他 task 的 id(external_ref) */
|
|
16
|
+
depends_on?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface SpecKitInput {
|
|
19
|
+
spec: string;
|
|
20
|
+
tasks: SpecKitTask[];
|
|
21
|
+
acceptance?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface SpecKitImportResult {
|
|
24
|
+
goal_id: string;
|
|
25
|
+
spec_name: string;
|
|
26
|
+
created_count: number;
|
|
27
|
+
existing_count: number;
|
|
28
|
+
subgoal_ids: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 解析 Spec Kit JSON 文本并验证结构。
|
|
32
|
+
*
|
|
33
|
+
* 必须包含:
|
|
34
|
+
* - spec: 非空字符串
|
|
35
|
+
* - tasks: 数组,每个元素含 id(字符串)和 desc(字符串)
|
|
36
|
+
*
|
|
37
|
+
* 可选:
|
|
38
|
+
* - acceptance: 字符串数组
|
|
39
|
+
*
|
|
40
|
+
* 解析失败或结构不合法时抛出 PatchWardenError("invalid_spec_kit_json" | "invalid_spec_kit_input")。
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseSpecKitJson(jsonText: string): SpecKitInput;
|
|
43
|
+
/**
|
|
44
|
+
* 将 Spec Kit tasks 导入到指定 Goal Session,创建对应的 subgoal。
|
|
45
|
+
*
|
|
46
|
+
* 行为:
|
|
47
|
+
* - 验证 input(spec 非空、tasks 为数组)
|
|
48
|
+
* - 幂等去重:按 external_ref(task.id)匹配现有 subgoal,已存在则跳过
|
|
49
|
+
* - 依赖解析:将 task.depends_on(external_ref)解析为本地 subgoal id
|
|
50
|
+
* - 先创建无依赖的 subgoal,再处理有依赖的,支持多级依赖
|
|
51
|
+
* - 依赖的 task 在同一批次中:用刚创建的 subgoal_id
|
|
52
|
+
* - 依赖的 task 不存在:忽略该依赖并记录 warning(console.warn)
|
|
53
|
+
* - 验收标准:input.acceptance 非空时写入 GoalStatus.acceptance_criteria
|
|
54
|
+
* - 持久化:用 writeGoalStatus 原子写入
|
|
55
|
+
*
|
|
56
|
+
* @param goalId 目标 Goal 标识
|
|
57
|
+
* @param input Spec Kit 输入(spec + tasks + acceptance)
|
|
58
|
+
* @param options 可选配置:workspaceRoot 用于测试
|
|
59
|
+
* @returns SpecKitImportResult
|
|
60
|
+
*/
|
|
61
|
+
export declare function importSpecKitTasks(goalId: string, input: SpecKitInput, options?: {
|
|
62
|
+
workspaceRoot?: string;
|
|
63
|
+
}): SpecKitImportResult;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.0: Spec Kit tasks 导入 — 将 Spec Kit 的 spec/tasks 结构转换为 Goal Session 子目标。
|
|
3
|
+
*
|
|
4
|
+
* - 解析 Spec Kit JSON(parseSpecKitJson)
|
|
5
|
+
* - 幂等导入:按 external_ref 去重,重复导入不产生重复 subgoal
|
|
6
|
+
* - 依赖解析:将 task.depends_on(external_ref 引用)解析为本地 subgoal id
|
|
7
|
+
* - 验收标准映射:input.acceptance → GoalStatus.acceptance_criteria
|
|
8
|
+
*
|
|
9
|
+
* 不引入第三方依赖,仅用 Node.js 内置模块。
|
|
10
|
+
*/
|
|
11
|
+
import { PatchWardenError } from "../errors.js";
|
|
12
|
+
import { addSubgoal } from "./goalStatus.js";
|
|
13
|
+
import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
|
|
14
|
+
// ── JSON 解析 ─────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* 解析 Spec Kit JSON 文本并验证结构。
|
|
17
|
+
*
|
|
18
|
+
* 必须包含:
|
|
19
|
+
* - spec: 非空字符串
|
|
20
|
+
* - tasks: 数组,每个元素含 id(字符串)和 desc(字符串)
|
|
21
|
+
*
|
|
22
|
+
* 可选:
|
|
23
|
+
* - acceptance: 字符串数组
|
|
24
|
+
*
|
|
25
|
+
* 解析失败或结构不合法时抛出 PatchWardenError("invalid_spec_kit_json" | "invalid_spec_kit_input")。
|
|
26
|
+
*/
|
|
27
|
+
export function parseSpecKitJson(jsonText) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(jsonText);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new PatchWardenError("invalid_spec_kit_json", `Failed to parse Spec Kit JSON: ${e instanceof Error ? e.message : String(e)}`, "Ensure the input is valid JSON.", true, {});
|
|
34
|
+
}
|
|
35
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
36
|
+
throw new PatchWardenError("invalid_spec_kit_input", "Spec Kit JSON must be a JSON object", 'Ensure the top-level value is an object with "spec" and "tasks" fields.', true, { actual_type: Array.isArray(parsed) ? "array" : typeof parsed });
|
|
37
|
+
}
|
|
38
|
+
const obj = parsed;
|
|
39
|
+
if (typeof obj.spec !== "string" || obj.spec === "") {
|
|
40
|
+
throw new PatchWardenError("invalid_spec_kit_input", 'Spec Kit JSON must contain a non-empty "spec" string field', 'Add a "spec" field with a non-empty string value describing the specification name.', true, { spec: obj.spec });
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(obj.tasks)) {
|
|
43
|
+
throw new PatchWardenError("invalid_spec_kit_input", 'Spec Kit JSON must contain a "tasks" array field', 'Add a "tasks" field with an array of task objects (each with "id" and "desc").', true, { tasks_type: Array.isArray(obj.tasks) ? "array" : typeof obj.tasks });
|
|
44
|
+
}
|
|
45
|
+
const tasks = [];
|
|
46
|
+
for (let i = 0; i < obj.tasks.length; i++) {
|
|
47
|
+
const raw = obj.tasks[i];
|
|
48
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
49
|
+
throw new PatchWardenError("invalid_spec_kit_input", `Spec Kit task at index ${i} must be an object`, "Each task must be an object with at least \"id\" and \"desc\" string fields.", true, { task_index: i, actual_type: typeof raw });
|
|
50
|
+
}
|
|
51
|
+
const t = raw;
|
|
52
|
+
if (typeof t.id !== "string" || t.id === "") {
|
|
53
|
+
throw new PatchWardenError("invalid_spec_kit_input", `Spec Kit task at index ${i} must have a non-empty "id" string`, "Ensure each task has a unique non-empty string \"id\".", true, { task_index: i, id: t.id });
|
|
54
|
+
}
|
|
55
|
+
if (typeof t.desc !== "string") {
|
|
56
|
+
throw new PatchWardenError("invalid_spec_kit_input", `Spec Kit task at index ${i} (id="${t.id}") must have a "desc" string`, "Ensure each task has a \"desc\" string describing the task.", true, { task_index: i, id: t.id, desc_type: typeof t.desc });
|
|
57
|
+
}
|
|
58
|
+
const task = { id: t.id, desc: t.desc };
|
|
59
|
+
if (Array.isArray(t.files)) {
|
|
60
|
+
task.files = t.files.filter((f) => typeof f === "string");
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(t.depends_on)) {
|
|
63
|
+
task.depends_on = t.depends_on.filter((d) => typeof d === "string");
|
|
64
|
+
}
|
|
65
|
+
tasks.push(task);
|
|
66
|
+
}
|
|
67
|
+
const result = { spec: obj.spec, tasks };
|
|
68
|
+
if (Array.isArray(obj.acceptance)) {
|
|
69
|
+
result.acceptance = obj.acceptance.filter((a) => typeof a === "string");
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
// ── 导入实现 ──────────────────────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* 将 Spec Kit tasks 导入到指定 Goal Session,创建对应的 subgoal。
|
|
76
|
+
*
|
|
77
|
+
* 行为:
|
|
78
|
+
* - 验证 input(spec 非空、tasks 为数组)
|
|
79
|
+
* - 幂等去重:按 external_ref(task.id)匹配现有 subgoal,已存在则跳过
|
|
80
|
+
* - 依赖解析:将 task.depends_on(external_ref)解析为本地 subgoal id
|
|
81
|
+
* - 先创建无依赖的 subgoal,再处理有依赖的,支持多级依赖
|
|
82
|
+
* - 依赖的 task 在同一批次中:用刚创建的 subgoal_id
|
|
83
|
+
* - 依赖的 task 不存在:忽略该依赖并记录 warning(console.warn)
|
|
84
|
+
* - 验收标准:input.acceptance 非空时写入 GoalStatus.acceptance_criteria
|
|
85
|
+
* - 持久化:用 writeGoalStatus 原子写入
|
|
86
|
+
*
|
|
87
|
+
* @param goalId 目标 Goal 标识
|
|
88
|
+
* @param input Spec Kit 输入(spec + tasks + acceptance)
|
|
89
|
+
* @param options 可选配置:workspaceRoot 用于测试
|
|
90
|
+
* @returns SpecKitImportResult
|
|
91
|
+
*/
|
|
92
|
+
export function importSpecKitTasks(goalId, input, options) {
|
|
93
|
+
// ── 验证 ───────────────────────────────────────────────────
|
|
94
|
+
if (!input || typeof input !== "object") {
|
|
95
|
+
throw new PatchWardenError("invalid_spec_kit_input", "Spec Kit input must be an object", "Pass a SpecKitInput object with spec, tasks, and optional acceptance fields.", true, {});
|
|
96
|
+
}
|
|
97
|
+
if (typeof input.spec !== "string" || input.spec === "") {
|
|
98
|
+
throw new PatchWardenError("invalid_spec_kit_input", 'Spec Kit input must contain a non-empty "spec" string', "Ensure input.spec is a non-empty string.", true, { spec: input.spec });
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(input.tasks)) {
|
|
101
|
+
throw new PatchWardenError("invalid_spec_kit_input", 'Spec Kit input must contain a "tasks" array', "Ensure input.tasks is an array of SpecKitTask objects.", true, { tasks_type: Array.isArray(input.tasks) ? "array" : typeof input.tasks });
|
|
102
|
+
}
|
|
103
|
+
const workspaceRoot = options?.workspaceRoot;
|
|
104
|
+
// ── 读取现有 GoalStatus ────────────────────────────────────
|
|
105
|
+
let goalStatus = readGoalStatus(goalId, workspaceRoot);
|
|
106
|
+
// ── 构建 external_ref → subgoal_id 映射(现有 subgoal)─────
|
|
107
|
+
const refToSubgoalId = new Map();
|
|
108
|
+
for (const sg of goalStatus.subgoals) {
|
|
109
|
+
if (sg.external_ref) {
|
|
110
|
+
refToSubgoalId.set(sg.external_ref, sg.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ── 幂等去重:区分新 task 和已存在的 task ─────────────────
|
|
114
|
+
const newTasks = [];
|
|
115
|
+
let existingCount = 0;
|
|
116
|
+
for (const task of input.tasks) {
|
|
117
|
+
if (refToSubgoalId.has(task.id)) {
|
|
118
|
+
existingCount++;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
newTasks.push(task);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 新批次中所有 task id 集合(用于判断依赖是否在同批次)
|
|
125
|
+
const newTaskIds = new Set(newTasks.map((t) => t.id));
|
|
126
|
+
// ── 按依赖顺序创建 subgoal(拓扑排序,迭代多轮)────────────
|
|
127
|
+
const createdIds = [];
|
|
128
|
+
let remaining = [...newTasks];
|
|
129
|
+
// 辅助:创建单个 subgoal 并设置 external_ref / scope_hints
|
|
130
|
+
const createSubgoalForTask = (gs, task, resolvedDeps) => {
|
|
131
|
+
const { goalStatus: newGs, subgoalId } = addSubgoal(gs, task.desc, resolvedDeps);
|
|
132
|
+
// 新 subgoal 是数组最后一个元素,追加 external_ref 和 scope_hints
|
|
133
|
+
const subgoals = [...newGs.subgoals];
|
|
134
|
+
const lastIdx = subgoals.length - 1;
|
|
135
|
+
const updated = {
|
|
136
|
+
...subgoals[lastIdx],
|
|
137
|
+
external_ref: task.id,
|
|
138
|
+
};
|
|
139
|
+
if (task.files && task.files.length > 0) {
|
|
140
|
+
updated.scope_hints = [...task.files];
|
|
141
|
+
}
|
|
142
|
+
subgoals[lastIdx] = updated;
|
|
143
|
+
return { goalStatus: { ...newGs, subgoals }, subgoalId };
|
|
144
|
+
};
|
|
145
|
+
while (remaining.length > 0) {
|
|
146
|
+
const stillRemaining = [];
|
|
147
|
+
let progress = false;
|
|
148
|
+
// 正向遍历:保持输入顺序,先创建的 task 可被同批次后续 task 依赖
|
|
149
|
+
for (const task of remaining) {
|
|
150
|
+
const deps = task.depends_on ?? [];
|
|
151
|
+
const resolvedDeps = [];
|
|
152
|
+
let hasUnresolvedInBatch = false;
|
|
153
|
+
for (const dep of deps) {
|
|
154
|
+
if (refToSubgoalId.has(dep)) {
|
|
155
|
+
// 依赖已存在(已有 subgoal 或本批次已创建)
|
|
156
|
+
resolvedDeps.push(refToSubgoalId.get(dep));
|
|
157
|
+
}
|
|
158
|
+
else if (newTaskIds.has(dep)) {
|
|
159
|
+
// 依赖同批次中尚未创建的 task
|
|
160
|
+
hasUnresolvedInBatch = true;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// 依赖的 task 不存在任何地方:忽略并记录 warning
|
|
164
|
+
console.warn(`[specKitImport] Task "${task.id}" depends on non-existent task "${dep}"; ignoring this dependency.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!hasUnresolvedInBatch) {
|
|
168
|
+
const { goalStatus: newGs, subgoalId } = createSubgoalForTask(goalStatus, task, resolvedDeps);
|
|
169
|
+
goalStatus = newGs;
|
|
170
|
+
refToSubgoalId.set(task.id, subgoalId);
|
|
171
|
+
createdIds.push(subgoalId);
|
|
172
|
+
progress = true;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
stillRemaining.push(task);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
remaining = stillRemaining;
|
|
179
|
+
if (!progress && remaining.length > 0) {
|
|
180
|
+
// 检测到循环依赖:强制创建剩余 task,忽略未解析的同批次依赖
|
|
181
|
+
for (const task of remaining) {
|
|
182
|
+
const deps = task.depends_on ?? [];
|
|
183
|
+
const resolvedDeps = [];
|
|
184
|
+
for (const dep of deps) {
|
|
185
|
+
if (refToSubgoalId.has(dep)) {
|
|
186
|
+
resolvedDeps.push(refToSubgoalId.get(dep));
|
|
187
|
+
}
|
|
188
|
+
else if (newTaskIds.has(dep)) {
|
|
189
|
+
console.warn(`[specKitImport] Circular dependency detected: task "${task.id}" depends on "${dep}" which could not be resolved; ignoring this dependency.`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.warn(`[specKitImport] Task "${task.id}" depends on non-existent task "${dep}"; ignoring this dependency.`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const { goalStatus: newGs, subgoalId } = createSubgoalForTask(goalStatus, task, resolvedDeps);
|
|
196
|
+
goalStatus = newGs;
|
|
197
|
+
refToSubgoalId.set(task.id, subgoalId);
|
|
198
|
+
createdIds.push(subgoalId);
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── 验收标准 ───────────────────────────────────────────────
|
|
204
|
+
if (input.acceptance && input.acceptance.length > 0) {
|
|
205
|
+
goalStatus = {
|
|
206
|
+
...goalStatus,
|
|
207
|
+
acceptance_criteria: [...input.acceptance],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// ── 持久化(原子写)────────────────────────────────────────
|
|
211
|
+
writeGoalStatus(goalId, goalStatus, workspaceRoot);
|
|
212
|
+
// ── 返回结果 ───────────────────────────────────────────────
|
|
213
|
+
return {
|
|
214
|
+
goal_id: goalId,
|
|
215
|
+
spec_name: input.spec,
|
|
216
|
+
created_count: createdIds.length,
|
|
217
|
+
existing_count: existingCount,
|
|
218
|
+
subgoal_ids: createdIds,
|
|
219
|
+
};
|
|
220
|
+
}
|
package/dist/goal/subgoalSync.js
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
|
|
12
12
|
import { updateSubgoalStatus } from "./goalStatus.js";
|
|
13
|
+
import { logger } from "../logging.js";
|
|
13
14
|
// ── 同步函数 ──────────────────────────────────────────────────────
|
|
14
15
|
/**
|
|
15
16
|
* v0.8.0: 当任务状态变为 done_by_agent 时,同步更新关联的 subgoal 状态。
|
|
@@ -42,7 +43,7 @@ export function syncSubgoalOnTaskDone(taskId, taskMeta, workspaceRoot) {
|
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
45
|
// subgoal 同步失败不应影响任务完成流程,只记录到 stderr
|
|
45
|
-
|
|
46
|
+
logger.error(`[goal] syncSubgoalOnTaskDone failed for task ${taskId}`, { error: err instanceof Error ? err.message : String(err) });
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
// ── Task 元信息读取 ───────────────────────────────────────────────
|
package/dist/httpServer.js
CHANGED
|
@@ -24,24 +24,25 @@ import { registerTools } from "./tools/registry.js";
|
|
|
24
24
|
import { healthCheck } from "./tools/healthCheck.js";
|
|
25
25
|
import { getToolCatalogSnapshot } from "./tools/registry.js";
|
|
26
26
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
27
|
+
import { logger } from "./logging.js";
|
|
27
28
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
28
29
|
const config = loadConfig();
|
|
29
30
|
const port = parseInt(process.env.PATCHWARDEN_HTTP_PORT || "") ||
|
|
30
31
|
config.httpPort ||
|
|
31
32
|
7331;
|
|
32
33
|
const host = "127.0.0.1";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
logger.info(`[patchwarden-http] Workspace: ${config.workspaceRoot}`);
|
|
35
|
+
logger.info(`[patchwarden-http] Listening: http://${host}:${port}/mcp`);
|
|
36
|
+
logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed to network`);
|
|
36
37
|
// ── Owner token (optional) ────────────────────────────────────────
|
|
37
38
|
const httpCfg = config.http || {};
|
|
38
39
|
const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
|
|
39
40
|
const ownerToken = process.env[ownerTokenEnv] || "";
|
|
40
41
|
if (ownerToken) {
|
|
41
|
-
|
|
42
|
+
logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
44
|
-
|
|
45
|
+
logger.info(`[patchwarden-http] ⚠️ No owner token set — all local requests accepted`);
|
|
45
46
|
}
|
|
46
47
|
function checkOwnerToken(req) {
|
|
47
48
|
if (!ownerToken)
|
|
@@ -123,7 +124,9 @@ async function handleMcpRequest(req, res) {
|
|
|
123
124
|
await transport.handleRequest(req, res);
|
|
124
125
|
}
|
|
125
126
|
catch (err) {
|
|
126
|
-
|
|
127
|
+
logger.error("[patchwarden-http] Request error", {
|
|
128
|
+
error: err instanceof Error ? err.message : String(err),
|
|
129
|
+
});
|
|
127
130
|
if (!res.headersSent) {
|
|
128
131
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
129
132
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
@@ -233,21 +236,21 @@ const httpServer = createServer(async (req, res) => {
|
|
|
233
236
|
// ── Start ─────────────────────────────────────────────────────────
|
|
234
237
|
httpServer.on("error", (err) => {
|
|
235
238
|
if (err.code === "EADDRINUSE") {
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
logger.fatal(`[patchwarden-http] Fatal: port ${port} is already in use on ${host}.`);
|
|
240
|
+
logger.fatal("[patchwarden-http] Stop the other PatchWarden HTTP instance or change httpPort in patchwarden.config.json.");
|
|
238
241
|
}
|
|
239
242
|
else {
|
|
240
|
-
|
|
243
|
+
logger.fatal(`[patchwarden-http] Fatal: ${err.message}`);
|
|
241
244
|
}
|
|
242
245
|
process.exit(1);
|
|
243
246
|
});
|
|
244
247
|
httpServer.listen(port, host, () => {
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
logger.info(`[patchwarden-http] ✅ Ready`);
|
|
249
|
+
logger.info(`[patchwarden-http] Admin: http://${host}:${port}/admin/tasks/:id/accept`);
|
|
247
250
|
});
|
|
248
251
|
// Graceful shutdown
|
|
249
252
|
process.on("SIGINT", () => {
|
|
250
|
-
|
|
253
|
+
logger.info("[patchwarden-http] Shutting down...");
|
|
251
254
|
httpServer.close(() => process.exit(0));
|
|
252
255
|
});
|
|
253
256
|
process.on("SIGTERM", () => {
|