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
package/src/tools/registry.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
CallToolRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import { getConfig } from "../config.js";
|
|
11
|
+
import { getAllConfiguredTestCommands, getAllConfiguredDirectCommands, getConfig } from "../config.js";
|
|
12
12
|
import { savePlan } from "../tools/savePlan.js";
|
|
13
13
|
import { getPlan } from "../tools/getPlan.js";
|
|
14
14
|
import { createTask } from "../tools/createTask.js";
|
|
@@ -28,7 +28,16 @@ import { getTaskSummary } from "../tools/getTaskSummary.js";
|
|
|
28
28
|
import { waitForTask } from "../tools/waitForTask.js";
|
|
29
29
|
import { errorPayload, PatchWardenError } from "../errors.js";
|
|
30
30
|
import { auditTask } from "../tools/auditTask.js";
|
|
31
|
+
import { safeStatus } from "../tools/safeStatus.js";
|
|
32
|
+
import { logger } from "../logging.js";
|
|
31
33
|
import { runTask } from "../runner/runTask.js";
|
|
34
|
+
import { createDirectSession } from "../tools/createDirectSession.js";
|
|
35
|
+
import { searchWorkspace } from "../tools/searchWorkspace.js";
|
|
36
|
+
import { applyPatch } from "../tools/applyPatch.js";
|
|
37
|
+
import { runVerification } from "../tools/runVerification.js";
|
|
38
|
+
import { finalizeDirectSession } from "../tools/finalizeDirectSession.js";
|
|
39
|
+
import { auditSession } from "../tools/auditSession.js";
|
|
40
|
+
import { syncFile } from "../tools/syncFile.js";
|
|
32
41
|
import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
|
|
33
42
|
import {
|
|
34
43
|
buildToolCatalogSnapshot,
|
|
@@ -56,19 +65,19 @@ export function getToolDefs(): ToolDef[] {
|
|
|
56
65
|
const agentDescription = agentNames.length > 0
|
|
57
66
|
? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
|
|
58
67
|
: "Configured local agent name. No agents are currently configured.";
|
|
59
|
-
const testCommands =
|
|
68
|
+
const testCommands = getAllConfiguredTestCommands(config).sort();
|
|
60
69
|
const tools: ToolDef[] = [
|
|
61
70
|
{
|
|
62
71
|
name: "save_plan",
|
|
63
72
|
description:
|
|
64
|
-
"Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution.",
|
|
73
|
+
"Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution. Supports plan_ref to load a plan file already placed inside .patchwarden/plans.",
|
|
65
74
|
inputSchema: {
|
|
66
75
|
type: "object",
|
|
67
76
|
properties: {
|
|
68
|
-
title: { type: "string", description: "Plan title" },
|
|
69
|
-
content: { type: "string", description: "Plan content in Markdown" },
|
|
77
|
+
title: { type: "string", description: "Plan title. Defaults to 'Inline plan' or 'Plan from file' when omitted." },
|
|
78
|
+
content: { type: "string", description: "Plan content in Markdown. Required unless plan_ref is provided." },
|
|
79
|
+
plan_ref: { type: "string", description: "Relative path to a plan file already inside .patchwarden/plans. When provided, the file content is loaded and title defaults to 'Plan from file' if title is empty." },
|
|
70
80
|
},
|
|
71
|
-
required: ["title", "content"],
|
|
72
81
|
},
|
|
73
82
|
},
|
|
74
83
|
{
|
|
@@ -110,7 +119,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
110
119
|
{
|
|
111
120
|
name: "create_task",
|
|
112
121
|
description:
|
|
113
|
-
"Create a repo-scoped task from exactly one source. A stale watcher preserves the task but returns execution_blocked and directs the client to health_check; otherwise call wait_for_task until terminal.",
|
|
122
|
+
"Create a repo-scoped task from exactly one source. For ChatGPT, prefer the guarded inspect_only, feature_small, or fix_tests template when it fits. A stale watcher preserves the task but returns execution_blocked and directs the client to health_check; otherwise call wait_for_task until terminal. Use execution_mode=assess_only to pre-assess risk and get an assessment_id without creating a task; then invoke the returned next_tool_call using only execution_mode=execute and the full assessment_id.",
|
|
114
123
|
inputSchema: {
|
|
115
124
|
type: "object",
|
|
116
125
|
properties: {
|
|
@@ -120,7 +129,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
120
129
|
template: {
|
|
121
130
|
type: "string",
|
|
122
131
|
enum: [...TASK_TEMPLATE_NAMES],
|
|
123
|
-
description: "Built-in guarded task template. Use with goal; rollback_scope_violation also requires source_task_id.",
|
|
132
|
+
description: "Built-in guarded task template. ChatGPT should prefer inspect_only for diagnosis, feature_small for a scoped change, and fix_tests for known failing verification. Use with goal; rollback_scope_violation also requires source_task_id.",
|
|
124
133
|
},
|
|
125
134
|
goal: { type: "string", description: "Required task goal when template is supplied." },
|
|
126
135
|
source_task_id: { type: "string", description: "Required source task for rollback_scope_violation review." },
|
|
@@ -147,7 +156,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
147
156
|
type: "string",
|
|
148
157
|
...(testCommands.length > 0 ? { enum: testCommands } : {}),
|
|
149
158
|
},
|
|
150
|
-
description: "Recommended
|
|
159
|
+
description: "Recommended exact-match commands PatchWarden runs independently after the agent exits. Repository-scoped commands are re-authorized after repo_path is resolved.",
|
|
151
160
|
},
|
|
152
161
|
timeout_seconds: {
|
|
153
162
|
type: "integer",
|
|
@@ -156,8 +165,18 @@ export function getToolDefs(): ToolDef[] {
|
|
|
156
165
|
default: config.defaultTaskTimeoutSeconds,
|
|
157
166
|
description: `Total task timeout in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds})`,
|
|
158
167
|
},
|
|
168
|
+
execution_mode: {
|
|
169
|
+
type: "string",
|
|
170
|
+
enum: ["assess_only", "execute"],
|
|
171
|
+
default: "execute",
|
|
172
|
+
description: "assess_only: run deterministic risk checks and return an assessment_id without creating a task. execute (default): create and queue the task. When combined with assessment_id, the task parameters are loaded from the assessment record and freshness is revalidated.",
|
|
173
|
+
},
|
|
174
|
+
assessment_id: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Assessment ID from a prior assess_only call. When provided with execution_mode=execute, task parameters are loaded from the assessment record and locked to it. The full 128-bit ID (32 hex chars) must be provided; short IDs are display-only.",
|
|
177
|
+
},
|
|
159
178
|
},
|
|
160
|
-
required: [
|
|
179
|
+
required: [],
|
|
161
180
|
},
|
|
162
181
|
},
|
|
163
182
|
{
|
|
@@ -232,13 +251,17 @@ export function getToolDefs(): ToolDef[] {
|
|
|
232
251
|
{
|
|
233
252
|
name: "read_workspace_file",
|
|
234
253
|
description:
|
|
235
|
-
"Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked.",
|
|
254
|
+
"Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked. In Direct mode (with session_id), reads are scoped to the session's repo_path and return sha256.",
|
|
236
255
|
inputSchema: {
|
|
237
256
|
type: "object",
|
|
238
257
|
properties: {
|
|
239
258
|
path: {
|
|
240
259
|
type: "string",
|
|
241
|
-
description: "Relative path to a file inside the workspace",
|
|
260
|
+
description: "Relative path to a file inside the workspace or session repo",
|
|
261
|
+
},
|
|
262
|
+
session_id: {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "Optional Direct session ID. When provided, read scope is limited to the session's repo_path and sha256 is returned.",
|
|
242
265
|
},
|
|
243
266
|
},
|
|
244
267
|
required: ["path"],
|
|
@@ -372,11 +395,13 @@ export function getToolDefs(): ToolDef[] {
|
|
|
372
395
|
{
|
|
373
396
|
name: "get_task_summary",
|
|
374
397
|
description:
|
|
375
|
-
"Return
|
|
398
|
+
"Return structured acceptance evidence. Use view=compact first for bounded counts and risk excerpts; use standard only when full changed-file and log-tail detail is required.",
|
|
376
399
|
inputSchema: {
|
|
377
400
|
type: "object",
|
|
378
401
|
properties: {
|
|
379
402
|
task_id: { type: "string", description: "Task ID" },
|
|
403
|
+
view: { type: "string", enum: ["compact", "standard"], default: "standard", description: "Compact returns bounded acceptance evidence; standard preserves the full legacy summary." },
|
|
404
|
+
max_items: { type: "integer", minimum: 1, maximum: 50, default: 8, description: "Maximum entries per compact evidence group." },
|
|
380
405
|
},
|
|
381
406
|
required: ["task_id"],
|
|
382
407
|
},
|
|
@@ -384,7 +409,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
384
409
|
{
|
|
385
410
|
name: "audit_task",
|
|
386
411
|
description:
|
|
387
|
-
"Independently audit a task's outputs. Verifies status, result.md, test.log, git.diff, repo_path consistency, cross-references agent claims with package.json scripts, and flags unverified release/publish claims. Writes independent-review.md to the task directory.",
|
|
412
|
+
"Independently audit a task's outputs. Verifies status, result.md, test.log, git.diff, repo_path consistency, cross-references agent claims with package.json scripts, and flags unverified release/publish claims. Evidence-backed failures, possible heuristic false positives, and manual-verification items are returned separately. Writes independent-review.md to the task directory.",
|
|
388
413
|
inputSchema: {
|
|
389
414
|
type: "object",
|
|
390
415
|
properties: {
|
|
@@ -393,8 +418,154 @@ export function getToolDefs(): ToolDef[] {
|
|
|
393
418
|
required: ["task_id"],
|
|
394
419
|
},
|
|
395
420
|
},
|
|
421
|
+
{
|
|
422
|
+
name: "safe_status",
|
|
423
|
+
description:
|
|
424
|
+
"Return minimal task lifecycle status without exposing diff, log content, file contents, or sensitive paths. Use this when only task state is needed and content-bearing tools may be blocked by upper-layer security.",
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: "object",
|
|
427
|
+
properties: {
|
|
428
|
+
task_id: { type: "string", description: "Task ID to check" },
|
|
429
|
+
},
|
|
430
|
+
required: ["task_id"],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
396
433
|
];
|
|
397
434
|
|
|
435
|
+
// Direct session tools
|
|
436
|
+
const directCommands = getAllConfiguredDirectCommands(config);
|
|
437
|
+
tools.push({
|
|
438
|
+
name: "create_direct_session",
|
|
439
|
+
description:
|
|
440
|
+
"Create a Direct editing session for ChatGPT to apply patches directly. Requires enableDirectProfile: true in config.",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
type: "object",
|
|
443
|
+
properties: {
|
|
444
|
+
repo_path: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description: "Repository path inside workspaceRoot (e.g., 'my-project')",
|
|
447
|
+
},
|
|
448
|
+
title: {
|
|
449
|
+
type: "string",
|
|
450
|
+
description: "Optional title describing the session's purpose",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
required: ["repo_path"],
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
tools.push({
|
|
458
|
+
name: "search_workspace",
|
|
459
|
+
description:
|
|
460
|
+
"Search file contents (grep-like) within a Direct session's repo_path. Skips .git, node_modules, dist, release, and sensitive files.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
session_id: { type: "string", description: "Session ID from create_direct_session" },
|
|
465
|
+
query: { type: "string", description: "Search query string" },
|
|
466
|
+
max_results: { type: "number", description: "Max results (default 20)" },
|
|
467
|
+
case_sensitive: { type: "boolean", description: "Case sensitive search (default false)" },
|
|
468
|
+
max_preview_chars: { type: "number", description: "Max preview chars per match (default 200)" },
|
|
469
|
+
include_globs: {
|
|
470
|
+
type: "array",
|
|
471
|
+
items: { type: "string" },
|
|
472
|
+
description: "Optional file name glob patterns to include (e.g., ['*.ts', '*.js'])",
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
required: ["session_id", "query"],
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
tools.push({
|
|
480
|
+
name: "apply_patch",
|
|
481
|
+
description:
|
|
482
|
+
"Apply JSON patch operations to a file within a Direct session's repo_path. Validates expected_sha256 before applying. Supports replace_exact, insert_before, insert_after, replace_whole_file.",
|
|
483
|
+
inputSchema: {
|
|
484
|
+
type: "object",
|
|
485
|
+
properties: {
|
|
486
|
+
session_id: { type: "string", description: "Session ID" },
|
|
487
|
+
path: { type: "string", description: "Relative file path within the session repo" },
|
|
488
|
+
expected_sha256: { type: "string", description: "Expected SHA-256 hash of the current file content" },
|
|
489
|
+
operations: {
|
|
490
|
+
type: "array",
|
|
491
|
+
items: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
type: { type: "string", enum: ["replace_exact", "insert_before", "insert_after", "replace_whole_file"] },
|
|
495
|
+
old_text: { type: "string", description: "Text to find (required for replace_exact, insert_before, insert_after)" },
|
|
496
|
+
new_text: { type: "string", description: "Replacement or insertion text" },
|
|
497
|
+
occurrence: { type: "string", enum: ["first", "all", "exactly_once"], description: "Match mode for replace_exact (default first)" },
|
|
498
|
+
},
|
|
499
|
+
required: ["type", "new_text"],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
required: ["session_id", "path", "expected_sha256", "operations"],
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
tools.push({
|
|
508
|
+
name: "run_verification",
|
|
509
|
+
description:
|
|
510
|
+
"Run a whitelisted verification command within a Direct session. Command must be in the Direct allowlist.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
session_id: { type: "string", description: "Session ID" },
|
|
515
|
+
command: {
|
|
516
|
+
type: "string",
|
|
517
|
+
description: "Verification command to run",
|
|
518
|
+
...(directCommands.length > 0 ? { enum: directCommands } : {}),
|
|
519
|
+
},
|
|
520
|
+
timeout_seconds: { type: "number", description: "Timeout in seconds (default 120)" },
|
|
521
|
+
},
|
|
522
|
+
required: ["session_id", "command"],
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
tools.push({
|
|
527
|
+
name: "finalize_direct_session",
|
|
528
|
+
description:
|
|
529
|
+
"Finalize a Direct session: capture after snapshot, generate diff/summary/change artifacts, mark session as finalized. Must be called before audit_session.",
|
|
530
|
+
inputSchema: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
session_id: { type: "string", description: "Session ID to finalize" },
|
|
534
|
+
},
|
|
535
|
+
required: ["session_id"],
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
tools.push({
|
|
540
|
+
name: "audit_session",
|
|
541
|
+
description:
|
|
542
|
+
"Independently audit a Direct session's changes. Performs 16 deterministic checks and returns pass/warn/fail decision. Requires session to be finalized first.",
|
|
543
|
+
inputSchema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: {
|
|
546
|
+
session_id: { type: "string", description: "Session ID to audit" },
|
|
547
|
+
},
|
|
548
|
+
required: ["session_id"],
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
tools.push({
|
|
553
|
+
name: "sync_file",
|
|
554
|
+
description:
|
|
555
|
+
"Copy a file from source to target within the same Direct session repo. Both paths must be inside the session repo_path. Returns before/after sha256 hashes and whether the target changed.",
|
|
556
|
+
inputSchema: {
|
|
557
|
+
type: "object",
|
|
558
|
+
properties: {
|
|
559
|
+
session_id: { type: "string", description: "Direct session ID" },
|
|
560
|
+
source_path: { type: "string", description: "Relative path to source file within repo" },
|
|
561
|
+
target_path: { type: "string", description: "Relative path to target file within repo" },
|
|
562
|
+
expected_source_sha256: { type: "string", description: "Optional: expected sha256 of source file" },
|
|
563
|
+
expected_target_sha256: { type: "string", description: "Optional: expected sha256 of target file before copy" },
|
|
564
|
+
},
|
|
565
|
+
required: ["session_id", "source_path", "target_path"],
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
398
569
|
// run_task: only available when explicitly enabled
|
|
399
570
|
if ((config as any).enableRunTaskTool === true) {
|
|
400
571
|
tools.push({
|
|
@@ -412,7 +583,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
412
583
|
}
|
|
413
584
|
|
|
414
585
|
const profile = resolveToolProfile(config.toolProfile);
|
|
415
|
-
const selected = selectToolsForProfile(tools, profile);
|
|
586
|
+
const selected = selectToolsForProfile(tools, profile, config.enableDirectProfile);
|
|
416
587
|
buildToolCatalogSnapshot(selected, profile);
|
|
417
588
|
return selected;
|
|
418
589
|
}
|
|
@@ -425,13 +596,41 @@ export function getToolCatalogSnapshot(): ToolCatalogSnapshot {
|
|
|
425
596
|
|
|
426
597
|
// ── Request handler ───────────────────────────────────────────────
|
|
427
598
|
|
|
599
|
+
function guardDirectProfileEnabled(): void {
|
|
600
|
+
const config = getConfig();
|
|
601
|
+
if (!config.enableDirectProfile) {
|
|
602
|
+
throw new PatchWardenError(
|
|
603
|
+
"direct_profile_disabled",
|
|
604
|
+
"Direct profile is disabled by local config.",
|
|
605
|
+
"Set enableDirectProfile: true in patchwarden.config.json to use Direct session tools.",
|
|
606
|
+
true,
|
|
607
|
+
{ operation: "direct_tool_call" }
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
428
612
|
export async function handleToolCall(name: string, args: Record<string, unknown> | undefined) {
|
|
613
|
+
const startTime = Date.now();
|
|
614
|
+
const taskId = args?.task_id ? String(args.task_id) : args?.session_id ? String(args.session_id) : undefined;
|
|
615
|
+
try {
|
|
616
|
+
const result = await handleToolCallInternal(name, args);
|
|
617
|
+
logger.audit(name, true, Date.now() - startTime, undefined, taskId);
|
|
618
|
+
return result;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
const errorReason = err instanceof Error ? err.message : String(err);
|
|
621
|
+
logger.audit(name, false, Date.now() - startTime, errorReason, taskId);
|
|
622
|
+
throw err;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function handleToolCallInternal(name: string, args: Record<string, unknown> | undefined) {
|
|
429
627
|
switch (name) {
|
|
430
628
|
case "save_plan": {
|
|
431
629
|
return toResult(
|
|
432
630
|
savePlan({
|
|
433
631
|
title: String(args?.title ?? ""),
|
|
434
|
-
content: String(args
|
|
632
|
+
content: args?.content !== undefined ? String(args.content) : "",
|
|
633
|
+
plan_ref: args?.plan_ref ? String(args.plan_ref) : undefined,
|
|
435
634
|
})
|
|
436
635
|
);
|
|
437
636
|
}
|
|
@@ -460,6 +659,8 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
|
|
|
460
659
|
timeout_seconds: args?.timeout_seconds !== undefined
|
|
461
660
|
? Number(args.timeout_seconds)
|
|
462
661
|
: undefined,
|
|
662
|
+
execution_mode: args?.execution_mode === "assess_only" ? "assess_only" : "execute",
|
|
663
|
+
assessment_id: args?.assessment_id ? String(args.assessment_id) : undefined,
|
|
463
664
|
})
|
|
464
665
|
);
|
|
465
666
|
}
|
|
@@ -491,7 +692,11 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
|
|
|
491
692
|
}
|
|
492
693
|
|
|
493
694
|
case "read_workspace_file": {
|
|
494
|
-
|
|
695
|
+
const sessionId = args?.session_id ? String(args.session_id) : undefined;
|
|
696
|
+
return toResult(readWorkspaceFile({
|
|
697
|
+
path: String(args?.path ?? ""),
|
|
698
|
+
session_id: sessionId,
|
|
699
|
+
}));
|
|
495
700
|
}
|
|
496
701
|
|
|
497
702
|
case "list_tasks": {
|
|
@@ -556,13 +761,20 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
|
|
|
556
761
|
}
|
|
557
762
|
|
|
558
763
|
case "get_task_summary": {
|
|
559
|
-
return toResult(getTaskSummary(String(args?.task_id ?? "")
|
|
764
|
+
return toResult(getTaskSummary(String(args?.task_id ?? ""), {
|
|
765
|
+
view: normalizeSummaryView(args?.view),
|
|
766
|
+
max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
|
|
767
|
+
}));
|
|
560
768
|
}
|
|
561
769
|
|
|
562
770
|
case "audit_task": {
|
|
563
771
|
return toResult(auditTask(String(args?.task_id ?? "")));
|
|
564
772
|
}
|
|
565
773
|
|
|
774
|
+
case "safe_status": {
|
|
775
|
+
return toResult(safeStatus(String(args?.task_id ?? "")));
|
|
776
|
+
}
|
|
777
|
+
|
|
566
778
|
case "run_task": {
|
|
567
779
|
const config = getConfig();
|
|
568
780
|
if ((config as any).enableRunTaskTool !== true) {
|
|
@@ -575,6 +787,72 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
|
|
|
575
787
|
return toResult(result);
|
|
576
788
|
}
|
|
577
789
|
|
|
790
|
+
case "create_direct_session": {
|
|
791
|
+
guardDirectProfileEnabled();
|
|
792
|
+
return toResult(createDirectSession({
|
|
793
|
+
repo_path: String(args?.repo_path ?? ""),
|
|
794
|
+
title: args?.title ? String(args.title) : undefined,
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
case "search_workspace": {
|
|
799
|
+
guardDirectProfileEnabled();
|
|
800
|
+
return toResult(searchWorkspace({
|
|
801
|
+
session_id: String(args?.session_id ?? ""),
|
|
802
|
+
query: String(args?.query ?? ""),
|
|
803
|
+
max_results: args?.max_results ? Number(args.max_results) : undefined,
|
|
804
|
+
case_sensitive: args?.case_sensitive !== undefined ? Boolean(args.case_sensitive) : undefined,
|
|
805
|
+
max_preview_chars: args?.max_preview_chars ? Number(args.max_preview_chars) : undefined,
|
|
806
|
+
include_globs: Array.isArray(args?.include_globs) ? args.include_globs.map(String) : undefined,
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
case "apply_patch": {
|
|
811
|
+
guardDirectProfileEnabled();
|
|
812
|
+
return toResult(applyPatch({
|
|
813
|
+
session_id: String(args?.session_id ?? ""),
|
|
814
|
+
path: String(args?.path ?? ""),
|
|
815
|
+
expected_sha256: String(args?.expected_sha256 ?? ""),
|
|
816
|
+
operations: Array.isArray(args?.operations) ? args.operations as any : [],
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
case "run_verification": {
|
|
821
|
+
guardDirectProfileEnabled();
|
|
822
|
+
return toResult(await runVerification({
|
|
823
|
+
session_id: String(args?.session_id ?? ""),
|
|
824
|
+
command: String(args?.command ?? ""),
|
|
825
|
+
timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
case "finalize_direct_session": {
|
|
830
|
+
guardDirectProfileEnabled();
|
|
831
|
+
return toResult(finalizeDirectSession({
|
|
832
|
+
session_id: String(args?.session_id ?? ""),
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
case "audit_session": {
|
|
837
|
+
guardDirectProfileEnabled();
|
|
838
|
+
return toResult(auditSession({
|
|
839
|
+
session_id: String(args?.session_id ?? ""),
|
|
840
|
+
}));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
case "sync_file": {
|
|
844
|
+
guardDirectProfileEnabled();
|
|
845
|
+
return toResult(syncFile(
|
|
846
|
+
String(args?.session_id ?? ""),
|
|
847
|
+
String(args?.source_path ?? ""),
|
|
848
|
+
String(args?.target_path ?? ""),
|
|
849
|
+
{
|
|
850
|
+
expected_source_sha256: args?.expected_source_sha256 ? String(args.expected_source_sha256) : undefined,
|
|
851
|
+
expected_target_sha256: args?.expected_target_sha256 ? String(args.expected_target_sha256) : undefined,
|
|
852
|
+
}
|
|
853
|
+
));
|
|
854
|
+
}
|
|
855
|
+
|
|
578
856
|
default:
|
|
579
857
|
throw new Error(`Unknown tool: ${name}`);
|
|
580
858
|
}
|
|
@@ -590,6 +868,14 @@ function normalizeWaitSeconds(args: Record<string, unknown> | undefined): number
|
|
|
590
868
|
return value === undefined ? undefined : Number(value);
|
|
591
869
|
}
|
|
592
870
|
|
|
871
|
+
function normalizeSummaryView(value: unknown): "compact" | "standard" {
|
|
872
|
+
if (value === undefined) return "standard";
|
|
873
|
+
if (value !== "compact" && value !== "standard") {
|
|
874
|
+
throw new Error('view must be "compact" or "standard".');
|
|
875
|
+
}
|
|
876
|
+
return value;
|
|
877
|
+
}
|
|
878
|
+
|
|
593
879
|
function toResult(data: unknown) {
|
|
594
880
|
return {
|
|
595
881
|
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
@@ -647,7 +933,7 @@ export function registerTools(server: Server) {
|
|
|
647
933
|
arguments: { detail: "self_diagnostic" },
|
|
648
934
|
},
|
|
649
935
|
connector_refresh_steps: [
|
|
650
|
-
"1. Run
|
|
936
|
+
"1. Run PatchWarden.cmd health locally to confirm the active profile and manifest hash.",
|
|
651
937
|
"2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
|
|
652
938
|
"3. Open a NEW ChatGPT conversation; old conversations retain their cached tool catalog.",
|
|
653
939
|
"4. Call health_check in the new conversation and verify tool_manifest_sha256 matches the local report.",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readDirectSession,
|
|
3
|
+
appendDirectSessionVerificationRun,
|
|
4
|
+
} from "../direct/directSessionStore.js";
|
|
5
|
+
import { guardDirectSessionActive } from "../direct/directGuards.js";
|
|
6
|
+
import { runDirectVerification } from "../direct/directVerification.js";
|
|
7
|
+
|
|
8
|
+
export interface RunVerificationInput {
|
|
9
|
+
session_id: string;
|
|
10
|
+
command: string;
|
|
11
|
+
timeout_seconds?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RunVerificationOutput {
|
|
15
|
+
command: string;
|
|
16
|
+
exit_code: number | null;
|
|
17
|
+
passed: boolean;
|
|
18
|
+
timed_out: boolean;
|
|
19
|
+
stdout_tail: string;
|
|
20
|
+
stderr_tail: string;
|
|
21
|
+
log_path: string;
|
|
22
|
+
next_action: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runVerification(
|
|
26
|
+
input: RunVerificationInput
|
|
27
|
+
): Promise<RunVerificationOutput> {
|
|
28
|
+
// 1. Read session and guard active
|
|
29
|
+
const session = readDirectSession(input.session_id);
|
|
30
|
+
guardDirectSessionActive(session);
|
|
31
|
+
|
|
32
|
+
// 2. Call runDirectVerification with command, resolvedRepoPath,
|
|
33
|
+
// sessionId, and timeoutSeconds
|
|
34
|
+
const timeoutSeconds = input.timeout_seconds ?? 120;
|
|
35
|
+
const result = await runDirectVerification({
|
|
36
|
+
command: input.command,
|
|
37
|
+
resolvedRepoPath: session.resolved_repo_path,
|
|
38
|
+
sessionId: input.session_id,
|
|
39
|
+
timeoutSeconds,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 3. Append verification run to session
|
|
43
|
+
appendDirectSessionVerificationRun(input.session_id, result.run);
|
|
44
|
+
|
|
45
|
+
// 4. Return result
|
|
46
|
+
return {
|
|
47
|
+
command: result.run.command,
|
|
48
|
+
exit_code: result.run.exit_code,
|
|
49
|
+
passed: result.run.passed,
|
|
50
|
+
timed_out: result.run.timed_out,
|
|
51
|
+
stdout_tail: result.run.stdout_tail,
|
|
52
|
+
stderr_tail: result.run.stderr_tail,
|
|
53
|
+
log_path: result.run.log_path,
|
|
54
|
+
next_action: result.run.passed
|
|
55
|
+
? "Call finalize_direct_session to complete the session."
|
|
56
|
+
: "Review the verification output and apply_patch to fix issues.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { getTasksDir, getConfig, type PatchWardenConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath } from "../security/pathGuard.js";
|
|
5
|
+
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
+
import type { TaskStatus, TaskPhase } from "./createTask.js";
|
|
7
|
+
import { readTaskRuntime } from "../taskRuntime.js";
|
|
8
|
+
import { readWatcherStatus } from "../watcherStatus.js";
|
|
9
|
+
|
|
10
|
+
export interface SafeStatusOutput {
|
|
11
|
+
task_id: string;
|
|
12
|
+
status: TaskStatus | "not_found";
|
|
13
|
+
phase: TaskPhase | null;
|
|
14
|
+
created_at: string | null;
|
|
15
|
+
started_at: string | null;
|
|
16
|
+
updated_at: string | null;
|
|
17
|
+
finished_at: string | null;
|
|
18
|
+
last_heartbeat_at: string | null;
|
|
19
|
+
current_command: string | null;
|
|
20
|
+
verify_status: "passed" | "failed" | "skipped" | null;
|
|
21
|
+
artifact_status: string | null;
|
|
22
|
+
watcher_state: string | null;
|
|
23
|
+
error_code: string | null;
|
|
24
|
+
error_summary: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStatusOutput {
|
|
28
|
+
const cfg = config || getConfig();
|
|
29
|
+
const tasksDir = getTasksDir(cfg);
|
|
30
|
+
|
|
31
|
+
const taskDir = resolve(tasksDir, taskId);
|
|
32
|
+
const statusFile = join(taskDir, "status.json");
|
|
33
|
+
|
|
34
|
+
// Check existence BEFORE guardReadPath so that a non-existent task
|
|
35
|
+
// returns a structured not_found response instead of throwing.
|
|
36
|
+
if (!existsSync(statusFile)) {
|
|
37
|
+
return {
|
|
38
|
+
task_id: taskId,
|
|
39
|
+
status: "not_found",
|
|
40
|
+
phase: null,
|
|
41
|
+
created_at: null,
|
|
42
|
+
started_at: null,
|
|
43
|
+
updated_at: null,
|
|
44
|
+
finished_at: null,
|
|
45
|
+
last_heartbeat_at: null,
|
|
46
|
+
current_command: null,
|
|
47
|
+
verify_status: null,
|
|
48
|
+
artifact_status: null,
|
|
49
|
+
watcher_state: null,
|
|
50
|
+
error_code: null,
|
|
51
|
+
error_summary: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
guardReadPath(statusFile, cfg.workspaceRoot, cfg.tasksDir);
|
|
56
|
+
guardSensitivePath(statusFile);
|
|
57
|
+
|
|
58
|
+
const raw = readFileSync(statusFile, "utf-8");
|
|
59
|
+
const status = JSON.parse(raw) as Record<string, unknown>;
|
|
60
|
+
const runtime = readTaskRuntime(taskDir);
|
|
61
|
+
const phase = (runtime.phase || status.phase || "queued") as TaskPhase;
|
|
62
|
+
const watcher = readWatcherStatus(cfg);
|
|
63
|
+
|
|
64
|
+
// Extract short error summary — never the full error text
|
|
65
|
+
let errorCode: string | null = null;
|
|
66
|
+
let errorSummary: string | null = null;
|
|
67
|
+
if (status.error && typeof status.error === "string") {
|
|
68
|
+
const errorStr = status.error as string;
|
|
69
|
+
// Keep only the first 200 chars as summary
|
|
70
|
+
errorSummary = errorStr.length > 200 ? errorStr.slice(0, 200) + "..." : errorStr;
|
|
71
|
+
// Try to extract an error code from the status
|
|
72
|
+
if (typeof status.status === "string" && (status.status as string).startsWith("failed")) {
|
|
73
|
+
errorCode = status.status as string;
|
|
74
|
+
}
|
|
75
|
+
} else if (typeof status.status === "string" && (status.status as string).startsWith("failed")) {
|
|
76
|
+
errorCode = status.status as string;
|
|
77
|
+
errorSummary = `Task ended with status: ${status.status}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
task_id: taskId,
|
|
82
|
+
status: (status.status as TaskStatus) || "not_found",
|
|
83
|
+
phase,
|
|
84
|
+
created_at: typeof status.created_at === "string" ? status.created_at : null,
|
|
85
|
+
started_at: typeof status.started_at === "string" ? status.started_at : null,
|
|
86
|
+
updated_at: typeof status.updated_at === "string" ? status.updated_at : null,
|
|
87
|
+
finished_at: typeof status.finished_at === "string" ? status.finished_at : null,
|
|
88
|
+
last_heartbeat_at: runtime.last_heartbeat_at || (typeof status.last_heartbeat_at === "string" ? status.last_heartbeat_at : null),
|
|
89
|
+
current_command: runtime.current_command ?? (typeof status.current_command === "string" ? status.current_command : null) ?? null,
|
|
90
|
+
verify_status: typeof status.verify_status === "string" ? (status.verify_status as "passed" | "failed" | "skipped") : null,
|
|
91
|
+
artifact_status: typeof status.artifact_status === "string" ? status.artifact_status : null,
|
|
92
|
+
watcher_state: watcher.status,
|
|
93
|
+
error_code: errorCode,
|
|
94
|
+
error_summary: errorSummary,
|
|
95
|
+
};
|
|
96
|
+
}
|