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.
Files changed (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  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 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -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 {};
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "node:fs";
3
2
  import { join, resolve, relative, basename, sep, isAbsolute } from "node:path";
4
3
  import { getTasksDir, getConfig } from "../config.js";
@@ -288,9 +287,10 @@ function readPackageScripts(repoPath) {
288
287
  try {
289
288
  guardSensitivePath(pkgJsonPath);
290
289
  const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
291
- if (!parsed.scripts || typeof parsed.scripts !== "object" || Array.isArray(parsed.scripts))
290
+ const scripts = parsed.scripts;
291
+ if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
292
292
  return {};
293
- return Object.fromEntries(Object.entries(parsed.scripts).filter(([, value]) => typeof value === "string"));
293
+ return Object.fromEntries(Object.entries(scripts).filter(([, value]) => typeof value === "string"));
294
294
  }
295
295
  catch {
296
296
  return {};
@@ -370,8 +370,8 @@ function findHighRiskCommandEvidence(text) {
370
370
  return [...new Set(found)];
371
371
  }
372
372
  export function checkUnrecordedCommandExecution(testLogContent, resultMdContent, verifyCommands, testCommand, repoPath = null) {
373
- const hasLog = testLogContent && testLogContent.length > 0;
374
- const hasResult = resultMdContent && resultMdContent.length > 0;
373
+ const hasLog = !!testLogContent && testLogContent.length > 0;
374
+ const hasResult = !!resultMdContent && resultMdContent.length > 0;
375
375
  if (!hasLog && !hasResult)
376
376
  return null;
377
377
  const combined = `${testLogContent || ""}\n${resultMdContent || ""}`;
@@ -381,9 +381,9 @@ export function checkUnrecordedCommandExecution(testLogContent, resultMdContent,
381
381
  }
382
382
  const whitelist = buildCommandWhitelist(verifyCommands, testCommand, repoPath);
383
383
  const discovered = [];
384
- if (hasLog)
384
+ if (hasLog && testLogContent)
385
385
  discovered.push(...extractCommands(testLogContent));
386
- if (hasResult)
386
+ if (hasResult && resultMdContent)
387
387
  discovered.push(...extractCommands(resultMdContent));
388
388
  const unrecorded = new Set();
389
389
  for (const cmd of discovered) {
@@ -804,7 +804,11 @@ export function auditTask(taskId) {
804
804
  return null;
805
805
  try {
806
806
  const v = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
807
- return v.status === "passed" ? "passed" : v.status === "failed" ? "failed" : "skipped";
807
+ if (v.status === "passed")
808
+ return "passed";
809
+ if (v.status === "failed")
810
+ return "failed";
811
+ return "skipped";
808
812
  }
809
813
  catch {
810
814
  return null;
@@ -11,4 +11,4 @@ export interface CreateDirectSessionOutput {
11
11
  expires_at: string;
12
12
  next_action: string;
13
13
  }
14
- export declare function createDirectSession(input: CreateDirectSessionInput): CreateDirectSessionOutput;
14
+ export declare function createDirectSession(input: CreateDirectSessionInput): Promise<CreateDirectSessionOutput>;
@@ -6,7 +6,7 @@ import { guardRuntimeSelfModification } from "../security/runtimeGuard.js";
6
6
  import { captureRepoSnapshot } from "../runner/changeCapture.js";
7
7
  import { createDirectSession as createDirectSessionRecord, } from "../direct/directSessionStore.js";
8
8
  // ── Tool implementation ────────────────────────────────────────────
9
- export function createDirectSession(input) {
9
+ export async function createDirectSession(input) {
10
10
  const config = getConfig();
11
11
  // ── Validate repo_path ───────────────────────────────────────────
12
12
  if (!input.repo_path || input.repo_path.trim() === "") {
@@ -33,7 +33,7 @@ export function createDirectSession(input) {
33
33
  // ── Runtime self-modification protection ─────────────────────────
34
34
  guardRuntimeSelfModification(resolvedRepoPath);
35
35
  // ── Capture repo snapshot ────────────────────────────────────────
36
- const snapshot = captureRepoSnapshot(resolvedRepoPath);
36
+ const snapshot = await captureRepoSnapshot(resolvedRepoPath);
37
37
  // ── Create session record ────────────────────────────────────────
38
38
  const session = createDirectSessionRecord({
39
39
  repo_path: input.repo_path,