patchwarden 0.4.0 → 0.6.0

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.
Files changed (139) hide show
  1. package/PatchWarden.cmd +51 -0
  2. package/README.en.md +1022 -0
  3. package/README.md +897 -358
  4. package/dist/assessments/agentAssessor.d.ts +15 -0
  5. package/dist/assessments/agentAssessor.js +293 -0
  6. package/dist/assessments/assessmentStore.d.ts +133 -0
  7. package/dist/assessments/assessmentStore.js +238 -0
  8. package/dist/assessments/confirmCli.d.ts +9 -0
  9. package/dist/assessments/confirmCli.js +26 -0
  10. package/dist/config.d.ts +22 -1
  11. package/dist/config.js +156 -3
  12. package/dist/direct/directAudit.d.ts +23 -0
  13. package/dist/direct/directAudit.js +309 -0
  14. package/dist/direct/directGuards.d.ts +20 -0
  15. package/dist/direct/directGuards.js +137 -0
  16. package/dist/direct/directPatch.d.ts +17 -0
  17. package/dist/direct/directPatch.js +113 -0
  18. package/dist/direct/directSessionStore.d.ts +63 -0
  19. package/dist/direct/directSessionStore.js +192 -0
  20. package/dist/direct/directVerification.d.ts +12 -0
  21. package/dist/direct/directVerification.js +96 -0
  22. package/dist/doctor.js +73 -5
  23. package/dist/runner/agentInvocation.d.ts +23 -0
  24. package/dist/runner/agentInvocation.js +106 -0
  25. package/dist/runner/changeCapture.d.ts +29 -0
  26. package/dist/runner/changeCapture.js +107 -5
  27. package/dist/runner/runTask.js +70 -44
  28. package/dist/runner/simpleProcess.d.ts +21 -0
  29. package/dist/runner/simpleProcess.js +206 -0
  30. package/dist/runner/watch.js +17 -2
  31. package/dist/security/commandGuard.d.ts +2 -1
  32. package/dist/security/commandGuard.js +25 -3
  33. package/dist/security/riskEngine.d.ts +27 -0
  34. package/dist/security/riskEngine.js +118 -0
  35. package/dist/security/runtimeGuard.d.ts +6 -0
  36. package/dist/security/runtimeGuard.js +28 -0
  37. package/dist/smoke-test.js +1353 -7
  38. package/dist/tools/applyPatch.d.ts +16 -0
  39. package/dist/tools/applyPatch.js +41 -0
  40. package/dist/tools/auditSession.d.ts +5 -0
  41. package/dist/tools/auditSession.js +12 -0
  42. package/dist/tools/auditTask.d.ts +7 -0
  43. package/dist/tools/auditTask.js +94 -2
  44. package/dist/tools/createDirectSession.d.ts +14 -0
  45. package/dist/tools/createDirectSession.js +54 -0
  46. package/dist/tools/createTask.d.ts +48 -1
  47. package/dist/tools/createTask.js +298 -47
  48. package/dist/tools/finalizeDirectSession.d.ts +19 -0
  49. package/dist/tools/finalizeDirectSession.js +84 -0
  50. package/dist/tools/getTaskSummary.d.ts +47 -0
  51. package/dist/tools/getTaskSummary.js +64 -1
  52. package/dist/tools/healthCheck.d.ts +22 -12
  53. package/dist/tools/healthCheck.js +23 -6
  54. package/dist/tools/readWorkspaceFile.d.ts +7 -1
  55. package/dist/tools/readWorkspaceFile.js +48 -2
  56. package/dist/tools/registry.js +217 -19
  57. package/dist/tools/runVerification.d.ts +16 -0
  58. package/dist/tools/runVerification.js +32 -0
  59. package/dist/tools/savePlan.d.ts +1 -0
  60. package/dist/tools/savePlan.js +38 -7
  61. package/dist/tools/searchWorkspace.d.ts +19 -0
  62. package/dist/tools/searchWorkspace.js +205 -0
  63. package/dist/tools/taskTemplates.js +2 -1
  64. package/dist/tools/toolCatalog.d.ts +3 -2
  65. package/dist/tools/toolCatalog.js +32 -5
  66. package/dist/tools/waitForTask.d.ts +2 -2
  67. package/dist/tools/waitForTask.js +1 -1
  68. package/dist/version.d.ts +2 -2
  69. package/dist/version.js +2 -2
  70. package/docs/chatgpt-usage.md +101 -0
  71. package/docs/release-checklist.md +14 -0
  72. package/docs/release-v0.6.0.md +71 -0
  73. package/examples/config.example.json +5 -0
  74. package/examples/openai-tunnel/README.md +11 -4
  75. package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
  76. package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
  77. package/package.json +12 -8
  78. package/scripts/brand-check.js +58 -12
  79. package/scripts/control-smoke.js +206 -0
  80. package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
  81. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  82. package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
  83. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
  84. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
  85. package/scripts/lifecycle-smoke.js +66 -6
  86. package/scripts/manage-patchwarden.ps1 +639 -0
  87. package/scripts/mcp-manifest-check.js +100 -58
  88. package/scripts/mcp-smoke.js +157 -2
  89. package/scripts/pack-clean.js +3 -4
  90. package/scripts/package-manifest-check.js +78 -0
  91. package/scripts/patchwarden-mcp-direct.cmd +7 -0
  92. package/scripts/patchwarden-mcp-stdio.cmd +1 -1
  93. package/scripts/restart-patchwarden.ps1 +5 -4
  94. package/scripts/start-patchwarden-tunnel.ps1 +261 -30
  95. package/scripts/tunnel-supervisor-smoke.js +36 -4
  96. package/scripts/watcher-supervisor-smoke.js +10 -6
  97. package/src/assessments/agentAssessor.ts +324 -0
  98. package/src/assessments/assessmentStore.ts +426 -0
  99. package/src/assessments/confirmCli.ts +29 -0
  100. package/src/config.ts +178 -4
  101. package/src/direct/directAudit.ts +400 -0
  102. package/src/direct/directGuards.ts +279 -0
  103. package/src/direct/directPatch.ts +258 -0
  104. package/src/direct/directSessionStore.ts +345 -0
  105. package/src/direct/directVerification.ts +138 -0
  106. package/src/doctor.ts +103 -7
  107. package/src/runner/agentInvocation.ts +125 -0
  108. package/src/runner/changeCapture.ts +140 -5
  109. package/src/runner/runTask.ts +61 -43
  110. package/src/runner/simpleProcess.ts +223 -0
  111. package/src/runner/watch.ts +18 -2
  112. package/src/security/commandGuard.ts +46 -4
  113. package/src/security/riskEngine.ts +160 -0
  114. package/src/security/runtimeGuard.ts +41 -0
  115. package/src/smoke-test.ts +1291 -4
  116. package/src/tools/applyPatch.ts +86 -0
  117. package/src/tools/auditSession.ts +28 -0
  118. package/src/tools/auditTask.ts +100 -2
  119. package/src/tools/createDirectSession.ts +113 -0
  120. package/src/tools/createTask.ts +405 -55
  121. package/src/tools/finalizeDirectSession.ts +144 -0
  122. package/src/tools/getTaskSummary.ts +111 -1
  123. package/src/tools/healthCheck.ts +23 -6
  124. package/src/tools/readWorkspaceFile.ts +85 -2
  125. package/src/tools/registry.ts +242 -19
  126. package/src/tools/runVerification.ts +58 -0
  127. package/src/tools/savePlan.ts +57 -7
  128. package/src/tools/searchWorkspace.ts +275 -0
  129. package/src/tools/taskTemplates.ts +2 -1
  130. package/src/tools/toolCatalog.ts +35 -6
  131. package/src/tools/waitForTask.ts +3 -3
  132. package/src/version.ts +2 -2
  133. package/tsconfig.json +18 -17
  134. package/Check-PatchWarden-Health.cmd +0 -6
  135. package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
  136. package/Restart-PatchWarden.cmd +0 -19
  137. package/Start-PatchWarden-Tunnel.cmd +0 -7
  138. package/docs/release-v0.3.0.md +0 -43
  139. package/docs/release-v0.4.0.md +0 -74
