patchwarden 1.5.0 → 1.6.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 (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -12,16 +12,29 @@
12
12
  * 所有文件系统函数都接受可选的 workspaceRoot 参数用于测试;
13
13
  * 默认从 getConfig().workspaceRoot 读取。
14
14
  */
15
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, statSync, } from "node:fs";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { getConfig } from "../config.js";
18
- import { guardWorkspacePath } from "../security/pathGuard.js";
18
+ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
19
19
  import { PatchWardenError } from "../errors.js";
20
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
21
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
22
+ import { mutateLockedJsonFileSync, readJsonObjectFileSync, withFileLock, withFileLockSync, } from "../utils/lockedJsonFile.js";
20
23
  import { createInitialGoalStatus, } from "./goalStatus.js";
21
24
  // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
22
25
  function resolveWorkspaceRoot(workspaceRoot) {
23
26
  return workspaceRoot ?? getConfig().workspaceRoot;
24
27
  }
28
+ const GOALS_PREFIX = join(".patchwarden", "goals");
29
+ const GOAL_ID_MAX_LENGTH = 128;
30
+ const GOAL_ID_PATTERN = /^goal_[A-Za-z0-9][A-Za-z0-9_-]*$/;
31
+ /** Validate the storage key before it can participate in path construction. */
32
+ export function assertValidGoalId(goalId) {
33
+ if (goalId.length > GOAL_ID_MAX_LENGTH ||
34
+ !GOAL_ID_PATTERN.test(goalId)) {
35
+ throw new PatchWardenError("invalid_goal_id", `Invalid goal id. Expected "goal_" followed by at most ${GOAL_ID_MAX_LENGTH - 5} ASCII letters, digits, underscores, or hyphens.`, "Use the goal_id returned by create_goal or list_goals.", true, { goal_id_length: goalId.length });
36
+ }
37
+ }
25
38
  // ── Goal ID 生成 ──────────────────────────────────────────────────
26
39
  /**
27
40
  * 从 title 生成 slug:小写、非字母数字字符替换为 `_`、合并连续 `_`、去除首尾 `_`、截断到 30 字符。
@@ -64,13 +77,16 @@ export function generateGoalId(title, existingIds) {
64
77
  * 不自动创建目录。
65
78
  */
66
79
  export function getGoalsDir(workspaceRoot) {
67
- return join(resolveWorkspaceRoot(workspaceRoot), ".patchwarden", "goals");
80
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
81
+ return guardPath(join(wsRoot, GOALS_PREFIX), wsRoot, GOALS_PREFIX);
68
82
  }
69
83
  /**
70
84
  * 返回 `{getGoalsDir()}/{goalId}` 路径。
71
85
  */
72
86
  export function getGoalDir(goalId, workspaceRoot) {
73
- return join(getGoalsDir(workspaceRoot), goalId);
87
+ assertValidGoalId(goalId);
88
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
89
+ return guardPath(join(getGoalsDir(wsRoot), goalId), wsRoot, GOALS_PREFIX);
74
90
  }
75
91
  // ── CRUD ──────────────────────────────────────────────────────────
76
92
  /**
@@ -82,11 +98,21 @@ export function getGoalDir(goalId, workspaceRoot) {
82
98
  * 返回 { goal_id, goal_dir }。
83
99
  */
