patchwarden 1.5.1 → 1.6.2

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 (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, join, resolve } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
10
+ const desktopRoot = join(root, "desktop");
11
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
12
+ const argv = process.argv.slice(2);
13
+ const requireClean = argv.includes("--require-clean");
14
+ const skipUiSmoke = argv.includes("--skip-ui-smoke");
15
+
16
+ function option(name) {
17
+ const index = argv.indexOf(name);
18
+ return index >= 0 ? argv[index + 1] : null;
19
+ }
20
+
21
+ function json(path) {
22
+ return JSON.parse(readFileSync(path, "utf8"));
23
+ }
24
+
25
+ function sha256(value) {
26
+ return createHash("sha256").update(value).digest("hex");
27
+ }
28
+
29
+ function directoryStats(directory) {
30
+ let files = 0;
31
+ let bytes = 0;
32
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
33
+ const path = join(directory, entry.name);
34
+ if (entry.isDirectory()) {
35
+ const nested = directoryStats(path);
36
+ files += nested.files;
37
+ bytes += nested.bytes;
38
+ } else if (entry.isFile()) {
39
+ files += 1;
40
+ bytes += statSync(path).size;
41
+ }
42
+ }
43
+ return { files, bytes };
44
+ }
45
+
46
+ function spawnSpec(command, args) {
47
+ if (process.platform === "win32" && /\.cmd$/i.test(command)) {
48
+ return { command: process.env.ComSpec || "cmd.exe", args: ["/d", "/s", "/c", command, ...args] };
49
+ }
50
+ return { command, args };
51
+ }
52
+
53
+ function capture(command, args, cwd = root) {
54
+ const spec = spawnSpec(command, args);
55
+ const result = spawnSync(spec.command, spec.args, { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
56
+ if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr || result.stdout || result.error?.message || "unknown error"}`);
57
+ return String(result.stdout || "").trim();
58
+ }
59
+
60
+ const rootPackage = json(join(root, "package.json"));
61
+ const rootLock = json(join(root, "package-lock.json"));
62
+ const desktopPackage = json(join(desktopRoot, "package.json"));
63
+ const desktopLock = json(join(desktopRoot, "package-lock.json"));
64
+ const versionSource = readFileSync(join(root, "src", "version.ts"), "utf8");
65
+ const sourceVersion = versionSource.match(/PATCHWARDEN_VERSION\s*=\s*"([^"]+)"/)?.[1] || null;
66
+ const versions = [rootPackage.version, rootLock.version, rootLock.packages?.[""]?.version, desktopPackage.version, desktopLock.version, desktopLock.packages?.[""]?.version, sourceVersion];
67
+ if (versions.some((version) => version !== rootPackage.version)) throw new Error(`Version mismatch: ${versions.join(", ")}`);
68
+ if (process.platform !== "win32") throw new Error("Desktop preflight requires Windows.");
69
+
70
+ const gitStatus = capture("git", ["status", "--porcelain=v1", "-z"]);
71
+ const changedFileCount = gitStatus ? gitStatus.split("\0").filter(Boolean).length : 0;
72
+ if (requireClean && changedFileCount > 0) throw new Error(`Release preflight requires a clean worktree; found ${changedFileCount} changed paths.`);
73
+
74
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
75
+ const defaultOutput = join(root, "release", `desktop-preflight-${rootPackage.version}-${stamp}`);
76
+ const outputRoot = resolve(option("--output") || defaultOutput);
77
+ if (existsSync(outputRoot)) throw new Error(`Preflight output already exists: ${outputRoot}`);
78
+ mkdirSync(outputRoot, { recursive: true });
79
+
80
+ const report = {
81
+ schema_version: "patchwarden-desktop-preflight-v1",
82
+ status: "running",
83
+ started_at: new Date().toISOString(),
84
+ finished_at: null,
85
+ version: rootPackage.version,
86
+ output_directory: outputRoot,
87
+ source: {
88
+ branch: capture("git", ["branch", "--show-current"]),
89
+ head: capture("git", ["rev-parse", "HEAD"]),
90
+ dirty: changedFileCount > 0,
91
+ changed_path_count: changedFileCount,
92
+ status_sha256: sha256(gitStatus),
93
+ tracked_diff_sha256: sha256(capture("git", ["diff", "--binary", "HEAD"])),
94
+ },
95
+ environment: {
96
+ platform: process.platform,
97
+ arch: process.arch,
98
+ node: process.version,
99
+ npm: capture(npmCommand, ["--version"]),
100
+ electron: desktopPackage.devDependencies.electron,
101
+ electron_builder: desktopPackage.devDependencies["electron-builder"],
102
+ },
103
+ options: { require_clean: requireClean, skip_ui_smoke: skipUiSmoke },
104
+ checks: [],
105
+ package: null,
106
+ runtime_manifest: null,
107
+ ui_smoke: null,
108
+ artifact: null,
109
+ error: null,
110
+ };
111
+
112
+ function saveReport() {
113
+ writeFileSync(join(outputRoot, "preflight-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
114
+ const lines = [
115
+ `# PatchWarden Desktop ${report.version} preflight`,
116
+ "",
117
+ `- Status: ${report.status}`,
118
+ `- Source: ${report.source.branch} @ ${report.source.head}`,
119
+ `- Dirty baseline: ${report.source.dirty} (${report.source.changed_path_count} paths, status SHA256 ${report.source.status_sha256})`,
120
+ `- Node/npm: ${report.environment.node} / ${report.environment.npm}`,
121
+ `- Electron: ${report.environment.electron}`,
122
+ `- Output: ${report.output_directory}`,
123
+ "",
124
+ "## Checks",
125
+ "",
126
+ ...report.checks.map((check) => `- ${check.ok ? "PASS" : "FAIL"} ${check.name} (${check.duration_ms} ms)`),
127
+ "",
128
+ report.runtime_manifest ? `Packaged runtime: ${report.runtime_manifest.tool_profile}, ${report.runtime_manifest.tool_count} tools, manifest SHA256 ${report.runtime_manifest.tool_manifest_sha256}.` : "Packaged runtime: not verified.",
129
+ report.ui_smoke ? `UI smoke: ${report.ui_smoke.ok ? "passed" : "failed"}.` : "UI smoke: not run.",
130
+ report.error ? `\nError: ${report.error}` : "",
131
+ ];
132
+ writeFileSync(join(outputRoot, "preflight-report.md"), `${lines.join("\n")}\n`, "utf8");
133
+ }
134
+
135
+ function run(name, command, args, cwd = root) {
136
+ const started = Date.now();
137
+ const spec = spawnSpec(command, args);
138
+ const result = spawnSync(spec.command, spec.args, { cwd, encoding: "utf8", windowsHide: true, maxBuffer: 128 * 1024 * 1024 });
139
+ const stdout = String(result.stdout || "");
140
+ const stderr = String(result.stderr || "");
141
+ if (stdout) process.stdout.write(stdout);
142
+ if (stderr) process.stderr.write(stderr);
143
+ const check = { name, ok: result.status === 0, exit_code: result.status, duration_ms: Date.now() - started };
144
+ report.checks.push(check);
145
+ saveReport();
146
+ if (!check.ok) throw new Error(`${name} failed with exit code ${result.status ?? "unknown"}: ${stderr || result.error?.message || "see command output"}`);
147
+ return stdout;
148
+ }
149
+
150
+ let isolatedRoot = null;
151
+ try {
152
+ run("root clean build", npmCommand, ["run", "build"]);
153
+ run("build output contract", npmCommand, ["run", "check:build-output"]);
154
+ const unitOutput = run("root unit tests", npmCommand, ["run", "test:unit"]);
155
+ const desktopOutput = run("desktop tests", npmCommand, ["run", "desktop:test"]);
156
+ const packageOutput = run("npm package surface", npmCommand, ["run", "verify:package"]);
157
+ run("desktop staging", npmCommand, ["run", "desktop:stage"]);
158
+ run("Electron directory package", npmCommand, ["exec", "electron-builder", "--", "--win", "dir", "--x64", `--config.directories.output=${outputRoot}`], desktopRoot);
159
+
160
+ report.package = {
161
+ npm_file_count: Number(packageOutput.match(/OK:\s+(\d+) package files/)?.[1] || 0),
162
+ root_unit_tests: Number([...unitOutput.matchAll(/tests\s+(\d+)/g)].at(-1)?.[1] || 0),
163
+ desktop_tests: Number([...desktopOutput.matchAll(/tests\s+(\d+)/g)].at(-1)?.[1] || 0),
164
+ };
165
+
166
+ const unpackedRoot = join(outputRoot, "win-unpacked");
167
+ const executable = join(unpackedRoot, "PatchWarden.exe");
168
+ const packagedCore = join(unpackedRoot, "resources", "core");
169
+ if (!existsSync(executable) || !existsSync(packagedCore)) throw new Error("Electron directory package is missing its executable or core runtime.");
170
+ for (const forbidden of ["dist/test", "src/test", "docs/archive", "scripts/checks/doctor-smoke.js"]) {
171
+ if (existsSync(join(packagedCore, forbidden))) throw new Error(`Packaged core contains development-only path: ${forbidden}`);
172
+ }
173
+ const packagedChecks = readdirSync(join(packagedCore, "scripts", "checks")).sort();
174
+ if (JSON.stringify(packagedChecks) !== JSON.stringify(["mcp-manifest-check.js"])) {
175
+ throw new Error(`Packaged core has unexpected check scripts: ${packagedChecks.join(", ")}`);
176
+ }
177
+ const coreStats = directoryStats(packagedCore);
178
+ report.package.packaged_core_file_count = coreStats.files;
179
+ report.package.packaged_core_size_bytes = coreStats.bytes;
180
+
181
+ isolatedRoot = join(tmpdir(), `patchwarden-preflight-${process.pid}-${Date.now()}`);
182
+ cpSync(packagedCore, isolatedRoot, { recursive: true });
183
+ const manifestOutput = run("isolated packaged runtime manifest", process.execPath, ["scripts/checks/mcp-manifest-check.js", "--profile", "chatgpt_core"], isolatedRoot);
184
+ report.runtime_manifest = JSON.parse(manifestOutput);
185
+ if (!report.runtime_manifest.ok || report.runtime_manifest.server_version !== rootPackage.version || report.runtime_manifest.tool_count !== 26) {
186
+ throw new Error(`Packaged runtime manifest did not match the ${rootPackage.version} 26-tool contract.`);
187
+ }
188
+
189
+ if (!skipUiSmoke) {
190
+ const smokeOutput = run("unpacked desktop UI smoke", process.execPath, ["scripts/smoke-unpacked.mjs", "--exe", executable, "--output", join(outputRoot, "ui-smoke")], desktopRoot);
191
+ report.ui_smoke = JSON.parse(smokeOutput.trim().split(/\r?\n/).at(-1));
192
+ if (!report.ui_smoke.ok) throw new Error("Unpacked desktop UI smoke did not pass.");
193
+ }
194
+
195
+ report.artifact = {
196
+ name: basename(executable),
197
+ size_bytes: readFileSync(executable).length,
198
+ sha256: sha256(readFileSync(executable)),
199
+ };
200
+ report.status = "passed";
201
+ } catch (error) {
202
+ report.status = "failed";
203
+ report.error = error instanceof Error ? error.message : String(error);
204
+ throw error;
205
+ } finally {
206
+ if (isolatedRoot) rmSync(isolatedRoot, { recursive: true, force: true });
207
+ report.finished_at = new Date().toISOString();
208
+ saveReport();
209
+ }
210
+
211
+ console.log(`[desktop-preflight] PASS: ${outputRoot}`);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- cpSync,
3
+ copyFileSync,
4
4
  existsSync,
