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,144 @@
|
|
|
1
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
readDirectSession,
|
|
4
|
+
updateDirectSession,
|
|
5
|
+
finalizeDirectSessionRecord,
|
|
6
|
+
getDirectSessionDir,
|
|
7
|
+
} from "../direct/directSessionStore.js";
|
|
8
|
+
import { guardDirectSessionActive } from "../direct/directGuards.js";
|
|
9
|
+
import {
|
|
10
|
+
captureRepoSnapshot,
|
|
11
|
+
buildChangeArtifacts,
|
|
12
|
+
type ChangeArtifacts,
|
|
13
|
+
type ClassifiedChange,
|
|
14
|
+
type ChangedFile,
|
|
15
|
+
} from "../runner/changeCapture.js";
|
|
16
|
+
|
|
17
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface FinalizeDirectSessionInput {
|
|
20
|
+
session_id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FinalizeDirectSessionOutput {
|
|
24
|
+
session_id: string;
|
|
25
|
+
changed_files_total: number;
|
|
26
|
+
source_changes: ClassifiedChange[];
|
|
27
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
28
|
+
runtime_generated_files: ClassifiedChange[];
|
|
29
|
+
suspicious_changes: ClassifiedChange[];
|
|
30
|
+
out_of_scope_changes: ClassifiedChange[];
|
|
31
|
+
verification_summary: Record<string, "passed" | "failed">;
|
|
32
|
+
diff_path: string;
|
|
33
|
+
summary_path: string;
|
|
34
|
+
finalized: boolean;
|
|
35
|
+
next_action: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Main function ──────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export function finalizeDirectSession(
|
|
41
|
+
input: FinalizeDirectSessionInput
|
|
42
|
+
): FinalizeDirectSessionOutput {
|
|
43
|
+
const { session_id } = input;
|
|
44
|
+
|
|
45
|
+
// 1. Read session and guard active (not expired, not finalized)
|
|
46
|
+
const session = readDirectSession(session_id);
|
|
47
|
+
guardDirectSessionActive(session);
|
|
48
|
+
|
|
49
|
+
// 2. Capture after snapshot
|
|
50
|
+
const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
|
|
51
|
+
|
|
52
|
+
// 3. Build change artifacts from before/after snapshots
|
|
53
|
+
const changeArtifacts = buildChangeArtifacts(
|
|
54
|
+
session.resolved_repo_path,
|
|
55
|
+
session.workspace_snapshot_before,
|
|
56
|
+
afterSnapshot
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 4. Finalize session record — writes changed-files.json, diff.patch,
|
|
60
|
+
// summary.json, summary.md and marks the session as finalized.
|
|
61
|
+
finalizeDirectSessionRecord(session_id, afterSnapshot, changeArtifacts);
|
|
62
|
+
|
|
63
|
+
// 5. Build verification summary from session.verification_runs
|
|
64
|
+
const verification_summary: Record<string, "passed" | "failed"> = {};
|
|
65
|
+
for (const run of session.verification_runs) {
|
|
66
|
+
verification_summary[run.command] = run.passed ? "passed" : "failed";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 6. Identify out-of-scope changes (paths outside resolved_repo_path)
|
|
70
|
+
const out_of_scope_changes = findOutOfScopeChanges(
|
|
71
|
+
changeArtifacts,
|
|
72
|
+
session.resolved_repo_path
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// 7. Build compact summary with categorized changes
|
|
76
|
+
const sessionDir = getDirectSessionDir(session_id);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
session_id,
|
|
80
|
+
changed_files_total: changeArtifacts.changed_files.length,
|
|
81
|
+
source_changes: changeArtifacts.artifact_hygiene.source_changes,
|
|
82
|
+
tracked_build_artifacts:
|
|
83
|
+
changeArtifacts.artifact_hygiene.tracked_build_artifacts,
|
|
84
|
+
runtime_generated_files:
|
|
85
|
+
changeArtifacts.artifact_hygiene.runtime_generated_files,
|
|
86
|
+
suspicious_changes: changeArtifacts.artifact_hygiene.suspicious_changes,
|
|
87
|
+
out_of_scope_changes,
|
|
88
|
+
verification_summary,
|
|
89
|
+
diff_path: join(sessionDir, "diff.patch"),
|
|
90
|
+
summary_path: join(sessionDir, "summary.md"),
|
|
91
|
+
finalized: true,
|
|
92
|
+
next_action: "Call audit_session to independently review the changes.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Filter changeArtifacts.changed_files for paths that resolve outside the
|
|
100
|
+
* session's resolved_repo_path and map them to ClassifiedChange format.
|
|
101
|
+
*/
|
|
102
|
+
function findOutOfScopeChanges(
|
|
103
|
+
artifacts: ChangeArtifacts,
|
|
104
|
+
resolvedRepoPath: string
|
|
105
|
+
): ClassifiedChange[] {
|
|
106
|
+
const normalizedRepo = resolve(resolvedRepoPath);
|
|
107
|
+
return artifacts.changed_files
|
|
108
|
+
.filter((file) => {
|
|
109
|
+
const fullPath = resolve(normalizedRepo, file.path);
|
|
110
|
+
const rel = relative(normalizedRepo, fullPath);
|
|
111
|
+
return isAbsolute(rel) || rel.startsWith("..");
|
|
112
|
+
})
|
|
113
|
+
.map((file) => toClassifiedChange(file));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Map a ChangedFile to the ClassifiedChange format by adding a reason string.
|
|
118
|
+
*/
|
|
119
|
+
function toClassifiedChange(file: ChangedFile): ClassifiedChange {
|
|
120
|
+
return {
|
|
121
|
+
path: file.path,
|
|
122
|
+
change: file.change,
|
|
123
|
+
tracked: file.tracked,
|
|
124
|
+
ignored: file.ignored,
|
|
125
|
+
kind: file.kind,
|
|
126
|
+
reason: classificationReason(file),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Produce a human-readable reason for a changed file classification.
|
|
132
|
+
* Mirrors the logic in changeCapture.ts classifyArtifactHygiene.
|
|
133
|
+
*/
|
|
134
|
+
function classificationReason(change: ChangedFile): string {
|
|
135
|
+
if (change.ignored)
|
|
136
|
+
return "untracked path is ignored by repository Git rules";
|
|
137
|
+
if (change.kind === "build_artifact" && change.tracked)
|
|
138
|
+
return "artifact-like path is tracked by Git and requires review";
|
|
139
|
+
if (change.kind === "build_artifact")
|
|
140
|
+
return "artifact-like path is not ignored and requires review";
|
|
141
|
+
if (change.kind === "runtime_generated")
|
|
142
|
+
return "runtime-generated path is not ignored and requires review";
|
|
143
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
144
|
+
}
|
|
@@ -78,9 +78,57 @@ export interface TaskSummaryOutput {
|
|
|
78
78
|
watcher: unknown;
|
|
79
79
|
pending_reason: string | null;
|
|
80
80
|
execution_blocked: boolean;
|
|
81
|
+
artifact_hygiene: Record<string, unknown>;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export
|
|
84
|
+
export interface CompactTaskSummaryOutput {
|
|
85
|
+
view: "compact";
|
|
86
|
+
task_id: string;
|
|
87
|
+
status: string;
|
|
88
|
+
terminal: boolean;
|
|
89
|
+
acceptance_status: TaskSummaryOutput["acceptance_status"];
|
|
90
|
+
phase: string;
|
|
91
|
+
repo_path: string;
|
|
92
|
+
changed_files_total: number;
|
|
93
|
+
out_of_scope_changes_total: number;
|
|
94
|
+
artifact_hygiene: {
|
|
95
|
+
counts: Record<string, number>;
|
|
96
|
+
source_changes: unknown[];
|
|
97
|
+
tracked_build_artifacts: unknown[];
|
|
98
|
+
ignored_untracked_artifacts: unknown[];
|
|
99
|
+
runtime_generated_files: unknown[];
|
|
100
|
+
suspicious_changes: unknown[];
|
|
101
|
+
max_items: number;
|
|
102
|
+
truncated: boolean;
|
|
103
|
+
};
|
|
104
|
+
release_artifacts_count: number;
|
|
105
|
+
release_artifact_paths: string[];
|
|
106
|
+
artifact_status: string | null;
|
|
107
|
+
verification_summary: TaskSummaryOutput["verification_summary"];
|
|
108
|
+
summary: string;
|
|
109
|
+
warnings: string[];
|
|
110
|
+
errors: string[];
|
|
111
|
+
failure_reason: string | null;
|
|
112
|
+
failed_command: string | null;
|
|
113
|
+
suggested_next_action: string;
|
|
114
|
+
execution_blocked: boolean;
|
|
115
|
+
pending_reason: string | null;
|
|
116
|
+
redacted: boolean;
|
|
117
|
+
redaction_categories: string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface GetTaskSummaryOptions {
|
|
121
|
+
view?: "compact" | "standard";
|
|
122
|
+
max_items?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type TaskSummaryResult = TaskSummaryOutput | CompactTaskSummaryOutput;
|
|
126
|
+
|
|
127
|
+
export function getTaskSummary(taskId: string): TaskSummaryOutput;
|
|
128
|
+
export function getTaskSummary(taskId: string, options: { view: "compact"; max_items?: number }): CompactTaskSummaryOutput;
|
|
129
|
+
export function getTaskSummary(taskId: string, options: { view?: "standard"; max_items?: number }): TaskSummaryOutput;
|
|
130
|
+
export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions): TaskSummaryResult;
|
|
131
|
+
export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions = {}): TaskSummaryResult {
|
|
84
132
|
const config = getConfig();
|
|
85
133
|
const taskDir = resolve(getTasksDir(config), taskId);
|
|
86
134
|
const statusFile = join(taskDir, "status.json");
|
|
@@ -91,7 +139,14 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
91
139
|
const result = resultRead.data;
|
|
92
140
|
const verify = verifyRead.data;
|
|
93
141
|
const terminal = TERMINAL_STATUSES.has(String(status.status));
|
|
94
|
-
|
|
142
|
+
// Phase 4: Use new_out_of_scope_changes (task-caused) for acceptance status.
|
|
143
|
+
// Pre-existing external dirty files that didn't change should NOT fail acceptance.
|
|
144
|
+
const outOfScope = asArray(
|
|
145
|
+
result.new_out_of_scope_changes
|
|
146
|
+
?? status.new_out_of_scope_changes
|
|
147
|
+
?? result.out_of_scope_changes
|
|
148
|
+
?? status.out_of_scope_changes
|
|
149
|
+
);
|
|
95
150
|
const verifyStatus = String(verify.status ?? result.verify_status ?? result.verify?.status ?? status.verify_status ?? "not_available");
|
|
96
151
|
const errors = [status.error, ...asArray(result.errors), ...asArray(result.known_issues)]
|
|
97
152
|
.filter((value): value is string => typeof value === "string" && value.trim() !== "");
|
|
@@ -156,6 +211,10 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
156
211
|
? Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : Date.now()) - startedAt)
|
|
157
212
|
: 0;
|
|
158
213
|
const changedFiles = asArray(result.changed_files ?? status.changed_files);
|
|
214
|
+
const changedFilesRead = tryReadJson(join(taskDir, "changed-files.json"));
|
|
215
|
+
const artifactHygiene = asRecord(result.artifact_hygiene ?? changedFilesRead.data.artifact_hygiene ?? {
|
|
216
|
+
counts: status.artifact_hygiene_counts || {},
|
|
217
|
+
});
|
|
159
218
|
const verifyCommands = asArray(verify.commands ?? result.verify_commands ?? result.verify?.commands);
|
|
160
219
|
const testLogSummary = summarizeTestLog(join(taskDir, "test.log"));
|
|
161
220
|
const verificationSummary = buildVerificationSummary(verifyStatus, verifyCommands, testLogSummary);
|
|
@@ -227,7 +286,11 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
227
286
|
watcher: status.watcher,
|
|
228
287
|
pending_reason: status.pending_reason || null,
|
|
229
288
|
execution_blocked: Boolean(status.execution_blocked),
|
|
289
|
+
artifact_hygiene: artifactHygiene,
|
|
230
290
|
};
|
|
291
|
+
if ((options.view || "standard") === "compact") {
|
|
292
|
+
return buildCompactSummary(output, normalizeMaxItems(options.max_items));
|
|
293
|
+
}
|
|
231
294
|
const safe = redactSensitiveValue(output);
|
|
232
295
|
return {
|
|
233
296
|
...safe.value,
|
|
@@ -236,6 +299,70 @@ export function getTaskSummary(taskId: string): TaskSummaryOutput {
|
|
|
236
299
|
} as TaskSummaryOutput;
|
|
237
300
|
}
|
|
238
301
|
|
|
302
|
+
function buildCompactSummary(output: Record<string, any>, maxItems: number): CompactTaskSummaryOutput {
|
|
303
|
+
const hygiene = asRecord(output.artifact_hygiene);
|
|
304
|
+
const groupNames = [
|
|
305
|
+
"source_changes",
|
|
306
|
+
"tracked_build_artifacts",
|
|
307
|
+
"ignored_untracked_artifacts",
|
|
308
|
+
"runtime_generated_files",
|
|
309
|
+
"suspicious_changes",
|
|
310
|
+
] as const;
|
|
311
|
+
const groups = Object.fromEntries(groupNames.map((name) => [name, asArray(hygiene[name]).slice(0, maxItems)]));
|
|
312
|
+
const truncated = groupNames.some((name) => asArray(hygiene[name]).length > maxItems);
|
|
313
|
+
|
|
314
|
+
// Phase 6: Read artifact_manifest.json for release artifact info
|
|
315
|
+
const taskDir = resolve(getTasksDir(getConfig()), String(output.task_id));
|
|
316
|
+
const manifestRead = tryReadJson(join(taskDir, "artifact_manifest.json"));
|
|
317
|
+
const manifest = asRecord(manifestRead.data);
|
|
318
|
+
const releaseArtifacts = asArray(manifest.artifacts);
|
|
319
|
+
const releaseArtifactPaths = releaseArtifacts.map((a: any) => String(a.path || "")).slice(0, maxItems);
|
|
320
|
+
|
|
321
|
+
const compact = {
|
|
322
|
+
view: "compact" as const,
|
|
323
|
+
task_id: String(output.task_id),
|
|
324
|
+
status: String(output.status),
|
|
325
|
+
terminal: Boolean(output.terminal),
|
|
326
|
+
acceptance_status: output.acceptance_status,
|
|
327
|
+
phase: String(output.phase),
|
|
328
|
+
repo_path: String(output.repo_path),
|
|
329
|
+
changed_files_total: asArray(output.changed_files).length,
|
|
330
|
+
out_of_scope_changes_total: asArray(output.out_of_scope_changes).length,
|
|
331
|
+
artifact_hygiene: {
|
|
332
|
+
counts: asRecord(hygiene.counts) as Record<string, number>,
|
|
333
|
+
...groups,
|
|
334
|
+
max_items: maxItems,
|
|
335
|
+
truncated,
|
|
336
|
+
},
|
|
337
|
+
release_artifacts_count: releaseArtifacts.length,
|
|
338
|
+
release_artifact_paths: releaseArtifactPaths,
|
|
339
|
+
artifact_status: String(output.artifact_status || manifest.status || "collected"),
|
|
340
|
+
verification_summary: output.verification_summary,
|
|
341
|
+
summary: String(output.summary).slice(0, 1000),
|
|
342
|
+
warnings: asArray(output.warnings).slice(0, maxItems),
|
|
343
|
+
errors: asArray(output.errors).slice(0, maxItems),
|
|
344
|
+
failure_reason: output.failure_reason || null,
|
|
345
|
+
failed_command: output.failed_command || null,
|
|
346
|
+
suggested_next_action: String(output.suggested_next_action),
|
|
347
|
+
execution_blocked: Boolean(output.execution_blocked),
|
|
348
|
+
pending_reason: output.pending_reason || null,
|
|
349
|
+
};
|
|
350
|
+
const safe = redactSensitiveValue(compact);
|
|
351
|
+
return {
|
|
352
|
+
...safe.value,
|
|
353
|
+
redacted: safe.redacted,
|
|
354
|
+
redaction_categories: safe.redaction_categories,
|
|
355
|
+
} as CompactTaskSummaryOutput;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function normalizeMaxItems(value: number | undefined): number {
|
|
359
|
+
if (value === undefined) return 8;
|
|
360
|
+
if (!Number.isInteger(value) || value < 1 || value > 50) {
|
|
361
|
+
throw new Error("max_items must be an integer from 1 to 50.");
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
|
|
239
366
|
function tryReadJson(path: string): { data: Record<string, any>; error?: string } {
|
|
240
367
|
if (!existsSync(path)) return { data: {} };
|
|
241
368
|
try {
|
|
@@ -249,6 +376,10 @@ function asArray(value: unknown): any[] {
|
|
|
249
376
|
return Array.isArray(value) ? value : [];
|
|
250
377
|
}
|
|
251
378
|
|
|
379
|
+
function asRecord(value: unknown): Record<string, any> {
|
|
380
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : {};
|
|
381
|
+
}
|
|
382
|
+
|
|
252
383
|
function summarizeTestLog(path: string): string {
|
|
253
384
|
if (!existsSync(path)) return "test.log missing";
|
|
254
385
|
const text = readFileSync(path, "utf-8");
|
package/src/tools/healthCheck.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
|
|
4
|
+
import { getConfig, getTasksDir, getDirectSessionsDir, resolveWorkspaceRoot } from "../config.js";
|
|
5
5
|
import { listAgents } from "./listAgents.js";
|
|
6
6
|
import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
|
|
7
7
|
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
8
8
|
import type { ToolCatalogSnapshot } from "./toolCatalog.js";
|
|
9
|
-
import { CHATGPT_CORE_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
9
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
10
10
|
import { readWatcherStatus } from "../watcherStatus.js";
|
|
11
11
|
import { listTasks } from "./listTasks.js";
|
|
12
12
|
|
|
@@ -35,9 +35,17 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
35
35
|
let profileConsistent = true;
|
|
36
36
|
if (catalog) {
|
|
37
37
|
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
let expectedNames: string[] | null = null;
|
|
39
|
+
if (activeProfile === "chatgpt_core") {
|
|
40
|
+
expectedNames = [...CHATGPT_CORE_TOOL_NAMES];
|
|
41
|
+
} else if (activeProfile === "chatgpt_direct" && config.enableDirectProfile) {
|
|
42
|
+
expectedNames = [...CHATGPT_DIRECT_TOOL_NAMES];
|
|
43
|
+
} else if (activeProfile === "chatgpt_direct" && !config.enableDirectProfile) {
|
|
44
|
+
// Degraded mode: only health_check should be exposed
|
|
45
|
+
expectedNames = ["health_check"];
|
|
46
|
+
profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
|
|
47
|
+
profileConsistent = false;
|
|
48
|
+
}
|
|
41
49
|
if (expectedNames) {
|
|
42
50
|
const catalogNames = new Set(catalog.tool_names);
|
|
43
51
|
for (const name of expectedNames) {
|
|
@@ -76,6 +84,11 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
76
84
|
const status = watcher.available && workspace.available && tasks.available && agentReady && catalogConsistent
|
|
77
85
|
? "healthy"
|
|
78
86
|
: "degraded";
|
|
87
|
+
|
|
88
|
+
const directSessionsDir = getDirectSessionsDir(config);
|
|
89
|
+
const directSessions = directoryStatus(directSessionsDir, true, workspaceRoot);
|
|
90
|
+
const activeProfile = resolveToolProfile(config.toolProfile);
|
|
91
|
+
|
|
79
92
|
return {
|
|
80
93
|
status,
|
|
81
94
|
server_version: catalog?.server_version || PATCHWARDEN_VERSION,
|
|
@@ -89,11 +102,15 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
89
102
|
catalog_consistent: catalogConsistent,
|
|
90
103
|
mismatch_report: mismatchReport,
|
|
91
104
|
tunnel_catalog_comparison: tunnelCatalogComparison,
|
|
105
|
+
direct_profile_enabled: config.enableDirectProfile ?? false,
|
|
106
|
+
direct_sessions_dir: directSessions,
|
|
107
|
+
direct_session_ttl_seconds: config.directSessionTtlSeconds,
|
|
108
|
+
...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
|
|
92
109
|
connector_visibility: {
|
|
93
110
|
status: "not_observable_server_side",
|
|
94
111
|
verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
|
|
95
112
|
refresh_steps: [
|
|
96
|
-
"1. Run
|
|
113
|
+
"1. Run PatchWarden.cmd health to confirm the active profile, tool count, and catalog consistency.",
|
|
97
114
|
"2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
|
|
98
115
|
"3. Open a NEW ChatGPT conversation (old conversations retain their cached tool catalog).",
|
|
99
116
|
"4. Call health_check in the new conversation; verify tool_manifest_sha256 matches the local report.",
|
|
@@ -105,6 +122,7 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
105
122
|
uptime_seconds: Math.round((Date.now() - SERVER_STARTED_AT) / 1000),
|
|
106
123
|
checked_at: new Date().toISOString(),
|
|
107
124
|
},
|
|
125
|
+
path_encoding: checkPathEncoding(),
|
|
108
126
|
watcher,
|
|
109
127
|
workspace_root: workspace,
|
|
110
128
|
tasks_dir: tasks,
|
|
@@ -232,3 +250,24 @@ function readTunnelStatus(): Record<string, unknown> & { last_error: string | nu
|
|
|
232
250
|
function safeText(value: unknown, maxLength: number): string {
|
|
233
251
|
return typeof value === "string" ? value.replace(/[\r\n]+/g, " ").slice(0, maxLength) : "";
|
|
234
252
|
}
|
|
253
|
+
|
|
254
|
+
function checkPathEncoding(): { encoding: string; warnings: string[] } {
|
|
255
|
+
const warnings: string[] = [];
|
|
256
|
+
// Node.js always uses UTF-8 for fs operations, but on Windows the console
|
|
257
|
+
// codepage can cause display issues. We check if the environment is likely
|
|
258
|
+
// to cause problems with non-ASCII paths.
|
|
259
|
+
if (process.platform === "win32") {
|
|
260
|
+
// Check if the system locale uses a codepage that might not support UTF-8
|
|
261
|
+
const lang = process.env.LANG || process.env.LC_ALL || "";
|
|
262
|
+
const chcp = process.env.PATCHWARDEN_CHCP || "";
|
|
263
|
+
if (chcp && !chcp.includes("65001") && !chcp.includes("utf")) {
|
|
264
|
+
warnings.push(`Windows codepage "${chcp}" may cause display issues with non-ASCII paths. Consider setting codepage to 65001 (UTF-8).`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Verify that Node.js fs operations use UTF-8 by default
|
|
268
|
+
// This is always true in Node.js >= 18
|
|
269
|
+
return {
|
|
270
|
+
encoding: "utf-8",
|
|
271
|
+
warnings,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import { readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolve, relative, isAbsolute } from "node:path";
|
|
2
4
|
import { getConfig } from "../config.js";
|
|
3
5
|
import { guardReadPath } from "../security/pathGuard.js";
|
|
4
6
|
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
5
7
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
8
|
+
import { PatchWardenError } from "../errors.js";
|
|
9
|
+
import { readDirectSession } from "../direct/directSessionStore.js";
|
|
10
|
+
import { guardDirectSessionActive, guardDirectReadPath, isBinaryFile, guardDirectFileSize } from "../direct/directGuards.js";
|
|
11
|
+
import { resolveToolProfile } from "./toolCatalog.js";
|
|
6
12
|
|
|
7
13
|
export interface ReadWorkspaceFileOutput {
|
|
8
14
|
path: string;
|
|
@@ -10,11 +16,41 @@ export interface ReadWorkspaceFileOutput {
|
|
|
10
16
|
size: number;
|
|
11
17
|
redacted?: boolean;
|
|
12
18
|
redaction_categories?: string[];
|
|
19
|
+
// Direct mode extensions
|
|
20
|
+
relative_path?: string;
|
|
21
|
+
sha256?: string;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
export
|
|
24
|
+
export interface ReadWorkspaceFileInput {
|
|
25
|
+
path: string;
|
|
26
|
+
session_id?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readWorkspaceFile(input: string | ReadWorkspaceFileInput): ReadWorkspaceFileOutput {
|
|
16
30
|
const config = getConfig();
|
|
17
31
|
|
|
32
|
+
// Support both old string API and new object API
|
|
33
|
+
const relativePath = typeof input === "string" ? input : input.path;
|
|
34
|
+
const sessionId = typeof input === "string" ? undefined : input.session_id;
|
|
35
|
+
|
|
36
|
+
// Direct mode: session_id provided
|
|
37
|
+
if (sessionId) {
|
|
38
|
+
return readWorkspaceFileDirect(relativePath, sessionId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// In chatgpt_direct profile, session_id is required
|
|
42
|
+
const profile = resolveToolProfile(config.toolProfile);
|
|
43
|
+
if (profile === "chatgpt_direct") {
|
|
44
|
+
throw new PatchWardenError(
|
|
45
|
+
"session_id_required",
|
|
46
|
+
"session_id is required when using the chatgpt_direct profile.",
|
|
47
|
+
"Provide a session_id from create_direct_session.",
|
|
48
|
+
true,
|
|
49
|
+
{ operation: "read_workspace_file" }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Compatibility mode: no session_id, original behavior
|
|
18
54
|
const safePath = guardReadPath(relativePath, config.workspaceRoot);
|
|
19
55
|
guardSensitivePath(safePath);
|
|
20
56
|
|
|
@@ -39,3 +75,50 @@ export function readWorkspaceFile(relativePath: string): ReadWorkspaceFileOutput
|
|
|
39
75
|
redaction_categories: redaction.redaction_categories,
|
|
40
76
|
};
|
|
41
77
|
}
|
|
78
|
+
|
|
79
|
+
function readWorkspaceFileDirect(relativePath: string, sessionId: string): ReadWorkspaceFileOutput {
|
|
80
|
+
const config = getConfig();
|
|
81
|
+
const session = readDirectSession(sessionId);
|
|
82
|
+
guardDirectSessionActive(session);
|
|
83
|
+
|
|
84
|
+
// Resolve path within session's repo
|
|
85
|
+
const safePath = guardDirectReadPath(relativePath, session.resolved_repo_path, config.workspaceRoot);
|
|
86
|
+
|
|
87
|
+
if (!existsSync(safePath)) {
|
|
88
|
+
throw new PatchWardenError(
|
|
89
|
+
"file_not_found",
|
|
90
|
+
`File "${relativePath}" not found in session repo.`,
|
|
91
|
+
"Use search_workspace to find available files.",
|
|
92
|
+
true,
|
|
93
|
+
{ path: relativePath, session_id: sessionId }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stat = statSync(safePath);
|
|
98
|
+
guardDirectFileSize(stat.size);
|
|
99
|
+
|
|
100
|
+
// Block binary files
|
|
101
|
+
if (isBinaryFile(safePath)) {
|
|
102
|
+
throw new PatchWardenError(
|
|
103
|
+
"binary_file_blocked",
|
|
104
|
+
`File "${relativePath}" appears to be a binary file.`,
|
|
105
|
+
"Binary files cannot be read in Direct mode.",
|
|
106
|
+
true,
|
|
107
|
+
{ path: relativePath }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const content = readFileSync(safePath, "utf-8");
|
|
112
|
+
const redaction = redactSensitiveContent(content);
|
|
113
|
+
const sha256 = createHash("sha256").update(content, "utf-8").digest("hex");
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
path: safePath,
|
|
117
|
+
relative_path: relativePath,
|
|
118
|
+
content: redaction.content,
|
|
119
|
+
size: stat.size,
|
|
120
|
+
sha256,
|
|
121
|
+
redacted: redaction.redacted,
|
|
122
|
+
redaction_categories: redaction.redaction_categories,
|
|
123
|
+
};
|
|
124
|
+
}
|