84
100
  export function createGoal(repoPath, title, description, workspaceRoot) {
101
+ const safeTitle = redactSensitiveContent(title)
102
+ .content
103
+ .replace(/[\r\n\t]+/g, " ")
104
+ .trim()
105
+ .slice(0, 500) || "Untitled goal";
106
+ const safeDescription = redactSensitiveContent(description).content;
107
+ if (Buffer.byteLength(safeDescription, "utf-8") > 1024 * 1024) {
108
+ throw new PatchWardenError("goal_description_too_large", "Goal description exceeds the 1 MiB limit.", "Split the work into smaller Goal Sessions.");
109
+ }
85
110
  const wsRoot = resolveWorkspaceRoot(workspaceRoot);
86
111
  const guardedRepo = guardWorkspacePath(repoPath, wsRoot);
87
112
  const goalsDir = getGoalsDir(workspaceRoot);
88
- const existingIds = [];
89
- if (existsSync(goalsDir)) {
113
+ mkdirSync(goalsDir, { recursive: true });
114
+ return withFileLockSync(join(goalsDir, "goal-creation"), () => {
115
+ const existingIds = [];
90
116
  for (const entry of readdirSync(goalsDir)) {
91
117
  const entryPath = join(goalsDir, entry);
92
118
  try {
@@ -95,35 +121,32 @@ export function createGoal(repoPath, title, description, workspaceRoot) {
95
121
  }
96
122
  }
97
123
  catch {
98
- // 跳过无法 stat 的条目
124
+ // Skip entries that cannot be inspected.
99
125
  }
100
126
  }
101
- }
102
- const goalId = generateGoalId(title, existingIds);
103
- const goalDir = join(goalsDir, goalId);
104
- // 创建目录结构
105
- mkdirSync(join(goalDir, "tasks"), { recursive: true });
106
- mkdirSync(join(goalDir, "artifacts"), { recursive: true });
107
- // 写入 GOAL.md
108
- const now = new Date().toISOString();
109
- const goalMd = [
110
- `# ${title}`,
111
- "",
112
- description,
113
- "",
114
- `- Created: ${now}`,
115
- `- Repo: ${guardedRepo}`,
116
- "- Status: active",
117
- "",
118
- ].join("\n");
119
- writeFileSync(join(goalDir, "GOAL.md"), goalMd, "utf-8");
120
- // 写入 GOALS.md
121
- const goalsMd = `# Subgoals: ${title}\n\n_No subgoals yet._\n`;
122
- writeFileSync(join(goalDir, "GOALS.md"), goalsMd, "utf-8");
123
- // 写入 goal_status.json
124
- const status = createInitialGoalStatus(goalId, title, guardedRepo);
125
- writeFileSync(join(goalDir, "goal_status.json"), JSON.stringify(status, null, 2) + "\n", "utf-8");
126
- return { goal_id: goalId, goal_dir: goalDir };
127
+ const goalId = generateGoalId(safeTitle, existingIds);
128
+ const goalDir = getGoalDir(goalId, wsRoot);
129
+ mkdirSync(goalDir);
130
+ mkdirSync(join(goalDir, "tasks"));
131
+ mkdirSync(join(goalDir, "artifacts"));
132
+ const now = new Date().toISOString();
133
+ const goalMd = [
134
+ `# ${safeTitle}`,
135
+ "",
136
+ safeDescription,
137
+ "",
138
+ `- Created: ${now}`,
139
+ `- Repo: ${guardedRepo}`,
140
+ "- Status: active",
141
+ "",
142
+ ].join("\n");
143
+ atomicWriteFileSync(join(goalDir, "GOAL.md"), goalMd);
144
+ const goalsMd = `# Subgoals: ${safeTitle}\n\n_No subgoals yet._\n`;
145
+ atomicWriteFileSync(join(goalDir, "GOALS.md"), goalsMd);
146
+ const status = createInitialGoalStatus(goalId, safeTitle, guardedRepo);
147
+ atomicWriteJsonFileSync(join(goalDir, "goal_status.json"), status);
148
+ return { goal_id: goalId, goal_dir: goalDir };
149
+ }, goalBusyOptions("creation"));
127
150
  }
