patchwarden 1.1.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.en.md +41 -7
  2. package/README.md +36 -9
  3. package/dist/controlCenter.js +197 -1
  4. package/dist/direct/directSessionStore.d.ts +2 -0
  5. package/dist/direct/directVerification.js +7 -0
  6. package/dist/doctor.js +1 -1
  7. package/dist/policy/projectPolicy.d.ts +55 -0
  8. package/dist/policy/projectPolicy.js +286 -0
  9. package/dist/smoke-test.js +8 -8
  10. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  11. package/dist/test/unit/evidence-pack.test.js +130 -0
  12. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  13. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  14. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  15. package/dist/test/unit/run-task-loop.test.js +380 -0
  16. package/dist/test/unit/schema-drift-check.test.js +10 -9
  17. package/dist/tools/evidencePack.d.ts +39 -0
  18. package/dist/tools/evidencePack.js +168 -0
  19. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  20. package/dist/tools/recommendAgentForTask.js +56 -0
  21. package/dist/tools/registry.js +376 -2
  22. package/dist/tools/releaseMode.d.ts +50 -0
  23. package/dist/tools/releaseMode.js +370 -0
  24. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  25. package/dist/tools/runDirectVerificationBundle.js +64 -0
  26. package/dist/tools/runTaskLoop.d.ts +57 -0
  27. package/dist/tools/runTaskLoop.js +417 -0
  28. package/dist/tools/runVerification.d.ts +4 -0
  29. package/dist/tools/runVerification.js +4 -0
  30. package/dist/tools/safeViews.d.ts +6 -0
  31. package/dist/tools/safeViews.js +2 -0
  32. package/dist/tools/taskLineage.d.ts +91 -0
  33. package/dist/tools/taskLineage.js +175 -0
  34. package/dist/tools/toolCatalog.d.ts +2 -2
  35. package/dist/tools/toolCatalog.js +6 -0
  36. package/dist/tools/toolRegistry.js +110 -0
  37. package/dist/version.d.ts +2 -2
  38. package/dist/version.js +2 -2
  39. package/docs/chatgpt-usage.md +31 -0
  40. package/docs/control-center/README.md +9 -0
  41. package/package.json +2 -2
  42. package/scripts/checks/control-center-smoke.js +87 -0
  43. package/scripts/checks/control-smoke.js +2 -2
  44. package/scripts/checks/mcp-manifest-check.js +12 -0
  45. package/scripts/checks/mcp-smoke.js +31 -7
  46. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  47. package/src/controlCenter.ts +198 -1
  48. package/src/direct/directSessionStore.ts +2 -0
  49. package/src/direct/directVerification.ts +7 -0
  50. package/src/doctor.ts +1 -1
  51. package/src/policy/projectPolicy.ts +344 -0
  52. package/src/smoke-test.ts +5 -5
  53. package/src/test/unit/evidence-pack.test.ts +142 -0
  54. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  55. package/src/test/unit/run-task-loop.test.ts +425 -0
  56. package/src/test/unit/schema-drift-check.test.ts +11 -9
  57. package/src/tools/evidencePack.ts +205 -0
  58. package/src/tools/listWorkspace.ts +71 -71
  59. package/src/tools/recommendAgentForTask.ts +79 -0
  60. package/src/tools/registry.ts +405 -2
  61. package/src/tools/releaseMode.ts +450 -0
  62. package/src/tools/runDirectVerificationBundle.ts +98 -0
  63. package/src/tools/runTaskLoop.ts +526 -0
  64. package/src/tools/runVerification.ts +8 -0
  65. package/src/tools/safeViews.ts +2 -0
  66. package/src/tools/taskLineage.ts +300 -0
  67. package/src/tools/toolCatalog.ts +6 -0
  68. package/src/tools/toolRegistry.ts +110 -0
  69. package/src/version.ts +2 -2
  70. package/ui/pages/dashboard.html +143 -2