@@ -0,0 +1,86 @@
1
+ import { getConfig } from "../config.js";
2
+ import {
3
+ readDirectSession,
4
+ appendDirectSessionOperation,
5
+ } from "../direct/directSessionStore.js";
6
+ import {
7
+ guardDirectSessionActive,
8
+ guardDirectWritePath,
9
+ guardDirectPatchSize,
10
+ } from "../direct/directGuards.js";
11
+ import {
12
+ applyPatchOperations,
13
+ validateExpectedSha256,
14
+ computeFileSha256,
15
+ type PatchOperation,
16
+ } from "../direct/directPatch.js";
17
+
18
+ export interface ApplyPatchInput {
19
+ session_id: string;
20
+ path: string;
21
+ expected_sha256: string;
22
+ operations: PatchOperation[];
23
+ }
24
+
25
+ export interface ApplyPatchOutput {
26
+ path: string;
27
+ before_sha256: string;
28
+ after_sha256: string;
29
+ operations_applied: number;
30
+ bytes_changed: number;
31
+ next_action: string;
32
+ }
33
+
34
+ export function applyPatch(input: ApplyPatchInput): ApplyPatchOutput {
35
+ const config = getConfig();
36
+
37
+ // 1. Read session and guard active (not expired, not finalized)
38
+ const session = readDirectSession(input.session_id);
39
+ guardDirectSessionActive(session);
40
+
41
+ // 2. Guard write path (inside repo, not sensitive, not binary,
42
+ // not node_modules/release/dist)
43
+ const resolvedPath = guardDirectWritePath(
44
+ input.path,
45
+ session.resolved_repo_path,
46
+ config.workspaceRoot
47
+ );
48
+
49
+ // 3. Calculate patch size and guard against directMaxPatchBytes
50
+ const patchBytes = JSON.stringify(input.operations).length;
51
+ guardDirectPatchSize(patchBytes);
52
+
53
+ // 4. Validate expected_sha256 matches current file hash
54
+ const beforeSha256 = validateExpectedSha256(
55
+ resolvedPath,
56
+ input.expected_sha256
57
+ );
58
+
59
+ // 5. Apply patch operations
60
+ const patchResult = applyPatchOperations(resolvedPath, input.operations);
61
+
62
+ // Re-read the file from disk to compute the authoritative after_sha256
63
+ const afterSha256 = computeFileSha256(resolvedPath);
64
+
65
+ // 6. Append operation record to session
66
+ appendDirectSessionOperation(input.session_id, {
67
+ index: session.operations.length,
68
+ timestamp: new Date().toISOString(),
69
+ path: input.path,
70
+ before_sha256: beforeSha256,
71
+ after_sha256: afterSha256,
72
+ operations_applied: patchResult.operations_applied,
73
+ bytes_changed: patchResult.bytes_changed,
74
+ });
75
+
76
+ // 7. Return result
77
+ return {
78
+ path: input.path,
79
+ before_sha256: beforeSha256,
80
+ after_sha256: afterSha256,
81
+ operations_applied: patchResult.operations_applied,
82
+ bytes_changed: patchResult.bytes_changed,
83
+ next_action:
84
+ "Call run_verification to test the changes, or apply_patch for more edits.",
85
+ };
86
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ auditDirectSession,
3
+ type DirectSessionAuditOutput,
4
+ } from "../direct/directAudit.js";
5
+ import { updateDirectSession } from "../direct/directSessionStore.js";
6
+
7
+ // ── Types ──────────────────────────────────────────────────────────
8
+
9
+ export interface AuditSessionInput {
10
+ session_id: string;
11
+ }
12
+
13
+ // ── Main function ──────────────────────────────────────────────────
14
+
15
+ export function auditSession(
16
+ input: AuditSessionInput
17
+ ): DirectSessionAuditOutput {
18
+ const { session_id } = input;
19
+
20
+ // 1. Perform independent audit (16 audit checks)
21
+ const output = auditDirectSession(session_id);
22
+
23
+ // 2. Mark session as audited
24
+ updateDirectSession(session_id, { audited: true });
25
+
26
+ // 3. Return the audit output
27
+ return output;
28
+ }
@@ -21,6 +21,13 @@ export interface AuditTaskOutput {
21
21
  summary: string;
22
22
  checks: AuditCheck[];
23
23
  risks: AuditRisk[];
24
+ confirmed_failures: AuditCheck[];
25
+ possible_false_positives: Array<{
26
+ check: string;
27
+ reason: string;
28
+ }>;
29
+ manual_verification_required: boolean;
30
+ manual_verification_items: string[];
24
31
  recommended_next_actions: string[];
25
32
  }
