patchwarden 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -175,7 +175,7 @@ try {
|
|
|
175
175
|
await test("git diff captures tracked and untracked task changes", async () => {
|
|
176
176
|
writeFileSync(join(repoPath, "preexisting-user-file.txt"), "preexisting and untouched\n", "utf-8");
|
|
177
177
|
const plan = savePlan({ title: "Change capture", content: "Modify fixture files." });
|
|
178
|
-
const task = createTask({
|
|
178
|
+
const task = await createTask({
|
|
179
179
|
plan_id: plan.plan_id,
|
|
180
180
|
agent: "writer",
|
|
181
181
|
repo_path: "repo",
|
|
@@ -231,7 +231,7 @@ try {
|
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
await test("inspect_only template fails when the agent changes repository files", async () => {
|
|
234
|
-
const task = createTask({
|
|
234
|
+
const task = await createTask({
|
|
235
235
|
template: "inspect_only",
|
|
236
236
|
goal: "Inspect the fixture without modifying files",
|
|
237
237
|
agent: "writer",
|
|
@@ -249,7 +249,7 @@ try {
|
|
|
249
249
|
|
|
250
250
|
await test("legacy test_command becomes one independent verification command", async () => {
|
|
251
251
|
const plan = savePlan({ title: "Legacy verification", content: "No changes." });
|
|
252
|
-
const task = createTask({
|
|
252
|
+
const task = await createTask({
|
|
253
253
|
plan_id: plan.plan_id,
|
|
254
254
|
agent: "noop",
|
|
255
255
|
repo_path: "repo",
|
|
@@ -267,7 +267,7 @@ try {
|
|
|
267
267
|
|
|
268
268
|
await test("no-change task returns an explicit empty diff result", async () => {
|
|
269
269
|
const plan = savePlan({ title: "No diff", content: "Do not change files." });
|
|
270
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo" });
|
|
270
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo" });
|
|
271
271
|
const result = await runTask(task.task_id);
|
|
272
272
|
if (!isDoneStatus(result.status)) throw new Error(`No-op task failed: ${JSON.stringify(result)}`);
|
|
273
273
|
const diff = getDiff(task.task_id);
|
|
@@ -288,7 +288,7 @@ try {
|
|
|
288
288
|
|
|
289
289
|
await test("large diff response is truncated while diff.patch remains complete", async () => {
|
|
290
290
|
const plan = savePlan({ title: "Large diff", content: "Create a large text file." });
|
|
291
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "largewriter", repo_path: "repo" });
|
|
291
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "largewriter", repo_path: "repo" });
|
|
292
292
|
const result = await runTask(task.task_id);
|
|
293
293
|
if (!isDoneStatus(result.status)) throw new Error(`Large task failed: ${JSON.stringify(result)}`);
|
|
294
294
|
const diff = getDiff(task.task_id);
|
|
@@ -300,7 +300,7 @@ try {
|
|
|
300
300
|
|
|
301
301
|
await test("non-Git repositories return hash-only evidence with a reason", async () => {
|
|
302
302
|
const plan = savePlan({ title: "Non-Git evidence", content: "Modify files in a non-Git repository." });
|
|
303
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "plain-repo" });
|
|
303
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "plain-repo" });
|
|
304
304
|
const result = await runTask(task.task_id);
|
|
305
305
|
if (!isDoneStatus(result.status)) throw new Error(`Non-Git task failed: ${JSON.stringify(result)}`);
|
|
306
306
|
const diff = getDiff(task.task_id);
|
|
@@ -315,7 +315,7 @@ try {
|
|
|
315
315
|
|
|
316
316
|
await test("binary Git changes remain reviewable as a textual Git binary patch", async () => {
|
|
317
317
|
const plan = savePlan({ title: "Binary evidence", content: "Create a binary fixture." });
|
|
318
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "binarywriter", repo_path: "repo" });
|
|
318
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "binarywriter", repo_path: "repo" });
|
|
319
319
|
const result = await runTask(task.task_id);
|
|
320
320
|
if (!isDoneStatus(result.status)) throw new Error(`Binary task failed: ${JSON.stringify(result)}`);
|
|
321
321
|
const diff = getDiff(task.task_id);
|
|
@@ -326,7 +326,7 @@ try {
|
|
|
326
326
|
|
|
327
327
|
await test("artifact hygiene separates source, ignored output, runtime data, and suspicious changes", async () => {
|
|
328
328
|
const plan = savePlan({ title: "Artifact hygiene", content: "Generate representative task outputs." });
|
|
329
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "artifactwriter", repo_path: "repo" });
|
|
329
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "artifactwriter", repo_path: "repo" });
|
|
330
330
|
const result = await runTask(task.task_id);
|
|
331
331
|
if (!isDoneStatus(result.status)) throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
|
|
332
332
|
const standard = getTaskSummary(task.task_id);
|
|
@@ -355,7 +355,7 @@ try {
|
|
|
355
355
|
|
|
356
356
|
await test("deleted tracked files are identified with file stats", async () => {
|
|
357
357
|
const plan = savePlan({ title: "Delete fixture", content: "Delete the designated fixture file." });
|
|
358
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "deleter", repo_path: "repo" });
|
|
358
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "deleter", repo_path: "repo" });
|
|
359
359
|
const result = await runTask(task.task_id);
|
|
360
360
|
if (!isDoneStatus(result.status)) throw new Error(`Delete task failed: ${JSON.stringify(result)}`);
|
|
361
361
|
const diff = getDiff(task.task_id);
|
|
@@ -366,7 +366,7 @@ try {
|
|
|
366
366
|
|
|
367
367
|
await test("wait_for_task stays in the tool loop and returns terminal acceptance", async () => {
|
|
368
368
|
const plan = savePlan({ title: "Wait loop", content: "Finish normally." });
|
|
369
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "repo" });
|
|
369
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "repo" });
|
|
370
370
|
const running = runTask(task.task_id);
|
|
371
371
|
const waited = await waitForTask(task.task_id, 5);
|
|
372
372
|
await raceWithTimeout(running, 15000, "wait_for_task loop did not terminate within 15s");
|
|
@@ -380,7 +380,7 @@ try {
|
|
|
380
380
|
|
|
381
381
|
await test("wait_for_task explicitly requires another call when the task is not terminal", async () => {
|
|
382
382
|
const plan = savePlan({ title: "Pending wait", content: "Remain queued for this check." });
|
|
383
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo" });
|
|
383
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo" });
|
|
384
384
|
const waited = await waitForTask(task.task_id, 1);
|
|
385
385
|
if (waited.terminal || !waited.timed_out || !waited.continuation_required) {
|
|
386
386
|
throw new Error(`Expected continuation response: ${JSON.stringify(waited)}`);
|
|
@@ -390,11 +390,17 @@ try {
|
|
|
390
390
|
|
|
391
391
|
await test("running task summary includes heartbeat, phase, command, and elapsed time", async () => {
|
|
392
392
|
const plan = savePlan({ title: "Running summary", content: "Wait." });
|
|
393
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
393
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
394
394
|
const running = runTask(task.task_id);
|
|
395
395
|
await waitForRunning(getTaskStatus, task.task_id);
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
// Wait for the task to transition from "preparing" to actually running a command.
|
|
397
|
+
// Async snapshot may delay the preparing phase, so poll until current_command appears.
|
|
398
|
+
let summary;
|
|
399
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
400
|
+
await sleep(50);
|
|
401
|
+
summary = getTaskSummary(task.task_id);
|
|
402
|
+
if (summary.current_command) break;
|
|
403
|
+
}
|
|
398
404
|
if (summary.terminal || !summary.last_heartbeat_at || !summary.phase || !summary.current_command || summary.elapsed_ms < 0) {
|
|
399
405
|
throw new Error(`Running summary incomplete: ${JSON.stringify(summary)}`);
|
|
400
406
|
}
|
|
@@ -404,7 +410,7 @@ try {
|
|
|
404
410
|
|
|
405
411
|
await test("verification failure produces failed_verification and structured evidence", async () => {
|
|
406
412
|
const plan = savePlan({ title: "Verify failure", content: "Finish normally." });
|
|
407
|
-
const task = createTask({
|
|
413
|
+
const task = await createTask({
|
|
408
414
|
plan_id: plan.plan_id,
|
|
409
415
|
agent: "writer",
|
|
410
416
|
repo_path: "repo",
|
|
@@ -434,7 +440,7 @@ try {
|
|
|
434
440
|
|
|
435
441
|
await test("out-of-scope workspace changes fail the task and generate a rollback plan", async () => {
|
|
436
442
|
const plan = savePlan({ title: "Scope violation", content: "Do not leave the repository." });
|
|
437
|
-
const task = createTask({
|
|
443
|
+
const task = await createTask({
|
|
438
444
|
plan_id: plan.plan_id,
|
|
439
445
|
agent: "scopebreaker",
|
|
440
446
|
repo_path: "repo",
|
|
@@ -505,7 +511,7 @@ try {
|
|
|
505
511
|
reloadConfig(cfg2);
|
|
506
512
|
|
|
507
513
|
const plan = savePlan({ title: "Noop with external dirty", content: "Do nothing." });
|
|
508
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
514
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
509
515
|
const result = await runTask(task.task_id);
|
|
510
516
|
|
|
511
517
|
if (!isDoneStatus(result.status)) {
|
|
@@ -590,7 +596,7 @@ try {
|
|
|
590
596
|
reloadConfig(cfg2);
|
|
591
597
|
|
|
592
598
|
const plan = savePlan({ title: "External dirty modifier", content: "Modify external tracked file." });
|
|
593
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
599
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
594
600
|
const result = await runTask(task.task_id);
|
|
595
601
|
|
|
596
602
|
if (result.status !== "failed_scope_violation") {
|
|
@@ -675,7 +681,7 @@ try {
|
|
|
675
681
|
reloadConfig(cfg2);
|
|
676
682
|
|
|
677
683
|
const plan = savePlan({ title: "Clean external modifier", content: "Modify clean external tracked file." });
|
|
678
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
684
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "extmod", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
679
685
|
const result = await runTask(task.task_id);
|
|
680
686
|
|
|
681
687
|
if (result.status !== "failed_scope_violation") {
|
|
@@ -752,7 +758,7 @@ try {
|
|
|
752
758
|
reloadConfig(cfg2);
|
|
753
759
|
|
|
754
760
|
const plan = savePlan({ title: "External rename", content: "Rename external tracked file." });
|
|
755
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "extrenamer", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
761
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "extrenamer", repo_path: "repo", verify_commands: ["node --check main.js"] });
|
|
756
762
|
const result = await runTask(task.task_id);
|
|
757
763
|
|
|
758
764
|
if (result.status !== "failed_scope_violation") {
|
|
@@ -794,7 +800,7 @@ try {
|
|
|
794
800
|
|
|
795
801
|
await test("timeout terminates a long-running agent", async () => {
|
|
796
802
|
const plan = savePlan({ title: "Timeout", content: "Wait." });
|
|
797
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 1 });
|
|
803
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 1 });
|
|
798
804
|
const started = Date.now();
|
|
799
805
|
const result = await runTask(task.task_id);
|
|
800
806
|
if (result.status !== "failed" || !result.error?.includes("timed out")) {
|
|
@@ -810,7 +816,7 @@ try {
|
|
|
810
816
|
|
|
811
817
|
await test("cancel_task safely stops a running agent", async () => {
|
|
812
818
|
const plan = savePlan({ title: "Cancel", content: "Wait." });
|
|
813
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
819
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
814
820
|
const running = runTask(task.task_id);
|
|
815
821
|
await waitForRunning(getTaskStatus, task.task_id);
|
|
816
822
|
const request = cancelTask(task.task_id);
|
|
@@ -823,7 +829,7 @@ try {
|
|
|
823
829
|
|
|
824
830
|
await test("kill_task immediately stops a running agent", async () => {
|
|
825
831
|
const plan = savePlan({ title: "Kill", content: "Wait." });
|
|
826
|
-
const task = createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
832
|
+
const task = await createTask({ plan_id: plan.plan_id, agent: "slow", repo_path: "repo", timeout_seconds: 30 });
|
|
827
833
|
const running = runTask(task.task_id);
|
|
828
834
|
await waitForRunning(getTaskStatus, task.task_id);
|
|
829
835
|
const request = killTask(task.task_id);
|
|
@@ -117,6 +117,7 @@ try {
|
|
|
117
117
|
"discard_worktree",
|
|
118
118
|
"discover_tools",
|
|
119
119
|
"explain_tool",
|
|
120
|
+
"export_goal_report",
|
|
120
121
|
"export_handoff",
|
|
121
122
|
"export_task_evidence_pack",
|
|
122
123
|
"finalize_direct_session",
|
|
@@ -133,6 +134,7 @@ try {
|
|
|
133
134
|
"get_task_summary",
|
|
134
135
|
"get_test_log",
|
|
135
136
|
"health_check",
|
|
137
|
+
"import_speckit_tasks",
|
|
136
138
|
"invoke_discovered_tool",
|
|
137
139
|
"kill_task",
|
|
138
140
|
"list_agents",
|
|
@@ -177,7 +179,7 @@ try {
|
|
|
177
179
|
if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
|
|
178
180
|
throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
|
|
179
181
|
}
|
|
180
|
-
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !==
|
|
182
|
+
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 66) {
|
|
181
183
|
throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
|
|
182
184
|
}
|
|
183
185
|
if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
OUT = ROOT / "docs" / "assets" / "patchwarden-oss-demo.gif"
|
|
10
|
+
|
|
11
|
+
W, H = 1280, 720
|
|
12
|
+
BG = (246, 248, 245)
|
|
13
|
+
INK = (32, 39, 39)
|
|
14
|
+
MUTED = (91, 101, 101)
|
|
15
|
+
GREEN = (38, 124, 92)
|
|
16
|
+
TEAL = (25, 112, 116)
|
|
17
|
+
BLUE = (48, 92, 160)
|
|
18
|
+
ORANGE = (183, 104, 36)
|
|
19
|
+
RED = (165, 62, 62)
|
|
20
|
+
LINE = (205, 214, 208)
|
|
21
|
+
PANEL = (255, 255, 252)
|
|
22
|
+
SHADOW = (218, 224, 219)
|
|
23
|
+
TERMINAL = (27, 34, 34)
|
|
24
|
+
TERMINAL_TEXT = (208, 230, 218)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
|
28
|
+
candidates = [
|
|
29
|
+
"C:/Windows/Fonts/segoeuib.ttf" if bold else "C:/Windows/Fonts/segoeui.ttf",
|
|
30
|
+
"C:/Windows/Fonts/arialbd.ttf" if bold else "C:/Windows/Fonts/arial.ttf",
|
|
31
|
+
]
|
|
32
|
+
for candidate in candidates:
|
|
33
|
+
try:
|
|
34
|
+
return ImageFont.truetype(candidate, size)
|
|
35
|
+
except OSError:
|
|
36
|
+
pass
|
|
37
|
+
return ImageFont.load_default()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
F_TITLE = font(44, True)
|
|
41
|
+
F_H2 = font(27, True)
|
|
42
|
+
F_BODY = font(22)
|
|
43
|
+
F_SMALL = font(18)
|
|
44
|
+
F_MONO = font(19)
|
|
45
|
+
F_MONO_SMALL = font(16)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def rounded(draw: ImageDraw.ImageDraw, box, fill, outline=None, radius=18, width=2):
|
|
49
|
+
x0, y0, x1, y1 = box
|
|
50
|
+
draw.rounded_rectangle((x0 + 6, y0 + 8, x1 + 6, y1 + 8), radius, fill=SHADOW)
|
|
51
|
+
draw.rounded_rectangle(box, radius, fill=fill, outline=outline or LINE, width=width)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def label(draw: ImageDraw.ImageDraw, xy, text, fill=INK, fnt=F_BODY, anchor=None):
|
|
55
|
+
draw.text(xy, text, fill=fill, font=fnt, anchor=anchor)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def pill(draw: ImageDraw.ImageDraw, xy, text, fill, text_fill=(255, 255, 255)):
|
|
59
|
+
x, y = xy
|
|
60
|
+
bbox = draw.textbbox((0, 0), text, font=F_SMALL)
|
|
61
|
+
pad_x = 14
|
|
62
|
+
pad_y = 7
|
|
63
|
+
w = bbox[2] - bbox[0] + pad_x * 2
|
|
64
|
+
h = bbox[3] - bbox[1] + pad_y * 2
|
|
65
|
+
draw.rounded_rectangle((x, y, x + w, y + h), h // 2, fill=fill)
|
|
66
|
+
draw.text((x + pad_x, y + pad_y - 1), text, fill=text_fill, font=F_SMALL)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def wrap(draw: ImageDraw.ImageDraw, text: str, width: int, fnt=F_BODY) -> list[str]:
|
|
70
|
+
lines: list[str] = []
|
|
71
|
+
current = ""
|
|
72
|
+
for word in text.split():
|
|
73
|
+
test = f"{current} {word}".strip()
|
|
74
|
+
if draw.textlength(test, font=fnt) <= width:
|
|
75
|
+
current = test
|
|
76
|
+
else:
|
|
77
|
+
if current:
|
|
78
|
+
lines.append(current)
|
|
79
|
+
current = word
|
|
80
|
+
if current:
|
|
81
|
+
lines.append(current)
|
|
82
|
+
return lines
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def draw_panel_text(draw, box, title, body, accent):
|
|
86
|
+
x0, y0, x1, y1 = box
|
|
87
|
+
rounded(draw, box, PANEL, LINE)
|
|
88
|
+
draw.rectangle((x0, y0, x0 + 8, y1), fill=accent)
|
|
89
|
+
label(draw, (x0 + 28, y0 + 24), title, fnt=F_H2)
|
|
90
|
+
y = y0 + 68
|
|
91
|
+
for line in wrap(draw, body, x1 - x0 - 56, F_BODY):
|
|
92
|
+
label(draw, (x0 + 28, y), line, fill=MUTED, fnt=F_BODY)
|
|
93
|
+
y += 30
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def draw_terminal(draw, box, lines):
|
|
97
|
+
rounded(draw, box, TERMINAL, (66, 78, 78), radius=16)
|
|
98
|
+
x0, y0, x1, _ = box
|
|
99
|
+
draw.ellipse((x0 + 20, y0 + 18, x0 + 32, y0 + 30), fill=(236, 98, 86))
|
|
100
|
+
draw.ellipse((x0 + 42, y0 + 18, x0 + 54, y0 + 30), fill=(241, 190, 76))
|
|
101
|
+
draw.ellipse((x0 + 64, y0 + 18, x0 + 76, y0 + 30), fill=(99, 198, 103))
|
|
102
|
+
y = y0 + 54
|
|
103
|
+
for line, color in lines:
|
|
104
|
+
label(draw, (x0 + 24, y), line, fill=color, fnt=F_MONO_SMALL)
|
|
105
|
+
y += 25
|
|
106
|
+
if y > box[3] - 24:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def draw_progress(draw, active: int):
|
|
111
|
+
steps = [
|
|
112
|
+
("Plan", BLUE),
|
|
113
|
+
("Guard", TEAL),
|
|
114
|
+
("Execute", ORANGE),
|
|
115
|
+
("Evidence", GREEN),
|
|
116
|
+
("Review", GREEN),
|
|
117
|
+
]
|
|
118
|
+
start_x = 165
|
|
119
|
+
y = 650
|
|
120
|
+
gap = 235
|
|
121
|
+
for i, (name, color) in enumerate(steps):
|
|
122
|
+
x = start_x + gap * i
|
|
123
|
+
if i > 0:
|
|
124
|
+
line_color = color if i <= active else LINE
|
|
125
|
+
draw.line((x - gap + 48, y, x - 48, y), fill=line_color, width=5)
|
|
126
|
+
fill = color if i <= active else (226, 232, 228)
|
|
127
|
+
text = (255, 255, 255) if i <= active else MUTED
|
|
128
|
+
draw.ellipse((x - 28, y - 28, x + 28, y + 28), fill=fill)
|
|
129
|
+
label(draw, (x, y - 1), str(i + 1), fill=text, fnt=F_H2, anchor="mm")
|
|
130
|
+
label(draw, (x, y + 44), name, fill=INK if i <= active else MUTED, fnt=F_SMALL, anchor="mm")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def base(title: str, subtitle: str, active: int) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
|
134
|
+
img = Image.new("RGB", (W, H), BG)
|
|
135
|
+
draw = ImageDraw.Draw(img)
|
|
136
|
+
label(draw, (64, 42), title, fnt=F_TITLE)
|
|
137
|
+
label(draw, (66, 96), subtitle, fill=MUTED, fnt=F_BODY)
|
|
138
|
+
pill(draw, (1040, 48), "privacy-safe demo", GREEN)
|
|
139
|
+
draw_progress(draw, active)
|
|
140
|
+
return img, draw
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def frame_intro():
|
|
144
|
+
img, draw = base(
|
|
145
|
+
"PatchWarden OSS Demo",
|
|
146
|
+
"A safe MCP task loop for ChatGPT, Codex, OpenCode, and local maintainers.",
|
|
147
|
+
0,
|
|
148
|
+
)
|
|
149
|
+
draw_panel_text(
|
|
150
|
+
draw,
|
|
151
|
+
(90, 170, 1190, 530),
|
|
152
|
+
"What this GIF shows",
|
|
153
|
+
"A model plans work, PatchWarden turns it into a bounded task, a registered local agent executes it, and the maintainer reviews redacted evidence before accepting anything.",
|
|
154
|
+
GREEN,
|
|
155
|
+
)
|
|
156
|
+
pill(draw, (130, 420), "no API keys", BLUE)
|
|
157
|
+
pill(draw, (280, 420), "no real account names", TEAL)
|
|
158
|
+
pill(draw, (520, 420), "no unrestricted shell", ORANGE)
|
|
159
|
+
pill(draw, (770, 420), "no auto-publish", RED)
|
|
160
|
+
return img
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def frame_plan():
|
|
164
|
+
img, draw = base(
|
|
165
|
+
"1. Model creates a bounded plan",
|
|
166
|
+
"The upstream client asks for a small maintainer task, not a raw shell command.",
|
|
167
|
+
0,
|
|
168
|
+
)
|
|
169
|
+
draw_panel_text(
|
|
170
|
+
draw,
|
|
171
|
+
(70, 160, 590, 520),
|
|
172
|
+
"ChatGPT / Codex",
|
|
173
|
+
"Goal: update docs for a small OSS workflow. Verify with npm.cmd test. Return summary, diff, and evidence only.",
|
|
174
|
+
BLUE,
|
|
175
|
+
)
|
|
176
|
+
draw_panel_text(
|
|
177
|
+
draw,
|
|
178
|
+
(690, 160, 1210, 520),
|
|
179
|
+
"PatchWarden MCP",
|
|
180
|
+
"Receives structured task input with repo_path, agent, template, and allowlisted verification commands.",
|
|
181
|
+
TEAL,
|
|
182
|
+
)
|
|
183
|
+
draw.line((600, 340, 680, 340), fill=BLUE, width=6)
|
|
184
|
+
draw.polygon([(680, 340), (660, 328), (660, 352)], fill=BLUE)
|
|
185
|
+
return img
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def frame_guard():
|
|
189
|
+
img, draw = base(
|
|
190
|
+
"2. PatchWarden checks the safety boundary",
|
|
191
|
+
"Local policy decides what can run and where files may change.",
|
|
192
|
+
1,
|
|
193
|
+
)
|
|
194
|
+
draw_panel_text(
|
|
195
|
+
draw,
|
|
196
|
+
(70, 160, 1210, 330),
|
|
197
|
+
"Guardrails",
|
|
198
|
+
"repo_path must stay under workspaceRoot. Agent launch commands come from trusted config. Verification commands must exactly match allowedTestCommands.",
|
|
199
|
+
TEAL,
|
|
200
|
+
)
|
|
201
|
+
draw_panel_text(
|
|
202
|
+
draw,
|
|
203
|
+
(70, 380, 580, 540),
|
|
204
|
+
"Blocked by default",
|
|
205
|
+
".env, tokens, SSH keys, cookies, credential files, out-of-workspace writes, and arbitrary shell.",
|
|
206
|
+
RED,
|
|
207
|
+
)
|
|
208
|
+
draw_panel_text(
|
|
209
|
+
draw,
|
|
210
|
+
(700, 380, 1210, 540),
|
|
211
|
+
"Allowed",
|
|
212
|
+
"A pre-registered local agent can work on the requested repository and produce auditable evidence.",
|
|
213
|
+
GREEN,
|
|
214
|
+
)
|
|
215
|
+
return img
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def frame_execute():
|
|
219
|
+
img, draw = base(
|
|
220
|
+
"3. Watcher launches the registered agent",
|
|
221
|
+
"The task is executed locally while PatchWarden records status and scope evidence.",
|
|
222
|
+
2,
|
|
223
|
+
)
|
|
224
|
+
draw_terminal(
|
|
225
|
+
draw,
|
|
226
|
+
(80, 160, 1200, 540),
|
|
227
|
+
[
|
|
228
|
+
("> patchwarden watcher", TERMINAL_TEXT),
|
|
229
|
+
("queued task_20260709_demo", (156, 205, 255)),
|
|
230
|
+
("repo_path: demo-oss-project", TERMINAL_TEXT),
|
|
231
|
+
("agent: opencode (registered)", TERMINAL_TEXT),
|
|
232
|
+
("verify: npm.cmd test (allowlisted)", TERMINAL_TEXT),
|
|
233
|
+
("scope: inside workspaceRoot", (142, 222, 170)),
|
|
234
|
+
("status: running -> done", (142, 222, 170)),
|
|
235
|
+
],
|
|
236
|
+
)
|
|
237
|
+
return img
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def frame_evidence():
|
|
241
|
+
img, draw = base(
|
|
242
|
+
"4. Evidence is written for review",
|
|
243
|
+
"The maintainer can inspect safe summaries before opening deeper logs or diffs.",
|
|
244
|
+
3,
|
|
245
|
+
)
|
|
246
|
+
draw_panel_text(
|
|
247
|
+
draw,
|
|
248
|
+
(80, 150, 410, 540),
|
|
249
|
+
"result.json",
|
|
250
|
+
"status, warnings, changed-file groups, and recommended next actions.",
|
|
251
|
+
GREEN,
|
|
252
|
+
)
|
|
253
|
+
draw_panel_text(
|
|
254
|
+
draw,
|
|
255
|
+
(475, 150, 805, 540),
|
|
256
|
+
"diff.patch",
|
|
257
|
+
"Git diff evidence when available, kept separate from the model plan.",
|
|
258
|
+
BLUE,
|
|
259
|
+
)
|
|
260
|
+
draw_panel_text(
|
|
261
|
+
draw,
|
|
262
|
+
(870, 150, 1200, 540),
|
|
263
|
+
"verify.json",
|
|
264
|
+
"Exact commands, exit codes, and verification status.",
|
|
265
|
+
ORANGE,
|
|
266
|
+
)
|
|
267
|
+
return img
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def frame_review():
|
|
271
|
+
img, draw = base(
|
|
272
|
+
"5. Maintainer accepts, fixes, or rejects",
|
|
273
|
+
"PatchWarden supports review. It does not silently push, publish, or restart services.",
|
|
274
|
+
4,
|
|
275
|
+
)
|
|
276
|
+
draw_panel_text(
|
|
277
|
+
draw,
|
|
278
|
+
(80, 160, 1200, 520),
|
|
279
|
+
"OSS maintainer handoff",
|
|
280
|
+
"Use safe_result, safe_audit, safe_test_summary, and evidence packs to decide the next step. Release actions still require separate human confirmation and GitHub/npm verification.",
|
|
281
|
+
GREEN,
|
|
282
|
+
)
|
|
283
|
+
pill(draw, (165, 410), "reviewable", GREEN)
|
|
284
|
+
pill(draw, (360, 410), "redacted", TEAL)
|
|
285
|
+
pill(draw, (525, 410), "bounded", BLUE)
|
|
286
|
+
pill(draw, (680, 410), "confirmation-gated", ORANGE)
|
|
287
|
+
return img
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def hold(frames, image, count):
|
|
291
|
+
for _ in range(count):
|
|
292
|
+
frames.append(image.copy())
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
scenes = [
|
|
298
|
+
frame_intro(),
|
|
299
|
+
frame_plan(),
|
|
300
|
+
frame_guard(),
|
|
301
|
+
frame_execute(),
|
|
302
|
+
frame_evidence(),
|
|
303
|
+
frame_review(),
|
|
304
|
+
]
|
|
305
|
+
frames: list[Image.Image] = []
|
|
306
|
+
for scene in scenes:
|
|
307
|
+
hold(frames, scene, 12)
|
|
308
|
+
frames[0].save(
|
|
309
|
+
OUT,
|
|
310
|
+
save_all=True,
|
|
311
|
+
append_images=frames[1:],
|
|
312
|
+
duration=120,
|
|
313
|
+
loop=0,
|
|
314
|
+
optimize=True,
|
|
315
|
+
)
|
|
316
|
+
print(OUT)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
main()
|
|
@@ -21,7 +21,7 @@ export interface AgentAssessorInput {
|
|
|
21
21
|
config: PatchWardenConfig;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function runAgentAssessment(input: AgentAssessorInput): AgentAssessmentSummary {
|
|
24
|
+
export async function runAgentAssessment(input: AgentAssessorInput): Promise<AgentAssessmentSummary> {
|
|
25
25
|
const logPaths: AgentAssessmentSummary["log_paths"] = {
|
|
26
26
|
stdout: resolve(input.assessmentDir, "agent-assessment-stdout.log"),
|
|
27
27
|
stderr: resolve(input.assessmentDir, "agent-assessment-stderr.log"),
|
|
@@ -53,7 +53,7 @@ export function runAgentAssessment(input: AgentAssessorInput): AgentAssessmentSu
|
|
|
53
53
|
// ── 2. Before snapshot (repo-scoped) ──
|
|
54
54
|
let repoBefore: RepoSnapshot;
|
|
55
55
|
try {
|
|
56
|
-
repoBefore = captureRepoSnapshot(input.repoPath);
|
|
56
|
+
repoBefore = await captureRepoSnapshot(input.repoPath);
|
|
57
57
|
} catch {
|
|
58
58
|
// If we can't capture before snapshot, conservatively skip agent assessment
|
|
59
59
|
emptySummary.status = "spawn_failed";
|
|
@@ -95,7 +95,7 @@ export function runAgentAssessment(input: AgentAssessorInput): AgentAssessmentSu
|
|
|
95
95
|
let readOnlyViolation = false;
|
|
96
96
|
let violationFiles: string[] = [];
|
|
97
97
|
try {
|
|
98
|
-
const repoAfter = captureRepoSnapshot(input.repoPath);
|
|
98
|
+
const repoAfter = await captureRepoSnapshot(input.repoPath);
|
|
99
99
|
const changes = compareSnapshots(repoBefore, repoAfter);
|
|
100
100
|
if (changes.length > 0) {
|
|
101
101
|
readOnlyViolation = true;
|
|
@@ -314,7 +314,7 @@ export function validateAssessmentFreshness(
|
|
|
314
314
|
* This is intentionally not registered as an MCP tool: a remote client may
|
|
315
315
|
* request confirmation, but it cannot grant confirmation to itself.
|
|
316
316
|
*/
|
|
317
|
-
export function confirmAssessment(assessmentId: string): AssessmentConfirmationResult {
|
|
317
|
+
export async function confirmAssessment(assessmentId: string): Promise<AssessmentConfirmationResult> {
|
|
318
318
|
if (!/^assessment_\d{8}_\d{6}_[0-9a-f]{32}$/.test(assessmentId)) {
|
|
319
319
|
throw new PatchWardenError(
|
|
320
320
|
"assessment_id_invalid",
|
|
@@ -334,7 +334,7 @@ export function confirmAssessment(assessmentId: string): AssessmentConfirmationR
|
|
|
334
334
|
);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
const snapshot = captureRepoSnapshot(assessment.resolved_repo_path);
|
|
337
|
+
const snapshot = await captureRepoSnapshot(assessment.resolved_repo_path);
|
|
338
338
|
const validation = validateAssessmentFreshness(assessmentId, snapshot, {
|
|
339
339
|
allow_unconfirmed: true,
|
|
340
340
|
// A standalone CLI has no active MCP profile snapshot. The execute path
|
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
import { loadConfig } from "../config.js";
|
|
11
11
|
import { confirmAssessment } from "./assessmentStore.js";
|
|
12
12
|
import { errorPayload } from "../errors.js";
|
|
13
|
+
import { logger } from "../logging.js";
|
|
13
14
|
|
|
14
15
|
loadConfig();
|
|
15
16
|
|
|
16
17
|
const assessmentId = process.argv[2];
|
|
17
18
|
if (!assessmentId || process.argv.length !== 3) {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
logger.info("Usage: patchwarden-confirm <full_assessment_id>");
|
|
20
|
+
logger.info("The display-only assessment_short_id is not accepted.");
|
|
20
21
|
process.exit(1);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
try {
|
|
24
|
-
const result = confirmAssessment(assessmentId);
|
|
25
|
+
const result = await confirmAssessment(assessmentId);
|
|
25
26
|
console.log(JSON.stringify(result, null, 2));
|
|
26
27
|
} catch (error) {
|
|
27
|
-
|
|
28
|
+
logger.error("confirm_assessment_failed", { error: errorPayload(error) });
|
|
28
29
|
process.exit(1);
|
|
29
30
|
}
|
package/src/config.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface PatchWardenConfig {
|
|
|
23
23
|
watcherStaleSeconds: number;
|
|
24
24
|
toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct" | "chatgpt_search";
|
|
25
25
|
repoAliases?: Record<string, string>;
|
|
26
|
+
httpPort?: number;
|
|
27
|
+
http?: { port?: number; host?: string; ownerTokenEnv?: string };
|
|
28
|
+
enableRunTaskTool?: boolean;
|
|
26
29
|
enableAgentAssessment?: boolean;
|
|
27
30
|
agentAssessmentTimeoutSeconds?: number;
|
|
28
31
|
agentAssessmentMaxOutputBytes?: number;
|
|
@@ -233,6 +236,40 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
|
|
|
233
236
|
) {
|
|
234
237
|
throw new Error('toolProfile must be "full", "chatgpt_core", "chatgpt_direct", or "chatgpt_search"');
|
|
235
238
|
}
|
|
239
|
+
if (config.repoAliases !== undefined) {
|
|
240
|
+
if (typeof config.repoAliases !== "object" || config.repoAliases === null || Array.isArray(config.repoAliases)) {
|
|
241
|
+
throw new Error("repoAliases must be an object mapping alias names to repository paths");
|
|
242
|
+
}
|
|
243
|
+
for (const [alias, target] of Object.entries(config.repoAliases)) {
|
|
244
|
+
if (typeof target !== "string") {
|
|
245
|
+
throw new Error(`repoAliases["${alias}"] must be a string`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (config.httpPort !== undefined) {
|
|
250
|
+
if (typeof config.httpPort !== "number" || !Number.isInteger(config.httpPort) || config.httpPort < 1 || config.httpPort > 65535) {
|
|
251
|
+
throw new Error("httpPort must be an integer from 1 to 65535");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (config.http !== undefined) {
|
|
255
|
+
if (typeof config.http !== "object" || config.http === null || Array.isArray(config.http)) {
|
|
256
|
+
throw new Error("http must be an object");
|
|
257
|
+
}
|
|
258
|
+
if (config.http.port !== undefined) {
|
|
259
|
+
if (typeof config.http.port !== "number" || !Number.isInteger(config.http.port) || config.http.port < 1 || config.http.port > 65535) {
|
|
260
|
+
throw new Error("http.port must be an integer from 1 to 65535");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (config.http.host !== undefined && typeof config.http.host !== "string") {
|
|
264
|
+
throw new Error("http.host must be a string");
|
|
265
|
+
}
|
|
266
|
+
if (config.http.ownerTokenEnv !== undefined && typeof config.http.ownerTokenEnv !== "string") {
|
|
267
|
+
throw new Error("http.ownerTokenEnv must be a string");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (config.enableRunTaskTool !== undefined && typeof config.enableRunTaskTool !== "boolean") {
|
|
271
|
+
throw new Error("enableRunTaskTool must be a boolean");
|
|
272
|
+
}
|
|
236
273
|
if (config.enableAgentAssessment !== undefined && typeof config.enableAgentAssessment !== "boolean") {
|
|
237
274
|
throw new Error("enableAgentAssessment must be a boolean");
|
|
238
275
|
}
|