@calltelemetry/openclaw-linear 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +1 -1
- package/prompts.yaml +24 -12
- package/src/__test__/fixtures/webhook-payloads.ts +9 -2
- package/src/__test__/webhook-scenarios.test.ts +150 -0
- package/src/infra/doctor.test.ts +3 -3
- package/src/pipeline/guidance.test.ts +222 -0
- package/src/pipeline/guidance.ts +156 -0
- package/src/pipeline/pipeline.ts +23 -2
- package/src/pipeline/webhook.ts +68 -23
- package/src/tools/linear-issues-tool.test.ts +453 -0
- package/src/tools/linear-issues-tool.ts +338 -0
- package/src/tools/tools.test.ts +36 -7
- package/src/tools/tools.ts +9 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
extractGuidance,
|
|
4
|
+
extractGuidanceFromPromptContext,
|
|
5
|
+
cacheGuidanceForTeam,
|
|
6
|
+
getCachedGuidanceForTeam,
|
|
7
|
+
formatGuidanceAppendix,
|
|
8
|
+
isGuidanceEnabled,
|
|
9
|
+
_resetGuidanceCacheForTesting,
|
|
10
|
+
} from "./guidance.js";
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
_resetGuidanceCacheForTesting();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// extractGuidance
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
describe("extractGuidance", () => {
|
|
22
|
+
it("extracts from top-level guidance field", () => {
|
|
23
|
+
const result = extractGuidance({ guidance: "Always use main branch." });
|
|
24
|
+
expect(result.guidance).toBe("Always use main branch.");
|
|
25
|
+
expect(result.source).toBe("webhook");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("falls back to promptContext when guidance is missing", () => {
|
|
29
|
+
const result = extractGuidance({
|
|
30
|
+
promptContext: "## Guidance\nUse TypeScript.\n\n## Issue\nENG-123",
|
|
31
|
+
});
|
|
32
|
+
expect(result.guidance).toBe("Use TypeScript.");
|
|
33
|
+
expect(result.source).toBe("promptContext");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("prefers top-level guidance over promptContext", () => {
|
|
37
|
+
const result = extractGuidance({
|
|
38
|
+
guidance: "From top-level",
|
|
39
|
+
promptContext: "## Guidance\nFrom promptContext",
|
|
40
|
+
});
|
|
41
|
+
expect(result.guidance).toBe("From top-level");
|
|
42
|
+
expect(result.source).toBe("webhook");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null when neither field exists", () => {
|
|
46
|
+
const result = extractGuidance({});
|
|
47
|
+
expect(result.guidance).toBeNull();
|
|
48
|
+
expect(result.source).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("ignores empty/whitespace guidance", () => {
|
|
52
|
+
const result = extractGuidance({ guidance: " " });
|
|
53
|
+
expect(result.guidance).toBeNull();
|
|
54
|
+
expect(result.source).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// extractGuidanceFromPromptContext
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe("extractGuidanceFromPromptContext", () => {
|
|
63
|
+
it("extracts from XML-style <guidance> tags", () => {
|
|
64
|
+
const pc = "Some preamble.\n<guidance>Use Go for API work.\nAlways add tests.</guidance>\nMore stuff.";
|
|
65
|
+
expect(extractGuidanceFromPromptContext(pc)).toBe("Use Go for API work.\nAlways add tests.");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("extracts from markdown ## Guidance heading", () => {
|
|
69
|
+
const pc = "## Issue\nENG-123\n\n## Guidance\nCommit messages must reference JIRA.\n\n## Comments\nSome comments.";
|
|
70
|
+
expect(extractGuidanceFromPromptContext(pc)).toBe("Commit messages must reference JIRA.");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("extracts from markdown # Guidance heading", () => {
|
|
74
|
+
const pc = "# Guidance\nRun make test.\n\n# Issue\nENG-456";
|
|
75
|
+
expect(extractGuidanceFromPromptContext(pc)).toBe("Run make test.");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns null when no guidance section found", () => {
|
|
79
|
+
const pc = "## Issue\nENG-123\n\n## Description\nSome text.";
|
|
80
|
+
expect(extractGuidanceFromPromptContext(pc)).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("handles guidance at end of string (no trailing heading)", () => {
|
|
84
|
+
const pc = "## Guidance\nAlways use TypeScript.";
|
|
85
|
+
expect(extractGuidanceFromPromptContext(pc)).toBe("Always use TypeScript.");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// formatGuidanceAppendix
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
describe("formatGuidanceAppendix", () => {
|
|
94
|
+
it("formats guidance as appendix block", () => {
|
|
95
|
+
const result = formatGuidanceAppendix("Use main branch.");
|
|
96
|
+
expect(result).toContain("## Additional Guidance");
|
|
97
|
+
expect(result).toContain("Use main branch.");
|
|
98
|
+
expect(result).toMatch(/^---\n/);
|
|
99
|
+
expect(result).toMatch(/\n---$/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns empty string for null", () => {
|
|
103
|
+
expect(formatGuidanceAppendix(null)).toBe("");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns empty string for empty string", () => {
|
|
107
|
+
expect(formatGuidanceAppendix("")).toBe("");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns empty string for whitespace-only", () => {
|
|
111
|
+
expect(formatGuidanceAppendix(" \n ")).toBe("");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("truncates at 2000 chars", () => {
|
|
115
|
+
const long = "x".repeat(3000);
|
|
116
|
+
const result = formatGuidanceAppendix(long);
|
|
117
|
+
// 2000 x's + the framing text
|
|
118
|
+
expect(result).toContain("x".repeat(2000));
|
|
119
|
+
expect(result).not.toContain("x".repeat(2001));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Cache: cacheGuidanceForTeam / getCachedGuidanceForTeam
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe("guidance cache", () => {
|
|
128
|
+
it("stores and retrieves guidance by team ID", () => {
|
|
129
|
+
cacheGuidanceForTeam("team-1", "Use TypeScript.");
|
|
130
|
+
expect(getCachedGuidanceForTeam("team-1")).toBe("Use TypeScript.");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("returns null for unknown team", () => {
|
|
134
|
+
expect(getCachedGuidanceForTeam("team-unknown")).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns null after TTL expiry", () => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
vi.spyOn(Date, "now").mockReturnValue(now);
|
|
140
|
+
cacheGuidanceForTeam("team-1", "guidance");
|
|
141
|
+
|
|
142
|
+
// Advance 25 hours
|
|
143
|
+
vi.spyOn(Date, "now").mockReturnValue(now + 25 * 60 * 60 * 1000);
|
|
144
|
+
expect(getCachedGuidanceForTeam("team-1")).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("evicts oldest entry when at capacity", () => {
|
|
148
|
+
// Fill cache to 50 entries
|
|
149
|
+
for (let i = 0; i < 50; i++) {
|
|
150
|
+
cacheGuidanceForTeam(`team-${i}`, `guidance-${i}`);
|
|
151
|
+
}
|
|
152
|
+
expect(getCachedGuidanceForTeam("team-0")).toBe("guidance-0");
|
|
153
|
+
|
|
154
|
+
// Adding one more should evict team-0 (oldest)
|
|
155
|
+
cacheGuidanceForTeam("team-new", "new guidance");
|
|
156
|
+
expect(getCachedGuidanceForTeam("team-0")).toBeNull();
|
|
157
|
+
expect(getCachedGuidanceForTeam("team-new")).toBe("new guidance");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("updates existing entry without eviction", () => {
|
|
161
|
+
cacheGuidanceForTeam("team-1", "old");
|
|
162
|
+
cacheGuidanceForTeam("team-1", "new");
|
|
163
|
+
expect(getCachedGuidanceForTeam("team-1")).toBe("new");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("clears on _resetGuidanceCacheForTesting", () => {
|
|
167
|
+
cacheGuidanceForTeam("team-1", "guidance");
|
|
168
|
+
_resetGuidanceCacheForTesting();
|
|
169
|
+
expect(getCachedGuidanceForTeam("team-1")).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// isGuidanceEnabled
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
describe("isGuidanceEnabled", () => {
|
|
178
|
+
it("returns true by default (no config)", () => {
|
|
179
|
+
expect(isGuidanceEnabled(undefined, "team-1")).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("returns true when enableGuidance not set", () => {
|
|
183
|
+
expect(isGuidanceEnabled({}, "team-1")).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns false when enableGuidance is false", () => {
|
|
187
|
+
expect(isGuidanceEnabled({ enableGuidance: false }, "team-1")).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns true when enableGuidance is true", () => {
|
|
191
|
+
expect(isGuidanceEnabled({ enableGuidance: true }, "team-1")).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("team override true overrides workspace false", () => {
|
|
195
|
+
const config = {
|
|
196
|
+
enableGuidance: false,
|
|
197
|
+
teamGuidanceOverrides: { "team-1": true },
|
|
198
|
+
};
|
|
199
|
+
expect(isGuidanceEnabled(config, "team-1")).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("team override false overrides workspace true", () => {
|
|
203
|
+
const config = {
|
|
204
|
+
enableGuidance: true,
|
|
205
|
+
teamGuidanceOverrides: { "team-1": false },
|
|
206
|
+
};
|
|
207
|
+
expect(isGuidanceEnabled(config, "team-1")).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("unset team inherits workspace setting", () => {
|
|
211
|
+
const config = {
|
|
212
|
+
enableGuidance: false,
|
|
213
|
+
teamGuidanceOverrides: { "team-other": true },
|
|
214
|
+
};
|
|
215
|
+
expect(isGuidanceEnabled(config, "team-1")).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("works with undefined teamId", () => {
|
|
219
|
+
expect(isGuidanceEnabled({ enableGuidance: false }, undefined)).toBe(false);
|
|
220
|
+
expect(isGuidanceEnabled({ enableGuidance: true }, undefined)).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear workspace & team guidance extraction, caching, and formatting.
|
|
3
|
+
*
|
|
4
|
+
* Guidance is delivered via AgentSessionEvent webhook payloads (both the
|
|
5
|
+
* top-level `guidance` field and embedded within `promptContext`). This
|
|
6
|
+
* module extracts it, caches it per-team (so Comment webhook paths can
|
|
7
|
+
* benefit), and formats it as a prompt appendix.
|
|
8
|
+
*
|
|
9
|
+
* @see https://linear.app/docs/agents-in-linear
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface GuidanceContext {
|
|
17
|
+
/** The raw guidance string (workspace + team merged by Linear) */
|
|
18
|
+
guidance: string | null;
|
|
19
|
+
/** Where the guidance came from */
|
|
20
|
+
source: "webhook" | "promptContext" | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Extraction
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract guidance from an AgentSessionEvent webhook payload.
|
|
29
|
+
* Priority: payload.guidance (string) > parsed from payload.promptContext > null.
|
|
30
|
+
*/
|
|
31
|
+
export function extractGuidance(payload: Record<string, unknown>): GuidanceContext {
|
|
32
|
+
// 1. Top-level guidance field (clean string from Linear)
|
|
33
|
+
const guidance = payload.guidance;
|
|
34
|
+
if (typeof guidance === "string" && guidance.trim()) {
|
|
35
|
+
return { guidance: guidance.trim(), source: "webhook" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Try parsing from promptContext
|
|
39
|
+
const pc = payload.promptContext;
|
|
40
|
+
if (typeof pc === "string" && pc.trim()) {
|
|
41
|
+
const extracted = extractGuidanceFromPromptContext(pc);
|
|
42
|
+
if (extracted) {
|
|
43
|
+
return { guidance: extracted, source: "promptContext" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { guidance: null, source: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Best-effort parse guidance from a promptContext string.
|
|
52
|
+
* Linear's promptContext format may include guidance as XML-style tags
|
|
53
|
+
* or markdown-headed sections.
|
|
54
|
+
*/
|
|
55
|
+
export function extractGuidanceFromPromptContext(promptContext: string): string | null {
|
|
56
|
+
// Pattern 1: XML-style tags (e.g. <guidance>...</guidance>)
|
|
57
|
+
const xmlMatch = promptContext.match(/<guidance>([\s\S]*?)<\/guidance>/i);
|
|
58
|
+
if (xmlMatch?.[1]?.trim()) return xmlMatch[1].trim();
|
|
59
|
+
|
|
60
|
+
// Pattern 2: Markdown heading "## Guidance" or "# Guidance"
|
|
61
|
+
const mdMatch = promptContext.match(/#{1,3}\s*[Gg]uidance\s*\n([\s\S]*?)(?=\n#{1,3}\s|$)/);
|
|
62
|
+
if (mdMatch?.[1]?.trim()) return mdMatch[1].trim();
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Cache (per-team, 24h TTL, 50-entry cap)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
interface CacheEntry {
|
|
72
|
+
guidance: string;
|
|
73
|
+
cachedAt: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const guidanceCache = new Map<string, CacheEntry>();
|
|
77
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
78
|
+
const CACHE_MAX_ENTRIES = 50;
|
|
79
|
+
|
|
80
|
+
/** Cache guidance by team ID. */
|
|
81
|
+
export function cacheGuidanceForTeam(teamId: string, guidance: string): void {
|
|
82
|
+
// Evict oldest if at capacity
|
|
83
|
+
if (guidanceCache.size >= CACHE_MAX_ENTRIES && !guidanceCache.has(teamId)) {
|
|
84
|
+
const oldest = guidanceCache.keys().next().value;
|
|
85
|
+
if (oldest !== undefined) guidanceCache.delete(oldest);
|
|
86
|
+
}
|
|
87
|
+
guidanceCache.set(teamId, { guidance, cachedAt: Date.now() });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Look up cached guidance for a team. Returns null on miss or expiry. */
|
|
91
|
+
export function getCachedGuidanceForTeam(teamId: string): string | null {
|
|
92
|
+
const entry = guidanceCache.get(teamId);
|
|
93
|
+
if (!entry) return null;
|
|
94
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
|
95
|
+
guidanceCache.delete(teamId);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return entry.guidance;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Test-only: reset the guidance cache. */
|
|
102
|
+
export function _resetGuidanceCacheForTesting(): void {
|
|
103
|
+
guidanceCache.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Formatting
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
const MAX_GUIDANCE_CHARS = 2000;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format guidance as a prompt appendix.
|
|
114
|
+
* Returns empty string if guidance is null/empty.
|
|
115
|
+
* Truncates at 2000 chars to protect token budget.
|
|
116
|
+
*/
|
|
117
|
+
export function formatGuidanceAppendix(guidance: string | null): string {
|
|
118
|
+
if (!guidance?.trim()) return "";
|
|
119
|
+
const trimmed = guidance.trim().slice(0, MAX_GUIDANCE_CHARS);
|
|
120
|
+
return [
|
|
121
|
+
`---`,
|
|
122
|
+
`## Additional Guidance (from Linear workspace/team settings)`,
|
|
123
|
+
trimmed,
|
|
124
|
+
`---`,
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Config toggle
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve whether guidance is enabled for a given team.
|
|
134
|
+
* Resolution: teamGuidanceOverrides[teamId] ?? enableGuidance ?? true
|
|
135
|
+
*/
|
|
136
|
+
export function isGuidanceEnabled(
|
|
137
|
+
pluginConfig: Record<string, unknown> | undefined,
|
|
138
|
+
teamId: string | undefined,
|
|
139
|
+
): boolean {
|
|
140
|
+
if (!pluginConfig) return true;
|
|
141
|
+
|
|
142
|
+
// Check per-team override first
|
|
143
|
+
if (teamId) {
|
|
144
|
+
const overrides = pluginConfig.teamGuidanceOverrides;
|
|
145
|
+
if (overrides && typeof overrides === "object" && !Array.isArray(overrides)) {
|
|
146
|
+
const teamOverride = (overrides as Record<string, unknown>)[teamId];
|
|
147
|
+
if (typeof teamOverride === "boolean") return teamOverride;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fall back to workspace-level toggle
|
|
152
|
+
const enabled = pluginConfig.enableGuidance;
|
|
153
|
+
if (typeof enabled === "boolean") return enabled;
|
|
154
|
+
|
|
155
|
+
return true; // default on
|
|
156
|
+
}
|
package/src/pipeline/pipeline.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
19
19
|
import type { LinearAgentApi, ActivityContent } from "../api/linear-api.js";
|
|
20
20
|
import { runAgent } from "../agent/agent.js";
|
|
21
21
|
import { setActiveSession, clearActiveSession } from "./active-session.js";
|
|
22
|
+
import { getCachedGuidanceForTeam, isGuidanceEnabled } from "./guidance.js";
|
|
22
23
|
import {
|
|
23
24
|
type Tier,
|
|
24
25
|
type DispatchStatus,
|
|
@@ -191,7 +192,7 @@ export interface IssueContext {
|
|
|
191
192
|
export function buildWorkerTask(
|
|
192
193
|
issue: IssueContext,
|
|
193
194
|
worktreePath: string,
|
|
194
|
-
opts?: { attempt?: number; gaps?: string[]; pluginConfig?: Record<string, unknown
|
|
195
|
+
opts?: { attempt?: number; gaps?: string[]; pluginConfig?: Record<string, unknown>; guidance?: string },
|
|
195
196
|
): { system: string; task: string } {
|
|
196
197
|
const prompts = loadPrompts(opts?.pluginConfig, worktreePath);
|
|
197
198
|
const vars: Record<string, string> = {
|
|
@@ -202,6 +203,9 @@ export function buildWorkerTask(
|
|
|
202
203
|
tier: "",
|
|
203
204
|
attempt: String(opts?.attempt ?? 0),
|
|
204
205
|
gaps: opts?.gaps?.length ? "- " + opts.gaps.join("\n- ") : "",
|
|
206
|
+
guidance: opts?.guidance
|
|
207
|
+
? `\n---\n## Additional Guidance (from Linear workspace/team settings)\n${opts.guidance.slice(0, 2000)}\n---`
|
|
208
|
+
: "",
|
|
205
209
|
};
|
|
206
210
|
|
|
207
211
|
let task = renderTemplate(prompts.worker.task, vars);
|
|
@@ -222,6 +226,7 @@ export function buildAuditTask(
|
|
|
222
226
|
issue: IssueContext,
|
|
223
227
|
worktreePath: string,
|
|
224
228
|
pluginConfig?: Record<string, unknown>,
|
|
229
|
+
opts?: { guidance?: string },
|
|
225
230
|
): { system: string; task: string } {
|
|
226
231
|
const prompts = loadPrompts(pluginConfig, worktreePath);
|
|
227
232
|
const vars: Record<string, string> = {
|
|
@@ -232,6 +237,9 @@ export function buildAuditTask(
|
|
|
232
237
|
tier: "",
|
|
233
238
|
attempt: "0",
|
|
234
239
|
gaps: "",
|
|
240
|
+
guidance: opts?.guidance
|
|
241
|
+
? `\n---\n## Additional Guidance (from Linear workspace/team settings)\n${opts.guidance.slice(0, 2000)}\n---`
|
|
242
|
+
: "",
|
|
235
243
|
};
|
|
236
244
|
|
|
237
245
|
return {
|
|
@@ -350,7 +358,13 @@ export async function triggerAudit(
|
|
|
350
358
|
? dispatch.worktrees.map(w => `${w.repoName}: ${w.path}`).join("\n")
|
|
351
359
|
: dispatch.worktreePath;
|
|
352
360
|
|
|
353
|
-
|
|
361
|
+
// Look up cached guidance for audit
|
|
362
|
+
const auditTeamId = issueDetails?.team?.id;
|
|
363
|
+
const auditGuidance = (auditTeamId && isGuidanceEnabled(pluginConfig, auditTeamId))
|
|
364
|
+
? getCachedGuidanceForTeam(auditTeamId) ?? undefined
|
|
365
|
+
: undefined;
|
|
366
|
+
|
|
367
|
+
const auditPrompt = buildAuditTask(issue, effectiveAuditPath, pluginConfig, { guidance: auditGuidance });
|
|
354
368
|
|
|
355
369
|
// Set Linear label
|
|
356
370
|
await linearApi.emitActivity(dispatch.agentSessionId ?? "", {
|
|
@@ -744,10 +758,17 @@ export async function spawnWorker(
|
|
|
744
758
|
? dispatch.worktrees.map(w => `${w.repoName}: ${w.path}`).join("\n")
|
|
745
759
|
: dispatch.worktreePath;
|
|
746
760
|
|
|
761
|
+
// Look up cached guidance for the issue's team
|
|
762
|
+
const workerTeamId = issueDetails?.team?.id;
|
|
763
|
+
const workerGuidance = (workerTeamId && isGuidanceEnabled(pluginConfig, workerTeamId))
|
|
764
|
+
? getCachedGuidanceForTeam(workerTeamId) ?? undefined
|
|
765
|
+
: undefined;
|
|
766
|
+
|
|
747
767
|
const workerPrompt = buildWorkerTask(issue, effectiveWorkerPath, {
|
|
748
768
|
attempt: dispatch.attempt,
|
|
749
769
|
gaps: opts?.gaps,
|
|
750
770
|
pluginConfig,
|
|
771
|
+
guidance: workerGuidance,
|
|
751
772
|
});
|
|
752
773
|
|
|
753
774
|
const workerSessionId = `linear-worker-${dispatch.issueIdentifier}-${dispatch.attempt}`;
|