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
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
statSync,
|
|
8
8
|
writeFileSync,
|
|
9
9
|
} from "node:fs";
|
|
10
|
-
import { join, relative, resolve } from "node:path";
|
|
10
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
13
13
|
|
|
@@ -19,6 +19,8 @@ const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
|
|
|
19
19
|
export interface FileFingerprint {
|
|
20
20
|
size: number;
|
|
21
21
|
sha256: string;
|
|
22
|
+
tracked: boolean;
|
|
23
|
+
ignored: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export interface RepoSnapshot {
|
|
@@ -28,6 +30,7 @@ export interface RepoSnapshot {
|
|
|
28
30
|
status: string;
|
|
29
31
|
workspace_dirty: boolean;
|
|
30
32
|
files: Record<string, FileFingerprint>;
|
|
33
|
+
dirty_paths: string[]; // paths that git status --porcelain reports as modified/added/deleted/untracked/renamed
|
|
31
34
|
warnings: string[];
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -37,6 +40,33 @@ export interface ChangedFile {
|
|
|
37
40
|
old_path?: string;
|
|
38
41
|
before_sha256: string | null;
|
|
39
42
|
after_sha256: string | null;
|
|
43
|
+
tracked: boolean;
|
|
44
|
+
ignored: boolean;
|
|
45
|
+
kind: "source" | "build_artifact" | "runtime_generated";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ClassifiedChange {
|
|
49
|
+
path: string;
|
|
50
|
+
change: ChangedFile["change"];
|
|
51
|
+
tracked: boolean;
|
|
52
|
+
ignored: boolean;
|
|
53
|
+
kind: ChangedFile["kind"];
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ArtifactHygiene {
|
|
58
|
+
counts: {
|
|
59
|
+
source_changes: number;
|
|
60
|
+
tracked_build_artifacts: number;
|
|
61
|
+
ignored_untracked_artifacts: number;
|
|
62
|
+
runtime_generated_files: number;
|
|
63
|
+
suspicious_changes: number;
|
|
64
|
+
};
|
|
65
|
+
source_changes: ClassifiedChange[];
|
|
66
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
67
|
+
ignored_untracked_artifacts: ClassifiedChange[];
|
|
68
|
+
runtime_generated_files: ClassifiedChange[];
|
|
69
|
+
suspicious_changes: ClassifiedChange[];
|
|
40
70
|
}
|
|
41
71
|
|
|
42
72
|
export interface ChangeArtifacts {
|
|
@@ -57,6 +87,7 @@ export interface ChangeArtifacts {
|
|
|
57
87
|
workspace_dirty_after: boolean;
|
|
58
88
|
patch_mode: "textual" | "no_changes" | "hash_only";
|
|
59
89
|
unavailable_reason: string | null;
|
|
90
|
+
artifact_hygiene: ArtifactHygiene;
|
|
60
91
|
}
|
|
61
92
|
|
|
62
93
|
export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
@@ -65,11 +96,45 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
65
96
|
let head: string | null = null;
|
|
66
97
|
let status = "";
|
|
67
98
|
let paths: string[] = [];
|
|
99
|
+
const trackedPaths = new Set<string>();
|
|
100
|
+
const ignoredPaths = new Set<string>();
|
|
68
101
|
|
|
102
|
+
const dirtyPaths = new Set<string>();
|
|
69
103
|
if (isGit) {
|
|
70
104
|
const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
|
|
71
105
|
if (headResult.status === 0) head = headResult.stdout.trim() || null;
|
|
72
106
|
status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
|
|
107
|
+
// Parse git status --porcelain to collect all dirty paths
|
|
108
|
+
for (const line of status.split("\n")) {
|
|
109
|
+
if (line.length < 4) continue;
|
|
110
|
+
const st = line.slice(0, 2); // XY status codes
|
|
111
|
+
const rawPath = line.slice(3);
|
|
112
|
+
// M=modified, A=added, D=deleted, ?=untracked, R=renamed, !=ignored
|
|
113
|
+
if (/[MAD\?R]/.test(st)) {
|
|
114
|
+
if (st.includes("R")) {
|
|
115
|
+
// Rename: rawPath is "oldname -> newname"
|
|
116
|
+
const parts = rawPath.split(" -> ");
|
|
117
|
+
if (parts.length === 2) {
|
|
118
|
+
dirtyPaths.add(normalizePath(parts[0]));
|
|
119
|
+
dirtyPaths.add(normalizePath(parts[1]));
|
|
120
|
+
} else {
|
|
121
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const tracked = runGit(repoPath, ["ls-files", "-z"]);
|
|
129
|
+
if (tracked.status === 0) {
|
|
130
|
+
for (const path of tracked.stdout.split("\0").filter(Boolean)) trackedPaths.add(normalizePath(path));
|
|
131
|
+
}
|
|
132
|
+
const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
|
|
133
|
+
if (ignored.status === 0) {
|
|
134
|
+
for (const path of ignored.stdout.split("\0").filter(Boolean)) ignoredPaths.add(normalizePath(path));
|
|
135
|
+
} else {
|
|
136
|
+
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
137
|
+
}
|
|
73
138
|
const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
|
|
74
139
|
if (listed.status === 0) {
|
|
75
140
|
paths = [...new Set([
|
|
@@ -92,7 +157,7 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
92
157
|
|
|
93
158
|
const files: Record<string, FileFingerprint> = {};
|
|
94
159
|
for (const inputPath of paths.sort()) {
|
|
95
|
-
const normalized = inputPath
|
|
160
|
+
const normalized = normalizePath(inputPath);
|
|
96
161
|
if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized)) continue;
|
|
97
162
|
const absolutePath = resolve(repoPath, inputPath);
|
|
98
163
|
try {
|
|
@@ -101,7 +166,12 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
101
166
|
const sha256 = stat.size <= MAX_HASH_BYTES
|
|
102
167
|
? createHash("sha256").update(readFileSync(absolutePath)).digest("hex")
|
|
103
168
|
: `large-file:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
104
|
-
files[normalized] = {
|
|
169
|
+
files[normalized] = {
|
|
170
|
+
size: stat.size,
|
|
171
|
+
sha256,
|
|
172
|
+
tracked: trackedPaths.has(normalized),
|
|
173
|
+
ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
|
|
174
|
+
};
|
|
105
175
|
} catch {
|
|
106
176
|
warnings.push(`could not fingerprint: ${normalized}`);
|
|
107
177
|
}
|
|
@@ -114,6 +184,7 @@ export function captureRepoSnapshot(repoPath: string): RepoSnapshot {
|
|
|
114
184
|
status,
|
|
115
185
|
workspace_dirty: status.trim().length > 0,
|
|
116
186
|
files,
|
|
187
|
+
dirty_paths: [...dirtyPaths],
|
|
117
188
|
warnings,
|
|
118
189
|
};
|
|
119
190
|
}
|
|
@@ -128,6 +199,7 @@ export function buildChangeArtifacts(
|
|
|
128
199
|
after: RepoSnapshot
|
|
129
200
|
): ChangeArtifacts {
|
|
130
201
|
const changedFiles = compareSnapshots(before, after);
|
|
202
|
+
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
131
203
|
const sections: string[] = [];
|
|
132
204
|
const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
|
|
133
205
|
|
|
@@ -179,6 +251,7 @@ export function buildChangeArtifacts(
|
|
|
179
251
|
? "Git could not produce a textual patch for the changed files; hash evidence remains available."
|
|
180
252
|
: "Repository is not a Git worktree; only bounded hash evidence is available.")
|
|
181
253
|
: null,
|
|
254
|
+
artifact_hygiene: artifactHygiene,
|
|
182
255
|
};
|
|
183
256
|
}
|
|
184
257
|
|
|
@@ -232,11 +305,11 @@ export function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): Cha
|
|
|
232
305
|
const left = before.files[path];
|
|
233
306
|
const right = after.files[path];
|
|
234
307
|
if (!left && right) {
|
|
235
|
-
changed.push(
|
|
308
|
+
changed.push(classifyChangedFile(path, "added", null, right));
|
|
236
309
|
} else if (left && !right) {
|
|
237
|
-
changed.push(
|
|
310
|
+
changed.push(classifyChangedFile(path, "deleted", left, null));
|
|
238
311
|
} else if (left.sha256 !== right.sha256) {
|
|
239
|
-
changed.push(
|
|
312
|
+
changed.push(classifyChangedFile(path, "modified", left, right));
|
|
240
313
|
}
|
|
241
314
|
}
|
|
242
315
|
const deletedByHash = new Map<string, ChangedFile[]>();
|
|
@@ -260,6 +333,9 @@ export function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): Cha
|
|
|
260
333
|
change: "renamed",
|
|
261
334
|
before_sha256: source.before_sha256,
|
|
262
335
|
after_sha256: file.after_sha256,
|
|
336
|
+
tracked: file.tracked || source.tracked,
|
|
337
|
+
ignored: file.ignored,
|
|
338
|
+
kind: classifyPathKind(file.path),
|
|
263
339
|
});
|
|
264
340
|
}
|
|
265
341
|
|
|
@@ -267,6 +343,276 @@ export function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): Cha
|
|
|
267
343
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
268
344
|
}
|
|
269
345
|
|
|
346
|
+
export function emptyArtifactHygiene(): ArtifactHygiene {
|
|
347
|
+
return {
|
|
348
|
+
counts: {
|
|
349
|
+
source_changes: 0,
|
|
350
|
+
tracked_build_artifacts: 0,
|
|
351
|
+
ignored_untracked_artifacts: 0,
|
|
352
|
+
runtime_generated_files: 0,
|
|
353
|
+
suspicious_changes: 0,
|
|
354
|
+
},
|
|
355
|
+
source_changes: [],
|
|
356
|
+
tracked_build_artifacts: [],
|
|
357
|
+
ignored_untracked_artifacts: [],
|
|
358
|
+
runtime_generated_files: [],
|
|
359
|
+
suspicious_changes: [],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Phase 4: External dirty file baseline ─────────────────────────
|
|
364
|
+
|
|
365
|
+
export interface ExternalDirtyFile {
|
|
366
|
+
path: string;
|
|
367
|
+
change: ChangedFile["change"];
|
|
368
|
+
before_sha256: string | null;
|
|
369
|
+
after_sha256: string | null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Extract files that are dirty in the workspace but outside the target repo.
|
|
374
|
+
* Used to establish a baseline before task execution.
|
|
375
|
+
*/
|
|
376
|
+
export function extractExternalDirtyFiles(
|
|
377
|
+
workspaceSnapshot: RepoSnapshot,
|
|
378
|
+
repoPath: string,
|
|
379
|
+
workspaceRoot: string
|
|
380
|
+
): ExternalDirtyFile[] {
|
|
381
|
+
const dirtyFiles: ExternalDirtyFile[] = [];
|
|
382
|
+
const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
|
|
383
|
+
for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
|
|
384
|
+
const absolutePath = resolve(workspaceRoot, path);
|
|
385
|
+
const rel = relative(repoPath, absolutePath);
|
|
386
|
+
// If the path is outside repoPath (starts with .. or is absolute)
|
|
387
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
388
|
+
// A file is "external dirty" if:
|
|
389
|
+
// 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
|
|
390
|
+
// 2. It's not tracked by git (untracked file), OR
|
|
391
|
+
// 3. It's explicitly ignored
|
|
392
|
+
const isDirty = dirtyPathSet.has(path);
|
|
393
|
+
const isUntracked = !fingerprint.tracked;
|
|
394
|
+
const isIgnored = fingerprint.ignored;
|
|
395
|
+
if (isDirty || isUntracked || isIgnored) {
|
|
396
|
+
dirtyFiles.push({
|
|
397
|
+
path,
|
|
398
|
+
change: isDirty ? "modified" : "added",
|
|
399
|
+
before_sha256: fingerprint.sha256,
|
|
400
|
+
after_sha256: null,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return dirtyFiles;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Compare external dirty files between baseline and post-task snapshots.
|
|
410
|
+
* Returns files that are NEW (not present in baseline) or CHANGED
|
|
411
|
+
* (same path but different sha256, meaning the task modified them).
|
|
412
|
+
*/
|
|
413
|
+
export function findNewExternalDirtyFiles(
|
|
414
|
+
baseline: ExternalDirtyFile[],
|
|
415
|
+
current: ExternalDirtyFile[]
|
|
416
|
+
): ExternalDirtyFile[] {
|
|
417
|
+
const baselineMap = new Map(baseline.map((f) => [f.path, f]));
|
|
418
|
+
return current.filter((f) => {
|
|
419
|
+
const baselineFile = baselineMap.get(f.path);
|
|
420
|
+
if (!baselineFile) return true; // New path — definitely new
|
|
421
|
+
// Same path but content changed during task execution
|
|
422
|
+
if (baselineFile.before_sha256 !== f.before_sha256) return true;
|
|
423
|
+
return false;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Phase 6: Artifact manifest ────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
export interface ArtifactManifestEntry {
|
|
430
|
+
path: string;
|
|
431
|
+
type: string;
|
|
432
|
+
size: number;
|
|
433
|
+
sha256: string;
|
|
434
|
+
generated_by: string;
|
|
435
|
+
created_at: string;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface ArtifactManifest {
|
|
439
|
+
task_id: string | null;
|
|
440
|
+
generated_at: string;
|
|
441
|
+
artifacts: ArtifactManifestEntry[];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function buildArtifactManifest(
|
|
445
|
+
changedFiles: ChangedFile[],
|
|
446
|
+
repoPath: string,
|
|
447
|
+
taskId?: string
|
|
448
|
+
): ArtifactManifest {
|
|
449
|
+
const entries: ArtifactManifestEntry[] = [];
|
|
450
|
+
for (const file of changedFiles) {
|
|
451
|
+
if (file.kind !== "build_artifact") continue;
|
|
452
|
+
const absolutePath = resolve(repoPath, file.path);
|
|
453
|
+
let size = 0;
|
|
454
|
+
let sha256 = file.after_sha256 || "unknown";
|
|
455
|
+
try {
|
|
456
|
+
const stat = lstatSync(absolutePath);
|
|
457
|
+
if (stat.isFile()) {
|
|
458
|
+
size = stat.size;
|
|
459
|
+
if (size <= MAX_HASH_BYTES) {
|
|
460
|
+
sha256 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch {
|
|
464
|
+
// File may have been deleted
|
|
465
|
+
}
|
|
466
|
+
entries.push({
|
|
467
|
+
path: file.path,
|
|
468
|
+
type: classifyArtifactType(file.path),
|
|
469
|
+
size,
|
|
470
|
+
sha256,
|
|
471
|
+
generated_by: "task_execution",
|
|
472
|
+
created_at: new Date().toISOString(),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
task_id: taskId || null,
|
|
477
|
+
generated_at: new Date().toISOString(),
|
|
478
|
+
artifacts: entries,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function classifyArtifactType(path: string): string {
|
|
483
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
484
|
+
const basename = normalized.split("/").pop() || "";
|
|
485
|
+
if (basename.endsWith(".exe")) return "windows_exe";
|
|
486
|
+
if (basename.endsWith(".apk")) return "android_apk";
|
|
487
|
+
if (basename.endsWith(".zip")) return "zip";
|
|
488
|
+
if (basename.endsWith(".asar")) return "asar";
|
|
489
|
+
if (basename.endsWith(".dll")) return "dll";
|
|
490
|
+
if (basename.endsWith(".pak")) return "pak";
|
|
491
|
+
return "release_directory_file";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── Phase 6: Changed file grouping ────────────────────────────────
|
|
495
|
+
|
|
496
|
+
export interface ChangedFileGroups {
|
|
497
|
+
source_changes: ChangedFile[];
|
|
498
|
+
docs_changes: ChangedFile[];
|
|
499
|
+
config_changes: ChangedFile[];
|
|
500
|
+
test_changes: ChangedFile[];
|
|
501
|
+
release_artifacts: ChangedFile[];
|
|
502
|
+
runtime_generated_files: ChangedFile[];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function groupChangedFiles(changedFiles: ChangedFile[]): ChangedFileGroups {
|
|
506
|
+
const groups: ChangedFileGroups = {
|
|
507
|
+
source_changes: [],
|
|
508
|
+
docs_changes: [],
|
|
509
|
+
config_changes: [],
|
|
510
|
+
test_changes: [],
|
|
511
|
+
release_artifacts: [],
|
|
512
|
+
runtime_generated_files: [],
|
|
513
|
+
};
|
|
514
|
+
for (const file of changedFiles) {
|
|
515
|
+
const normalized = normalizePath(file.path).toLowerCase();
|
|
516
|
+
const parts = normalized.split("/");
|
|
517
|
+
const basename = parts[parts.length - 1] || "";
|
|
518
|
+
// Check for docs
|
|
519
|
+
if (parts.some((p) => p === "docs") || /\.(md|rst|txt)$/.test(basename)) {
|
|
520
|
+
groups.docs_changes.push(file);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Check for config
|
|
524
|
+
if (basename === "package.json" || basename === "tsconfig.json" || basename === ".gitignore" ||
|
|
525
|
+
basename.startsWith(".config") || basename.endsWith(".config.js") || basename.endsWith(".config.ts")) {
|
|
526
|
+
groups.config_changes.push(file);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
// Check for test files
|
|
530
|
+
if (basename.includes(".test.") || basename.includes(".spec.") || parts.some((p) => p === "test" || p === "tests" || p === "__tests__")) {
|
|
531
|
+
groups.test_changes.push(file);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
// Check for build artifacts / release
|
|
535
|
+
if (file.kind === "build_artifact") {
|
|
536
|
+
groups.release_artifacts.push(file);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
// Check for runtime generated
|
|
540
|
+
if (file.kind === "runtime_generated") {
|
|
541
|
+
groups.runtime_generated_files.push(file);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
// Default: source changes
|
|
545
|
+
groups.source_changes.push(file);
|
|
546
|
+
}
|
|
547
|
+
return groups;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function classifyChangedFile(
|
|
551
|
+
path: string,
|
|
552
|
+
change: ChangedFile["change"],
|
|
553
|
+
before: FileFingerprint | null,
|
|
554
|
+
after: FileFingerprint | null
|
|
555
|
+
): ChangedFile {
|
|
556
|
+
return {
|
|
557
|
+
path,
|
|
558
|
+
change,
|
|
559
|
+
before_sha256: before?.sha256 || null,
|
|
560
|
+
after_sha256: after?.sha256 || null,
|
|
561
|
+
tracked: Boolean(after?.tracked || before?.tracked),
|
|
562
|
+
ignored: Boolean(after?.ignored ?? before?.ignored),
|
|
563
|
+
kind: classifyPathKind(path),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function classifyArtifactHygiene(changes: ChangedFile[]): ArtifactHygiene {
|
|
568
|
+
const hygiene = emptyArtifactHygiene();
|
|
569
|
+
const entries = changes.map((change): ClassifiedChange => ({
|
|
570
|
+
path: change.path,
|
|
571
|
+
change: change.change,
|
|
572
|
+
tracked: change.tracked,
|
|
573
|
+
ignored: change.ignored,
|
|
574
|
+
kind: change.kind,
|
|
575
|
+
reason: classificationReason(change),
|
|
576
|
+
}));
|
|
577
|
+
hygiene.source_changes = entries.filter((entry) => entry.kind === "source" && !entry.ignored);
|
|
578
|
+
hygiene.tracked_build_artifacts = entries.filter((entry) => entry.kind === "build_artifact" && entry.tracked);
|
|
579
|
+
hygiene.ignored_untracked_artifacts = entries.filter((entry) => entry.ignored && !entry.tracked);
|
|
580
|
+
hygiene.runtime_generated_files = entries.filter((entry) => entry.kind === "runtime_generated");
|
|
581
|
+
hygiene.suspicious_changes = entries.filter((entry) =>
|
|
582
|
+
(entry.kind === "build_artifact" || entry.kind === "runtime_generated") && !entry.ignored
|
|
583
|
+
);
|
|
584
|
+
hygiene.counts = {
|
|
585
|
+
source_changes: hygiene.source_changes.length,
|
|
586
|
+
tracked_build_artifacts: hygiene.tracked_build_artifacts.length,
|
|
587
|
+
ignored_untracked_artifacts: hygiene.ignored_untracked_artifacts.length,
|
|
588
|
+
runtime_generated_files: hygiene.runtime_generated_files.length,
|
|
589
|
+
suspicious_changes: hygiene.suspicious_changes.length,
|
|
590
|
+
};
|
|
591
|
+
return hygiene;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function classifyPathKind(path: string): ChangedFile["kind"] {
|
|
595
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
596
|
+
const parts = normalized.split("/");
|
|
597
|
+
const basename = parts[parts.length - 1] || "";
|
|
598
|
+
if (basename === "sync-store.json" || /\.(log|tmp|temp|pid)$/.test(basename)) return "runtime_generated";
|
|
599
|
+
if (parts.some((part) => ["dist", "release", "build", "out", "coverage", ".next"].includes(part))) return "build_artifact";
|
|
600
|
+
if (/\.(exe|dll|pak|bin|zip|tgz|tar\.gz)$/.test(basename)) return "build_artifact";
|
|
601
|
+
return "source";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function classificationReason(change: ChangedFile): string {
|
|
605
|
+
if (change.ignored) return "untracked path is ignored by repository Git rules";
|
|
606
|
+
if (change.kind === "build_artifact" && change.tracked) return "artifact-like path is tracked by Git and requires review";
|
|
607
|
+
if (change.kind === "build_artifact") return "artifact-like path is not ignored and requires review";
|
|
608
|
+
if (change.kind === "runtime_generated") return "runtime-generated path is not ignored and requires review";
|
|
609
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function normalizePath(value: string): string {
|
|
613
|
+
return value.replace(/\\/g, "/");
|
|
614
|
+
}
|
|
615
|
+
|
|
270
616
|
function walkWorkspace(root: string): string[] {
|
|
271
617
|
const result: string[] = [];
|
|
272
618
|
const visit = (directory: string) => {
|