5
5
  mkdirSync,
6
6
  readFileSync,
@@ -16,7 +16,8 @@ import { fileURLToPath } from "node:url";
16
16
 
17
17
  const scriptDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
18
18
  const root = resolve(scriptDir, "..", "..");
19
- const releaseDir = resolve(root, "release");
19
+ const releaseRoot = resolve(root, "release");
20
+ const releaseDir = resolve(releaseRoot, "package");
20
21
  const archivePath = resolve(root, "patchwarden-release.tar.gz");
21
22
 
22
23
  // Read version from package.json for zip naming
@@ -31,11 +32,6 @@ const include = [
31
32
  "src",
32
33
  "ui",
33
34
  "PatchWarden.cmd",
34
- "PatchWarden-Control.cmd",
35
- "PatchWarden-Control-Tray.cmd",
36
- "PatchWarden-Desktop.cmd",
37
- "Restart-PatchWarden-Control.cmd",
38
- "Stop-PatchWarden.cmd",
39
35
  ".gitignore",
40
36
  "LICENSE",
41
37
  "README.md",
@@ -57,6 +53,8 @@ const forbidden = [
57
53
  /(^|[\\/])\.env$/,
58
54
  /\.log$/,
59
55
  /^docs[\\/]optimization-proposal\.md$/i,
56
+ /^docs[\\/]archive(?:[\\/]|$)/i,
57
+ /^(?:dist|src)[\\/]test(?:[\\/]|$)/i,
60
58
  /(^|[\\/])kill-patchwarden\.(cmd|ps1)$/i,
61
59
  ];
62
60
 
@@ -173,9 +171,15 @@ function readZipEntryNames(zipPath) {
173
171
  return names;
174
172
  }
175
173
 
174
+ const stageRelativeToRoot = toPosix(relative(root, releaseDir));
175
+ if (stageRelativeToRoot !== "release/package") {
176
+ throw new Error(`Refusing to clean non-isolated release staging path: ${stageRelativeToRoot}`);
177
+ }
178
+
176
179
  console.log("[pack-clean] Preparing clean release directory...");
177
180
  rmSync(releaseDir, { recursive: true, force: true });
178
181
  rmSync(archivePath, { force: true });
182
+ mkdirSync(releaseRoot, { recursive: true });
179
183
  mkdirSync(releaseDir, { recursive: true });
180
184
 
181
185
  for (const item of include) {
@@ -185,17 +189,26 @@ for (const item of include) {
185
189
  console.log(` skip missing: ${item}`);
186
190
  continue;
187
191
  }
188
- cpSync(source, target, {
189
- recursive: true,
190
- filter(sourcePath) {
191
- const rel = toPosix(relative(root, sourcePath));
192
- return !isForbidden(rel);
193
- },
194
- });
192
+ copyAllowedEntry(source, target);
195
193
  console.log(` copied: ${item}`);
196
194
  }
197
195
 
196
+ // Keep an independent second boundary before the fail-closed validation. This
197
+ // protects package contents if a future copy implementation changes behavior.
198
+ pruneForbiddenEntries(releaseDir);
199
+
198
200
  const releaseFiles = listFiles(releaseDir);
201
+ const requiredReleaseEntries = ["package.json", "README.md", "dist/index.js"];
202
+ const missingReleaseEntries = requiredReleaseEntries.filter(
203
+ (entry) => !existsSync(resolve(releaseDir, entry))
204
+ );
205
+ if (releaseFiles.length === 0 || missingReleaseEntries.length > 0) {
206
+ console.error("[pack-clean] Release directory is incomplete:");
207
+ for (const entry of missingReleaseEntries) {
208
+ console.error(` missing: ${entry}`);
209
+ }
210
+ process.exit(1);
211
+ }
199
212
  const badReleaseEntries = releaseFiles.filter((file) =>
200
213
  isForbidden(toPosix(relative(releaseDir, file)))
201
214
  );
@@ -296,6 +309,38 @@ function listFiles(dir) {
296
309
  return entries;
297
310
  }
298
311
 
312
+ function copyAllowedEntry(source, target) {
313
+ const rel = toPosix(relative(root, source));
314
+ if (isForbidden(rel)) {
315
+ return;
316
+ }
317
+
318
+ const stat = statSync(source);
319
+ if (stat.isDirectory()) {
320
+ mkdirSync(target, { recursive: true });
321
+ for (const name of readdirSync(source)) {
322
+ copyAllowedEntry(join(source, name), join(target, name));
323
+ }
324
+ return;
325
+ }
326
+
327
+ copyFileSync(source, target);
328
+ }
329
+
330
+ function pruneForbiddenEntries(dir) {
331
+ for (const name of readdirSync(dir)) {
332
+ const fullPath = join(dir, name);
333
+ const rel = toPosix(relative(releaseDir, fullPath));
334
+ if (isForbidden(rel)) {
335
+ rmSync(fullPath, { recursive: true, force: true });
336
+ continue;
337
+ }
338
+ if (statSync(fullPath).isDirectory()) {
339
+ pruneForbiddenEntries(fullPath);
340
+ }
341
+ }
342
+ }
343
+
299
344
  function isForbidden(value) {
300
345
  return forbidden.some((pattern) => pattern.test(value));
301
346
  }
@@ -1,10 +1,11 @@
1
- import { writeFileSync } from "node:fs";
2
- import { resolve, relative, isAbsolute } from "node:path";
1
+ import { resolve, relative, isAbsolute, sep } from "node:path";
3
2
  import { PatchWardenConfig } from "../config.js";
4
3
  import { buildAgentInvocation, buildAssessmentPrompt } from "../runner/agentInvocation.js";
5
4
  import { runSimpleProcessSync } from "../runner/simpleProcess.js";
6
5
  import { captureRepoSnapshot, compareSnapshots, type RepoSnapshot, type ChangedFile } from "../runner/changeCapture.js";
7
6
  import type { AgentAssessmentOutput, AgentAssessmentSummary } from "./assessmentStore.js";
7
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
8
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
8
9
 
9
10
  const ASSESSMENT_MARKER = "===ASSESSMENT_JSON===";
10
11
 
@@ -45,9 +46,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
45
46
  };