@@ -0,0 +1,175 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { getConfig } from "../config.js";
5
+ import { guardReadPath } from "../security/pathGuard.js";
6
+ import { redactSensitiveValue } from "../security/contentRedaction.js";
7
+ import { PatchWardenError } from "../errors.js";
8
+ export function createLineageId(now = new Date()) {
9
+ const stamp = now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
10
+ return `lineage_${stamp}_${randomBytes(4).toString("hex")}`;
11
+ }
12
+ export function writeTaskLineage(record) {
13
+ const config = getConfig();
14
+ const lineageDir = resolve(config.workspaceRoot, ".patchwarden", "lineages", record.lineage_id);
15
+ mkdirSync(lineageDir, { recursive: true });
16
+ const safeRecord = redactSensitiveValue(record).value;
17
+ writeFileSync(join(lineageDir, "lineage.json"), JSON.stringify(safeRecord, null, 2) + "\n", "utf-8");
18
+ writeFileSync(join(lineageDir, "SUMMARY.md"), buildSummaryMarkdown(safeRecord), "utf-8");
19
+ return toSafeTaskLineage(safeRecord);
20
+ }
21
+ export function getTaskLineage(lineageId, options = {}) {
22
+ const maxItems = normalizeMaxItems(options.max_items);
23
+ if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
24
+ throw new PatchWardenError("invalid_lineage_id", "lineage_id may contain only letters, numbers, underscores, and hyphens.", "Pass a lineage_id returned by run_task_loop.", true, { lineage_id: lineageId });
25
+ }
26
+ const config = getConfig();
27
+ const lineageFile = resolve(config.workspaceRoot, ".patchwarden", "lineages", lineageId, "lineage.json");
28
+ guardReadPath(lineageFile, config.workspaceRoot, ".patchwarden/lineages");
29
+ if (!existsSync(lineageFile)) {
30
+ throw new PatchWardenError("lineage_not_found", `Task lineage not found: "${lineageId}".`, "Pass a lineage_id returned by run_task_loop.", true, { lineage_id: lineageId });
31
+ }
32
+ const raw = readFileSync(lineageFile, "utf-8").replace(/^\uFEFF/, "");
33
+ const record = JSON.parse(raw);
34
+ return toSafeTaskLineage(redactSensitiveValue(record).value, maxItems);
35
+ }
36
+ export function toSafeTaskLineage(record, maxItems = 8) {
37
+ const rounds = record.rounds.slice(0, maxItems);
38
+ const latest = record.rounds[record.rounds.length - 1];
39
+ const directSessions = normalizeDirectSessions(record.direct_sessions);
40
+ return {
41
+ lineage_id: record.lineage_id,
42
+ goal: record.goal,
43
+ repo_path: record.repo_path,
44
+ created_at: record.created_at,
45
+ updated_at: record.updated_at,
46
+ final_status: record.final_status,
47
+ stop_reason: record.stop_reason,
48
+ next_action: record.next_action,
49
+ tasks: {
50
+ main: record.main_task,
51
+ fix: record.fix_tasks.slice(0, maxItems),
52
+ cleanup: record.cleanup_tasks.slice(0, maxItems),
53
+ direct_sessions: directSessions.slice(0, maxItems),
54
+ },
55
+ worktree: normalizeWorktree(record.worktree),
56
+ agent_routing: record.agent_routing ? {
57
+ requested_agent: record.agent_routing.requested_agent,
58
+ selected_agent: truncate(String(record.agent_routing.selected_agent), 120),
59
+ reason: truncate(String(record.agent_routing.reason), 240),
60
+ fallback: Boolean(record.agent_routing.fallback),
61
+ } : null,
62
+ verification: {
63
+ latest_status: latest?.verification_status || "not_available",
64
+ passed: latest?.verification_status === "passed",
65
+ },
66
+ rounds,
67
+ warnings: record.warnings.slice(0, maxItems).map((value) => truncate(value, 240)),
68
+ errors: record.errors.slice(0, maxItems).map((value) => truncate(value, 240)),
69
+ truncated: record.rounds.length > maxItems ||
70
+ record.fix_tasks.length > maxItems ||
71
+ record.cleanup_tasks.length > maxItems ||
72
+ directSessions.length > maxItems ||
73
+ record.warnings.length > maxItems ||
74
+ record.errors.length > maxItems,
75
+ };
76
+ }
77
+ function buildSummaryMarkdown(record) {
78
+ const rounds = record.rounds.map((round) => `- ${round.role} ${round.task_id}: ${round.status}, verification=${round.verification_status}, audit=${round.audit_verdict}`);
79
+ return [
80
+ "# PatchWarden Task Lineage",
81
+ "",
82
+ `- Lineage: ${record.lineage_id}`,
83
+ `- Goal: ${record.goal}`,
84
+ `- Repo: ${record.repo_path}`,
85
+ `- Final status: ${record.final_status}`,
86
+ `- Stop reason: ${record.stop_reason}`,
87
+ `- Next action: ${record.next_action}`,
88
+ `- Isolation: ${normalizeWorktree(record.worktree).isolation_mode}`,
89
+ `- Worktree: ${formatWorktree(record.worktree)}`,
90
+ `- Agent routing: ${formatAgentRouting(record.agent_routing)}`,
91
+ "",
92
+ "## Tasks",
93
+ `- Main: ${record.main_task || "none"}`,
94
+ `- Fix tasks: ${record.fix_tasks.length > 0 ? record.fix_tasks.join(", ") : "none"}`,
95
+ `- Cleanup tasks: ${record.cleanup_tasks.length > 0 ? record.cleanup_tasks.join(", ") : "none"}`,
96
+ `- Direct sessions: ${formatDirectSessions(record.direct_sessions)}`,
97
+ "",
98
+ "## Rounds",
99
+ ...(rounds.length > 0 ? rounds : ["- None."]),
100
+ "",
101
+ ].join("\n");
102
+ }
103
+ function normalizeWorktree(value) {
104
+ if (!value) {
105
+ return {
106
+ isolation_mode: "current_repo",
107
+ cleanup: "keep",
108
+ status: "not_used",
109
+ next_action: "none",
110
+ };
111
+ }
112
+ return {
113
+ isolation_mode: value.isolation_mode === "worktree" ? "worktree" : "current_repo",
114
+ worktree_id: value.worktree_id ? truncate(String(value.worktree_id), 120) : undefined,
115
+ worktree_path: value.worktree_path ? truncate(String(value.worktree_path), 260) : undefined,
116
+ branch: value.branch ? truncate(String(value.branch), 160) : undefined,
117
+ requested_base_branch: value.requested_base_branch ? truncate(String(value.requested_base_branch), 160) : undefined,
118
+ cleanup: value.cleanup,
119
+ status: value.status,
120
+ next_action: truncate(String(value.next_action || "review_worktree"), 240),
121
+ };
122
+ }
123
+ function formatWorktree(value) {
124
+ const worktree = normalizeWorktree(value);
125
+ if (worktree.isolation_mode !== "worktree")
126
+ return "not used";
127
+ const id = worktree.worktree_id || "unknown";
128
+ const status = worktree.status || "unknown";
129
+ const branch = worktree.branch ? ` branch=${worktree.branch}` : "";
130
+ return `${id} status=${status}${branch}`;
131
+ }
132
+ function formatAgentRouting(value) {
133
+ if (!value)
134
+ return "not recorded";
135
+ const requested = value.requested_agent ? ` requested=${value.requested_agent}` : "";
136
+ return `${value.selected_agent}${requested} reason=${truncate(value.reason, 160)}`;
137
+ }
138
+ function normalizeDirectSessions(value) {
139
+ return value.map((entry) => {
140
+ if (typeof entry === "string")
141
+ return { session_id: entry };
142
+ return {
143
+ session_id: String(entry.session_id || ""),
144
+ status: entry.status,
145
+ command_count: entry.command_count,
146
+ passed_commands: entry.passed_commands,
147
+ failed_commands: entry.failed_commands,
148
+ timed_out_commands: entry.timed_out_commands,
149
+ audit_decision: entry.audit_decision,
150
+ changed_files_total: entry.changed_files_total,
151
+ next_action: entry.next_action ? truncate(String(entry.next_action), 240) : undefined,
152
+ };
153
+ }).filter((entry) => entry.session_id !== "");
154
+ }
155
+ function formatDirectSessions(value) {
156
+ const sessions = normalizeDirectSessions(value);
157
+ if (sessions.length === 0)
158
+ return "none";
159
+ return sessions.map((entry) => {
160
+ const status = entry.status ? ` status=${entry.status}` : "";
161
+ const audit = entry.audit_decision ? ` audit=${entry.audit_decision}` : "";
162
+ return `${entry.session_id}${status}${audit}`;
163
+ }).join(", ");
164
+ }
165
+ function normalizeMaxItems(value) {
166
+ if (value === undefined)
167
+ return 8;
168
+ if (!Number.isInteger(value) || value < 1 || value > 50) {
169
+ throw new Error("max_items must be an integer from 1 to 50.");
170
+ }
171
+ return value;
172
+ }
173
+ function truncate(value, maxChars) {
174
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
175
+ }
@@ -12,8 +12,8 @@ export interface ToolCatalogSnapshot {
12
12
  tool_names: string[];
13
13
  tool_manifest_sha256: string;
14
14
  }
