@calltelemetry/openclaw-linear 0.5.0 → 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/index.ts +11 -5
- package/package.json +1 -1
- package/prompts.yaml +27 -9
- package/src/agent.ts +41 -7
- package/src/artifacts.ts +265 -0
- package/src/claude-tool.ts +1 -1
- package/src/codex-worktree.ts +4 -0
- package/src/dispatch-state.ts +1 -0
- package/src/pipeline.ts +74 -4
- package/src/webhook.ts +73 -15
package/index.ts
CHANGED
|
@@ -196,15 +196,21 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
196
196
|
try {
|
|
197
197
|
const raw = execFileSync(bin, ["--version"], {
|
|
198
198
|
encoding: "utf8",
|
|
199
|
-
timeout:
|
|
199
|
+
timeout: 15_000,
|
|
200
200
|
env: { ...process.env, CLAUDECODE: undefined } as any,
|
|
201
201
|
}).trim();
|
|
202
202
|
cliChecks[name] = raw || "unknown";
|
|
203
203
|
} catch {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
// Fallback: check if the file exists (execFileSync can fail in worker contexts)
|
|
205
|
+
try {
|
|
206
|
+
require("node:fs").accessSync(bin, require("node:fs").constants.X_OK);
|
|
207
|
+
cliChecks[name] = "installed (version check skipped)";
|
|
208
|
+
} catch {
|
|
209
|
+
cliChecks[name] = "not found";
|
|
210
|
+
api.logger.warn(
|
|
211
|
+
`${name} CLI not found at ${bin}. The ${name}_run tool will fail. Install with: ${installCmd}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
208
214
|
}
|
|
209
215
|
}
|
|
210
216
|
|
package/package.json
CHANGED
package/prompts.yaml
CHANGED
|
@@ -5,13 +5,19 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Edit these to customize worker/audit behavior without rebuilding the plugin.
|
|
7
7
|
# Override path via `promptsPath` in plugin config.
|
|
8
|
+
#
|
|
9
|
+
# Access model:
|
|
10
|
+
# Zoe (orchestrator) — linearis READ ONLY (issues read/list/search)
|
|
11
|
+
# Worker — NO linearis access. Return text output only.
|
|
12
|
+
# Auditor — linearis READ + WRITE (can update status, close, comment)
|
|
8
13
|
|
|
9
14
|
worker:
|
|
10
15
|
system: |
|
|
11
|
-
You are implementing a Linear issue. Your job is to
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
You are a coding worker implementing a Linear issue. Your ONLY job is to write code and return a text summary.
|
|
17
|
+
You do NOT have access to linearis or any Linear issue management tools.
|
|
18
|
+
Do NOT attempt to update, close, comment on, or modify the Linear issue in any way.
|
|
19
|
+
Do NOT mark the issue as Done — the audit system handles all issue lifecycle.
|
|
20
|
+
Just write code and return your implementation summary as text.
|
|
15
21
|
task: |
|
|
16
22
|
Implement issue {{identifier}}: {{title}}
|
|
17
23
|
|
|
@@ -25,15 +31,22 @@ worker:
|
|
|
25
31
|
2. Plan your approach
|
|
26
32
|
3. Implement the solution in the worktree
|
|
27
33
|
4. Run tests to verify your changes
|
|
28
|
-
5.
|
|
29
|
-
|
|
34
|
+
5. Return a text summary of what you changed, what tests you ran, and any notes
|
|
35
|
+
|
|
36
|
+
Your text output will be captured automatically. Do NOT use linearis or attempt to post comments.
|
|
30
37
|
|
|
31
38
|
audit:
|
|
32
39
|
system: |
|
|
33
|
-
You are an independent auditor. Your job is to verify that work was completed correctly
|
|
40
|
+
You are an independent auditor. Your job is to verify that work was completed correctly
|
|
41
|
+
and then update the Linear issue accordingly.
|
|
34
42
|
The Linear issue body is the SOURCE OF TRUTH for what "done" means.
|
|
35
|
-
Worker
|
|
43
|
+
Worker output is secondary evidence of what was attempted.
|
|
36
44
|
You must be thorough and objective. Do not rubber-stamp.
|
|
45
|
+
|
|
46
|
+
You have WRITE access to linearis. After auditing, you are responsible for:
|
|
47
|
+
- Posting an audit summary comment on the issue
|
|
48
|
+
- Updating the issue status if the audit passes
|
|
49
|
+
Use `linearis` CLI via exec for these operations.
|
|
37
50
|
task: |
|
|
38
51
|
Audit issue {{identifier}}: {{title}}
|
|
39
52
|
|
|
@@ -44,12 +57,16 @@ audit:
|
|
|
44
57
|
|
|
45
58
|
Checklist:
|
|
46
59
|
1. Identify ALL acceptance criteria from the issue body
|
|
47
|
-
2. Read worker comments
|
|
60
|
+
2. Read worker comments: `linearis issues read {{identifier}}`
|
|
48
61
|
3. Verify each acceptance criterion is addressed in the code
|
|
49
62
|
4. Run tests in the worktree — verify they pass
|
|
50
63
|
5. Check test coverage if expectations are stated in the issue
|
|
51
64
|
6. Review the code diff for quality and correctness
|
|
52
65
|
|
|
66
|
+
After auditing:
|
|
67
|
+
- Post your audit findings as a comment: `linearis comments create {{identifier}} --body "..."`
|
|
68
|
+
- If PASS: update status: `linearis issues update {{identifier}} --status "Done"`
|
|
69
|
+
|
|
53
70
|
You MUST return a JSON verdict as the last line of your response:
|
|
54
71
|
{"pass": true/false, "criteria": ["list of criteria found"], "gaps": ["list of unmet criteria"], "testResults": "summary of test output"}
|
|
55
72
|
|
|
@@ -59,3 +76,4 @@ rework:
|
|
|
59
76
|
{{gaps}}
|
|
60
77
|
|
|
61
78
|
Address these specific issues in your rework. Focus on the gaps listed above.
|
|
79
|
+
Remember: you do NOT have linearis access. Just fix the code and return a text summary.
|
package/src/agent.ts
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
5
|
import type { LinearAgentApi, ActivityContent } from "./linear-api.js";
|
|
4
6
|
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Agent directory resolution (config-based, not ext API which ignores agentId)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
interface AgentDirs {
|
|
12
|
+
workspaceDir: string;
|
|
13
|
+
agentDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveAgentDirs(agentId: string, config: Record<string, any>): AgentDirs {
|
|
17
|
+
const home = process.env.HOME ?? "/home/claw";
|
|
18
|
+
const agentList = config?.agents?.list as Array<Record<string, any>> | undefined;
|
|
19
|
+
const agentEntry = agentList?.find((a) => a.id === agentId);
|
|
20
|
+
|
|
21
|
+
// Workspace: agent-specific override → agents.defaults.workspace → fallback
|
|
22
|
+
const workspaceDir = agentEntry?.workspace
|
|
23
|
+
?? config?.agents?.defaults?.workspace
|
|
24
|
+
?? join(home, ".openclaw", "workspace");
|
|
25
|
+
|
|
26
|
+
// Agent runtime dir: always ~/.openclaw/agents/{agentId}/agent
|
|
27
|
+
// (matches OpenClaw's internal structure)
|
|
28
|
+
const agentDir = join(home, ".openclaw", "agents", agentId, "agent");
|
|
29
|
+
mkdirSync(agentDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
return { workspaceDir, agentDir };
|
|
32
|
+
}
|
|
33
|
+
|
|
5
34
|
// Import extensionAPI for embedded agent runner (internal, not in public SDK)
|
|
6
35
|
let _extensionAPI: typeof import("/home/claw/.npm-global/lib/node_modules/openclaw/dist/extensionAPI.js") | null = null;
|
|
7
36
|
async function getExtensionAPI() {
|
|
@@ -66,17 +95,22 @@ async function runEmbedded(
|
|
|
66
95
|
): Promise<AgentRunResult> {
|
|
67
96
|
const ext = await getExtensionAPI();
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
98
|
+
// Load config so we can resolve agent dirs and providers correctly.
|
|
99
|
+
const config = await api.runtime.config.loadConfig();
|
|
100
|
+
const configAny = config as Record<string, any>;
|
|
101
|
+
|
|
102
|
+
// Resolve workspace and agent dirs from config (ext API ignores agentId).
|
|
103
|
+
const dirs = resolveAgentDirs(agentId, configAny);
|
|
104
|
+
const { workspaceDir, agentDir } = dirs;
|
|
72
105
|
const runId = randomUUID();
|
|
73
106
|
|
|
74
|
-
//
|
|
75
|
-
const
|
|
107
|
+
// Build session file path under the correct agent's sessions directory.
|
|
108
|
+
const sessionsDir = join(agentDir, "sessions");
|
|
109
|
+
try { mkdirSync(sessionsDir, { recursive: true }); } catch {}
|
|
110
|
+
const sessionFile = join(sessionsDir, `${sessionId}.jsonl`);
|
|
76
111
|
|
|
77
112
|
// Resolve model/provider from config — default is anthropic which requires
|
|
78
113
|
// a separate API key. Our agents use openrouter.
|
|
79
|
-
const configAny = config as Record<string, any>;
|
|
80
114
|
const agentList = configAny?.agents?.list as Array<Record<string, any>> | undefined;
|
|
81
115
|
const agentEntry = agentList?.find((a) => a.id === agentId);
|
|
82
116
|
const modelRef: string =
|
|
@@ -89,7 +123,7 @@ async function runEmbedded(
|
|
|
89
123
|
const provider = slashIdx > 0 ? modelRef.slice(0, slashIdx) : ext.DEFAULT_PROVIDER;
|
|
90
124
|
const model = slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
|
|
91
125
|
|
|
92
|
-
api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model}`);
|
|
126
|
+
api.logger.info(`Embedded agent run: agent=${agentId} session=${sessionId} runId=${runId} provider=${provider} model=${model} workspaceDir=${workspaceDir} agentDir=${agentDir}`);
|
|
93
127
|
|
|
94
128
|
const emit = (content: ActivityContent) => {
|
|
95
129
|
streaming.linearApi.emitActivity(streaming.agentSessionId, content).catch((err) => {
|
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/claude-tool.ts
CHANGED
|
@@ -138,7 +138,6 @@ export async function runClaude(
|
|
|
138
138
|
if (model ?? pluginConfig?.claudeModel) {
|
|
139
139
|
args.push("--model", (model ?? pluginConfig?.claudeModel) as string);
|
|
140
140
|
}
|
|
141
|
-
args.push("-C", workingDir);
|
|
142
141
|
args.push("-p", prompt);
|
|
143
142
|
|
|
144
143
|
api.logger.info(`Claude exec: ${CLAUDE_BIN} ${args.join(" ").slice(0, 200)}...`);
|
|
@@ -150,6 +149,7 @@ export async function runClaude(
|
|
|
150
149
|
|
|
151
150
|
const child = spawn(CLAUDE_BIN, args, {
|
|
152
151
|
stdio: ["ignore", "pipe", "pipe"],
|
|
152
|
+
cwd: workingDir,
|
|
153
153
|
env,
|
|
154
154
|
timeout: 0,
|
|
155
155
|
});
|
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 {
|
|
@@ -187,12 +188,18 @@ export async function handleLinearWebhook(
|
|
|
187
188
|
.map((c: any) => ` - **${c.user?.name ?? "Unknown"}**: ${c.body?.slice(0, 200)}`)
|
|
188
189
|
.join("\n");
|
|
189
190
|
|
|
191
|
+
const notifIssueRef = enrichedIssue?.identifier ?? issue.id;
|
|
190
192
|
const message = [
|
|
191
|
-
`
|
|
193
|
+
`You are an orchestrator responding to a Linear issue notification. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
192
194
|
``,
|
|
193
|
-
|
|
195
|
+
`**Tool access:**`,
|
|
196
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${notifIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
197
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
198
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
194
199
|
``,
|
|
195
|
-
|
|
200
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
201
|
+
``,
|
|
202
|
+
`## Issue: ${notifIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
196
203
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
197
204
|
``,
|
|
198
205
|
`**Description:**`,
|
|
@@ -200,7 +207,7 @@ export async function handleLinearWebhook(
|
|
|
200
207
|
commentSummary ? `\n**Recent comments:**\n${commentSummary}` : "",
|
|
201
208
|
comment?.body ? `\n**Triggering comment:**\n> ${comment.body}` : "",
|
|
202
209
|
``,
|
|
203
|
-
`Respond concisely.
|
|
210
|
+
`Respond concisely. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
204
211
|
].filter(Boolean).join("\n");
|
|
205
212
|
|
|
206
213
|
// Dispatch agent with session lifecycle (non-blocking)
|
|
@@ -363,11 +370,19 @@ export async function handleLinearWebhook(
|
|
|
363
370
|
.map((c: any) => `**${c.user?.name ?? c.actorName ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
364
371
|
.join("\n\n");
|
|
365
372
|
|
|
373
|
+
const issueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
366
374
|
const message = [
|
|
367
|
-
`You are an
|
|
368
|
-
|
|
375
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
376
|
+
``,
|
|
377
|
+
`**Tool access:**`,
|
|
378
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${issueRef}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
379
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
380
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
381
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
382
|
+
``,
|
|
383
|
+
`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`,
|
|
369
384
|
``,
|
|
370
|
-
`## Issue: ${
|
|
385
|
+
`## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
371
386
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
372
387
|
``,
|
|
373
388
|
`**Description:**`,
|
|
@@ -375,7 +390,7 @@ export async function handleLinearWebhook(
|
|
|
375
390
|
commentContext ? `\n**Conversation:**\n${commentContext}` : "",
|
|
376
391
|
userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
|
|
377
392
|
``,
|
|
378
|
-
`Respond to the user's request.
|
|
393
|
+
`Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
|
|
379
394
|
].filter(Boolean).join("\n");
|
|
380
395
|
|
|
381
396
|
// Run agent directly (non-blocking)
|
|
@@ -543,11 +558,19 @@ export async function handleLinearWebhook(
|
|
|
543
558
|
.map((c: any) => `**${c.user?.name ?? "User"}**: ${(c.body ?? "").slice(0, 300)}`)
|
|
544
559
|
.join("\n\n");
|
|
545
560
|
|
|
561
|
+
const followUpIssueRef = enrichedIssue?.identifier ?? issue.identifier ?? issue.id;
|
|
546
562
|
const message = [
|
|
547
|
-
`You are an
|
|
548
|
-
|
|
563
|
+
`You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
|
|
564
|
+
``,
|
|
565
|
+
`**Tool access:**`,
|
|
566
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${followUpIssueRef}\`), list, and search. Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
567
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
568
|
+
`- \`spawn_agent\`/\`ask_agent\`: Delegate to other crew agents.`,
|
|
569
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
570
|
+
``,
|
|
571
|
+
`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`,
|
|
549
572
|
``,
|
|
550
|
-
`## Issue: ${
|
|
573
|
+
`## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
|
|
551
574
|
`**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
|
|
552
575
|
``,
|
|
553
576
|
`**Description:**`,
|
|
@@ -555,7 +578,7 @@ export async function handleLinearWebhook(
|
|
|
555
578
|
commentContext ? `\n**Recent conversation:**\n${commentContext}` : "",
|
|
556
579
|
`\n**User's follow-up message:**\n> ${userMessage}`,
|
|
557
580
|
``,
|
|
558
|
-
`Respond to the user's follow-up. Be concise and action-oriented.`,
|
|
581
|
+
`Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
|
|
559
582
|
].filter(Boolean).join("\n");
|
|
560
583
|
|
|
561
584
|
setActiveSession({
|
|
@@ -683,6 +706,14 @@ export async function handleLinearWebhook(
|
|
|
683
706
|
|
|
684
707
|
api.logger.info(`Comment mention: @${mentionedAgent} on ${issue.identifier ?? issue.id} by ${commentor}`);
|
|
685
708
|
|
|
709
|
+
// Guard: skip if an agent run is already active for this issue
|
|
710
|
+
// (prevents dual-dispatch when both Comment.create and AgentSessionEvent fire)
|
|
711
|
+
if (activeRuns.has(issue.id)) {
|
|
712
|
+
api.logger.info(`Comment mention: agent already running for ${issue.identifier ?? issue.id} — skipping`);
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
activeRuns.add(issue.id);
|
|
716
|
+
|
|
686
717
|
// React with eyes to acknowledge the comment
|
|
687
718
|
if (comment?.id) {
|
|
688
719
|
linearApi.createReaction(comment.id, "eyes").catch(() => {});
|
|
@@ -712,9 +743,14 @@ export async function handleLinearWebhook(
|
|
|
712
743
|
const assignee = enrichedIssue.assignee?.name ?? "Unassigned";
|
|
713
744
|
|
|
714
745
|
const taskMessage = [
|
|
715
|
-
`
|
|
746
|
+
`You are an orchestrator responding to a Linear issue comment. Your text output will be automatically posted as a comment on the issue (do NOT post a comment yourself — the handler does it).`,
|
|
747
|
+
``,
|
|
748
|
+
`**Tool access:**`,
|
|
749
|
+
`- \`linearis\` CLI: READ ONLY. You can read issues (\`linearis issues read ${enrichedIssue.identifier ?? "API-XXX"}\`), list issues (\`linearis issues list\`), and search (\`linearis issues search "..."\`). Do NOT use linearis to update, close, comment, or modify issues.`,
|
|
750
|
+
`- \`code_run\`: Dispatch coding work to a worker. Workers return text — they cannot access linearis.`,
|
|
751
|
+
`- Standard tools: exec, read, edit, write, web_search, etc.`,
|
|
716
752
|
``,
|
|
717
|
-
|
|
753
|
+
`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post linearis comments — the audit system handles lifecycle transitions.`,
|
|
718
754
|
``,
|
|
719
755
|
`**Issue:** ${enrichedIssue.identifier ?? enrichedIssue.id} — ${enrichedIssue.title ?? "(untitled)"}`,
|
|
720
756
|
`**Status:** ${state} | **Priority:** ${priority} | **Assignee:** ${assignee} | **Labels:** ${labels}`,
|
|
@@ -725,7 +761,7 @@ export async function handleLinearWebhook(
|
|
|
725
761
|
`**${commentor} wrote:**`,
|
|
726
762
|
`> ${commentBody}`,
|
|
727
763
|
``,
|
|
728
|
-
`Respond to their message. Be concise and direct.
|
|
764
|
+
`Respond to their message. Be concise and direct. For work requests, dispatch via \`code_run\` and summarize the result.`,
|
|
729
765
|
].filter(Boolean).join("\n");
|
|
730
766
|
|
|
731
767
|
// Dispatch to agent with full session lifecycle (non-blocking)
|
|
@@ -814,6 +850,7 @@ export async function handleLinearWebhook(
|
|
|
814
850
|
}
|
|
815
851
|
} finally {
|
|
816
852
|
clearActiveSession(issue.id);
|
|
853
|
+
activeRuns.delete(issue.id);
|
|
817
854
|
}
|
|
818
855
|
})();
|
|
819
856
|
|
|
@@ -1214,11 +1251,32 @@ async function handleDispatch(
|
|
|
1214
1251
|
api.logger.warn(`@dispatch: could not create agent session: ${err}`);
|
|
1215
1252
|
}
|
|
1216
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
|
+
|
|
1217
1274
|
// 7. Register dispatch in persistent state
|
|
1218
1275
|
const now = new Date().toISOString();
|
|
1219
1276
|
await registerDispatch(identifier, {
|
|
1220
1277
|
issueId: issue.id,
|
|
1221
1278
|
issueIdentifier: identifier,
|
|
1279
|
+
issueTitle: enrichedIssue.title ?? "(untitled)",
|
|
1222
1280
|
worktreePath: worktree.path,
|
|
1223
1281
|
branch: worktree.branch,
|
|
1224
1282
|
tier: assessment.tier,
|