46
47
 
47
48
  // ── 1. Build prompt and write to file for {prompt_file} support ──
48
- const assessmentPrompt = buildAssessmentPrompt(input.goal, input.planContent, input.repoPath);
49
+ const safeGoal = redactSensitiveContent(input.goal).content.slice(0, 10_000);
50
+ const safePlan = redactSensitiveContent(input.planContent).content.slice(0, 1024 * 1024);
51
+ const assessmentPrompt = buildAssessmentPrompt(safeGoal, safePlan, input.repoPath);
49
52
  const promptFilePath = resolve(input.assessmentDir, "agent-assessment-prompt.md");
50
- writeFileSync(promptFilePath, assessmentPrompt, "utf-8");
53
+ atomicWriteFileSync(promptFilePath, assessmentPrompt);
51
54
  logPaths.prompt = promptFilePath;
52
55
 
53
56
  // ── 2. Before snapshot (repo-scoped) ──
@@ -89,6 +92,8 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
89
92
  maxStderrBytes: Math.max(16384, Math.floor(input.maxOutputBytes / 4)),
90
93
  stdoutPath: logPaths.stdout,
91
94
  stderrPath: logPaths.stderr,
95
+ environmentVariableNames: invocation.environmentVariableNames,
96
+ blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
92
97
  });
