patchwarden 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
// ── Type definitions ──────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -12,14 +12,28 @@ export interface PatchWardenConfig {
|
|
|
12
12
|
workspaceRoot: string;
|
|
13
13
|
plansDir: string;
|
|
14
14
|
tasksDir: string;
|
|
15
|
+
assessmentsDir: string;
|
|
16
|
+
assessmentTtlSeconds: number;
|
|
15
17
|
agents: Record<string, AgentConfig>;
|
|
16
18
|
allowedTestCommands: string[];
|
|
19
|
+
repoAllowedTestCommands?: Record<string, string[]>;
|
|
17
20
|
maxReadFileBytes: number;
|
|
18
21
|
defaultTaskTimeoutSeconds: number;
|
|
19
22
|
maxTaskTimeoutSeconds: number;
|
|
20
23
|
watcherStaleSeconds: number;
|
|
21
|
-
toolProfile?: "full" | "chatgpt_core";
|
|
24
|
+
toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct";
|
|
22
25
|
repoAliases?: Record<string, string>;
|
|
26
|
+
enableAgentAssessment?: boolean;
|
|
27
|
+
agentAssessmentTimeoutSeconds?: number;
|
|
28
|
+
agentAssessmentMaxOutputBytes?: number;
|
|
29
|
+
agentAssessmentAgentName?: string;
|
|
30
|
+
enableDirectProfile?: boolean;
|
|
31
|
+
directSessionsDir: string;
|
|
32
|
+
directSessionTtlSeconds: number;
|
|
33
|
+
directMaxPatchBytes: number;
|
|
34
|
+
directMaxFileBytes: number;
|
|
35
|
+
directAllowedCommands?: string[];
|
|
36
|
+
repoDirectAllowedCommands?: Record<string, string[]>;
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
// ── Defaults ──────────────────────────────────────────────────────
|
|
@@ -28,6 +42,8 @@ const DEFAULT_CONFIG: PatchWardenConfig = {
|
|
|
28
42
|
workspaceRoot: process.cwd(),
|
|
29
43
|
plansDir: ".patchwarden/plans",
|
|
30
44
|
tasksDir: ".patchwarden/tasks",
|
|
45
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
46
|
+
assessmentTtlSeconds: 3600,
|
|
31
47
|
agents: {
|
|
32
48
|
codex: {
|
|
33
49
|
command: "codex",
|
|
@@ -56,11 +72,28 @@ const DEFAULT_CONFIG: PatchWardenConfig = {
|
|
|
56
72
|
"pytest",
|
|
57
73
|
"cargo test",
|
|
58
74
|
],
|
|
75
|
+
repoAllowedTestCommands: {},
|
|
59
76
|
maxReadFileBytes: 200_000,
|
|
60
77
|
defaultTaskTimeoutSeconds: 900,
|
|
61
78
|
maxTaskTimeoutSeconds: 3600,
|
|
62
79
|
watcherStaleSeconds: 30,
|
|
63
80
|
toolProfile: "full",
|
|
81
|
+
enableAgentAssessment: false,
|
|
82
|
+
agentAssessmentTimeoutSeconds: 120,
|
|
83
|
+
agentAssessmentMaxOutputBytes: 524288,
|
|
84
|
+
enableDirectProfile: false,
|
|
85
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
86
|
+
directSessionTtlSeconds: 3600,
|
|
87
|
+
directMaxPatchBytes: 200_000,
|
|
88
|
+
directMaxFileBytes: 500_000,
|
|
89
|
+
directAllowedCommands: [
|
|
90
|
+
"npm test",
|
|
91
|
+
"npm run test",
|
|
92
|
+
"npm run build",
|
|
93
|
+
"npm run lint",
|
|
94
|
+
"node --check main.js",
|
|
95
|
+
],
|
|
96
|
+
repoDirectAllowedCommands: {},
|
|
64
97
|
};
|
|
65
98
|
|
|
66
99
|
// ── Load config ───────────────────────────────────────────────────
|
|
@@ -69,7 +102,15 @@ let _config: PatchWardenConfig | null = null;
|
|
|
69
102
|
|
|
70
103
|
export function loadConfig(configPath?: string): PatchWardenConfig {
|
|
71
104
|
if (_config) return _config;
|
|
105
|
+
return loadConfigInternal(configPath);
|
|
106
|
+
}
|
|
72
107
|
|
|
108
|
+
export function reloadConfig(configPath?: string): PatchWardenConfig {
|
|
109
|
+
_config = null;
|
|
110
|
+
return loadConfigInternal(configPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function loadConfigInternal(configPath?: string): PatchWardenConfig {
|
|
73
114
|
const explicitPath = configPath || process.env.PATCHWARDEN_CONFIG;
|
|
74
115
|
const candidatePaths = explicitPath
|
|
75
116
|
? [explicitPath]
|
|
@@ -119,6 +160,10 @@ export function getTasksDir(config: PatchWardenConfig): string {
|
|
|
119
160
|
return resolve(config.workspaceRoot, config.tasksDir);
|
|
120
161
|
}
|
|
121
162
|
|
|
163
|
+
export function getAssessmentsDir(config: PatchWardenConfig): string {
|
|
164
|
+
return resolve(config.workspaceRoot, config.assessmentsDir);
|
|
165
|
+
}
|
|
166
|
+
|
|
122
167
|
function stripBom(value: string): string {
|
|
123
168
|
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
|
|
124
169
|
}
|
|
@@ -133,12 +178,37 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
133
178
|
if (!config.tasksDir || typeof config.tasksDir !== "string") {
|
|
134
179
|
throw new Error("tasksDir must be a non-empty string");
|
|
135
180
|
}
|
|
181
|
+
if (!config.assessmentsDir || typeof config.assessmentsDir !== "string") {
|
|
182
|
+
throw new Error("assessmentsDir must be a non-empty string");
|
|
183
|
+
}
|
|
184
|
+
if (!Number.isInteger(config.assessmentTtlSeconds) || config.assessmentTtlSeconds < 60 || config.assessmentTtlSeconds > 86400) {
|
|
185
|
+
throw new Error("assessmentTtlSeconds must be an integer from 60 to 86400");
|
|
186
|
+
}
|
|
136
187
|
if (!config.agents || typeof config.agents !== "object") {
|
|
137
188
|
throw new Error("agents must be an object");
|
|
138
189
|
}
|
|
139
190
|
if (!Array.isArray(config.allowedTestCommands)) {
|
|
140
191
|
throw new Error("allowedTestCommands must be an array");
|
|
141
192
|
}
|
|
193
|
+
if (config.allowedTestCommands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
194
|
+
throw new Error("allowedTestCommands must contain only non-empty command strings");
|
|
195
|
+
}
|
|
196
|
+
if (!config.repoAllowedTestCommands || typeof config.repoAllowedTestCommands !== "object" || Array.isArray(config.repoAllowedTestCommands)) {
|
|
197
|
+
throw new Error("repoAllowedTestCommands must be an object mapping workspace-relative repository paths to command arrays");
|
|
198
|
+
}
|
|
199
|
+
const repoAllowedTestCommands: Record<string, string[]> = {};
|
|
200
|
+
for (const [repoKey, commands] of Object.entries(config.repoAllowedTestCommands)) {
|
|
201
|
+
const normalizedKey = normalizeRepoKey(repoKey);
|
|
202
|
+
const resolvedRepo = resolve(config.workspaceRoot, normalizedKey);
|
|
203
|
+
const relativeRepo = relative(resolve(config.workspaceRoot), resolvedRepo);
|
|
204
|
+
if (isAbsolute(repoKey) || relativeRepo === ".." || relativeRepo.startsWith(`..${sep}`) || isAbsolute(relativeRepo)) {
|
|
205
|
+
throw new Error(`repoAllowedTestCommands key must stay inside workspaceRoot: "${repoKey}"`);
|
|
206
|
+
}
|
|
207
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
208
|
+
throw new Error(`repoAllowedTestCommands["${repoKey}"] must be an array of non-empty command strings`);
|
|
209
|
+
}
|
|
210
|
+
repoAllowedTestCommands[normalizedKey] = [...new Set(commands.map((command) => command.trim()))];
|
|
211
|
+
}
|
|
142
212
|
if (!Number.isFinite(config.maxReadFileBytes) || config.maxReadFileBytes <= 0) {
|
|
143
213
|
throw new Error("maxReadFileBytes must be a positive number");
|
|
144
214
|
}
|
|
@@ -154,12 +224,116 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
154
224
|
if (!Number.isInteger(config.watcherStaleSeconds) || config.watcherStaleSeconds < 5 || config.watcherStaleSeconds > 3600) {
|
|
155
225
|
throw new Error("watcherStaleSeconds must be an integer from 5 to 3600");
|
|
156
226
|
}
|
|
157
|
-
if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core") {
|
|
158
|
-
throw new Error('toolProfile must be "full" or "
|
|
227
|
+
if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core" && config.toolProfile !== "chatgpt_direct") {
|
|
228
|
+
throw new Error('toolProfile must be "full", "chatgpt_core", or "chatgpt_direct"');
|
|
229
|
+
}
|
|
230
|
+
if (config.enableAgentAssessment !== undefined && typeof config.enableAgentAssessment !== "boolean") {
|
|
231
|
+
throw new Error("enableAgentAssessment must be a boolean");
|
|
232
|
+
}
|
|
233
|
+
if (config.agentAssessmentTimeoutSeconds !== undefined) {
|
|
234
|
+
if (!Number.isInteger(config.agentAssessmentTimeoutSeconds) || config.agentAssessmentTimeoutSeconds < 10 || config.agentAssessmentTimeoutSeconds > 600) {
|
|
235
|
+
throw new Error("agentAssessmentTimeoutSeconds must be an integer from 10 to 600");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (config.agentAssessmentMaxOutputBytes !== undefined) {
|
|
239
|
+
if (!Number.isInteger(config.agentAssessmentMaxOutputBytes) || config.agentAssessmentMaxOutputBytes < 16384 || config.agentAssessmentMaxOutputBytes > 8388608) {
|
|
240
|
+
throw new Error("agentAssessmentMaxOutputBytes must be an integer from 16384 to 8388608");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.agentAssessmentAgentName !== undefined && typeof config.agentAssessmentAgentName !== "string") {
|
|
244
|
+
throw new Error("agentAssessmentAgentName must be a string");
|
|
245
|
+
}
|
|
246
|
+
if (config.enableDirectProfile !== undefined && typeof config.enableDirectProfile !== "boolean") {
|
|
247
|
+
throw new Error("enableDirectProfile must be a boolean");
|
|
248
|
+
}
|
|
249
|
+
if (!config.directSessionsDir || typeof config.directSessionsDir !== "string") {
|
|
250
|
+
throw new Error("directSessionsDir must be a non-empty string");
|
|
251
|
+
}
|
|
252
|
+
if (!Number.isInteger(config.directSessionTtlSeconds) || config.directSessionTtlSeconds < 60 || config.directSessionTtlSeconds > 86400) {
|
|
253
|
+
throw new Error("directSessionTtlSeconds must be an integer from 60 to 86400");
|
|
254
|
+
}
|
|
255
|
+
if (!Number.isInteger(config.directMaxPatchBytes) || config.directMaxPatchBytes <= 0) {
|
|
256
|
+
throw new Error("directMaxPatchBytes must be a positive integer");
|
|
257
|
+
}
|
|
258
|
+
if (!Number.isInteger(config.directMaxFileBytes) || config.directMaxFileBytes <= 0) {
|
|
259
|
+
throw new Error("directMaxFileBytes must be a positive integer");
|
|
260
|
+
}
|
|
261
|
+
if (config.directAllowedCommands !== undefined) {
|
|
262
|
+
if (!Array.isArray(config.directAllowedCommands)) {
|
|
263
|
+
throw new Error("directAllowedCommands must be an array");
|
|
264
|
+
}
|
|
265
|
+
if (config.directAllowedCommands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
266
|
+
throw new Error("directAllowedCommands must contain only non-empty command strings");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (config.repoDirectAllowedCommands !== undefined) {
|
|
270
|
+
if (typeof config.repoDirectAllowedCommands !== "object" || Array.isArray(config.repoDirectAllowedCommands)) {
|
|
271
|
+
throw new Error("repoDirectAllowedCommands must be an object mapping workspace-relative repository paths to command arrays");
|
|
272
|
+
}
|
|
273
|
+
const repoDirectAllowedCommands: Record<string, string[]> = {};
|
|
274
|
+
for (const [repoKey, commands] of Object.entries(config.repoDirectAllowedCommands)) {
|
|
275
|
+
const normalizedKey = normalizeRepoKey(repoKey);
|
|
276
|
+
const resolvedRepo = resolve(config.workspaceRoot, normalizedKey);
|
|
277
|
+
const relativeRepo = relative(resolve(config.workspaceRoot), resolvedRepo);
|
|
278
|
+
if (isAbsolute(repoKey) || relativeRepo === ".." || relativeRepo.startsWith(`..${sep}`) || isAbsolute(relativeRepo)) {
|
|
279
|
+
throw new Error(`repoDirectAllowedCommands key must stay inside workspaceRoot: "${repoKey}"`);
|
|
280
|
+
}
|
|
281
|
+
if (!Array.isArray(commands) || commands.some((command) => typeof command !== "string" || command.trim() === "")) {
|
|
282
|
+
throw new Error(`repoDirectAllowedCommands["${repoKey}"] must be an array of non-empty command strings`);
|
|
283
|
+
}
|
|
284
|
+
repoDirectAllowedCommands[normalizedKey] = [...new Set(commands.map((command) => command.trim()))];
|
|
285
|
+
}
|
|
286
|
+
config.repoDirectAllowedCommands = repoDirectAllowedCommands;
|
|
159
287
|
}
|
|
160
288
|
|
|
161
289
|
return {
|
|
162
290
|
...config,
|
|
163
291
|
workspaceRoot: resolve(config.workspaceRoot),
|
|
292
|
+
allowedTestCommands: [...new Set(config.allowedTestCommands.map((command) => command.trim()))],
|
|
293
|
+
repoAllowedTestCommands,
|
|
164
294
|
};
|
|
165
295
|
}
|
|
296
|
+
|
|
297
|
+
export function getRepoAllowedTestCommands(config: PatchWardenConfig, repoPath: string): string[] {
|
|
298
|
+
const target = comparablePath(resolve(repoPath));
|
|
299
|
+
for (const [repoKey, commands] of Object.entries(config.repoAllowedTestCommands || {})) {
|
|
300
|
+
if (comparablePath(resolve(config.workspaceRoot, repoKey)) === target) return [...commands];
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function getAllConfiguredTestCommands(config: PatchWardenConfig): string[] {
|
|
306
|
+
return [...new Set([
|
|
307
|
+
...config.allowedTestCommands,
|
|
308
|
+
...Object.values(config.repoAllowedTestCommands || {}).flat(),
|
|
309
|
+
])];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function getDirectSessionsDir(config: PatchWardenConfig): string {
|
|
313
|
+
return resolve(config.workspaceRoot, config.directSessionsDir);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function getRepoDirectAllowedCommands(config: PatchWardenConfig, repoPath: string): string[] {
|
|
317
|
+
const target = comparablePath(resolve(repoPath));
|
|
318
|
+
for (const [repoKey, commands] of Object.entries(config.repoDirectAllowedCommands || {})) {
|
|
319
|
+
if (comparablePath(resolve(config.workspaceRoot, repoKey)) === target) return [...commands];
|
|
320
|
+
}
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function getAllConfiguredDirectCommands(config: PatchWardenConfig): string[] {
|
|
325
|
+
return [...new Set([
|
|
326
|
+
...(config.directAllowedCommands || []),
|
|
327
|
+
...Object.values(config.repoDirectAllowedCommands || {}).flat(),
|
|
328
|
+
])];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeRepoKey(value: string): string {
|
|
332
|
+
const trimmed = String(value).trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
333
|
+
return trimmed === "" ? "." : trimmed;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function comparablePath(value: string): string {
|
|
337
|
+
const normalized = resolve(value).replace(/\\/g, "/");
|
|
338
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
339
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
5
|
+
import { PatchWardenError } from "../errors.js";
|
|
6
|
+
import {
|
|
7
|
+
readDirectSession,
|
|
8
|
+
getDirectSessionDir,
|
|
9
|
+
type DirectSessionRecord,
|
|
10
|
+
type DirectSessionVerificationRun,
|
|
11
|
+
} from "./directSessionStore.js";
|
|
12
|
+
import type { ChangedFile, ChangeArtifacts } from "../runner/changeCapture.js";
|
|
13
|
+
|
|
14
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface AuditCheck {
|
|
17
|
+
name: string;
|
|
18
|
+
result: "pass" | "warn" | "fail";
|
|
19
|
+
detail: string;
|
|
20
|
+
reason_code?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DirectSessionAuditOutput {
|
|
24
|
+
session_id: string;
|
|
25
|
+
decision: "pass" | "warn" | "fail";
|
|
26
|
+
reason_codes: string[];
|
|
27
|
+
blocking_findings: string[];
|
|
28
|
+
warnings: string[];
|
|
29
|
+
evidence: {
|
|
30
|
+
changed_files_total: number;
|
|
31
|
+
verification_runs: DirectSessionVerificationRun[];
|
|
32
|
+
diff_path: string;
|
|
33
|
+
summary_path: string;
|
|
34
|
+
audit_path: string;
|
|
35
|
+
};
|
|
36
|
+
next_action: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Main audit function ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
export function auditDirectSession(sessionId: string): DirectSessionAuditOutput {
|
|
42
|
+
const config = getConfig();
|
|
43
|
+
const session = readDirectSession(sessionId);
|
|
44
|
+
const sessionDir = getDirectSessionDir(sessionId);
|
|
45
|
+
|
|
46
|
+
const checks: AuditCheck[] = [];
|
|
47
|
+
const reasonCodes: string[] = [];
|
|
48
|
+
const blockingFindings: string[] = [];
|
|
49
|
+
const warnings: string[] = [];
|
|
50
|
+
|
|
51
|
+
// Check 1: session finalized
|
|
52
|
+
checks.push({
|
|
53
|
+
name: "session_finalized",
|
|
54
|
+
result: session.finalized ? "pass" : "fail",
|
|
55
|
+
detail: session.finalized
|
|
56
|
+
? "Session has been finalized."
|
|
57
|
+
: "Session has not been finalized. Call finalize_direct_session first.",
|
|
58
|
+
reason_code: session.finalized ? undefined : "session_not_finalized",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Get change artifacts if available
|
|
62
|
+
const artifacts = session.change_artifacts;
|
|
63
|
+
|
|
64
|
+
// Check 2: diff empty
|
|
65
|
+
const changedFilesTotal = artifacts?.changed_files?.length ?? 0;
|
|
66
|
+
checks.push({
|
|
67
|
+
name: "diff_empty",
|
|
68
|
+
result: changedFilesTotal === 0 ? "warn" : "pass",
|
|
69
|
+
detail:
|
|
70
|
+
changedFilesTotal === 0
|
|
71
|
+
? "No file changes detected in this session."
|
|
72
|
+
: `${changedFilesTotal} file(s) changed.`,
|
|
73
|
+
reason_code: changedFilesTotal === 0 ? "empty_diff" : undefined,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (artifacts) {
|
|
77
|
+
// Check 3: out-of-scope changes
|
|
78
|
+
const outOfScope = findOutOfScopeChanges(artifacts, session.resolved_repo_path);
|
|
79
|
+
checks.push({
|
|
80
|
+
name: "out_of_scope_changes",
|
|
81
|
+
result: outOfScope.length === 0 ? "pass" : "fail",
|
|
82
|
+
detail:
|
|
83
|
+
outOfScope.length === 0
|
|
84
|
+
? "All changes are within the session repo_path."
|
|
85
|
+
: `${outOfScope.length} file(s) modified outside session repo_path: ${outOfScope.join(", ")}`,
|
|
86
|
+
reason_code: outOfScope.length > 0 ? "out_of_scope_changes" : undefined,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Check 4: sensitive files
|
|
90
|
+
const sensitiveFiles = findSensitiveChanges(artifacts);
|
|
91
|
+
checks.push({
|
|
92
|
+
name: "sensitive_file_access",
|
|
93
|
+
result: sensitiveFiles.length === 0 ? "pass" : "fail",
|
|
94
|
+
detail:
|
|
95
|
+
sensitiveFiles.length === 0
|
|
96
|
+
? "No sensitive files modified."
|
|
97
|
+
: `Sensitive files modified: ${sensitiveFiles.join(", ")}`,
|
|
98
|
+
reason_code: sensitiveFiles.length > 0 ? "sensitive_file_modified" : undefined,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Check 5: node_modules modification
|
|
102
|
+
const nodeModulesChanges = findPathChanges(artifacts, "node_modules");
|
|
103
|
+
checks.push({
|
|
104
|
+
name: "node_modules_modified",
|
|
105
|
+
result: nodeModulesChanges.length === 0 ? "pass" : "fail",
|
|
106
|
+
detail:
|
|
107
|
+
nodeModulesChanges.length === 0
|
|
108
|
+
? "No node_modules modifications."
|
|
109
|
+
: `node_modules modified: ${nodeModulesChanges.join(", ")}`,
|
|
110
|
+
reason_code: nodeModulesChanges.length > 0 ? "node_modules_modified" : undefined,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Check 6: release/dist modification
|
|
114
|
+
const releaseChanges = findPathChanges(artifacts, "release");
|
|
115
|
+
const distChanges = findPathChanges(artifacts, "dist");
|
|
116
|
+
const artifactDirChanges = [...releaseChanges, ...distChanges];
|
|
117
|
+
const hasBuildArtifacts = artifacts.artifact_hygiene.counts.tracked_build_artifacts > 0;
|
|
118
|
+
checks.push({
|
|
119
|
+
name: "release_dist_modified",
|
|
120
|
+
result: artifactDirChanges.length === 0
|
|
121
|
+
? "pass"
|
|
122
|
+
: hasBuildArtifacts
|
|
123
|
+
? "warn"
|
|
124
|
+
: "fail",
|
|
125
|
+
detail:
|
|
126
|
+
artifactDirChanges.length === 0
|
|
127
|
+
? "No release/dist modifications."
|
|
128
|
+
: `release/dist modified: ${artifactDirChanges.join(", ")}${hasBuildArtifacts ? " (build-generated)" : " (not build-generated)"}`,
|
|
129
|
+
reason_code: artifactDirChanges.length > 0
|
|
130
|
+
? hasBuildArtifacts
|
|
131
|
+
? "build_artifact_modified"
|
|
132
|
+
: "release_dist_manually_modified"
|
|
133
|
+
: undefined,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Check 7: file deletion
|
|
137
|
+
const deletedFiles = artifacts.changed_files.filter((f) => f.change === "deleted");
|
|
138
|
+
checks.push({
|
|
139
|
+
name: "file_deletion",
|
|
140
|
+
result: deletedFiles.length === 0 ? "pass" : "fail",
|
|
141
|
+
detail:
|
|
142
|
+
deletedFiles.length === 0
|
|
143
|
+
? "No files deleted."
|
|
144
|
+
: `Files deleted: ${deletedFiles.map((f) => f.path).join(", ")}`,
|
|
145
|
+
reason_code: deletedFiles.length > 0 ? "file_deleted" : undefined,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Check 8: file rename
|
|
149
|
+
const renamedFiles = artifacts.changed_files.filter((f) => f.change === "renamed");
|
|
150
|
+
checks.push({
|
|
151
|
+
name: "file_rename",
|
|
152
|
+
result: renamedFiles.length === 0 ? "pass" : "fail",
|
|
153
|
+
detail:
|
|
154
|
+
renamedFiles.length === 0
|
|
155
|
+
? "No files renamed."
|
|
156
|
+
: `Files renamed: ${renamedFiles.map((f) => `${f.old_path} → ${f.path}`).join(", ")}`,
|
|
157
|
+
reason_code: renamedFiles.length > 0 ? "file_renamed" : undefined,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Check 9: package-lock / dependency changes
|
|
161
|
+
const packageLockChanges = artifacts.changed_files.filter(
|
|
162
|
+
(f) =>
|
|
163
|
+
f.path === "package-lock.json" ||
|
|
164
|
+
f.path === "package.json" ||
|
|
165
|
+
f.path === "yarn.lock" ||
|
|
166
|
+
f.path === "pnpm-lock.yaml"
|
|
167
|
+
);
|
|
168
|
+
checks.push({
|
|
169
|
+
name: "dependency_changes",
|
|
170
|
+
result: packageLockChanges.length === 0 ? "pass" : "warn",
|
|
171
|
+
detail:
|
|
172
|
+
packageLockChanges.length === 0
|
|
173
|
+
? "No dependency file changes."
|
|
174
|
+
: `Dependency files changed: ${packageLockChanges.map((f) => f.path).join(", ")}`,
|
|
175
|
+
reason_code: packageLockChanges.length > 0 ? "dependency_file_changed" : undefined,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Check 13: title vs changed files consistency (warning only)
|
|
179
|
+
if (session.title && changedFilesTotal > 0) {
|
|
180
|
+
checks.push({
|
|
181
|
+
name: "title_change_consistency",
|
|
182
|
+
result: "pass",
|
|
183
|
+
detail: `Session title: "${session.title}", ${changedFilesTotal} file(s) changed. (Semantic check not performed - deterministic audit only.)`,
|
|
184
|
+
reason_code: undefined,
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
checks.push({
|
|
188
|
+
name: "title_change_consistency",
|
|
189
|
+
result: "pass",
|
|
190
|
+
detail: "No title provided or no changes to compare.",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check 14: suspicious changes
|
|
195
|
+
const suspiciousCount = artifacts.artifact_hygiene.counts.suspicious_changes;
|
|
196
|
+
checks.push({
|
|
197
|
+
name: "suspicious_changes",
|
|
198
|
+
result: suspiciousCount === 0 ? "pass" : "warn",
|
|
199
|
+
detail:
|
|
200
|
+
suspiciousCount === 0
|
|
201
|
+
? "No suspicious changes detected."
|
|
202
|
+
: `${suspiciousCount} suspicious change(s) detected.`,
|
|
203
|
+
reason_code: suspiciousCount > 0 ? "suspicious_changes" : undefined,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Check 15: runtime generated files
|
|
207
|
+
const runtimeCount = artifacts.artifact_hygiene.counts.runtime_generated_files;
|
|
208
|
+
checks.push({
|
|
209
|
+
name: "runtime_generated_files",
|
|
210
|
+
result: runtimeCount === 0 ? "pass" : "warn",
|
|
211
|
+
detail:
|
|
212
|
+
runtimeCount === 0
|
|
213
|
+
? "No runtime-generated files detected."
|
|
214
|
+
: `${runtimeCount} runtime-generated file(s) detected.`,
|
|
215
|
+
reason_code: runtimeCount > 0 ? "runtime_generated_files" : undefined,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Check 16: tracked build artifacts
|
|
219
|
+
const buildArtifactCount = artifacts.artifact_hygiene.counts.tracked_build_artifacts;
|
|
220
|
+
checks.push({
|
|
221
|
+
name: "tracked_build_artifacts",
|
|
222
|
+
result: buildArtifactCount === 0 ? "pass" : "warn",
|
|
223
|
+
detail:
|
|
224
|
+
buildArtifactCount === 0
|
|
225
|
+
? "No tracked build artifacts detected."
|
|
226
|
+
: `${buildArtifactCount} tracked build artifact(s) detected.`,
|
|
227
|
+
reason_code: buildArtifactCount > 0 ? "tracked_build_artifacts" : undefined,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check 10: at least one verification command run
|
|
232
|
+
const verificationRuns = session.verification_runs || [];
|
|
233
|
+
const hasVerification = verificationRuns.length > 0;
|
|
234
|
+
const hasSourceChanges = artifacts
|
|
235
|
+
? artifacts.changed_files.some((f) => f.kind === "source")
|
|
236
|
+
: false;
|
|
237
|
+
|
|
238
|
+
checks.push({
|
|
239
|
+
name: "verification_run",
|
|
240
|
+
result: hasVerification
|
|
241
|
+
? "pass"
|
|
242
|
+
: hasSourceChanges
|
|
243
|
+
? "fail"
|
|
244
|
+
: "warn",
|
|
245
|
+
detail: hasVerification
|
|
246
|
+
? `${verificationRuns.length} verification command(s) run.`
|
|
247
|
+
: hasSourceChanges
|
|
248
|
+
? "Source files were modified but no verification commands were run."
|
|
249
|
+
: "No verification commands were run (no source changes detected).",
|
|
250
|
+
reason_code: !hasVerification
|
|
251
|
+
? hasSourceChanges
|
|
252
|
+
? "source_changes_without_verification"
|
|
253
|
+
: "no_verification_run"
|
|
254
|
+
: undefined,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Check 11: verification commands passed
|
|
258
|
+
if (hasVerification) {
|
|
259
|
+
const allPassed = verificationRuns.every((r) => r.passed);
|
|
260
|
+
checks.push({
|
|
261
|
+
name: "verification_passed",
|
|
262
|
+
result: allPassed ? "pass" : "fail",
|
|
263
|
+
detail: allPassed
|
|
264
|
+
? "All verification commands passed."
|
|
265
|
+
: `${verificationRuns.filter((r) => !r.passed).length} verification command(s) failed.`,
|
|
266
|
+
reason_code: !allPassed ? "verification_failed" : undefined,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Collect results
|
|
271
|
+
for (const check of checks) {
|
|
272
|
+
if (check.reason_code) reasonCodes.push(check.reason_code);
|
|
273
|
+
if (check.result === "fail") blockingFindings.push(`${check.name}: ${check.detail}`);
|
|
274
|
+
if (check.result === "warn") warnings.push(`${check.name}: ${check.detail}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Determine decision
|
|
278
|
+
const hasFail = checks.some((c) => c.result === "fail");
|
|
279
|
+
const hasWarn = checks.some((c) => c.result === "warn");
|
|
280
|
+
const decision: "pass" | "warn" | "fail" = hasFail ? "fail" : hasWarn ? "warn" : "pass";
|
|
281
|
+
|
|
282
|
+
const diffPath = join(sessionDir, "diff.patch");
|
|
283
|
+
const summaryPath = join(sessionDir, "summary.md");
|
|
284
|
+
const auditPath = join(sessionDir, "audit.md");
|
|
285
|
+
|
|
286
|
+
const output: DirectSessionAuditOutput = {
|
|
287
|
+
session_id: sessionId,
|
|
288
|
+
decision,
|
|
289
|
+
reason_codes: reasonCodes,
|
|
290
|
+
blocking_findings: blockingFindings,
|
|
291
|
+
warnings,
|
|
292
|
+
evidence: {
|
|
293
|
+
changed_files_total: changedFilesTotal,
|
|
294
|
+
verification_runs: verificationRuns,
|
|
295
|
+
diff_path: diffPath,
|
|
296
|
+
summary_path: summaryPath,
|
|
297
|
+
audit_path: auditPath,
|
|
298
|
+
},
|
|
299
|
+
next_action: decision === "pass"
|
|
300
|
+
? "Audit passed. Changes are safe to accept."
|
|
301
|
+
: decision === "warn"
|
|
302
|
+
? "Audit completed with warnings. Review the warnings before accepting changes."
|
|
303
|
+
: "Audit failed. Review the blocking findings and create a new session to fix issues.",
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Write audit.json and audit.md
|
|
307
|
+
writeFileSync(
|
|
308
|
+
join(sessionDir, "audit.json"),
|
|
309
|
+
JSON.stringify(output, null, 2),
|
|
310
|
+
"utf-8"
|
|
311
|
+
);
|
|
312
|
+
writeFileSync(auditPath, formatAuditMd(output, checks, session), "utf-8");
|
|
313
|
+
|
|
314
|
+
return output;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Helper functions ───────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function findOutOfScopeChanges(
|
|
320
|
+
artifacts: ChangeArtifacts,
|
|
321
|
+
resolvedRepoPath: string
|
|
322
|
+
): string[] {
|
|
323
|
+
const normalizedRepo = resolve(resolvedRepoPath);
|
|
324
|
+
return artifacts.changed_files
|
|
325
|
+
.filter((f) => {
|
|
326
|
+
const fullPath = resolve(normalizedRepo, f.path);
|
|
327
|
+
const rel = relative(normalizedRepo, fullPath);
|
|
328
|
+
return isAbsolute(rel) || rel.startsWith("..");
|
|
329
|
+
})
|
|
330
|
+
.map((f) => f.path);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function findSensitiveChanges(artifacts: ChangeArtifacts): string[] {
|
|
334
|
+
return artifacts.changed_files
|
|
335
|
+
.filter((f) => isSensitivePath(f.path))
|
|
336
|
+
.map((f) => f.path);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function findPathChanges(artifacts: ChangeArtifacts, dirName: string): string[] {
|
|
340
|
+
return artifacts.changed_files
|
|
341
|
+
.filter((f) => {
|
|
342
|
+
const normalized = f.path.replace(/\\/g, "/");
|
|
343
|
+
return normalized.startsWith(`${dirName}/`) || normalized.includes(`/${dirName}/`);
|
|
344
|
+
})
|
|
345
|
+
.map((f) => f.path);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function formatAuditMd(
|
|
349
|
+
output: DirectSessionAuditOutput,
|
|
350
|
+
checks: AuditCheck[],
|
|
351
|
+
session: DirectSessionRecord
|
|
352
|
+
): string {
|
|
353
|
+
const lines: string[] = [
|
|
354
|
+
"# Direct Session Audit Report",
|
|
355
|
+
"",
|
|
356
|
+
`**Session ID:** ${output.session_id}`,
|
|
357
|
+
`**Decision:** ${output.decision.toUpperCase()}`,
|
|
358
|
+
`**Changed files:** ${output.evidence.changed_files_total}`,
|
|
359
|
+
"",
|
|
360
|
+
"## Checks",
|
|
361
|
+
"",
|
|
362
|
+
"| Check | Result | Detail |",
|
|
363
|
+
"|---|---|---|",
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
for (const check of checks) {
|
|
367
|
+
const emoji = check.result === "pass" ? "PASS" : check.result === "warn" ? "WARN" : "FAIL";
|
|
368
|
+
lines.push(`| ${check.name} | ${emoji} | ${check.detail.replace(/\|/g, "\\|")} |`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (output.blocking_findings.length > 0) {
|
|
372
|
+
lines.push("", "## Blocking Findings", "");
|
|
373
|
+
for (const finding of output.blocking_findings) {
|
|
374
|
+
lines.push(`- ${finding}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (output.warnings.length > 0) {
|
|
379
|
+
lines.push("", "## Warnings", "");
|
|
380
|
+
for (const warning of output.warnings) {
|
|
381
|
+
lines.push(`- ${warning}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
lines.push("", "## Evidence", "");
|
|
386
|
+
lines.push(`- Diff: \`${output.evidence.diff_path}\``);
|
|
387
|
+
lines.push(`- Summary: \`${output.evidence.summary_path}\``);
|
|
388
|
+
lines.push(`- Verification runs: ${output.evidence.verification_runs.length}`);
|
|
389
|
+
|
|
390
|
+
if (output.evidence.verification_runs.length > 0) {
|
|
391
|
+
lines.push("", "### Verification Results", "");
|
|
392
|
+
for (const run of output.evidence.verification_runs) {
|
|
393
|
+
lines.push(`- **${run.command}**: ${run.passed ? "PASSED" : "FAILED"} (exit code: ${run.exit_code})`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
lines.push("", `**Next action:** ${output.next_action}`);
|
|
398
|
+
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|