26
33
 
@@ -78,6 +85,16 @@ export function auditTask(taskId: string): AuditTaskOutput {
78
85
  const checks: AuditCheck[] = [];
79
86
  const risks: AuditRisk[] = [];
80
87
  const actions: string[] = [];
88
+ const possibleFalsePositives: AuditTaskOutput["possible_false_positives"] = [];
89
+ const manualVerificationItems: string[] = [];
90
+ const addManualVerification = (item: string) => {
91
+ if (!manualVerificationItems.includes(item)) manualVerificationItems.push(item);
92
+ };
93
+ const addPossibleFalsePositive = (check: string, reason: string) => {
94
+ if (!possibleFalsePositives.some((item) => item.check === check && item.reason === reason)) {
95
+ possibleFalsePositives.push({ check, reason });
96
+ }
97
+ };
81
98
 
82
99
  // ── 1. Task status ──
83
100
  const taskStatus = statusData.status || "unknown";
@@ -87,6 +104,9 @@ export function auditTask(taskId: string): AuditTaskOutput {
87
104
  result: taskStatus === "done" ? "pass" : failedStatuses.has(taskStatus) ? "fail" : "warn",
88
105
  detail: `Task status is "${taskStatus}".`,
89
106
  });
107
+ if (taskStatus !== "done" && !failedStatuses.has(taskStatus)) {
108
+ addManualVerification(`Task status is "${taskStatus}"; audit evidence may be incomplete until terminal state.`);
109
+ }
90
110
 
