patchwarden 0.6.4 → 1.1.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.
- package/README.en.md +18 -18
- package/README.md +20 -20
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +15 -2
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/package.json +3 -2
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +53 -6
- package/scripts/checks/mcp-smoke.js +36 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +17 -3
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
package/dist/runner/runTask.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js
|
|
|
11
11
|
import { buildChangeArtifacts, captureRepoSnapshot, compareSnapshots, emptyArtifactHygiene, writeSnapshot, extractExternalDirtyFiles, findNewExternalDirtyFiles, buildArtifactManifest, groupChangedFiles, } from "./changeCapture.js";
|
|
12
12
|
import { errorPayload } from "../errors.js";
|
|
13
13
|
import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
|
|
14
|
+
import { runPostTaskCleanup } from "./postTaskCleanup.js";
|
|
14
15
|
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
15
16
|
const GRACEFUL_KILL_MS = 2000;
|
|
16
17
|
const MAX_CAPTURE_CHARS = 100_000;
|
|
@@ -81,6 +82,14 @@ export async function runTask(taskId) {
|
|
|
81
82
|
current_command: null,
|
|
82
83
|
error: null,
|
|
83
84
|
});
|
|
85
|
+
// v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
|
|
86
|
+
// can detect PID reuse and orphaned tasks. watcher_instance_id comes from
|
|
87
|
+
// the watcher process env; when run_task MCP tool is used directly, this
|
|
88
|
+
// field is intentionally left undefined so ownership cannot be falsely claimed.
|
|
89
|
+
writeTaskRuntime(taskDir, {
|
|
90
|
+
task_started_at: new Date(startedAtMs).toISOString(),
|
|
91
|
+
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
|
|
92
|
+
});
|
|
84
93
|
setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
|
|
85
94
|
let beforeSnapshot;
|
|
86
95
|
let beforeWorkspaceSnapshot;
|
|
@@ -166,7 +175,7 @@ export async function runTask(taskId) {
|
|
|
166
175
|
: `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
|
|
167
176
|
}
|
|
168
177
|
else if (verifyResults.length === verifyCommands.length) {
|
|
169
|
-
finalStatus = "
|
|
178
|
+
finalStatus = "done_by_agent";
|
|
170
179
|
}
|
|
171
180
|
else {
|
|
172
181
|
finalError = "Verification did not complete all configured commands.";
|
|
@@ -174,13 +183,35 @@ export async function runTask(taskId) {
|
|
|
174
183
|
}
|
|
175
184
|
else {
|
|
176
185
|
testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
|
|
177
|
-
finalStatus = "
|
|
186
|
+
finalStatus = "done_by_agent";
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
catch (error) {
|
|
181
190
|
lastCaughtError = error;
|
|
182
191
|
finalError = errorMessage(error);
|
|
183
192
|
}
|
|
193
|
+
let cleanupReport = {
|
|
194
|
+
enabled: true,
|
|
195
|
+
removed: [],
|
|
196
|
+
skipped: [],
|
|
197
|
+
source_files_touched: 0,
|
|
198
|
+
};
|
|
199
|
+
if (finalStatus !== "canceled") {
|
|
200
|
+
try {
|
|
201
|
+
cleanupReport = runPostTaskCleanup(repoPath, taskDir);
|
|
202
|
+
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
cleanupReport = {
|
|
206
|
+
enabled: true,
|
|
207
|
+
removed: [],
|
|
208
|
+
skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
|
|
209
|
+
source_files_touched: 0,
|
|
210
|
+
};
|
|
211
|
+
writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
212
|
+
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
184
215
|
setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
185
216
|
const artifactCollectionStartedAt = new Date().toISOString();
|
|
186
217
|
let changes;
|
|
@@ -252,7 +283,7 @@ export async function runTask(taskId) {
|
|
|
252
283
|
"",
|
|
253
284
|
].join("\n"), "utf-8");
|
|
254
285
|
// Don't override test failure with artifact failure
|
|
255
|
-
if (finalStatus === "
|
|
286
|
+
if (finalStatus === "done_by_agent") {
|
|
256
287
|
finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
257
288
|
finalStatus = "failed";
|
|
258
289
|
}
|
|
@@ -276,7 +307,7 @@ export async function runTask(taskId) {
|
|
|
276
307
|
}
|
|
277
308
|
catch (error) {
|
|
278
309
|
finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
279
|
-
if (finalStatus === "
|
|
310
|
+
if (finalStatus === "done_by_agent")
|
|
280
311
|
finalStatus = "failed";
|
|
281
312
|
}
|
|
282
313
|
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
@@ -338,9 +369,9 @@ export async function runTask(taskId) {
|
|
|
338
369
|
}
|
|
339
370
|
writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
340
371
|
writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
341
|
-
if (!["canceled", "
|
|
372
|
+
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus))
|
|
342
373
|
finalStatus = "failed";
|
|
343
|
-
const finalPhase = finalStatus === "
|
|
374
|
+
const finalPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
|
|
344
375
|
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
345
376
|
// Phase 7: Run Android build environment diagnostics if android_app exists
|
|
346
377
|
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
@@ -400,6 +431,7 @@ export async function runTask(taskId) {
|
|
|
400
431
|
artifact_collection_error: artifactCollectionError,
|
|
401
432
|
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
402
433
|
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
434
|
+
cleanup: cleanupReport,
|
|
403
435
|
artifact_manifest: artifactManifest,
|
|
404
436
|
out_of_scope_changes: outOfScopeChanges,
|
|
405
437
|
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
@@ -417,7 +449,7 @@ export async function runTask(taskId) {
|
|
|
417
449
|
commands_observed: [],
|
|
418
450
|
verify: verifyJson,
|
|
419
451
|
artifacts: [
|
|
420
|
-
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
|
|
452
|
+
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json", "post-task-cleanup.json",
|
|
421
453
|
...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
|
|
422
454
|
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
423
455
|
],
|
|
@@ -433,7 +465,7 @@ export async function runTask(taskId) {
|
|
|
433
465
|
failed_command: followup.failed_command,
|
|
434
466
|
suggested_next_action: followup.suggested_next_action,
|
|
435
467
|
safe_followup_prompt: followup.safe_followup_prompt,
|
|
436
|
-
next_steps: finalStatus === "
|
|
468
|
+
next_steps: finalStatus === "done_by_agent"
|
|
437
469
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
438
470
|
: ["Resolve the reported failure before accepting the work."],
|
|
439
471
|
}, null, 2), "utf-8");
|
|
@@ -459,6 +491,7 @@ export async function runTask(taskId) {
|
|
|
459
491
|
artifact_collection_error: artifactCollectionError,
|
|
460
492
|
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
461
493
|
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
494
|
+
cleanup: cleanupReport,
|
|
462
495
|
out_of_scope_changes: outOfScopeChanges,
|
|
463
496
|
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
464
497
|
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
@@ -469,6 +502,7 @@ export async function runTask(taskId) {
|
|
|
469
502
|
workspace_dirty_before: changes.workspace_dirty_before,
|
|
470
503
|
workspace_dirty_after: changes.workspace_dirty_after,
|
|
471
504
|
workspace_dirty: changes.workspace_dirty_after,
|
|
505
|
+
acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
|
|
472
506
|
});
|
|
473
507
|
writeTaskRuntime(taskDir, {
|
|
474
508
|
phase: finalPhase,
|
|
@@ -485,7 +519,7 @@ export async function runTask(taskId) {
|
|
|
485
519
|
}
|
|
486
520
|
function buildFailureFollowup(status, error, commands) {
|
|
487
521
|
const failed = commands.find((command) => command.status !== "passed");
|
|
488
|
-
if (status === "done") {
|
|
522
|
+
if (status === "done_by_agent" || status === "done") {
|
|
489
523
|
return {
|
|
490
524
|
failure_reason: null,
|
|
491
525
|
failed_command: null,
|
|
@@ -548,6 +582,12 @@ async function runManagedProcess(options) {
|
|
|
548
582
|
catch (error) {
|
|
549
583
|
return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
|
|
550
584
|
}
|
|
585
|
+
// v0.7.0: record child_started_at immediately so diagnose_task can detect
|
|
586
|
+
// PID reuse by comparing this timestamp with the live process start time.
|
|
587
|
+
writeTaskRuntime(options.taskDir, {
|
|
588
|
+
child_pid: child.pid,
|
|
589
|
+
child_started_at: new Date().toISOString(),
|
|
590
|
+
});
|
|
551
591
|
const stdoutStream = openStream(options.stdoutPath);
|
|
552
592
|
const stderrStream = openStream(options.stderrPath);
|
|
553
593
|
let stdout = "";
|
|
@@ -795,7 +835,7 @@ function buildResultMarkdown(input) {
|
|
|
795
835
|
input.error || "Agent execution and configured verification completed successfully.",
|
|
796
836
|
"",
|
|
797
837
|
"## Risks",
|
|
798
|
-
input.status === "done"
|
|
838
|
+
input.status === "done_by_agent" || input.status === "done"
|
|
799
839
|
? "- Review git.diff and changed-files.json before accepting the task."
|
|
800
840
|
: "- Task did not complete successfully; outputs may be partial.",
|
|
801
841
|
"",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: discovery token store — server-side token store for invoke_discovered_tool.
|
|
3
|
+
*
|
|
4
|
+
* discover_tools 为每个搜索结果生成 discoveryToken,server-side 保存 token 真实信息。
|
|
5
|
+
* invoke_discovered_tool 只接受 token id(不信任客户端回传的 token JSON)。
|
|
6
|
+
*
|
|
7
|
+
* 安全契约:
|
|
8
|
+
* - 不引入第三方 npm 依赖(用 node:crypto 的 randomBytes)
|
|
9
|
+
* - 不持久化到磁盘(内存 Map,进程内共享,单例)
|
|
10
|
+
* - token 单次使用语义(consumeToken 后即删除)
|
|
11
|
+
*/
|
|
12
|
+
import type { ToolRisk } from "../tools/toolRegistry.js";
|
|
13
|
+
import type { ToolProfile } from "../tools/toolCatalog.js";
|
|
14
|
+
export interface DiscoveryTokenRecord {
|
|
15
|
+
token: string;
|
|
16
|
+
toolName: string;
|
|
17
|
+
risk: ToolRisk;
|
|
18
|
+
allowedScope?: string[];
|
|
19
|
+
issuedAt: string;
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
query: string;
|
|
22
|
+
schemaDigest: string;
|
|
23
|
+
profile: ToolProfile;
|
|
24
|
+
}
|
|
25
|
+
export interface IssueTokenInput {
|
|
26
|
+
toolName: string;
|
|
27
|
+
risk: ToolRisk;
|
|
28
|
+
query: string;
|
|
29
|
+
schemaDigest: string;
|
|
30
|
+
profile: ToolProfile;
|
|
31
|
+
allowedScope?: string[];
|
|
32
|
+
ttlMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 生成并保存 discovery token,返回 token id。
|
|
36
|
+
* token id 格式:dst_{YYYYMMDD}_{randomHex12}
|
|
37
|
+
*/
|
|
38
|
+
export declare function issueToken(input: IssueTokenInput): string;
|
|
39
|
+
/**
|
|
40
|
+
* 消费 token(单次使用语义)。
|
|
41
|
+
* - 不存在 → 抛 PatchWardenError("token_not_found")
|
|
42
|
+
* - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
|
|
43
|
+
* - 有效 → 从 store 删除并返回 record
|
|
44
|
+
*/
|
|
45
|
+
export declare function consumeToken(tokenId: string): DiscoveryTokenRecord;
|
|
46
|
+
/**
|
|
47
|
+
* 只读查看 token,不消费、不校验过期。
|
|
48
|
+
* 不存在返回 null,存在返回 record(不删除)。
|
|
49
|
+
*/
|
|
50
|
+
export declare function peekToken(tokenId: string): DiscoveryTokenRecord | null;
|
|
51
|
+
/**
|
|
52
|
+
* 撤销 token,返回是否删除成功。
|
|
53
|
+
*/
|
|
54
|
+
export declare function revokeToken(tokenId: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 清空所有 token(测试用)。
|
|
57
|
+
*/
|
|
58
|
+
export declare function clearAllTokens(): void;
|
|
59
|
+
/**
|
|
60
|
+
* 返回当前 store 中的 token 数量(测试/诊断用)。
|
|
61
|
+
*/
|
|
62
|
+
export declare function getActiveTokenCount(): number;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: discovery token store — server-side token store for invoke_discovered_tool.
|
|
3
|
+
*
|
|
4
|
+
* discover_tools 为每个搜索结果生成 discoveryToken,server-side 保存 token 真实信息。
|
|
5
|
+
* invoke_discovered_tool 只接受 token id(不信任客户端回传的 token JSON)。
|
|
6
|
+
*
|
|
7
|
+
* 安全契约:
|
|
8
|
+
* - 不引入第三方 npm 依赖(用 node:crypto 的 randomBytes)
|
|
9
|
+
* - 不持久化到磁盘(内存 Map,进程内共享,单例)
|
|
10
|
+
* - token 单次使用语义(consumeToken 后即删除)
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes } from "node:crypto";
|
|
13
|
+
import { PatchWardenError } from "../errors.js";
|
|
14
|
+
// ── 常量 ──────────────────────────────────────────────────────────
|
|
15
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
|
|
16
|
+
// ── 模块级 token store(单例,进程内共享) ───────────────────────
|
|
17
|
+
const tokenStore = new Map();
|
|
18
|
+
// ── 内部工具 ──────────────────────────────────────────────────────
|
|
19
|
+
function formatDatePart(date) {
|
|
20
|
+
const y = date.getFullYear();
|
|
21
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
22
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
23
|
+
return `${y}${m}${d}`;
|
|
24
|
+
}
|
|
25
|
+
function generateTokenId() {
|
|
26
|
+
const datePart = formatDatePart(new Date());
|
|
27
|
+
const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
|
|
28
|
+
return `dst_${datePart}_${randomPart}`;
|
|
29
|
+
}
|
|
30
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* 生成并保存 discovery token,返回 token id。
|
|
33
|
+
* token id 格式:dst_{YYYYMMDD}_{randomHex12}
|
|
34
|
+
*/
|
|
35
|
+
export function issueToken(input) {
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const issuedAt = now.toISOString();
|
|
38
|
+
const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
|
|
39
|
+
const token = generateTokenId();
|
|
40
|
+
const record = {
|
|
41
|
+
token,
|
|
42
|
+
toolName: input.toolName,
|
|
43
|
+
risk: input.risk,
|
|
44
|
+
allowedScope: input.allowedScope,
|
|
45
|
+
issuedAt,
|
|
46
|
+
expiresAt,
|
|
47
|
+
query: input.query,
|
|
48
|
+
schemaDigest: input.schemaDigest,
|
|
49
|
+
profile: input.profile,
|
|
50
|
+
};
|
|
51
|
+
tokenStore.set(token, record);
|
|
52
|
+
return token;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 消费 token(单次使用语义)。
|
|
56
|
+
* - 不存在 → 抛 PatchWardenError("token_not_found")
|
|
57
|
+
* - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
|
|
58
|
+
* - 有效 → 从 store 删除并返回 record
|
|
59
|
+
*/
|
|
60
|
+
export function consumeToken(tokenId) {
|
|
61
|
+
const record = tokenStore.get(tokenId);
|
|
62
|
+
if (!record) {
|
|
63
|
+
throw new PatchWardenError("token_not_found", `Discovery token not found: ${tokenId}`, "Call discover_tools to obtain a fresh discovery token before invoking invoke_discovered_tool.", true, { token: tokenId });
|
|
64
|
+
}
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const expiresAtMs = Date.parse(record.expiresAt);
|
|
67
|
+
if (expiresAtMs < now) {
|
|
68
|
+
// 过期 token 从 store 删除,避免累积
|
|
69
|
+
tokenStore.delete(tokenId);
|
|
70
|
+
throw new PatchWardenError("token_expired", `Discovery token expired: ${tokenId}`, "Call discover_tools again to obtain a fresh discovery token.", true, { token: tokenId, expired_at: record.expiresAt });
|
|
71
|
+
}
|
|
72
|
+
tokenStore.delete(tokenId);
|
|
73
|
+
return record;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 只读查看 token,不消费、不校验过期。
|
|
77
|
+
* 不存在返回 null,存在返回 record(不删除)。
|
|
78
|
+
*/
|
|
79
|
+
export function peekToken(tokenId) {
|
|
80
|
+
const record = tokenStore.get(tokenId);
|
|
81
|
+
return record ?? null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 撤销 token,返回是否删除成功。
|
|
85
|
+
*/
|
|
86
|
+
export function revokeToken(tokenId) {
|
|
87
|
+
return tokenStore.delete(tokenId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 清空所有 token(测试用)。
|
|
91
|
+
*/
|
|
92
|
+
export function clearAllTokens() {
|
|
93
|
+
tokenStore.clear();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 返回当前 store 中的 token 数量(测试/诊断用)。
|
|
97
|
+
*/
|
|
98
|
+
export function getActiveTokenCount() {
|
|
99
|
+
return tokenStore.size;
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: toolInvocationGuard — invoke_discovered_tool 调用前强制校验。
|
|
3
|
+
*
|
|
4
|
+
* consumeToken 已完成 token 存在性与过期校验,guard 收到的是已消费的 record。
|
|
5
|
+
* guard 依次执行 8 项校验,任一失败抛出对应错误码:
|
|
6
|
+
* 1. token_tool_mismatch — toolName 与 record.toolName 一致性
|
|
7
|
+
* 2. profile_not_allowed — toolMeta.profiles 必须包含当前 profile
|
|
8
|
+
* 3. risk_exceeded — toolMeta.risk 不能超过 record.risk(schema 漂移拦截)
|
|
9
|
+
* 4. sensitive_path_blocked — workspace_read_sensitive 路径敏感校验
|
|
10
|
+
* 5. assessment_required — workspace_write 需要 assessmentId
|
|
11
|
+
* 6. command_not_allowed — command 工具 args.command 元字符预检
|
|
12
|
+
* 7. release_confirmation_required — release 工具需要 assessmentId
|
|
13
|
+
* 8. credential_sensitive_blocked — credential_sensitive 总是拒绝
|
|
14
|
+
*
|
|
15
|
+
* 加上 consumeToken 内的 token_not_found / token_expired,共 10 项调用前强制校验。
|
|
16
|
+
*/
|
|
17
|
+
import { type PatchWardenToolMeta } from "../tools/toolRegistry.js";
|
|
18
|
+
import type { ToolProfile } from "../tools/toolCatalog.js";
|
|
19
|
+
import type { DiscoveryTokenRecord } from "./discoveryTokenStore.js";
|
|
20
|
+
export interface GuardInput {
|
|
21
|
+
toolName: string;
|
|
22
|
+
toolMeta: PatchWardenToolMeta;
|
|
23
|
+
args: Record<string, unknown>;
|
|
24
|
+
discoveryTokenRecord: DiscoveryTokenRecord;
|
|
25
|
+
profile: ToolProfile;
|
|
26
|
+
assessmentId?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GuardResult {
|
|
29
|
+
allowed: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type InvocationErrorCode = "token_tool_mismatch" | "profile_not_allowed" | "risk_exceeded" | "sensitive_path_blocked" | "assessment_required" | "command_not_allowed" | "release_confirmation_required" | "credential_sensitive_blocked";
|
|
32
|
+
/**
|
|
33
|
+
* 调用前强制校验。任一校验失败抛 PatchWardenError,全部通过返回 { allowed: true }。
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkInvocation(input: GuardInput): GuardResult;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: toolInvocationGuard — invoke_discovered_tool 调用前强制校验。
|
|
3
|
+
*
|
|
4
|
+
* consumeToken 已完成 token 存在性与过期校验,guard 收到的是已消费的 record。
|
|
5
|
+
* guard 依次执行 8 项校验,任一失败抛出对应错误码:
|
|
6
|
+
* 1. token_tool_mismatch — toolName 与 record.toolName 一致性
|
|
7
|
+
* 2. profile_not_allowed — toolMeta.profiles 必须包含当前 profile
|
|
8
|
+
* 3. risk_exceeded — toolMeta.risk 不能超过 record.risk(schema 漂移拦截)
|
|
9
|
+
* 4. sensitive_path_blocked — workspace_read_sensitive 路径敏感校验
|
|
10
|
+
* 5. assessment_required — workspace_write 需要 assessmentId
|
|
11
|
+
* 6. command_not_allowed — command 工具 args.command 元字符预检
|
|
12
|
+
* 7. release_confirmation_required — release 工具需要 assessmentId
|
|
13
|
+
* 8. credential_sensitive_blocked — credential_sensitive 总是拒绝
|
|
14
|
+
*
|
|
15
|
+
* 加上 consumeToken 内的 token_not_found / token_expired,共 10 项调用前强制校验。
|
|
16
|
+
*/
|
|
17
|
+
import { PatchWardenError } from "../errors.js";
|
|
18
|
+
import { guardSensitivePath } from "./sensitiveGuard.js";
|
|
19
|
+
import { TOOL_RISK_RANK } from "../tools/toolRegistry.js";
|
|
20
|
+
// ── 内部常量 ──────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* 常见路径参数字段名,用于 workspace_read_sensitive 校验时扫描 args。
|
|
23
|
+
* 任一字段为字符串路径都会触发 guardSensitivePath。
|
|
24
|
+
*/
|
|
25
|
+
const PATH_ARG_FIELDS = [
|
|
26
|
+
"path",
|
|
27
|
+
"file",
|
|
28
|
+
"filePath",
|
|
29
|
+
"file_path",
|
|
30
|
+
"filename",
|
|
31
|
+
"repo_path",
|
|
32
|
+
"repoPath",
|
|
33
|
+
"target_path",
|
|
34
|
+
"targetPath",
|
|
35
|
+
"destination",
|
|
36
|
+
"dest",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* shell 元字符黑名单,用于 command 风险工具 args.command 预检。
|
|
40
|
+
* 真正的白名单校验在 handler 内通过 commandGuard 完成,guard 只做元字符预检。
|
|
41
|
+
*/
|
|
42
|
+
const SHELL_METACHAR_PATTERN = /[|&;`$()<>]/;
|
|
43
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* 调用前强制校验。任一校验失败抛 PatchWardenError,全部通过返回 { allowed: true }。
|
|
46
|
+
*/
|
|
47
|
+
export function checkInvocation(input) {
|
|
48
|
+
const { toolName, toolMeta, args, discoveryTokenRecord, profile, assessmentId } = input;
|
|
49
|
+
// ① toolName 一致性:toolName 必须等于 record.toolName
|
|
50
|
+
if (toolName !== discoveryTokenRecord.toolName) {
|
|
51
|
+
throw new PatchWardenError("token_tool_mismatch", `Token tool mismatch: token was issued for "${discoveryTokenRecord.toolName}" but invocation targets "${toolName}".`, "Call discover_tools again and use a token issued for the intended tool name.", true, {
|
|
52
|
+
token: discoveryTokenRecord.token,
|
|
53
|
+
expected_tool: discoveryTokenRecord.toolName,
|
|
54
|
+
actual_tool: toolName,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// ② profile 允许:toolMeta.profiles 必须包含当前 profile
|
|
58
|
+
if (!toolMeta.profiles.includes(profile)) {
|
|
59
|
+
throw new PatchWardenError("profile_not_allowed", `Profile "${profile}" is not allowed for tool "${toolName}". Allowed profiles: ${toolMeta.profiles.join(", ")}.`, "Switch to a profile that exposes this tool, or call discover_tools under a supported profile.", true, {
|
|
60
|
+
tool: toolName,
|
|
61
|
+
current_profile: profile,
|
|
62
|
+
allowed_profiles: toolMeta.profiles,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ③ 风险等级校验:toolMeta.risk 不能超过 record.risk
|
|
66
|
+
// token record 的 risk 是 discover 时记录的工具 risk,应与 toolMeta.risk 一致。
|
|
67
|
+
// 如果工具 schema 漂移导致 risk 变了,这里会拦截。
|
|
68
|
+
const toolRiskRank = TOOL_RISK_RANK[toolMeta.risk];
|
|
69
|
+
const tokenRiskRank = TOOL_RISK_RANK[discoveryTokenRecord.risk];
|
|
70
|
+
if (toolRiskRank > tokenRiskRank) {
|
|
71
|
+
throw new PatchWardenError("risk_exceeded", `Risk exceeded: tool "${toolName}" risk is "${toolMeta.risk}" (rank ${toolRiskRank}) but discovery token was issued for risk "${discoveryTokenRecord.risk}" (rank ${tokenRiskRank}). Tool schema may have drifted.`, "Call discover_tools again to obtain a fresh token reflecting the current tool risk.", true, {
|
|
72
|
+
tool: toolName,
|
|
73
|
+
tool_risk: toolMeta.risk,
|
|
74
|
+
token_risk: discoveryTokenRecord.risk,
|
|
75
|
+
tool_risk_rank: toolRiskRank,
|
|
76
|
+
token_risk_rank: tokenRiskRank,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// ④ workspace_read_sensitive:检查 args 中的路径参数
|
|
80
|
+
checkSensitivePaths(toolMeta, toolName, args);
|
|
81
|
+
// ⑤ workspace_write:要求 assessmentId 非空
|
|
82
|
+
if (toolMeta.risk === "workspace_write") {
|
|
83
|
+
if (!assessmentId || assessmentId.trim() === "") {
|
|
84
|
+
throw new PatchWardenError("assessment_required", `Assessment required: tool "${toolName}" has workspace_write risk and requires a non-empty assessmentId.`, "Run the assessment flow first and pass the resulting assessmentId to invoke_discovered_tool.", true, {
|
|
85
|
+
tool: toolName,
|
|
86
|
+
risk: toolMeta.risk,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ⑥ command:args.command 元字符预检
|
|
91
|
+
if (toolMeta.risk === "command") {
|
|
92
|
+
checkCommandMetachars(toolMeta, toolName, args);
|
|
93
|
+
}
|
|
94
|
+
// ⑦ release:要求 assessmentId 非空(二次确认语义)
|
|
95
|
+
if (toolMeta.risk === "release") {
|
|
96
|
+
if (!assessmentId || assessmentId.trim() === "") {
|
|
97
|
+
throw new PatchWardenError("release_confirmation_required", `Release confirmation required: tool "${toolName}" has release risk and requires a non-empty assessmentId as explicit confirmation.`, "Run the release assessment flow first and pass the resulting assessmentId to confirm the release.", true, {
|
|
98
|
+
tool: toolName,
|
|
99
|
+
risk: toolMeta.risk,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ⑧ credential_sensitive:总是拒绝
|
|
104
|
+
if (toolMeta.risk === "credential_sensitive") {
|
|
105
|
+
throw new PatchWardenError("credential_sensitive_blocked", `Credential-sensitive tool "${toolName}" cannot be invoked through invoke_discovered_tool.`, "Use the dedicated credential-handling tooling outside the dynamic invoke path.", true, {
|
|
106
|
+
tool: toolName,
|
|
107
|
+
risk: toolMeta.risk,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { allowed: true };
|
|
111
|
+
}
|
|
112
|
+
// ── 内部校验函数 ──────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* workspace_read_sensitive 风险工具:扫描 args 中的路径参数字段,
|
|
115
|
+
* 对每个字符串路径调用 guardSensitivePath(敏感路径会抛 sensitive_path_blocked)。
|
|
116
|
+
* 没有路径参数则跳过。
|
|
117
|
+
*/
|
|
118
|
+
function checkSensitivePaths(toolMeta, toolName, args) {
|
|
119
|
+
if (toolMeta.risk !== "workspace_read_sensitive") {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (const field of PATH_ARG_FIELDS) {
|
|
123
|
+
const value = args[field];
|
|
124
|
+
if (typeof value === "string" && value.length > 0) {
|
|
125
|
+
// guardSensitivePath 内部会在敏感时抛 PatchWardenError("sensitive_path_blocked", ...)
|
|
126
|
+
guardSensitivePath(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* command 风险工具:args.command 元字符预检。
|
|
132
|
+
* - args.command 不存在或为空 → 跳过(handler 内部会再校验)
|
|
133
|
+
* - 含 shell 元字符 → 抛 command_not_allowed
|
|
134
|
+
* 真正的白名单校验在 handler 内通过 commandGuard 完成。
|
|
135
|
+
*/
|
|
136
|
+
function checkCommandMetachars(_toolMeta, toolName, args) {
|
|
137
|
+
const command = args.command;
|
|
138
|
+
if (command === undefined || command === null) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (typeof command !== "string") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (command.trim() === "") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (SHELL_METACHAR_PATTERN.test(command)) {
|
|
148
|
+
throw new PatchWardenError("command_not_allowed", `Command not allowed: tool "${toolName}" args.command contains shell metacharacters. Detected pattern in "${command}".`, "Provide a plain command without shell metacharacters (| & ; ` $ ( ) < >). The handler will validate against the command allowlist.", true, {
|
|
149
|
+
tool: toolName,
|
|
150
|
+
command,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/smoke-test.js
CHANGED
|
@@ -17,7 +17,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
import { spawnSync } from "node:child_process";
|
|
20
|
-
import { loadConfig, getConfig, reloadConfig } from "./config.js";
|
|
20
|
+
import { loadConfig, getConfig, getTasksDir, reloadConfig } from "./config.js";
|
|
21
21
|
import { savePlan } from "./tools/savePlan.js";
|
|
22
22
|
import { getPlan } from "./tools/getPlan.js";
|
|
23
23
|
import { createTask } from "./tools/createTask.js";
|
|
@@ -533,8 +533,8 @@ test("D8b. tool profiles are exact and schema changes alter the manifest hash",
|
|
|
533
533
|
try {
|
|
534
534
|
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
535
535
|
const fullTools = getToolDefs();
|
|
536
|
-
if (fullTools.length !==
|
|
537
|
-
throw new Error(`Expected
|
|
536
|
+
if (fullTools.length !== 54)
|
|
537
|
+
throw new Error(`Expected 54 full tools, got ${fullTools.length}`);
|
|
538
538
|
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", getConfig().enableDirectProfile);
|
|
539
539
|
const names = coreTools.map((tool) => tool.name);
|
|
540
540
|
if (JSON.stringify(names) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES)) {
|
|
@@ -926,6 +926,11 @@ test("I1. list_tasks returns tasks array", () => {
|
|
|
926
926
|
mgmtPlanId = savePlan({ title: "Mgmt Test", content: "# Test" }).plan_id;
|
|
927
927
|
mgmtTaskId = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
|
|
928
928
|
mgmtTaskId2 = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." }).task_id;
|
|
929
|
+
const collectingStatusFile = join(getTasksDir(getConfig()), mgmtTaskId2, "status.json");
|
|
930
|
+
const collectingStatus = JSON.parse(readFileSync(collectingStatusFile, "utf-8"));
|
|
931
|
+
collectingStatus.status = "collecting_artifacts";
|
|
932
|
+
collectingStatus.phase = "collecting_artifacts";
|
|
933
|
+
writeFileSync(collectingStatusFile, JSON.stringify(collectingStatus, null, 2), "utf-8");
|
|
929
934
|
const result = listTasks({ limit: 5 });
|
|
930
935
|
if (!Array.isArray(result.tasks))
|
|
931
936
|
throw new Error("tasks not array");
|
|
@@ -943,9 +948,12 @@ test("I2b. list_tasks filters by repo and active status with watcher evidence",
|
|
|
943
948
|
if (result.returned !== result.tasks.length || !result.watcher?.status) {
|
|
944
949
|
throw new Error(`Missing list_tasks pagination or watcher evidence: ${JSON.stringify(result)}`);
|
|
945
950
|
}
|
|
946
|
-
if (result.tasks.some((task) => !["pending", "running"].includes(task.status) || task.repo_path !== ".")) {
|
|
951
|
+
if (result.tasks.some((task) => !["pending", "running", "collecting_artifacts"].includes(task.status) || task.repo_path !== ".")) {
|
|
947
952
|
throw new Error(`list_tasks active/repo filter mismatch: ${JSON.stringify(result.tasks)}`);
|
|
948
953
|
}
|
|
954
|
+
if (!result.tasks.some((task) => task.task_id === mgmtTaskId2 && task.status === "collecting_artifacts")) {
|
|
955
|
+
throw new Error(`list_tasks active_only omitted collecting_artifacts task: ${JSON.stringify(result.tasks)}`);
|
|
956
|
+
}
|
|
949
957
|
});
|
|
950
958
|
test("I3. cancel_task cancels pending task", () => {
|
|
951
959
|
const task = createTask({ plan_id: mgmtPlanId, agent: "codex", repo_path: "." });
|
|
@@ -1866,11 +1874,11 @@ writeFileSync(directConfigPath, JSON.stringify({
|
|
|
1866
1874
|
process.env.PATCHWARDEN_CONFIG = directConfigPath;
|
|
1867
1875
|
reloadConfig();
|
|
1868
1876
|
let directSessionId = "";
|
|
1869
|
-
test("M1. chatgpt_core still has
|
|
1877
|
+
test("M1. chatgpt_core still has 21 tools", () => {
|
|
1870
1878
|
const tools = getToolDefs();
|
|
1871
1879
|
const coreTools = selectToolsForProfile(tools, "chatgpt_core", true);
|
|
1872
|
-
if (coreTools.length !==
|
|
1873
|
-
throw new Error(`Expected
|
|
1880
|
+
if (coreTools.length !== 21)
|
|
1881
|
+
throw new Error(`Expected 21, got ${coreTools.length}`);
|
|
1874
1882
|
if (JSON.stringify(coreTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_CORE_TOOL_NAMES)) {
|
|
1875
1883
|
throw new Error("Tool names mismatch");
|
|
1876
1884
|
}
|
|
@@ -1883,11 +1891,11 @@ test("M2. chatgpt_direct disabled exposes only health_check", () => {
|
|
|
1883
1891
|
if (disabledTools[0].name !== "health_check")
|
|
1884
1892
|
throw new Error(`Expected health_check, got ${disabledTools[0].name}`);
|
|
1885
1893
|
});
|
|
1886
|
-
test("M3. chatgpt_direct enabled has
|
|
1894
|
+
test("M3. chatgpt_direct enabled has 13 tools", () => {
|
|
1887
1895
|
const tools = getToolDefs();
|
|
1888
1896
|
const directTools = selectToolsForProfile(tools, "chatgpt_direct", true);
|
|
1889
|
-
if (directTools.length !==
|
|
1890
|
-
throw new Error(`Expected
|
|
1897
|
+
if (directTools.length !== 13)
|
|
1898
|
+
throw new Error(`Expected 13, got ${directTools.length}`);
|
|
1891
1899
|
if (JSON.stringify(directTools.map((t) => t.name)) !== JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES)) {
|
|
1892
1900
|
throw new Error("Tool names mismatch");
|
|
1893
1901
|
}
|
package/dist/taskRuntime.d.ts
CHANGED
|
@@ -5,6 +5,23 @@ export interface TaskRuntimeData {
|
|
|
5
5
|
current_command: string | null;
|
|
6
6
|
runner_pid?: number;
|
|
7
7
|
child_pid?: number;
|
|
8
|
+
/**
|
|
9
|
+
* v0.7.0: ISO timestamp when the child process was spawned.
|
|
10
|
+
* Used by diagnose_task to detect PID reuse: if a live PID exists but
|
|
11
|
+
* its start time does not match child_started_at, the OS likely reused the PID.
|
|
12
|
+
*/
|
|
13
|
+
child_started_at?: string;
|
|
14
|
+
/**
|
|
15
|
+
* v0.7.0: ISO timestamp when the task itself started (runner began executing).
|
|
16
|
+
* Distinct from child_started_at, which records when the agent child process spawned.
|
|
17
|
+
*/
|
|
18
|
+
task_started_at?: string;
|
|
19
|
+
/**
|
|
20
|
+
* v0.7.0: Watcher instance ID that picked up and is executing this task.
|
|
21
|
+
* Used by diagnose_task to detect orphaned tasks when the current watcher
|
|
22
|
+
* instance_id differs from the one recorded at task start.
|
|
23
|
+
*/
|
|
24
|
+
watcher_instance_id?: string;
|
|
8
25
|
}
|
|
9
26
|
export declare function readTaskRuntime(taskDir: string): Partial<TaskRuntimeData>;
|
|
10
27
|
export declare function writeTaskRuntime(taskDir: string, patch: Partial<TaskRuntimeData>): TaskRuntimeData;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|