patchwarden 0.6.4 → 1.5.0

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 (254) hide show
  1. package/README.en.md +54 -20
  2. package/README.md +48 -21
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +212 -3
  8. package/dist/direct/directSessionStore.d.ts +2 -0
  9. package/dist/direct/directVerification.js +7 -0
  10. package/dist/doctor.js +32 -1
  11. package/dist/goal/acceptanceEngine.d.ts +62 -0
  12. package/dist/goal/acceptanceEngine.js +103 -0
  13. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  14. package/dist/goal/acceptanceTemplate.js +104 -0
  15. package/dist/goal/goalGraph.d.ts +58 -0
  16. package/dist/goal/goalGraph.js +189 -0
  17. package/dist/goal/goalProgress.d.ts +81 -0
  18. package/dist/goal/goalProgress.js +216 -0
  19. package/dist/goal/goalStatus.d.ts +60 -0
  20. package/dist/goal/goalStatus.js +137 -0
  21. package/dist/goal/goalStore.d.ts +79 -0
  22. package/dist/goal/goalStore.js +211 -0
  23. package/dist/goal/handoffExport.d.ts +34 -0
  24. package/dist/goal/handoffExport.js +183 -0
  25. package/dist/goal/subgoalSync.d.ts +40 -0
  26. package/dist/goal/subgoalSync.js +72 -0
  27. package/dist/goal/worktreeManager.d.ts +96 -0
  28. package/dist/goal/worktreeManager.js +292 -0
  29. package/dist/logging.d.ts +44 -0
  30. package/dist/logging.js +68 -0
  31. package/dist/policy/projectPolicy.d.ts +55 -0
  32. package/dist/policy/projectPolicy.js +286 -0
  33. package/dist/release/releaseGate.d.ts +99 -0
  34. package/dist/release/releaseGate.js +475 -0
  35. package/dist/runner/postTaskCleanup.d.ts +13 -0
  36. package/dist/runner/postTaskCleanup.js +147 -0
  37. package/dist/runner/runTask.js +50 -10
  38. package/dist/security/discoveryTokenStore.d.ts +62 -0
  39. package/dist/security/discoveryTokenStore.js +100 -0
  40. package/dist/security/toolInvocationGuard.d.ts +35 -0
  41. package/dist/security/toolInvocationGuard.js +153 -0
  42. package/dist/smoke-test.js +18 -10
  43. package/dist/taskRuntime.d.ts +17 -0
  44. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  45. package/dist/test/unit/acceptance-engine.test.js +228 -0
  46. package/dist/test/unit/agent-router.test.d.ts +1 -0
  47. package/dist/test/unit/agent-router.test.js +287 -0
  48. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  49. package/dist/test/unit/audit-checks.test.js +350 -0
  50. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  51. package/dist/test/unit/diagnose-task.test.js +457 -0
  52. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  53. package/dist/test/unit/discovery-token-store.test.js +139 -0
  54. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  55. package/dist/test/unit/evidence-pack.test.js +130 -0
  56. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  57. package/dist/test/unit/goal-graph.test.js +298 -0
  58. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  59. package/dist/test/unit/goal-progress.test.js +381 -0
  60. package/dist/test/unit/goal-status.test.d.ts +1 -0
  61. package/dist/test/unit/goal-status.test.js +215 -0
  62. package/dist/test/unit/goal-store.test.d.ts +1 -0
  63. package/dist/test/unit/goal-store.test.js +253 -0
  64. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  65. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  66. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  67. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  68. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  69. package/dist/test/unit/handoff-export.test.js +263 -0
  70. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  72. package/dist/test/unit/logging.test.js +127 -5
  73. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  74. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  75. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  76. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  77. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  78. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  79. package/dist/test/unit/release-gate.test.d.ts +1 -0
  80. package/dist/test/unit/release-gate.test.js +242 -0
  81. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  82. package/dist/test/unit/run-task-loop.test.js +380 -0
  83. package/dist/test/unit/safe-views.test.d.ts +1 -0
  84. package/dist/test/unit/safe-views.test.js +171 -0
  85. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  86. package/dist/test/unit/schema-drift-check.test.js +176 -0
  87. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  88. package/dist/test/unit/subgoal-sync.test.js +183 -0
  89. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  90. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  91. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  92. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  93. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  94. package/dist/test/unit/toolSearch.test.js +571 -0
  95. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  96. package/dist/test/unit/worktree-manager.test.js +176 -0
  97. package/dist/tools/auditTask.d.ts +103 -1
  98. package/dist/tools/auditTask.js +461 -8
  99. package/dist/tools/cancelTask.js +1 -1
  100. package/dist/tools/checkReleaseGate.d.ts +21 -0
  101. package/dist/tools/checkReleaseGate.js +22 -0
  102. package/dist/tools/createTask.d.ts +18 -2
  103. package/dist/tools/createTask.js +29 -1
  104. package/dist/tools/diagnoseTask.d.ts +45 -0
  105. package/dist/tools/diagnoseTask.js +347 -0
  106. package/dist/tools/discardWorktree.d.ts +24 -0
  107. package/dist/tools/discardWorktree.js +27 -0
  108. package/dist/tools/discoverTools.d.ts +11 -0
  109. package/dist/tools/discoverTools.js +39 -0
  110. package/dist/tools/evidencePack.d.ts +39 -0
  111. package/dist/tools/evidencePack.js +168 -0
  112. package/dist/tools/explainTool.d.ts +11 -0
  113. package/dist/tools/explainTool.js +21 -0
  114. package/dist/tools/getTaskSummary.js +2 -1
  115. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  116. package/dist/tools/goalSubgoalTask.js +110 -0
  117. package/dist/tools/healthCheck.d.ts +1 -0
  118. package/dist/tools/healthCheck.js +5 -1
  119. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  120. package/dist/tools/invokeDiscoveredTool.js +112 -0
  121. package/dist/tools/listTasks.d.ts +3 -1
  122. package/dist/tools/listTasks.js +14 -1
  123. package/dist/tools/mergeWorktree.d.ts +24 -0
  124. package/dist/tools/mergeWorktree.js +27 -0
  125. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  126. package/dist/tools/recommendAgentForTask.js +56 -0
  127. package/dist/tools/reconcileTasks.d.ts +41 -0
  128. package/dist/tools/reconcileTasks.js +352 -0
  129. package/dist/tools/registry.js +926 -2
  130. package/dist/tools/releaseMode.d.ts +50 -0
  131. package/dist/tools/releaseMode.js +370 -0
  132. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  133. package/dist/tools/runDirectVerificationBundle.js +64 -0
  134. package/dist/tools/runTaskLoop.d.ts +57 -0
  135. package/dist/tools/runTaskLoop.js +417 -0
  136. package/dist/tools/runVerification.d.ts +4 -0
  137. package/dist/tools/runVerification.js +4 -0
  138. package/dist/tools/safeStatus.d.ts +31 -1
  139. package/dist/tools/safeStatus.js +43 -1
  140. package/dist/tools/safeViews.d.ts +262 -0
  141. package/dist/tools/safeViews.js +252 -0
  142. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  143. package/dist/tools/schemaDriftCheck.js +80 -0
  144. package/dist/tools/taskLineage.d.ts +91 -0
  145. package/dist/tools/taskLineage.js +175 -0
  146. package/dist/tools/toolCatalog.d.ts +4 -3
  147. package/dist/tools/toolCatalog.js +39 -11
  148. package/dist/tools/toolRegistry.d.ts +61 -0
  149. package/dist/tools/toolRegistry.js +834 -0
  150. package/dist/tools/toolSearch.d.ts +110 -0
  151. package/dist/tools/toolSearch.js +331 -0
  152. package/dist/tools/toolUsageStats.d.ts +41 -0
  153. package/dist/tools/toolUsageStats.js +116 -0
  154. package/dist/tools/waitForTask.js +1 -0
  155. package/dist/version.d.ts +2 -2
  156. package/dist/version.js +2 -2
  157. package/dist/watcherStatus.d.ts +29 -0
  158. package/dist/watcherStatus.js +92 -1
  159. package/docs/chatgpt-usage.md +31 -0
  160. package/docs/control-center/README.md +9 -0
  161. package/package.json +3 -2
  162. package/scripts/checks/control-center-smoke.js +87 -0
  163. package/scripts/checks/control-smoke.js +2 -2
  164. package/scripts/checks/doctor-smoke.js +1 -0
  165. package/scripts/checks/lifecycle-smoke.js +13 -9
  166. package/scripts/checks/mcp-manifest-check.js +65 -6
  167. package/scripts/checks/mcp-smoke.js +60 -9
  168. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  169. package/src/agents/agentRouter.ts +149 -0
  170. package/src/config.ts +9 -3
  171. package/src/controlCenter.ts +215 -4
  172. package/src/direct/directSessionStore.ts +2 -0
  173. package/src/direct/directVerification.ts +7 -0
  174. package/src/doctor.ts +36 -1
  175. package/src/goal/acceptanceEngine.ts +160 -0
  176. package/src/goal/acceptanceTemplate.ts +121 -0
  177. package/src/goal/goalGraph.ts +225 -0
  178. package/src/goal/goalProgress.ts +301 -0
  179. package/src/goal/goalStatus.ts +234 -0
  180. package/src/goal/goalStore.ts +306 -0
  181. package/src/goal/handoffExport.ts +211 -0
  182. package/src/goal/subgoalSync.ts +82 -0
  183. package/src/goal/worktreeManager.ts +404 -0
  184. package/src/logging.ts +91 -0
  185. package/src/policy/projectPolicy.ts +344 -0
  186. package/src/release/releaseGate.ts +567 -0
  187. package/src/runner/postTaskCleanup.ts +154 -0
  188. package/src/runner/runTask.ts +49 -10
  189. package/src/security/discoveryTokenStore.ts +157 -0
  190. package/src/security/toolInvocationGuard.ts +251 -0
  191. package/src/smoke-test.ts +15 -7
  192. package/src/taskRuntime.ts +17 -0
  193. package/src/test/unit/acceptance-engine.test.ts +261 -0
  194. package/src/test/unit/agent-router.test.ts +342 -0
  195. package/src/test/unit/audit-checks.test.ts +567 -0
  196. package/src/test/unit/diagnose-task.test.ts +544 -0
  197. package/src/test/unit/discovery-token-store.test.ts +181 -0
  198. package/src/test/unit/evidence-pack.test.ts +142 -0
  199. package/src/test/unit/goal-graph.test.ts +347 -0
  200. package/src/test/unit/goal-progress.test.ts +538 -0
  201. package/src/test/unit/goal-status.test.ts +270 -0
  202. package/src/test/unit/goal-store.test.ts +318 -0
  203. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  204. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  205. package/src/test/unit/handoff-export.test.ts +295 -0
  206. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  207. package/src/test/unit/logging.test.ts +177 -5
  208. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  209. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  210. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  211. package/src/test/unit/release-gate.test.ts +314 -0
  212. package/src/test/unit/run-task-loop.test.ts +425 -0
  213. package/src/test/unit/safe-views.test.ts +184 -0
  214. package/src/test/unit/schema-drift-check.test.ts +260 -0
  215. package/src/test/unit/subgoal-sync.test.ts +236 -0
  216. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  217. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  218. package/src/test/unit/toolSearch.test.ts +756 -0
  219. package/src/test/unit/worktree-manager.test.ts +247 -0
  220. package/src/tools/auditTask.ts +831 -402
  221. package/src/tools/cancelTask.ts +1 -1
  222. package/src/tools/checkReleaseGate.ts +45 -0
  223. package/src/tools/createTask.ts +64 -6
  224. package/src/tools/diagnoseTask.ts +460 -0
  225. package/src/tools/discardWorktree.ts +42 -0
  226. package/src/tools/discoverTools.ts +51 -0
  227. package/src/tools/evidencePack.ts +205 -0
  228. package/src/tools/explainTool.ts +26 -0
  229. package/src/tools/getTaskSummary.ts +2 -1
  230. package/src/tools/goalSubgoalTask.ts +170 -0
  231. package/src/tools/healthCheck.ts +4 -1
  232. package/src/tools/invokeDiscoveredTool.ts +163 -0
  233. package/src/tools/listTasks.ts +16 -2
  234. package/src/tools/listWorkspace.ts +71 -71
  235. package/src/tools/mergeWorktree.ts +42 -0
  236. package/src/tools/recommendAgentForTask.ts +79 -0
  237. package/src/tools/reconcileTasks.ts +464 -0
  238. package/src/tools/registry.ts +1093 -73
  239. package/src/tools/releaseMode.ts +450 -0
  240. package/src/tools/runDirectVerificationBundle.ts +98 -0
  241. package/src/tools/runTaskLoop.ts +526 -0
  242. package/src/tools/runVerification.ts +8 -0
  243. package/src/tools/safeStatus.ts +73 -2
  244. package/src/tools/safeViews.ts +273 -0
  245. package/src/tools/schemaDriftCheck.ts +120 -0
  246. package/src/tools/taskLineage.ts +300 -0
  247. package/src/tools/toolCatalog.ts +40 -11
  248. package/src/tools/toolRegistry.ts +896 -0
  249. package/src/tools/toolSearch.ts +464 -0
  250. package/src/tools/toolUsageStats.ts +151 -0
  251. package/src/tools/waitForTask.ts +1 -0
  252. package/src/version.ts +2 -2
  253. package/src/watcherStatus.ts +97 -1
  254. package/ui/pages/dashboard.html +143 -2
