@calltelemetry/openclaw-linear 0.9.15 → 0.9.16
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 +104 -48
- package/index.ts +7 -0
- package/openclaw.plugin.json +44 -2
- package/package.json +1 -1
- package/prompts.yaml +3 -1
- package/src/__test__/fixtures/recorded-sub-issue-flow.ts +37 -50
- package/src/agent/agent.test.ts +1 -1
- package/src/agent/agent.ts +39 -6
- package/src/api/linear-api.test.ts +188 -1
- package/src/api/linear-api.ts +114 -5
- package/src/infra/multi-repo.test.ts +127 -1
- package/src/infra/multi-repo.ts +74 -6
- package/src/infra/tmux-runner.ts +599 -0
- package/src/infra/tmux.ts +158 -0
- package/src/infra/token-refresh-timer.ts +44 -0
- package/src/pipeline/active-session.ts +19 -1
- package/src/pipeline/artifacts.ts +42 -0
- package/src/pipeline/dispatch-state.ts +3 -0
- package/src/pipeline/guidance.test.ts +53 -0
- package/src/pipeline/guidance.ts +38 -0
- package/src/pipeline/memory-search.ts +40 -0
- package/src/pipeline/pipeline.ts +184 -17
- package/src/pipeline/retro.ts +231 -0
- package/src/pipeline/webhook.test.ts +1 -1
- package/src/pipeline/webhook.ts +271 -30
- package/src/tools/claude-tool.ts +68 -10
- package/src/tools/cli-shared.ts +50 -2
- package/src/tools/code-tool.ts +230 -150
- package/src/tools/codex-tool.ts +61 -9
- package/src/tools/gemini-tool.ts +61 -10
- package/src/tools/steering-tools.ts +176 -0
- package/src/tools/tools.test.ts +47 -15
- package/src/tools/tools.ts +17 -4
- package/src/__test__/smoke-linear-api.test.ts +0 -847
package/README.md
CHANGED
|
@@ -23,20 +23,25 @@ Connect Linear to AI agents. Issues get triaged, implemented, and audited — au
|
|
|
23
23
|
- [x] Linear OAuth app webhook (AgentSessionEvent created/prompted)
|
|
24
24
|
- [x] Linear API integration (issues, comments, labels, state transitions)
|
|
25
25
|
- [x] Agent routing (`@mentions`, natural language intent classifier)
|
|
26
|
+
- [x] Intent gate + scope enforcement (all 4 webhook paths, 3-layer defense)
|
|
26
27
|
- [x] Auto-triage (story points, labels, priority — read-only)
|
|
27
28
|
- [x] Complexity-tier dispatch (small → Haiku, medium → Sonnet, high → Opus)
|
|
28
29
|
- [x] Isolated git worktrees per dispatch
|
|
29
30
|
- [x] Worker → Auditor pipeline (hard-enforced, not LLM-mediated)
|
|
30
31
|
- [x] Audit rework loop (gaps fed back, automatic retry)
|
|
31
32
|
- [x] Watchdog timeout + escalation
|
|
32
|
-
- [x] Webhook deduplication (60s sliding window
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
33
|
+
- [x] Webhook deduplication (60s sliding window + `activeRuns` race guard on all paths)
|
|
34
|
+
- [x] Webhook auto-provisioning (`webhooks setup` CLI, `doctor --fix`)
|
|
35
|
+
- [x] Multi-repo worktree support
|
|
36
|
+
- [x] Project planner (interview → user stories → sub-issues → DAG dispatch)
|
|
37
|
+
- [x] Cross-model plan review (Claude ↔ Codex ↔ Gemini)
|
|
36
38
|
- [x] Issue closure with summary report
|
|
37
|
-
- [
|
|
39
|
+
- [x] Sub-issue decomposition (orchestrator-level only)
|
|
38
40
|
- [x] `spawn_agent` / `ask_agent` sub-agent tools
|
|
39
|
-
- [x] CI + coverage badges (
|
|
41
|
+
- [x] CI + coverage badges (1170+ tests, Codecov integration)
|
|
42
|
+
- [x] Setup wizard (`openclaw openclaw-linear setup`) + `doctor --fix` auto-repair
|
|
43
|
+
- [x] Project context auto-detection (repo, framework, build/test commands → worker/audit prompts)
|
|
44
|
+
- [x] Per-backend CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`) with Linear session activity streaming
|
|
40
45
|
- [ ] **Worktree → PR merge** — `createPullRequest()` exists but is not wired into the pipeline. After audit pass, commits sit on a `codex/{identifier}` branch. You create the PR manually.
|
|
41
46
|
- [ ] **Sub-agent worktree sharing** — Sub-agents spawned via `spawn_agent`/`ask_agent` do not inherit the parent worktree. They run in their own session without code access.
|
|
42
47
|
- [ ] **Parallel worktree conflict resolution** — DAG dispatch runs up to 3 issues concurrently in separate worktrees, but there's no merge conflict detection across them.
|
|
@@ -107,12 +112,13 @@ The end result: you work in Linear. You create issues, assign them, comment in p
|
|
|
107
112
|
### Multi-Agent & Routing
|
|
108
113
|
|
|
109
114
|
- **Named agents** — Define agents with different roles and expertise. Route work by `@mention` or natural language ("hey kaylee look at this").
|
|
110
|
-
- **Intent classification** — An LLM classifier (~300 tokens, ~2s)
|
|
115
|
+
- **Intent classification** — An LLM classifier (~300 tokens, ~2s) runs on every user request across all webhook paths — not just comments. Classifies intent and gates work requests on untriaged issues. Regex fallback if the classifier fails.
|
|
116
|
+
- **Scope enforcement** — Three-layer defense prevents agents from building code on issues that haven't been planned. Intent gate blocks `request_work` pre-dispatch; prompt-level constraints limit CLI tools to planning-only; a `before_tool_call` hook prepends hard constraints to worker prompts.
|
|
111
117
|
- **One-time detour** — `@mention` a different agent in a session and it handles that single interaction. The session stays with the original agent.
|
|
112
118
|
|
|
113
119
|
### Multi-Backend & Multi-Repo
|
|
114
120
|
|
|
115
|
-
- **Three coding backends** — Codex (OpenAI), Claude (Anthropic), Gemini (Google). Configurable globally or per-agent.
|
|
121
|
+
- **Three coding backends** — Codex (OpenAI), Claude (Anthropic), Gemini (Google). Configurable globally or per-agent. Each backend registers as a dedicated tool (`cli_codex`, `cli_claude`, `cli_gemini`) so agents and Linear session UI show exactly which backend is running. Per-agent overrides let you assign different backends to different team members.
|
|
116
122
|
- **Multi-repo dispatch** — Tag an issue with `<!-- repos: api, frontend -->` and the worker gets isolated worktrees for each repo. One issue, multiple codebases, one agent session.
|
|
117
123
|
|
|
118
124
|
### Operations
|
|
@@ -698,24 +704,53 @@ flowchart LR
|
|
|
698
704
|
| "hey kaylee can you look at this?" | Routes to Kaylee (no `@` needed) |
|
|
699
705
|
| "@mal close this issue" | Routes to Mal (one-time detour) and closes the issue |
|
|
700
706
|
| "what can I do here?" | Default agent responds (not silently dropped) |
|
|
701
|
-
| "fix the search bug" | Default agent dispatches work |
|
|
707
|
+
| "fix the search bug" | Default agent dispatches work (if issue is In Progress) |
|
|
708
|
+
| "build me a stock trading app" (on Backlog issue) | Blocked — agent explains the issue needs scoping first |
|
|
702
709
|
| "close this" / "mark as done" / "this is resolved" | Generates closure report, transitions issue to completed |
|
|
703
710
|
|
|
704
|
-
|
|
711
|
+
Intent classification runs on **all webhook paths** — not just `Comment.create`. `AgentSessionEvent.created`, `AgentSessionEvent.prompted`, and `@mention` fast paths all classify intent and enforce scope.
|
|
705
712
|
|
|
706
713
|
> **Tip:** Configure `classifierAgentId` to point to a small/fast model agent (like Haiku) for low-latency, low-cost intent classification. The classifier only needs ~300 tokens per call.
|
|
707
714
|
|
|
715
|
+
### Scope Enforcement — Work Request Gate
|
|
716
|
+
|
|
717
|
+
When a user's intent is classified as `request_work`, the plugin checks the issue's workflow state before dispatching. Issues that haven't reached **In Progress** get a polite rejection instead of an agent run:
|
|
718
|
+
|
|
719
|
+
```
|
|
720
|
+
This issue (ENG-123) is in Backlog — it needs planning and scoping before implementation.
|
|
721
|
+
|
|
722
|
+
To move forward:
|
|
723
|
+
1. Update the issue description with requirements and acceptance criteria
|
|
724
|
+
2. Move the issue to In Progress
|
|
725
|
+
3. Then ask me to implement it
|
|
726
|
+
|
|
727
|
+
I can help you scope and plan — just ask questions or discuss the approach.
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
This prevents the most common failure mode: an agent receiving a casual comment like "build X" on an unscoped issue and immediately spinning up a CLI tool to build something that was never planned.
|
|
731
|
+
|
|
732
|
+
**What's allowed on untriaged issues:**
|
|
733
|
+
- Questions, discussion, scope refinement
|
|
734
|
+
- Planning (`plan_start`, `plan_continue`, `plan_finalize`)
|
|
735
|
+
- Agent routing (`@mention` or natural language)
|
|
736
|
+
- Issue closure
|
|
737
|
+
- `cli_codex`/`cli_claude`/`cli_gemini` in **planning mode only** — workers can explore code and write plan files but cannot create, modify, or delete source code
|
|
738
|
+
|
|
739
|
+
**What's blocked on untriaged issues:**
|
|
740
|
+
- `request_work` intent — the gate returns a rejection message before any agent runs
|
|
741
|
+
- Full CLI tool implementation — even if the orchestrator LLM ignores prompt rules, a `before_tool_call` hook prepends hard planning-only constraints to the worker's prompt
|
|
742
|
+
|
|
708
743
|
### Agent Routing
|
|
709
744
|
|
|
710
745
|
The plugin supports a multi-agent team where one agent is the default (`isDefault: true` in agent profiles) and others are routed to on demand. Routing works across all webhook paths:
|
|
711
746
|
|
|
712
|
-
| Webhook Path | How agent is selected |
|
|
713
|
-
|
|
714
|
-
| `Comment.create` | `@mention`
|
|
715
|
-
| `AgentSessionEvent.created` | Scans user's message for `@mention` aliases → routes to mentioned agent
|
|
716
|
-
| `AgentSessionEvent.prompted` | Same as `created` — scans follow-up message for `@mention` → one-time detour
|
|
717
|
-
| `Issue.update` (assignment) | Always dispatches to default agent. |
|
|
718
|
-
| `Issue.create` (triage) | Always dispatches to default agent. |
|
|
747
|
+
| Webhook Path | How agent is selected | Scope gate |
|
|
748
|
+
|---|---|---|
|
|
749
|
+
| `Comment.create` | `@mention` → specific agent (with intent gate). No mention → intent classifier may detect agent name ("hey kaylee") → `ask_agent` intent. Otherwise → default agent. | `request_work` blocked on untriaged |
|
|
750
|
+
| `AgentSessionEvent.created` | Scans user's message for `@mention` aliases → routes to mentioned agent. No mention → default agent. | `request_work` blocked on untriaged |
|
|
751
|
+
| `AgentSessionEvent.prompted` | Same as `created` — scans follow-up message for `@mention` → one-time detour. No mention → default agent. | `request_work` blocked on untriaged |
|
|
752
|
+
| `Issue.update` (assignment) | Always dispatches to default agent. | Full dispatch pipeline |
|
|
753
|
+
| `Issue.create` (triage) | Always dispatches to default agent. | Read-only triage |
|
|
719
754
|
|
|
720
755
|
**One-time detour:** When you `@mention` an agent in a session that belongs to a different default agent, the mentioned agent handles that single interaction. The session itself stays owned by whoever created it — subsequent messages without `@mentions` go back to the default. This lets you ask a specific agent for help without permanently switching context.
|
|
721
756
|
|
|
@@ -854,7 +889,7 @@ Add settings under the plugin entry in `openclaw.json`:
|
|
|
854
889
|
| `notifications` | object | — | Notification targets (see [Notifications](#notifications)) |
|
|
855
890
|
| `inactivitySec` | number | `120` | Kill agent if silent this long |
|
|
856
891
|
| `maxTotalSec` | number | `7200` | Max total agent session time |
|
|
857
|
-
| `toolTimeoutSec` | number | `600` | Max single
|
|
892
|
+
| `toolTimeoutSec` | number | `600` | Max single CLI tool time |
|
|
858
893
|
| `enableGuidance` | boolean | `true` | Inject Linear workspace/team guidance into agent prompts |
|
|
859
894
|
| `teamGuidanceOverrides` | object | — | Per-team guidance toggle. Key = team ID, value = boolean. Unset teams inherit `enableGuidance`. |
|
|
860
895
|
| `claudeApiKey` | string | — | Anthropic API key for Claude CLI (passed as `ANTHROPIC_API_KEY` env var). Required if using Claude backend. |
|
|
@@ -921,7 +956,7 @@ Create `coding-tools.json` in the plugin root to configure which CLI backend age
|
|
|
921
956
|
}
|
|
922
957
|
```
|
|
923
958
|
|
|
924
|
-
|
|
959
|
+
Each backend registers as a dedicated tool — `cli_codex`, `cli_claude`, or `cli_gemini` — so agents and Linear's session UI show exactly which backend is running. The agent's prompt references the correct tool name automatically. Resolution order for which tool is exposed: per-agent override (`agentCodingTools`) > global default (`codingTool`) > `"codex"`.
|
|
925
960
|
|
|
926
961
|
#### Claude API Key
|
|
927
962
|
|
|
@@ -1095,7 +1130,7 @@ rework:
|
|
|
1095
1130
|
| `{{reviewModel}}` | Name of cross-model reviewer (planner review) |
|
|
1096
1131
|
| `{{crossModelFeedback}}` | Review recommendations (planner review) |
|
|
1097
1132
|
| `{{guidance}}` | Linear workspace/team guidance (if available, empty string otherwise) |
|
|
1098
|
-
| `{{projectContext}}` |
|
|
1133
|
+
| `{{projectContext}}` | Auto-detected project context (repo paths, framework, build commands, test commands) injected into worker and audit prompts. |
|
|
1099
1134
|
|
|
1100
1135
|
### CLI
|
|
1101
1136
|
|
|
@@ -1256,7 +1291,7 @@ If an agent goes silent (LLM timeout, API hang, CLI lockup), the watchdog handle
|
|
|
1256
1291
|
|---|---|---|
|
|
1257
1292
|
| `inactivitySec` | 120s | Kill if no output for this long |
|
|
1258
1293
|
| `maxTotalSec` | 7200s (2 hrs) | Hard ceiling on total session time |
|
|
1259
|
-
| `toolTimeoutSec` | 600s (10 min) | Max time for a single
|
|
1294
|
+
| `toolTimeoutSec` | 600s (10 min) | Max time for a single CLI tool call |
|
|
1260
1295
|
|
|
1261
1296
|
Configure per-agent in `agent-profiles.json` or globally in plugin config.
|
|
1262
1297
|
|
|
@@ -1266,9 +1301,9 @@ Configure per-agent in `agent-profiles.json` or globally in plugin config.
|
|
|
1266
1301
|
|
|
1267
1302
|
Every agent session gets these registered tools. They're available as native tool calls — no CLI parsing, no shell execution, no flag guessing.
|
|
1268
1303
|
|
|
1269
|
-
### `
|
|
1304
|
+
### `cli_codex` / `cli_claude` / `cli_gemini` — Coding backend tools
|
|
1270
1305
|
|
|
1271
|
-
|
|
1306
|
+
Three per-backend tools that send tasks to their respective coding CLIs. Each agent sees only the tool matching its configured backend (e.g., an agent configured for `codex` gets `cli_codex`). The tool name is visible in Linear's agent session UI, so you always know which backend is running. The agent writes the prompt; the plugin handles worktree setup, session activity streaming, and output capture.
|
|
1272
1307
|
|
|
1273
1308
|
### `linear_issues` — Native Linear API
|
|
1274
1309
|
|
|
@@ -1289,7 +1324,7 @@ Agents call `linear_issues` with typed JSON parameters. The tool wraps the Linea
|
|
|
1289
1324
|
|
|
1290
1325
|
Delegate work to other crew agents. `spawn_agent` is fire-and-forget (parallel), `ask_agent` waits for a reply (synchronous). Disabled with `enableOrchestration: false`.
|
|
1291
1326
|
|
|
1292
|
-
Sub-agents run in their own context — they do **not** share the parent's worktree or get
|
|
1327
|
+
Sub-agents run in their own context — they do **not** share the parent's worktree or get CLI tool access. They're useful for reasoning, research, and coordination (e.g., "ask Inara how to phrase this error message") but cannot directly modify code. To give a sub-agent code context, include the relevant snippets in the task message.
|
|
1293
1328
|
|
|
1294
1329
|
### `dispatch_history` — Recent dispatch context
|
|
1295
1330
|
|
|
@@ -1299,10 +1334,10 @@ Returns recent dispatch activity. Agents use this for situational awareness when
|
|
|
1299
1334
|
|
|
1300
1335
|
Tool access varies by context. Orchestrators get the full toolset; workers and auditors are restricted:
|
|
1301
1336
|
|
|
1302
|
-
| Context | `linear_issues` | `
|
|
1337
|
+
| Context | `linear_issues` | `cli_*` | `spawn_agent` / `ask_agent` | Filesystem |
|
|
1303
1338
|
|---|---|---|---|---|
|
|
1304
|
-
| Orchestrator (triaged issue) | Full (read, create, update, comment) | Yes | Yes | Read + write |
|
|
1305
|
-
| Orchestrator (untriaged issue) | Read only |
|
|
1339
|
+
| Orchestrator (triaged issue) | Full (read, create, update, comment) | Yes (backend-specific tool) | Yes | Read + write |
|
|
1340
|
+
| Orchestrator (untriaged issue) | Read only | Planning only | Yes | Read + write |
|
|
1306
1341
|
| Worker | None | None | None | Read + write |
|
|
1307
1342
|
| Auditor | Prompt-constrained (has tool, instructed to verify only) | None | None | Read only (by prompt) |
|
|
1308
1343
|
| Sub-agent (spawn/ask) | None | None | Yes (can chain) | Inherited from parent |
|
|
@@ -1311,7 +1346,7 @@ Tool access varies by context. Orchestrators get the full toolset; workers and a
|
|
|
1311
1346
|
|
|
1312
1347
|
**Auditors** have access to `linear_issues` (the tool is registered) but are instructed via prompt to verify only — they return a JSON verdict, not code or issue mutations. Write access is not enforced at the tool level.
|
|
1313
1348
|
|
|
1314
|
-
**Sub-agents** spawned via `spawn_agent`/`ask_agent` run in their own session with no worktree access and no
|
|
1349
|
+
**Sub-agents** spawned via `spawn_agent`/`ask_agent` run in their own session with no worktree access and no CLI tools. They're information workers — useful for reasoning and coordination, not code execution.
|
|
1315
1350
|
|
|
1316
1351
|
---
|
|
1317
1352
|
|
|
@@ -1372,9 +1407,9 @@ The handler dispatches by `type + action`:
|
|
|
1372
1407
|
```mermaid
|
|
1373
1408
|
flowchart TD
|
|
1374
1409
|
A["POST /linear/webhook"] --> B{"Event Type"}
|
|
1375
|
-
B --> C["AgentSessionEvent.created<br/>→ dedup → scan @mentions → run agent"]
|
|
1376
|
-
B --> D["AgentSessionEvent.prompted<br/>→ dedup → scan @mentions → resume agent"]
|
|
1377
|
-
B --> E["Comment.create<br/>→ filter self → dedup → intent classify → route"]
|
|
1410
|
+
B --> C["AgentSessionEvent.created<br/>→ dedup → scan @mentions → intent classify → scope gate → run agent"]
|
|
1411
|
+
B --> D["AgentSessionEvent.prompted<br/>→ dedup → scan @mentions → intent classify → scope gate → resume agent"]
|
|
1412
|
+
B --> E["Comment.create<br/>→ filter self → dedup → intent classify → scope gate → route"]
|
|
1378
1413
|
B --> F["Issue.update<br/>→ check assignment → dispatch"]
|
|
1379
1414
|
B --> G["Issue.create<br/>→ triage (estimate, labels, priority)"]
|
|
1380
1415
|
B --> H["AppUserNotification<br/>→ discarded (duplicates workspace events)"]
|
|
@@ -1382,26 +1417,39 @@ flowchart TD
|
|
|
1382
1417
|
|
|
1383
1418
|
### Intent Classification
|
|
1384
1419
|
|
|
1385
|
-
|
|
1420
|
+
Every user request — across all 4 webhook dispatch paths — is classified through a two-tier intent system before the agent runs:
|
|
1386
1421
|
|
|
1387
|
-
1. **LLM classifier** (~300 tokens, ~2-5s) — a small/fast model parses the
|
|
1422
|
+
1. **LLM classifier** (~300 tokens, ~2-5s) — a small/fast model parses the message and returns structured JSON with intent + reasoning
|
|
1388
1423
|
2. **Regex fallback** — if the LLM call fails or times out, static patterns catch common cases
|
|
1389
1424
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
|
1393
|
-
|
|
1394
|
-
| `
|
|
1395
|
-
| `
|
|
1396
|
-
| `
|
|
1397
|
-
| `
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1425
|
+
**Where it runs:**
|
|
1426
|
+
|
|
1427
|
+
| Webhook Path | Classification Point |
|
|
1428
|
+
|---|---|
|
|
1429
|
+
| `Comment.create` (no @mention) | Before routing by intent |
|
|
1430
|
+
| `Comment.create` (@mention fast path) | After resolving agent, before dispatch |
|
|
1431
|
+
| `AgentSessionEvent.created` | After enriching issue, before building prompt |
|
|
1432
|
+
| `AgentSessionEvent.prompted` | After enriching issue, before building follow-up prompt |
|
|
1433
|
+
|
|
1434
|
+
**Intents:**
|
|
1435
|
+
|
|
1436
|
+
| Intent | Trigger | Handler | Blocked on untriaged? |
|
|
1437
|
+
|---|---|---|---|
|
|
1438
|
+
| `plan_start` | "let's plan the features" | Start planner interview session | No |
|
|
1439
|
+
| `plan_finalize` | "looks good, ship it" | Run plan audit + cross-model review | No |
|
|
1440
|
+
| `plan_abandon` | "cancel planning" | End planning session | No |
|
|
1441
|
+
| `plan_continue` | Any message during active planning | Continue planner conversation | No |
|
|
1442
|
+
| `ask_agent` | "@kaylee" or "hey kaylee" | Route to specific agent by name | No |
|
|
1443
|
+
| `request_work` | "fix the search bug" | Dispatch to default agent | **Yes** |
|
|
1444
|
+
| `question` | "what's the status?" | Agent answers without code changes | No |
|
|
1445
|
+
| `close_issue` | "close this" / "mark as done" | Generate closure report + transition state | No |
|
|
1446
|
+
| `general` | Noise, automated messages | Silently dropped | No |
|
|
1447
|
+
|
|
1448
|
+
The `request_work` intent is the only one gated by issue state. When the issue is not in **In Progress** (i.e., `stateType !== "started"`), the gate returns a rejection message explaining what the user needs to do. All other intents are allowed regardless of issue state — you can always plan, ask questions, discuss scope, and close issues.
|
|
1401
1449
|
|
|
1402
1450
|
### Hook Lifecycle
|
|
1403
1451
|
|
|
1404
|
-
The plugin registers
|
|
1452
|
+
The plugin registers four lifecycle hooks via `api.on()` in `index.ts`:
|
|
1405
1453
|
|
|
1406
1454
|
**`agent_end`** — Dispatch pipeline state machine. When a sub-agent (worker or auditor) finishes:
|
|
1407
1455
|
- Looks up the session key in dispatch state to find the active dispatch
|
|
@@ -1413,6 +1461,13 @@ The plugin registers three lifecycle hooks via `api.on()` in `index.ts`:
|
|
|
1413
1461
|
- Reads dispatch state and finds up to 3 active dispatches
|
|
1414
1462
|
- Prepends a `<dispatch-history>` block so the agent has situational awareness of concurrent work
|
|
1415
1463
|
|
|
1464
|
+
**`before_tool_call`** — Planning-only enforcement for CLI tools (`cli_codex`, `cli_claude`, `cli_gemini`). When the active issue is not in "started" state:
|
|
1465
|
+
- Fetches the issue's current workflow state from the Linear API
|
|
1466
|
+
- If the issue is in Triage, Todo, Backlog, or any non-started state, prepends hard constraints to the worker's prompt:
|
|
1467
|
+
- Workers may read/explore files and write plan files (PLAN.md, design docs)
|
|
1468
|
+
- Workers must NOT create/modify/delete source code, run deployments, or make system changes
|
|
1469
|
+
- This is the deepest layer of scope enforcement — even if the orchestrator LLM ignores prompt-level scope rules and calls a CLI tool anyway, the worker receives constraints that prevent implementation
|
|
1470
|
+
|
|
1416
1471
|
**`message_sending`** — Narration guard. Catches short (~250 char) "Let me explore..." responses where the agent narrates intent without actually calling tools:
|
|
1417
1472
|
- Appends a warning: "Agent acknowledged but may not have completed the task"
|
|
1418
1473
|
- Prevents users from thinking the agent did something when it only said it would
|
|
@@ -1711,7 +1766,8 @@ openclaw openclaw-linear prompts show # View the active prompts
|
|
|
1711
1766
|
## CLI Reference
|
|
1712
1767
|
|
|
1713
1768
|
```bash
|
|
1714
|
-
#
|
|
1769
|
+
# Setup & auth
|
|
1770
|
+
openclaw openclaw-linear setup # Guided first-time setup (profiles, auth, webhook, doctor)
|
|
1715
1771
|
openclaw openclaw-linear auth # Run OAuth flow
|
|
1716
1772
|
openclaw openclaw-linear status # Check connection
|
|
1717
1773
|
|
|
@@ -1775,8 +1831,8 @@ journalctl --user -u openclaw-gateway -f # Watch live logs
|
|
|
1775
1831
|
|---|---|
|
|
1776
1832
|
| Agent goes silent | Watchdog auto-kills after `inactivitySec` and retries. Check logs for `Watchdog KILL`. |
|
|
1777
1833
|
| Dispatch stuck after watchdog | Both retries failed. Check `.claw/log.jsonl`. Re-assign issue to restart. |
|
|
1778
|
-
| `
|
|
1779
|
-
| `
|
|
1834
|
+
| `cli_*` uses wrong backend | Check `coding-tools.json` — per-agent override > global default. Run `code-run doctor` to see routing. |
|
|
1835
|
+
| `cli_*` fails at runtime | Run `openclaw openclaw-linear code-run doctor` — checks binary, API key, and live callability for each backend. |
|
|
1780
1836
|
| Webhook events not arriving | Run `openclaw openclaw-linear webhooks setup` to auto-provision. Both webhooks must point to `/linear/webhook`. Check tunnel is running. |
|
|
1781
1837
|
| Tunnel down / webhooks silently failing | `systemctl status cloudflared` (or `systemctl --user status cloudflared`). Restart with `systemctl restart cloudflared`. Test: `curl -s -X POST https://your-domain.com/linear/webhook -H 'Content-Type: application/json' -d '{"type":"test"}'` — should return `"ok"`. |
|
|
1782
1838
|
| OAuth token expired | Auto-refreshes. If stuck, re-run `openclaw openclaw-linear auth` and restart. |
|
package/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { createPlannerTools } from "./src/tools/planner-tools.js";
|
|
|
18
18
|
import { registerDispatchCommands } from "./src/infra/commands.js";
|
|
19
19
|
import { createDispatchHistoryTool } from "./src/tools/dispatch-history-tool.js";
|
|
20
20
|
import { readDispatchState as readStateForHook, listActiveDispatches as listActiveForHook } from "./src/pipeline/dispatch-state.js";
|
|
21
|
+
import { startTokenRefreshTimer, stopTokenRefreshTimer } from "./src/infra/token-refresh-timer.js";
|
|
21
22
|
|
|
22
23
|
export default function register(api: OpenClawPluginApi) {
|
|
23
24
|
const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
|
|
@@ -337,4 +338,10 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
337
338
|
api.logger.info(
|
|
338
339
|
`Linear agent extension registered (agent: ${agentId}, token: ${tokenInfo.source !== "none" ? `${tokenInfo.source}` : "missing"}, ${cliSummary}, orchestration: ${orchestration})`,
|
|
339
340
|
);
|
|
341
|
+
|
|
342
|
+
// Start proactive token refresh timer (runs immediately, then every 6h)
|
|
343
|
+
startTokenRefreshTimer(api, pluginConfig);
|
|
344
|
+
|
|
345
|
+
// Clean up timer on process exit
|
|
346
|
+
process.on("beforeExit", () => stopTokenRefreshTimer());
|
|
340
347
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -17,7 +17,46 @@
|
|
|
17
17
|
"codexBaseRepo": { "type": "string", "description": "Path to git repo for Codex worktrees", "default": "/home/claw/ai-workspace" },
|
|
18
18
|
"enableOrchestration": { "type": "boolean", "description": "Allow agents to spawn sub-agents via spawn_agent/ask_agent tools", "default": true },
|
|
19
19
|
"worktreeBaseDir": { "type": "string", "description": "Base directory for persistent git worktrees (default: ~/.openclaw/worktrees)" },
|
|
20
|
-
"repos": {
|
|
20
|
+
"repos": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "Multi-repo map — each key is a repo name, value is a path string or object with path + GitHub identity",
|
|
23
|
+
"additionalProperties": {
|
|
24
|
+
"oneOf": [
|
|
25
|
+
{ "type": "string", "description": "Filesystem path (backward compat)" },
|
|
26
|
+
{
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["path"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"path": { "type": "string", "description": "Absolute filesystem path to git repo" },
|
|
31
|
+
"github": { "type": "string", "description": "GitHub owner/repo (e.g. 'calltelemetry/cisco-cdr')" },
|
|
32
|
+
"hostname": { "type": "string", "description": "Git host (default: 'github.com')", "default": "github.com" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"teamMappings": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"description": "Map Linear team keys to repos, agents, and team-specific context",
|
|
41
|
+
"additionalProperties": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"properties": {
|
|
44
|
+
"repos": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "type": "string" },
|
|
47
|
+
"description": "Repo names (keys in 'repos' config) for this team"
|
|
48
|
+
},
|
|
49
|
+
"defaultAgent": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "OpenClaw agent ID to handle issues from this team"
|
|
52
|
+
},
|
|
53
|
+
"context": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Extra context injected into worker/audit prompts for this team"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
21
60
|
"dispatchStatePath": { "type": "string", "description": "Path to dispatch state JSON file (default: ~/.openclaw/linear-dispatch-state.json)" },
|
|
22
61
|
"planningStatePath": { "type": "string", "description": "Path to planning state JSON file (default: ~/.openclaw/linear-planning-state.json)" },
|
|
23
62
|
"notifications": {
|
|
@@ -61,7 +100,10 @@
|
|
|
61
100
|
"toolTimeoutSec": { "type": "number", "description": "Max runtime for a single code_run CLI invocation in seconds (default: 600)", "default": 600 },
|
|
62
101
|
"claudeApiKey": { "type": "string", "description": "Anthropic API key for Claude CLI backend (passed as ANTHROPIC_API_KEY env var)", "sensitive": true },
|
|
63
102
|
"enableGuidance": { "type": "boolean", "description": "Inject Linear workspace/team guidance into agent prompts (default: true)", "default": true },
|
|
64
|
-
"teamGuidanceOverrides": { "type": "object", "description": "Per-team guidance toggle. Key = Linear team ID, value = boolean. Unset teams inherit enableGuidance.", "additionalProperties": { "type": "boolean" } }
|
|
103
|
+
"teamGuidanceOverrides": { "type": "object", "description": "Per-team guidance toggle. Key = Linear team ID, value = boolean. Unset teams inherit enableGuidance.", "additionalProperties": { "type": "boolean" } },
|
|
104
|
+
"enableTmux": { "type": "boolean", "default": true, "description": "Enable tmux wrapping for code_run CLI backends (enables steering)" },
|
|
105
|
+
"enableRetro": { "type": "boolean", "default": true, "description": "Enable post-task retrospective analysis" },
|
|
106
|
+
"retroDir": { "type": "string", "description": "Override path for coding retrospective files (default: {stateDir}/shared/coding)" }
|
|
65
107
|
}
|
|
66
108
|
}
|
|
67
109
|
}
|
package/package.json
CHANGED
package/prompts.yaml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Template variables: {{identifier}}, {{title}}, {{description}},
|
|
4
4
|
# {{worktreePath}}, {{gaps}}, {{tier}}, {{attempt}}, {{guidance}},
|
|
5
|
-
# {{projectContext}}
|
|
5
|
+
# {{projectContext}}, {{teamContext}}
|
|
6
6
|
#
|
|
7
7
|
# Edit these to customize worker/audit behavior without rebuilding the plugin.
|
|
8
8
|
# Override path via `promptsPath` in plugin config.
|
|
@@ -27,6 +27,7 @@ worker:
|
|
|
27
27
|
|
|
28
28
|
Worktree: {{worktreePath}}
|
|
29
29
|
{{projectContext}}
|
|
30
|
+
{{teamContext}}
|
|
30
31
|
|
|
31
32
|
Before writing any code, read these files in the worktree root (if they exist):
|
|
32
33
|
- CLAUDE.md — project conventions, tech stack, build/test commands, architecture
|
|
@@ -70,6 +71,7 @@ audit:
|
|
|
70
71
|
|
|
71
72
|
Worktree: {{worktreePath}}
|
|
72
73
|
{{projectContext}}
|
|
74
|
+
{{teamContext}}
|
|
73
75
|
|
|
74
76
|
Before auditing, read these files in the worktree root (if they exist):
|
|
75
77
|
- CLAUDE.md — project conventions, tech stack, build/test commands
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Recorded API responses from sub-issue decomposition smoke test.
|
|
3
3
|
* Auto-generated — do not edit manually.
|
|
4
4
|
* Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts
|
|
5
|
-
* Last recorded: 2026-02-
|
|
5
|
+
* Last recorded: 2026-02-24T05:15:32.100Z
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const RECORDED = {
|
|
@@ -44,20 +44,20 @@ export const RECORDED = {
|
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
46
|
"createParent": {
|
|
47
|
-
"id": "
|
|
48
|
-
"identifier": "UAT-
|
|
47
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
48
|
+
"identifier": "UAT-638"
|
|
49
49
|
},
|
|
50
50
|
"createSubIssue1": {
|
|
51
|
-
"id": "
|
|
52
|
-
"identifier": "UAT-
|
|
51
|
+
"id": "a1702cad-3206-4a2b-bb0c-c6ad47df8983",
|
|
52
|
+
"identifier": "UAT-639"
|
|
53
53
|
},
|
|
54
54
|
"createSubIssue2": {
|
|
55
|
-
"id": "
|
|
56
|
-
"identifier": "UAT-
|
|
55
|
+
"id": "d988eb3a-7d45-4d9b-8ee2-4d7580dd183e",
|
|
56
|
+
"identifier": "UAT-640"
|
|
57
57
|
},
|
|
58
58
|
"subIssue1Details": {
|
|
59
|
-
"id": "
|
|
60
|
-
"identifier": "UAT-
|
|
59
|
+
"id": "a1702cad-3206-4a2b-bb0c-c6ad47df8983",
|
|
60
|
+
"identifier": "UAT-639",
|
|
61
61
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
62
62
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
63
63
|
"estimate": 2,
|
|
@@ -75,6 +75,7 @@ export const RECORDED = {
|
|
|
75
75
|
},
|
|
76
76
|
"team": {
|
|
77
77
|
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
78
|
+
"key": "UAT",
|
|
78
79
|
"name": "UAT",
|
|
79
80
|
"issueEstimationType": "tShirt"
|
|
80
81
|
},
|
|
@@ -83,16 +84,16 @@ export const RECORDED = {
|
|
|
83
84
|
},
|
|
84
85
|
"project": null,
|
|
85
86
|
"parent": {
|
|
86
|
-
"id": "
|
|
87
|
-
"identifier": "UAT-
|
|
87
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
88
|
+
"identifier": "UAT-638"
|
|
88
89
|
},
|
|
89
90
|
"relations": {
|
|
90
91
|
"nodes": []
|
|
91
92
|
}
|
|
92
93
|
},
|
|
93
94
|
"subIssue2Details": {
|
|
94
|
-
"id": "
|
|
95
|
-
"identifier": "UAT-
|
|
95
|
+
"id": "d988eb3a-7d45-4d9b-8ee2-4d7580dd183e",
|
|
96
|
+
"identifier": "UAT-640",
|
|
96
97
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
97
98
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
98
99
|
"estimate": 3,
|
|
@@ -110,6 +111,7 @@ export const RECORDED = {
|
|
|
110
111
|
},
|
|
111
112
|
"team": {
|
|
112
113
|
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
114
|
+
"key": "UAT",
|
|
113
115
|
"name": "UAT",
|
|
114
116
|
"issueEstimationType": "tShirt"
|
|
115
117
|
},
|
|
@@ -118,18 +120,18 @@ export const RECORDED = {
|
|
|
118
120
|
},
|
|
119
121
|
"project": null,
|
|
120
122
|
"parent": {
|
|
121
|
-
"id": "
|
|
122
|
-
"identifier": "UAT-
|
|
123
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
124
|
+
"identifier": "UAT-638"
|
|
123
125
|
},
|
|
124
126
|
"relations": {
|
|
125
127
|
"nodes": []
|
|
126
128
|
}
|
|
127
129
|
},
|
|
128
130
|
"parentDetails": {
|
|
129
|
-
"id": "
|
|
130
|
-
"identifier": "UAT-
|
|
131
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
132
|
+
"identifier": "UAT-638",
|
|
131
133
|
"title": "[SMOKE TEST] Sub-Issue Parent: Search Feature",
|
|
132
|
-
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-
|
|
134
|
+
"description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-24T05:15:30.285Z",
|
|
133
135
|
"estimate": null,
|
|
134
136
|
"state": {
|
|
135
137
|
"name": "Backlog",
|
|
@@ -145,17 +147,12 @@ export const RECORDED = {
|
|
|
145
147
|
},
|
|
146
148
|
"team": {
|
|
147
149
|
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
150
|
+
"key": "UAT",
|
|
148
151
|
"name": "UAT",
|
|
149
152
|
"issueEstimationType": "tShirt"
|
|
150
153
|
},
|
|
151
154
|
"comments": {
|
|
152
|
-
"nodes": [
|
|
153
|
-
{
|
|
154
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
155
|
-
"user": null,
|
|
156
|
-
"createdAt": "2026-02-22T03:40:53.165Z"
|
|
157
|
-
}
|
|
158
|
-
]
|
|
155
|
+
"nodes": []
|
|
159
156
|
},
|
|
160
157
|
"project": null,
|
|
161
158
|
"parent": null,
|
|
@@ -164,11 +161,11 @@ export const RECORDED = {
|
|
|
164
161
|
}
|
|
165
162
|
},
|
|
166
163
|
"createRelation": {
|
|
167
|
-
"id": "
|
|
164
|
+
"id": "139541b2-b088-4290-9ded-5b0167a42741"
|
|
168
165
|
},
|
|
169
166
|
"subIssue1WithRelation": {
|
|
170
|
-
"id": "
|
|
171
|
-
"identifier": "UAT-
|
|
167
|
+
"id": "a1702cad-3206-4a2b-bb0c-c6ad47df8983",
|
|
168
|
+
"identifier": "UAT-639",
|
|
172
169
|
"title": "[SMOKE TEST] Sub-Issue 1: Backend API",
|
|
173
170
|
"description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
|
|
174
171
|
"estimate": 2,
|
|
@@ -186,30 +183,25 @@ export const RECORDED = {
|
|
|
186
183
|
},
|
|
187
184
|
"team": {
|
|
188
185
|
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
186
|
+
"key": "UAT",
|
|
189
187
|
"name": "UAT",
|
|
190
188
|
"issueEstimationType": "tShirt"
|
|
191
189
|
},
|
|
192
190
|
"comments": {
|
|
193
|
-
"nodes": [
|
|
194
|
-
{
|
|
195
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
196
|
-
"user": null,
|
|
197
|
-
"createdAt": "2026-02-22T03:40:53.603Z"
|
|
198
|
-
}
|
|
199
|
-
]
|
|
191
|
+
"nodes": []
|
|
200
192
|
},
|
|
201
193
|
"project": null,
|
|
202
194
|
"parent": {
|
|
203
|
-
"id": "
|
|
204
|
-
"identifier": "UAT-
|
|
195
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
196
|
+
"identifier": "UAT-638"
|
|
205
197
|
},
|
|
206
198
|
"relations": {
|
|
207
199
|
"nodes": [
|
|
208
200
|
{
|
|
209
201
|
"type": "blocks",
|
|
210
202
|
"relatedIssue": {
|
|
211
|
-
"id": "
|
|
212
|
-
"identifier": "UAT-
|
|
203
|
+
"id": "d988eb3a-7d45-4d9b-8ee2-4d7580dd183e",
|
|
204
|
+
"identifier": "UAT-640",
|
|
213
205
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
|
|
214
206
|
}
|
|
215
207
|
}
|
|
@@ -217,8 +209,8 @@ export const RECORDED = {
|
|
|
217
209
|
}
|
|
218
210
|
},
|
|
219
211
|
"subIssue2WithRelation": {
|
|
220
|
-
"id": "
|
|
221
|
-
"identifier": "UAT-
|
|
212
|
+
"id": "d988eb3a-7d45-4d9b-8ee2-4d7580dd183e",
|
|
213
|
+
"identifier": "UAT-640",
|
|
222
214
|
"title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
|
|
223
215
|
"description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
|
|
224
216
|
"estimate": 3,
|
|
@@ -236,22 +228,17 @@ export const RECORDED = {
|
|
|
236
228
|
},
|
|
237
229
|
"team": {
|
|
238
230
|
"id": "08cba264-d774-4afd-bc93-ee8213d12ef8",
|
|
231
|
+
"key": "UAT",
|
|
239
232
|
"name": "UAT",
|
|
240
233
|
"issueEstimationType": "tShirt"
|
|
241
234
|
},
|
|
242
235
|
"comments": {
|
|
243
|
-
"nodes": [
|
|
244
|
-
{
|
|
245
|
-
"body": "This thread is for an agent session with ctclaw.",
|
|
246
|
-
"user": null,
|
|
247
|
-
"createdAt": "2026-02-22T03:40:53.840Z"
|
|
248
|
-
}
|
|
249
|
-
]
|
|
236
|
+
"nodes": []
|
|
250
237
|
},
|
|
251
238
|
"project": null,
|
|
252
239
|
"parent": {
|
|
253
|
-
"id": "
|
|
254
|
-
"identifier": "UAT-
|
|
240
|
+
"id": "f236a0f3-a365-4e05-9b84-7c965da87c03",
|
|
241
|
+
"identifier": "UAT-638"
|
|
255
242
|
},
|
|
256
243
|
"relations": {
|
|
257
244
|
"nodes": []
|
package/src/agent/agent.test.ts
CHANGED
|
@@ -228,7 +228,7 @@ describe("runAgent subprocess", () => {
|
|
|
228
228
|
const noisyOutput = [
|
|
229
229
|
"[plugins] Dispatch gateway methods registered",
|
|
230
230
|
"[plugins] Linear agent extension registered (agent: zoe)",
|
|
231
|
-
'[plugins]
|
|
231
|
+
'[plugins] cli tools registered: cli_codex, cli_claude, cli_gemini (agent default: cli_codex)',
|
|
232
232
|
JSON.stringify({ payloads: [{ text: "clean response" }], meta: {} }),
|
|
233
233
|
].join("\n");
|
|
234
234
|
(api.runtime.system as any).runCommandWithTimeout = vi.fn().mockResolvedValue({
|