patchwarden 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.en.md +43 -2
  2. package/README.md +43 -2
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2197
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/doctor.d.ts +18 -1
  44. package/dist/doctor.js +579 -348
  45. package/dist/goal/goalReport.d.ts +54 -0
  46. package/dist/goal/goalReport.js +204 -0
  47. package/dist/goal/goalStatus.d.ts +6 -0
  48. package/dist/goal/specKitImport.d.ts +63 -0
  49. package/dist/goal/specKitImport.js +220 -0
  50. package/dist/goal/subgoalSync.js +2 -1
  51. package/dist/httpServer.js +15 -12
  52. package/dist/index.js +7 -4
  53. package/dist/logging.d.ts +7 -1
  54. package/dist/logging.js +8 -0
  55. package/dist/runner/changeCapture.d.ts +3 -3
  56. package/dist/runner/changeCapture.js +63 -39
  57. package/dist/runner/cli.js +7 -6
  58. package/dist/runner/postTaskCleanup.js +26 -2
  59. package/dist/runner/runTask.js +245 -221
  60. package/dist/runner/simpleProcess.js +4 -4
  61. package/dist/runner/watch.js +17 -14
  62. package/dist/security/contentRedaction.d.ts +6 -0
  63. package/dist/security/contentRedaction.js +22 -0
  64. package/dist/smoke-test.js +257 -251
  65. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  66. package/dist/test/unit/apply-patch.test.js +225 -0
  67. package/dist/test/unit/create-task.test.d.ts +1 -0
  68. package/dist/test/unit/create-task.test.js +197 -0
  69. package/dist/test/unit/direct-guards.test.js +124 -9
  70. package/dist/test/unit/evidence-pack.test.js +95 -1
  71. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  72. package/dist/test/unit/get-task-status.test.js +174 -0
  73. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  74. package/dist/test/unit/get-task-summary.test.js +146 -0
  75. package/dist/test/unit/goal-report.test.d.ts +1 -0
  76. package/dist/test/unit/goal-report.test.js +159 -0
  77. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  78. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  79. package/dist/test/unit/path-guard.test.js +24 -0
  80. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  81. package/dist/test/unit/spec-kit-import.test.js +341 -0
  82. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  83. package/dist/test/unit/wait-for-task.test.js +144 -0
  84. package/dist/tools/auditTask.d.ts +8 -63
  85. package/dist/tools/auditTask.js +12 -8
  86. package/dist/tools/createDirectSession.d.ts +1 -1
  87. package/dist/tools/createDirectSession.js +2 -2
  88. package/dist/tools/createTask.d.ts +2 -2
  89. package/dist/tools/createTask.js +4 -4
  90. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  91. package/dist/tools/dispatch/coreDispatch.js +282 -0
  92. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  93. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  94. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  95. package/dist/tools/dispatch/directDispatch.js +115 -0
  96. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  97. package/dist/tools/dispatch/goalDispatch.js +91 -0
  98. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  99. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  100. package/dist/tools/dispatch/types.d.ts +23 -0
  101. package/dist/tools/dispatch/types.js +15 -0
  102. package/dist/tools/evidencePack.d.ts +6 -0
  103. package/dist/tools/evidencePack.js +213 -6
  104. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  105. package/dist/tools/finalizeDirectSession.js +3 -3
  106. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  107. package/dist/tools/goalSubgoalTask.js +2 -2
  108. package/dist/tools/healthCheck.js +3 -3
  109. package/dist/tools/registry.d.ts +3 -3
  110. package/dist/tools/registry.js +60 -503
  111. package/dist/tools/retryTask.d.ts +2 -2
  112. package/dist/tools/retryTask.js +2 -2
  113. package/dist/tools/runTaskLoop.js +4 -4
  114. package/dist/tools/safeViews.d.ts +2 -2
  115. package/dist/tools/safeViews.js +2 -2
  116. package/dist/tools/toolRegistry.js +22 -0
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/docs/CODE_WIKI.md +983 -0
  120. package/docs/agentseal-integration.md +150 -0
  121. package/docs/architecture.md +63 -0
  122. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  123. package/docs/dashboard-overview.md +86 -0
  124. package/docs/demo.md +8 -0
  125. package/docs/direct-session-workflow.md +98 -0
  126. package/docs/evidence-pack-schema.md +215 -0
  127. package/docs/execution-plan-2026-07-09.md +315 -0
  128. package/docs/lineage-evidence-pack-workflow.md +127 -0
  129. package/docs/mcp-inspector-testing.md +200 -0
  130. package/docs/open-source-application.md +162 -0
  131. package/docs/opencode-worker.md +151 -0
  132. package/docs/openhands-worker.md +181 -0
  133. package/docs/release-evidence.md +72 -0
  134. package/docs/roadmap-execution-and-acceptance.md +365 -0
  135. package/docs/spec-kit-integration.md +131 -0
  136. package/docs/task-safe-review-workflow.md +98 -0
  137. package/docs/threat-model.md +79 -0
  138. package/docs/user-feedback.md +40 -0
  139. package/docs/why-patchwarden.md +110 -0
  140. package/package.json +2 -2
  141. package/scripts/checks/control-center-smoke.js +356 -0
  142. package/scripts/checks/lifecycle-smoke.js +29 -23
  143. package/scripts/checks/mcp-smoke.js +3 -1
  144. package/scripts/generate-demo-gif.py +320 -0
  145. package/src/assessments/agentAssessor.ts +3 -3
  146. package/src/assessments/assessmentStore.ts +2 -2
  147. package/src/assessments/confirmCli.ts +5 -4
  148. package/src/config.ts +37 -0
  149. package/src/control/middleware/auth.ts +17 -0
  150. package/src/control/middleware/static.ts +71 -0
  151. package/src/control/routes/audit.ts +321 -0
  152. package/src/control/routes/evidence.ts +107 -0
  153. package/src/control/routes/lineage.ts +92 -0
  154. package/src/control/routes/policy.ts +81 -0
  155. package/src/control/routes/process.ts +204 -0
  156. package/src/control/routes/sessions.ts +251 -0
  157. package/src/control/routes/status.ts +325 -0
  158. package/src/control/routes/taskActions.ts +248 -0
  159. package/src/control/routes/tasks.ts +323 -0
  160. package/src/control/routes/workspace.ts +203 -0
  161. package/src/control/runtime.ts +472 -0
  162. package/src/control/server.ts +471 -0
  163. package/src/control/shared.ts +294 -0
  164. package/src/controlCenter.ts +7 -2347
  165. package/src/direct/directGuards.ts +28 -7
  166. package/src/doctor.ts +741 -481
  167. package/src/goal/goalReport.ts +271 -0
  168. package/src/goal/goalStatus.ts +6 -0
  169. package/src/goal/specKitImport.ts +355 -0
  170. package/src/goal/subgoalSync.ts +4 -2
  171. package/src/httpServer.ts +17 -14
  172. package/src/index.ts +7 -4
  173. package/src/logging.ts +10 -1
  174. package/src/runner/changeCapture.ts +70 -42
  175. package/src/runner/cli.ts +7 -6
  176. package/src/runner/postTaskCleanup.ts +26 -2
  177. package/src/runner/runTask.ts +325 -223
  178. package/src/runner/simpleProcess.ts +4 -4
  179. package/src/runner/watch.ts +17 -14
  180. package/src/security/contentRedaction.ts +29 -0
  181. package/src/smoke-test.ts +252 -250
  182. package/src/test/unit/apply-patch.test.ts +293 -0
  183. package/src/test/unit/create-task.test.ts +255 -0
  184. package/src/test/unit/direct-guards.test.ts +178 -8
  185. package/src/test/unit/evidence-pack.test.ts +110 -1
  186. package/src/test/unit/get-task-status.test.ts +203 -0
  187. package/src/test/unit/get-task-summary.test.ts +173 -0
  188. package/src/test/unit/goal-report.test.ts +189 -0
  189. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  190. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  191. package/src/test/unit/path-guard.test.ts +30 -0
  192. package/src/test/unit/spec-kit-import.test.ts +429 -0
  193. package/src/test/unit/wait-for-task.test.ts +176 -0
  194. package/src/tools/auditTask.ts +99 -59
  195. package/src/tools/createDirectSession.ts +3 -3
  196. package/src/tools/createTask.ts +7 -7
  197. package/src/tools/dispatch/coreDispatch.ts +374 -0
  198. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  199. package/src/tools/dispatch/directDispatch.ts +167 -0
  200. package/src/tools/dispatch/goalDispatch.ts +127 -0
  201. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  202. package/src/tools/dispatch/types.ts +24 -0
  203. package/src/tools/evidencePack.ts +291 -6
  204. package/src/tools/finalizeDirectSession.ts +4 -4
  205. package/src/tools/goalSubgoalTask.ts +2 -2
  206. package/src/tools/healthCheck.ts +3 -3
  207. package/src/tools/registry.ts +68 -628
  208. package/src/tools/retryTask.ts +2 -2
  209. package/src/tools/runTaskLoop.ts +4 -4
  210. package/src/tools/safeViews.ts +2 -2
  211. package/src/tools/toolRegistry.ts +22 -0
  212. package/src/version.ts +1 -1
  213. package/ui/pages/audit.html +192 -3
  214. package/ui/pages/dashboard.html +959 -44
  215. package/ui/pages/direct-sessions.html +505 -53
  216. package/ui/pages/task-detail.html +456 -438
  217. package/ui/pages/tasks.html +598 -61
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Dispatch handlers for Goal Session tools (v0.8.0+).
3
+ *
4
+ * Covers goal lifecycle, subgoal management, Spec Kit import,
5
+ * worktree merge/discard, and goal reporting.
6
+ */
7
+
8
+ import { createGoal, listGoals, readGoal, readGoalStatus } from "../../goal/goalStore.js";
9
+ import { suggestNextSubgoal } from "../../goal/goalGraph.js";
10
+ import { exportHandoff } from "../../goal/handoffExport.js";
11
+ import { acceptSubgoal, rejectSubgoal, summarizeGoalProgress } from "../../goal/goalProgress.js";
12
+ import { exportGoalReport } from "../../goal/goalReport.js";
13
+ import { importSpecKitTasks, parseSpecKitJson } from "../../goal/specKitImport.js";
14
+ import { createSubgoalTask } from "../goalSubgoalTask.js";
15
+ import { mergeWorktreeTool } from "../mergeWorktree.js";
16
+ import { discardWorktreeTool } from "../discardWorktree.js";
17
+ import type { ToolHandlerMap } from "./types.js";
18
+ import { toResult } from "./types.js";
19
+
20
+ export const goalHandlers: ToolHandlerMap = {
21
+ create_goal: async (args) => {
22
+ return toResult(
23
+ createGoal(
24
+ String(args?.repo_path ?? ""),
25
+ String(args?.title ?? ""),
26
+ String(args?.goal_description ?? ""),
27
+ ),
28
+ );
29
+ },
30
+
31
+ list_goals: async () => {
32
+ return toResult({ goals: listGoals() });
33
+ },
34
+
35
+ read_goal: async (args) => {
36
+ return toResult(readGoal(String(args?.goal_id ?? "")));
37
+ },
38
+
39
+ create_subgoal_task: async (args) => {
40
+ return toResult(
41
+ await createSubgoalTask({
42
+ goal_id: String(args?.goal_id ?? ""),
43
+ subgoal_title: String(args?.subgoal_title ?? ""),
44
+ depends_on: Array.isArray(args?.depends_on) ? args.depends_on.map(String) : undefined,
45
+ plan_id: args?.plan_id ? String(args.plan_id) : undefined,
46
+ inline_plan: args?.inline_plan ? String(args.inline_plan) : undefined,
47
+ plan_title: args?.plan_title ? String(args.plan_title) : undefined,
48
+ template: args?.template ? (String(args.template) as any) : undefined,
49
+ goal: args?.goal ? String(args.goal) : undefined,
50
+ agent: args?.agent ? String(args.agent) : undefined,
51
+ repo_path: String(args?.repo_path ?? ""),
52
+ test_command: args?.test_command ? String(args.test_command) : undefined,
53
+ verify_commands: Array.isArray(args?.verify_commands)
54
+ ? args.verify_commands.map(String)
55
+ : undefined,
56
+ timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
57
+ scope: Array.isArray(args?.scope) ? args.scope.map(String) : undefined,
58
+ forbidden: Array.isArray(args?.forbidden) ? args.forbidden.map(String) : undefined,
59
+ verification: Array.isArray(args?.verification) ? args.verification.map(String) : undefined,
60
+ done_evidence: Array.isArray(args?.done_evidence) ? args.done_evidence.map(String) : undefined,
61
+ isolate_worktree:
62
+ args?.isolate_worktree === undefined ? undefined : Boolean(args.isolate_worktree),
63
+ }),
64
+ );
65
+ },
66
+
67
+ accept_subgoal: async (args) => {
68
+ return toResult(
69
+ acceptSubgoal(String(args?.goal_id ?? ""), String(args?.subgoal_id ?? "")),
70
+ );
71
+ },
72
+
73
+ reject_subgoal: async (args) => {
74
+ return toResult(
75
+ rejectSubgoal(
76
+ String(args?.goal_id ?? ""),
77
+ String(args?.subgoal_id ?? ""),
78
+ String(args?.reason ?? ""),
79
+ ),
80
+ );
81
+ },
82
+
83
+ suggest_next_subgoal: async (args) => {
84
+ const goalStatus = readGoalStatus(String(args?.goal_id ?? ""));
85
+ return toResult(suggestNextSubgoal(goalStatus));
86
+ },
87
+
88
+ summarize_goal_progress: async (args) => {
89
+ return toResult(summarizeGoalProgress(String(args?.goal_id ?? "")));
90
+ },
91
+
92
+ export_handoff: async (args) => {
93
+ const goalId = String(args?.goal_id ?? "");
94
+ const goalStatus = readGoalStatus(goalId);
95
+ return toResult(exportHandoff(goalId, goalStatus));
96
+ },
97
+
98
+ export_goal_report: async (args) => {
99
+ const goalId = String(args?.goal_id ?? "");
100
+ return toResult(exportGoalReport(goalId));
101
+ },
102
+
103
+ import_speckit_tasks: async (args) => {
104
+ const goalId = String(args?.goal_id ?? "");
105
+ const jsonText = String(args?.spec_kit_json ?? "");
106
+ const input = parseSpecKitJson(jsonText);
107
+ return toResult(importSpecKitTasks(goalId, input));
108
+ },
109
+
110
+ merge_worktree: async (args) => {
111
+ return toResult(
112
+ mergeWorktreeTool({
113
+ worktree_id: String(args?.worktree_id ?? ""),
114
+ repo_path: String(args?.repo_path ?? ""),
115
+ }),
116
+ );
117
+ },
118
+
119
+ discard_worktree: async (args) => {
120
+ return toResult(
121
+ discardWorktreeTool({
122
+ worktree_id: String(args?.worktree_id ?? ""),
123
+ repo_path: String(args?.repo_path ?? ""),
124
+ }),
125
+ );
126
+ },
127
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Dispatch handlers for release-mode tools (v1.3.0+).
3
+ *
4
+ * Wraps releaseCheck, releasePrepare, releaseVerify, and releaseCleanup
5
+ * from releaseMode.ts.
6
+ */
7
+
8
+ import {
9
+ releaseCheck,
10
+ releasePrepare,
11
+ releaseVerify,
12
+ releaseCleanup,
13
+ } from "../releaseMode.js";
14
+ import type { ToolHandlerMap } from "./types.js";
15
+ import { toResult } from "./types.js";
16
+
17
+ export const releaseHandlers: ToolHandlerMap = {
18
+ release_check: async (args) => {
19
+ return toResult(
20
+ await releaseCheck({
21
+ repo_path: String(args?.repo_path ?? ""),
22
+ target_stage: String(args?.target_stage ?? "local_ready") as any,
23
+ package_name: args?.package_name ? String(args.package_name) : undefined,
24
+ version: args?.version ? String(args.version) : undefined,
25
+ github_repo: args?.github_repo ? String(args.github_repo) : undefined,
26
+ branch: args?.branch ? String(args.branch) : undefined,
27
+ }),
28
+ );
29
+ },
30
+
31
+ release_prepare: async (args) => {
32
+ return toResult(
33
+ releasePrepare({
34
+ repo_path: String(args?.repo_path ?? ""),
35
+ required_commands: Array.isArray(args?.required_commands)
36
+ ? args.required_commands.map(String)
37
+ : undefined,
38
+ timeout_seconds:
39
+ args?.timeout_seconds !== undefined ? Number(args.timeout_seconds) : undefined,
40
+ }),
41
+ );
42
+ },
43
+
44
+ release_verify: async (args) => {
45
+ return toResult(
46
+ await releaseVerify({
47
+ repo_path: String(args?.repo_path ?? ""),
48
+ package_name: args?.package_name ? String(args.package_name) : undefined,
49
+ version: args?.version ? String(args.version) : undefined,
50
+ github_repo: args?.github_repo ? String(args.github_repo) : undefined,
51
+ branch: args?.branch ? String(args.branch) : undefined,
52
+ }),
53
+ );
54
+ },
55
+
56
+ release_cleanup: async (args) => {
57
+ return toResult(
58
+ releaseCleanup({
59
+ repo_path: String(args?.repo_path ?? ""),
60
+ dry_run: args?.dry_run !== undefined ? Boolean(args.dry_run) : undefined,
61
+ patterns: Array.isArray(args?.patterns) ? args.patterns.map(String) : undefined,
62
+ }),
63
+ );
64
+ },
65
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared types and helpers for the dispatch map.
3
+ *
4
+ * Each domain dispatch file exports a `ToolHandlerMap` whose handlers
5
+ * directly call the existing tool functions — no logic is rewritten.
6
+ */
7
+
8
+ export type ToolHandler = (
9
+ args: Record<string, unknown> | undefined,
10
+ ) => Promise<{ content: Array<{ type: string; text: string }> }>;
11
+
12
+ export type ToolHandlerMap = Record<string, ToolHandler>;
13
+
14
+ /**
15
+ * Wrap a plain data value into the MCP CallToolResult content envelope.
16
+ * Mirrors the original `toResult` helper from registry.ts.
17
+ */
18
+ export function toResult(data: unknown): {
19
+ content: Array<{ type: string; text: string }>;
20
+ } {
21
+ return {
22
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
23
+ };
24
+ }
@@ -1,8 +1,9 @@
1
+ import { execSync } from "node:child_process";
1
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { join, resolve } from "node:path";
3
4
  import { getConfig } from "../config.js";
4
5
  import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
5
- import { redactSensitiveValue } from "../security/contentRedaction.js";
6
+ import { countRedactionsByCategory, redactSensitiveValue } from "../security/contentRedaction.js";
6
7
  import { getProjectPolicySummary } from "../policy/projectPolicy.js";
7
8
  import { getTaskLineage, type SafeTaskLineage } from "./taskLineage.js";
8
9
  import { getLastToolCatalogSnapshot } from "./toolCatalog.js";
@@ -17,6 +18,12 @@ export interface SafeEvidencePack {
17
18
  files: {
18
19
  json: string;
19
20
  markdown: string;
21
+ risk: string;
22
+ verify: string;
23
+ diffstat: string;
24
+ lineage: string;
25
+ attestation: string;
26
+ redactions: string;
20
27
  };
21
28
  lineage: SafeTaskLineage;
22
29
  policy: {
@@ -48,15 +55,63 @@ export function exportTaskEvidencePack(input: { lineage_id: string; max_items?:
48
55
  const policySummary = readPolicySummary(lineage.repo_path);
49
56
  const catalog = safeCatalog();
50
57
  const generatedAt = new Date().toISOString();
58
+
59
+ const filePaths = {
60
+ json: join(packDir, "evidence.json"),
61
+ markdown: join(packDir, "EVIDENCE.md"),
62
+ risk: join(packDir, "risk.json"),
63
+ verify: join(packDir, "verify.json"),
64
+ diffstat: join(packDir, "diffstat.json"),
65
+ lineage: join(packDir, "lineage.json"),
66
+ attestation: join(packDir, "attestation.json"),
67
+ redactions: join(packDir, "redactions.json"),
68
+ };
69
+
70
+ // Build v2 structured artifact contents (raw, before redaction).
71
+ const riskContent = buildRiskContent(lineage);
72
+ const verifyContent = buildVerifyContent(lineage);
73
+ const diffstatContent = buildDiffstatContent(lineage, config);
74
+ const lineageSummary = buildLineageSummary(lineage);
75
+ const attestationContent = buildAttestation(catalog, generatedAt);
76
+
77
+ // Count redactions across all v2 raw contents for the audit trail.
78
+ const v2RawPayloads = [
79
+ riskContent,
80
+ verifyContent,
81
+ diffstatContent,
82
+ lineageSummary,
83
+ attestationContent,
84
+ ].map((value) => JSON.stringify(value));
85
+ const aggregated = new Map<string, { category: string; reason: string; count: number }>();
86
+ for (const raw of v2RawPayloads) {
87
+ for (const entry of countRedactionsByCategory(raw)) {
88
+ const existing = aggregated.get(entry.category);
89
+ if (existing) existing.count += entry.count;
90
+ else aggregated.set(entry.category, { category: entry.category, reason: entry.reason, count: entry.count });
91
+ }
92
+ }
93
+ const redactionsList = [...aggregated.values()];
94
+ const redactionsContent = {
95
+ redactions: redactionsList,
96
+ total_redacted: redactionsList.reduce((sum, entry) => sum + entry.count, 0),
97
+ bounded: true,
98
+ note: "Only categories and counts are recorded; original secret values are never persisted.",
99
+ };
100
+
101
+ // Redact and write each v2 file.
102
+ writeFileSync(filePaths.risk, JSON.stringify(redactSensitiveValue(riskContent).value, null, 2) + "\n", "utf-8");
103
+ writeFileSync(filePaths.verify, JSON.stringify(redactSensitiveValue(verifyContent).value, null, 2) + "\n", "utf-8");
104
+ writeFileSync(filePaths.diffstat, JSON.stringify(redactSensitiveValue(diffstatContent).value, null, 2) + "\n", "utf-8");
105
+ writeFileSync(filePaths.lineage, JSON.stringify(redactSensitiveValue(lineageSummary).value, null, 2) + "\n", "utf-8");
106
+ writeFileSync(filePaths.attestation, JSON.stringify(redactSensitiveValue(attestationContent).value, null, 2) + "\n", "utf-8");
107
+ writeFileSync(filePaths.redactions, JSON.stringify(redactSensitiveValue(redactionsContent).value, null, 2) + "\n", "utf-8");
108
+
51
109
  const safePack = redactSensitiveValue({
52
110
  evidence_pack_id: `evidence_${lineageId}`,
53
111
  lineage_id: lineageId,
54
112
  generated_at: generatedAt,
55
113
  path: packDir,
56
- files: {
57
- json: join(packDir, "evidence.json"),
58
- markdown: join(packDir, "EVIDENCE.md"),
59
- },
114
+ files: filePaths,
60
115
  lineage,
61
116
  policy: {
62
117
  valid: policySummary.valid,
@@ -64,7 +119,16 @@ export function exportTaskEvidencePack(input: { lineage_id: string; max_items?:
64
119
  release_readiness: policySummary.release_readiness,
65
120
  },
66
121
  catalog,
67
- omitted: ["stdout", "stderr", "diff", "verification command logs", "full artifact markdown", "sensitive file contents"],
122
+ omitted: [
123
+ "stdout",
124
+ "stderr",
125
+ "diff",
126
+ "verification command logs",
127
+ "full artifact markdown",
128
+ "sensitive file contents",
129
+ "raw secret values (redactions.json stores only categories and counts)",
130
+ "full diff content (diffstat.json stores only file-level line counts)",
131
+ ],
68
132
  next_action: buildNextAction(lineage),
69
133
  bounded: true,
70
134
  }).value as SafeEvidencePack;
@@ -168,6 +232,217 @@ function buildNextAction(lineage: SafeTaskLineage): string {
168
232
  return lineage.next_action || "Review lineage before exporting release evidence.";
169
233
  }
170
234
 
235
+ type RiskSeverity = "high" | "medium" | "low";
236
+
237
+ interface RiskItem {
238
+ source: string;
239
+ task_id?: string;
240
+ severity: RiskSeverity;
241
+ category: string;
242
+ detail: string;
243
+ }
244
+
245
+ function buildRiskContent(lineage: SafeTaskLineage): { risks: RiskItem[]; count: number; by_severity: Record<RiskSeverity, number> } {
246
+ const risks: RiskItem[] = [];
247
+ for (const round of lineage.rounds) {
248
+ for (const check of round.fail_checks) {
249
+ risks.push({ source: "round", task_id: round.task_id, severity: "high", category: "fail_check", detail: check });
250
+ }
251
+ for (const check of round.warn_checks) {
252
+ risks.push({ source: "round", task_id: round.task_id, severity: "medium", category: "warn_check", detail: check });
253
+ }
254
+ }
255
+ for (const warning of lineage.warnings) {
256
+ risks.push({ source: "lineage", severity: "low", category: "warning", detail: warning });
257
+ }
258
+ const by_severity: Record<RiskSeverity, number> = {
259
+ high: risks.filter((risk) => risk.severity === "high").length,
260
+ medium: risks.filter((risk) => risk.severity === "medium").length,
261
+ low: risks.filter((risk) => risk.severity === "low").length,
262
+ };
263
+ return { risks, count: risks.length, by_severity };
264
+ }
265
+
266
+ interface VerifyRecord {
267
+ source: string;
268
+ task_id?: string;
269
+ role?: string;
270
+ session_id?: string;
271
+ verification_status: string;
272
+ audit_verdict: string;
273
+ passed: boolean;
274
+ command_count?: number;
275
+ passed_commands?: number;
276
+ failed_commands?: number;
277
+ }
278
+
279
+ function buildVerifyContent(lineage: SafeTaskLineage): {
280
+ records: VerifyRecord[];
281
+ count: number;
282
+ summary: { total: number; passed: number; failed: number };
283
+ latest_status: string;
284
+ overall_passed: boolean;
285
+ } {
286
+ const records: VerifyRecord[] = [];
287
+ for (const round of lineage.rounds) {
288
+ records.push({
289
+ source: "round",
290
+ task_id: round.task_id,
291
+ role: round.role,
292
+ verification_status: round.verification_status,
293
+ audit_verdict: round.audit_verdict,
294
+ passed: round.verification_status === "passed",
295
+ });
296
+ }
297
+ for (const session of lineage.tasks.direct_sessions) {
298
+ records.push({
299
+ source: "direct_session",
300
+ session_id: session.session_id,
301
+ verification_status: session.status || "unknown",
302
+ audit_verdict: session.audit_decision || "not_run",
303
+ passed: session.status === "passed",
304
+ command_count: session.command_count,
305
+ passed_commands: session.passed_commands,
306
+ failed_commands: session.failed_commands,
307
+ });
308
+ }
309
+ const passed = records.filter((record) => record.passed).length;
310
+ return {
311
+ records,
312
+ count: records.length,
313
+ summary: { total: records.length, passed, failed: records.length - passed },
314
+ latest_status: lineage.verification.latest_status,
315
+ overall_passed: lineage.verification.passed,
316
+ };
317
+ }
318
+
319
+ interface DiffstatFile {
320
+ path: string;
321
+ status: string;
322
+ additions: number;
323
+ deletions: number;
324
+ task_id: string;
325
+ }
326
+
327
+ function buildDiffstatContent(
328
+ lineage: SafeTaskLineage,
329
+ config: ReturnType<typeof getConfig>
330
+ ): { files: DiffstatFile[]; count: number; totals: { additions: number; deletions: number } } {
331
+ const taskIds = [
332
+ lineage.tasks.main,
333
+ ...lineage.tasks.fix,
334
+ ...lineage.tasks.cleanup,
335
+ ].filter((id): id is string => Boolean(id));
336
+
337
+ const files: DiffstatFile[] = [];
338
+ let totalAdditions = 0;
339
+ let totalDeletions = 0;
340
+
341
+ for (const taskId of taskIds) {
342
+ if (!/^[A-Za-z0-9_-]+$/.test(taskId)) continue;
343
+ const statsPath = resolve(config.workspaceRoot, config.tasksDir, taskId, "file-stats.json");
344
+ try {
345
+ guardWorkspacePath(statsPath, config.workspaceRoot);
346
+ if (!existsSync(statsPath)) continue;
347
+ const raw = readFileSync(statsPath, "utf-8").replace(/^\uFEFF/, "");
348
+ const stats = JSON.parse(raw) as { files?: Array<{ path?: unknown; status?: unknown; additions?: unknown; deletions?: unknown }> };
349
+ if (!Array.isArray(stats.files)) continue;
350
+ for (const file of stats.files) {
351
+ const additions = Number(file.additions) || 0;
352
+ const deletions = Number(file.deletions) || 0;
353
+ files.push({
354
+ path: String(file.path || ""),
355
+ status: String(file.status || "unknown"),
356
+ additions,
357
+ deletions,
358
+ task_id: taskId,
359
+ });
360
+ totalAdditions += additions;
361
+ totalDeletions += deletions;
362
+ }
363
+ } catch {
364
+ // Skip unreadable or malformed file-stats.json entries.
365
+ }
366
+ }
367
+
368
+ return {
369
+ files,
370
+ count: files.length,
371
+ totals: { additions: totalAdditions, deletions: totalDeletions },
372
+ };
373
+ }
374
+
375
+ function buildLineageSummary(lineage: SafeTaskLineage): {
376
+ lineage_id: string;
377
+ goal: string;
378
+ final_status: string;
379
+ stop_reason: string;
380
+ iterations_count: number;
381
+ task_counts: { main: number; fix: number; cleanup: number; direct_sessions: number };
382
+ verification: { latest_status: string; passed: boolean };
383
+ worktree: { isolation_mode: string; status: string };
384
+ agent_routing: { selected_agent: string } | null;
385
+ warnings_count: number;
386
+ errors_count: number;
387
+ truncated: boolean;
388
+ } {
389
+ return {
390
+ lineage_id: lineage.lineage_id,
391
+ goal: lineage.goal,
392
+ final_status: lineage.final_status,
393
+ stop_reason: lineage.stop_reason,
394
+ iterations_count: lineage.rounds.length,
395
+ task_counts: {
396
+ main: lineage.tasks.main ? 1 : 0,
397
+ fix: lineage.tasks.fix.length,
398
+ cleanup: lineage.tasks.cleanup.length,
399
+ direct_sessions: lineage.tasks.direct_sessions.length,
400
+ },
401
+ verification: lineage.verification,
402
+ worktree: {
403
+ isolation_mode: lineage.worktree.isolation_mode,
404
+ status: lineage.worktree.status,
405
+ },
406
+ agent_routing: lineage.agent_routing ? { selected_agent: lineage.agent_routing.selected_agent } : null,
407
+ warnings_count: lineage.warnings.length,
408
+ errors_count: lineage.errors.length,
409
+ truncated: lineage.truncated,
410
+ };
411
+ }
412
+
413
+ function readGitCommitShort(): string {
414
+ try {
415
+ return execSync("git rev-parse --short HEAD", { encoding: "utf-8", timeout: 5000 }).trim();
416
+ } catch {
417
+ return "unknown";
418
+ }
419
+ }
420
+
421
+ function buildAttestation(
422
+ catalog: SafeEvidencePack["catalog"],
423
+ generatedAt: string
424
+ ): {
425
+ patchwarden_version: string;
426
+ package_version: string;
427
+ commit: string;
428
+ node_version: string;
429
+ os: { platform: string; arch: string };
430
+ tool_profile: string | null;
431
+ schema_epoch: string;
432
+ generated_at: string;
433
+ } {
434
+ return {
435
+ patchwarden_version: PATCHWARDEN_VERSION,
436
+ package_version: PATCHWARDEN_VERSION,
437
+ commit: readGitCommitShort(),
438
+ node_version: process.version,
439
+ os: { platform: process.platform, arch: process.arch },
440
+ tool_profile: catalog.tool_profile,
441
+ schema_epoch: catalog.schema_epoch,
442
+ generated_at: generatedAt,
443
+ };
444
+ }
445
+
171
446
  function buildEvidenceMarkdown(pack: SafeEvidencePack): string {
172
447
  const direct = pack.lineage.tasks.direct_sessions
173
448
  .map((entry) => `- ${entry.session_id}: status=${entry.status || "unknown"}, audit=${entry.audit_decision || "unknown"}`)
@@ -196,6 +471,16 @@ function buildEvidenceMarkdown(pack: SafeEvidencePack): string {
196
471
  `- Policy valid: ${pack.policy.valid}`,
197
472
  `- Policy issue count: ${pack.policy.issue_count}`,
198
473
  "",
474
+ "## Evidence Pack v2 Files",
475
+ "- `evidence.json` — full bounded evidence pack (machine-readable).",
476
+ "- `EVIDENCE.md` — human-readable evidence summary.",
477
+ "- `risk.json` — aggregated risk items with severity (high/medium/low).",
478
+ "- `verify.json` — structured verification records per round and direct session.",
479
+ "- `diffstat.json` — file-level additions/deletions without full diff content.",
480
+ "- `lineage.json` — bounded lineage summary (goal, status, task counts).",
481
+ "- `attestation.json` — version, commit, Node/OS, tool profile, schema epoch.",
482
+ "- `redactions.json` — redaction categories and counts (no original secret values).",
483
+ "",
199
484
  "## Omitted",
200
485
  ...pack.omitted.map((item) => `- ${item}`),
201
486
  "",
@@ -37,9 +37,9 @@ export interface FinalizeDirectSessionOutput {
37
37
 
38
38
  // ── Main function ──────────────────────────────────────────────────
39
39
 
40
- export function finalizeDirectSession(
40
+ export async function finalizeDirectSession(
41
41
  input: FinalizeDirectSessionInput
42
- ): FinalizeDirectSessionOutput {
42
+ ): Promise<FinalizeDirectSessionOutput> {
43
43
  const { session_id } = input;
44
44
 
45
45
  // 1. Read session and guard active (not expired, not finalized)
@@ -47,10 +47,10 @@ export function finalizeDirectSession(
47
47
  guardDirectSessionActive(session);
48
48
 
49
49
  // 2. Capture after snapshot
50
- const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
50
+ const afterSnapshot = await captureRepoSnapshot(session.resolved_repo_path);
51
51
 
52
52
  // 3. Build change artifacts from before/after snapshots
53
- const changeArtifacts = buildChangeArtifacts(
53
+ const changeArtifacts = await buildChangeArtifacts(
54
54
  session.resolved_repo_path,
55
55
  session.workspace_snapshot_before,
56
56
  afterSnapshot
@@ -64,7 +64,7 @@ export interface CreateSubgoalTaskOutput {
64
64
  * @throws PatchWardenError("invalid_dependency") 当 depends_on 引用不存在的 subgoal
65
65
  * @throws PatchWardenError("invalid_execution_mode") 当 execution_mode 为 "assess_only"
66
66
  */
67
- export function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalTaskOutput {
67
+ export async function createSubgoalTask(input: CreateSubgoalTaskInput): Promise<CreateSubgoalTaskOutput> {
68
68
  // assess_only 不产生 task,无法关联到 subgoal
69
69
  if (input.execution_mode === "assess_only") {
70
70
  throw new PatchWardenError(
@@ -113,7 +113,7 @@ export function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalT
113
113
  // 3. 调用 createTask 创建关联任务(强制 execute 模式,确保返回 CreateTaskOutput)
114
114
  let taskId: string;
115
115
  try {
116
- const taskResult = createTask({
116
+ const taskResult = await createTask({
117
117
  plan_id: input.plan_id,
118
118
  inline_plan: input.inline_plan,
119
119
  plan_title: input.plan_title,
@@ -195,14 +195,14 @@ function directoryStatus(path: string, allowCreatable: boolean, createRoot?: str
195
195
  try {
196
196
  accessSync(parent, constants.R_OK | constants.W_OK);
197
197
  return { available: true, path, readable: false, writable: true, reason: "Directory will be created on first use." };
198
- } catch {}
198
+ } catch {} // probe failure handled by return value
199
199
  }
200
200
  return { available: false, path, readable: false, writable: false, reason: "Directory does not exist." };
201
201
  }
202
202
  let readable = false;
203
203
  let writable = false;
204
- try { accessSync(path, constants.R_OK); readable = true; } catch {}
205
- try { accessSync(path, constants.W_OK); writable = true; } catch {}
204
+ try { accessSync(path, constants.R_OK); readable = true; } catch {} // probe failure handled by return value
205
+ try { accessSync(path, constants.W_OK); writable = true; } catch {} // probe failure handled by return value
206
206
  return {
207
207
  available: readable && writable,
208
208
  path,