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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SearchWorkspaceInput {
|
|
2
|
+
session_id: string;
|
|
3
|
+
query: string;
|
|
4
|
+
max_results?: number;
|
|
5
|
+
case_sensitive?: boolean;
|
|
6
|
+
max_preview_chars?: number;
|
|
7
|
+
include_globs?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface SearchMatch {
|
|
10
|
+
path: string;
|
|
11
|
+
line: number;
|
|
12
|
+
preview: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchWorkspaceOutput {
|
|
15
|
+
results: SearchMatch[];
|
|
16
|
+
total_matches: number;
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function searchWorkspace(input: SearchWorkspaceInput): SearchWorkspaceOutput;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { relative, join, extname } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { PatchWardenError } from "../errors.js";
|
|
5
|
+
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
+
import { validateDirectSessionFreshness, } from "../direct/directSessionStore.js";
|
|
7
|
+
import { guardDirectSessionActive } from "../direct/directGuards.js";
|
|
8
|
+
// ── Constants ──────────────────────────────────────────────────────
|
|
9
|
+
const SKIP_DIRECTORIES = new Set([
|
|
10
|
+
".git",
|
|
11
|
+
".patchwarden",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"release",
|
|
15
|
+
"coverage",
|
|
16
|
+
".next",
|
|
17
|
+
".turbo",
|
|
18
|
+
]);
|
|
19
|
+
const BINARY_EXTENSIONS = new Set([
|
|
20
|
+
".exe",
|
|
21
|
+
".dll",
|
|
22
|
+
".so",
|
|
23
|
+
".png",
|
|
24
|
+
".jpg",
|
|
25
|
+
".jpeg",
|
|
26
|
+
".gif",
|
|
27
|
+
".bmp",
|
|
28
|
+
".ico",
|
|
29
|
+
".webp",
|
|
30
|
+
".pdf",
|
|
31
|
+
".mp3",
|
|
32
|
+
".mp4",
|
|
33
|
+
".zip",
|
|
34
|
+
".gz",
|
|
35
|
+
".tar",
|
|
36
|
+
".bin",
|
|
37
|
+
".dat",
|
|
38
|
+
".class",
|
|
39
|
+
".jar",
|
|
40
|
+
".wasm",
|
|
41
|
+
".node",
|
|
42
|
+
]);
|
|
43
|
+
// Safety valve: stop scanning after this many total matches to prevent
|
|
44
|
+
// runaway searches in large repositories with very common terms.
|
|
45
|
+
const MAX_TOTAL_MATCHES = 10_000;
|
|
46
|
+
// ── Tool implementation ────────────────────────────────────────────
|
|
47
|
+
export function searchWorkspace(input) {
|
|
48
|
+
const config = getConfig();
|
|
49
|
+
// ── Validate session freshness ───────────────────────────────────
|
|
50
|
+
const validation = validateDirectSessionFreshness(input.session_id);
|
|
51
|
+
if (!validation.valid || !validation.session) {
|
|
52
|
+
throw new PatchWardenError(validation.failure_reason || "session_invalid", `Direct session "${input.session_id}" is not valid: ${validation.failure_reason}.`, "Create a new direct session with create_direct_session.", true, {
|
|
53
|
+
session_id: input.session_id,
|
|
54
|
+
failure_reason: validation.failure_reason,
|
|
55
|
+
operation: "search_workspace",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const session = validation.session;
|
|
59
|
+
guardDirectSessionActive(session);
|
|
60
|
+
// ── Validate query ───────────────────────────────────────────────
|
|
61
|
+
if (!input.query || input.query.trim() === "") {
|
|
62
|
+
throw new PatchWardenError("invalid_input", "query is required and must be a non-empty string.", "Provide a search query to look for in file contents.", true, { operation: "search_workspace", session_id: input.session_id });
|
|
63
|
+
}
|
|
64
|
+
// ── Resolve parameters ───────────────────────────────────────────
|
|
65
|
+
const maxResults = input.max_results ?? 20;
|
|
66
|
+
const caseSensitive = input.case_sensitive ?? false;
|
|
67
|
+
const maxPreviewChars = input.max_preview_chars ?? 200;
|
|
68
|
+
const includeGlobs = input.include_globs;
|
|
69
|
+
const query = caseSensitive ? input.query : input.query.toLowerCase();
|
|
70
|
+
const repoPath = session.resolved_repo_path;
|
|
71
|
+
if (!existsSync(repoPath)) {
|
|
72
|
+
throw new PatchWardenError("repo_not_found", `Session repo path "${repoPath}" no longer exists.`, "Create a new direct session with create_direct_session.", true, {
|
|
73
|
+
session_id: input.session_id,
|
|
74
|
+
resolved_repo_path: repoPath,
|
|
75
|
+
operation: "search_workspace",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ── Recursive search ─────────────────────────────────────────────
|
|
79
|
+
const results = [];
|
|
80
|
+
let totalMatches = 0;
|
|
81
|
+
let truncated = false;
|
|
82
|
+
const visit = (directory) => {
|
|
83
|
+
if (truncated && totalMatches >= MAX_TOTAL_MATCHES)
|
|
84
|
+
return;
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (totalMatches >= MAX_TOTAL_MATCHES) {
|
|
94
|
+
truncated = true;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Skip blacklisted directories
|
|
98
|
+
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const fullPath = join(directory, entry.name);
|
|
102
|
+
const relPath = relative(repoPath, fullPath).replace(/\\/g, "/");
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
visit(fullPath);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!entry.isFile())
|
|
108
|
+
continue;
|
|
109
|
+
// Skip sensitive files
|
|
110
|
+
if (isSensitivePath(relPath))
|
|
111
|
+
continue;
|
|
112
|
+
// Skip binary files by extension
|
|
113
|
+
const ext = extname(entry.name).toLowerCase();
|
|
114
|
+
if (BINARY_EXTENSIONS.has(ext))
|
|
115
|
+
continue;
|
|
116
|
+
// Apply include_globs filter
|
|
117
|
+
if (includeGlobs && includeGlobs.length > 0) {
|
|
118
|
+
if (!matchesAnyGlob(relPath, includeGlobs))
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// Read file content and search line by line
|
|
122
|
+
let content;
|
|
123
|
+
try {
|
|
124
|
+
const fileStat = statSync(fullPath);
|
|
125
|
+
if (fileStat.size > config.directMaxFileBytes)
|
|
126
|
+
continue;
|
|
127
|
+
content = readFileSync(fullPath, "utf-8");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const lines = content.split(/\r?\n/);
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
if (totalMatches >= MAX_TOTAL_MATCHES) {
|
|
135
|
+
truncated = true;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const line = lines[i];
|
|
139
|
+
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
140
|
+
if (haystack.includes(query)) {
|
|
141
|
+
totalMatches++;
|
|
142
|
+
if (results.length < maxResults) {
|
|
143
|
+
const preview = line.length > maxPreviewChars
|
|
144
|
+
? line.slice(0, maxPreviewChars)
|
|
145
|
+
: line;
|
|
146
|
+
results.push({
|
|
147
|
+
path: relPath,
|
|
148
|
+
line: i + 1,
|
|
149
|
+
preview,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
truncated = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
visit(repoPath);
|
|
160
|
+
return {
|
|
161
|
+
results,
|
|
162
|
+
total_matches: totalMatches,
|
|
163
|
+
truncated,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ── Glob matching ──────────────────────────────────────────────────
|
|
167
|
+
function matchesAnyGlob(filePath, globs) {
|
|
168
|
+
for (const pattern of globs) {
|
|
169
|
+
if (matchGlob(filePath, pattern))
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Simple glob matching: "*" matches any sequence of characters (including
|
|
176
|
+
* path separators), "?" matches a single character.
|
|
177
|
+
*
|
|
178
|
+
* Examples:
|
|
179
|
+
* "*.ts" matches "foo.ts", "src/bar.ts"
|
|
180
|
+
* "src/*.ts" matches "src/foo.ts", "src/sub/bar.ts"
|
|
181
|
+
* "*.test.ts" matches "foo.test.ts", "src/bar.test.ts"
|
|
182
|
+
*/
|
|
183
|
+
function matchGlob(filePath, pattern) {
|
|
184
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
185
|
+
const regex = globToRegex(normalizedPattern);
|
|
186
|
+
return regex.test(filePath);
|
|
187
|
+
}
|
|
188
|
+
function globToRegex(pattern) {
|
|
189
|
+
let regexStr = "";
|
|
190
|
+
for (const char of pattern) {
|
|
191
|
+
if (char === "*") {
|
|
192
|
+
regexStr += ".*";
|
|
193
|
+
}
|
|
194
|
+
else if (char === "?") {
|
|
195
|
+
regexStr += ".";
|
|
196
|
+
}
|
|
197
|
+
else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
198
|
+
regexStr += "\\" + char;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
regexStr += char;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return new RegExp("^" + regexStr + "$");
|
|
205
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PatchWardenConfig } from "../config.js";
|
|
2
|
+
export interface SyncFileResult {
|
|
3
|
+
source_path: string;
|
|
4
|
+
target_path: string;
|
|
5
|
+
before_target_sha256: string | null;
|
|
6
|
+
after_target_sha256: string;
|
|
7
|
+
source_sha256: string;
|
|
8
|
+
copied_bytes: number;
|
|
9
|
+
changed: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Copy a file from source to target within the same session repo.
|
|
13
|
+
* Both source and target must be inside the session's repo_path.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncFile(sessionId: string, sourcePath: string, targetPath: string, options?: {
|
|
16
|
+
expected_source_sha256?: string;
|
|
17
|
+
expected_target_sha256?: string;
|
|
18
|
+
}, config?: PatchWardenConfig): SyncFileResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { PatchWardenError } from "../errors.js";
|
|
4
|
+
import { getConfig } from "../config.js";
|
|
5
|
+
import { guardDirectPath, guardDirectWritePath } from "../direct/directGuards.js";
|
|
6
|
+
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
7
|
+
import { computeFileSha256 } from "../direct/directPatch.js";
|
|
8
|
+
/**
|
|
9
|
+
* Copy a file from source to target within the same session repo.
|
|
10
|
+
* Both source and target must be inside the session's repo_path.
|
|
11
|
+
*/
|
|
12
|
+
export function syncFile(sessionId, sourcePath, targetPath, options, config) {
|
|
13
|
+
const cfg = config || getConfig();
|
|
14
|
+
const sessionsDir = resolve(cfg.workspaceRoot, cfg.directSessionsDir);
|
|
15
|
+
// Load session to get repo_path
|
|
16
|
+
const sessionFile = resolve(sessionsDir, sessionId, "session.json");
|
|
17
|
+
if (!existsSync(sessionFile)) {
|
|
18
|
+
throw new PatchWardenError("direct_session_not_found", `Direct session "${sessionId}" not found.`, "Create a direct session first using direct_start_session.", true, { session_id: sessionId });
|
|
19
|
+
}
|
|
20
|
+
const session = JSON.parse(readFileSync(sessionFile, "utf-8"));
|
|
21
|
+
const repoPath = resolve(session.repo_path);
|
|
22
|
+
const workspaceRoot = cfg.workspaceRoot;
|
|
23
|
+
// Guard source path — must be inside repo
|
|
24
|
+
const resolvedSource = guardDirectPath(sourcePath, repoPath, workspaceRoot);
|
|
25
|
+
guardSensitivePath(resolvedSource);
|
|
26
|
+
if (!existsSync(resolvedSource)) {
|
|
27
|
+
throw new PatchWardenError("source_file_not_found", `Source file does not exist: "${sourcePath}".`, "Ensure the source path is correct.", true, { source_path: sourcePath });
|
|
28
|
+
}
|
|
29
|
+
// Guard target path — must be inside repo, not in blocked dirs
|
|
30
|
+
const resolvedTarget = guardDirectWritePath(targetPath, repoPath, workspaceRoot);
|
|
31
|
+
guardSensitivePath(resolvedTarget);
|
|
32
|
+
// Verify source sha256 if provided
|
|
33
|
+
const sourceSha256 = computeFileSha256(resolvedSource);
|
|
34
|
+
if (options?.expected_source_sha256 && options.expected_source_sha256 !== sourceSha256) {
|
|
35
|
+
throw new PatchWardenError("source_hash_mismatch", `Source file hash mismatch. Expected "${options.expected_source_sha256}" but got "${sourceSha256}".`, "Re-read the source file to get the current sha256.", true, { expected_sha256: options.expected_source_sha256, actual_sha256: sourceSha256 });
|
|
36
|
+
}
|
|
37
|
+
// Get target sha256 before copy
|
|
38
|
+
let beforeTargetSha256 = null;
|
|
39
|
+
if (existsSync(resolvedTarget)) {
|
|
40
|
+
beforeTargetSha256 = computeFileSha256(resolvedTarget);
|
|
41
|
+
// Verify target sha256 if provided
|
|
42
|
+
if (options?.expected_target_sha256 && options.expected_target_sha256 !== beforeTargetSha256) {
|
|
43
|
+
throw new PatchWardenError("target_hash_mismatch", `Target file hash mismatch. Expected "${options.expected_target_sha256}" but got "${beforeTargetSha256}".`, "Re-read the target file to get the current sha256.", true, { expected_sha256: options.expected_target_sha256, actual_sha256: beforeTargetSha256 });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Read source content
|
|
47
|
+
const sourceContent = readFileSync(resolvedSource);
|
|
48
|
+
const copiedBytes = sourceContent.length;
|
|
49
|
+
// Create target directory if needed
|
|
50
|
+
mkdirSync(dirname(resolvedTarget), { recursive: true });
|
|
51
|
+
// Write to target
|
|
52
|
+
writeFileSync(resolvedTarget, sourceContent, "utf-8");
|
|
53
|
+
// Compute after hash
|
|
54
|
+
const afterTargetSha256 = computeFileSha256(resolvedTarget);
|
|
55
|
+
const changed = beforeTargetSha256 !== afterTargetSha256;
|
|
56
|
+
return {
|
|
57
|
+
source_path: sourcePath,
|
|
58
|
+
target_path: targetPath,
|
|
59
|
+
before_target_sha256: beforeTargetSha256,
|
|
60
|
+
after_target_sha256: afterTargetSha256,
|
|
61
|
+
source_sha256: sourceSha256,
|
|
62
|
+
copied_bytes: copiedBytes,
|
|
63
|
+
changed,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -14,7 +14,7 @@ export declare function getTaskLogTail(taskId: string, file: "stdout" | "stderr"
|
|
|
14
14
|
lines?: number;
|
|
15
15
|
redact?: boolean;
|
|
16
16
|
}): {
|
|
17
|
-
file: "
|
|
17
|
+
file: "test" | "stdout" | "stderr" | "verify";
|
|
18
18
|
lines: number;
|
|
19
19
|
total_bytes: number;
|
|
20
20
|
task_id: string;
|
|
@@ -37,7 +37,7 @@ export declare function getTaskLogTail(taskId: string, file: "stdout" | "stderr"
|
|
|
37
37
|
truncated?: undefined;
|
|
38
38
|
} | {
|
|
39
39
|
task_id: string;
|
|
40
|
-
file: "
|
|
40
|
+
file: "test" | "stdout" | "stderr" | "verify";
|
|
41
41
|
filename: string;
|
|
42
42
|
content: string;
|
|
43
43
|
available: boolean;
|
|
@@ -19,7 +19,8 @@ export function expandTaskTemplate(input) {
|
|
|
19
19
|
`## Goal\n${goal}`,
|
|
20
20
|
"## Safety boundaries",
|
|
21
21
|
"- Work only inside the resolved repository path.",
|
|
22
|
-
"-
|
|
22
|
+
"- Leave repository changes uncommitted for review; remote operations are outside this task.",
|
|
23
|
+
"- Keep credentials private and do not modify unrelated files.",
|
|
23
24
|
"- Report exact files inspected or changed and any remaining uncertainty.",
|
|
24
25
|
];
|
|
25
26
|
switch (input.template) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ToolProfile = "full" | "chatgpt_core";
|
|
1
|
+
export type ToolProfile = "full" | "chatgpt_core" | "chatgpt_direct";
|
|
2
2
|
export interface CatalogTool {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
@@ -12,8 +12,9 @@ export interface ToolCatalogSnapshot {
|
|
|
12
12
|
tool_names: string[];
|
|
13
13
|
tool_manifest_sha256: string;
|
|
14
14
|
}
|
|
15
|
-
export declare const CHATGPT_CORE_TOOL_NAMES: readonly ["health_check", "list_agents", "list_workspace", "read_workspace_file", "save_plan", "create_task", "wait_for_task", "get_task_summary", "get_diff", "get_result", "get_result_json", "get_test_log", "get_task_status", "list_tasks", "cancel_task", "audit_task"];
|
|
15
|
+
export declare const CHATGPT_CORE_TOOL_NAMES: readonly ["health_check", "list_agents", "list_workspace", "read_workspace_file", "save_plan", "create_task", "wait_for_task", "get_task_summary", "get_diff", "get_result", "get_result_json", "get_test_log", "get_task_status", "list_tasks", "cancel_task", "audit_task", "safe_status"];
|
|
16
|
+
export declare const CHATGPT_DIRECT_TOOL_NAMES: readonly ["health_check", "list_workspace", "create_direct_session", "search_workspace", "read_workspace_file", "apply_patch", "run_verification", "finalize_direct_session", "audit_session", "sync_file"];
|
|
16
17
|
export declare function resolveToolProfile(configProfile?: string): ToolProfile;
|
|
17
|
-
export declare function selectToolsForProfile<T extends CatalogTool>(tools: T[], profile: ToolProfile): T[];
|
|
18
|
+
export declare function selectToolsForProfile<T extends CatalogTool>(tools: T[], profile: ToolProfile, enableDirectProfile?: boolean): T[];
|
|
18
19
|
export declare function buildToolCatalogSnapshot(tools: CatalogTool[], profile: ToolProfile): ToolCatalogSnapshot;
|
|
19
20
|
export declare function getLastToolCatalogSnapshot(): ToolCatalogSnapshot | null;
|
|
@@ -17,23 +17,52 @@ export const CHATGPT_CORE_TOOL_NAMES = [
|
|
|
17
17
|
"list_tasks",
|
|
18
18
|
"cancel_task",
|
|
19
19
|
"audit_task",
|
|
20
|
+
"safe_status",
|
|
21
|
+
];
|
|
22
|
+
export const CHATGPT_DIRECT_TOOL_NAMES = [
|
|
23
|
+
"health_check",
|
|
24
|
+
"list_workspace",
|
|
25
|
+
"create_direct_session",
|
|
26
|
+
"search_workspace",
|
|
27
|
+
"read_workspace_file",
|
|
28
|
+
"apply_patch",
|
|
29
|
+
"run_verification",
|
|
30
|
+
"finalize_direct_session",
|
|
31
|
+
"audit_session",
|
|
32
|
+
"sync_file",
|
|
20
33
|
];
|
|
21
34
|
let lastSnapshot = null;
|
|
22
35
|
export function resolveToolProfile(configProfile) {
|
|
23
36
|
const raw = (process.env.PATCHWARDEN_TOOL_PROFILE || configProfile || "full").trim();
|
|
24
|
-
if (raw !== "full" && raw !== "chatgpt_core") {
|
|
25
|
-
throw new Error(`Invalid tool profile "${raw}". Expected "full" or "
|
|
37
|
+
if (raw !== "full" && raw !== "chatgpt_core" && raw !== "chatgpt_direct") {
|
|
38
|
+
throw new Error(`Invalid tool profile "${raw}". Expected "full", "chatgpt_core", or "chatgpt_direct".`);
|
|
26
39
|
}
|
|
27
40
|
return raw;
|
|
28
41
|
}
|
|
29
|
-
export function selectToolsForProfile(tools, profile) {
|
|
42
|
+
export function selectToolsForProfile(tools, profile, enableDirectProfile) {
|
|
30
43
|
if (profile === "full")
|
|
31
44
|
return tools;
|
|
32
45
|
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
33
|
-
|
|
46
|
+
if (profile === "chatgpt_core") {
|
|
47
|
+
return CHATGPT_CORE_TOOL_NAMES.map((name) => {
|
|
48
|
+
const tool = byName.get(name);
|
|
49
|
+
if (!tool)
|
|
50
|
+
throw new Error(`chatgpt_core tool profile requires missing tool "${name}".`);
|
|
51
|
+
return tool;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// chatgpt_direct
|
|
55
|
+
if (!enableDirectProfile) {
|
|
56
|
+
// Degraded mode: only expose health_check for diagnostics
|
|
57
|
+
const tool = byName.get("health_check");
|
|
58
|
+
if (!tool)
|
|
59
|
+
throw new Error(`chatgpt_direct degraded mode requires missing tool "health_check".`);
|
|
60
|
+
return [tool];
|
|
61
|
+
}
|
|
62
|
+
return CHATGPT_DIRECT_TOOL_NAMES.map((name) => {
|
|
34
63
|
const tool = byName.get(name);
|
|
35
64
|
if (!tool)
|
|
36
|
-
throw new Error(`
|
|
65
|
+
throw new Error(`chatgpt_direct tool profile requires missing tool "${name}".`);
|
|
37
66
|
return tool;
|
|
38
67
|
});
|
|
39
68
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type TaskSummaryResult } from "./getTaskSummary.js";
|
|
2
2
|
export interface WaitForTaskProgressSummary {
|
|
3
3
|
phase: string;
|
|
4
4
|
heartbeat_at: string;
|
|
@@ -19,7 +19,7 @@ export interface WaitForTaskOutput {
|
|
|
19
19
|
name: "wait_for_task" | "audit_task" | "health_check";
|
|
20
20
|
arguments: Record<string, unknown>;
|
|
21
21
|
};
|
|
22
|
-
summary?:
|
|
22
|
+
summary?: TaskSummaryResult;
|
|
23
23
|
progress_summary?: WaitForTaskProgressSummary;
|
|
24
24
|
}
|
|
25
25
|
export declare function waitForTask(taskId: string, waitSeconds?: number): Promise<WaitForTaskOutput>;
|
|
@@ -54,6 +54,6 @@ export async function waitForTask(taskId, waitSeconds = 25) {
|
|
|
54
54
|
: executionBlocked
|
|
55
55
|
? { name: "health_check", arguments: {} }
|
|
56
56
|
: { name: "wait_for_task", arguments: { task_id: taskId, timeout_seconds: waitSeconds } },
|
|
57
|
-
...(terminal ? { summary: getTaskSummary(taskId) } : { progress_summary: progressSummary }),
|
|
57
|
+
...(terminal ? { summary: getTaskSummary(taskId, { view: "compact" }) } : { progress_summary: progressSummary }),
|
|
58
58
|
};
|
|
59
59
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PATCHWARDEN_VERSION = "0.
|
|
2
|
-
export declare const TOOL_SCHEMA_EPOCH = "2026-06-
|
|
1
|
+
export declare const PATCHWARDEN_VERSION = "0.6.1";
|
|
2
|
+
export declare const TOOL_SCHEMA_EPOCH = "2026-06-24-v7";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PATCHWARDEN_VERSION = "0.
|
|
2
|
-
export const TOOL_SCHEMA_EPOCH = "2026-06-
|
|
1
|
+
export const PATCHWARDEN_VERSION = "0.6.1";
|
|
2
|
+
export const TOOL_SCHEMA_EPOCH = "2026-06-24-v7";
|
package/dist/watcherStatus.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface WatcherStatusSnapshot {
|
|
|
11
11
|
instance_id: string | null;
|
|
12
12
|
launcher_pid: number | null;
|
|
13
13
|
reason: string | null;
|
|
14
|
+
activity: string | null;
|
|
14
15
|
}
|
|
15
16
|
export declare function getWatcherHeartbeatPath(config?: PatchWardenConfig): string;
|
|
16
17
|
export declare function readWatcherStatus(config?: PatchWardenConfig, nowMs?: number): WatcherStatusSnapshot;
|
package/dist/watcherStatus.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { getConfig, getTasksDir } from "./config.js";
|
|
4
4
|
export function getWatcherHeartbeatPath(config = getConfig()) {
|
|
@@ -8,6 +8,22 @@ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
|
|
|
8
8
|
const staleAfterSeconds = config.watcherStaleSeconds;
|
|
9
9
|
const heartbeatPath = getWatcherHeartbeatPath(config);
|
|
10
10
|
if (!existsSync(heartbeatPath)) {
|
|
11
|
+
// Even if watcher heartbeat is missing, check if a task is actively running
|
|
12
|
+
const taskFallback = checkRunningTaskHeartbeat(config, nowMs, staleAfterSeconds);
|
|
13
|
+
if (taskFallback) {
|
|
14
|
+
return {
|
|
15
|
+
status: "healthy",
|
|
16
|
+
available: true,
|
|
17
|
+
stale_after_seconds: staleAfterSeconds,
|
|
18
|
+
last_heartbeat_at: taskFallback.heartbeat_at,
|
|
19
|
+
heartbeat_age_seconds: taskFallback.age_seconds,
|
|
20
|
+
heartbeat_pid: null,
|
|
21
|
+
instance_id: null,
|
|
22
|
+
launcher_pid: null,
|
|
23
|
+
reason: null,
|
|
24
|
+
activity: taskFallback.activity,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
11
27
|
return {
|
|
12
28
|
status: "missing",
|
|
13
29
|
available: false,
|
|
@@ -18,6 +34,7 @@ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
|
|
|
18
34
|
instance_id: null,
|
|
19
35
|
launcher_pid: null,
|
|
20
36
|
reason: "Watcher heartbeat has not been observed. Start or restart the PatchWarden watcher.",
|
|
37
|
+
activity: null,
|
|
21
38
|
};
|
|
22
39
|
}
|
|
23
40
|
try {
|
|
@@ -29,16 +46,47 @@ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
|
|
|
29
46
|
const ageMs = Math.max(0, nowMs - heartbeatMs);
|
|
30
47
|
const ageSeconds = Math.round(ageMs / 1000);
|
|
31
48
|
const healthy = ageMs < staleAfterSeconds * 1000;
|
|
49
|
+
if (healthy) {
|
|
50
|
+
return {
|
|
51
|
+
status: "healthy",
|
|
52
|
+
available: true,
|
|
53
|
+
stale_after_seconds: staleAfterSeconds,
|
|
54
|
+
last_heartbeat_at: String(data.last_heartbeat_at),
|
|
55
|
+
heartbeat_age_seconds: ageSeconds,
|
|
56
|
+
heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
|
|
57
|
+
instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
|
|
58
|
+
launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
|
|
59
|
+
reason: null,
|
|
60
|
+
activity: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Watcher heartbeat is stale — check if a task is actively running
|
|
64
|
+
const taskFallback = checkRunningTaskHeartbeat(config, nowMs, staleAfterSeconds);
|
|
65
|
+
if (taskFallback) {
|
|
66
|
+
return {
|
|
67
|
+
status: "healthy",
|
|
68
|
+
available: true,
|
|
69
|
+
stale_after_seconds: staleAfterSeconds,
|
|
70
|
+
last_heartbeat_at: taskFallback.heartbeat_at,
|
|
71
|
+
heartbeat_age_seconds: taskFallback.age_seconds,
|
|
72
|
+
heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
|
|
73
|
+
instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
|
|
74
|
+
launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
|
|
75
|
+
reason: null,
|
|
76
|
+
activity: taskFallback.activity,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
32
79
|
return {
|
|
33
|
-
status:
|
|
34
|
-
available:
|
|
80
|
+
status: "stale",
|
|
81
|
+
available: false,
|
|
35
82
|
stale_after_seconds: staleAfterSeconds,
|
|
36
83
|
last_heartbeat_at: String(data.last_heartbeat_at),
|
|
37
84
|
heartbeat_age_seconds: ageSeconds,
|
|
38
85
|
heartbeat_pid: Number.isInteger(Number(data.pid)) ? Number(data.pid) : null,
|
|
39
86
|
instance_id: typeof data.instance_id === "string" ? data.instance_id : null,
|
|
40
87
|
launcher_pid: Number.isInteger(Number(data.launcher_pid)) ? Number(data.launcher_pid) : null,
|
|
41
|
-
reason:
|
|
88
|
+
reason: "Watcher heartbeat is stale. Restart the PatchWarden watcher.",
|
|
89
|
+
activity: null,
|
|
42
90
|
};
|
|
43
91
|
}
|
|
44
92
|
catch {
|
|
@@ -52,9 +100,53 @@ export function readWatcherStatus(config = getConfig(), nowMs = Date.now()) {
|
|
|
52
100
|
instance_id: null,
|
|
53
101
|
launcher_pid: null,
|
|
54
102
|
reason: "Watcher heartbeat file is unreadable.",
|
|
103
|
+
activity: null,
|
|
55
104
|
};
|
|
56
105
|
}
|
|
57
106
|
}
|
|
107
|
+
function checkRunningTaskHeartbeat(config, nowMs, staleAfterSeconds) {
|
|
108
|
+
const tasksDir = getTasksDir(config);
|
|
109
|
+
if (!existsSync(tasksDir))
|
|
110
|
+
return null;
|
|
111
|
+
let entries;
|
|
112
|
+
try {
|
|
113
|
+
entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.isDirectory())
|
|
120
|
+
continue;
|
|
121
|
+
const taskDir = join(tasksDir, entry.name);
|
|
122
|
+
const statusFile = join(taskDir, "status.json");
|
|
123
|
+
const runtimeFile = join(taskDir, "runtime.json");
|
|
124
|
+
if (!existsSync(statusFile) || !existsSync(runtimeFile))
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
const status = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
128
|
+
if (status.status !== "running")
|
|
129
|
+
continue;
|
|
130
|
+
const runtime = JSON.parse(readFileSync(runtimeFile, "utf-8"));
|
|
131
|
+
const heartbeatAt = String(runtime.last_heartbeat_at || "");
|
|
132
|
+
const heartbeatMs = Date.parse(heartbeatAt);
|
|
133
|
+
if (!Number.isFinite(heartbeatMs))
|
|
134
|
+
continue;
|
|
135
|
+
const ageMs = Math.max(0, nowMs - heartbeatMs);
|
|
136
|
+
if (ageMs < staleAfterSeconds * 1000) {
|
|
137
|
+
return {
|
|
138
|
+
heartbeat_at: heartbeatAt,
|
|
139
|
+
age_seconds: Math.round(ageMs / 1000),
|
|
140
|
+
activity: `watcher busy executing task ${entry.name}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
58
150
|
export function derivePendingReason(task, watcher) {
|
|
59
151
|
if (task.status === "pending") {
|
|
60
152
|
if (watcher.status === "stale")
|