@calltelemetry/openclaw-linear 0.5.1 → 0.5.2
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/package.json +1 -1
- package/src/artifacts.ts +265 -0
- package/src/codex-worktree.ts +4 -0
- package/src/dispatch-state.ts +1 -0
- package/src/pipeline.ts +74 -4
- package/src/webhook.ts +22 -0
package/package.json
CHANGED
package/src/artifacts.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifacts.ts — .claw/ per-worktree artifact convention.
|
|
3
|
+
*
|
|
4
|
+
* Provides a standard directory structure for storing artifacts during
|
|
5
|
+
* the lifecycle of a dispatched issue. Any OpenClaw plugin that works
|
|
6
|
+
* with a worktree can write to {worktreePath}/.claw/.
|
|
7
|
+
*
|
|
8
|
+
* Structure:
|
|
9
|
+
* .claw/
|
|
10
|
+
* manifest.json — issue metadata + lifecycle timestamps
|
|
11
|
+
* plan.md — implementation plan
|
|
12
|
+
* worker-{N}.md — worker output per attempt (truncated)
|
|
13
|
+
* audit-{N}.json — audit verdict per attempt
|
|
14
|
+
* log.jsonl — append-only interaction log
|
|
15
|
+
* summary.md — agent-curated final summary
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import type { AuditVerdict } from "./pipeline.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const MAX_ARTIFACT_SIZE = 8192; // 8KB per output file
|
|
27
|
+
const MAX_PREVIEW_SIZE = 500; // For log entry previews
|
|
28
|
+
const MAX_PROMPT_PREVIEW = 200; // For log entry prompt previews
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface ClawManifest {
|
|
35
|
+
issueIdentifier: string;
|
|
36
|
+
issueTitle: string;
|
|
37
|
+
issueId: string;
|
|
38
|
+
tier: string;
|
|
39
|
+
model: string;
|
|
40
|
+
dispatchedAt: string;
|
|
41
|
+
worktreePath: string;
|
|
42
|
+
branch: string;
|
|
43
|
+
attempts: number;
|
|
44
|
+
status: string;
|
|
45
|
+
plugin: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface LogEntry {
|
|
49
|
+
ts: string;
|
|
50
|
+
phase: "worker" | "audit" | "verdict" | "dispatch";
|
|
51
|
+
attempt: number;
|
|
52
|
+
agent: string;
|
|
53
|
+
prompt: string;
|
|
54
|
+
outputPreview: string;
|
|
55
|
+
success: boolean;
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Directory setup
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function clawDir(worktreePath: string): string {
|
|
64
|
+
return join(worktreePath, ".claw");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Creates .claw/ directory. Returns the path. */
|
|
68
|
+
export function ensureClawDir(worktreePath: string): string {
|
|
69
|
+
const dir = clawDir(worktreePath);
|
|
70
|
+
if (!existsSync(dir)) {
|
|
71
|
+
mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ensure .claw/ is in .gitignore at the worktree root.
|
|
78
|
+
* Appends if not already present. Idempotent.
|
|
79
|
+
*/
|
|
80
|
+
export function ensureGitignore(worktreePath: string): void {
|
|
81
|
+
const gitignorePath = join(worktreePath, ".gitignore");
|
|
82
|
+
try {
|
|
83
|
+
const content = existsSync(gitignorePath)
|
|
84
|
+
? readFileSync(gitignorePath, "utf-8")
|
|
85
|
+
: "";
|
|
86
|
+
if (!content.split("\n").some((line) => line.trim() === ".claw" || line.trim() === ".claw/")) {
|
|
87
|
+
const nl = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
88
|
+
appendFileSync(gitignorePath, `${nl}.claw/\n`, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Best effort — don't block pipeline
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Manifest
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
export function writeManifest(worktreePath: string, manifest: ClawManifest): void {
|
|
100
|
+
const dir = ensureClawDir(worktreePath);
|
|
101
|
+
writeFileSync(join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function readManifest(worktreePath: string): ClawManifest | null {
|
|
105
|
+
try {
|
|
106
|
+
const raw = readFileSync(join(clawDir(worktreePath), "manifest.json"), "utf-8");
|
|
107
|
+
return JSON.parse(raw) as ClawManifest;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function updateManifest(worktreePath: string, updates: Partial<ClawManifest>): void {
|
|
114
|
+
const current = readManifest(worktreePath);
|
|
115
|
+
if (!current) return;
|
|
116
|
+
writeManifest(worktreePath, { ...current, ...updates });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Phase artifacts
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** Save worker output for a given attempt. Truncated to MAX_ARTIFACT_SIZE. */
|
|
124
|
+
export function saveWorkerOutput(worktreePath: string, attempt: number, output: string): void {
|
|
125
|
+
const dir = ensureClawDir(worktreePath);
|
|
126
|
+
const truncated = output.length > MAX_ARTIFACT_SIZE
|
|
127
|
+
? output.slice(0, MAX_ARTIFACT_SIZE) + "\n\n--- truncated ---"
|
|
128
|
+
: output;
|
|
129
|
+
writeFileSync(join(dir, `worker-${attempt}.md`), truncated, "utf-8");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Save a plan (extracted from worker output or provided directly). */
|
|
133
|
+
export function savePlan(worktreePath: string, plan: string): void {
|
|
134
|
+
const dir = ensureClawDir(worktreePath);
|
|
135
|
+
writeFileSync(join(dir, "plan.md"), plan, "utf-8");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Save audit verdict for a given attempt. */
|
|
139
|
+
export function saveAuditVerdict(worktreePath: string, attempt: number, verdict: AuditVerdict): void {
|
|
140
|
+
const dir = ensureClawDir(worktreePath);
|
|
141
|
+
writeFileSync(join(dir, `audit-${attempt}.json`), JSON.stringify(verdict, null, 2) + "\n", "utf-8");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Interaction log
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/** Append a structured log entry to .claw/log.jsonl. */
|
|
149
|
+
export function appendLog(worktreePath: string, entry: LogEntry): void {
|
|
150
|
+
const dir = ensureClawDir(worktreePath);
|
|
151
|
+
const truncated: LogEntry = {
|
|
152
|
+
...entry,
|
|
153
|
+
prompt: entry.prompt.slice(0, MAX_PROMPT_PREVIEW),
|
|
154
|
+
outputPreview: entry.outputPreview.slice(0, MAX_PREVIEW_SIZE),
|
|
155
|
+
};
|
|
156
|
+
appendFileSync(join(dir, "log.jsonl"), JSON.stringify(truncated) + "\n", "utf-8");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Summary
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/** Write the final curated summary. */
|
|
164
|
+
export function writeSummary(worktreePath: string, summary: string): void {
|
|
165
|
+
const dir = ensureClawDir(worktreePath);
|
|
166
|
+
writeFileSync(join(dir, "summary.md"), summary, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build a markdown summary from all .claw/ artifacts.
|
|
171
|
+
* Used at issue completion to generate a memory-friendly summary.
|
|
172
|
+
*/
|
|
173
|
+
export function buildSummaryFromArtifacts(worktreePath: string): string | null {
|
|
174
|
+
const manifest = readManifest(worktreePath);
|
|
175
|
+
if (!manifest) return null;
|
|
176
|
+
|
|
177
|
+
const parts: string[] = [];
|
|
178
|
+
parts.push(`# Dispatch: ${manifest.issueIdentifier} — ${manifest.issueTitle}`);
|
|
179
|
+
parts.push(`**Tier:** ${manifest.tier} | **Status:** ${manifest.status} | **Attempts:** ${manifest.attempts}`);
|
|
180
|
+
parts.push("");
|
|
181
|
+
|
|
182
|
+
// Include plan if exists
|
|
183
|
+
try {
|
|
184
|
+
const plan = readFileSync(join(clawDir(worktreePath), "plan.md"), "utf-8");
|
|
185
|
+
parts.push("## Plan");
|
|
186
|
+
parts.push(plan.slice(0, 2000));
|
|
187
|
+
parts.push("");
|
|
188
|
+
} catch { /* no plan */ }
|
|
189
|
+
|
|
190
|
+
// Include each attempt's worker + audit
|
|
191
|
+
for (let i = 0; i < manifest.attempts; i++) {
|
|
192
|
+
parts.push(`## Attempt ${i}`);
|
|
193
|
+
|
|
194
|
+
// Worker output preview
|
|
195
|
+
try {
|
|
196
|
+
const worker = readFileSync(join(clawDir(worktreePath), `worker-${i}.md`), "utf-8");
|
|
197
|
+
parts.push("### Worker Output");
|
|
198
|
+
parts.push(worker.slice(0, 1500));
|
|
199
|
+
parts.push("");
|
|
200
|
+
} catch { /* no worker output */ }
|
|
201
|
+
|
|
202
|
+
// Audit verdict
|
|
203
|
+
try {
|
|
204
|
+
const raw = readFileSync(join(clawDir(worktreePath), `audit-${i}.json`), "utf-8");
|
|
205
|
+
const verdict = JSON.parse(raw) as AuditVerdict;
|
|
206
|
+
parts.push(`### Audit: ${verdict.pass ? "PASS" : "FAIL"}`);
|
|
207
|
+
if (verdict.criteria.length) parts.push(`**Criteria:** ${verdict.criteria.join(", ")}`);
|
|
208
|
+
if (verdict.gaps.length) parts.push(`**Gaps:** ${verdict.gaps.join(", ")}`);
|
|
209
|
+
if (verdict.testResults) parts.push(`**Tests:** ${verdict.testResults}`);
|
|
210
|
+
parts.push("");
|
|
211
|
+
} catch { /* no audit */ }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
parts.push("---");
|
|
215
|
+
parts.push(`*Artifacts: ${worktreePath}/.claw/*`);
|
|
216
|
+
|
|
217
|
+
return parts.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Memory integration
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Write dispatch summary to the orchestrator's memory directory.
|
|
226
|
+
* Auto-indexed by OpenClaw's sqlite+embeddings memory system.
|
|
227
|
+
*/
|
|
228
|
+
export function writeDispatchMemory(
|
|
229
|
+
issueIdentifier: string,
|
|
230
|
+
summary: string,
|
|
231
|
+
workspaceDir: string,
|
|
232
|
+
): void {
|
|
233
|
+
const memDir = join(workspaceDir, "memory");
|
|
234
|
+
if (!existsSync(memDir)) {
|
|
235
|
+
mkdirSync(memDir, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
writeFileSync(
|
|
238
|
+
join(memDir, `dispatch-${issueIdentifier}.md`),
|
|
239
|
+
summary,
|
|
240
|
+
"utf-8",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve the orchestrator agent's workspace directory from config.
|
|
246
|
+
* Same config-based approach as resolveAgentDirs in agent.ts.
|
|
247
|
+
*/
|
|
248
|
+
export function resolveOrchestratorWorkspace(
|
|
249
|
+
api: any,
|
|
250
|
+
pluginConfig?: Record<string, unknown>,
|
|
251
|
+
): string {
|
|
252
|
+
const home = process.env.HOME ?? "/home/claw";
|
|
253
|
+
const agentId = (pluginConfig?.defaultAgentId as string) ?? "default";
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const config = api.runtime.config.loadConfig() as Record<string, any>;
|
|
257
|
+
const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
|
|
258
|
+
const agentEntry = agentList?.find((a: any) => a.id === agentId);
|
|
259
|
+
return agentEntry?.workspace
|
|
260
|
+
?? config?.agents?.defaults?.workspace
|
|
261
|
+
?? join(home, ".openclaw", "workspace");
|
|
262
|
+
} catch {
|
|
263
|
+
return join(home, ".openclaw", "workspace");
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/codex-worktree.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, statSync, readdirSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { ensureGitignore } from "./artifacts.js";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_BASE_REPO = "/home/claw/ai-workspace";
|
|
7
8
|
const DEFAULT_WORKTREE_BASE_DIR = path.join(homedir(), ".openclaw", "worktrees");
|
|
@@ -90,6 +91,7 @@ export function createWorktree(
|
|
|
90
91
|
try {
|
|
91
92
|
// Verify it's a valid git worktree
|
|
92
93
|
git(["rev-parse", "--git-dir"], worktreePath);
|
|
94
|
+
ensureGitignore(worktreePath);
|
|
93
95
|
return { path: worktreePath, branch, resumed: true };
|
|
94
96
|
} catch {
|
|
95
97
|
// Directory exists but isn't a valid worktree — remove and recreate
|
|
@@ -105,11 +107,13 @@ export function createWorktree(
|
|
|
105
107
|
if (branchExists) {
|
|
106
108
|
// Recreate worktree from existing branch — preserves previous work
|
|
107
109
|
git(["worktree", "add", worktreePath, branch], repo);
|
|
110
|
+
ensureGitignore(worktreePath);
|
|
108
111
|
return { path: worktreePath, branch, resumed: true };
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
// Fresh start: new branch off HEAD
|
|
112
115
|
git(["worktree", "add", "-b", branch, worktreePath], repo);
|
|
116
|
+
ensureGitignore(worktreePath);
|
|
113
117
|
return { path: worktreePath, branch, resumed: false };
|
|
114
118
|
}
|
|
115
119
|
|
package/src/dispatch-state.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface ActiveDispatch {
|
|
|
57
57
|
workerSessionKey?: string; // session key for current worker sub-agent
|
|
58
58
|
auditSessionKey?: string; // session key for current audit sub-agent
|
|
59
59
|
stuckReason?: string; // only set when status === "stuck"
|
|
60
|
+
issueTitle?: string; // for artifact summaries and memory headings
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export interface CompletedDispatch {
|
package/src/pipeline.ts
CHANGED
|
@@ -34,6 +34,16 @@ import {
|
|
|
34
34
|
getActiveDispatch,
|
|
35
35
|
} from "./dispatch-state.js";
|
|
36
36
|
import { type NotifyFn } from "./notify.js";
|
|
37
|
+
import {
|
|
38
|
+
saveWorkerOutput,
|
|
39
|
+
saveAuditVerdict,
|
|
40
|
+
appendLog,
|
|
41
|
+
updateManifest,
|
|
42
|
+
writeSummary,
|
|
43
|
+
buildSummaryFromArtifacts,
|
|
44
|
+
writeDispatchMemory,
|
|
45
|
+
resolveOrchestratorWorkspace,
|
|
46
|
+
} from "./artifacts.js";
|
|
37
47
|
|
|
38
48
|
// ---------------------------------------------------------------------------
|
|
39
49
|
// Prompt loading
|
|
@@ -264,6 +274,9 @@ export async function triggerAudit(
|
|
|
264
274
|
|
|
265
275
|
api.logger.info(`${TAG} worker completed, triggering audit (attempt ${dispatch.attempt})`);
|
|
266
276
|
|
|
277
|
+
// Update .claw/ manifest
|
|
278
|
+
try { updateManifest(dispatch.worktreePath, { status: "auditing", attempts: dispatch.attempt }); } catch {}
|
|
279
|
+
|
|
267
280
|
// Fetch fresh issue details for audit context
|
|
268
281
|
const issueDetails = await linearApi.getIssueDetails(dispatch.issueId).catch(() => null);
|
|
269
282
|
const issue: IssueContext = {
|
|
@@ -371,6 +384,15 @@ export async function processVerdict(
|
|
|
371
384
|
}
|
|
372
385
|
}
|
|
373
386
|
|
|
387
|
+
// Log audit interaction to .claw/
|
|
388
|
+
try {
|
|
389
|
+
appendLog(dispatch.worktreePath, {
|
|
390
|
+
ts: new Date().toISOString(), phase: "audit", attempt: dispatch.attempt,
|
|
391
|
+
agent: "auditor", prompt: "(audit task)",
|
|
392
|
+
outputPreview: auditOutput.slice(0, 500), success: event.success,
|
|
393
|
+
});
|
|
394
|
+
} catch {}
|
|
395
|
+
|
|
374
396
|
// Parse verdict
|
|
375
397
|
const verdict = parseVerdict(auditOutput);
|
|
376
398
|
if (!verdict) {
|
|
@@ -406,9 +428,13 @@ async function handleAuditPass(
|
|
|
406
428
|
dispatch: ActiveDispatch,
|
|
407
429
|
verdict: AuditVerdict,
|
|
408
430
|
): Promise<void> {
|
|
409
|
-
const { api, linearApi, notify, configPath } = hookCtx;
|
|
431
|
+
const { api, linearApi, notify, pluginConfig, configPath } = hookCtx;
|
|
410
432
|
const TAG = `[${dispatch.issueIdentifier}]`;
|
|
411
433
|
|
|
434
|
+
// Save audit verdict to .claw/
|
|
435
|
+
try { saveAuditVerdict(dispatch.worktreePath, dispatch.attempt, verdict); } catch {}
|
|
436
|
+
try { updateManifest(dispatch.worktreePath, { status: "done", attempts: dispatch.attempt + 1 }); } catch {}
|
|
437
|
+
|
|
412
438
|
// CAS transition: auditing → done
|
|
413
439
|
try {
|
|
414
440
|
await transitionDispatch(dispatch.issueIdentifier, "auditing", "done", undefined, configPath);
|
|
@@ -428,11 +454,26 @@ async function handleAuditPass(
|
|
|
428
454
|
project: dispatch.project,
|
|
429
455
|
}, configPath);
|
|
430
456
|
|
|
431
|
-
//
|
|
457
|
+
// Build summary from .claw/ artifacts and write to memory
|
|
458
|
+
let summary: string | null = null;
|
|
459
|
+
try {
|
|
460
|
+
summary = buildSummaryFromArtifacts(dispatch.worktreePath);
|
|
461
|
+
if (summary) {
|
|
462
|
+
writeSummary(dispatch.worktreePath, summary);
|
|
463
|
+
const wsDir = resolveOrchestratorWorkspace(api, pluginConfig);
|
|
464
|
+
writeDispatchMemory(dispatch.issueIdentifier, summary, wsDir);
|
|
465
|
+
api.logger.info(`${TAG} .claw/ summary and memory written`);
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
api.logger.warn(`${TAG} failed to write summary/memory: ${err}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Post approval comment (with summary excerpt if available)
|
|
432
472
|
const criteriaList = verdict.criteria.map((c) => `- ${c}`).join("\n");
|
|
473
|
+
const summaryExcerpt = summary ? `\n\n**Summary:**\n${summary.slice(0, 2000)}` : "";
|
|
433
474
|
await linearApi.createComment(
|
|
434
475
|
dispatch.issueId,
|
|
435
|
-
`## Audit Passed\n\n**Criteria verified:**\n${criteriaList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Attempt ${dispatch.attempt + 1} — audit passed
|
|
476
|
+
`## Audit Passed\n\n**Criteria verified:**\n${criteriaList}\n\n**Tests:** ${verdict.testResults || "N/A"}${summaryExcerpt}\n\n---\n*Attempt ${dispatch.attempt + 1} — audit passed. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
436
477
|
).catch((err) => api.logger.error(`${TAG} failed to post audit pass comment: ${err}`));
|
|
437
478
|
|
|
438
479
|
api.logger.info(`${TAG} audit PASSED — dispatch completed (attempt ${dispatch.attempt})`);
|
|
@@ -458,8 +499,13 @@ async function handleAuditFail(
|
|
|
458
499
|
const maxAttempts = (pluginConfig?.maxReworkAttempts as number) ?? 2;
|
|
459
500
|
const nextAttempt = dispatch.attempt + 1;
|
|
460
501
|
|
|
502
|
+
// Save audit verdict to .claw/ (both escalation and rework paths)
|
|
503
|
+
try { saveAuditVerdict(dispatch.worktreePath, dispatch.attempt, verdict); } catch {}
|
|
504
|
+
|
|
461
505
|
if (nextAttempt > maxAttempts) {
|
|
462
506
|
// Escalate — too many failures
|
|
507
|
+
try { updateManifest(dispatch.worktreePath, { status: "stuck", attempts: nextAttempt }); } catch {}
|
|
508
|
+
|
|
463
509
|
try {
|
|
464
510
|
await transitionDispatch(
|
|
465
511
|
dispatch.issueIdentifier,
|
|
@@ -476,10 +522,20 @@ async function handleAuditFail(
|
|
|
476
522
|
throw err;
|
|
477
523
|
}
|
|
478
524
|
|
|
525
|
+
// Write summary + memory for stuck dispatches too
|
|
526
|
+
try {
|
|
527
|
+
const summary = buildSummaryFromArtifacts(dispatch.worktreePath);
|
|
528
|
+
if (summary) {
|
|
529
|
+
writeSummary(dispatch.worktreePath, summary);
|
|
530
|
+
const wsDir = resolveOrchestratorWorkspace(api, pluginConfig);
|
|
531
|
+
writeDispatchMemory(dispatch.issueIdentifier, summary, wsDir);
|
|
532
|
+
}
|
|
533
|
+
} catch {}
|
|
534
|
+
|
|
479
535
|
const gapsList = verdict.gaps.map((g) => `- ${g}`).join("\n");
|
|
480
536
|
await linearApi.createComment(
|
|
481
537
|
dispatch.issueId,
|
|
482
|
-
`## Audit Failed — Escalating\n\n**Attempt ${nextAttempt} of ${maxAttempts + 1}**\n\n**Gaps:**\n${gapsList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Max rework attempts reached. Needs human review
|
|
538
|
+
`## Audit Failed — Escalating\n\n**Attempt ${nextAttempt} of ${maxAttempts + 1}**\n\n**Gaps:**\n${gapsList}\n\n**Tests:** ${verdict.testResults || "N/A"}\n\n---\n*Max rework attempts reached. Needs human review. Artifacts: \`${dispatch.worktreePath}/.claw/\`*`,
|
|
483
539
|
).catch((err) => api.logger.error(`${TAG} failed to post escalation comment: ${err}`));
|
|
484
540
|
|
|
485
541
|
api.logger.warn(`${TAG} audit FAILED ${nextAttempt}x — escalating to human`);
|
|
@@ -608,6 +664,7 @@ export async function spawnWorker(
|
|
|
608
664
|
|
|
609
665
|
api.logger.info(`${TAG} spawning worker agent session=${workerSessionId} (attempt ${dispatch.attempt})`);
|
|
610
666
|
|
|
667
|
+
const workerStartTime = Date.now();
|
|
611
668
|
const result = await runAgent({
|
|
612
669
|
api,
|
|
613
670
|
agentId: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
@@ -616,6 +673,19 @@ export async function spawnWorker(
|
|
|
616
673
|
timeoutMs: (pluginConfig?.codexTimeoutMs as number) ?? 10 * 60_000,
|
|
617
674
|
});
|
|
618
675
|
|
|
676
|
+
// Save worker output to .claw/
|
|
677
|
+
const workerElapsed = Date.now() - workerStartTime;
|
|
678
|
+
try { saveWorkerOutput(dispatch.worktreePath, dispatch.attempt, result.output); } catch {}
|
|
679
|
+
try {
|
|
680
|
+
appendLog(dispatch.worktreePath, {
|
|
681
|
+
ts: new Date().toISOString(), phase: "worker", attempt: dispatch.attempt,
|
|
682
|
+
agent: (pluginConfig?.defaultAgentId as string) ?? "default",
|
|
683
|
+
prompt: workerPrompt.task.slice(0, 200),
|
|
684
|
+
outputPreview: result.output.slice(0, 500),
|
|
685
|
+
success: result.success, durationMs: workerElapsed,
|
|
686
|
+
});
|
|
687
|
+
} catch {}
|
|
688
|
+
|
|
619
689
|
// runAgent returns inline — trigger audit directly.
|
|
620
690
|
// Re-read dispatch state since it may have changed during worker run.
|
|
621
691
|
const freshState = await readDispatchState(configPath);
|
package/src/webhook.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { readDispatchState, getActiveDispatch, registerDispatch, updateDispatchS
|
|
|
9
9
|
import { createDiscordNotifier, createNoopNotifier, type NotifyFn } from "./notify.js";
|
|
10
10
|
import { assessTier } from "./tier-assess.js";
|
|
11
11
|
import { createWorktree, prepareWorkspace } from "./codex-worktree.js";
|
|
12
|
+
import { ensureClawDir, writeManifest } from "./artifacts.js";
|
|
12
13
|
|
|
13
14
|
// ── Agent profiles (loaded from config, no hardcoded names) ───────
|
|
14
15
|
interface AgentProfile {
|
|
@@ -1250,11 +1251,32 @@ async function handleDispatch(
|
|
|
1250
1251
|
api.logger.warn(`@dispatch: could not create agent session: ${err}`);
|
|
1251
1252
|
}
|
|
1252
1253
|
|
|
1254
|
+
// 6b. Initialize .claw/ artifact directory
|
|
1255
|
+
try {
|
|
1256
|
+
ensureClawDir(worktree.path);
|
|
1257
|
+
writeManifest(worktree.path, {
|
|
1258
|
+
issueIdentifier: identifier,
|
|
1259
|
+
issueTitle: enrichedIssue.title ?? "(untitled)",
|
|
1260
|
+
issueId: issue.id,
|
|
1261
|
+
tier: assessment.tier,
|
|
1262
|
+
model: assessment.model,
|
|
1263
|
+
dispatchedAt: new Date().toISOString(),
|
|
1264
|
+
worktreePath: worktree.path,
|
|
1265
|
+
branch: worktree.branch,
|
|
1266
|
+
attempts: 0,
|
|
1267
|
+
status: "dispatched",
|
|
1268
|
+
plugin: "openclaw-linear",
|
|
1269
|
+
});
|
|
1270
|
+
} catch (err) {
|
|
1271
|
+
api.logger.warn(`@dispatch: .claw/ init failed: ${err}`);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1253
1274
|
// 7. Register dispatch in persistent state
|
|
1254
1275
|
const now = new Date().toISOString();
|
|
1255
1276
|
await registerDispatch(identifier, {
|
|
1256
1277
|
issueId: issue.id,
|
|
1257
1278
|
issueIdentifier: identifier,
|
|
1279
|
+
issueTitle: enrichedIssue.title ?? "(untitled)",
|
|
1258
1280
|
worktreePath: worktree.path,
|
|
1259
1281
|
branch: worktree.branch,
|
|
1260
1282
|
tier: assessment.tier,
|