15
- export declare const CHATGPT_CORE_TOOL_NAMES: readonly ["health_check", "list_agents", "list_workspace", "read_workspace_file", "save_plan", "create_task", "wait_for_task", "get_task_summary", "get_diff", "get_result", "get_result_json", "get_test_log", "get_task_status", "list_tasks", "cancel_task", "audit_task", "safe_status", "safe_result", "safe_audit", "safe_test_summary", "safe_diff_summary"];
16
- export declare const CHATGPT_DIRECT_TOOL_NAMES: readonly ["health_check", "list_workspace", "create_direct_session", "search_workspace", "read_workspace_file", "apply_patch", "run_verification", "finalize_direct_session", "audit_session", "safe_direct_summary", "safe_finalize_direct_session", "safe_audit_direct_session", "sync_file"];
15
+ export declare const CHATGPT_CORE_TOOL_NAMES: readonly ["health_check", "list_agents", "list_workspace", "read_workspace_file", "save_plan", "create_task", "run_task_loop", "recommend_agent_for_task", "get_task_lineage", "export_task_evidence_pack", "get_project_policy", "wait_for_task", "get_task_summary", "get_diff", "get_result", "get_result_json", "get_test_log", "get_task_status", "list_tasks", "cancel_task", "audit_task", "safe_status", "safe_result", "safe_audit", "safe_test_summary", "safe_diff_summary"];
16
+ export declare const CHATGPT_DIRECT_TOOL_NAMES: readonly ["health_check", "list_workspace", "create_direct_session", "search_workspace", "read_workspace_file", "apply_patch", "run_verification", "run_direct_verification_bundle", "finalize_direct_session", "audit_session", "safe_direct_summary", "safe_finalize_direct_session", "safe_audit_direct_session", "sync_file"];
17
17
  export declare const CHATGPT_SEARCH_TOOL_NAMES: readonly ["health_check", "discover_tools", "explain_tool", "invoke_discovered_tool", "safe_status"];
