patchwarden 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +29 -0
- package/dist/runner/changeCapture.js +107 -5
- package/dist/runner/runTask.js +70 -44
- 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/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 +94 -2
- 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 +47 -0
- package/dist/tools/getTaskSummary.js +64 -1
- package/dist/tools/healthCheck.d.ts +22 -12
- package/dist/tools/healthCheck.js +23 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +217 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -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/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +3 -2
- package/dist/tools/toolCatalog.js +32 -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/docs/chatgpt-usage.md +101 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -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 +12 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- 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 +66 -6
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +100 -58
- package/scripts/mcp-smoke.js +157 -2
- package/scripts/pack-clean.js +3 -4
- 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/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/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +140 -5
- package/src/runner/runTask.ts +61 -43
- 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/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +100 -2
- 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 +111 -1
- package/src/tools/healthCheck.ts +23 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +242 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +35 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- 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/dist/doctor.js
CHANGED
|
@@ -17,7 +17,7 @@ import { isSensitivePath } from "./security/sensitiveGuard.js";
|
|
|
17
17
|
import { guardPlanContent } from "./security/planGuard.js";
|
|
18
18
|
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
19
19
|
import { getToolDefs } from "./tools/registry.js";
|
|
20
|
-
import { CHATGPT_CORE_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
20
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
21
21
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
22
22
|
// ── State ──────────────────────────────────────────────────────────
|
|
23
23
|
let ok = 0;
|
|
@@ -59,6 +59,7 @@ function cmd(cmdStr) {
|
|
|
59
59
|
// ══════════════════════════════════════════════════════════════════
|
|
60
60
|
async function main() {
|
|
61
61
|
console.log("PatchWarden Doctor\n");
|
|
62
|
+
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
62
63
|
// 1. Node version
|
|
63
64
|
const nodeVer = process.version;
|
|
64
65
|
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
@@ -66,6 +67,16 @@ async function main() {
|
|
|
66
67
|
// 2. npm
|
|
67
68
|
const npmVer = cmd("npm --version");
|
|
68
69
|
check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
|
|
70
|
+
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
71
|
+
// binary but must not be exposed as an MCP tool.
|
|
72
|
+
try {
|
|
73
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
74
|
+
check("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing");
|
|
75
|
+
check("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build");
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
|
|
79
|
+
}
|
|
69
80
|
// 3. Git
|
|
70
81
|
const gitVer = cmd("git --version");
|
|
71
82
|
warnCheck("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work");
|
|
@@ -81,9 +92,15 @@ async function main() {
|
|
|
81
92
|
break;
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
|
-
|
|
95
|
+
const configDetail = configPathUsed
|
|
85
96
|
? configPathUsed
|
|
86
|
-
: 'Create one: cp examples/config.example.json patchwarden.config.json'
|
|
97
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
98
|
+
if (allowDefaultConfig) {
|
|
99
|
+
warnCheck("Config file exists", configPathUsed !== "", configDetail);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
check("Config file exists", configPathUsed !== "", configDetail);
|
|
103
|
+
}
|
|
87
104
|
// 5. PATCHWARDEN_CONFIG env
|
|
88
105
|
if (process.env.PATCHWARDEN_CONFIG) {
|
|
89
106
|
results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
|
|
@@ -198,15 +215,26 @@ async function main() {
|
|
|
198
215
|
try {
|
|
199
216
|
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
200
217
|
const fullTools = getToolDefs();
|
|
201
|
-
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core");
|
|
218
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
202
219
|
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
203
220
|
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
204
|
-
check("Full tool profile exposes
|
|
221
|
+
check("Full tool profile exposes 28 tools", fullTools.length === 28, `${fullTools.length} tools`);
|
|
205
222
|
check(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", "));
|
|
206
223
|
check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
207
224
|
createSchema?.properties?.verify_commands &&
|
|
208
225
|
waitSchema?.properties?.timeout_seconds &&
|
|
209
226
|
waitSchema?.properties?.wait_seconds));
|
|
227
|
+
// chatgpt_direct profile checks
|
|
228
|
+
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
229
|
+
check("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`);
|
|
230
|
+
if (config?.enableDirectProfile) {
|
|
231
|
+
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
232
|
+
check(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", "));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
|
|
236
|
+
ok++;
|
|
237
|
+
}
|
|
210
238
|
}
|
|
211
239
|
finally {
|
|
212
240
|
if (previousProfile === undefined)
|
|
@@ -259,6 +287,31 @@ async function main() {
|
|
|
259
287
|
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
260
288
|
check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
261
289
|
}
|
|
290
|
+
// Direct tool module checks
|
|
291
|
+
const directToolModules = [
|
|
292
|
+
"createDirectSession",
|
|
293
|
+
"searchWorkspace",
|
|
294
|
+
"applyPatch",
|
|
295
|
+
"runVerification",
|
|
296
|
+
"finalizeDirectSession",
|
|
297
|
+
"auditSession",
|
|
298
|
+
];
|
|
299
|
+
for (const t of directToolModules) {
|
|
300
|
+
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
301
|
+
check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
302
|
+
}
|
|
303
|
+
// Direct support module checks
|
|
304
|
+
const directSupportModules = [
|
|
305
|
+
"directSessionStore",
|
|
306
|
+
"directGuards",
|
|
307
|
+
"directPatch",
|
|
308
|
+
"directVerification",
|
|
309
|
+
"directAudit",
|
|
310
|
+
];
|
|
311
|
+
for (const t of directSupportModules) {
|
|
312
|
+
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
313
|
+
check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
314
|
+
}
|
|
262
315
|
// Task directory writable
|
|
263
316
|
if (config) {
|
|
264
317
|
const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
|
|
@@ -334,11 +387,26 @@ async function main() {
|
|
|
334
387
|
}
|
|
335
388
|
}
|
|
336
389
|
}
|
|
390
|
+
// 14b. Direct profile config checks
|
|
391
|
+
if (config) {
|
|
392
|
+
const directCmds = config.directAllowedCommands || [];
|
|
393
|
+
warnCheck("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
|
|
394
|
+
// npm run doctor should NOT be in default Direct whitelist
|
|
395
|
+
const hasDoctor = directCmds.some((c) => c === "npm run doctor");
|
|
396
|
+
check("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
|
|
397
|
+
check("directSessionTtlSeconds is valid", config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400, `${config.directSessionTtlSeconds}s`);
|
|
398
|
+
check("directMaxPatchBytes is positive", config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
|
|
399
|
+
check("directMaxFileBytes is positive", config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
|
|
400
|
+
}
|
|
337
401
|
// 15. Tunnel example files check
|
|
338
402
|
const tunnelFiles = [
|
|
339
403
|
"examples/openai-tunnel/README.md",
|
|
340
404
|
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
341
405
|
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
406
|
+
"scripts/patchwarden-mcp-direct.cmd",
|
|
407
|
+
"PatchWarden.cmd",
|
|
408
|
+
"scripts/manage-patchwarden.ps1",
|
|
409
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
342
410
|
];
|
|
343
411
|
for (const tf of tunnelFiles) {
|
|
344
412
|
const full = resolve(process.cwd(), tf);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PatchWardenConfig } from "../config.js";
|
|
2
|
+
export interface AgentInvocation {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
commandLabel: string;
|
|
7
|
+
promptMode: "inline" | "file";
|
|
8
|
+
promptFilePath?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build agent invocation parameters from config.
|
|
12
|
+
* Replaces {repo}, {prompt}, and {prompt_file} placeholders.
|
|
13
|
+
* runTask and agentAssessor share this function to ensure consistent agent startup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAgentInvocation(agentName: string, repoPath: string, prompt: string, config: PatchWardenConfig, promptFilePath?: string): AgentInvocation;
|
|
16
|
+
/**
|
|
17
|
+
* Build task execution prompt. Mechanically extracted from runTask.ts.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildExecutionPrompt(plan: string, repoPath: string, testCommand: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build agentAssessor inspect-only prompt.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildAssessmentPrompt(goal: string, planContent: string, repoPath: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { guardAgentCommand, sanitizePromptArg } from "../security/commandGuard.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build agent invocation parameters from config.
|
|
5
|
+
* Replaces {repo}, {prompt}, and {prompt_file} placeholders.
|
|
6
|
+
* runTask and agentAssessor share this function to ensure consistent agent startup.
|
|
7
|
+
*/
|
|
8
|
+
export function buildAgentInvocation(agentName, repoPath, prompt, config, promptFilePath) {
|
|
9
|
+
const agentCmd = guardAgentCommand(agentName, config);
|
|
10
|
+
const sanitizedPrompt = sanitizePromptArg(prompt);
|
|
11
|
+
const hasPromptFilePlaceholder = agentCmd.args.includes("{prompt_file}");
|
|
12
|
+
const promptMode = hasPromptFilePlaceholder && promptFilePath ? "file" : "inline";
|
|
13
|
+
const resolvedArgs = agentCmd.args.map((arg) => {
|
|
14
|
+
if (arg === "{repo}")
|
|
15
|
+
return repoPath;
|
|
16
|
+
if (arg === "{prompt}")
|
|
17
|
+
return sanitizedPrompt;
|
|
18
|
+
if (arg === "{prompt_file}" && promptMode === "file" && promptFilePath)
|
|
19
|
+
return promptFilePath;
|
|
20
|
+
return arg;
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
command: agentCmd.command,
|
|
24
|
+
args: resolvedArgs,
|
|
25
|
+
cwd: repoPath,
|
|
26
|
+
commandLabel: `${basename(agentCmd.command)} (configured agent command)`,
|
|
27
|
+
promptMode,
|
|
28
|
+
...(promptMode === "file" && promptFilePath ? { promptFilePath } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build task execution prompt. Mechanically extracted from runTask.ts.
|
|
33
|
+
*/
|
|
34
|
+
export function buildExecutionPrompt(plan, repoPath, testCommand) {
|
|
35
|
+
let prompt = `You are executing a pre-written plan in a local repository.
|
|
36
|
+
|
|
37
|
+
## Repository
|
|
38
|
+
${repoPath}
|
|
39
|
+
|
|
40
|
+
## Plan
|
|
41
|
+
${plan}
|
|
42
|
+
|
|
43
|
+
## Instructions
|
|
44
|
+
1. Read the plan carefully.
|
|
45
|
+
2. Implement the changes in this repository only.
|
|
46
|
+
3. Do NOT modify files outside this repository.
|
|
47
|
+
4. Leave repository changes uncommitted for review; remote operations are outside this task.
|
|
48
|
+
5. After implementing, describe what you changed.
|
|
49
|
+
6. Output a summary with what was done, files modified, and issues encountered.
|
|
50
|
+
`;
|
|
51
|
+
if (testCommand) {
|
|
52
|
+
prompt += `\n7. You may run ${testCommand}; PatchWarden will independently run it again for verification.`;
|
|
53
|
+
}
|
|
54
|
+
return prompt;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build agentAssessor inspect-only prompt.
|
|
58
|
+
*/
|
|
59
|
+
export function buildAssessmentPrompt(goal, planContent, repoPath) {
|
|
60
|
+
return `You are performing a READ-ONLY risk assessment of a planned task. Do NOT modify any files.
|
|
61
|
+
|
|
62
|
+
## Repository
|
|
63
|
+
${repoPath}
|
|
64
|
+
|
|
65
|
+
## Goal
|
|
66
|
+
${goal}
|
|
67
|
+
|
|
68
|
+
## Plan
|
|
69
|
+
${planContent}
|
|
70
|
+
|
|
71
|
+
## Instructions
|
|
72
|
+
1. Read the plan and goal carefully.
|
|
73
|
+
2. Inspect the repository to understand the scope of changes.
|
|
74
|
+
3. Do NOT create, edit, delete, rename, or generate any files.
|
|
75
|
+
4. Assess the risk level of executing this plan.
|
|
76
|
+
5. Identify affected file paths within the repository.
|
|
77
|
+
6. Check for destructive actions, sensitive file access, or out-of-scope changes.
|
|
78
|
+
7. Output your assessment as JSON after the marker ===ASSESSMENT_JSON=== on a new line.
|
|
79
|
+
|
|
80
|
+
## Required JSON output format
|
|
81
|
+
After your analysis, output exactly this format:
|
|
82
|
+
|
|
83
|
+
===ASSESSMENT_JSON===
|
|
84
|
+
{
|
|
85
|
+
"risk_level": "low" | "medium" | "high",
|
|
86
|
+
"reason_codes": ["short", "descriptive", "codes"],
|
|
87
|
+
"affected_paths": ["relative/path/to/file"],
|
|
88
|
+
"destructive_actions": ["description of any destructive actions"],
|
|
89
|
+
"requires_user_confirm": false,
|
|
90
|
+
"confidence": 0.0,
|
|
91
|
+
"notes": "Brief summary of findings"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
## Risk level guidelines
|
|
95
|
+
- low: Small source change, no sensitive files, no destructive actions
|
|
96
|
+
- medium: Multiple files, build artifacts, dependency changes, or uncertain scope
|
|
97
|
+
- high: Destructive actions, sensitive file access, out-of-scope changes, or large-scale deletion
|
|
98
|
+
|
|
99
|
+
## Constraints
|
|
100
|
+
- affected_paths must be relative paths within the repository (no absolute paths)
|
|
101
|
+
- reason_codes: max 50 entries, each max 100 chars
|
|
102
|
+
- destructive_actions: max 20 entries, each max 200 chars
|
|
103
|
+
- notes: max 2000 chars
|
|
104
|
+
- confidence: number from 0.0 to 1.0
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface FileFingerprint {
|
|
2
2
|
size: number;
|
|
3
3
|
sha256: string;
|
|
4
|
+
tracked: boolean;
|
|
5
|
+
ignored: boolean;
|
|
4
6
|
}
|
|
5
7
|
export interface RepoSnapshot {
|
|
6
8
|
captured_at: string;
|
|
@@ -17,6 +19,31 @@ export interface ChangedFile {
|
|
|
17
19
|
old_path?: string;
|
|
18
20
|
before_sha256: string | null;
|
|
19
21
|
after_sha256: string | null;
|
|
22
|
+
tracked: boolean;
|
|
23
|
+
ignored: boolean;
|
|
24
|
+
kind: "source" | "build_artifact" | "runtime_generated";
|
|
25
|
+
}
|
|
26
|
+
export interface ClassifiedChange {
|
|
27
|
+
path: string;
|
|
28
|
+
change: ChangedFile["change"];
|
|
29
|
+
tracked: boolean;
|
|
30
|
+
ignored: boolean;
|
|
31
|
+
kind: ChangedFile["kind"];
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ArtifactHygiene {
|
|
35
|
+
counts: {
|
|
36
|
+
source_changes: number;
|
|
37
|
+
tracked_build_artifacts: number;
|
|
38
|
+
ignored_untracked_artifacts: number;
|
|
39
|
+
runtime_generated_files: number;
|
|
40
|
+
suspicious_changes: number;
|
|
41
|
+
};
|
|
42
|
+
source_changes: ClassifiedChange[];
|
|
43
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
44
|
+
ignored_untracked_artifacts: ClassifiedChange[];
|
|
45
|
+
runtime_generated_files: ClassifiedChange[];
|
|
46
|
+
suspicious_changes: ClassifiedChange[];
|
|
20
47
|
}
|
|
21
48
|
export interface ChangeArtifacts {
|
|
22
49
|
changed_files: ChangedFile[];
|
|
@@ -36,8 +63,10 @@ export interface ChangeArtifacts {
|
|
|
36
63
|
workspace_dirty_after: boolean;
|
|
37
64
|
patch_mode: "textual" | "no_changes" | "hash_only";
|
|
38
65
|
unavailable_reason: string | null;
|
|
66
|
+
artifact_hygiene: ArtifactHygiene;
|
|
39
67
|
}
|
|
40
68
|
export declare function captureRepoSnapshot(repoPath: string): RepoSnapshot;
|
|
41
69
|
export declare function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void;
|
|
42
70
|
export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): ChangeArtifacts;
|
|
43
71
|
export declare function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[];
|
|
72
|
+
export declare function emptyArtifactHygiene(): ArtifactHygiene;
|
|
@@ -13,11 +13,26 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
13
13
|
let head = null;
|
|
14
14
|
let status = "";
|
|
15
15
|
let paths = [];
|
|
16
|
+
const trackedPaths = new Set();
|
|
17
|
+
const ignoredPaths = new Set();
|
|
16
18
|
if (isGit) {
|
|
17
19
|
const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
|
|
18
20
|
if (headResult.status === 0)
|
|
19
21
|
head = headResult.stdout.trim() || null;
|
|
20
22
|
status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
|
|
23
|
+
const tracked = runGit(repoPath, ["ls-files", "-z"]);
|
|
24
|
+
if (tracked.status === 0) {
|
|
25
|
+
for (const path of tracked.stdout.split("\0").filter(Boolean))
|
|
26
|
+
trackedPaths.add(normalizePath(path));
|
|
27
|
+
}
|
|
28
|
+
const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
|
|
29
|
+
if (ignored.status === 0) {
|
|
30
|
+
for (const path of ignored.stdout.split("\0").filter(Boolean))
|
|
31
|
+
ignoredPaths.add(normalizePath(path));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
35
|
+
}
|
|
21
36
|
const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
|
|
22
37
|
if (listed.status === 0) {
|
|
23
38
|
paths = [...new Set([
|
|
@@ -40,7 +55,7 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
40
55
|
}
|
|
41
56
|
const files = {};
|
|
42
57
|
for (const inputPath of paths.sort()) {
|
|
43
|
-
const normalized = inputPath
|
|
58
|
+
const normalized = normalizePath(inputPath);
|
|
44
59
|
if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized))
|
|
45
60
|
continue;
|
|
46
61
|
const absolutePath = resolve(repoPath, inputPath);
|
|
@@ -51,7 +66,12 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
51
66
|
const sha256 = stat.size <= MAX_HASH_BYTES
|
|
52
67
|
? createHash("sha256").update(readFileSync(absolutePath)).digest("hex")
|
|
53
68
|
: `large-file:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
54
|
-
files[normalized] = {
|
|
69
|
+
files[normalized] = {
|
|
70
|
+
size: stat.size,
|
|
71
|
+
sha256,
|
|
72
|
+
tracked: trackedPaths.has(normalized),
|
|
73
|
+
ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
|
|
74
|
+
};
|
|
55
75
|
}
|
|
56
76
|
catch {
|
|
57
77
|
warnings.push(`could not fingerprint: ${normalized}`);
|
|
@@ -72,6 +92,7 @@ export function writeSnapshot(taskDir, filename, snapshot) {
|
|
|
72
92
|
}
|
|
73
93
|
export function buildChangeArtifacts(repoPath, before, after) {
|
|
74
94
|
const changedFiles = compareSnapshots(before, after);
|
|
95
|
+
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
75
96
|
const sections = [];
|
|
76
97
|
const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
|
|
77
98
|
if (before.is_git && after.is_git && scopedPaths.length > 0) {
|
|
@@ -122,6 +143,7 @@ export function buildChangeArtifacts(repoPath, before, after) {
|
|
|
122
143
|
? "Git could not produce a textual patch for the changed files; hash evidence remains available."
|
|
123
144
|
: "Repository is not a Git worktree; only bounded hash evidence is available.")
|
|
124
145
|
: null,
|
|
146
|
+
artifact_hygiene: artifactHygiene,
|
|
125
147
|
};
|
|
126
148
|
}
|
|
127
149
|
function buildFileStats(repoPath, before, after, changedFiles) {
|
|
@@ -168,13 +190,13 @@ export function compareSnapshots(before, after) {
|
|
|
168
190
|
const left = before.files[path];
|
|
169
191
|
const right = after.files[path];
|
|
170
192
|
if (!left && right) {
|
|
171
|
-
changed.push(
|
|
193
|
+
changed.push(classifyChangedFile(path, "added", null, right));
|
|
172
194
|
}
|
|
173
195
|
else if (left && !right) {
|
|
174
|
-
changed.push(
|
|
196
|
+
changed.push(classifyChangedFile(path, "deleted", left, null));
|
|
175
197
|
}
|
|
176
198
|
else if (left.sha256 !== right.sha256) {
|
|
177
|
-
changed.push(
|
|
199
|
+
changed.push(classifyChangedFile(path, "modified", left, right));
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
const deletedByHash = new Map();
|
|
@@ -198,11 +220,91 @@ export function compareSnapshots(before, after) {
|
|
|
198
220
|
change: "renamed",
|
|
199
221
|
before_sha256: source.before_sha256,
|
|
200
222
|
after_sha256: file.after_sha256,
|
|
223
|
+
tracked: file.tracked || source.tracked,
|
|
224
|
+
ignored: file.ignored,
|
|
225
|
+
kind: classifyPathKind(file.path),
|
|
201
226
|
});
|
|
202
227
|
}
|
|
203
228
|
return [...changed.filter((item) => !consumed.has(item)), ...renamed]
|
|
204
229
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
205
230
|
}
|
|
231
|
+
export function emptyArtifactHygiene() {
|
|
232
|
+
return {
|
|
233
|
+
counts: {
|
|
234
|
+
source_changes: 0,
|
|
235
|
+
tracked_build_artifacts: 0,
|
|
236
|
+
ignored_untracked_artifacts: 0,
|
|
237
|
+
runtime_generated_files: 0,
|
|
238
|
+
suspicious_changes: 0,
|
|
239
|
+
},
|
|
240
|
+
source_changes: [],
|
|
241
|
+
tracked_build_artifacts: [],
|
|
242
|
+
ignored_untracked_artifacts: [],
|
|
243
|
+
runtime_generated_files: [],
|
|
244
|
+
suspicious_changes: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function classifyChangedFile(path, change, before, after) {
|
|
248
|
+
return {
|
|
249
|
+
path,
|
|
250
|
+
change,
|
|
251
|
+
before_sha256: before?.sha256 || null,
|
|
252
|
+
after_sha256: after?.sha256 || null,
|
|
253
|
+
tracked: Boolean(after?.tracked || before?.tracked),
|
|
254
|
+
ignored: Boolean(after?.ignored ?? before?.ignored),
|
|
255
|
+
kind: classifyPathKind(path),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function classifyArtifactHygiene(changes) {
|
|
259
|
+
const hygiene = emptyArtifactHygiene();
|
|
260
|
+
const entries = changes.map((change) => ({
|
|
261
|
+
path: change.path,
|
|
262
|
+
change: change.change,
|
|
263
|
+
tracked: change.tracked,
|
|
264
|
+
ignored: change.ignored,
|
|
265
|
+
kind: change.kind,
|
|
266
|
+
reason: classificationReason(change),
|
|
267
|
+
}));
|
|
268
|
+
hygiene.source_changes = entries.filter((entry) => entry.kind === "source" && !entry.ignored);
|
|
269
|
+
hygiene.tracked_build_artifacts = entries.filter((entry) => entry.kind === "build_artifact" && entry.tracked);
|
|
270
|
+
hygiene.ignored_untracked_artifacts = entries.filter((entry) => entry.ignored && !entry.tracked);
|
|
271
|
+
hygiene.runtime_generated_files = entries.filter((entry) => entry.kind === "runtime_generated");
|
|
272
|
+
hygiene.suspicious_changes = entries.filter((entry) => (entry.kind === "build_artifact" || entry.kind === "runtime_generated") && !entry.ignored);
|
|
273
|
+
hygiene.counts = {
|
|
274
|
+
source_changes: hygiene.source_changes.length,
|
|
275
|
+
tracked_build_artifacts: hygiene.tracked_build_artifacts.length,
|
|
276
|
+
ignored_untracked_artifacts: hygiene.ignored_untracked_artifacts.length,
|
|
277
|
+
runtime_generated_files: hygiene.runtime_generated_files.length,
|
|
278
|
+
suspicious_changes: hygiene.suspicious_changes.length,
|
|
279
|
+
};
|
|
280
|
+
return hygiene;
|
|
281
|
+
}
|
|
282
|
+
function classifyPathKind(path) {
|
|
283
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
284
|
+
const parts = normalized.split("/");
|
|
285
|
+
const basename = parts[parts.length - 1] || "";
|
|
286
|
+
if (basename === "sync-store.json" || /\.(log|tmp|temp|pid)$/.test(basename))
|
|
287
|
+
return "runtime_generated";
|
|
288
|
+
if (parts.some((part) => ["dist", "release", "build", "out", "coverage", ".next"].includes(part)))
|
|
289
|
+
return "build_artifact";
|
|
290
|
+
if (/\.(exe|dll|pak|bin|zip|tgz|tar\.gz)$/.test(basename))
|
|
291
|
+
return "build_artifact";
|
|
292
|
+
return "source";
|
|
293
|
+
}
|
|
294
|
+
function classificationReason(change) {
|
|
295
|
+
if (change.ignored)
|
|
296
|
+
return "untracked path is ignored by repository Git rules";
|
|
297
|
+
if (change.kind === "build_artifact" && change.tracked)
|
|
298
|
+
return "artifact-like path is tracked by Git and requires review";
|
|
299
|
+
if (change.kind === "build_artifact")
|
|
300
|
+
return "artifact-like path is not ignored and requires review";
|
|
301
|
+
if (change.kind === "runtime_generated")
|
|
302
|
+
return "runtime-generated path is not ignored and requires review";
|
|
303
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
304
|
+
}
|
|
305
|
+
function normalizePath(value) {
|
|
306
|
+
return value.replace(/\\/g, "/");
|
|
307
|
+
}
|
|
206
308
|
function walkWorkspace(root) {
|
|
207
309
|
const result = [];
|
|
208
310
|
const visit = (directory) => {
|