91
111
  // ── 2. result.md ──
92
112
  const resultFile = join(taskDir, "result.md");
@@ -111,6 +131,9 @@ export function auditTask(taskId: string): AuditTaskOutput {
111
131
  result: existsSync(verifyJsonFile) ? "pass" : "warn",
112
132
  detail: existsSync(verifyJsonFile) ? "verify.json found." : "verify.json is missing.",
113
133
  });
134
+ if (!existsSync(verifyJsonFile)) {
135
+ addManualVerification("verify.json is missing; determine whether independent verification was expected.");
136
+ }
114
137
  if (existsSync(verifyJsonFile)) {
115
138
  try {
116
139
  const verify = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
@@ -133,6 +156,42 @@ export function auditTask(taskId: string): AuditTaskOutput {
133
156
  : "No out-of-scope changes recorded.",
134
157
  });
135
158
 
159
+ const changedFilesFile = join(taskDir, "changed-files.json");
160
+ if (existsSync(changedFilesFile)) {
161
+ try {
162
+ const changeEvidence = JSON.parse(readFileSync(changedFilesFile, "utf-8"));
163
+ const hygiene = changeEvidence.artifact_hygiene;
164
+ if (hygiene?.counts) {
165
+ const trackedArtifacts = Number(hygiene.counts.tracked_build_artifacts || 0);
166
+ const ignoredArtifacts = Number(hygiene.counts.ignored_untracked_artifacts || 0);
167
+ const runtimeFiles = Number(hygiene.counts.runtime_generated_files || 0);
168
+ const suspicious = Number(hygiene.counts.suspicious_changes || 0);
169
+ checks.push({
170
+ name: "artifact_hygiene",
171
+ result: suspicious > 0 ? "warn" : "pass",
172
+ detail: suspicious > 0
173
+ ? `${suspicious} generated or artifact-like change(s) are tracked or not ignored and require review.`
174
+ : `${ignoredArtifacts} ignored artifact change(s) and ${runtimeFiles} runtime-generated change(s) are classified separately from source risk.`,
175
+ });
176
+ if (trackedArtifacts > 0) {
177
+ risks.push({ severity: "medium", description: `${trackedArtifacts} tracked build artifact change(s) require intentional source-control review.` });
178
+ addPossibleFalsePositive("artifact_hygiene", "Tracked build outputs may be intentional release assets rather than accidental source changes.");
179
+ }
180
+ if (suspicious > 0) {
181
+ actions.push("Review artifact_hygiene.suspicious_changes before accepting the task; add generated paths to Git ignore rules when appropriate.");
182
+ addPossibleFalsePositive("artifact_hygiene", "Artifact-like path classification is heuristic and may include intentionally maintained files.");
183
+ addManualVerification("Review artifact_hygiene.suspicious_changes and decide whether each path is intentional.");
184
+ }
185
+ } else {
186
+ checks.push({ name: "artifact_hygiene", result: "warn", detail: "Change evidence uses the legacy format without artifact classification." });
187
+ addManualVerification("Legacy changed-files evidence has no artifact classification; inspect changed paths manually.");
188
+ }
189
+ } catch {
190
+ checks.push({ name: "artifact_hygiene", result: "warn", detail: "changed-files.json could not be parsed for artifact classification." });
191
+ addManualVerification("changed-files.json could not be parsed; inspect repository changes directly.");
192
+ }
193
+ }
194
+
136
195
  // ── 3. test.log ──
137
196
  const testLogFile = join(taskDir, "test.log");
138
197
  const hasTestLog = existsSync(testLogFile);
@@ -141,6 +200,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
141
200
  result: hasTestLog ? "pass" : "warn",
142
201
  detail: hasTestLog ? "test.log found." : "test.log is missing.",
143
202
  });
