patchwarden 0.6.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/README.md +3 -3
- package/dist/doctor.js +1 -1
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/changeCapture.d.ts +41 -0
- package/dist/runner/changeCapture.js +171 -1
- package/dist/runner/runTask.js +204 -24
- package/dist/smoke-test.js +8 -8
- 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/auditTask.js +11 -5
- package/dist/tools/getTaskSummary.d.ts +3 -0
- package/dist/tools/getTaskSummary.js +15 -1
- package/dist/tools/healthCheck.d.ts +5 -0
- package/dist/tools/healthCheck.js +21 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -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/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +2 -0
- 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/performance-notes.md +55 -0
- package/docs/release-v0.6.1.md +75 -0
- package/package.json +3 -2
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/lifecycle-smoke.js +336 -2
- package/scripts/mcp-manifest-check.js +30 -7
- package/scripts/mcp-smoke.js +11 -8
- package/scripts/pack-clean.js +157 -1
- package/scripts/unit-tests.js +36 -0
- package/src/doctor.ts +1 -1
- package/src/logging.ts +152 -0
- package/src/runner/changeCapture.ts +212 -1
- package/src/runner/runTask.ts +220 -22
- package/src/smoke-test.ts +5 -5
- 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/auditTask.ts +11 -5
- package/src/tools/getTaskSummary.ts +22 -1
- package/src/tools/healthCheck.ts +22 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/toolCatalog.ts +2 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
当前稳定版本:**v0.6.
|
|
12
|
-
[发布说明](docs/release-v0.6.
|
|
11
|
+
当前稳定版本:**v0.6.1**。查看
|
|
12
|
+
[发布说明](docs/release-v0.6.1.md)、
|
|
13
13
|
[迁移指南](docs/migration-from-safe-bifrost.md)和
|
|
14
|
-
[GitHub Release](https://github.com/jiezeng2004-design/PatchWarden/releases/tag/v0.6.
|
|
14
|
+
[GitHub Release](https://github.com/jiezeng2004-design/PatchWarden/releases/tag/v0.6.1)。
|
|
15
15
|
|
|
16
16
|
PatchWarden 是一个面向本地编程 Agent 的安全 MCP 桥接器。上游的
|
|
17
17
|
ChatGPT、Codex、OpenCode 或其他 MCP 客户端负责规划与验收,
|
package/dist/doctor.js
CHANGED
|
@@ -218,7 +218,7 @@ async function main() {
|
|
|
218
218
|
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
219
219
|
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
220
220
|
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
221
|
-
check("Full tool profile exposes
|
|
221
|
+
check("Full tool profile exposes 30 tools", fullTools.length === 30, `${fullTools.length} tools`);
|
|
222
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(", "));
|
|
223
223
|
check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
|
|
224
224
|
createSchema?.properties?.verify_commands &&
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type LogLevel = "info" | "warn" | "error" | "audit";
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
level: LogLevel;
|
|
5
|
+
message: string;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
|
|
10
|
+
* Verbose mode enables logging of sanitized tool-call arguments.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isVerboseLogging(): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Structured logger that emits JSON lines to stderr.
|
|
15
|
+
*
|
|
16
|
+
* Each entry contains `timestamp`, `level`, `message`, plus any optional
|
|
17
|
+
* context fields supplied by the caller.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Logger {
|
|
20
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
21
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
22
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
23
|
+
/**
|
|
24
|
+
* Emit a tool-call audit log entry.
|
|
25
|
+
*
|
|
26
|
+
* Required fields: `tool`, `ok`, `duration_ms`.
|
|
27
|
+
* Optional fields: `error_reason`, `task_id`.
|
|
28
|
+
*
|
|
29
|
+
* By default raw arguments are NOT logged. When verbose mode is enabled
|
|
30
|
+
* (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
|
|
31
|
+
* are serialized and sanitized via `redactSensitiveContent` before being
|
|
32
|
+
* included in the `args` field.
|
|
33
|
+
*/
|
|
34
|
+
audit(tool: string, ok: boolean, durationMs: number, errorReason?: string, taskId?: string, args?: unknown): void;
|
|
35
|
+
}
|
|
36
|
+
/** Default singleton logger instance. */
|
|
37
|
+
export declare const logger: Logger;
|
|
38
|
+
/**
|
|
39
|
+
* Produce a structured error log entry for an unhandled rejection or
|
|
40
|
+
* uncaught exception. Writes JSON to stderr.
|
|
41
|
+
*/
|
|
42
|
+
export declare function logUnhandledError(error: unknown): void;
|
|
43
|
+
/**
|
|
44
|
+
* Register process-level handlers for `unhandledRejection` and
|
|
45
|
+
* `uncaughtException`.
|
|
46
|
+
*
|
|
47
|
+
* Both handlers log structured error output to stderr. The
|
|
48
|
+
* `uncaughtException` handler does NOT swallow the fatal error — after
|
|
49
|
+
* logging it exits with a non-zero status code to preserve the default
|
|
50
|
+
* crash behaviour required for truly fatal failures.
|
|
51
|
+
*/
|
|
52
|
+
export declare function installGlobalHandlers(): void;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
2
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when PATCHWARDEN_VERBOSE_LOG is set to "true".
|
|
5
|
+
* Verbose mode enables logging of sanitized tool-call arguments.
|
|
6
|
+
*/
|
|
7
|
+
export function isVerboseLogging() {
|
|
8
|
+
return process.env.PATCHWARDEN_VERBOSE_LOG === "true";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Serialize a value to a JSON string, falling back to String() on failure
|
|
12
|
+
* (e.g. circular references, functions, symbols).
|
|
13
|
+
*/
|
|
14
|
+
function safeStringify(value) {
|
|
15
|
+
try {
|
|
16
|
+
const result = JSON.stringify(value);
|
|
17
|
+
return result === undefined ? String(value) : result;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Write a single JSON log line to stderr.
|
|
25
|
+
*
|
|
26
|
+
* All log output goes to stderr — NEVER stdout — so that MCP JSON-RPC
|
|
27
|
+
* traffic on stdout is never polluted by log messages.
|
|
28
|
+
*/
|
|
29
|
+
function emit(entry) {
|
|
30
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
31
|
+
}
|
|
32
|
+
// ── Logger ────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Structured logger that emits JSON lines to stderr.
|
|
35
|
+
*
|
|
36
|
+
* Each entry contains `timestamp`, `level`, `message`, plus any optional
|
|
37
|
+
* context fields supplied by the caller.
|
|
38
|
+
*/
|
|
39
|
+
export class Logger {
|
|
40
|
+
info(message, context) {
|
|
41
|
+
emit({ timestamp: new Date().toISOString(), level: "info", message, ...context });
|
|
42
|
+
}
|
|
43
|
+
warn(message, context) {
|
|
44
|
+
emit({ timestamp: new Date().toISOString(), level: "warn", message, ...context });
|
|
45
|
+
}
|
|
46
|
+
error(message, context) {
|
|
47
|
+
emit({ timestamp: new Date().toISOString(), level: "error", message, ...context });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Emit a tool-call audit log entry.
|
|
51
|
+
*
|
|
52
|
+
* Required fields: `tool`, `ok`, `duration_ms`.
|
|
53
|
+
* Optional fields: `error_reason`, `task_id`.
|
|
54
|
+
*
|
|
55
|
+
* By default raw arguments are NOT logged. When verbose mode is enabled
|
|
56
|
+
* (PATCHWARDEN_VERBOSE_LOG=true) and `args` is provided, the arguments
|
|
57
|
+
* are serialized and sanitized via `redactSensitiveContent` before being
|
|
58
|
+
* included in the `args` field.
|
|
59
|
+
*/
|
|
60
|
+
audit(tool, ok, durationMs, errorReason, taskId, args) {
|
|
61
|
+
const entry = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
level: "audit",
|
|
64
|
+
message: "tool_call_audit",
|
|
65
|
+
tool,
|
|
66
|
+
ok,
|
|
67
|
+
duration_ms: durationMs,
|
|
68
|
+
};
|
|
69
|
+
if (errorReason !== undefined)
|
|
70
|
+
entry.error_reason = errorReason;
|
|
71
|
+
if (taskId !== undefined)
|
|
72
|
+
entry.task_id = taskId;
|
|
73
|
+
if (args !== undefined && isVerboseLogging()) {
|
|
74
|
+
const redacted = redactSensitiveContent(safeStringify(args));
|
|
75
|
+
entry.args = redacted.content;
|
|
76
|
+
}
|
|
77
|
+
emit(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Default singleton logger instance. */
|
|
81
|
+
export const logger = new Logger();
|
|
82
|
+
// ── Unhandled error helpers ───────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Produce a structured error log entry for an unhandled rejection or
|
|
85
|
+
* uncaught exception. Writes JSON to stderr.
|
|
86
|
+
*/
|
|
87
|
+
export function logUnhandledError(error) {
|
|
88
|
+
const entry = {
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
level: "error",
|
|
91
|
+
message: "unhandled_error",
|
|
92
|
+
error: error instanceof Error
|
|
93
|
+
? error.message
|
|
94
|
+
: typeof error === "string"
|
|
95
|
+
? error
|
|
96
|
+
: safeStringify(error),
|
|
97
|
+
error_name: error instanceof Error ? error.name : typeof error,
|
|
98
|
+
};
|
|
99
|
+
if (error instanceof Error && error.stack) {
|
|
100
|
+
entry.stack = error.stack;
|
|
101
|
+
}
|
|
102
|
+
emit(entry);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Register process-level handlers for `unhandledRejection` and
|
|
106
|
+
* `uncaughtException`.
|
|
107
|
+
*
|
|
108
|
+
* Both handlers log structured error output to stderr. The
|
|
109
|
+
* `uncaughtException` handler does NOT swallow the fatal error — after
|
|
110
|
+
* logging it exits with a non-zero status code to preserve the default
|
|
111
|
+
* crash behaviour required for truly fatal failures.
|
|
112
|
+
*/
|
|
113
|
+
export function installGlobalHandlers() {
|
|
114
|
+
process.on("unhandledRejection", (reason) => {
|
|
115
|
+
logUnhandledError(reason);
|
|
116
|
+
});
|
|
117
|
+
process.on("uncaughtException", (error) => {
|
|
118
|
+
logUnhandledError(error);
|
|
119
|
+
// Do not swallow: exit with failure so the process does not continue
|
|
120
|
+
// in an undefined state.
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -11,6 +11,7 @@ export interface RepoSnapshot {
|
|
|
11
11
|
status: string;
|
|
12
12
|
workspace_dirty: boolean;
|
|
13
13
|
files: Record<string, FileFingerprint>;
|
|
14
|
+
dirty_paths: string[];
|
|
14
15
|
warnings: string[];
|
|
15
16
|
}
|
|
16
17
|
export interface ChangedFile {
|
|
@@ -70,3 +71,43 @@ export declare function writeSnapshot(taskDir: string, filename: string, snapsho
|
|
|
70
71
|
export declare function buildChangeArtifacts(repoPath: string, before: RepoSnapshot, after: RepoSnapshot): ChangeArtifacts;
|
|
71
72
|
export declare function compareSnapshots(before: RepoSnapshot, after: RepoSnapshot): ChangedFile[];
|
|
72
73
|
export declare function emptyArtifactHygiene(): ArtifactHygiene;
|
|
74
|
+
export interface ExternalDirtyFile {
|
|
75
|
+
path: string;
|
|
76
|
+
change: ChangedFile["change"];
|
|
77
|
+
before_sha256: string | null;
|
|
78
|
+
after_sha256: string | null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Extract files that are dirty in the workspace but outside the target repo.
|
|
82
|
+
* Used to establish a baseline before task execution.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractExternalDirtyFiles(workspaceSnapshot: RepoSnapshot, repoPath: string, workspaceRoot: string): ExternalDirtyFile[];
|
|
85
|
+
/**
|
|
86
|
+
* Compare external dirty files between baseline and post-task snapshots.
|
|
87
|
+
* Returns files that are NEW (not present in baseline) or CHANGED
|
|
88
|
+
* (same path but different sha256, meaning the task modified them).
|
|
89
|
+
*/
|
|
90
|
+
export declare function findNewExternalDirtyFiles(baseline: ExternalDirtyFile[], current: ExternalDirtyFile[]): ExternalDirtyFile[];
|
|
91
|
+
export interface ArtifactManifestEntry {
|
|
92
|
+
path: string;
|
|
93
|
+
type: string;
|
|
94
|
+
size: number;
|
|
95
|
+
sha256: string;
|
|
96
|
+
generated_by: string;
|
|
97
|
+
created_at: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ArtifactManifest {
|
|
100
|
+
task_id: string | null;
|
|
101
|
+
generated_at: string;
|
|
102
|
+
artifacts: ArtifactManifestEntry[];
|
|
103
|
+
}
|
|
104
|
+
export declare function buildArtifactManifest(changedFiles: ChangedFile[], repoPath: string, taskId?: string): ArtifactManifest;
|
|
105
|
+
export interface ChangedFileGroups {
|
|
106
|
+
source_changes: ChangedFile[];
|
|
107
|
+
docs_changes: ChangedFile[];
|
|
108
|
+
config_changes: ChangedFile[];
|
|
109
|
+
test_changes: ChangedFile[];
|
|
110
|
+
release_artifacts: ChangedFile[];
|
|
111
|
+
runtime_generated_files: ChangedFile[];
|
|
112
|
+
}
|
|
113
|
+
export declare function groupChangedFiles(changedFiles: ChangedFile[]): ChangedFileGroups;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
6
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
@@ -15,11 +15,36 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
15
15
|
let paths = [];
|
|
16
16
|
const trackedPaths = new Set();
|
|
17
17
|
const ignoredPaths = new Set();
|
|
18
|
+
const dirtyPaths = new Set();
|
|
18
19
|
if (isGit) {
|
|
19
20
|
const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
|
|
20
21
|
if (headResult.status === 0)
|
|
21
22
|
head = headResult.stdout.trim() || null;
|
|
22
23
|
status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
|
|
24
|
+
// Parse git status --porcelain to collect all dirty paths
|
|
25
|
+
for (const line of status.split("\n")) {
|
|
26
|
+
if (line.length < 4)
|
|
27
|
+
continue;
|
|
28
|
+
const st = line.slice(0, 2); // XY status codes
|
|
29
|
+
const rawPath = line.slice(3);
|
|
30
|
+
// M=modified, A=added, D=deleted, ?=untracked, R=renamed, !=ignored
|
|
31
|
+
if (/[MAD\?R]/.test(st)) {
|
|
32
|
+
if (st.includes("R")) {
|
|
33
|
+
// Rename: rawPath is "oldname -> newname"
|
|
34
|
+
const parts = rawPath.split(" -> ");
|
|
35
|
+
if (parts.length === 2) {
|
|
36
|
+
dirtyPaths.add(normalizePath(parts[0]));
|
|
37
|
+
dirtyPaths.add(normalizePath(parts[1]));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
23
48
|
const tracked = runGit(repoPath, ["ls-files", "-z"]);
|
|
24
49
|
if (tracked.status === 0) {
|
|
25
50
|
for (const path of tracked.stdout.split("\0").filter(Boolean))
|
|
@@ -84,6 +109,7 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
84
109
|
status,
|
|
85
110
|
workspace_dirty: status.trim().length > 0,
|
|
86
111
|
files,
|
|
112
|
+
dirty_paths: [...dirtyPaths],
|
|
87
113
|
warnings,
|
|
88
114
|
};
|
|
89
115
|
}
|
|
@@ -244,6 +270,150 @@ export function emptyArtifactHygiene() {
|
|
|
244
270
|
suspicious_changes: [],
|
|
245
271
|
};
|
|
246
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Extract files that are dirty in the workspace but outside the target repo.
|
|
275
|
+
* Used to establish a baseline before task execution.
|
|
276
|
+
*/
|
|
277
|
+
export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot) {
|
|
278
|
+
const dirtyFiles = [];
|
|
279
|
+
const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
|
|
280
|
+
for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
|
|
281
|
+
const absolutePath = resolve(workspaceRoot, path);
|
|
282
|
+
const rel = relative(repoPath, absolutePath);
|
|
283
|
+
// If the path is outside repoPath (starts with .. or is absolute)
|
|
284
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
285
|
+
// A file is "external dirty" if:
|
|
286
|
+
// 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
|
|
287
|
+
// 2. It's not tracked by git (untracked file), OR
|
|
288
|
+
// 3. It's explicitly ignored
|
|
289
|
+
const isDirty = dirtyPathSet.has(path);
|
|
290
|
+
const isUntracked = !fingerprint.tracked;
|
|
291
|
+
const isIgnored = fingerprint.ignored;
|
|
292
|
+
if (isDirty || isUntracked || isIgnored) {
|
|
293
|
+
dirtyFiles.push({
|
|
294
|
+
path,
|
|
295
|
+
change: isDirty ? "modified" : "added",
|
|
296
|
+
before_sha256: fingerprint.sha256,
|
|
297
|
+
after_sha256: null,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return dirtyFiles;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Compare external dirty files between baseline and post-task snapshots.
|
|
306
|
+
* Returns files that are NEW (not present in baseline) or CHANGED
|
|
307
|
+
* (same path but different sha256, meaning the task modified them).
|
|
308
|
+
*/
|
|
309
|
+
export function findNewExternalDirtyFiles(baseline, current) {
|
|
310
|
+
const baselineMap = new Map(baseline.map((f) => [f.path, f]));
|
|
311
|
+
return current.filter((f) => {
|
|
312
|
+
const baselineFile = baselineMap.get(f.path);
|
|
313
|
+
if (!baselineFile)
|
|
314
|
+
return true; // New path — definitely new
|
|
315
|
+
// Same path but content changed during task execution
|
|
316
|
+
if (baselineFile.before_sha256 !== f.before_sha256)
|
|
317
|
+
return true;
|
|
318
|
+
return false;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
export function buildArtifactManifest(changedFiles, repoPath, taskId) {
|
|
322
|
+
const entries = [];
|
|
323
|
+
for (const file of changedFiles) {
|
|
324
|
+
if (file.kind !== "build_artifact")
|
|
325
|
+
continue;
|
|
326
|
+
const absolutePath = resolve(repoPath, file.path);
|
|
327
|
+
let size = 0;
|
|
328
|
+
let sha256 = file.after_sha256 || "unknown";
|
|
329
|
+
try {
|
|
330
|
+
const stat = lstatSync(absolutePath);
|
|
331
|
+
if (stat.isFile()) {
|
|
332
|
+
size = stat.size;
|
|
333
|
+
if (size <= MAX_HASH_BYTES) {
|
|
334
|
+
sha256 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// File may have been deleted
|
|
340
|
+
}
|
|
341
|
+
entries.push({
|
|
342
|
+
path: file.path,
|
|
343
|
+
type: classifyArtifactType(file.path),
|
|
344
|
+
size,
|
|
345
|
+
sha256,
|
|
346
|
+
generated_by: "task_execution",
|
|
347
|
+
created_at: new Date().toISOString(),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
task_id: taskId || null,
|
|
352
|
+
generated_at: new Date().toISOString(),
|
|
353
|
+
artifacts: entries,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function classifyArtifactType(path) {
|
|
357
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
358
|
+
const basename = normalized.split("/").pop() || "";
|
|
359
|
+
if (basename.endsWith(".exe"))
|
|
360
|
+
return "windows_exe";
|
|
361
|
+
if (basename.endsWith(".apk"))
|
|
362
|
+
return "android_apk";
|
|
363
|
+
if (basename.endsWith(".zip"))
|
|
364
|
+
return "zip";
|
|
365
|
+
if (basename.endsWith(".asar"))
|
|
366
|
+
return "asar";
|
|
367
|
+
if (basename.endsWith(".dll"))
|
|
368
|
+
return "dll";
|
|
369
|
+
if (basename.endsWith(".pak"))
|
|
370
|
+
return "pak";
|
|
371
|
+
return "release_directory_file";
|
|
372
|
+
}
|
|
373
|
+
export function groupChangedFiles(changedFiles) {
|
|
374
|
+
const groups = {
|
|
375
|
+
source_changes: [],
|
|
376
|
+
docs_changes: [],
|
|
377
|
+
config_changes: [],
|
|
378
|
+
test_changes: [],
|
|
379
|
+
release_artifacts: [],
|
|
380
|
+
runtime_generated_files: [],
|
|
381
|
+
};
|
|
382
|
+
for (const file of changedFiles) {
|
|
383
|
+
const normalized = normalizePath(file.path).toLowerCase();
|
|
384
|
+
const parts = normalized.split("/");
|
|
385
|
+
const basename = parts[parts.length - 1] || "";
|
|
386
|
+
// Check for docs
|
|
387
|
+
if (parts.some((p) => p === "docs") || /\.(md|rst|txt)$/.test(basename)) {
|
|
388
|
+
groups.docs_changes.push(file);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Check for config
|
|
392
|
+
if (basename === "package.json" || basename === "tsconfig.json" || basename === ".gitignore" ||
|
|
393
|
+
basename.startsWith(".config") || basename.endsWith(".config.js") || basename.endsWith(".config.ts")) {
|
|
394
|
+
groups.config_changes.push(file);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// Check for test files
|
|
398
|
+
if (basename.includes(".test.") || basename.includes(".spec.") || parts.some((p) => p === "test" || p === "tests" || p === "__tests__")) {
|
|
399
|
+
groups.test_changes.push(file);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// Check for build artifacts / release
|
|
403
|
+
if (file.kind === "build_artifact") {
|
|
404
|
+
groups.release_artifacts.push(file);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
// Check for runtime generated
|
|
408
|
+
if (file.kind === "runtime_generated") {
|
|
409
|
+
groups.runtime_generated_files.push(file);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Default: source changes
|
|
413
|
+
groups.source_changes.push(file);
|
|
414
|
+
}
|
|
415
|
+
return groups;
|
|
416
|
+
}
|
|
247
417
|
function classifyChangedFile(path, change, before, after) {
|
|
248
418
|
return {
|
|
249
419
|
path,
|