18
18
  export declare function resolveToolProfile(configProfile?: string): ToolProfile;
19
19
  export declare function selectToolsForProfile<T extends CatalogTool>(tools: T[], profile: ToolProfile, enableDirectProfile?: boolean): T[];
@@ -7,6 +7,11 @@ export const CHATGPT_CORE_TOOL_NAMES = [
7
7
  "read_workspace_file",
8
8
  "save_plan",
9
9
  "create_task",
10
+ "run_task_loop",
11
+ "recommend_agent_for_task",
12
+ "get_task_lineage",
13
+ "export_task_evidence_pack",
14
+ "get_project_policy",
10
15
  "wait_for_task",
11
16
  "get_task_summary",
12
17
  "get_diff",
@@ -31,6 +36,7 @@ export const CHATGPT_DIRECT_TOOL_NAMES = [
31
36
  "read_workspace_file",
32
37
  "apply_patch",
33
38
  "run_verification",
39
+ "run_direct_verification_bundle",
34
40
  "finalize_direct_session",
35
41
  "audit_session",
36
42
  "safe_direct_summary",
@@ -350,6 +350,61 @@ const STATIC_TOOL_META = {
350
350
  requiresConfirmation: false,
351
351
  relatedTools: ["save_plan", "wait_for_task", "get_task_status"],
352
352
  },
353
+ run_task_loop: {
354
+ title: "Run Task Loop",
355
+ summary: "运行受保护的任务循环:创建任务、等待、验证、审计,并在低风险测试失败时创建 fix_tests 后续任务",
356
+ profiles: ["full", "chatgpt_core"],
357
+ modes: ["delegate", "audit"],
358
+ tags: ["loop", "task", "lineage", "verify", "audit", "fix"],
359
+ aliases: ["task_loop", "run_loop", "guarded_loop"],
360
+ risk: "workspace_write",
361
+ requiresConfirmation: false,
362
+ relatedTools: ["create_task", "wait_for_task", "safe_result", "safe_audit", "get_task_lineage"],
363
+ },
364
+ recommend_agent_for_task: {
365
+ title: "Recommend Agent For Task",
366
+ summary: "Return bounded agent routing guidance without starting an agent or creating a task",
367
+ profiles: ["full", "chatgpt_core"],
368
+ modes: ["delegate", "diagnostic"],
369
+ tags: ["agent", "routing", "recommend", "task", "safe"],
370
+ aliases: ["recommend_agent", "route_agent", "agent_routing"],
371
+ risk: "readonly",
372
+ requiresConfirmation: false,
373
+ relatedTools: ["list_agents", "create_task", "run_task_loop"],
374
+ },
375
+ get_task_lineage: {
376
+ title: "Get Task Lineage",
377
+ summary: "读取 run_task_loop 生成的任务链路安全摘要,不返回完整日志或 diff",
378
+ profiles: ["full", "chatgpt_core"],
379
+ modes: ["delegate", "audit", "diagnostic"],
380
+ tags: ["lineage", "loop", "summary", "task", "safe"],
381
+ aliases: ["lineage", "task_lineage"],
382
+ risk: "readonly",
383
+ requiresConfirmation: false,
384
+ relatedTools: ["run_task_loop", "safe_result", "safe_audit"],
385
+ },
386
+ export_task_evidence_pack: {
387
+ title: "Export Task Evidence Pack",
388
+ summary: "Export bounded lineage, verification, Direct, policy, and catalog evidence without logs or diffs",
389
+ profiles: ["full", "chatgpt_core"],
390
+ modes: ["delegate", "audit", "diagnostic"],
391
+ tags: ["evidence", "lineage", "summary", "release", "safe"],
392
+ aliases: ["evidence_pack", "export_evidence", "task_evidence"],
393
+ risk: "workspace_write",
394
+ requiresConfirmation: false,
395
+ relatedTools: ["get_task_lineage", "run_task_loop", "get_project_policy"],
396
+ },
397
+ get_project_policy: {
398
+ title: "Get Project Policy",
399
+ summary: "Read bounded project policy and release readiness without exposing secrets or expanding command permissions",
400
+ profiles: ["full", "chatgpt_core"],
401
+ modes: ["diagnostic", "release"],
402
+ tags: ["policy", "project", "release", "readiness", "safe"],
403
+ aliases: ["project_policy", "policy", "get_policy"],
404
+ risk: "readonly",
405
+ requiresConfirmation: false,
406
+ relatedTools: ["release_check", "release_prepare", "release_cleanup"],
407
+ },
353
408
  sync_file: {
354
409
  title: "Sync File",
355
410
  summary: "在 Direct 会话中同步文件(带 sha256 校验)",
@@ -461,6 +516,17 @@ const STATIC_TOOL_META = {
461
516
  requiresConfirmation: false,
462
517
  relatedTools: ["finalize_direct_session", "audit_session"],
463
518
  },
519
+ run_direct_verification_bundle: {
520
+ title: "Run Direct Verification Bundle",
521
+ summary: "Run multiple Direct allowlisted verification commands and return bounded status without stdout or stderr tails",
522
+ profiles: ["full", "chatgpt_direct"],
523
+ modes: ["direct", "audit"],
524
+ tags: ["direct", "verify", "verification", "bundle", "safe"],
525
+ aliases: ["direct_verify_bundle", "verification_bundle"],
526
+ risk: "command",
527
+ requiresConfirmation: false,
528
+ relatedTools: ["run_verification", "safe_finalize_direct_session", "safe_audit_direct_session"],
529
+ },
464
530
  safe_direct_summary: {
465
531
  title: "Safe Direct Summary",
466
532
  summary: "Direct 会话安全摘要(不含完整 diff 或验证 stdout/stderr)",
@@ -639,6 +705,50 @@ const STATIC_TOOL_META = {
639
705
  requiresConfirmation: true,
640
706
  relatedTools: ["audit_task", "safe_status"],
641
707
  },
708
+ release_check: {
709
+ title: "Release Check",
710
+ summary: "Run bounded release readiness checks through the existing release gate without remote writes",
711
+ profiles: ["full"],
712
+ modes: ["release", "diagnostic"],
713
+ tags: ["release", "check", "gate", "readiness", "policy"],
714
+ aliases: ["release_check", "check_release"],
715
+ risk: "release",
716
+ requiresConfirmation: true,
717
+ relatedTools: ["check_release_gate", "get_project_policy", "release_prepare", "release_verify"],
718
+ },
719
+ release_prepare: {
720
+ title: "Release Prepare",
721
+ summary: "Run policy-approved local release preparation commands through the existing command guard",
722
+ profiles: ["full"],
723
+ modes: ["release"],
724
+ tags: ["release", "prepare", "build", "test", "policy"],
725
+ aliases: ["prepare_release", "release_ready"],
726
+ risk: "release",
727
+ requiresConfirmation: true,
728
+ relatedTools: ["get_project_policy", "release_check", "release_verify"],
729
+ },
730
+ release_verify: {
731
+ title: "Release Verify",
732
+ summary: "Verify npm, GitHub release, and CI facts with read-only remote checks",
733
+ profiles: ["full"],
734
+ modes: ["release", "diagnostic"],
735
+ tags: ["release", "verify", "npm", "github", "ci"],
736
+ aliases: ["verify_release", "release_remote_verify"],
737
+ risk: "release",
738
+ requiresConfirmation: true,
739
+ relatedTools: ["release_check", "check_release_gate"],
740
+ },
741
+ release_cleanup: {
742
+ title: "Release Cleanup",
743
+ summary: "Dry-run-first cleanup for low-risk release artifacts under project policy",
744
+ profiles: ["full"],
745
+ modes: ["release"],
746
+ tags: ["release", "cleanup", "artifacts", "policy"],
747
+ aliases: ["cleanup_release", "release_artifact_cleanup"],
748
+ risk: "workspace_write",
749
+ requiresConfirmation: true,
750
+ relatedTools: ["get_project_policy", "release_prepare"],
751
+ },
642
752
  merge_worktree: {
643
753
  title: "Merge Worktree",
644
754
  summary: "v1.0.0: 合并隔离 worktree 变更回主工作区",
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const PATCHWARDEN_VERSION = "1.1.0";
2
- export declare const TOOL_SCHEMA_EPOCH = "2026-06-27-v9";
1
+ export declare const PATCHWARDEN_VERSION = "1.5.0";
2
+ export declare const TOOL_SCHEMA_EPOCH = "2026-07-05-v13";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const PATCHWARDEN_VERSION = "1.1.0";
2
- export const TOOL_SCHEMA_EPOCH = "2026-06-27-v9";
1
+ export const PATCHWARDEN_VERSION = "1.5.0";
2
+ export const TOOL_SCHEMA_EPOCH = "2026-07-05-v13";
@@ -92,6 +92,37 @@ safety boundaries and complete evidence.
92
92
  7. Changes remain uncommitted for review; commit, push, and publish are outside
93
93
  the ordinary task scope.
94
94
 
95
+ For routine guarded work, `run_task_loop` can perform the assess-only preflight,
96
+ task creation, waiting, safe summary review, audit, and bounded `fix_tests`
97
+ follow-up cycle in one tool call. It still uses the existing Watcher and
98
+ allow-listed verification commands, stops at local confirmation boundaries, and
99
+ returns a `lineage_id` for `get_task_lineage` instead of full logs or diffs.
100
+
101
+ For v1.4 Direct-assisted verification, set `direct_verify=true` only when the
102
+ local Direct profile is enabled and the desired Direct verification commands are
103
+ already allow-listed. The loop creates a Direct session after the normal task
104
+ and audit have succeeded, runs verification, safe-finalizes, safe-audits, and
105
+ stores bounded Direct evidence in lineage. It does not call Direct patching
106
+ tools, publish, push, tag, create releases, or restart live services.
107
+
108
+ For v1.5 isolated loop work, set `agent="auto"` when you want PatchWarden to
109
+ pick from configured local agents using bounded routing, and set
110
+ `isolation_mode="worktree"` only when the target repo is a git repository and
111
+ you want the task to run in an isolated worktree. Worktree mode records evidence
112
+ in lineage but never auto-merges or auto-deletes the worktree. After a loop
113
+ finishes, call `export_task_evidence_pack(lineage_id)` to write bounded
114
+ `evidence.json` and `EVIDENCE.md` files without stdout/stderr tails, full diffs,
115
+ verification logs, or sensitive file content.
116
+
117
+ For v1.3 policy-aware work, call `get_project_policy` before release-oriented
118
+ changes. It reads the bounded effective `.patchwarden/project-policy.json`
119
+ summary and release readiness without granting new command permissions. Release
120
+ mode tools are full-profile only: `release_check` wraps the existing release
121
+ gate, `release_prepare` runs only already allow-listed local commands,
122
+ `release_verify` performs read-only npm/GitHub/CI checks, and `release_cleanup`
123
+ defaults to dry run. None of these tools publish, push, tag, create GitHub
124
+ Releases, restart live tunnels/watchers, or return full logs/diffs.
125
+
95
126
  `needs_confirm` assessments must be confirmed locally with
96
127
  `patchwarden-confirm <full_assessment_id>`. The confirmation command is not an
97
128
  MCP tool. A `blocked` assessment cannot be confirmed.
@@ -27,6 +27,15 @@ automation-friendly commands.
27
27
 
28
28
  ## Design Notes
29
29
 
30
+ - v1.3 Dashboard panels show bounded task lineage, project policy, and release
31
+ readiness summaries. The backing APIs are read-only and do not expose full
32
+ stdout/stderr, full diffs, long logs, or secret-bearing files.
33
+ - v1.4 extends the lineage panel with Direct-assisted verification status and
34
+ exposes a safe Direct session summary API without stdout/stderr tails or diffs.
35
+ - v1.5 adds an Evidence Pack dashboard card plus read-only
36
+ `/api/evidence-packs` and `/api/evidence-packs/:lineage_id` routes. These
37
+ APIs return bounded lineage/policy/catalog evidence and omit stdout/stderr,
38
+ full diffs, verification logs, and sensitive file content.
30
39
  - `control-center-mvp.md`: first Web dashboard scope.
31
40
  - `control-center-phase2.md`: follow-up management and diagnostics scope.
32
41
  - `control-center-daily-driver.md`: current daily-use contract.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchwarden",
3
- "version": "1.1.0",
4
- "description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped tasks, safe summaries, audits, and Direct editing.",
3
+ "version": "1.5.0",
4
+ "description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped task loops, safe summaries, audits, and Direct editing.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -229,6 +229,7 @@ async function testStaticFiles() {
229
229
  if (!rootCt.includes("text/html")) checks.push(`GET / -> Content-Type ${rootCt} (expected text/html)`);
230
230
  if (!root.body.includes("setup-checklist-card")) checks.push("dashboard missing setup checklist card");
231
231
  if (!root.body.includes("Show Core / Direct log tails")) checks.push("dashboard activity log is not collapsed behind a summary");
232
+ if (!root.body.includes("evidence-pack-card")) checks.push("dashboard missing v1.5 evidence pack card");
232
233
 
233
234
  if (vendor.status !== 200) checks.push(`GET /vendor/tailwindcss-browser.js -> status ${vendor.status}`);
234
235
  const vendorCt = vendor.headers["content-type"] || "";
@@ -667,6 +668,92 @@ async function testOtherGetApis() {
667
668
  problems.push(`/api/audit error: ${err.message}`);
668
669
  }
669
670
 
671
+ // /api/lineages
672
+ try {
673
+ const res = await httpGet(`${BASE_URL}/api/lineages`);
674
+ if (res.status !== 200) problems.push(`/api/lineages -> ${res.status} (expected 200)`);
675
+ else {
676
+ const json = tryJson(res.body);
677
+ if (!json || !Array.isArray(json.lineages) || typeof json.total !== "number") {
678
+ problems.push(`/api/lineages missing bounded lineage summary fields: ${res.body.slice(0, 120)}`);
679
+ }
680
+ }
681
+ } catch (err) {
682
+ problems.push(`/api/lineages error: ${err.message}`);
683
+ }
684
+
685
+ // /api/project-policy
686
+ try {
687
+ const res = await httpGet(`${BASE_URL}/api/project-policy?repo_path=.`);
688
+ if (res.status !== 200) problems.push(`/api/project-policy -> ${res.status} (expected 200)`);
689
+ else {
690
+ const json = tryJson(res.body);
691
+ if (!json || !("effective_policy" in json) || !Array.isArray(json.issues)) {
692
+ problems.push(`/api/project-policy missing policy summary fields: ${res.body.slice(0, 120)}`);
693
+ }
694
+ const text = res.body.toLowerCase();
695
+ if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
696
+ problems.push("/api/project-policy leaked log/diff-shaped fields");
697
+ }
698
+ }
699
+ } catch (err) {
700
+ problems.push(`/api/project-policy error: ${err.message}`);
701
+ }
702
+
703
+ // /api/release/status
704
+ try {
705
+ const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
706
+ if (res.status !== 200) problems.push(`/api/release/status -> ${res.status} (expected 200)`);
707
+ else {
708
+ const json = tryJson(res.body);
709
+ if (!json || !("release_readiness" in json) || json.remote_write_performed !== false) {
710
+ problems.push(`/api/release/status missing read-only release status fields: ${res.body.slice(0, 120)}`);
711
+ }
712
+ const text = res.body.toLowerCase();
713
+ if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
714
+ problems.push("/api/release/status leaked log/diff-shaped fields");
715
+ }
716
+ }
717
+ } catch (err) {
718
+ problems.push(`/api/release/status error: ${err.message}`);
719
+ }
720
+
721
+ // /api/evidence-packs
722
+ try {
723
+ const res = await httpGet(`${BASE_URL}/api/evidence-packs`);
724
+ if (res.status !== 200) problems.push(`/api/evidence-packs -> ${res.status} (expected 200)`);
725
+ else {
726
+ const json = tryJson(res.body);
727
+ if (!json || !Array.isArray(json.evidence_packs) || typeof json.total !== "number") {
728
+ problems.push(`/api/evidence-packs missing bounded evidence summary fields: ${res.body.slice(0, 120)}`);
729
+ }
730
+ const text = res.body.toLowerCase();
731
+ if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff.patch") || text.includes("verification.log")) {
732
+ problems.push("/api/evidence-packs leaked log/diff-shaped fields");
733
+ }
734
+ }
735
+ } catch (err) {
736
+ problems.push(`/api/evidence-packs error: ${err.message}`);
737
+ }
738
+
739
+ // /api/direct-sessions/:id/summary (safe, bounded route; missing id is still non-leaky)
740
+ try {
741
+ const res = await httpGet(`${BASE_URL}/api/direct-sessions/direct_missing_summary/summary`);
742
+ if (res.status !== 200) problems.push(`/api/direct-sessions/:id/summary -> ${res.status} (expected 200)`);
743
+ else {
744
+ const json = tryJson(res.body);
745
+ if (!json || json.session_id !== "direct_missing_summary") {
746
+ problems.push(`/api/direct-sessions/:id/summary missing safe summary envelope: ${res.body.slice(0, 120)}`);
747
+ }
748
+ const text = res.body.toLowerCase();
749
+ if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff_patch") || text.includes("verification.log")) {
750
+ problems.push("/api/direct-sessions/:id/summary leaked log/diff-shaped fields");
751
+ }
752
+ }
753
+ } catch (err) {
754
+ problems.push(`/api/direct-sessions/:id/summary error: ${err.message}`);
755
+ }
756
+
670
757
  // /api/logs/core
671
758
  try {
672
759
  const res = await httpGet(`${BASE_URL}/api/logs/core`);
@@ -76,7 +76,7 @@ try {
76
76
  ready: false,
77
77
  pid: null,
78
78
  tool_profile: "chatgpt_direct",
79
- tool_count: 13,
79
+ tool_count: 14,
80
80
  tools_ready: true,
81
81
  }), "utf8");
82
82
  writeFileSync(join(directRuntime, "tunnel-client.pid"), String(fakeTunnel.pid), "utf8");
@@ -114,7 +114,7 @@ try {
114
114
  reason_code: "stale_fixture",
115
115
  last_error: "stale failure",
116
116
  tool_profile: "chatgpt_core",
117
- tool_count: 21,
117
+ tool_count: 26,
118
118
  tools_ready: true,
119
119
  }), "utf8");
120
120
  healthServer = spawn(
@@ -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"],