patchwarden 0.4.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 (153) hide show
  1. package/Check-PatchWarden-Health.cmd +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +526 -0
  4. package/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  5. package/Restart-PatchWarden.cmd +19 -0
  6. package/Start-PatchWarden-Tunnel.cmd +7 -0
  7. package/dist/config.d.ts +24 -0
  8. package/dist/config.js +131 -0
  9. package/dist/doctor.d.ts +10 -0
  10. package/dist/doctor.js +388 -0
  11. package/dist/errors.d.ts +16 -0
  12. package/dist/errors.js +27 -0
  13. package/dist/httpServer.d.ts +17 -0
  14. package/dist/httpServer.js +255 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.js +24 -0
  17. package/dist/runner/changeCapture.d.ts +43 -0
  18. package/dist/runner/changeCapture.js +245 -0
  19. package/dist/runner/cli.d.ts +9 -0
  20. package/dist/runner/cli.js +26 -0
  21. package/dist/runner/runTask.d.ts +8 -0
  22. package/dist/runner/runTask.js +821 -0
  23. package/dist/runner/watch.d.ts +19 -0
  24. package/dist/runner/watch.js +136 -0
  25. package/dist/security/commandGuard.d.ts +22 -0
  26. package/dist/security/commandGuard.js +56 -0
  27. package/dist/security/contentRedaction.d.ts +12 -0
  28. package/dist/security/contentRedaction.js +65 -0
  29. package/dist/security/pathGuard.d.ts +3 -0
  30. package/dist/security/pathGuard.js +98 -0
  31. package/dist/security/planGuard.d.ts +1 -0
  32. package/dist/security/planGuard.js +65 -0
  33. package/dist/security/sensitiveGuard.d.ts +2 -0
  34. package/dist/security/sensitiveGuard.js +60 -0
  35. package/dist/smoke-test.d.ts +15 -0
  36. package/dist/smoke-test.js +989 -0
  37. package/dist/taskProgress.d.ts +6 -0
  38. package/dist/taskProgress.js +36 -0
  39. package/dist/taskRuntime.d.ts +10 -0
  40. package/dist/taskRuntime.js +25 -0
  41. package/dist/tools/auditTask.d.ts +19 -0
  42. package/dist/tools/auditTask.js +307 -0
  43. package/dist/tools/cancelTask.d.ts +30 -0
  44. package/dist/tools/cancelTask.js +64 -0
  45. package/dist/tools/createTask.d.ts +42 -0
  46. package/dist/tools/createTask.js +188 -0
  47. package/dist/tools/getPlan.d.ts +12 -0
  48. package/dist/tools/getPlan.js +29 -0
  49. package/dist/tools/getTaskFile.d.ts +24 -0
  50. package/dist/tools/getTaskFile.js +84 -0
  51. package/dist/tools/getTaskProgress.d.ts +1 -0
  52. package/dist/tools/getTaskProgress.js +4 -0
  53. package/dist/tools/getTaskStatus.d.ts +46 -0
  54. package/dist/tools/getTaskStatus.js +36 -0
  55. package/dist/tools/getTaskStdoutTail.d.ts +9 -0
  56. package/dist/tools/getTaskStdoutTail.js +56 -0
  57. package/dist/tools/getTaskSummary.d.ts +66 -0
  58. package/dist/tools/getTaskSummary.js +237 -0
  59. package/dist/tools/healthCheck.d.ts +79 -0
  60. package/dist/tools/healthCheck.js +230 -0
  61. package/dist/tools/killTask.d.ts +15 -0
  62. package/dist/tools/killTask.js +4 -0
  63. package/dist/tools/listAgents.d.ts +12 -0
  64. package/dist/tools/listAgents.js +42 -0
  65. package/dist/tools/listTasks.d.ts +36 -0
  66. package/dist/tools/listTasks.js +94 -0
  67. package/dist/tools/listWorkspace.d.ts +10 -0
  68. package/dist/tools/listWorkspace.js +54 -0
  69. package/dist/tools/readWorkspaceFile.d.ts +8 -0
  70. package/dist/tools/readWorkspaceFile.js +26 -0
  71. package/dist/tools/registry.d.ts +24 -0
  72. package/dist/tools/registry.js +566 -0
  73. package/dist/tools/retryTask.d.ts +8 -0
  74. package/dist/tools/retryTask.js +45 -0
  75. package/dist/tools/savePlan.d.ts +10 -0
  76. package/dist/tools/savePlan.js +28 -0
  77. package/dist/tools/taskOutputs.d.ts +49 -0
  78. package/dist/tools/taskOutputs.js +136 -0
  79. package/dist/tools/taskTemplates.d.ts +15 -0
  80. package/dist/tools/taskTemplates.js +116 -0
  81. package/dist/tools/toolCatalog.d.ts +19 -0
  82. package/dist/tools/toolCatalog.js +71 -0
  83. package/dist/tools/waitForTask.d.ts +25 -0
  84. package/dist/tools/waitForTask.js +59 -0
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.js +2 -0
  87. package/dist/watcherStatus.d.ts +20 -0
  88. package/dist/watcherStatus.js +77 -0
  89. package/docs/assets/patchwarden-chatgpt-demo.svg +30 -0
  90. package/docs/demo.md +58 -0
  91. package/docs/migration-from-safe-bifrost.md +31 -0
  92. package/docs/release-v0.3.0.md +43 -0
  93. package/docs/release-v0.4.0.md +74 -0
  94. package/examples/config.example.json +40 -0
  95. package/examples/openai-tunnel/README.md +48 -0
  96. package/examples/openai-tunnel/chatgpt-test-prompt.md +53 -0
  97. package/examples/openai-tunnel/tunnel-client.example.yaml +39 -0
  98. package/package.json +73 -0
  99. package/scripts/brand-check.js +44 -0
  100. package/scripts/doctor-smoke.js +52 -0
  101. package/scripts/get-patchwarden-health.ps1 +114 -0
  102. package/scripts/http-mcp-smoke.js +351 -0
  103. package/scripts/lifecycle-smoke.js +453 -0
  104. package/scripts/mcp-manifest-check.js +120 -0
  105. package/scripts/mcp-smoke.js +336 -0
  106. package/scripts/pack-clean.js +142 -0
  107. package/scripts/patchwarden-mcp-stdio.cmd +7 -0
  108. package/scripts/restart-patchwarden.ps1 +242 -0
  109. package/scripts/start-patchwarden-tunnel.ps1 +503 -0
  110. package/scripts/tunnel-supervisor-smoke.js +77 -0
  111. package/scripts/watcher-supervisor-smoke.js +195 -0
  112. package/src/config.ts +165 -0
  113. package/src/doctor.ts +438 -0
  114. package/src/errors.ts +26 -0
  115. package/src/httpServer.ts +277 -0
  116. package/src/index.ts +34 -0
  117. package/src/runner/changeCapture.ts +304 -0
  118. package/src/runner/cli.ts +34 -0
  119. package/src/runner/runTask.ts +935 -0
  120. package/src/runner/watch.ts +152 -0
  121. package/src/security/commandGuard.ts +97 -0
  122. package/src/security/contentRedaction.ts +85 -0
  123. package/src/security/pathGuard.ts +175 -0
  124. package/src/security/planGuard.ts +93 -0
  125. package/src/security/sensitiveGuard.ts +72 -0
  126. package/src/smoke-test.ts +1078 -0
  127. package/src/taskProgress.ts +41 -0
  128. package/src/taskRuntime.ts +37 -0
  129. package/src/tools/auditTask.ts +340 -0
  130. package/src/tools/cancelTask.ts +70 -0
  131. package/src/tools/createTask.ts +337 -0
  132. package/src/tools/getPlan.ts +48 -0
  133. package/src/tools/getTaskFile.ts +111 -0
  134. package/src/tools/getTaskProgress.ts +5 -0
  135. package/src/tools/getTaskStatus.ts +85 -0
  136. package/src/tools/getTaskStdoutTail.ts +61 -0
  137. package/src/tools/getTaskSummary.ts +307 -0
  138. package/src/tools/healthCheck.ts +234 -0
  139. package/src/tools/killTask.ts +5 -0
  140. package/src/tools/listAgents.ts +54 -0
  141. package/src/tools/listTasks.ts +139 -0
  142. package/src/tools/listWorkspace.ts +71 -0
  143. package/src/tools/readWorkspaceFile.ts +41 -0
  144. package/src/tools/registry.ts +666 -0
  145. package/src/tools/retryTask.ts +53 -0
  146. package/src/tools/savePlan.ts +46 -0
  147. package/src/tools/taskOutputs.ts +155 -0
  148. package/src/tools/taskTemplates.ts +162 -0
  149. package/src/tools/toolCatalog.ts +92 -0
  150. package/src/tools/waitForTask.ts +90 -0
  151. package/src/version.ts +2 -0
  152. package/src/watcherStatus.ts +102 -0
  153. package/tsconfig.json +17 -0