@@ -12,7 +12,7 @@
12
12
  * Port override: PATCHWARDEN_CONTROL_PORT=<n> or --port <n>
13
13
  */
14
14
  import { createServer, get as httpGet } from "node:http";
15
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
16
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { randomUUID } from "node:crypto";
@@ -25,6 +25,10 @@ import { readWatcherStatus } from "./watcherStatus.js";
25
25
  import { redactSensitiveContent } from "./security/contentRedaction.js";
26
26
  import { guardWorkspacePath } from "./security/pathGuard.js";
27
27
  import { auditTask } from "./tools/auditTask.js";
28
+ import { getProjectPolicySummary } from "./policy/projectPolicy.js";
29
+ import { safeDirectSummary } from "./tools/safeViews.js";
30
+ import { toSafeTaskLineage } from "./tools/taskLineage.js";
31
+ import { listEvidencePacks, readEvidencePack } from "./tools/evidencePack.js";
28
32
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "./version.js";
29
33
  // ── Paths ─────────────────────────────────────────────────────────
30
34
  const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -231,6 +235,26 @@ function readJsonFileSafe(filePath) {
231
235
  return null;
232
236
  }
233
237
  }
238
+ function isPathInside(root, target) {
239
+ const rel = relative(root, target);
240
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
241
+ }
242
+ function readJsonFileSafeUnder(root, relPath) {
243
+ const base = resolve(root);
244
+ const target = resolve(base, relPath);
245
+ if (!isPathInside(base, target) || !existsSync(target))
246
+ return null;
247
+ try {
248
+ const realBase = realpathSync(base);
249
+ const realTarget = realpathSync(target);
250
+ if (!isPathInside(realBase, realTarget))
251
+ return null;
252
+ return readJsonFileSafe(realTarget);
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ }
234
258
  function readTextFileSafe(filePath) {
235
259
  if (!existsSync(filePath))
236
260
  return null;
@@ -419,6 +443,7 @@ function resolveWorkspaceRootSafe() {
419
443
  }
420
444
  const TERMINAL_TASK_STATUSES = new Set([
421
445
  "done",
446
+ "done_by_agent",
422
447
  "failed",
423
448
  "failed_verification",
424
449
  "failed_scope_violation",
@@ -901,6 +926,105 @@ function handleStaleTasks(res) {
901
926
  sendJson(res, 200, { stale_tasks: [], total: 0, reason: errorMessage(err) });
902
927
  }
903
928
  }
929
+ function handleLineages(res) {
930
+ try {
931
+ const root = join(config.workspaceRoot, ".patchwarden", "lineages");
932
+ if (!existsSync(root)) {
933
+ sendJson(res, 200, { lineages: [], total: 0, reason: null });
934
+ return;
935
+ }
936
+ const lineages = readdirSync(root, { withFileTypes: true })
937
+ .filter((entry) => entry.isDirectory())
938
+ .map((entry) => readJsonFileSafeUnder(root, join(entry.name, "lineage.json")))
939
+ .filter((entry) => entry !== null)
940
+ .map((entry) => toSafeTaskLineage(entry, 6))
941
+ .sort((a, b) => b.updated_at.localeCompare(a.updated_at))
942
+ .slice(0, 50);
943
+ sendJson(res, 200, { lineages, total: lineages.length, reason: null });
944
+ }
945
+ catch (err) {
946
+ sendJson(res, 200, { lineages: [], total: 0, reason: errorMessage(err) });
947
+ }
948
+ }
949
+ function handleLineageDetail(res, lineageId) {
950
+ try {
951
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
952
+ sendJson(res, 400, { error: "Invalid lineage id" });
953
+ return;
954
+ }
955
+ const data = readJsonFileSafeUnder(join(config.workspaceRoot, ".patchwarden", "lineages"), join(lineageId, "lineage.json"));
956
+ if (!data) {
957
+ sendJson(res, 404, { error: "Lineage not found" });
958
+ return;
959
+ }
960
+ sendJson(res, 200, toSafeTaskLineage(data, 20));
961
+ }
962
+ catch (err) {
963
+ sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err) });
964
+ }
965
+ }
966
+ function handleProjectPolicy(res, repoPath) {
967
+ try {
968
+ sendJson(res, 200, getProjectPolicySummary(repoPath || "."));
969
+ }
970
+ catch (err) {
971
+ sendJson(res, 200, {
972
+ repo_path: repoPath || ".",
973
+ valid: false,
974
+ issues: [{ code: "policy_unavailable", severity: "error", field: "repo_path", message: errorMessage(err) }],
975
+ });
976
+ }
977
+ }
978
+ function handleReleaseStatus(res, repoPath) {
979
+ try {
980
+ const policy = getProjectPolicySummary(repoPath || ".");
981
+ sendJson(res, 200, {
982
+ repo_path: repoPath || ".",
983
+ resolved_repo_path: policy.resolved_repo_path,
984
+ policy_valid: policy.valid,
985
+ policy_issue_count: policy.issues.length,
986
+ policy_issues: policy.issues.slice(0, 10),
987
+ release_readiness: policy.release_readiness,
988
+ next_action: policy.valid && policy.release_readiness.version_consistent !== false
989
+ ? "Run release_check or release_prepare from the full MCP profile when ready."
990
+ : "Fix project-policy or version consistency issues before release preparation.",
991
+ remote_write_performed: false,
992
+ });
993
+ }
994
+ catch (err) {
995
+ sendJson(res, 200, {
996
+ repo_path: repoPath || ".",
997
+ policy_valid: false,
998
+ error: errorMessage(err),
999
+ remote_write_performed: false,
1000
+ });
1001
+ }
1002
+ }
1003
+ function handleEvidencePacks(res) {
1004
+ try {
1005
+ sendJson(res, 200, listEvidencePacks({ max_items: 50 }));
1006
+ }
1007
+ catch (err) {
1008
+ sendJson(res, 200, { evidence_packs: [], total: 0, reason: errorMessage(err) });
1009
+ }
1010
+ }
1011
+ function handleEvidencePackDetail(res, lineageId) {
1012
+ try {
1013
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
1014
+ sendJson(res, 400, { error: "Invalid lineage id" });
1015
+ return;
1016
+ }
1017
+ const pack = readEvidencePack(lineageId);
1018
+ if (!pack) {
1019
+ sendJson(res, 404, { error: "Evidence pack not found" });
1020
+ return;
1021
+ }
1022
+ sendJson(res, 200, pack);
1023
+ }
1024
+ catch (err) {
1025
+ sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err), bounded: true });
1026
+ }
1027
+ }
904
1028
  /**
905
1029
  * Reconcile a stale task. Does NOT delete the task. Reads the task files,
906
1030
  * decides whether it is safe to mark the task as stale/archived, writes a
@@ -928,13 +1052,19 @@ function handleReconcile(res, taskId) {
928
1052
  const statusData = readJsonFileSafe(statusPath) ?? {};
929
1053
  const runtimeData = readJsonFileSafe(runtimePath) ?? {};
930
1054
  const watcher = readWatcherStatusSafe();
1055
+ const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
1056
+ const taskStatus = String(statusData.status || "pending");
1057
+ const taskAcceptanceStatus = taskStatus === "done_by_agent"
1058
+ ? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE.includes(statusData.acceptance_status) ? statusData.acceptance_status : "pending")
1059
+ : null;
931
1060
  const taskEntry = {
932
1061
  task_id: taskId,
933
1062
  plan_id: String(statusData.plan_id || ""),
934
1063
  title: "",
935
1064
  agent: String(statusData.agent || ""),
936
- status: String(statusData.status || "pending"),
1065
+ status: taskStatus,
937
1066
  phase: String(runtimeData.phase || statusData.phase || "queued"),
1067
+ acceptance_status: taskAcceptanceStatus,
938
1068
  created_at: String(statusData.created_at || ""),
939
1069
  updated_at: String(statusData.updated_at || ""),
940
1070
  workspace_root: String(statusData.workspace_root || config.workspaceRoot),
@@ -1072,13 +1202,19 @@ function handleTaskDetail(res, taskId) {
1072
1202
  let stale = null;
1073
1203
  if (statusData) {
1074
1204
  const watcher = readWatcherStatusSafe();
1205
+ const VALID_ACCEPTANCE2 = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
1206
+ const taskStatus2 = String(statusData.status || "pending");
1207
+ const taskAcceptanceStatus2 = taskStatus2 === "done_by_agent"
1208
+ ? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE2.includes(statusData.acceptance_status) ? statusData.acceptance_status : "pending")
1209
+ : null;
1075
1210
  const entry = {
1076
1211
  task_id: taskId,
1077
1212
  plan_id: String(statusData.plan_id || ""),
1078
1213
  title: "",
1079
1214
  agent: String(statusData.agent || ""),
1080
- status: String(statusData.status || "pending"),
1215
+ status: taskStatus2,
1081
1216
  phase: String(runtimeData?.phase || statusData.phase || "queued"),
1217
+ acceptance_status: taskAcceptanceStatus2,
1082
1218
  created_at: String(statusData.created_at || ""),
1083
1219
  updated_at: String(statusData.updated_at || ""),
1084
1220
  workspace_root: String(statusData.workspace_root || config.workspaceRoot),
@@ -1485,6 +1621,27 @@ function handleDirectSessionDetail(res, sessionId) {
1485
1621
  sendJson(res, 200, { session_id: sessionId, error: errorMessage(err) });
1486
1622
  }
1487
1623
  }
1624
+ function handleDirectSessionSafeSummary(res, sessionId) {
1625
+ try {
1626
+ if (sessionId === "." ||
1627
+ sessionId === ".." ||
1628
+ sessionId.includes("/") ||
1629
+ sessionId.includes("\\") ||
1630
+ sessionId.includes("\0")) {
1631
+ sendJson(res, 400, { error: "Invalid session id" });
1632
+ return;
1633
+ }
1634
+ sendJson(res, 200, safeDirectSummary(sessionId, { max_items: 12 }));
1635
+ }
1636
+ catch (err) {
1637
+ sendJson(res, 200, {
1638
+ session_id: sessionId,
1639
+ error: errorMessage(err),
1640
+ large_logs_omitted: true,
1641
+ diff_omitted: true,
1642
+ });
1643
+ }
1644
+ }
1488
1645
  function parseReviewVerdict(content) {
1489
1646
  // independent-review.md format: "**Verdict**: PASS" (case-insensitive)
1490
1647
  const m = content.match(/\*\*Verdict\*\*\s*:\s*([A-Za-z]+)/);
@@ -1774,6 +1931,46 @@ async function handleRequest(req, res) {
1774
1931
  handleStaleTasks(res);
1775
1932
  return;
1776
1933
  }
1934
+ if (method === "GET" && pathname === "/api/lineages") {
1935
+ handleLineages(res);
1936
+ return;
1937
+ }
1938
+ const lineageMatch = pathname.match(/^\/api\/lineages\/([^/]+)$/);
1939
+ if (method === "GET" && lineageMatch) {
1940
+ let lineageId;
1941
+ try {
1942
+ lineageId = decodeURIComponent(lineageMatch[1]);
1943
+ }
1944
+ catch {
1945
+ lineageId = lineageMatch[1];
1946
+ }
1947
+ handleLineageDetail(res, lineageId);
1948
+ return;
1949
+ }
1950
+ if (method === "GET" && pathname === "/api/project-policy") {
1951
+ handleProjectPolicy(res, parsedUrl.searchParams.get("repo_path") || ".");
1952
+ return;
1953
+ }
1954
+ if (method === "GET" && pathname === "/api/release/status") {
1955
+ handleReleaseStatus(res, parsedUrl.searchParams.get("repo_path") || ".");
1956
+ return;
1957
+ }
1958
+ if (method === "GET" && pathname === "/api/evidence-packs") {
1959
+ handleEvidencePacks(res);
1960
+ return;
1961
+ }
1962
+ const evidencePackMatch = pathname.match(/^\/api\/evidence-packs\/([^/]+)$/);
1963
+ if (method === "GET" && evidencePackMatch) {
1964
+ let lineageId;
1965
+ try {
1966
+ lineageId = decodeURIComponent(evidencePackMatch[1]);
1967
+ }
1968
+ catch {
1969
+ lineageId = evidencePackMatch[1];
1970
+ }
1971
+ handleEvidencePackDetail(res, lineageId);
1972
+ return;
1973
+ }
1777
1974
  const taskMatch = pathname.match(/^\/api\/tasks\/([^/]+)$/);
1778
1975
  if (method === "GET" && taskMatch) {
1779
1976
  let taskId;
@@ -1832,6 +2029,18 @@ async function handleRequest(req, res) {
1832
2029
  handleDirectSessions(res);
1833
2030
  return;
1834
2031
  }
2032
+ const directSessionSummaryMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)\/summary$/);
2033
+ if (method === "GET" && directSessionSummaryMatch) {
2034
+ let sessionId;
2035
+ try {
2036
+ sessionId = decodeURIComponent(directSessionSummaryMatch[1]);
2037
+ }
2038
+ catch {
2039
+ sessionId = directSessionSummaryMatch[1];
2040
+ }
2041
+ handleDirectSessionSafeSummary(res, sessionId);
2042
+ return;
2043
+ }
1835
2044
  const directSessionMatch = pathname.match(/^\/api\/direct-sessions\/([^/]+)$/);
1836
2045
  if (method === "GET" && directSessionMatch) {
1837
2046
  let sessionId;
@@ -13,6 +13,8 @@ export interface DirectSessionVerificationRun {
13
13
  exit_code: number | null;
14
14
  passed: boolean;
15
15
  timed_out: boolean;
16
+ redacted?: boolean;
17
+ redaction_categories?: string[];
16
18
  stdout_tail: string;
17
19
  stderr_tail: string;
18
20
  started_at: string;
@@ -40,6 +40,13 @@ export async function runDirectVerification(input) {
40
40
  exit_code: result.exitCode,
41
41
  passed,
42
42
  timed_out: result.timedOut,
43
+ redacted: stdoutRedacted.redacted || stderrRedacted.redacted,
44
+ redaction_categories: [
45
+ ...new Set([
46
+ ...stdoutRedacted.redaction_categories,
47
+ ...stderrRedacted.redaction_categories,
48
+ ]),
49
+ ],
43
50
  stdout_tail: stdoutTail,
44
51
  stderr_tail: stderrTail,
45
52
  started_at: startedAt,
package/dist/doctor.js CHANGED
@@ -18,6 +18,9 @@ import { guardPlanContent } from "./security/planGuard.js";
18
18
  import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
19
19
  import { getToolDefs } from "./tools/registry.js";
20
20
  import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
21
+ import { buildToolRegistry } from "./tools/toolRegistry.js";
22
+ import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
23
+ import { runReleaseGateCheck } from "./release/releaseGate.js";
21
24
  import { PATCHWARDEN_VERSION } from "./version.js";
22
25
  // ── State ──────────────────────────────────────────────────────────
23
26
  let ok = 0;
@@ -218,7 +221,7 @@ async function main() {
218
221
  const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
219
222
  const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
220
223
  const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
221
- check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
224
+ check("Full tool profile exposes 64 tools", fullTools.length === 64, `${fullTools.length} tools`);
222
225
  check(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", "));
223
226
  check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
224
227
  createSchema?.properties?.verify_commands &&
@@ -235,6 +238,25 @@ async function main() {
235
238
  results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
236
239
  ok++;
237
240
  }
241
+ // 16. Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
242
+ // 用 fullTools 同时构建 registry 和 toolDefs Map,确保 schema digest 比对基准一致。
243
+ const driftRegistry = buildToolRegistry(fullTools);
244
+ const driftToolDefs = new Map();
245
+ for (const tool of fullTools) {
246
+ driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
247
+ }
248
+ const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
249
+ if (driftResult.ok) {
250
+ results.push(`[OK] Schema drift check — no drift detected`);
251
+ ok++;
252
+ }
253
+ else {
254
+ // 每个 warning 单独输出为 WARN,与其他自检项的多警告模式一致
255
+ for (const w of driftResult.warnings) {
256
+ results.push(`[WARN] Schema drift: ${w}`);
257
+ warn++;
258
+ }
259
+ }
238
260
  }
239
261
  finally {
240
262
  if (previousProfile === undefined)
@@ -242,6 +264,15 @@ async function main() {
242
264
  else
243
265
  process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
244
266
  }
267
+ // 17. Release gate module loadable (v1.0.0) — module integrity only.
268
+ // Does NOT execute local_ready (would recurse into doctor:ci).
269
+ try {
270
+ const releaseGateReady = typeof runReleaseGateCheck === "function";
271
+ check("Release gate module loadable", releaseGateReady, releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid");
272
+ }
273
+ catch (error) {
274
+ check("Release gate module loadable", false, error instanceof Error ? error.message : String(error));
275
+ }
245
276
  // 9. HTTP port check
246
277
  const httpPort = config?.http?.port || 7331;
247
278
  try {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * v0.7.2: 验收引擎 — 基于证据推导 4 级验收结论。
3
+ *
4
+ * 输入:status.json、task_meta(goal/scope/forbidden/verification/done_evidence)、
5
+ * audit checks(pass/warn/fail)、release claims、artifact 信息
6
+ * 输出:ACCEPTED / NEEDS_FIX / REJECTED / BLOCKED_BY_APPROVAL
7
+ * + reasons + required_evidence + next_suggested_task
8
+ *
9
+ * 决策规则:
10
+ * 1. 存在 fail 级检查项 → REJECTED
11
+ * 2. 存在 release claims 但无法验证远端 → BLOCKED_BY_APPROVAL
12
+ * 3. 存在 warn 级检查项或文档不同步 → NEEDS_FIX
13
+ * 4. 所有检查通过 → ACCEPTED
14
+ */
15
+ import type { AcceptanceStatus } from "../tools/createTask.js";
16
+ export type AcceptanceVerdict = "ACCEPTED" | "NEEDS_FIX" | "REJECTED" | "BLOCKED_BY_APPROVAL";
17
+ export interface AcceptanceCheck {
18
+ name: string;
19
+ result: "pass" | "warn" | "fail";
20
+ detail: string;
21
+ }
22
+ export interface AcceptanceEvidence {
23
+ task_id: string;
24
+ task_status: string;
25
+ result_md_exists: boolean;
26
+ result_json_exists: boolean;
27
+ verify_json_exists: boolean;
28
+ test_log_exists: boolean;
29
+ git_diff_exists: boolean;
30
+ verify_status: "passed" | "failed" | "skipped" | null;
31
+ new_out_of_scope_changes: Array<{
32
+ path: string;
33
+ change: string;
34
+ }>;
35
+ goal: string | null;
36
+ scope: string[] | null;
37
+ forbidden: string[] | null;
38
+ verification: string[] | null;
39
+ done_evidence: string[] | null;
40
+ artifact_status: string | null;
41
+ release_claims_unverified: boolean;
42
+ checks: AcceptanceCheck[];
43
+ }
44
+ export interface AcceptanceResult {
45
+ verdict: AcceptanceVerdict;
46
+ acceptance_status: AcceptanceStatus;
47
+ reason: string;
48
+ reasons: string[];
49
+ required_evidence: string[];
50
+ next_suggested_task: string;
51
+ fail_checks: AcceptanceCheck[];
52
+ warn_checks: AcceptanceCheck[];
53
+ }
54
+ /**
55
+ * 核心验收决策函数。
56
+ * 输入所有证据,输出 4 级验收结论。
57
+ */
58
+ export declare function evaluateAcceptance(evidence: AcceptanceEvidence): AcceptanceResult;
59
+ /**
60
+ * 将 AcceptanceVerdict 映射到 AcceptanceStatus(用于回写 status.json)。
61
+ */
62
+ export declare function verdictToStatus(verdict: AcceptanceVerdict): AcceptanceStatus;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * v0.7.2: 验收引擎 — 基于证据推导 4 级验收结论。
3
+ *
4
+ * 输入:status.json、task_meta(goal/scope/forbidden/verification/done_evidence)、
5
+ * audit checks(pass/warn/fail)、release claims、artifact 信息
6
+ * 输出:ACCEPTED / NEEDS_FIX / REJECTED / BLOCKED_BY_APPROVAL
7
+ * + reasons + required_evidence + next_suggested_task
8
+ *
9
+ * 决策规则:
10
+ * 1. 存在 fail 级检查项 → REJECTED
11
+ * 2. 存在 release claims 但无法验证远端 → BLOCKED_BY_APPROVAL
12
+ * 3. 存在 warn 级检查项或文档不同步 → NEEDS_FIX
13
+ * 4. 所有检查通过 → ACCEPTED
14
+ */
15
+ // ── 验收决策 ──────────────────────────────────────────────────────
16
+ /**
17
+ * 核心验收决策函数。
18
+ * 输入所有证据,输出 4 级验收结论。
19
+ */
20
+ export function evaluateAcceptance(evidence) {
21
+ const reasons = [];
22
+ const requiredEvidence = [];
23
+ const failChecks = [];
24
+ const warnChecks = [];
25
+ // 收集 fail 和 warn 检查项
26
+ for (const check of evidence.checks) {
27
+ if (check.result === "fail") {
28
+ failChecks.push(check);
29
+ }
30
+ else if (check.result === "warn") {
31
+ warnChecks.push(check);
32
+ }
33
+ }
34
+ // ── Rule 1: fail 级检查项 → REJECTED ──────────────────────
35
+ if (failChecks.length > 0) {
36
+ for (const check of failChecks) {
37
+ reasons.push(`[FAIL] ${check.name}: ${check.detail}`);
38
+ }
39
+ return {
40
+ verdict: "REJECTED",
41
+ acceptance_status: "rejected",
42
+ reason: `${failChecks.length} fail-level check(s): ${failChecks.map((c) => c.name).join(", ")}`,
43
+ reasons,
44
+ required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Fix all fail-level issues and re-run audit_task"],
45
+ next_suggested_task: `Fix fail-level issues: ${failChecks.map((c) => c.name).join(", ")}`,
46
+ fail_checks: failChecks,
47
+ warn_checks: warnChecks,
48
+ };
49
+ }
50
+ // ── Rule 2: release claims 无法验证 → BLOCKED_BY_APPROVAL ──
51
+ if (evidence.release_claims_unverified) {
52
+ reasons.push("[BLOCKED] Release claims detected but remote state cannot be verified");
53
+ requiredEvidence.push("Verify remote release state (npm publish, GitHub release, git tag)");
54
+ return {
55
+ verdict: "BLOCKED_BY_APPROVAL",
56
+ acceptance_status: "blocked",
57
+ reason: "Release claims require manual verification of remote state",
58
+ reasons,
59
+ required_evidence: requiredEvidence,
60
+ next_suggested_task: "Manually verify remote release state, then re-run audit_task",
61
+ fail_checks: failChecks,
62
+ warn_checks: warnChecks,
63
+ };
64
+ }
65
+ // ── Rule 3: warn 级检查项 → NEEDS_FIX ──────────────────────
66
+ if (warnChecks.length > 0) {
67
+ for (const check of warnChecks) {
68
+ reasons.push(`[WARN] ${check.name}: ${check.detail}`);
69
+ }
70
+ return {
71
+ verdict: "NEEDS_FIX",
72
+ acceptance_status: "needs_fix",
73
+ reason: `${warnChecks.length} warn-level check(s) require fixes: ${warnChecks.map((c) => c.name).join(", ")}`,
74
+ reasons,
75
+ required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Resolve all warn-level issues"],
76
+ next_suggested_task: `Fix warn-level issues: ${warnChecks.map((c) => c.name).join(", ")}`,
77
+ fail_checks: failChecks,
78
+ warn_checks: warnChecks,
79
+ };
80
+ }
81
+ // ── Rule 4: 所有检查通过 → ACCEPTED ───────────────────────
82
+ return {
83
+ verdict: "ACCEPTED",
84
+ acceptance_status: "accepted",
85
+ reason: "All acceptance checks passed",
86
+ reasons: ["All checks passed, no warnings or failures"],
87
+ required_evidence: [],
88
+ next_suggested_task: "Task accepted. Proceed to next goal or task.",
89
+ fail_checks: failChecks,
90
+ warn_checks: warnChecks,
91
+ };
92
+ }
93
+ /**
94
+ * 将 AcceptanceVerdict 映射到 AcceptanceStatus(用于回写 status.json)。
95
+ */
96
+ export function verdictToStatus(verdict) {
97
+ switch (verdict) {
98
+ case "ACCEPTED": return "accepted";
99
+ case "REJECTED": return "rejected";
100
+ case "NEEDS_FIX": return "needs_fix";
101
+ case "BLOCKED_BY_APPROVAL": return "blocked";
102
+ }
103
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * v0.7.2: ACCEPTANCE.md 模板生成 — 人类可读的验收报告导出。
3
+ *
4
+ * 将 AcceptanceResult 渲染为 Markdown,落盘到 .patchwarden/tasks/{task_id}/ACCEPTANCE.md。
5
+ */
6
+ import type { AcceptanceResult, AcceptanceEvidence } from "./acceptanceEngine.js";
7
+ /**
8
+ * 生成 ACCEPTANCE.md 内容。
9
+ */
10
+ export declare function renderAcceptanceMarkdown(taskId: string, result: AcceptanceResult, evidence: AcceptanceEvidence): string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * v0.7.2: ACCEPTANCE.md 模板生成 — 人类可读的验收报告导出。
3
+ *
4
+ * 将 AcceptanceResult 渲染为 Markdown,落盘到 .patchwarden/tasks/{task_id}/ACCEPTANCE.md。
5
+ */
6
+ import { PATCHWARDEN_VERSION } from "../version.js";
7
+ /**
8
+ * 生成 ACCEPTANCE.md 内容。
9
+ */
10
+ export function renderAcceptanceMarkdown(taskId, result, evidence) {
11
+ const lines = [];
12
+ const now = new Date().toISOString();
13
+ lines.push("# Acceptance Report — " + taskId);
14
+ lines.push("");
15
+ lines.push("Generated: " + now);
16
+ lines.push("");
17
+ // ── Verdict ──────────────────────────────────────────────
18
+ lines.push("## Verdict");
19
+ lines.push("");
20
+ lines.push("| Field | Value |");
21
+ lines.push("| --- | --- |");
22
+ lines.push("| Verdict | **" + result.verdict + "** |");
23
+ lines.push("| Acceptance Status | " + (result.acceptance_status ?? "null") + " |");
24
+ lines.push("| Reason | " + result.reason + " |");
25
+ lines.push("| Next Suggested Task | " + result.next_suggested_task + " |");
26
+ lines.push("");
27
+ // ── Evidence Summary ────────────────────────────────────
28
+ lines.push("## Evidence Summary");
29
+ lines.push("");
30
+ lines.push("| Evidence | Present |");
31
+ lines.push("| --- | --- |");
32
+ lines.push("| result.md | " + (evidence.result_md_exists ? "YES" : "NO") + " |");
33
+ lines.push("| result.json | " + (evidence.result_json_exists ? "YES" : "NO") + " |");
34
+ lines.push("| verify.json | " + (evidence.verify_json_exists ? "YES" : "NO") + " |");
35
+ lines.push("| test.log | " + (evidence.test_log_exists ? "YES" : "NO") + " |");
36
+ lines.push("| git.diff | " + (evidence.git_diff_exists ? "YES" : "NO") + " |");
37
+ lines.push("| verify_status | " + (evidence.verify_status ?? "N/A") + " |");
38
+ lines.push("| artifact_status | " + (evidence.artifact_status ?? "N/A") + " |");
39
+ lines.push("| out_of_scope_changes | " + evidence.new_out_of_scope_changes.length + " |");
40
+ lines.push("| release_claims_unverified | " + (evidence.release_claims_unverified ? "YES" : "NO") + " |");
41
+ lines.push("");
42
+ // ── Task Metadata ────────────────────────────────────────
43
+ if (evidence.goal || evidence.scope || evidence.forbidden || evidence.verification || evidence.done_evidence) {
44
+ lines.push("## Task Acceptance Criteria");
45
+ lines.push("");
46
+ if (evidence.goal) {
47
+ lines.push("- **Goal**: " + evidence.goal);
48
+ }
49
+ if (evidence.scope && evidence.scope.length > 0) {
50
+ lines.push("- **Scope**: " + evidence.scope.join(", "));
51
+ }
52
+ if (evidence.forbidden && evidence.forbidden.length > 0) {
53
+ lines.push("- **Forbidden**: " + evidence.forbidden.join(", "));
54
+ }
55
+ if (evidence.verification && evidence.verification.length > 0) {
56
+ lines.push("- **Verification**: " + evidence.verification.join(", "));
57
+ }
58
+ if (evidence.done_evidence && evidence.done_evidence.length > 0) {
59
+ lines.push("- **Done Evidence**: " + evidence.done_evidence.join(", "));
60
+ }
61
+ lines.push("");
62
+ }
63
+ // ── Fail Checks ─────────────────────────────────────────
64
+ if (result.fail_checks.length > 0) {
65
+ lines.push("## Fail-Level Checks");
66
+ lines.push("");
67
+ for (const check of result.fail_checks) {
68
+ lines.push("- **" + check.name + "**: " + check.detail);
69
+ }
70
+ lines.push("");
71
+ }
72
+ // ── Warn Checks ─────────────────────────────────────────
73
+ if (result.warn_checks.length > 0) {
74
+ lines.push("## Warn-Level Checks");
75
+ lines.push("");
76
+ for (const check of result.warn_checks) {
77
+ lines.push("- **" + check.name + "**: " + check.detail);
78
+ }
79
+ lines.push("");
80
+ }
81
+ // ── Reasons ─────────────────────────────────────────────
82
+ if (result.reasons.length > 0) {
83
+ lines.push("## Reasons");
84
+ lines.push("");
85
+ for (const reason of result.reasons) {
86
+ lines.push("- " + reason);
87
+ }
88
+ lines.push("");
89
+ }
90
+ // ── Required Evidence ────────────────────────────────────
91
+ if (result.required_evidence.length > 0) {
92
+ lines.push("## Required Evidence");
93
+ lines.push("");
94
+ for (const reqEvidence of result.required_evidence) {
95
+ lines.push("- " + reqEvidence);
96
+ }
97
+ lines.push("");
98
+ }
99
+ lines.push("---");
100
+ lines.push("");
101
+ lines.push("This report was generated by PatchWarden v" + PATCHWARDEN_VERSION + " acceptanceEngine.");
102
+ lines.push("");
103
+ return lines.join("\n");
104
+ }