128
151
  /**
129
152
  * 列出所有 goal 的摘要信息,按 updated_at 降序排列。
@@ -136,8 +159,8 @@ export function listGoals(workspaceRoot) {
136
159
  }
137
160
  const results = [];
138
161
  for (const entry of readdirSync(goalsDir)) {
139
- const entryPath = join(goalsDir, entry);
140
162
  try {
163
+ const entryPath = getGoalDir(entry, workspaceRoot);
141
164
  if (!statSync(entryPath).isDirectory()) {
142
165
  continue;
143
166
  }
@@ -172,7 +195,8 @@ export function readGoal(goalId, workspaceRoot) {
172
195
  let goalDescription = "";
173
196
  const goalMdPath = join(goalDir, "GOAL.md");
174
197
  if (existsSync(goalMdPath)) {
175
- goalDescription = readFileSync(goalMdPath, "utf-8");
198
+ const safeGoalMdPath = guardPath(goalMdPath, resolveWorkspaceRoot(workspaceRoot), GOALS_PREFIX);
199
+ goalDescription = readFileSync(safeGoalMdPath, "utf-8");
176
200
  }
177
201
  return {
178
202
  goal_id: status.goal_id,
@@ -186,26 +210,109 @@ export function readGoal(goalId, workspaceRoot) {
186
210
  };
187
211
  }
188
212
  /**
189
- * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
190
- * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
213
+ * Replace goal_status.json under the shared cross-process mutation lock.
191
214
  */
192
215
  export function writeGoalStatus(goalId, status, workspaceRoot) {
193
216
  const goalDir = getGoalDir(goalId, workspaceRoot);
194
- const finalPath = join(goalDir, "goal_status.json");
195
- const tmpPath = join(goalDir, "goal_status.json.tmp");
196
- writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
197
- renameSync(tmpPath, finalPath);
217
+ if (!existsSync(goalDir)) {
218
+ throw new PatchWardenError("goal_not_found", `Goal directory not found: "${goalDir}"`, "Ensure the goal has been created before writing its status.", true, { goal_id: goalId, goal_dir: goalDir });
219
+ }
220
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
221
+ if (!existsSync(statusPath)) {
222
+ withFileLockSync(statusPath, () => {
223
+ atomicWriteJsonFileSync(statusPath, normalizeGoalLifecycle(validateGoalStatus(goalId, status, workspaceRoot)));
224
+ }, goalBusyOptions(goalId));
225
+ return;
226
+ }
227
+ mutateGoalStatus(goalId, () => ({ next: status, result: undefined }), workspaceRoot);
198
228
  }
199
229
  /**
200
230
  * 读取 goal_status.json 并 JSON.parse。
201
231
  * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
202
232
  */
