specweave 1.0.590 → 1.0.592
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/.claude-plugin/marketplace.json +2 -2
- package/dist/dashboard/assets/{index-DH3l1QBV.js → index-C4kubt_5.js} +1 -1
- package/dist/dashboard/assets/index-DAfC5dt1.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.js +18 -1
- package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
- package/dist/src/utils/plugin-copier.d.ts +27 -0
- package/dist/src/utils/plugin-copier.d.ts.map +1 -1
- package/dist/src/utils/plugin-copier.js +88 -1
- package/dist/src/utils/plugin-copier.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/lib/integrations/ado/ado-spec-sync.js +0 -1
- package/plugins/specweave/lib/integrations/github/metadata-issue-lookup.js +3 -1
- package/plugins/specweave/lib/integrations/jira/jira-spec-sync.js +100 -90
- package/plugins/specweave/lib/integrations/jira/jira-status-sync.js +54 -2
- package/plugins/specweave/skills/team-lead/SKILL.md +67 -4
- package/plugins/specweave/skills/team-lead/agents/_protocol.md +44 -0
- package/plugins/specweave/skills/team-lead/agents/backend.md +2 -0
- package/plugins/specweave/skills/team-lead/agents/database.md +2 -0
- package/plugins/specweave/skills/team-lead/agents/testing.md +2 -0
- package/plugins/specweave/skills/team-lead/reference/dynamic-workflows.md +144 -0
- package/dist/dashboard/assets/index-vkwF92tD.css +0 -1
|
@@ -9,8 +9,6 @@ import { toDescription } from "./content-format-adapter.js";
|
|
|
9
9
|
import { getEpicLinkFieldForProject } from "./jira-field-discovery.js";
|
|
10
10
|
import { searchAllIssues } from "./jira-paginated-search.js";
|
|
11
11
|
import axios from "axios";
|
|
12
|
-
import { CircuitBreakerRegistry } from "../../../../../src/core/sync/circuit-breaker-registry.js";
|
|
13
|
-
import { SyncRetryQueue } from "../../../../../src/core/sync/sync-retry-queue.js";
|
|
14
12
|
import { SyncError } from "../../../../../src/core/errors/sync-error.js";
|
|
15
13
|
import { LockManager } from "../../../../../src/utils/lock-manager.js";
|
|
16
14
|
function buildStoryDescription(us) {
|
|
@@ -55,6 +53,9 @@ class JiraSpecSync {
|
|
|
55
53
|
}
|
|
56
54
|
});
|
|
57
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Execute fn under file lock (if lockManager configured).
|
|
58
|
+
*/
|
|
58
59
|
async withLock(fn) {
|
|
59
60
|
if (!this.lockManager) return fn();
|
|
60
61
|
const acquired = await this.lockManager.acquire();
|
|
@@ -67,6 +68,9 @@ class JiraSpecSync {
|
|
|
67
68
|
await this.lockManager.release();
|
|
68
69
|
}
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Check circuit breaker before making API calls.
|
|
73
|
+
*/
|
|
70
74
|
checkCircuitBreaker() {
|
|
71
75
|
if (!this.circuitBreakerRegistry) return;
|
|
72
76
|
const breaker = this.circuitBreakerRegistry.get("jira");
|
|
@@ -74,10 +78,16 @@ class JiraSpecSync {
|
|
|
74
78
|
throw new SyncError("jira", 0, "", "Circuit breaker open for jira");
|
|
75
79
|
}
|
|
76
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Record success on circuit breaker.
|
|
83
|
+
*/
|
|
77
84
|
recordApiSuccess() {
|
|
78
85
|
if (!this.circuitBreakerRegistry) return;
|
|
79
86
|
this.circuitBreakerRegistry.get("jira").recordSuccess();
|
|
80
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Record failure on circuit breaker and optionally enqueue retry.
|
|
90
|
+
*/
|
|
81
91
|
async recordApiFailure(error, operation) {
|
|
82
92
|
const httpStatus = error?.response?.status ?? 0;
|
|
83
93
|
if (this.circuitBreakerRegistry) {
|
|
@@ -111,56 +121,56 @@ class JiraSpecSync {
|
|
|
111
121
|
console.log(`
|
|
112
122
|
\u{1F504} Syncing spec ${specId} to Jira Epic...`);
|
|
113
123
|
return this.withLock(async () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
this.checkCircuitBreaker();
|
|
125
|
+
try {
|
|
126
|
+
const spec = await this.specManager.loadSpec(specId);
|
|
127
|
+
if (!spec) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
specId,
|
|
131
|
+
provider: "jira",
|
|
132
|
+
error: `Spec ${specId} not found`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const existingLink = spec.metadata.externalLinks?.jira;
|
|
136
|
+
let epic;
|
|
137
|
+
if (existingLink?.epicKey) {
|
|
138
|
+
console.log(` Found existing Jira Epic ${existingLink.epicKey}`);
|
|
139
|
+
epic = await this.updateJiraEpic(existingLink.epicKey, spec);
|
|
140
|
+
} else {
|
|
141
|
+
console.log(" Creating new Jira Epic...");
|
|
142
|
+
epic = await this.createJiraEpic(spec);
|
|
143
|
+
await this.specManager.linkToExternal(specId, "jira", {
|
|
144
|
+
id: epic.key,
|
|
145
|
+
url: epic.url,
|
|
146
|
+
projectKey: this.config.projectKey,
|
|
147
|
+
domain: this.config.domain
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const changes = await this.syncUserStories(epic.key, spec);
|
|
151
|
+
this.recordApiSuccess();
|
|
152
|
+
console.log("\u2705 Sync complete!");
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
specId,
|
|
156
|
+
provider: "jira",
|
|
157
|
+
externalId: epic.key,
|
|
158
|
+
url: epic.url,
|
|
159
|
+
changes
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
await this.recordApiFailure(error, "syncSpecToJira");
|
|
163
|
+
const axiosData = error?.response?.data;
|
|
164
|
+
const detail = axiosData ? JSON.stringify(axiosData) : "";
|
|
165
|
+
console.error("\u274C Error syncing to Jira:", error?.message || error, detail ? `
|
|
166
|
+
Response: ${detail}` : "");
|
|
118
167
|
return {
|
|
119
168
|
success: false,
|
|
120
169
|
specId,
|
|
121
170
|
provider: "jira",
|
|
122
|
-
error:
|
|
171
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
123
172
|
};
|
|
124
173
|
}
|
|
125
|
-
const existingLink = spec.metadata.externalLinks?.jira;
|
|
126
|
-
let epic;
|
|
127
|
-
if (existingLink?.epicKey) {
|
|
128
|
-
console.log(` Found existing Jira Epic ${existingLink.epicKey}`);
|
|
129
|
-
epic = await this.updateJiraEpic(existingLink.epicKey, spec);
|
|
130
|
-
} else {
|
|
131
|
-
console.log(" Creating new Jira Epic...");
|
|
132
|
-
epic = await this.createJiraEpic(spec);
|
|
133
|
-
await this.specManager.linkToExternal(specId, "jira", {
|
|
134
|
-
id: epic.key,
|
|
135
|
-
url: epic.url,
|
|
136
|
-
projectKey: this.config.projectKey,
|
|
137
|
-
domain: this.config.domain
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
const changes = await this.syncUserStories(epic.key, spec);
|
|
141
|
-
this.recordApiSuccess();
|
|
142
|
-
console.log("\u2705 Sync complete!");
|
|
143
|
-
return {
|
|
144
|
-
success: true,
|
|
145
|
-
specId,
|
|
146
|
-
provider: "jira",
|
|
147
|
-
externalId: epic.key,
|
|
148
|
-
url: epic.url,
|
|
149
|
-
changes
|
|
150
|
-
};
|
|
151
|
-
} catch (error) {
|
|
152
|
-
await this.recordApiFailure(error, "syncSpecToJira");
|
|
153
|
-
const axiosData = error?.response?.data;
|
|
154
|
-
const detail = axiosData ? JSON.stringify(axiosData) : "";
|
|
155
|
-
console.error("\u274C Error syncing to Jira:", error?.message || error, detail ? `
|
|
156
|
-
Response: ${detail}` : "");
|
|
157
|
-
return {
|
|
158
|
-
success: false,
|
|
159
|
-
specId,
|
|
160
|
-
provider: "jira",
|
|
161
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
174
|
});
|
|
165
175
|
}
|
|
166
176
|
/**
|
|
@@ -170,60 +180,60 @@ class JiraSpecSync {
|
|
|
170
180
|
console.log(`
|
|
171
181
|
\u{1F504} Syncing FROM Jira to spec ${specId}...`);
|
|
172
182
|
return this.withLock(async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
this.checkCircuitBreaker();
|
|
184
|
+
try {
|
|
185
|
+
const spec = await this.specManager.loadSpec(specId);
|
|
186
|
+
if (!spec) {
|
|
187
|
+
return {
|
|
188
|
+
success: false,
|
|
189
|
+
specId,
|
|
190
|
+
provider: "jira",
|
|
191
|
+
error: `Spec ${specId} not found`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const jiraLink = spec.metadata.externalLinks?.jira;
|
|
195
|
+
if (!jiraLink?.epicKey) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
specId,
|
|
199
|
+
provider: "jira",
|
|
200
|
+
error: "Spec not linked to Jira Epic"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const epic = await this.fetchJiraEpic(jiraLink.epicKey);
|
|
204
|
+
const conflicts = await this.detectConflicts(spec, epic);
|
|
205
|
+
if (conflicts.length === 0) {
|
|
206
|
+
console.log("\u2705 No conflicts - spec and Jira in sync");
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
specId,
|
|
210
|
+
provider: "jira",
|
|
211
|
+
externalId: epic.key,
|
|
212
|
+
url: epic.url
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
console.log(`\u26A0\uFE0F Detected ${conflicts.length} conflict(s)`);
|
|
216
|
+
await this.writeConflictReport(specId, conflicts);
|
|
217
|
+
await this.resolveConflicts(spec, conflicts);
|
|
218
|
+
console.log("\u2705 Sync FROM Jira complete!");
|
|
177
219
|
return {
|
|
178
|
-
success:
|
|
220
|
+
success: true,
|
|
179
221
|
specId,
|
|
180
222
|
provider: "jira",
|
|
181
|
-
|
|
223
|
+
externalId: epic.key,
|
|
224
|
+
url: epic.url,
|
|
225
|
+
conflicts
|
|
182
226
|
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await this.recordApiFailure(error, "syncFromJira");
|
|
229
|
+
console.error("\u274C Error syncing FROM Jira:", error);
|
|
186
230
|
return {
|
|
187
231
|
success: false,
|
|
188
232
|
specId,
|
|
189
233
|
provider: "jira",
|
|
190
|
-
error:
|
|
234
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
191
235
|
};
|
|
192
236
|
}
|
|
193
|
-
const epic = await this.fetchJiraEpic(jiraLink.epicKey);
|
|
194
|
-
const conflicts = await this.detectConflicts(spec, epic);
|
|
195
|
-
if (conflicts.length === 0) {
|
|
196
|
-
console.log("\u2705 No conflicts - spec and Jira in sync");
|
|
197
|
-
return {
|
|
198
|
-
success: true,
|
|
199
|
-
specId,
|
|
200
|
-
provider: "jira",
|
|
201
|
-
externalId: epic.key,
|
|
202
|
-
url: epic.url
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
console.log(`\u26A0\uFE0F Detected ${conflicts.length} conflict(s)`);
|
|
206
|
-
await this.writeConflictReport(specId, conflicts);
|
|
207
|
-
await this.resolveConflicts(spec, conflicts);
|
|
208
|
-
console.log("\u2705 Sync FROM Jira complete!");
|
|
209
|
-
return {
|
|
210
|
-
success: true,
|
|
211
|
-
specId,
|
|
212
|
-
provider: "jira",
|
|
213
|
-
externalId: epic.key,
|
|
214
|
-
url: epic.url,
|
|
215
|
-
conflicts
|
|
216
|
-
};
|
|
217
|
-
} catch (error) {
|
|
218
|
-
await this.recordApiFailure(error, "syncFromJira");
|
|
219
|
-
console.error("\u274C Error syncing FROM Jira:", error);
|
|
220
|
-
return {
|
|
221
|
-
success: false,
|
|
222
|
-
specId,
|
|
223
|
-
provider: "jira",
|
|
224
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
237
|
});
|
|
228
238
|
}
|
|
229
239
|
/**
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { detectDeploymentType, getApiBaseUrl } from "./jira-deployment-detector.js";
|
|
3
3
|
import { toCommentBody } from "./content-format-adapter.js";
|
|
4
|
-
import { CircuitBreakerRegistry } from "../../../../../src/core/sync/circuit-breaker-registry.js";
|
|
5
|
-
import { SyncRetryQueue } from "../../../../../src/core/sync/sync-retry-queue.js";
|
|
6
4
|
import { SyncError } from "../../../../../src/core/errors/sync-error.js";
|
|
7
5
|
import { LockManager } from "../../../../../src/utils/lock-manager.js";
|
|
8
6
|
class JiraStatusSync {
|
|
@@ -30,6 +28,9 @@ class JiraStatusSync {
|
|
|
30
28
|
}
|
|
31
29
|
});
|
|
32
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Execute fn under file lock (if lockManager configured).
|
|
33
|
+
*/
|
|
33
34
|
async withLock(fn) {
|
|
34
35
|
if (!this.lockManager) return fn();
|
|
35
36
|
const acquired = await this.lockManager.acquire();
|
|
@@ -42,6 +43,10 @@ class JiraStatusSync {
|
|
|
42
43
|
await this.lockManager.release();
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Check circuit breaker before making API calls.
|
|
48
|
+
* Throws if breaker is open.
|
|
49
|
+
*/
|
|
45
50
|
checkCircuitBreaker() {
|
|
46
51
|
if (!this.circuitBreakerRegistry) return;
|
|
47
52
|
const breaker = this.circuitBreakerRegistry.get("jira");
|
|
@@ -49,10 +54,16 @@ class JiraStatusSync {
|
|
|
49
54
|
throw new SyncError("jira", 0, "", "Circuit breaker open for jira");
|
|
50
55
|
}
|
|
51
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Record success on circuit breaker.
|
|
59
|
+
*/
|
|
52
60
|
recordSuccess() {
|
|
53
61
|
if (!this.circuitBreakerRegistry) return;
|
|
54
62
|
this.circuitBreakerRegistry.get("jira").recordSuccess();
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle API error: record failure on circuit breaker, enqueue retry, throw SyncError.
|
|
66
|
+
*/
|
|
56
67
|
async handleApiError(error, operation) {
|
|
57
68
|
const httpStatus = error?.response?.status ?? 0;
|
|
58
69
|
const responseBody = JSON.stringify(error?.response?.data ?? "");
|
|
@@ -72,6 +83,9 @@ class JiraStatusSync {
|
|
|
72
83
|
}
|
|
73
84
|
throw new SyncError("jira", httpStatus, responseBody, detail);
|
|
74
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Initialize: detect deployment type and update client baseURL
|
|
88
|
+
*/
|
|
75
89
|
async init() {
|
|
76
90
|
const deployment = await detectDeploymentType(this.domain, {
|
|
77
91
|
email: this.client.defaults.auth?.username || "",
|
|
@@ -79,6 +93,12 @@ class JiraStatusSync {
|
|
|
79
93
|
});
|
|
80
94
|
this.client.defaults.baseURL = deployment.baseUrl;
|
|
81
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Get current status from JIRA issue
|
|
98
|
+
*
|
|
99
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
100
|
+
* @returns Current issue status
|
|
101
|
+
*/
|
|
82
102
|
async getStatus(issueKey) {
|
|
83
103
|
return this.withLock(async () => {
|
|
84
104
|
this.checkCircuitBreaker();
|
|
@@ -93,6 +113,19 @@ class JiraStatusSync {
|
|
|
93
113
|
}
|
|
94
114
|
});
|
|
95
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Update JIRA issue status via transitions
|
|
118
|
+
*
|
|
119
|
+
* JIRA requires using transitions to change status.
|
|
120
|
+
* Cannot directly set status field.
|
|
121
|
+
*
|
|
122
|
+
* Handles missing transitions gracefully by logging a warning
|
|
123
|
+
* instead of throwing an error.
|
|
124
|
+
*
|
|
125
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
126
|
+
* @param status - Desired status
|
|
127
|
+
* @returns true if transition succeeded, false if not available
|
|
128
|
+
*/
|
|
96
129
|
async updateStatus(issueKey, status) {
|
|
97
130
|
return this.withLock(async () => {
|
|
98
131
|
this.checkCircuitBreaker();
|
|
@@ -120,6 +153,13 @@ class JiraStatusSync {
|
|
|
120
153
|
}
|
|
121
154
|
});
|
|
122
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Post comment about status change to JIRA issue
|
|
158
|
+
*
|
|
159
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
160
|
+
* @param oldStatus - Previous SpecWeave status
|
|
161
|
+
* @param newStatus - New SpecWeave status
|
|
162
|
+
*/
|
|
123
163
|
async postStatusComment(issueKey, oldStatus, newStatus) {
|
|
124
164
|
return this.withLock(async () => {
|
|
125
165
|
this.checkCircuitBreaker();
|
|
@@ -142,6 +182,18 @@ _Synced from SpecWeave_`;
|
|
|
142
182
|
}
|
|
143
183
|
});
|
|
144
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Post AC progress comment with proper ADF formatting and dedup.
|
|
187
|
+
*
|
|
188
|
+
* Builds native ADF with:
|
|
189
|
+
* - Bold header showing completion percentage
|
|
190
|
+
* - Bullet list with checkmark/cross emojis per AC
|
|
191
|
+
* - Fingerprint marker to prevent duplicate comments
|
|
192
|
+
*
|
|
193
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
194
|
+
* @param acStates - Array of AC states with id, description, completed
|
|
195
|
+
* @returns true if comment was posted, false if skipped (duplicate)
|
|
196
|
+
*/
|
|
145
197
|
async postProgressComment(issueKey, acStates) {
|
|
146
198
|
return this.withLock(async () => {
|
|
147
199
|
this.checkCircuitBreaker();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Phase-agnostic orchestrator for parallel multi-agent work — brainstorm, plan, implement, review, research, or test. Auto-detects mode from intent. Use for implementation (3+ domains or 15+ tasks), brainstorming (multiple perspectives), parallel planning (PM + Architect), code review (delegates to sw:code-reviewer), research (multiple topics), or testing (parallel test layers). Also use when user says "team setup", "parallel agents", "team lead", "agent teams", "brainstorm with agents", "plan in parallel", "review code", "research this".
|
|
3
|
-
version: 1.
|
|
2
|
+
description: Phase-agnostic orchestrator for parallel multi-agent work — brainstorm, plan, implement, review, research, or test. Auto-detects mode from intent. Use for implementation (3+ domains or 15+ tasks), brainstorming (multiple perspectives), parallel planning (PM + Architect), code review (delegates to sw:code-reviewer), research (multiple topics), or testing (parallel test layers). Also use when user says "team setup", "parallel agents", "team lead", "agent teams", "brainstorm with agents", "plan in parallel", "review code", "research this". Also auto-selects a dynamic Workflow for batch-class work (migrations, repo-wide audits, deep research, high-stakes verification) without being asked.
|
|
3
|
+
version: 1.1.0
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Team Lead
|
|
@@ -12,6 +12,7 @@ version: 1.0.0
|
|
|
12
12
|
- **Read**: Load the master `spec.md` once for fan-out decisions and read agent prompt templates under `plugins/specweave/skills/team-lead/agents/`. During the active phase, prefer `PLAN_READY` summaries from agents over re-reading full plan files.
|
|
13
13
|
- **Bash**: Inspect team state (`specweave doctor`, `ls ~/.claude/teams/`, grep metadata.json) and run CLI-only operations — never to implement code.
|
|
14
14
|
- **TeamCreate / Task / SendMessage**: Core orchestration primitives for spawning agents, routing messages, and handling heartbeats.
|
|
15
|
+
- **Workflow**: Dynamic-workflow engine for batch-class fan-out (migrations, repo-wide audits, high-stakes verification, deep research) — auto-selected per §0.4, never requires the user to ask. See `reference/dynamic-workflows.md`.
|
|
15
16
|
|
|
16
17
|
## MANDATORY: Orchestrator Identity (NEVER SKIP)
|
|
17
18
|
|
|
@@ -22,9 +23,10 @@ version: 1.0.0
|
|
|
22
23
|
- **NEVER** say "I'll do this directly" — that defeats the purpose of team-lead
|
|
23
24
|
- Even if you just finished a previous team-lead session in this conversation, you MUST create a **new** team and spawn **new** agents
|
|
24
25
|
- **Fan-out threshold**: Spawn agents when **domains ≥ 3** OR **tasks ≥ 15** OR `--parallel` flag is set. For 1-domain or <15-task work, execute directly without spawning.
|
|
25
|
-
- Your only tools are: `TeamCreate`, `Task`, `SendMessage`, `Read` (for agent templates; during active phase use PLAN_READY summaries instead of reading full plan files), `Bash` (only for team state inspection), and `Skill()` (only during closure phase for grill/done)
|
|
26
|
+
- Your only tools are: `TeamCreate`, `Task`, `SendMessage`, `Workflow` (for batch-class fan-out per §0.4), `Read` (for agent templates; during active phase use PLAN_READY summaries instead of reading full plan files), `Bash` (only for team state inspection), and `Skill()` (only during closure phase for grill/done)
|
|
27
|
+
- **`Workflow` IS orchestration, not implementation** — per §0.4 you MAY launch a `Workflow()` for batch-class work (migrations, repo-wide audits, high-stakes verification, deep research). Delegating to a Workflow does NOT violate this rule; writing the code yourself does.
|
|
26
28
|
|
|
27
|
-
**The test**: If you're about to call `Edit()` or write code, STOP — you're violating this rule.
|
|
29
|
+
**The test**: If you're about to call `Edit()` or write code yourself, STOP — you're violating this rule. (Launching a `Workflow()` that spawns agents to do the work is fine.)
|
|
28
30
|
|
|
29
31
|
---
|
|
30
32
|
|
|
@@ -128,6 +130,8 @@ sw:team-lead --preset api-only --max-agents 4 "Add payments API"
|
|
|
128
130
|
|
|
129
131
|
**team_name**: `brainstorm-{topic-slug}`
|
|
130
132
|
|
|
133
|
+
**Engine** (per §0.4): Agent Teams — brainstorm needs interactive steering + the idea tree, not a deterministic batch. Pattern: perspective-diverse verify (advocate / critic / pragmatist fan out → synthesize).
|
|
134
|
+
|
|
131
135
|
Skip increment pre-flight entirely. Brainstorm doesn't need a spec — it explores possibilities.
|
|
132
136
|
|
|
133
137
|
1. Create team: `TeamCreate({ team_name: "brainstorm-{slug}", description: "Brainstorm: {topic}" })`
|
|
@@ -249,6 +253,8 @@ Pass through any arguments the user provided (--pr N, --changes, --cross-repo, p
|
|
|
249
253
|
|
|
250
254
|
**team_name**: `research-{topic-slug}`
|
|
251
255
|
|
|
256
|
+
**Engine** (per §0.4): prefer a **Dynamic Workflow** deep-research pipeline (fan-out searches → fetch → adversarially verify → synthesize a cited report, results held off-context) for multi-source / multi-subtopic research; use Agent Teams only for 2-3 steerable researchers. Falls back to Agent Teams when `quality.workflows.enabled` is off.
|
|
257
|
+
|
|
252
258
|
Skip increment pre-flight. Research is exploratory — no spec needed.
|
|
253
259
|
|
|
254
260
|
1. Create team: `TeamCreate({ team_name: "research-{slug}", description: "Research: {topic}" })`
|
|
@@ -285,6 +291,8 @@ Skip increment pre-flight. Research is exploratory — no spec needed.
|
|
|
285
291
|
|
|
286
292
|
**team_name**: `test-{increment-id}`
|
|
287
293
|
|
|
294
|
+
**Engine** (per §0.4): route **many independent same-shape tests** to a **Dynamic Workflow** (generate → run → fix-loop until green); keep Agent Teams when test layers have setup dependencies. Falls back to Agent Teams when `quality.workflows.enabled` is off.
|
|
295
|
+
|
|
288
296
|
Testing mode requires an increment (it needs to know WHAT to test).
|
|
289
297
|
|
|
290
298
|
1. **Verify increment exists** (same as implementation mode — see below)
|
|
@@ -321,6 +329,48 @@ Testing mode requires an increment (it needs to know WHAT to test).
|
|
|
321
329
|
|
|
322
330
|
---
|
|
323
331
|
|
|
332
|
+
## 0.4. Orchestration Engine Selection (AUTO-DETECT — never wait for a flag)
|
|
333
|
+
|
|
334
|
+
**Run AFTER §0 Mode Detection, BEFORE §0.5 Increment Pre-Flight.** Pick the engine from the *shape* of the work, not from the user's words. The user must NEVER have to say "use a workflow" — you infer it.
|
|
335
|
+
|
|
336
|
+
Three engines:
|
|
337
|
+
|
|
338
|
+
| Engine | What it is | Owns |
|
|
339
|
+
|--------|-----------|------|
|
|
340
|
+
| **Dynamic Workflow** | `Workflow()` — deterministic JS scheduler, ≤16 concurrent / 1000 total subagents, results in script vars (off context), resumable, background | Large homogeneous fan-out: migrations, repo-wide audits, many-similar-tests, adversarial/judge verification |
|
|
341
|
+
| **Agent Teams** (default) | `TeamCreate` + `Task` + `SendMessage` + tmux — model-driven, domain-aware, SpecWeave-lifecycle owner | Feature work: 3+ domains or 15+ tasks with cross-domain contracts + closure + sync |
|
|
342
|
+
| **Inline** | This orchestrator, no spawn | <15 tasks, single domain, sequential |
|
|
343
|
+
|
|
344
|
+
### Decision table (first match wins)
|
|
345
|
+
|
|
346
|
+
| Signal in the work | Engine | Why |
|
|
347
|
+
|--------------------|--------|-----|
|
|
348
|
+
| ≥~25 **same-shape** tasks, no cross-task deps (rename X→Y across N files, add header to N modules, port N tests) | **Dynamic Workflow** (top-level, bypass team-lead) | Pure fan-out under ONE budget + ONE scheduler — no glue needed |
|
|
349
|
+
| Codebase **migration** / repo-wide **audit** / lint-sweep / dependency-bump across many files | **Dynamic Workflow** (top-level) | Domain-blind batch; team-lead's lifecycle is dead weight here |
|
|
350
|
+
| High-stakes **verification** needing N independent checks (adversarial-verify, judge-panel, completeness-critic) | **Dynamic Workflow** (top-level) | Built-in reusable patterns; deterministic reduce |
|
|
351
|
+
| **Deep research** spanning many sources/subtopics | **Dynamic Workflow** (pipeline) | fan-out → fetch → verify → synthesize, results off-context |
|
|
352
|
+
| 3+ domains OR 15+ tasks **with cross-domain contracts** (frontend ⇐ OpenAPI ⇐ schema), needs increment activation + closure + GitHub/Jira/ADO sync | **Agent Teams** | Phased dependencies defeat fan-out; only team-lead owns the SpecWeave contract |
|
|
353
|
+
| A single Agent-Teams **domain agent** discovers its OWN task list is a ≥~25 same-shape batch | Agent Teams **+ inner Workflow ×1** (see `agents/_protocol.md` Workflow Mode) | Legal one-level borrow; team-lead stays outer coordinator |
|
|
354
|
+
| <15 tasks, 1 domain, sequential | **Inline** | Spawning overhead unjustified |
|
|
355
|
+
|
|
356
|
+
### AUTO-DETECT signals (no flag required)
|
|
357
|
+
|
|
358
|
+
- **→ Dynamic Workflow**: `migrate`, `migration`, `port`, `rename across`, `repo-wide`, `codebase-wide`, `audit`, `sweep`, `bulk`, `every file`, `all <N> …`, `double-check`, `verify independently`, `judge`, `tournament`, `deep research`, `survey the literature`, `ultracode` keyword / `/effort ultracode`.
|
|
359
|
+
- **→ Agent Teams**: `build <feature>`, `team`, `parallel agents`, `multi-domain`, increment with 3+ domains, contracts (Prisma/OpenAPI/shared types) in scope, anything needing `sw:done`/sync.
|
|
360
|
+
- **→ Inline**: small single-domain change, hotfix, one-file edit.
|
|
361
|
+
|
|
362
|
+
### Guards (CHECK before routing to any Workflow)
|
|
363
|
+
|
|
364
|
+
1. **Default-off** — if `quality.workflows.enabled` is absent or `false`, do NOT auto-fire a Workflow and do NOT honor the inner borrow; fall back to Agent Teams / inline (classic behavior, byte-for-byte). This is what keeps the enhancement non-destructive.
|
|
365
|
+
2. **Budget gate** — read `quality.workflows.perRunTokenBudget` + `maxConcurrentWorkflows` from config. If a route would exceed either, DOWNGRADE to inline/sequential and tell the user.
|
|
366
|
+
3. **Nesting law** — `Workflow()` allows ONE nesting level and THROWS inside a leaf subagent. Legal chain: `team-lead (orchestrator, not a Workflow) → Task member → Workflow ×1 → leaves (cannot nest)`. team-lead may fire a top-level Workflow OR delegate the borrow to a member — never both for the same work.
|
|
367
|
+
4. **One top-level Workflow beats N nested ones** — for pure batch-class work, prefer a SINGLE top-level `Workflow` with `parallel()` over the items (one budget, one scheduler) instead of N domain agents each launching their own.
|
|
368
|
+
5. **Inject SpecWeave context** — Workflow is domain-blind (zero knowledge of specs/ACs/increments/gates). Every Workflow-routed agent prompt MUST carry the increment path, file-ownership patterns, AC list, and contract artifacts, or it produces unanchored code.
|
|
369
|
+
|
|
370
|
+
See `reference/dynamic-workflows.md` for primitives, quality patterns, and the full `quality.workflows` config schema.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
324
374
|
## 0.5. Increment Pre-Flight (IMPLEMENTATION and TESTING modes only)
|
|
325
375
|
|
|
326
376
|
**This section applies only to IMPLEMENTATION mode (default) and TESTING mode.**
|
|
@@ -402,6 +452,7 @@ fi
|
|
|
402
452
|
| Spawn agent | `Task` | `team_name`, `name`, `subagent_type`, `prompt`, `mode: "bypassPermissions"` |
|
|
403
453
|
| Send message | `SendMessage` | `type`, `recipient`, `content`, `summary` |
|
|
404
454
|
| Shutdown agent | `SendMessage` | `type: "shutdown_request"`, `recipient` |
|
|
455
|
+
| Run batch fan-out | `Workflow` | `script` (a `meta` literal + `agent()`/`parallel()`/`pipeline()`) — for batch-class work per §0.4; see `reference/dynamic-workflows.md` |
|
|
405
456
|
|
|
406
457
|
---
|
|
407
458
|
|
|
@@ -993,6 +1044,18 @@ When an agent is declared stuck:
|
|
|
993
1044
|
- Heartbeat STATUS messages let team-lead detect problems early instead of after long silences
|
|
994
1045
|
- If an agent's task count exceeds 40 despite the cap, split before spawning
|
|
995
1046
|
|
|
1047
|
+
### Workflow-Aware Stuck Detection (agent borrowing a Workflow)
|
|
1048
|
+
|
|
1049
|
+
A domain agent that borrows a Workflow (see `agents/_protocol.md` Workflow Mode) goes **silent for minutes** while the background run executes — the normal 5min/20min windows would false-kill a healthy agent. Additive handling:
|
|
1050
|
+
|
|
1051
|
+
1. **Delegation state** — when an agent's STATUS contains `DELEGATING … workflow_id=`, mark it `state=workflow-delegating` and record the `workflow_id`. SUSPEND the normal 5/20 windows.
|
|
1052
|
+
2. **Extended windows** — while delegating, apply `quality.workflows.stuckDetection.{workflowNoProgressMin, workflowTotalStuckMin}` (defaults 20min / 60min) instead. Silence here is expected, not stuck.
|
|
1053
|
+
3. **Clear on completion** — a `STATUS: WORKFLOW_DONE` (or a `BLOCKING_ISSUE` citing the same `workflow_id`) restores `state=active` and the normal 5/20 windows from the next heartbeat.
|
|
1054
|
+
4. **Probe before kill** — if `workflowTotalStuckMin` elapses with no `WORKFLOW_DONE`, send ONE `STATUS_CHECK` asking the agent to report the Workflow's own progress (it is resumable/background). Declare stuck only after a second unanswered probe; prefer leaving the increment open over killing mid-batch.
|
|
1055
|
+
5. **Loop-detection unchanged** — the "same task number 3+ heartbeats = stuck loop" rule still applies to NON-delegating agents; a delegating agent emits no task-number heartbeats and is exempt by construction.
|
|
1056
|
+
|
|
1057
|
+
**Default-off**: with no `quality.workflows` config, no agent ever enters `state=workflow-delegating`, so behavior is identical to today.
|
|
1058
|
+
|
|
996
1059
|
---
|
|
997
1060
|
|
|
998
1061
|
## 9. Workflow Summary
|
|
@@ -86,3 +86,47 @@ Approve only if you have no unsaved state. Approving terminates your process.
|
|
|
86
86
|
- Do NOT wait for team-lead response to PLAN_READY — proceed immediately
|
|
87
87
|
- ALL repository operations MUST use `repositories/{ORG}/` directory structure
|
|
88
88
|
- Create `.specweave/increments/` in YOUR assigned repo, NOT in the umbrella project root
|
|
89
|
+
- **Workflow Mode** (below) is OPT-IN and default-off — only borrow a `Workflow()` for a ≥~25 same-shape independent-task batch, exactly once, and only if `quality.workflows.agentBorrow.enabled` is true
|
|
90
|
+
|
|
91
|
+
## Workflow Mode (OPT-IN — large homogeneous batch only)
|
|
92
|
+
|
|
93
|
+
You normally implement tasks sequentially. **IF AND ONLY IF** your assigned task list is a **large homogeneous batch** — **≥~25 tasks of the same shape with no cross-task dependencies** (e.g. apply the same rename/migration/edit to N files, port N similar tests, audit N modules) — you MAY call `Workflow()` **EXACTLY ONCE** to fan them out.
|
|
94
|
+
|
|
95
|
+
**Gate (ALL must hold, else implement sequentially):**
|
|
96
|
+
- `quality.workflows.enabled === true` AND `quality.workflows.agentBorrow.enabled === true` in `.specweave/config.json` (both default **off** — if absent/false, do NOT use Workflow).
|
|
97
|
+
- ≥~25 same-shape, independent tasks (`quality.workflows.batchShapeThreshold`, default 25).
|
|
98
|
+
- Projected concurrency ≤ `quality.workflows.maxSubagentsPerWorkflow` (≤16) and projected tokens ≤ `quality.workflows.perRunTokenBudget`.
|
|
99
|
+
|
|
100
|
+
**Nesting law (HARD):** You were spawned by team-lead via the `Task` tool, so you ARE allowed ONE `Workflow()` call. The subagents your Workflow spawns are **leaves** — they CANNOT call `Workflow()` (it throws). One borrow, one level down. Never nest.
|
|
101
|
+
|
|
102
|
+
**Heartbeat-before-Workflow (MANDATORY — prevents false-kill):** A background Workflow makes you silent for minutes. BEFORE the call, emit a STATUS that declares the delegation so team-lead extends its stuck-detection window (see SKILL.md §8b Workflow-Aware Stuck Detection):
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
SendMessage({ type: "message", recipient: "team-lead",
|
|
106
|
+
content: "STATUS: T-{N}/{total} — DELEGATING {batchCount} tasks to background Workflow (workflow_id={id}). Expect silence until WORKFLOW_DONE.",
|
|
107
|
+
summary: "[Domain] entering Workflow Mode — {batchCount} tasks" })
|
|
108
|
+
```
|
|
109
|
+
On completion emit `STATUS: WORKFLOW_DONE — {done}/{batchCount} ok, {failed} failed. Resuming T-{M}.` then resume normal heartbeats.
|
|
110
|
+
|
|
111
|
+
**Template** (one call; results stay in script vars, NOT your context). Inject SpecWeave context (spec/AC refs, file paths) into every agent prompt — Workflow is domain-blind:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
export const meta = {
|
|
115
|
+
name: "batch-fanout",
|
|
116
|
+
description: "Fan out a homogeneous batch of same-shape tasks",
|
|
117
|
+
phases: [{ title: "batch" }],
|
|
118
|
+
};
|
|
119
|
+
const items = [/* the >=25 same-shape task descriptors: { target, acRef, file } */];
|
|
120
|
+
const results = await parallel(
|
|
121
|
+
items.map((it) => () => agent(
|
|
122
|
+
`Apply <the same transform> to ${it.target}. Spec: ${it.acRef}. Write ONLY ${it.file}.`,
|
|
123
|
+
{ label: it.target, phase: "batch", isolation: "worktree" }
|
|
124
|
+
))
|
|
125
|
+
);
|
|
126
|
+
log(`batch complete: ${results.filter(Boolean).length}/${items.length}`);
|
|
127
|
+
return results;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Budget guard:** Keep projected tokens under `quality.workflows.perRunTokenBudget`. If your batch would exceed it, split into fewer items or fall back to sequential — do NOT raise the cap yourself.
|
|
131
|
+
|
|
132
|
+
**If the gate fails for any reason → implement the tasks sequentially as usual.** Workflow Mode is an optimization, never a requirement.
|
|
@@ -35,3 +35,5 @@ DOMAIN RULES:
|
|
|
35
35
|
- Every new API endpoint needs request/response validation
|
|
36
36
|
- Error handling follows project conventions
|
|
37
37
|
- All services must have unit tests
|
|
38
|
+
- Workflow Mode: for >=~25 independent same-shape endpoints/DTOs,
|
|
39
|
+
you MAY borrow a Workflow() ONCE (see _protocol.md; gated on quality.workflows.agentBorrow.enabled)
|
|
@@ -30,3 +30,5 @@ DOMAIN RULES:
|
|
|
30
30
|
- Always create migrations (never modify schema without a migration)
|
|
31
31
|
- Seed data must be idempotent
|
|
32
32
|
- Schema changes should be backward-compatible when possible
|
|
33
|
+
- Workflow Mode: for a bulk migration touching >=~25 independent tables/files,
|
|
34
|
+
you MAY borrow a Workflow() ONCE (see _protocol.md; gated on quality.workflows.agentBorrow.enabled)
|
|
@@ -33,3 +33,5 @@ DOMAIN RULES:
|
|
|
33
33
|
- Tests must cover all ACs from spec.md
|
|
34
34
|
- Follow existing test patterns and utilities
|
|
35
35
|
- E2E tests include a11y checks when applicable
|
|
36
|
+
- Workflow Mode: if you must generate/port >=~25 independent same-shape test files,
|
|
37
|
+
you MAY borrow a Workflow() ONCE (see _protocol.md; gated on quality.workflows.agentBorrow.enabled)
|