@@ -0,0 +1,188 @@
1
+ import { mkdirSync, writeFileSync, existsSync, statSync } from "node:fs";
2
+ import { resolve, join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { getTasksDir, getPlansDir, getConfig } from "../config.js";
5
+ import { guardPath, guardWorkspacePath, guardReadPath } from "../security/pathGuard.js";
6
+ import { guardTestCommand } from "../security/commandGuard.js";
7
+ import { writeTaskProgress } from "../taskProgress.js";
8
+ import { PatchWardenError } from "../errors.js";
9
+ import { savePlan } from "./savePlan.js";
10
+ import { expandTaskTemplate, TASK_TEMPLATE_NAMES, } from "./taskTemplates.js";
11
+ import { PATCHWARDEN_VERSION } from "../version.js";
12
+ import { getLastToolCatalogSnapshot, resolveToolProfile } from "./toolCatalog.js";
13
+ import { derivePendingReason, readWatcherStatus, } from "../watcherStatus.js";
14
+ export function createTask(input) {
15
+ const config = getConfig();
16
+ const tasksDir = getTasksDir(config);
17
+ const plansDir = getPlansDir(config);
18
+ const planSources = [
19
+ input.plan_id?.trim() ? "plan_id" : "",
20
+ input.inline_plan?.trim() ? "inline_plan" : "",
21
+ input.template ? "template" : "",
22
+ ].filter(Boolean);
23
+ if (planSources.length !== 1) {
24
+ throw new PatchWardenError("invalid_plan_source", "create_task requires exactly one of plan_id, inline_plan, or template.", "Use an existing plan_id, pass inline_plan text, or choose one built-in template.");
25
+ }
26
+ if (input.template && !TASK_TEMPLATE_NAMES.includes(input.template)) {
27
+ throw new PatchWardenError("invalid_task_template", `Unknown task template "${input.template}".`, `Use one of: ${TASK_TEMPLATE_NAMES.join(", ")}.`);
28
+ }
29
+ // Resolve repo alias if configured
30
+ let resolvedRepoPath = input.repo_path?.trim() || "";
31
+ const aliases = config.repoAliases;
32
+ if (aliases && resolvedRepoPath && aliases[resolvedRepoPath]) {
33
+ resolvedRepoPath = aliases[resolvedRepoPath];
34
+ }
35
+ if (!resolvedRepoPath || resolvedRepoPath === "") {
36
+ throw new PatchWardenError("repo_path_required", "create_task requires an explicit repo_path; PatchWarden will not default to workspaceRoot.", 'Pass a repository path inside workspaceRoot, for example repo_path: "my-project".', true, { operation: "create_task", safe_alternative: "Pass an existing repository directory under workspaceRoot." });
37
+ }
38
+ // Validate agent
39
+ if (!config.agents[input.agent]) {
40
+ throw new PatchWardenError("agent_not_configured", `Unknown agent "${input.agent}". Available: ${Object.keys(config.agents).join(", ")}`, "Call list_agents and use an available configured agent.");
41
+ }
42
+ // Validate repo_path is within workspace
43
+ const safeRepoPath = guardWorkspacePath(resolvedRepoPath, config.workspaceRoot);
44
+ if (!existsSync(safeRepoPath)) {
45
+ throw new PatchWardenError("repo_path_not_found", `repo_path "${resolvedRepoPath}" resolves to "${safeRepoPath}", but that path does not exist.`, "Create the repository directory first or pass an existing path under workspaceRoot.", true, { operation: "create_task", path: resolvedRepoPath, resolved_repo_path: safeRepoPath, safe_alternative: "Use an existing repository directory under workspaceRoot." });
46
+ }
47
+ if (!statSync(safeRepoPath).isDirectory()) {
48
+ throw new PatchWardenError("repo_path_not_directory", `repo_path "${resolvedRepoPath}" resolves to a file, not a directory.`, "Pass the repository directory instead of a file path.", true, { operation: "create_task", path: resolvedRepoPath, resolved_repo_path: safeRepoPath, safe_alternative: "Pass the containing repository directory instead of a file." });
49
+ }
50
+ // Runtime self-modification protection: refuse to modify the active
51
+ // PatchWarden runtime directory or its critical subdirectories.
52
+ const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
53
+ const criticalDirs = ["dist", "src", "scripts", "release"];
54
+ if (safeRepoPath === runtimeRoot || safeRepoPath.startsWith(runtimeRoot + resolve("/")[0])) {
55
+ const isCritical = criticalDirs.some((dir) => safeRepoPath === join(runtimeRoot, dir) ||
56
+ safeRepoPath.startsWith(join(runtimeRoot, dir) + resolve("/")[0]));
57
+ if (safeRepoPath === runtimeRoot || isCritical) {
58
+ throw new PatchWardenError("runtime_self_modification_blocked", `repo_path "${resolvedRepoPath}" points to the active PatchWarden runtime or its critical subdirectories.`, "Use a dev copy or git worktree for PatchWarden development. The running MCP server must not be modified by a task.", true, {
59
+ operation: "create_task",
60
+ path: resolvedRepoPath,
61
+ resolved_repo_path: safeRepoPath,
62
+ runtime_root: runtimeRoot,
63
+ safe_alternative: "Clone or copy PatchWarden to a separate directory for development tasks.",
64
+ });
65
+ }
66
+ }
67
+ // Validate test command — must be in allowlist, no swallowing
68
+ let testCmd = "";
69
+ if (input.test_command && input.test_command.trim() !== "") {
70
+ testCmd = guardTestCommand(input.test_command, config);
71
+ // guardTestCommand throws if not in allowedTestCommands
72
+ }
73
+ if (input.verify_commands !== undefined && !Array.isArray(input.verify_commands)) {
74
+ throw new PatchWardenError("invalid_verify_commands", "verify_commands must be an array of allow-listed command strings.", "Pass an array such as [\"npm test\", \"npm run build\"].");
75
+ }
76
+ if ((input.verify_commands?.length || 0) > 20) {
77
+ throw new PatchWardenError("invalid_verify_commands", "verify_commands cannot contain more than 20 commands.", "Keep verification focused and use no more than 20 allow-listed commands.");
78
+ }
79
+ const verifyCommands = [...new Set([
80
+ ...(input.verify_commands || []).map((command) => guardTestCommand(command, config)),
81
+ ...(testCmd ? [testCmd] : []),
82
+ ])];
83
+ const timeoutSeconds = input.timeout_seconds ?? config.defaultTaskTimeoutSeconds;
84
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
85
+ throw new PatchWardenError("invalid_timeout", "timeout_seconds must be a positive integer", `Use a whole number from 1 to ${config.maxTaskTimeoutSeconds}.`);
86
+ }
87
+ if (timeoutSeconds > config.maxTaskTimeoutSeconds) {
88
+ throw new PatchWardenError("invalid_timeout", `timeout_seconds cannot exceed configured maximum ${config.maxTaskTimeoutSeconds}`, `Use a value no greater than ${config.maxTaskTimeoutSeconds}.`);
89
+ }
90
+ let planId = input.plan_id?.trim() || "";
91
+ let planSource = "saved";
92
+ let changePolicy = "repo_scoped_changes";
93
+ if (planId) {
94
+ const planFile = join(resolve(plansDir, planId), "plan.md");
95
+ guardReadPath(planFile, config.workspaceRoot, config.plansDir);
96
+ if (!existsSync(planFile)) {
97
+ throw new PatchWardenError("plan_not_found", `Plan "${planId}" not found.`, "Call save_plan first, or pass inline_plan/template directly to create_task.");
98
+ }
99
+ }
100
+ else if (input.inline_plan?.trim()) {
101
+ const saved = savePlan({
102
+ title: input.plan_title?.trim() || "Inline task plan",
103
+ content: input.inline_plan.trim(),
104
+ });
105
+ planId = saved.plan_id;
106
+ planSource = "inline";
107
+ }
108
+ else {
109
+ const expanded = expandTaskTemplate({
110
+ template: input.template,
111
+ goal: input.goal || "",
112
+ source_task_id: input.source_task_id,
113
+ verify_commands: verifyCommands,
114
+ });
115
+ const saved = savePlan({ title: expanded.title, content: expanded.content });
116
+ planId = saved.plan_id;
117
+ planSource = "template";
118
+ changePolicy = expanded.change_policy;
119
+ }
120
+ const taskId = `task_${Date.now()}_${planId.replace(/^plan_/, "")}`;
121
+ const taskDir = resolve(tasksDir, taskId);
122
+ guardPath(taskDir, config.workspaceRoot, config.tasksDir);
123
+ mkdirSync(taskDir, { recursive: true });
124
+ const status = "pending";
125
+ const statusFile = join(taskDir, "status.json");
126
+ const statusData = {
127
+ task_id: taskId,
128
+ plan_id: planId,
129
+ plan_source: planSource,
130
+ template: input.template || null,
131
+ change_policy: changePolicy,
132
+ agent: input.agent,
133
+ workspace_root: resolve(config.workspaceRoot),
134
+ repo_path: resolvedRepoPath,
135
+ resolved_repo_path: safeRepoPath,
136
+ test_command: testCmd,
137
+ verify_commands: verifyCommands,
138
+ timeout_seconds: timeoutSeconds,
139
+ status,
140
+ phase: "queued",
141
+ created_at: new Date().toISOString(),
142
+ updated_at: new Date().toISOString(),
143
+ last_heartbeat_at: new Date().toISOString(),
144
+ current_command: null,
145
+ error: null,
146
+ };
147
+ writeFileSync(statusFile, JSON.stringify(statusData, null, 2), "utf-8");
148
+ writeTaskProgress(taskDir, "queued", {
149
+ heartbeatAt: statusData.last_heartbeat_at,
150
+ note: `Waiting for watcher. Timeout: ${timeoutSeconds} seconds.`,
151
+ });
152
+ const catalog = getLastToolCatalogSnapshot();
153
+ const watcher = readWatcherStatus(config);
154
+ const pendingReason = derivePendingReason({ status, phase: statusData.phase }, watcher);
155
+ const hasWaitForTask = catalog?.tool_names?.includes("wait_for_task") ?? true;
156
+ const nextActionWait = `Call wait_for_task with task_id ${taskId}; keep calling it until terminal is true, then review the returned summary.`;
157
+ const nextActionPoll = `Task created. Monitor status with get_task_status(task_id: "${taskId}") and check progress.md. When status reaches done/failed, review get_task_summary or get_result.`;
158
+ const nextActionBlocked = `Task was saved but execution is blocked because the watcher is ${watcher.status}. Call health_check and restart the owned watcher; the queued task will be picked up after recovery.`;
159
+ const followupCandidates = ["health_check", "get_task_status", "list_tasks", "wait_for_task", "cancel_task"];
160
+ const availableFollowupTools = catalog
161
+ ? followupCandidates.filter((name) => catalog.tool_names.includes(name))
162
+ : followupCandidates;
163
+ return {
164
+ task_id: taskId,
165
+ plan_id: planId,
166
+ agent: input.agent,
167
+ status,
168
+ timeout_seconds: timeoutSeconds,
169
+ continuation_required: watcher.available && hasWaitForTask,
170
+ next_action: !watcher.available ? nextActionBlocked : hasWaitForTask ? nextActionWait : nextActionPoll,
171
+ path: taskDir,
172
+ plan_source: planSource,
173
+ ...(input.template ? { template: input.template } : {}),
174
+ change_policy: changePolicy,
175
+ server_version: PATCHWARDEN_VERSION,
176
+ tool_profile: catalog?.tool_profile || resolveToolProfile(config.toolProfile),
177
+ tool_manifest_sha256: catalog?.tool_manifest_sha256 || null,
178
+ execution_blocked: !watcher.available,
179
+ pending_reason: pendingReason,
180
+ watcher,
181
+ available_followup_tools: availableFollowupTools,
182
+ next_tool_call: !watcher.available
183
+ ? { name: "health_check", arguments: { detail: "standard" } }
184
+ : hasWaitForTask
185
+ ? { name: "wait_for_task", arguments: { task_id: taskId, timeout_seconds: 25 } }
186
+ : { name: "get_task_status", arguments: { task_id: taskId } },
187
+ };
188
+ }
@@ -0,0 +1,12 @@
1
+ export interface GetPlanInput {
2
+ plan_id: string;
3
+ }
4
+ export interface GetPlanOutput {
5
+ plan_id: string;
6
+ title: string;
7
+ content: string;
8
+ path: string;
9
+ redacted?: boolean;
10
+ redaction_categories?: string[];
11
+ }
12
+ export declare function getPlan(input: GetPlanInput): GetPlanOutput;
@@ -0,0 +1,29 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getPlansDir, getConfig } from "../config.js";
4
+ import { guardReadPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
7
+ export function getPlan(input) {
8
+ const config = getConfig();
9
+ const plansDir = getPlansDir(config);
10
+ // Restrict to plans directory only
11
+ const planDir = resolve(plansDir, input.plan_id);
12
+ const planFile = join(planDir, "plan.md");
13
+ guardReadPath(planFile, config.workspaceRoot, config.plansDir);
14
+ guardSensitivePath(planFile);
15
+ if (!existsSync(planFile)) {
16
+ throw new Error(`Plan not found: "${input.plan_id}". Check the plan ID or save a plan first.`);
17
+ }
18
+ const raw = readFileSync(planFile, "utf-8");
19
+ const redaction = redactSensitiveContent(raw);
20
+ const titleLine = raw.split("\n")[0]?.replace(/^#\s*/, "") || input.plan_id;
21
+ return {
22
+ plan_id: input.plan_id,
23
+ title: titleLine,
24
+ content: redaction.content,
25
+ path: planFile,
26
+ redacted: redaction.redacted,
27
+ redaction_categories: redaction.redaction_categories,
28
+ };
29
+ }
@@ -0,0 +1,24 @@
1
+ export interface GetTaskFileOutput {
2
+ task_id: string;
3
+ filename: string;
4
+ content: string;
5
+ path: string;
6
+ available: boolean;
7
+ reason?: "task_not_terminal" | "artifact_missing";
8
+ task_status?: string;
9
+ phase?: string;
10
+ pending_reason?: string | null;
11
+ watcher?: unknown;
12
+ next_action?: string;
13
+ next_tool_call?: {
14
+ name: string;
15
+ arguments: Record<string, unknown>;
16
+ };
17
+ redacted?: boolean;
18
+ redaction_categories?: string[];
19
+ }
20
+ export declare const TASK_READ_ONLY_FILES: string[];
21
+ /**
22
+ * Read a task output file: result.md, git.diff, test.log, etc.
23
+ */
24
+ export declare function getTaskFile(taskId: string, filename: string): GetTaskFileOutput;
@@ -0,0 +1,84 @@
1
+ import { readFileSync, existsSync, statSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getTasksDir, getConfig } from "../config.js";
4
+ import { guardPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
7
+ import { getTaskStatus } from "./getTaskStatus.js";
8
+ export const TASK_READ_ONLY_FILES = [
9
+ "result.md",
10
+ "result.json",
11
+ "git.diff",
12
+ "diff.patch",
13
+ "test.log",
14
+ "verify.log",
15
+ "verify.json",
16
+ "status.json",
17
+ "plan.md",
18
+ "error.log",
19
+ "stdout.log",
20
+ "stderr.log",
21
+ "progress.md",
22
+ "changed-files.json",
23
+ "file-stats.json",
24
+ "rollback-plan.json",
25
+ "rollback_scope_violation_plan.md",
26
+ ];
27
+ /**
28
+ * Read a task output file: result.md, git.diff, test.log, etc.
29
+ */
30
+ export function getTaskFile(taskId, filename) {
31
+ const config = getConfig();
32
+ const tasksDir = getTasksDir(config);
33
+ // Only allow known filenames
34
+ if (!TASK_READ_ONLY_FILES.includes(filename)) {
35
+ throw new Error(`File "${filename}" is not allowed. Allowed: ${TASK_READ_ONLY_FILES.join(", ")}`);
36
+ }
37
+ const taskDir = resolve(tasksDir, taskId);
38
+ const filePath = join(taskDir, filename);
39
+ guardPath(filePath, config.workspaceRoot, config.tasksDir);
40
+ guardSensitivePath(filePath);
41
+ if (!existsSync(filePath)) {
42
+ const status = getTaskStatus(taskId);
43
+ const terminal = !["pending", "running"].includes(status.status);
44
+ const watcherBlocked = status.status === "pending" && !status.watcher.available;
45
+ return {
46
+ task_id: taskId,
47
+ filename,
48
+ content: "",
49
+ path: filePath,
50
+ available: false,
51
+ reason: terminal ? "artifact_missing" : "task_not_terminal",
52
+ task_status: status.status,
53
+ phase: status.phase,
54
+ pending_reason: status.pending_reason,
55
+ watcher: status.watcher,
56
+ next_action: terminal
57
+ ? `The task is terminal but ${filename} is missing. Review get_task_summary and audit_task warnings.`
58
+ : watcherBlocked
59
+ ? "The task is queued but the watcher is unavailable. Call health_check and restart the owned watcher."
60
+ : "The task is still running or queued. Call wait_for_task or get_task_status before reading this artifact.",
61
+ next_tool_call: terminal
62
+ ? { name: "get_task_summary", arguments: { task_id: taskId } }
63
+ : watcherBlocked
64
+ ? { name: "health_check", arguments: { detail: "standard" } }
65
+ : { name: "wait_for_task", arguments: { task_id: taskId, timeout_seconds: 25 } },
66
+ redacted: false,
67
+ redaction_categories: [],
68
+ };
69
+ }
70
+ const stat = statSync(filePath);
71
+ if (stat.size > config.maxReadFileBytes) {
72
+ throw new Error(`File "${filename}" is ${stat.size} bytes, exceeds max of ${config.maxReadFileBytes} bytes.`);
73
+ }
74
+ const redaction = redactSensitiveContent(readFileSync(filePath, "utf-8"));
75
+ return {
76
+ task_id: taskId,
77
+ filename,
78
+ content: redaction.content,
79
+ path: filePath,
80
+ available: true,
81
+ redacted: redaction.redacted,
82
+ redaction_categories: redaction.redaction_categories,
83
+ };
84
+ }
@@ -0,0 +1 @@
1
+ export declare function getTaskProgress(taskId: string): import("./getTaskFile.js").GetTaskFileOutput;
@@ -0,0 +1,4 @@
1
+ import { getTaskFile } from "./getTaskFile.js";
2
+ export function getTaskProgress(taskId) {
3
+ return getTaskFile(taskId, "progress.md");
4
+ }
@@ -0,0 +1,46 @@
1
+ import type { TaskStatus } from "./createTask.js";
2
+ import type { TaskPhase } from "./createTask.js";
3
+ import { type PendingReason, type WatcherStatusSnapshot } from "../watcherStatus.js";
4
+ export interface GetTaskStatusOutput {
5
+ task_id: string;
6
+ plan_id: string;
7
+ plan_source?: "saved" | "inline" | "template";
8
+ template?: string | null;
9
+ change_policy?: "repo_scoped_changes" | "no_changes";
10
+ agent: string;
11
+ workspace_root: string;
12
+ repo_path: string;
13
+ resolved_repo_path: string;
14
+ status: TaskStatus;
15
+ phase: TaskPhase;
16
+ created_at: string;
17
+ updated_at: string;
18
+ last_heartbeat_at: string;
19
+ current_command: string | null;
20
+ timeout_seconds: number;
21
+ started_at?: string;
22
+ finished_at?: string;
23
+ changed_files?: Array<{
24
+ path: string;
25
+ change: string;
26
+ }>;
27
+ out_of_scope_changes?: Array<{
28
+ path: string;
29
+ change: string;
30
+ }>;
31
+ verify_status?: "passed" | "failed" | "skipped";
32
+ verify_commands?: string[];
33
+ diff_available?: boolean;
34
+ diff_truncated?: boolean;
35
+ workspace_dirty_before?: boolean;
36
+ workspace_dirty_after?: boolean;
37
+ workspace_dirty?: boolean;
38
+ error: string | null;
39
+ watcher_status: WatcherStatusSnapshot["status"];
40
+ watcher_last_heartbeat_at: string | null;
41
+ watcher_heartbeat_age_seconds: number | null;
42
+ watcher: WatcherStatusSnapshot;
43
+ pending_reason: PendingReason;
44
+ execution_blocked: boolean;
45
+ }
46
+ export declare function getTaskStatus(taskId: string): GetTaskStatusOutput;
@@ -0,0 +1,36 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getTasksDir, getConfig } from "../config.js";
4
+ import { guardReadPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+ import { readTaskRuntime } from "../taskRuntime.js";
7
+ import { derivePendingReason, readWatcherStatus, } from "../watcherStatus.js";
8
+ export function getTaskStatus(taskId) {
9
+ const config = getConfig();
10
+ const tasksDir = getTasksDir(config);
11
+ const taskDir = resolve(tasksDir, taskId);
12
+ const statusFile = join(taskDir, "status.json");
13
+ guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
14
+ guardSensitivePath(statusFile);
15
+ if (!existsSync(statusFile)) {
16
+ throw new Error(`Task not found: "${taskId}". Check the task ID or create a task first.`);
17
+ }
18
+ const raw = readFileSync(statusFile, "utf-8");
19
+ const status = JSON.parse(raw);
20
+ const runtime = readTaskRuntime(taskDir);
21
+ const phase = runtime.phase || status.phase || "queued";
22
+ const watcher = readWatcherStatus(config);
23
+ const pendingReason = derivePendingReason({ status: status.status, phase }, watcher);
24
+ return {
25
+ ...status,
26
+ phase,
27
+ last_heartbeat_at: runtime.last_heartbeat_at || status.last_heartbeat_at || status.updated_at,
28
+ current_command: runtime.current_command ?? status.current_command ?? null,
29
+ watcher_status: watcher.status,
30
+ watcher_last_heartbeat_at: watcher.last_heartbeat_at,
31
+ watcher_heartbeat_age_seconds: watcher.heartbeat_age_seconds,
32
+ watcher,
33
+ pending_reason: pendingReason,
34
+ execution_blocked: status.status === "pending" && !watcher.available,
35
+ };
36
+ }
@@ -0,0 +1,9 @@
1
+ export declare function getTaskStdoutTail(taskId: string, lines?: number): {
2
+ task_id: string;
3
+ lines: number;
4
+ stdout_tail: string;
5
+ stderr_tail: string;
6
+ source: string;
7
+ redacted: boolean;
8
+ redaction_categories: string[];
9
+ };
@@ -0,0 +1,56 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getTasksDir, getConfig } from "../config.js";
4
+ import { guardReadPath } from "../security/pathGuard.js";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
6
+ export function getTaskStdoutTail(taskId, lines = 80) {
7
+ const config = getConfig();
8
+ const tasksDir = getTasksDir(config);
9
+ const taskDir = join(tasksDir, taskId);
10
+ const statusFile = join(taskDir, "status.json");
11
+ // First verify the task exists
12
+ guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
13
+ if (!existsSync(statusFile)) {
14
+ throw new Error(`Task not found: "${taskId}"`);
15
+ }
16
+ const stdoutLog = join(taskDir, "stdout.log");
17
+ const stderrLog = join(taskDir, "stderr.log");
18
+ const resultFile = join(taskDir, "result.md");
19
+ const maxLines = Math.min(lines, 200);
20
+ let stdoutTail = "";
21
+ let stderrTail = "";
22
+ let source = "none";
23
+ // All log files are optional — don't throw if missing
24
+ if (existsSync(stdoutLog)) {
25
+ const raw = readFileSync(stdoutLog, "utf-8");
26
+ stdoutTail = raw.split("\n").slice(-maxLines).join("\n");
27
+ source = "stdout.log";
28
+ }
29
+ if (existsSync(stderrLog)) {
30
+ const raw = readFileSync(stderrLog, "utf-8");
31
+ stderrTail = raw.split("\n").slice(-Math.min(maxLines, 40)).join("\n");
32
+ }
33
+ // Fallback to result.md
34
+ if (!stdoutTail && existsSync(resultFile)) {
35
+ const raw = readFileSync(resultFile, "utf-8");
36
+ const stdoutMatch = raw.match(/## Agent stdout\s*\n+```\s*([\s\S]*?)```/i);
37
+ const stderrMatch = raw.match(/## Agent stderr\s*\n+```\s*([\s\S]*?)```/i);
38
+ if (stdoutMatch)
39
+ stdoutTail = stdoutMatch[1].split("\n").slice(-maxLines).join("\n");
40
+ if (stderrMatch && !stderrTail)
41
+ stderrTail = stderrMatch[1].split("\n").slice(-Math.min(maxLines, 40)).join("\n");
42
+ if (source === "none")
43
+ source = "result.md";
44
+ }
45
+ const stdoutRedacted = redactSensitiveContent(stdoutTail || "(no output yet — task may be pending or not started)");
46
+ const stderrRedacted = redactSensitiveContent(stderrTail || "(no stderr)");
47
+ return {
48
+ task_id: taskId,
49
+ lines: stdoutTail ? stdoutTail.split("\n").length : 0,
50
+ stdout_tail: stdoutRedacted.content,
51
+ stderr_tail: stderrRedacted.content,
52
+ source,
53
+ redacted: stdoutRedacted.redacted || stderrRedacted.redacted,
54
+ redaction_categories: [...new Set([...stdoutRedacted.redaction_categories, ...stderrRedacted.redaction_categories])],
55
+ };
56
+ }
@@ -0,0 +1,66 @@
1
+ export interface TaskSummaryOutput {
2
+ task_id: string;
3
+ status: string;
4
+ terminal: boolean;
5
+ acceptance_status: "pending" | "ready_for_review" | "needs_review" | "failed";
6
+ acceptance_reviewed_at: string | null;
7
+ acceptance_reviewer: string | null;
8
+ phase: string;
9
+ agent: string;
10
+ workspace_root: string;
11
+ repo_path: string;
12
+ resolved_repo_path: string;
13
+ changed_files: unknown[];
14
+ out_of_scope_changes: unknown[];
15
+ workspace_dirty_before: boolean;
16
+ workspace_dirty_after: boolean;
17
+ verify_status: string;
18
+ verify_commands: unknown[];
19
+ last_heartbeat_at: string;
20
+ current_command: string | null;
21
+ elapsed_ms: number;
22
+ summary: string;
23
+ test_summary: string;
24
+ diff_available: boolean;
25
+ diff_truncated: boolean;
26
+ result_available: boolean;
27
+ result_json_available: boolean;
28
+ verify_available: boolean;
29
+ test_log_available: boolean;
30
+ warnings: string[];
31
+ errors: string[];
32
+ artifacts: Record<string, boolean>;
33
+ plan_source: string;
34
+ template: string | null;
35
+ change_policy: string;
36
+ failure_reason: string | null;
37
+ failed_command: string | null;
38
+ suggested_next_action: string;
39
+ safe_followup_prompt: string | null;
40
+ verification_summary: {
41
+ status: string;
42
+ command_count: number;
43
+ passed_commands: number;
44
+ failed_commands: number;
45
+ skipped_commands: number;
46
+ headline: string;
47
+ };
48
+ failed_command_detail: {
49
+ command: string;
50
+ exit_code: number | null;
51
+ stderr_tail: string;
52
+ duration_ms: number;
53
+ } | null;
54
+ log_tails: {
55
+ stdout: string;
56
+ stderr: string;
57
+ test: string;
58
+ verify: string;
59
+ };
60
+ redacted: boolean;
61
+ redaction_categories: string[];
62
+ watcher: unknown;
63
+ pending_reason: string | null;
64
+ execution_blocked: boolean;
65
+ }
66
+ export declare function getTaskSummary(taskId: string): TaskSummaryOutput;