203
233
  export function readGoalStatus(goalId, workspaceRoot) {
204
- const goalDir = getGoalDir(goalId, workspaceRoot);
205
- const statusPath = join(goalDir, "goal_status.json");
234
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
206
235
  if (!existsSync(statusPath)) {
207
236
  throw new PatchWardenError("goal_not_found", `goal_status.json not found for goal "${goalId}" at "${statusPath}"`, "Ensure the goal has been created via createGoal before reading its status.", true, { goal_id: goalId, status_path: statusPath });
208
237
  }
209
- const raw = readFileSync(statusPath, "utf-8");
210
- return JSON.parse(raw);
238
+ let parsed;
239
+ try {
240
+ parsed = readJsonObjectFileSync(statusPath);
241
+ }
242
+ catch {
243
+ throw invalidGoalStatus(goalId, "goal_status.json is not valid JSON");
244
+ }
245
+ return validateGoalStatus(goalId, parsed, workspaceRoot);
246
+ }
247
+ export function mutateGoalStatus(goalId, mutation, workspaceRoot) {
248
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
249
+ ensureGoalStatusExists(goalId, statusPath);
250
+ return mutateLockedJsonFileSync(statusPath, (raw) => {
251
+ const current = validateGoalStatus(goalId, raw, workspaceRoot);
252
+ const outcome = mutation(current);
253
+ if (!outcome.next)
254
+ return outcome;
255
+ return {
256
+ next: normalizeGoalLifecycle(validateGoalStatus(goalId, outcome.next, workspaceRoot)),
257
+ result: outcome.result,
258
+ };
259
+ }, goalBusyOptions(goalId));
260
+ }
261
+ export async function mutateGoalStatusAsync(goalId, mutation, workspaceRoot) {
262
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
263
+ ensureGoalStatusExists(goalId, statusPath);
264
+ return withFileLock(statusPath, async () => {
265
+ const current = validateGoalStatus(goalId, readJsonObjectFileSync(statusPath), workspaceRoot);
266
+ const outcome = await mutation(current);
267
+ if (outcome.next) {
268
+ atomicWriteJsonFileSync(statusPath, normalizeGoalLifecycle(validateGoalStatus(goalId, outcome.next, workspaceRoot)));
269
+ }
270
+ return outcome.result;
271
+ }, goalBusyOptions(goalId));
272
+ }
273
+ function getGoalStatusPath(goalId, workspaceRoot) {
274
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
275
+ return guardPath(join(getGoalDir(goalId, wsRoot), "goal_status.json"), wsRoot, GOALS_PREFIX);
276
+ }
277
+ function ensureGoalStatusExists(goalId, statusPath) {
278
+ if (!existsSync(statusPath)) {
279
+ throw new PatchWardenError("goal_not_found", `goal_status.json not found for goal "${goalId}" at "${statusPath}"`, "Ensure the goal has been created via createGoal before updating it.", true, { goal_id: goalId, status_path: statusPath });
280
+ }
281
+ }
282
+ function validateGoalStatus(goalId, value, workspaceRoot) {
283
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
284
+ throw invalidGoalStatus(goalId, "expected a JSON object");
285
+ }
286
+ const candidate = value;
287
+ if (candidate.goal_id !== goalId) {
288
+ throw invalidGoalStatus(goalId, "goal_id does not match its directory");
289
+ }
290
+ if (typeof candidate.title !== "string" || typeof candidate.repo_path !== "string") {
291
+ throw invalidGoalStatus(goalId, "title or repo_path is missing");
292
+ }
293
+ if (candidate.status !== "active" && candidate.status !== "completed" && candidate.status !== "abandoned") {
294
+ throw invalidGoalStatus(goalId, "status is invalid");
295
+ }
296
+ if (!Array.isArray(candidate.subgoals)) {
297
+ throw invalidGoalStatus(goalId, "subgoals must be an array");
298
+ }
299
+ guardWorkspacePath(candidate.repo_path, resolveWorkspaceRoot(workspaceRoot));
300
+ return candidate;
301
+ }
302
+ function normalizeGoalLifecycle(status) {
303
+ if (status.status === "active" &&
304
+ status.subgoals.length > 0 &&
305
+ status.subgoals.every((subgoal) => subgoal.status === "accepted")) {
306
+ return { ...status, status: "completed", updated_at: new Date().toISOString() };
307
+ }
308
+ return status;
309
+ }
310
+ function invalidGoalStatus(goalId, detail) {
311
+ return new PatchWardenError("invalid_goal_status", `Invalid goal status for "${goalId}": ${detail}.`, "Recreate the Goal Session or restore a valid goal_status.json artifact.", true, { goal_id: goalId });
312
+ }
313
+ function goalBusyOptions(goalId) {
314
+ return {
315
+ waitMs: 0,
316
+ busyError: () => new PatchWardenError("goal_busy", `Goal "${goalId}" is currently being updated.`, "Retry after the active Goal operation finishes.", true, { goal_id: goalId }),
317
+ };
211
318
  }
@@ -7,9 +7,10 @@
7
7
  * 以避免与 goalStore 形成循环依赖。
8
8
  */
9
9
  import { suggestNextSubgoal } from "./goalGraph.js";