93
98
 
94
99
  // ── 5. After snapshot + read-only violation check ──
@@ -123,11 +128,11 @@ export async function runAgentAssessment(input: AgentAssessorInput): Promise<Age
123
128
  stderr_truncated: result.stderrTruncated,
124
129
  log_paths: { ...logPaths, violation: resolve(input.assessmentDir, "agent-assessment-violation.json") },
125
130
  };
126
- writeFileSync(summary.log_paths.violation!, JSON.stringify({
131
+ atomicWriteJsonFileSync(summary.log_paths.violation!, {
127
132
  violation: "read_only_violation",
128
133
  files: violationFiles,
129
134
  exit_code: result.exitCode,
130
- }, null, 2), "utf-8");
135
+ });
131
136
  writeSummary(logPaths.assessment, summary);
132
137
  return summary;
133
138
  }
@@ -253,12 +258,16 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
253
258
  const jsonEnd = jsonText.lastIndexOf("}");
254
259
  if (jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart) return { output: null, sanitized_reasons };
255
260
 
256
- let parsed: any;
261
+ let parsedValue: unknown;
257
262
  try {
258
- parsed = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
263
+ parsedValue = JSON.parse(jsonText.slice(jsonStart, jsonEnd + 1));
259
264
  } catch {
260
265
  return { output: null, sanitized_reasons };
261
266
  }
