patchwarden 1.1.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 +83 -8
- package/README.md +78 -10
- 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 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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 +261 -255
- 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.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- 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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- 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 +45 -0
- package/dist/tools/evidencePack.js +375 -0
- 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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- 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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -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 +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -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 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- 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/policy/projectPolicy.ts +344 -0
- 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 +254 -252
- 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 +251 -0
- 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/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- 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 +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- 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);
|
|
@@ -105,6 +105,11 @@ try {
|
|
|
105
105
|
throw new Error("get_task_summary schema must expose view and max_items");
|
|
106
106
|
}
|
|
107
107
|
const safeRequirements = {
|
|
108
|
+
run_task_loop: ["repo_path", "goal", "verify_commands", "direct_verify", "isolation_mode", "worktree_cleanup"],
|
|
109
|
+
recommend_agent_for_task: ["repo_path", "goal"],
|
|
110
|
+
get_task_lineage: ["lineage_id"],
|
|
111
|
+
export_task_evidence_pack: ["lineage_id"],
|
|
112
|
+
get_project_policy: ["repo_path"],
|
|
108
113
|
safe_result: ["task_id"],
|
|
109
114
|
safe_audit: ["task_id"],
|
|
110
115
|
safe_test_summary: ["task_id"],
|
|
@@ -124,6 +129,7 @@ try {
|
|
|
124
129
|
create_direct_session: ["repo_path"],
|
|
125
130
|
apply_patch: ["session_id", "path", "expected_sha256", "operations"],
|
|
126
131
|
run_verification: ["session_id", "command"],
|
|
132
|
+
run_direct_verification_bundle: ["session_id", "commands"],
|
|
127
133
|
finalize_direct_session: ["session_id"],
|
|
128
134
|
audit_session: ["session_id"],
|
|
129
135
|
safe_direct_summary: ["session_id"],
|
|
@@ -200,6 +206,11 @@ try {
|
|
|
200
206
|
health_check: ["detail"],
|
|
201
207
|
list_tasks: ["repo_path", "active_only"],
|
|
202
208
|
get_task_summary: ["view", "max_items"],
|
|
209
|
+
run_task_loop: ["repo_path", "goal", "verify_commands", "direct_verify", "isolation_mode", "worktree_cleanup"],
|
|
210
|
+
recommend_agent_for_task: ["repo_path", "goal"],
|
|
211
|
+
get_task_lineage: ["lineage_id"],
|
|
212
|
+
export_task_evidence_pack: ["lineage_id"],
|
|
213
|
+
get_project_policy: ["repo_path"],
|
|
203
214
|
safe_result: ["task_id"],
|
|
204
215
|
safe_audit: ["task_id"],
|
|
205
216
|
safe_test_summary: ["task_id"],
|
|
@@ -208,6 +219,7 @@ try {
|
|
|
208
219
|
create_direct_session: ["repo_path"],
|
|
209
220
|
apply_patch: ["session_id", "path", "expected_sha256", "operations"],
|
|
210
221
|
run_verification: ["session_id", "command"],
|
|
222
|
+
run_direct_verification_bundle: ["session_id", "commands"],
|
|
211
223
|
finalize_direct_session: ["session_id"],
|
|
212
224
|
audit_session: ["session_id"],
|
|
213
225
|
safe_direct_summary: ["session_id"],
|
|
@@ -117,12 +117,16 @@ try {
|
|
|
117
117
|
"discard_worktree",
|
|
118
118
|
"discover_tools",
|
|
119
119
|
"explain_tool",
|
|
120
|
+
"export_goal_report",
|
|
120
121
|
"export_handoff",
|
|
122
|
+
"export_task_evidence_pack",
|
|
121
123
|
"finalize_direct_session",
|
|
122
124
|
"get_diff",
|
|
123
125
|
"get_plan",
|
|
126
|
+
"get_project_policy",
|
|
124
127
|
"get_result",
|
|
125
128
|
"get_result_json",
|
|
129
|
+
"get_task_lineage",
|
|
126
130
|
"get_task_log_tail",
|
|
127
131
|
"get_task_progress",
|
|
128
132
|
"get_task_status",
|
|
@@ -130,6 +134,7 @@ try {
|
|
|
130
134
|
"get_task_summary",
|
|
131
135
|
"get_test_log",
|
|
132
136
|
"health_check",
|
|
137
|
+
"import_speckit_tasks",
|
|
133
138
|
"invoke_discovered_tool",
|
|
134
139
|
"kill_task",
|
|
135
140
|
"list_agents",
|
|
@@ -139,9 +144,16 @@ try {
|
|
|
139
144
|
"merge_worktree",
|
|
140
145
|
"read_goal",
|
|
141
146
|
"read_workspace_file",
|
|
147
|
+
"recommend_agent_for_task",
|
|
142
148
|
"reconcile_tasks",
|
|
143
149
|
"reject_subgoal",
|
|
150
|
+
"release_check",
|
|
151
|
+
"release_cleanup",
|
|
152
|
+
"release_prepare",
|
|
153
|
+
"release_verify",
|
|
144
154
|
"retry_task",
|
|
155
|
+
"run_direct_verification_bundle",
|
|
156
|
+
"run_task_loop",
|
|
145
157
|
"run_verification",
|
|
146
158
|
"safe_audit",
|
|
147
159
|
"safe_audit_direct_session",
|
|
@@ -167,7 +179,7 @@ try {
|
|
|
167
179
|
if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
|
|
168
180
|
throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
|
|
169
181
|
}
|
|
170
|
-
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !==
|
|
182
|
+
if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 66) {
|
|
171
183
|
throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
|
|
172
184
|
}
|
|
173
185
|
if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
|
|
@@ -417,7 +429,7 @@ try {
|
|
|
417
429
|
await disabledClient.close();
|
|
418
430
|
ok("chatgpt_direct disabled exposes only health_check with diagnostic");
|
|
419
431
|
|
|
420
|
-
// 2. chatgpt_direct enabled:
|
|
432
|
+
// 2. chatgpt_direct enabled: 14 tools + minimal create_direct_session
|
|
421
433
|
const enabledConfigPath = join(tempRoot, "direct-enabled.json");
|
|
422
434
|
const directRepo = join(workspaceRoot, "direct-fixture");
|
|
423
435
|
mkdirSync(join(directRepo, "src"), { recursive: true });
|
|
@@ -473,6 +485,7 @@ try {
|
|
|
473
485
|
"health_check",
|
|
474
486
|
"list_workspace",
|
|
475
487
|
"read_workspace_file",
|
|
488
|
+
"run_direct_verification_bundle",
|
|
476
489
|
"run_verification",
|
|
477
490
|
"safe_audit_direct_session",
|
|
478
491
|
"safe_direct_summary",
|
|
@@ -483,8 +496,8 @@ try {
|
|
|
483
496
|
if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
|
|
484
497
|
throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
|
|
485
498
|
}
|
|
486
|
-
if (enabledTools._meta.tool_count !==
|
|
487
|
-
throw new Error(`chatgpt_direct enabled tool_count should be
|
|
499
|
+
if (enabledTools._meta.tool_count !== 14) {
|
|
500
|
+
throw new Error(`chatgpt_direct enabled tool_count should be 14, got ${enabledTools._meta.tool_count}`);
|
|
488
501
|
}
|
|
489
502
|
|
|
490
503
|
// Minimal create_direct_session
|
|
@@ -497,6 +510,19 @@ try {
|
|
|
497
510
|
if (!sessionResult.session_id || !sessionResult.session_id.startsWith("direct_")) {
|
|
498
511
|
throw new Error(`create_direct_session failed: ${JSON.stringify(sessionResult)}`);
|
|
499
512
|
}
|
|
513
|
+
const bundleResult = JSON.parse(
|
|
514
|
+
(await enabledClient.callTool({
|
|
515
|
+
name: "run_direct_verification_bundle",
|
|
516
|
+
arguments: { session_id: sessionResult.session_id, commands: ["npm test"], timeout_seconds: 30 },
|
|
517
|
+
})).content?.[0]?.text || "{}"
|
|
518
|
+
);
|
|
519
|
+
if (bundleResult.status !== "passed" || bundleResult.command_count !== 1 || bundleResult.passed_commands !== 1) {
|
|
520
|
+
throw new Error(`run_direct_verification_bundle failed: ${JSON.stringify(bundleResult)}`);
|
|
521
|
+
}
|
|
522
|
+
const bundleText = JSON.stringify(bundleResult).toLowerCase();
|
|
523
|
+
if (bundleText.includes("stdout_tail") || bundleText.includes("stderr_tail") || bundleText.includes("verification.log")) {
|
|
524
|
+
throw new Error("run_direct_verification_bundle leaked log tail fields");
|
|
525
|
+
}
|
|
500
526
|
|
|
501
527
|
const enabledHealth = JSON.parse(
|
|
502
528
|
(await enabledClient.callTool({ name: "health_check", arguments: {} })).content?.[0]?.text || "{}"
|
|
@@ -504,12 +530,12 @@ try {
|
|
|
504
530
|
if (enabledHealth.direct_profile_enabled !== true) {
|
|
505
531
|
throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
|
|
506
532
|
}
|
|
507
|
-
if (enabledHealth.direct_tool_count !==
|
|
508
|
-
throw new Error(`direct_tool_count should be
|
|
533
|
+
if (enabledHealth.direct_tool_count !== 14) {
|
|
534
|
+
throw new Error(`direct_tool_count should be 14, got ${enabledHealth.direct_tool_count}`);
|
|
509
535
|
}
|
|
510
536
|
|
|
511
537
|
await enabledClient.close();
|
|
512
|
-
ok("chatgpt_direct enabled exposes
|
|
538
|
+
ok("chatgpt_direct enabled exposes 14 tools and create_direct_session works");
|
|
513
539
|
} catch (error) {
|
|
514
540
|
fail("MCP smoke test", error);
|
|
515
541
|
} finally {
|
|
@@ -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()
|