203
+ if (!hasTestLog) addManualVerification("test.log is missing; confirm whether the task required an agent-side test run.");
144
204
 
145
205
  // ── 4. git.diff ──
146
206
  const diffFile = join(taskDir, "git.diff");
@@ -149,6 +209,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
149
209
  result: existsSync(diffFile) ? "pass" : "warn",
150
210
  detail: existsSync(diffFile) ? "git.diff found." : "git.diff is missing.",
151
211
  });
212
+ if (!existsSync(diffFile)) addManualVerification("git.diff is missing; inspect repository state before accepting code changes.");
152
213
 
153
214
  // ── 5. repo_path consistency — use resolved_repo_path, NOT resolve() ──
154
215
  let repoPathSafe = "";
@@ -192,6 +253,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
192
253
  });
193
254
  } catch {
194
255
  checks.push({ name: "package_json_scripts", result: "warn", detail: "package.json exists but could not be read (may be sensitive)." });
256
+ addManualVerification("package.json could not be read, so documented commands were not fully cross-checked.");
195
257
  }
196
258
  }
197
259
 
@@ -246,6 +308,11 @@ export function auditTask(taskId: string): AuditTaskOutput {
246
308
  description: `Command "npm run ${scriptName}" referenced in docs but not found in package.json scripts.`,
247
309
  });
248
310
  actions.push(`Verify whether "npm run ${scriptName}" should exist or if the agent fabricated it.`);
311
+ addPossibleFalsePositive(
312
+ `npm_script_${scriptName}`,
313
+ "Documentation may describe another package, historical version, or example command rather than the current package.json."
314
+ );
315
+ addManualVerification(`Check whether documented command "npm run ${scriptName}" belongs to another package or should be added here.`);
249
316
  }
250
317
  }
251
318
  if (allNpmRunRefs.size > 0 && pkgScripts.length > 0) {
@@ -271,6 +338,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
271
338
  description: `${allReleaseClaims.length} remote publish/release/deploy claim(s) found in docs. PatchWarden cannot independently verify npm/GitHub actions. Manual confirmation required.`,
272
339
  });
273
340
  actions.push("Manually verify all npm publish / GitHub release / git tag claims before accepting the task as complete.");
341
+ addManualVerification("Verify remote npm, GitHub Release, and Git tag claims against authoritative remote services.");
274
342
  }