267
+ if (!parsedValue || typeof parsedValue !== "object" || Array.isArray(parsedValue)) {
268
+ return { output: null, sanitized_reasons };
269
+ }
270
+ const parsed = parsedValue as Record<string, unknown>;
262
271
 
263
272
  // ── Strict validation ──
264
273
  if (parsed.risk_level !== "low" && parsed.risk_level !== "medium" && parsed.risk_level !== "high") {
@@ -272,7 +281,9 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
272
281
 
273
282
  if (!Array.isArray(parsed.reason_codes)) return { output: null, sanitized_reasons };
274
283
  if (parsed.reason_codes.length > 50) return { output: null, sanitized_reasons };
275
- const reasonCodes = parsed.reason_codes.filter((r: any) => typeof r === "string" && r.length <= 100);
284
+ const reasonCodes = parsed.reason_codes.filter((reason): reason is string =>
285
+ typeof reason === "string" && reason.length <= 100
286
+ );
276
287
  if (reasonCodes.length !== parsed.reason_codes.length) {
277
288
  sanitized_reasons.push("reason_codes_filtered");
278
289
  }
@@ -287,7 +298,10 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
287
298
  if (isAbsolute(p)) { pathsSanitized = true; continue; }
288
299
  const resolved = resolve(repoPath, p);
289
300
  const rel = relative(repoPath, resolved);
290
- if (rel.startsWith("..")) { pathsSanitized = true; continue; }
301
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
302
+ pathsSanitized = true;
303
+ continue;
304
+ }
291
305
  affectedPaths.push(p);
