patchwarden 1.5.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.
Files changed (217) hide show
  1. package/README.en.md +43 -2
  2. package/README.md +43 -2
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2197
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/doctor.d.ts +18 -1
  44. package/dist/doctor.js +579 -348
  45. package/dist/goal/goalReport.d.ts +54 -0
  46. package/dist/goal/goalReport.js +204 -0
  47. package/dist/goal/goalStatus.d.ts +6 -0
  48. package/dist/goal/specKitImport.d.ts +63 -0
  49. package/dist/goal/specKitImport.js +220 -0
  50. package/dist/goal/subgoalSync.js +2 -1
  51. package/dist/httpServer.js +15 -12
  52. package/dist/index.js +7 -4
  53. package/dist/logging.d.ts +7 -1
  54. package/dist/logging.js +8 -0
  55. package/dist/runner/changeCapture.d.ts +3 -3
  56. package/dist/runner/changeCapture.js +63 -39
  57. package/dist/runner/cli.js +7 -6
  58. package/dist/runner/postTaskCleanup.js +26 -2
  59. package/dist/runner/runTask.js +245 -221
  60. package/dist/runner/simpleProcess.js +4 -4
  61. package/dist/runner/watch.js +17 -14
  62. package/dist/security/contentRedaction.d.ts +6 -0
  63. package/dist/security/contentRedaction.js +22 -0
  64. package/dist/smoke-test.js +257 -251
  65. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  66. package/dist/test/unit/apply-patch.test.js +225 -0
  67. package/dist/test/unit/create-task.test.d.ts +1 -0
  68. package/dist/test/unit/create-task.test.js +197 -0
  69. package/dist/test/unit/direct-guards.test.js +124 -9
  70. package/dist/test/unit/evidence-pack.test.js +95 -1
  71. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  72. package/dist/test/unit/get-task-status.test.js +174 -0
  73. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  74. package/dist/test/unit/get-task-summary.test.js +146 -0
  75. package/dist/test/unit/goal-report.test.d.ts +1 -0
  76. package/dist/test/unit/goal-report.test.js +159 -0
  77. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  78. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  79. package/dist/test/unit/path-guard.test.js +24 -0
  80. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  81. package/dist/test/unit/spec-kit-import.test.js +341 -0
  82. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  83. package/dist/test/unit/wait-for-task.test.js +144 -0
  84. package/dist/tools/auditTask.d.ts +8 -63
  85. package/dist/tools/auditTask.js +12 -8
  86. package/dist/tools/createDirectSession.d.ts +1 -1
  87. package/dist/tools/createDirectSession.js +2 -2
  88. package/dist/tools/createTask.d.ts +2 -2
  89. package/dist/tools/createTask.js +4 -4
  90. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  91. package/dist/tools/dispatch/coreDispatch.js +282 -0
  92. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  93. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  94. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  95. package/dist/tools/dispatch/directDispatch.js +115 -0
  96. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  97. package/dist/tools/dispatch/goalDispatch.js +91 -0
  98. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  99. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  100. package/dist/tools/dispatch/types.d.ts +23 -0
  101. package/dist/tools/dispatch/types.js +15 -0
  102. package/dist/tools/evidencePack.d.ts +6 -0
  103. package/dist/tools/evidencePack.js +213 -6
  104. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  105. package/dist/tools/finalizeDirectSession.js +3 -3
  106. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  107. package/dist/tools/goalSubgoalTask.js +2 -2
  108. package/dist/tools/healthCheck.js +3 -3
  109. package/dist/tools/registry.d.ts +3 -3
  110. package/dist/tools/registry.js +60 -503
  111. package/dist/tools/retryTask.d.ts +2 -2
  112. package/dist/tools/retryTask.js +2 -2
  113. package/dist/tools/runTaskLoop.js +4 -4
  114. package/dist/tools/safeViews.d.ts +2 -2
  115. package/dist/tools/safeViews.js +2 -2
  116. package/dist/tools/toolRegistry.js +22 -0
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/docs/CODE_WIKI.md +983 -0
  120. package/docs/agentseal-integration.md +150 -0
  121. package/docs/architecture.md +63 -0
  122. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  123. package/docs/dashboard-overview.md +86 -0
  124. package/docs/demo.md +8 -0
  125. package/docs/direct-session-workflow.md +98 -0
  126. package/docs/evidence-pack-schema.md +215 -0
  127. package/docs/execution-plan-2026-07-09.md +315 -0
  128. package/docs/lineage-evidence-pack-workflow.md +127 -0
  129. package/docs/mcp-inspector-testing.md +200 -0
  130. package/docs/open-source-application.md +162 -0
  131. package/docs/opencode-worker.md +151 -0
  132. package/docs/openhands-worker.md +181 -0
  133. package/docs/release-evidence.md +72 -0
  134. package/docs/roadmap-execution-and-acceptance.md +365 -0
  135. package/docs/spec-kit-integration.md +131 -0
  136. package/docs/task-safe-review-workflow.md +98 -0
  137. package/docs/threat-model.md +79 -0
  138. package/docs/user-feedback.md +40 -0
  139. package/docs/why-patchwarden.md +110 -0
  140. package/package.json +2 -2
  141. package/scripts/checks/control-center-smoke.js +356 -0
  142. package/scripts/checks/lifecycle-smoke.js +29 -23
  143. package/scripts/checks/mcp-smoke.js +3 -1
  144. package/scripts/generate-demo-gif.py +320 -0
  145. package/src/assessments/agentAssessor.ts +3 -3
  146. package/src/assessments/assessmentStore.ts +2 -2
  147. package/src/assessments/confirmCli.ts +5 -4
  148. package/src/config.ts +37 -0
  149. package/src/control/middleware/auth.ts +17 -0
  150. package/src/control/middleware/static.ts +71 -0
  151. package/src/control/routes/audit.ts +321 -0
  152. package/src/control/routes/evidence.ts +107 -0
  153. package/src/control/routes/lineage.ts +92 -0
  154. package/src/control/routes/policy.ts +81 -0
  155. package/src/control/routes/process.ts +204 -0
  156. package/src/control/routes/sessions.ts +251 -0
  157. package/src/control/routes/status.ts +325 -0
  158. package/src/control/routes/taskActions.ts +248 -0
  159. package/src/control/routes/tasks.ts +323 -0
  160. package/src/control/routes/workspace.ts +203 -0
  161. package/src/control/runtime.ts +472 -0
  162. package/src/control/server.ts +471 -0
  163. package/src/control/shared.ts +294 -0
  164. package/src/controlCenter.ts +7 -2347
  165. package/src/direct/directGuards.ts +28 -7
  166. package/src/doctor.ts +741 -481
  167. package/src/goal/goalReport.ts +271 -0
  168. package/src/goal/goalStatus.ts +6 -0
  169. package/src/goal/specKitImport.ts +355 -0
  170. package/src/goal/subgoalSync.ts +4 -2
  171. package/src/httpServer.ts +17 -14
  172. package/src/index.ts +7 -4
  173. package/src/logging.ts +10 -1
  174. package/src/runner/changeCapture.ts +70 -42
  175. package/src/runner/cli.ts +7 -6
  176. package/src/runner/postTaskCleanup.ts +26 -2
  177. package/src/runner/runTask.ts +325 -223
  178. package/src/runner/simpleProcess.ts +4 -4
  179. package/src/runner/watch.ts +17 -14
  180. package/src/security/contentRedaction.ts +29 -0
  181. package/src/smoke-test.ts +252 -250
  182. package/src/test/unit/apply-patch.test.ts +293 -0
  183. package/src/test/unit/create-task.test.ts +255 -0
  184. package/src/test/unit/direct-guards.test.ts +178 -8
  185. package/src/test/unit/evidence-pack.test.ts +110 -1
  186. package/src/test/unit/get-task-status.test.ts +203 -0
  187. package/src/test/unit/get-task-summary.test.ts +173 -0
  188. package/src/test/unit/goal-report.test.ts +189 -0
  189. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  190. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  191. package/src/test/unit/path-guard.test.ts +30 -0
  192. package/src/test/unit/spec-kit-import.test.ts +429 -0
  193. package/src/test/unit/wait-for-task.test.ts +176 -0
  194. package/src/tools/auditTask.ts +99 -59
  195. package/src/tools/createDirectSession.ts +3 -3
  196. package/src/tools/createTask.ts +7 -7
  197. package/src/tools/dispatch/coreDispatch.ts +374 -0
  198. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  199. package/src/tools/dispatch/directDispatch.ts +167 -0
  200. package/src/tools/dispatch/goalDispatch.ts +127 -0
  201. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  202. package/src/tools/dispatch/types.ts +24 -0
  203. package/src/tools/evidencePack.ts +291 -6
  204. package/src/tools/finalizeDirectSession.ts +4 -4
  205. package/src/tools/goalSubgoalTask.ts +2 -2
  206. package/src/tools/healthCheck.ts +3 -3
  207. package/src/tools/registry.ts +68 -628
  208. package/src/tools/retryTask.ts +2 -2
  209. package/src/tools/runTaskLoop.ts +4 -4
  210. package/src/tools/safeViews.ts +2 -2
  211. package/src/tools/toolRegistry.ts +22 -0
  212. package/src/version.ts +1 -1
  213. package/ui/pages/audit.html +192 -3
  214. package/ui/pages/dashboard.html +959 -44
  215. package/ui/pages/direct-sessions.html +505 -53
  216. package/ui/pages/task-detail.html +456 -438
  217. package/ui/pages/tasks.html +598 -61
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { getToolDefs, handleToolCall } from "../../tools/registry.js";
7
7
  import { reloadConfig } from "../../config.js";