275
343
 
276
344
  // ── 9. test.log Exit code check ──
@@ -282,6 +350,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
282
350
  checks.push({ name: "test_command_in_log", result: "pass", detail: "test.log contains the configured test command." });
283
351
  } else if (statusData.test_command) {
284
352
  checks.push({ name: "test_command_in_log", result: "warn", detail: `test.log does not clearly show "${statusData.test_command}".` });
353
+ addManualVerification("Confirm that test.log belongs to the configured test command.");
285
354
  }
286
355
 
287
356
  // Extract Exit code
@@ -298,6 +367,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
298
367
  } else if (statusData.test_command) {
299
368
  checks.push({ name: "test_exit_code", result: "warn", detail: "test.log does not contain 'Exit code:' line — cannot verify test result." });
300
369
  risks.push({ severity: "medium", description: "Test command was configured but test.log has no exit code." });
370
+ addManualVerification("The configured test command has no recorded exit code; rerun or inspect verification evidence.");
301
371
  }
302
372
  }
303
373
 
@@ -306,9 +376,11 @@ export function auditTask(taskId: string): AuditTaskOutput {
306
376
  const warnCount = checks.filter((c) => c.result === "warn").length;
307
377
  const passCount = checks.filter((c) => c.result === "pass").length;
308
378
  const verdict: AuditTaskOutput["verdict"] = failCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
379
+ const confirmedFailures = checks.filter((check) => check.result === "fail");
309
380
 
310
381
  let summary = `Audit complete: ${passCount} pass, ${warnCount} warn, ${failCount} fail across ${checks.length} checks. `;
311
- summary += risks.length > 0 ? `${risks.length} risk(s) identified.` : "No risks identified.";
382
+ summary += risks.length > 0 ? `${risks.length} risk(s) identified. ` : "No risks identified. ";
383
+ summary += `${confirmedFailures.length} confirmed failure(s), ${possibleFalsePositives.length} possible false-positive warning(s), and ${manualVerificationItems.length} manual verification item(s).`;
312
384
 
313
385
  if (actions.length === 0) {
314
386
  actions.push("No specific actions recommended.");
@@ -330,11 +402,37 @@ export function auditTask(taskId: string): AuditTaskOutput {
330
402
  "## Risks",
331
403
  ...risks.map((r) => `- [${r.severity}] ${r.description}`),
332
404
  "",
405
+ "## Confirmed Failures",
406
+ ...(confirmedFailures.length > 0
407
+ ? confirmedFailures.map((check) => `- **${check.name}**: ${check.detail}`)
408
+ : ["- None."]),
409
+ "",
410
+ "## Possible False Positives",
411
+ ...(possibleFalsePositives.length > 0
412
+ ? possibleFalsePositives.map((item) => `- **${item.check}**: ${item.reason}`)
413
+ : ["- None identified."]),
414
+ "",
415
+ "## Manual Verification Required",
416
+ ...(manualVerificationItems.length > 0
417
+ ? manualVerificationItems.map((item) => `- ${item}`)
418
+ : ["- No additional manual verification identified by this audit."]),
419
+ "",
333
420
  "## Recommended Actions",
334
421
  ...actions.map((a) => `- ${a}`),
335
422
  ].join("\n");
336
423
 
337
424
  writeFileSync(join(taskDir, "independent-review.md"), reviewMd, "utf-8");
338
425
 
339
- return { task_id: taskId, verdict, summary, checks, risks, recommended_next_actions: actions };
426
+ return {
427
+ task_id: taskId,
428
+ verdict,
429
+ summary,
430
+ checks,
431
+ risks,
432
+ confirmed_failures: confirmedFailures,
433
+ possible_false_positives: possibleFalsePositives,
434
+ manual_verification_required: manualVerificationItems.length > 0,
435
+ manual_verification_items: manualVerificationItems,
436
+ recommended_next_actions: actions,
437
+ };
340
438
  }
@@ -0,0 +1,113 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { getConfig } from "../config.js";
3
+ import { PatchWardenError } from "../errors.js";
4
+ import { guardWorkspacePath } from "../security/pathGuard.js";
5
+ import { guardRuntimeSelfModification } from "../security/runtimeGuard.js";
6
+ import { captureRepoSnapshot } from "../runner/changeCapture.js";
7
+ import {
8
+ createDirectSession as createDirectSessionRecord,
9
+ } from "../direct/directSessionStore.js";
10
+
11
+ // ── Types ──────────────────────────────────────────────────────────
12
+
13
+ export interface CreateDirectSessionInput {
14
+ repo_path: string;
15
+ title?: string;
16
+ }
17
+
18
+ export interface CreateDirectSessionOutput {
19
+ session_id: string;
20
+ repo_path: string;
21
+ resolved_repo_path: string;
22
+ workspace_clean: boolean;
23
+ allowed_commands: string[];
24
+ expires_at: string;
25
+ next_action: string;
26
+ }
27
+
28
+ // ── Tool implementation ────────────────────────────────────────────
29
+
30
+ export function createDirectSession(
31
+ input: CreateDirectSessionInput
32
+ ): CreateDirectSessionOutput {
33
+ const config = getConfig();
34
+
35
+ // ── Validate repo_path ───────────────────────────────────────────
36
+
37
+ if (!input.repo_path || input.repo_path.trim() === "") {
38
+ throw new PatchWardenError(
39
+ "invalid_input",
40
+ "repo_path is required and must be a non-empty string.",
41
+ "Provide a repository path inside the configured workspaceRoot.",
42
+ true,
43
+ { operation: "create_direct_session" }
44
+ );
45
+ }
46
+
47
+ const resolvedRepoPath = guardWorkspacePath(
48
+ input.repo_path,
49
+ config.workspaceRoot
50
+ );
51
+
52
+ // ── Verify existence ─────────────────────────────────────────────
53
+
54
+ if (!existsSync(resolvedRepoPath)) {
55
+ throw new PatchWardenError(
56
+ "repo_not_found",
57
+ `repo_path "${input.repo_path}" does not exist (resolved: "${resolvedRepoPath}").`,
58
+ "Provide a valid repository path inside the configured workspaceRoot.",
59
+ true,
60
+ {
61
+ repo_path: input.repo_path,
62
+ resolved_repo_path: resolvedRepoPath,
63
+ operation: "create_direct_session",
64
+ }
65
+ );
66
+ }
67
+
68
+ // ── Verify it is a directory ─────────────────────────────────────
69
+
70
+ const stat = statSync(resolvedRepoPath);
71
+ if (!stat.isDirectory()) {
72
+ throw new PatchWardenError(
73
+ "repo_not_directory",
74
+ `repo_path "${input.repo_path}" is not a directory.`,
75
+ "Provide a directory path, not a file path.",
76
+ true,
77
+ {
78
+ repo_path: input.repo_path,
79
+ resolved_repo_path: resolvedRepoPath,
80
+ operation: "create_direct_session",
81
+ }
82
+ );
83
+ }
84
+
85
+ // ── Runtime self-modification protection ─────────────────────────
86
+
87
+ guardRuntimeSelfModification(resolvedRepoPath);
88
+
89
+ // ── Capture repo snapshot ────────────────────────────────────────
90
+
91
+ const snapshot = captureRepoSnapshot(resolvedRepoPath);
92
+
93
+ // ── Create session record ────────────────────────────────────────
94
+
95
+ const session = createDirectSessionRecord({
96
+ repo_path: input.repo_path,
97
+ resolved_repo_path: resolvedRepoPath,
98
+ title: input.title,
99
+ snapshot,
100
+ });
101
+
102
+ return {
103
+ session_id: session.session_id,
104
+ repo_path: session.repo_path,
105
+ resolved_repo_path: session.resolved_repo_path,
106
+ workspace_clean: !snapshot.workspace_dirty,
107
+ allowed_commands: session.allowed_commands,
108
+ expires_at: session.expires_at,
109
+ next_action:
110
+ "Use search_workspace/read_workspace_file, then apply_patch to make file changes within this session. " +
111
+ "After editing, call run_verification, finalize_direct_session, and audit_session.",
112
+ };
113
+ }