292
306
  }
293
307
  if (pathsSanitized) {
@@ -297,7 +311,7 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
297
311
  if (!Array.isArray(parsed.destructive_actions)) return { output: null, sanitized_reasons };
298
312
  if (parsed.destructive_actions.length > 20) return { output: null, sanitized_reasons };
299
313
  const destructiveActions = parsed.destructive_actions.filter(
300
- (d: any) => typeof d === "string" && d.length <= 200
314
+ (action): action is string => typeof action === "string" && action.length <= 200
301
315
  );
302
316
 
303
317
  if (typeof parsed.requires_user_confirm !== "boolean") return { output: null, sanitized_reasons };
@@ -320,5 +334,5 @@ function parseAssessmentJson(stdout: string, repoPath: string): ParsedAssessment
320
334
  }
321
335
 
322
336
  function writeSummary(path: string, summary: AgentAssessmentSummary): void {
323
- writeFileSync(path, JSON.stringify(summary, null, 2), "utf-8");
337
+ atomicWriteJsonFileSync(path, summary);
324
338
  }
@@ -0,0 +1,172 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getAssessmentsDir, getConfig } from "../config.js";
4
+ import { guardPath } from "../security/pathGuard.js";
5
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
6
+ import { readJsonObjectFileSync } from "../utils/lockedJsonFile.js";
7
+ import type { AssessmentValidationResult } from "./assessmentStore.js";
8
+ import { ASSESSMENT_SECURITY_SNAPSHOT_VERSION } from "./securitySnapshot.js";
9
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
10
+
11
+ const DIAGNOSTIC_VERSION = "assessment-validation-diagnostic-v2";
12
+ const DIAGNOSTIC_FILE = "last-validation-failure.json";
13
+ const VALIDATOR_MODULE_PATH = "dist/assessments/assessmentStore.js";
14
+ const VALIDATOR_BUILD_ID = `patchwarden-${PATCHWARDEN_VERSION}:${TOOL_SCHEMA_EPOCH}:${ASSESSMENT_SECURITY_SNAPSHOT_VERSION}`;
15
+
16
+ export interface AssessmentValidationFailureDiagnostic {
17
+ diagnostic_version: string;
18
+ error_code: string;
19
+ reason_code: string;
20
+ assessment_id: string | null;
21
+ assessment_config_sha256: string | null;
22
+ current_config_sha256: string | null;
23
+ assessment_config_hash: string | null;
24
+ current_config_hash: string | null;
25
+ snapshot_version: string | null;
26
+ assessment_fingerprint_version: string | null;
27
+ current_fingerprint_version: string;
28
+ assessment_schema_epoch: string | null;
29
+ current_schema_epoch: string;
30
+ assessment_created_at: string | null;
31
+ assessment_expires_at: string | null;
32
+ config_changed: boolean;
33
+ changed_field_names: string[];
34
+ changed_config_sections: string[];
35
+ validator_module_path: string;
36
+ validator_build_id: string;
37
+ watcher_instance_id: string | null;
38
+ observed_at: string;
39
+ }
40
+
41
+ export function buildAssessmentValidationFailureDiagnostic(
42
+ result: AssessmentValidationResult,
43
+ ): AssessmentValidationFailureDiagnostic | null {
44
+ if (result.valid || !result.failure_reason) return null;
45
+ const reasonCode = safeCode(result.failure_reason);
46
+ const assessmentHash = safeHash(result.expected_hash);
47
+ const currentHash = safeHash(result.actual_hash);
48
+ const changedSections = [...new Set((result.config_change_categories || [])
49
+ .map((name) => safeCode(name))
50
+ .filter(Boolean))]
51
+ .sort()
52
+ .slice(0, 32);
53
+ const assessmentVersion = safeText(result.assessment?.assessment_security_snapshot_version, 80);
54
+ return {
55
+ diagnostic_version: DIAGNOSTIC_VERSION,
56
+ error_code: reasonCode,
57
+ reason_code: reasonCode,
58
+ assessment_id: safeAssessmentId(result.assessment?.assessment_id),
59
+ assessment_config_sha256: assessmentHash,
60
+ current_config_sha256: currentHash,
61
+ assessment_config_hash: assessmentHash,
62
+ current_config_hash: currentHash,
63
+ snapshot_version: assessmentVersion,
64
+ assessment_fingerprint_version: assessmentVersion,
65
+ current_fingerprint_version: ASSESSMENT_SECURITY_SNAPSHOT_VERSION,
66
+ assessment_schema_epoch: safeText(result.assessment?.assessment_schema_epoch, 80),
67
+ current_schema_epoch: TOOL_SCHEMA_EPOCH,
68
+ assessment_created_at: safeTimestamp(result.assessment?.created_at),
69
+ assessment_expires_at: safeTimestamp(result.assessment?.expires_at),
70
+ config_changed: result.failure_reason === "assessment_stale_config",
71
+ changed_field_names: changedSections,
72
+ changed_config_sections: changedSections,
73
+ validator_module_path: VALIDATOR_MODULE_PATH,
74
+ validator_build_id: VALIDATOR_BUILD_ID,
75
+ watcher_instance_id: safeInstanceId(process.env.PATCHWARDEN_WATCHER_INSTANCE_ID),
76
+ observed_at: new Date().toISOString(),
77
+ };
78
+ }
79
+
80
+ /** Persist a bounded value-free failure summary without changing validation behavior. */
81
+ export function recordAssessmentValidationFailure(result: AssessmentValidationResult): boolean {
82
+ const diagnostic = buildAssessmentValidationFailureDiagnostic(result);
83
+ if (!diagnostic) return false;
84
+ try {
85
+ const config = getConfig();
86
+ const assessmentsDir = getAssessmentsDir(config);
87
+ const file = guardPath(
88
+ join(assessmentsDir, DIAGNOSTIC_FILE),
89
+ config.workspaceRoot,
90
+ config.assessmentsDir,
91
+ );
92
+ mkdirSync(dirname(file), { recursive: true });
93
+ atomicWriteJsonFileSync(file, diagnostic);
94
+ return true;
95
+ } catch {
96
+ // Diagnostics must never replace the original fail-closed validation error.
97
+ return false;
98
+ }
99
+ }
100
+
101
+ export function readLastAssessmentValidationFailure(): AssessmentValidationFailureDiagnostic | null {
102
+ try {
103
+ const config = getConfig();
104
+ const file = guardPath(
105
+ join(getAssessmentsDir(config), DIAGNOSTIC_FILE),
106
+ config.workspaceRoot,
107
+ config.assessmentsDir,
108
+ );
109
+ if (!existsSync(file)) return null;
110
+ const raw = readJsonObjectFileSync<Record<string, unknown>>(file);
111
+ if (raw.diagnostic_version !== DIAGNOSTIC_VERSION) return null;
112
+ const changedSections = Array.isArray(raw.changed_config_sections)
113
+ ? [...new Set(raw.changed_config_sections.map(safeCode).filter(Boolean))].sort().slice(0, 32)
114
+ : [];
115
+ return {
116
+ diagnostic_version: DIAGNOSTIC_VERSION,
117
+ error_code: safeCode(raw.error_code),
118
+ reason_code: safeCode(raw.reason_code),
119
+ assessment_id: safeAssessmentId(raw.assessment_id),
120
+ assessment_config_sha256: safeHash(raw.assessment_config_sha256),
121
+ current_config_sha256: safeHash(raw.current_config_sha256),
122
+ assessment_config_hash: safeHash(raw.assessment_config_hash),
123
+ current_config_hash: safeHash(raw.current_config_hash),
124
+ snapshot_version: safeText(raw.snapshot_version, 80),
125
+ assessment_fingerprint_version: safeText(raw.assessment_fingerprint_version, 80),
126
+ current_fingerprint_version: safeText(raw.current_fingerprint_version, 80) || ASSESSMENT_SECURITY_SNAPSHOT_VERSION,
127
+ assessment_schema_epoch: safeText(raw.assessment_schema_epoch, 80),
128
+ current_schema_epoch: safeText(raw.current_schema_epoch, 80) || TOOL_SCHEMA_EPOCH,
129
+ assessment_created_at: safeTimestamp(raw.assessment_created_at),
130
+ assessment_expires_at: safeTimestamp(raw.assessment_expires_at),
131
+ config_changed: raw.config_changed === true,
132
+ changed_field_names: Array.isArray(raw.changed_field_names)
133
+ ? [...new Set(raw.changed_field_names.map(safeCode).filter(Boolean))].sort().slice(0, 32)
134
+ : [],
135
+ changed_config_sections: changedSections,
136
+ validator_module_path: safeText(raw.validator_module_path, 120) || VALIDATOR_MODULE_PATH,
137
+ validator_build_id: safeText(raw.validator_build_id, 160) || VALIDATOR_BUILD_ID,
138
+ watcher_instance_id: safeInstanceId(raw.watcher_instance_id),
139
+ observed_at: safeTimestamp(raw.observed_at) || new Date(0).toISOString(),
140
+ };
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function safeAssessmentId(value: unknown): string | null {
147
+ return typeof value === "string" && /^assessment_\d{8}_\d{6}_[0-9a-f]{32}$/.test(value) ? value : null;
148
+ }
149
+
150
+ function safeInstanceId(value: unknown): string | null {
151
+ return typeof value === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(value) ? value : null;
152
+ }
153
+
154
+ function safeCode(value: unknown): string {
155
+ return typeof value === "string" && /^[a-z0-9_]{1,80}$/.test(value) ? value : "";
156
+ }
157
+
158
+ function safeHash(value: unknown): string | null {
159
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value) ? value : null;
160
+ }
161
+
162
+ function safeText(value: unknown, maxLength: number): string | null {
163
+ return typeof value === "string" && value.length > 0
164
+ ? value.replace(/[\r\n]+/g, " ").slice(0, maxLength)
165
+ : null;
166
+ }
167
+
168
+ function safeTimestamp(value: unknown): string | null {
169
+ return typeof value === "string" && Number.isFinite(Date.parse(value))
170
+ ? new Date(value).toISOString()
171
+ : null;
172
+ }