8
8
  import { PatchWardenError } from "../../errors.js";
9
- // ── 9 个 Goal Session MCP 工具名 ──────────────────────────────────
9
+ // ── 11 个 Goal Session MCP 工具名 ─────────────────────────────────
10
10
  const GOAL_TOOL_NAMES = [
11
11
  "create_goal",
12
12
  "list_goals",
@@ -17,6 +17,8 @@ const GOAL_TOOL_NAMES = [
17
17
  "suggest_next_subgoal",
18
18
  "summarize_goal_progress",
19
19
  "export_handoff",
20
+ "export_goal_report",
21
+ "import_speckit_tasks",
20
22
  ];
21
23
  // ── 测试环境隔离:使用临时 workspaceRoot + full profile ───────────
22
24
  let tempDir;
@@ -56,21 +58,21 @@ afterEach(() => {
56
58
  // ── Tests ─────────────────────────────────────────────────────────
57
59
  describe("goal tools registry", () => {
58
60
  describe("getToolDefs — 工具定义", () => {
59
- it("包含全部 9 个 goal 工具", () => {
61
+ it("包含全部 11 个 goal 工具", () => {
60
62
  const tools = getToolDefs();
61
63
  const names = new Set(tools.map((t) => t.name));
62
64
  for (const name of GOAL_TOOL_NAMES) {
63
65
  assert.ok(names.has(name), `Missing tool definition: ${name}`);
64
66
  }
65
67
  });
66
- it("每个 goal 工具描述以 v0.8.0 开头且 inputSchema.type 为 object", () => {
68
+ it("每个 goal 工具描述非空且 inputSchema.type 为 object", () => {
67
69
  const tools = getToolDefs();
68
70
  const byName = new Map(tools.map((t) => [t.name, t]));
69
71
  for (const name of GOAL_TOOL_NAMES) {
70
72
  const tool = byName.get(name);
71
73
  assert.ok(tool, `Tool not found: ${name}`);
72
74
  assert.equal(tool.inputSchema.type, "object", `${name} schema type should be object`);
73
- assert.ok(tool.description.startsWith("v0.8.0:"), `${name} description should start with v0.8.0:`);
75
+ assert.ok(typeof tool.description === "string" && tool.description.length > 0, `${name} description should be a non-empty string`);
74
76
  }
75
77
  });
76
78
  it("create_goal required 字段为 title/goal_description/repo_path", () => {
@@ -69,6 +69,30 @@ describe("guardPath", () => {
69
69
  rmSync(target, { recursive: true, force: true });
70
70
  }
71
71
  });
72
+ it("rejects Windows junction escape", { skip: process.platform !== "win32" ? "Windows-only test" : undefined }, () => {
73
+ const target = mkdtempSync(join(tmpdir(), "pw-junction-target-"));
74
+ try {
75
+ const linkPath = join(tempDir, "escape-junction");
76
+ symlinkSync(target, linkPath, "junction");
77
+ assert.throws(() => guardPath("escape-junction/secret.txt", tempDir), PatchWardenError);
78
+ }
79
+ finally {
80
+ rmSync(target, { recursive: true, force: true });
81
+ }
82
+ });
83
+ it("rejects intermediate symlink escape (TOCTOU defense)", { skip: process.platform === "win32" ? "Windows symlink permissions unstable" : undefined }, () => {
84
+ const target = mkdtempSync(join(tmpdir(), "pw-toctou-target-"));
85
+ try {
86
+ const linkPath = join(tempDir, "escape-link");
87
+ symlinkSync(target, linkPath);
88
+ mkdirSync(join(target, "subdir"), { recursive: true });
89
+ writeFileSync(join(target, "subdir", "secret.txt"), "secret", "utf-8");
90
+ assert.throws(() => guardPath("escape-link/subdir/secret.txt", tempDir), PatchWardenError);
91
+ }
92
+ finally {
93
+ rmSync(target, { recursive: true, force: true });
94
+ }
95
+ });
72
96
  });
73
97
  describe("guardWorkspacePath", () => {
74
98
  let tempDir;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,341 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createGoal, readGoalStatus } from "../../goal/goalStore.js";
7
+ import { importSpecKitTasks, parseSpecKitJson, } from "../../goal/specKitImport.js";
8
+ import { PatchWardenError } from "../../errors.js";
9
+ // ── Helpers ───────────────────────────────────────────────────────
10
+ let tempDir;
11
+ let goalId;
12
+ beforeEach(() => {
13
+ tempDir = mkdtempSync(join(tmpdir(), "pw-speckit-"));
14
+ const result = createGoal("repo", "Spec Kit Test Goal", "desc", tempDir);
15
+ goalId = result.goal_id;
16
+ });
17
+ afterEach(() => {
18
+ rmSync(tempDir, { recursive: true, force: true });
19
+ });
20
+ function makeStandardInput() {
21
+ return {
22
+ spec: "feature-auth",
23
+ tasks: [
24
+ {
25
+ id: "T1",
26
+ desc: "Set up database schema",
27
+ files: ["src/db/schema.ts"],
28
+ },
29
+ {
30
+ id: "T2",
31
+ desc: "Implement auth service",
32
+ files: ["src/auth/service.ts", "src/auth/types.ts"],
33
+ },
34
+ {
35
+ id: "T3",
36
+ desc: "Write integration tests",
37
+ files: ["test/auth.test.ts"],
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ // ── Tests ─────────────────────────────────────────────────────────
43
+ describe("specKitImport", () => {
44
+ describe("parseSpecKitJson", () => {
45
+ it("解析合法 JSON 返回 SpecKitInput", () => {
46
+ const json = JSON.stringify({
47
+ spec: "my-spec",
48
+ tasks: [
49
+ { id: "T1", desc: "Task one", files: ["a.ts"] },
50
+ { id: "T2", desc: "Task two", depends_on: ["T1"] },
51
+ ],
52
+ acceptance: ["T1 passes", "T2 passes"],
53
+ });
54
+ const input = parseSpecKitJson(json);
55
+ assert.equal(input.spec, "my-spec");
56
+ assert.equal(input.tasks.length, 2);
57
+ assert.equal(input.tasks[0].id, "T1");
58
+ assert.equal(input.tasks[0].desc, "Task one");
59
+ assert.deepEqual(input.tasks[0].files, ["a.ts"]);
60
+ assert.deepEqual(input.tasks[1].depends_on, ["T1"]);
61
+ assert.deepEqual(input.acceptance, ["T1 passes", "T2 passes"]);
62
+ });
63
+ it("缺 spec 字段抛出错误", () => {
64
+ const json = JSON.stringify({ tasks: [] });
65
+ assert.throws(() => parseSpecKitJson(json), (err) => {
66
+ assert.ok(err instanceof PatchWardenError);
67
+ assert.equal(err.reason, "invalid_spec_kit_input");
68
+ return true;
69
+ });
70
+ });
71
+ it("spec 为空字符串抛出错误", () => {
72
+ const json = JSON.stringify({ spec: "", tasks: [] });
73
+ assert.throws(() => parseSpecKitJson(json), (err) => {
74
+ assert.ok(err instanceof PatchWardenError);
75
+ assert.equal(err.reason, "invalid_spec_kit_input");
76
+ return true;
77
+ });
78
+ });
79
+ it("缺 tasks 字段抛出错误", () => {
80
+ const json = JSON.stringify({ spec: "my-spec" });
81
+ assert.throws(() => parseSpecKitJson(json), (err) => {
82
+ assert.ok(err instanceof PatchWardenError);
83
+ assert.equal(err.reason, "invalid_spec_kit_input");
84
+ return true;
85
+ });
86
+ });
87
+ it("tasks 不是数组抛出错误", () => {
88
+ const json = JSON.stringify({ spec: "my-spec", tasks: "not-an-array" });
89
+ assert.throws(() => parseSpecKitJson(json), (err) => {
90
+ assert.ok(err instanceof PatchWardenError);
91
+ assert.equal(err.reason, "invalid_spec_kit_input");
92
+ return true;
93
+ });
94
+ });
95
+ it("task 缺 id 抛出错误", () => {
96
+ const json = JSON.stringify({
97
+ spec: "my-spec",
98
+ tasks: [{ desc: "no id" }],
99
+ });
100
+ assert.throws(() => parseSpecKitJson(json), (err) => {
101
+ assert.ok(err instanceof PatchWardenError);
102
+ assert.equal(err.reason, "invalid_spec_kit_input");
103
+ return true;
104
+ });
105
+ });
106
+ it("无效 JSON 文本抛出错误", () => {
107
+ assert.throws(() => parseSpecKitJson("{ not valid json"), (err) => {
108
+ assert.ok(err instanceof PatchWardenError);
109
+ assert.equal(err.reason, "invalid_spec_kit_json");
110
+ return true;
111
+ });
112
+ });
113
+ });
114
+ describe("importSpecKitTasks — 标准导入", () => {
115
+ it("3 个无依赖 task → created_count=3, existing_count=0", () => {
116
+ const input = makeStandardInput();
117
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
118
+ assert.equal(result.created_count, 3);
119
+ assert.equal(result.existing_count, 0);
120
+ assert.equal(result.subgoal_ids.length, 3);
121
+ assert.equal(result.spec_name, "feature-auth");
122
+ assert.equal(result.goal_id, goalId);
123
+ });
124
+ it("每个 subgoal 的 title = task.desc", () => {
125
+ const input = makeStandardInput();
126
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
127
+ const status = readGoalStatus(goalId, tempDir);
128
+ assert.equal(status.subgoals.length, 3);
129
+ assert.equal(status.subgoals[0].title, "Set up database schema");
130
+ assert.equal(status.subgoals[1].title, "Implement auth service");
131
+ assert.equal(status.subgoals[2].title, "Write integration tests");
132
+ });
133
+ it("每个 subgoal 的 external_ref = task.id", () => {
134
+ const input = makeStandardInput();
135
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
136
+ const status = readGoalStatus(goalId, tempDir);
137
+ assert.equal(status.subgoals[0].external_ref, "T1");
138
+ assert.equal(status.subgoals[1].external_ref, "T2");
139
+ assert.equal(status.subgoals[2].external_ref, "T3");
140
+ });
141
+ it("task.files 映射到 scope_hints", () => {
142
+ const input = makeStandardInput();
143
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
144
+ const status = readGoalStatus(goalId, tempDir);
145
+ assert.deepEqual(status.subgoals[0].scope_hints, ["src/db/schema.ts"]);
146
+ assert.deepEqual(status.subgoals[1].scope_hints, [
147
+ "src/auth/service.ts",
148
+ "src/auth/types.ts",
149
+ ]);
150
+ assert.deepEqual(status.subgoals[2].scope_hints, ["test/auth.test.ts"]);
151
+ });
152
+ it("subgoal_ids 对应新创建的 subgoal id", () => {
153
+ const input = makeStandardInput();
154
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
155
+ assert.deepEqual(result.subgoal_ids, ["subgoal-001", "subgoal-002", "subgoal-003"]);
156
+ });
157
+ });
158
+ describe("importSpecKitTasks — 依赖关系导入", () => {
159
+ it("T2 depends_on T1 → T2 的 depends_on 含 T1 的 subgoal_id(非 external_ref)", () => {
160
+ const input = {
161
+ spec: "feature-deps",
162
+ tasks: [
163
+ { id: "T1", desc: "Foundation task" },
164
+ { id: "T2", desc: "Dependent task", depends_on: ["T1"] },
165
+ ],
166
+ };
167
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
168
+ assert.equal(result.created_count, 2);
169
+ const status = readGoalStatus(goalId, tempDir);
170
+ assert.equal(status.subgoals.length, 2);
171
+ const t1Subgoal = status.subgoals[0];
172
+ const t2Subgoal = status.subgoals[1];
173
+ assert.equal(t1Subgoal.external_ref, "T1");
174
+ assert.equal(t2Subgoal.external_ref, "T2");
175
+ // T2 depends_on 应包含 T1 的 subgoal_id,而非 external_ref
176
+ assert.ok(t2Subgoal.depends_on.includes(t1Subgoal.id));
177
+ assert.ok(!t2Subgoal.depends_on.includes("T1"));
178
+ });
179
+ it("多级依赖 T3 → T2 → T1 全部正确解析", () => {
180
+ const input = {
181
+ spec: "multi-level",
182
+ tasks: [
183
+ { id: "T1", desc: "Level 1" },
184
+ { id: "T2", desc: "Level 2", depends_on: ["T1"] },
185
+ { id: "T3", desc: "Level 3", depends_on: ["T2"] },
186
+ ],
187
+ };
188
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
189
+ const status = readGoalStatus(goalId, tempDir);
190
+ assert.equal(status.subgoals.length, 3);
191
+ assert.deepEqual(status.subgoals[0].depends_on, []);
192
+ assert.deepEqual(status.subgoals[1].depends_on, ["subgoal-001"]);
193
+ assert.deepEqual(status.subgoals[2].depends_on, ["subgoal-002"]);
194
+ });
195
+ it("依赖不存在的 task → 忽略该依赖并继续创建", () => {
196
+ const input = {
197
+ spec: "missing-dep",
198
+ tasks: [
199
+ { id: "T1", desc: "Task with missing dep", depends_on: ["NONEXISTENT"] },
200
+ ],
201
+ };
202
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
203
+ assert.equal(result.created_count, 1);
204
+ const status = readGoalStatus(goalId, tempDir);
205
+ assert.equal(status.subgoals.length, 1);
206
+ assert.deepEqual(status.subgoals[0].depends_on, []);
207
+ });
208
+ });
209
+ describe("importSpecKitTasks — 验收标准映射", () => {
210
+ it("input.acceptance 写入 GoalStatus.acceptance_criteria", () => {
211
+ const input = {
212
+ spec: "feature-acceptance",
213
+ tasks: [{ id: "T1", desc: "Task one" }],
214
+ acceptance: ["T1 passes", "T2 passes"],
215
+ };
216
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
217
+ const status = readGoalStatus(goalId, tempDir);
218
+ assert.deepEqual(status.acceptance_criteria, ["T1 passes", "T2 passes"]);
219
+ });
220
+ it("无 acceptance → GoalStatus.acceptance_criteria 不存在", () => {
221
+ const input = {
222
+ spec: "no-acceptance",
223
+ tasks: [{ id: "T1", desc: "Task one" }],
224
+ };
225
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
226
+ const status = readGoalStatus(goalId, tempDir);
227
+ assert.equal(status.acceptance_criteria, undefined);
228
+ });
229
+ it("空 acceptance 数组 → GoalStatus.acceptance_criteria 不存在", () => {
230
+ const input = {
231
+ spec: "empty-acceptance",
232
+ tasks: [{ id: "T1", desc: "Task one" }],
233
+ acceptance: [],
234
+ };
235
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
236
+ const status = readGoalStatus(goalId, tempDir);
237
+ assert.equal(status.acceptance_criteria, undefined);
238
+ });
239
+ });
240
+ describe("importSpecKitTasks — 幂等去重", () => {
241
+ it("重复导入相同 JSON → 第二次 created_count=0, existing_count=3", () => {
242
+ const input = makeStandardInput();
243
+ // 第一次导入
244
+ const result1 = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
245
+ assert.equal(result1.created_count, 3);
246
+ assert.equal(result1.existing_count, 0);
247
+ // 第二次导入相同 JSON
248
+ const result2 = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
249
+ assert.equal(result2.created_count, 0);
250
+ assert.equal(result2.existing_count, 3);
251
+ assert.deepEqual(result2.subgoal_ids, []);
252
+ });
253
+ it("重复导入后 subgoal 总数仍为 3", () => {
254
+ const input = makeStandardInput();
255
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
256
+ importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
257
+ const status = readGoalStatus(goalId, tempDir);
258
+ assert.equal(status.subgoals.length, 3);
259
+ });
260
+ it("部分新 task 导入:只创建新增的", () => {
261
+ // 第一次导入 T1, T2
262
+ const input1 = {
263
+ spec: "partial",
264
+ tasks: [
265
+ { id: "T1", desc: "Task one" },
266
+ { id: "T2", desc: "Task two" },
267
+ ],
268
+ };
269
+ const result1 = importSpecKitTasks(goalId, input1, { workspaceRoot: tempDir });
270
+ assert.equal(result1.created_count, 2);
271
+ // 第二次导入 T1, T2, T3(T1/T2 已存在)
272
+ const input2 = {
273
+ spec: "partial",
274
+ tasks: [
275
+ { id: "T1", desc: "Task one" },
276
+ { id: "T2", desc: "Task two" },
277
+ { id: "T3", desc: "Task three" },
278
+ ],
279
+ };
280
+ const result2 = importSpecKitTasks(goalId, input2, { workspaceRoot: tempDir });
281
+ assert.equal(result2.created_count, 1);
282
+ assert.equal(result2.existing_count, 2);
283
+ const status = readGoalStatus(goalId, tempDir);
284
+ assert.equal(status.subgoals.length, 3);
285
+ });
286
+ });
287
+ describe("importSpecKitTasks — 无效输入", () => {
288
+ it("spec 为空 → 抛出错误且不创建 subgoal", () => {
289
+ const input = { spec: "", tasks: [] };
290
+ assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
291
+ assert.ok(err instanceof PatchWardenError);
292
+ assert.equal(err.reason, "invalid_spec_kit_input");
293
+ return true;
294
+ });
295
+ const status = readGoalStatus(goalId, tempDir);
296
+ assert.equal(status.subgoals.length, 0);
297
+ });
298
+ it("tasks 不是数组 → 抛出错误且不创建 subgoal", () => {
299
+ const input = { spec: "test", tasks: "not-array" };
300
+ assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
301
+ assert.ok(err instanceof PatchWardenError);
302
+ assert.equal(err.reason, "invalid_spec_kit_input");
303
+ return true;
304
+ });
305
+ const status = readGoalStatus(goalId, tempDir);
306
+ assert.equal(status.subgoals.length, 0);
307
+ });
308
+ it("tasks 缺失 → 抛出错误且不创建 subgoal", () => {
309
+ const input = { spec: "test" };
310
+ assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
311
+ assert.ok(err instanceof PatchWardenError);
312
+ assert.equal(err.reason, "invalid_spec_kit_input");
313
+ return true;
314
+ });
315
+ const status = readGoalStatus(goalId, tempDir);
316
+ assert.equal(status.subgoals.length, 0);
317
+ });
318
+ });
319
+ describe("importSpecKitTasks — JSON 文本端到端", () => {
320
+ it("parseSpecKitJson + importSpecKitTasks 完整流程", () => {
321
+ const json = JSON.stringify({
322
+ spec: "e2e-spec",
323
+ tasks: [
324
+ { id: "T1", desc: "First task", files: ["a.ts"] },
325
+ { id: "T2", desc: "Second task", depends_on: ["T1"] },
326
+ ],
327
+ acceptance: ["All tests pass"],
328
+ });
329
+ const input = parseSpecKitJson(json);
330
+ const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
331
+ assert.equal(result.created_count, 2);
332
+ assert.equal(result.spec_name, "e2e-spec");
333
+ const status = readGoalStatus(goalId, tempDir);
334
+ assert.equal(status.subgoals.length, 2);
335
+ assert.equal(status.subgoals[0].external_ref, "T1");
336
+ assert.equal(status.subgoals[1].external_ref, "T2");
337
+ assert.deepEqual(status.subgoals[1].depends_on, ["subgoal-001"]);
338
+ assert.deepEqual(status.acceptance_criteria, ["All tests pass"]);
339
+ });
340
+ });
341
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { reloadConfig } from "../../config.js";
7
+ import { waitForTask } from "../../tools/waitForTask.js";
8
+ let tempDir;
9
+ let tasksDir;
10
+ let prevConfigEnv;
11
+ function writeConfig() {
12
+ const configPath = join(tempDir, "patchwarden.config.json");
13
+ writeFileSync(configPath, JSON.stringify({
14
+ workspaceRoot: tempDir,
15
+ tasksDir: ".patchwarden/tasks",
16
+ plansDir: ".patchwarden/plans",
17
+ assessmentsDir: ".patchwarden/assessments",
18
+ directSessionsDir: ".patchwarden/direct-sessions",
19
+ agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
20
+ allowedTestCommands: ["npm test"],
21
+ }), "utf-8");
22
+ prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
23
+ process.env.PATCHWARDEN_CONFIG = configPath;
24
+ reloadConfig();
25
+ }
26
+ function writeTaskStatus(taskId, status) {
27
+ const taskDir = join(tasksDir, taskId);
28
+ mkdirSync(taskDir, { recursive: true });
29
+ writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
30
+ return taskDir;
31
+ }
32
+ function baseStatus(taskId, overrides = {}) {
33
+ return {
34
+ task_id: taskId,
35
+ plan_id: "plan-001",
36
+ agent: "codex",
37
+ workspace_root: tempDir,
38
+ repo_path: "repo",
39
+ resolved_repo_path: join(tempDir, "repo"),
40
+ created_at: "2026-07-12T10:00:00Z",
41
+ started_at: "2026-07-12T10:00:05Z",
42
+ finished_at: "2026-07-12T10:05:00Z",
43
+ updated_at: "2026-07-12T10:05:00Z",
44
+ timeout_seconds: 900,
45
+ error: null,
46
+ ...overrides,
47
+ };
48
+ }
49
+ describe("waitForTask", () => {
50
+ beforeEach(() => {
51
+ tempDir = mkdtempSync(join(tmpdir(), "pw-waitfortask-"));
52
+ tasksDir = join(tempDir, ".patchwarden", "tasks");
53
+ mkdirSync(tasksDir, { recursive: true });
54
+ writeConfig();
55
+ });
56
+ afterEach(() => {
57
+ if (prevConfigEnv === undefined)
58
+ delete process.env.PATCHWARDEN_CONFIG;
59
+ else
60
+ process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
61
+ reloadConfig();
62
+ rmSync(tempDir, { recursive: true, force: true });
63
+ });
64
+ // ── already terminal: done ──
65
+ it("returns immediately when task is already done", async () => {
66
+ writeTaskStatus("task-done-001", baseStatus("task-done-001", {
67
+ status: "done",
68
+ phase: "completed",
69
+ verify_status: "passed",
70
+ }));
71
+ const result = await waitForTask("task-done-001", 5);
72
+ assert.equal(result.task_id, "task-done-001");
73
+ assert.equal(result.status, "done");
74
+ assert.equal(result.terminal, true);
75
+ assert.equal(result.timed_out, false);
76
+ assert.equal(result.continuation_required, false);
77
+ // terminal → includes compact summary, next tool is audit_task
78
+ assert.ok(result.summary);
79
+ assert.equal(result.next_tool_call.name, "audit_task");
80
+ assert.equal(result.progress_summary, undefined);
81
+ });
82
+ // ── already terminal: failed ──
83
+ it("returns immediately when task is already failed", async () => {
84
+ writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
85
+ status: "failed",
86
+ phase: "failed",
87
+ error: "Agent exited with code 1",
88
+ }));
89
+ const result = await waitForTask("task-failed-001", 5);
90
+ assert.equal(result.terminal, true);
91
+ assert.equal(result.status, "failed");
92
+ assert.equal(result.timed_out, false);
93
+ assert.equal(result.continuation_required, false);
94
+ assert.ok(result.summary);
95
+ assert.equal(result.next_tool_call.name, "audit_task");
96
+ });
97
+ // ── running + timeout → continuation_required ──
98
+ it("returns continuation_required when running task times out", async () => {
99
+ writeTaskStatus("task-running-001", baseStatus("task-running-001", {
100
+ status: "running",
101
+ phase: "executing_agent",
102
+ finished_at: undefined,
103
+ }));
104
+ const result = await waitForTask("task-running-001", 1);
105
+ assert.equal(result.task_id, "task-running-001");
106
+ assert.equal(result.status, "running");
107
+ assert.equal(result.terminal, false);
108
+ assert.equal(result.timed_out, true);
109
+ assert.equal(result.continuation_required, true);
110
+ // non-terminal → includes progress_summary, next tool is wait_for_task
111
+ assert.ok(result.progress_summary);
112
+ assert.equal(result.next_tool_call.name, "wait_for_task");
113
+ assert.equal(result.summary, undefined);
114
+ // waited approximately 1 second
115
+ assert.ok(result.waited_ms >= 900 && result.waited_ms <= 3000);
116
+ });
117
+ // ── canceled (terminal) ──
118
+ it("returns terminal result for canceled task", async () => {
119
+ writeTaskStatus("task-canceled-001", baseStatus("task-canceled-001", {
120
+ status: "canceled",
121
+ phase: "canceled",
122
+ }));
123
+ const result = await waitForTask("task-canceled-001", 5);
124
+ assert.equal(result.terminal, true);
125
+ assert.equal(result.status, "canceled");
126
+ assert.equal(result.timed_out, false);
127
+ assert.equal(result.continuation_required, false);
128
+ assert.ok(result.summary);
129
+ assert.equal(result.next_tool_call.name, "audit_task");
130
+ });
131
+ // ── task not found ──
132
+ it("throws when task does not exist", async () => {
133
+ await assert.rejects(() => waitForTask("non-existent-task", 1), /(File not found|Task not found)/);
134
+ });
135
+ // ── invalid wait_seconds ──
136
+ it("throws when wait_seconds is out of range", async () => {
137
+ writeTaskStatus("task-validate-001", baseStatus("task-validate-001", {
138
+ status: "done",
139
+ phase: "completed",
140
+ }));
141
+ await assert.rejects(() => waitForTask("task-validate-001", 0), /wait_seconds must be an integer from 1 to 30/);
142
+ await assert.rejects(() => waitForTask("task-validate-001", 31), /wait_seconds must be an integer from 1 to 30/);
143
+ });
144
+ });
@@ -32,97 +32,42 @@ export interface AuditTaskOutput {
32
32
  manual_verification_items: string[];
33
33
  recommended_next_actions: string[];
34
34
  }
35
+ import type { ChangedFile } from "../runner/changeCapture.js";
35
36
  /**
36
37
  * Check 1: forbidden_scope_violation (fail level).
37
38
  * Returns null when no forbidden patterns are configured (skip).
38
39
  * Returns pass when changed files do not match any forbidden pattern.
39
40
  * Returns fail when any changed file path (or old_path) matches a forbidden glob.
40
41
  */
41
- export declare function checkForbiddenScope(changedFiles: any, forbidden: any): {
42
- name: string;
43
- result: string;
44
- detail: string;
45
- } | null;
42
+ export declare function checkForbiddenScope(changedFiles: ChangedFile[], forbidden: string[] | null): AuditCheck | null;
46
43
  /**
47
44
  * Check 2: done_evidence_missing (warn level).
48
45
  * Returns null when no done_evidence list is configured (skip).
49
46
  * Returns pass when all listed files exist in the task directory.
50
47
  * Returns warn when any listed file is missing.
51
48
  */
52
- export declare function checkDoneEvidenceMissing(taskDir: any, doneEvidence: any): {
53
- name: string;
54
- result: string;
55
- detail: string;
56
- } | null;
49
+ export declare function checkDoneEvidenceMissing(taskDir: string, doneEvidence: string[] | null): AuditCheck | null;
57
50
  /**
58
51
  * Check 3: readme_changelog_sync (warn level).
59
52
  * Returns null when no code files (by extension) were changed (skip).
60
53
  * Returns pass when README.md or CHANGELOG.md (basename, case-insensitive) is also changed.
61
54
  * Returns warn when code changed but neither documentation file was updated.
62
55
  */
63
- export declare function checkReadmeChangelogSync(changedFiles: any): {
64
- name: string;
65
- result: string;
66
- detail: string;
67
- } | null;
56
+ export declare function checkReadmeChangelogSync(changedFiles: ChangedFile[]): AuditCheck | null;
68
57
  /**
69
58
  * Check 4: package_manifest_consistency (warn level).
70
59
  * Returns null when package.json is not among changed files (skip).
71
60
  * Returns pass when package.json parses as JSON and has non-empty name and version strings.
72
61
  * Returns warn otherwise.
73
62
  */
74
- export declare function checkPackageManifestConsistency(changedFiles: any, repoPathSafe: any): {
75
- name: string;
76
- result: string;
77
- detail: string;
78
- } | null;
63
+ export declare function checkPackageManifestConsistency(changedFiles: ChangedFile[], repoPathSafe: string): AuditCheck | null;
79
64
  /**
80
65
  * Check 5: sensitive_path_access (fail level).
81
66
  * Returns null when there are no changed files (skip).
82
67
  * Returns pass when no changed file path (or old_path) is sensitive.
83
68
  * Returns fail when any changed file path is flagged by isSensitivePath.
84
69
  */
85
- export declare function checkSensitivePathAccess(changedFiles: any): {
86
- name: string;
87
- result: string;
88
- detail: string;
89
- } | null;
90
- export declare function checkUnrecordedCommandExecution(testLogContent: string | null, resultMdContent: string | null, verifyCommands: string[], testCommand: string | null, repoPath?: string | null): {
91
- name: string;
92
- result: string;
93
- detail: string;
94
- } | null;
95
- export declare function auditTask(taskId: any): {
96
- task_id: any;
97
- verdict: string;
98
- acceptance: {
99
- verdict: import("../goal/acceptanceEngine.js").AcceptanceVerdict;
100
- status: string;
101
- reason: string;
102
- reasons: string[];
103
- required_evidence: string[];
104
- next_suggested_task: string;
105
- fail_checks: import("../goal/acceptanceEngine.js").AcceptanceCheck[];
106
- warn_checks: import("../goal/acceptanceEngine.js").AcceptanceCheck[];
107
- };
108
- summary: string;
109
- checks: {
110
- name: string;
111
- result: string;
112
- detail: string;
113
- }[];
114
- risks: {
115
- severity: string;
116
- description: string;
117
- }[];
118
- confirmed_failures: {
119
- name: string;
120
- result: string;
121
- detail: string;
122
- }[];
123
- possible_false_positives: any[];
124
- manual_verification_required: boolean;
125
- manual_verification_items: any[];
126
- recommended_next_actions: string[];
127
- };
70
+ export declare function checkSensitivePathAccess(changedFiles: ChangedFile[]): AuditCheck | null;
71
+ export declare function checkUnrecordedCommandExecution(testLogContent: string | null, resultMdContent: string | null, verifyCommands: string[], testCommand: string | null, repoPath?: string | null): AuditCheck | null;
72
+ export declare function auditTask(taskId: string): AuditTaskOutput;
128
73
  export {};