patchwarden 1.1.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -0,0 +1,490 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { getConfig } from "../config.js";
5
+ import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
6
+ import { countRedactionsByCategory, redactSensitiveValue } from "../security/contentRedaction.js";
7
+ import { getProjectPolicySummary } from "../policy/projectPolicy.js";
8
+ import { getTaskLineage, type SafeTaskLineage } from "./taskLineage.js";
9
+ import { getLastToolCatalogSnapshot } from "./toolCatalog.js";
10
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
11
+ import { PatchWardenError } from "../errors.js";
12
+
13
+ export interface SafeEvidencePack {
14
+ evidence_pack_id: string;
15
+ lineage_id: string;
16
+ generated_at: string;
17
+ path: string;
18
+ files: {
19
+ json: string;
20
+ markdown: string;
21
+ risk: string;
22
+ verify: string;
23
+ diffstat: string;
24
+ lineage: string;
25
+ attestation: string;
26
+ redactions: string;
27
+ };
28
+ lineage: SafeTaskLineage;
29
+ policy: {
30
+ valid: boolean;
31
+ issue_count: number;
32
+ release_readiness: unknown;
33
+ };
34
+ catalog: {
35
+ server_version: string;
36
+ schema_epoch: string;
37
+ tool_profile: string | null;
38
+ tool_count: number | null;
39
+ tool_manifest_sha256: string | null;
40
+ };
41
+ omitted: string[];
42
+ next_action: string;
43
+ bounded: true;
44
+ }
45
+
46
+ export function exportTaskEvidencePack(input: { lineage_id: string; max_items?: number }): SafeEvidencePack {
47
+ const lineageId = normalizeLineageId(input.lineage_id);
48
+ const maxItems = normalizeMaxItems(input.max_items);
49
+ const config = getConfig();
50
+ const lineage = getTaskLineage(lineageId, { max_items: maxItems });
51
+ const packDir = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", lineageId);
52
+ guardWorkspacePath(packDir, config.workspaceRoot);
53
+ mkdirSync(packDir, { recursive: true });
54
+
55
+ const policySummary = readPolicySummary(lineage.repo_path);
56
+ const catalog = safeCatalog();
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
+
109
+ const safePack = redactSensitiveValue({
110
+ evidence_pack_id: `evidence_${lineageId}`,
111
+ lineage_id: lineageId,
112
+ generated_at: generatedAt,
113
+ path: packDir,
114
+ files: filePaths,
115
+ lineage,
116
+ policy: {
117
+ valid: policySummary.valid,
118
+ issue_count: policySummary.issues.length,
119
+ release_readiness: policySummary.release_readiness,
120
+ },
121
+ catalog,
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
+ ],
132
+ next_action: buildNextAction(lineage),
133
+ bounded: true,
134
+ }).value as SafeEvidencePack;
135
+
136
+ writeFileSync(safePack.files.json, JSON.stringify(safePack, null, 2) + "\n", "utf-8");
137
+ writeFileSync(safePack.files.markdown, buildEvidenceMarkdown(safePack), "utf-8");
138
+ return safePack;
139
+ }
140
+
141
+ export function listEvidencePacks(options: { max_items?: number } = {}): { evidence_packs: SafeEvidencePack[]; total: number; truncated: boolean } {
142
+ const maxItems = normalizeMaxItems(options.max_items);
143
+ const config = getConfig();
144
+ const root = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs");
145
+ guardWorkspacePath(root, config.workspaceRoot);
146
+ if (!existsSync(root)) return { evidence_packs: [], total: 0, truncated: false };
147
+ const packs = readdirSync(root, { withFileTypes: true })
148
+ .filter((entry) => entry.isDirectory())
149
+ .map((entry) => readEvidencePack(entry.name))
150
+ .filter((entry): entry is SafeEvidencePack => entry !== null)
151
+ .sort((a, b) => b.generated_at.localeCompare(a.generated_at));
152
+ return {
153
+ evidence_packs: packs.slice(0, maxItems),
154
+ total: packs.length,
155
+ truncated: packs.length > maxItems,
156
+ };
157
+ }
158
+
159
+ export function readEvidencePack(lineageId: string): SafeEvidencePack | null {
160
+ const safeLineageId = normalizeLineageId(lineageId);
161
+ const config = getConfig();
162
+ const filePath = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", safeLineageId, "evidence.json");
163
+ guardReadPath(filePath, config.workspaceRoot, ".patchwarden/evidence-packs");
164
+ if (!existsSync(filePath)) return null;
165
+ const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
166
+ return redactSensitiveValue(JSON.parse(raw)).value as SafeEvidencePack;
167
+ }
168
+
169
+ function normalizeLineageId(value: string): string {
170
+ const lineageId = String(value || "").trim();
171
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
172
+ throw new PatchWardenError(
173
+ "invalid_lineage_id",
174
+ "lineage_id may contain only letters, numbers, underscores, and hyphens.",
175
+ "Pass a lineage_id returned by run_task_loop.",
176
+ true,
177
+ { lineage_id: lineageId }
178
+ );
179
+ }
180
+ return lineageId;
181
+ }
182
+
183
+ function normalizeMaxItems(value: number | undefined): number {
184
+ if (value === undefined) return 12;
185
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
186
+ throw new Error("max_items must be an integer from 1 to 50.");
187
+ }
188
+ return value;
189
+ }
190
+
191
+ function readPolicySummary(repoPath: string): { valid: boolean; issues: unknown[]; release_readiness: unknown } {
192
+ try {
193
+ const policy = getProjectPolicySummary(repoPath || ".");
194
+ return {
195
+ valid: Boolean(policy.valid),
196
+ issues: Array.isArray(policy.issues) ? policy.issues : [],
197
+ release_readiness: policy.release_readiness || null,
198
+ };
199
+ } catch (err) {
200
+ return {
201
+ valid: false,
202
+ issues: [{ code: "policy_unavailable", message: err instanceof Error ? err.message : String(err) }],
203
+ release_readiness: null,
204
+ };
205
+ }
206
+ }
207
+
208
+ function safeCatalog(): SafeEvidencePack["catalog"] {
209
+ try {
210
+ const catalog = getLastToolCatalogSnapshot();
211
+ if (!catalog) throw new Error("catalog snapshot unavailable");
212
+ return {
213
+ server_version: catalog.server_version,
214
+ schema_epoch: catalog.schema_epoch,
215
+ tool_profile: catalog.tool_profile,
216
+ tool_count: catalog.tool_count,
217
+ tool_manifest_sha256: catalog.tool_manifest_sha256,
218
+ };
219
+ } catch {
220
+ return {
221
+ server_version: PATCHWARDEN_VERSION,
222
+ schema_epoch: TOOL_SCHEMA_EPOCH,
223
+ tool_profile: null,
224
+ tool_count: null,
225
+ tool_manifest_sha256: null,
226
+ };
227
+ }
228
+ }
229
+
230
+ function buildNextAction(lineage: SafeTaskLineage): string {
231
+ if (lineage.stop_reason === "success") return "Use this evidence pack for review or release readiness.";
232
+ return lineage.next_action || "Review lineage before exporting release evidence.";
233
+ }
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
+
446
+ function buildEvidenceMarkdown(pack: SafeEvidencePack): string {
447
+ const direct = pack.lineage.tasks.direct_sessions
448
+ .map((entry) => `- ${entry.session_id}: status=${entry.status || "unknown"}, audit=${entry.audit_decision || "unknown"}`)
449
+ .join("\n");
450
+ const rounds = pack.lineage.rounds
451
+ .map((round) => `- ${round.role} ${round.task_id}: status=${round.status}, verification=${round.verification_status}, audit=${round.audit_verdict}`)
452
+ .join("\n");
453
+ return [
454
+ "# PatchWarden Evidence Pack",
455
+ "",
456
+ `- Evidence pack: ${pack.evidence_pack_id}`,
457
+ `- Lineage: ${pack.lineage_id}`,
458
+ `- Generated: ${pack.generated_at}`,
459
+ `- Final status: ${pack.lineage.final_status}`,
460
+ `- Stop reason: ${pack.lineage.stop_reason}`,
461
+ `- Worktree: ${pack.lineage.worktree.isolation_mode} (${pack.lineage.worktree.status})`,
462
+ `- Tool manifest: ${pack.catalog.tool_manifest_sha256 || "unavailable"}`,
463
+ "",
464
+ "## Verification Rounds",
465
+ rounds || "- None.",
466
+ "",
467
+ "## Direct Evidence",
468
+ direct || "- None.",
469
+ "",
470
+ "## Policy And Release",
471
+ `- Policy valid: ${pack.policy.valid}`,
472
+ `- Policy issue count: ${pack.policy.issue_count}`,
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
+ "",
484
+ "## Omitted",
485
+ ...pack.omitted.map((item) => `- ${item}`),
486
+ "",
487
+ `Next action: ${pack.next_action}`,
488
+ "",
489
+ ].join("\n");
490
+ }
@@ -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,
@@ -1,71 +1,71 @@
1
- import { readdirSync, statSync, existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { getConfig } from "../config.js";
4
- import { guardPath } from "../security/pathGuard.js";
5
- import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
-
7
- export interface ListEntry {
8
- name: string;
9
- type: "file" | "directory";
10
- size?: number;
11
- }
12
-
13
- export interface ListWorkspaceOutput {
14
- path: string;
15
- entries: ListEntry[];
16
- }
17
-
18
- const MAX_LIST_FILES = 200;
19
-
20
- export function listWorkspace(relativePath?: string): ListWorkspaceOutput {
21
- const config = getConfig();
22
- const targetPath = relativePath
23
- ? resolve(config.workspaceRoot, relativePath)
24
- : config.workspaceRoot;
25
-
26
- const guarded = guardPath(targetPath, config.workspaceRoot);
27
-
28
- if (!existsSync(guarded)) {
29
- throw new Error(`Path not found: "${relativePath || "."}"`);
30
- }
31
-
32
- const stat = statSync(guarded);
33
- if (!stat.isDirectory()) {
34
- throw new Error(`Not a directory: "${relativePath || "."}"`);
35
- }
36
-
37
- const entries: ListEntry[] = [];
38
- const raw = readdirSync(guarded);
39
-
40
- for (const name of raw) {
41
- // Skip hidden files and sensitive paths
42
- const fullPath = resolve(guarded, name);
43
- try {
44
- guardSensitivePath(fullPath);
45
- } catch {
46
- continue; // skip sensitive files silently
47
- }
48
-
49
- try {
50
- const s = statSync(fullPath);
51
- entries.push({
52
- name,
53
- type: s.isDirectory() ? "directory" : "file",
54
- size: s.isFile() ? s.size : undefined,
55
- });
56
- } catch {
57
- // can't stat, skip
58
- }
59
-
60
- if (entries.length >= MAX_LIST_FILES) break;
61
- }
62
-
63
- return {
64
- path: relativePath || ".",
65
- entries: entries.sort((a, b) => {
66
- // directories first, then alphabetically
67
- if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
68
- return a.name.localeCompare(b.name);
69
- }),
70
- };
71
- }
1
+ import { readdirSync, statSync, existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { getConfig } from "../config.js";
4
+ import { guardPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+
7
+ export interface ListEntry {
8
+ name: string;
9
+ type: "file" | "directory";
10
+ size?: number;
11
+ }
12
+
13
+ export interface ListWorkspaceOutput {
14
+ path: string;
15
+ entries: ListEntry[];
16
+ }
17
+
18
+ const MAX_LIST_FILES = 200;
19
+
20
+ export function listWorkspace(relativePath?: string): ListWorkspaceOutput {
21
+ const config = getConfig();
22
+ const targetPath = relativePath
23
+ ? resolve(config.workspaceRoot, relativePath)
24
+ : config.workspaceRoot;
25
+
26
+ const guarded = guardPath(targetPath, config.workspaceRoot);
27
+
28
+ if (!existsSync(guarded)) {
29
+ throw new Error(`Path not found: "${relativePath || "."}"`);
30
+ }
31
+
32
+ const stat = statSync(guarded);
33
+ if (!stat.isDirectory()) {
34
+ throw new Error(`Not a directory: "${relativePath || "."}"`);
35
+ }
36
+
37
+ const entries: ListEntry[] = [];
38
+ const raw = readdirSync(guarded);
39
+
40
+ for (const name of raw) {
41
+ // Skip hidden files and sensitive paths
42
+ const fullPath = resolve(guarded, name);
43
+ try {
44
+ guardSensitivePath(fullPath);
45
+ } catch {
46
+ continue; // skip sensitive files silently
47
+ }
48
+
49
+ try {
50
+ const s = statSync(fullPath);
51
+ entries.push({
52
+ name,
53
+ type: s.isDirectory() ? "directory" : "file",
54
+ size: s.isFile() ? s.size : undefined,
55
+ });
56
+ } catch {
57
+ // can't stat, skip
58
+ }
59
+
60
+ if (entries.length >= MAX_LIST_FILES) break;
61
+ }
62
+
63
+ return {
64
+ path: relativePath || ".",
65
+ entries: entries.sort((a, b) => {
66
+ // directories first, then alphabetically
67
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
68
+ return a.name.localeCompare(b.name);
69
+ }),
70
+ };
71
+ }