10
- import { getConfig } from "../config.js";
11
- import { mkdirSync, writeFileSync } from "node:fs";
12
- import { join, resolve } from "node:path";
10
+ import { getGoalDir } from "./goalStore.js";
11
+ import { mkdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
13
14
  // ── Markdown 生成 ────────────────────────────────────────────────
14
15
  /**
15
16
  * 生成 Goal Session 交接文档(Markdown 字符串)。
@@ -170,11 +171,10 @@ export function generateHandoff(goalId, goalStatus, recentDiff, recentTestResult
170
171
  */
171
172
  export function exportHandoff(goalId, goalStatus, workspaceRoot) {
172
173
  const content = generateHandoff(goalId, goalStatus);
173
- const root = workspaceRoot ?? getConfig().workspaceRoot;
174
- const goalDir = resolve(root, ".patchwarden", "goals", goalId);
174
+ const goalDir = getGoalDir(goalId, workspaceRoot);
175
175
  mkdirSync(goalDir, { recursive: true });
176
176
  const handoffPath = join(goalDir, "handoff.md");
177
- writeFileSync(handoffPath, content, "utf-8");
177
+ atomicWriteFileSync(handoffPath, content);
178
178
  const preview = content.length > 500 ? content.slice(0, 500) + "..." : content;
179
179
  return {
180
180
  handoff_path: handoffPath,
@@ -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,221 @@
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 { mutateGoalStatus } 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
+ return mutateGoalStatus(goalId, (currentGoalStatus) => {
105
+ let goalStatus = currentGoalStatus;
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
+ return {
211
+ next: goalStatus,
212
+ result: {
213
+ goal_id: goalId,
214
+ spec_name: input.spec,
215
+ created_count: createdIds.length,
216
+ existing_count: existingCount,
217
+ subgoal_ids: createdIds,
218
+ },
219
+ };
220
+ }, workspaceRoot);
221
+ }
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import { readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
- import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
11
+ import { mutateGoalStatus } 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 状态。
@@ -31,18 +32,20 @@ export function syncSubgoalOnTaskDone(taskId, taskMeta, workspaceRoot) {
31
32
  if (!goalId || !subgoalId)
32
33
  return;
33
34
  try {
34
- const goalStatus = readGoalStatus(goalId, workspaceRoot);
35
- const subgoal = goalStatus.subgoals.find((s) => s.id === subgoalId);
36
- if (!subgoal)
37
- return;
38
- if (subgoal.status !== "running")
39
- return;
40
- const updated = updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent");
41
- writeGoalStatus(goalId, updated, workspaceRoot);
35
+ mutateGoalStatus(goalId, (goalStatus) => {
36
+ const subgoal = goalStatus.subgoals.find((entry) => entry.id === subgoalId);
37
+ if (!subgoal || subgoal.status !== "running") {
38
+ return { result: undefined };
39
+ }
40
+ return {
41
+ next: updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent"),
42
+ result: undefined,
43
+ };
44
+ }, workspaceRoot);
42
45
  }
43
46
  catch (err) {
44
47
  // subgoal 同步失败不应影响任务完成流程,只记录到 stderr
45
- console.error(`[goal] syncSubgoalOnTaskDone failed for task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
48
+ logger.error(`[goal] syncSubgoalOnTaskDone failed for task ${taskId}`, { error: err instanceof Error ? err.message : String(err) });
46
49
  }
47
50
  }
48
51
  // ── Task 元信息读取 ───────────────────────────────────────────────
@@ -31,6 +31,8 @@ export interface WorktreeStatus {
31
31
  }
32
32
  /** worktree 根目录名,放在 workspaceRoot 下。 */
33
33
  export declare const WorktreeDir = "_workspacetrees";
34
+ /** Accept only IDs emitted by generateWorktreeId(). */
35
+ export declare function assertValidWorktreeId(worktreeId: string): void;
34
36
  /**
35
37
  * 返回 `<workspaceRoot>/_workspacetrees` 目录路径。不自动创建目录。
36
38
  */