wave-agent-sdk 1.0.9 → 1.1.0
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/dist/agent.d.ts +9 -6
- package/dist/agent.js +35 -33
- package/dist/builtin/index.d.ts +1 -0
- package/dist/builtin/index.js +20 -0
- package/dist/builtin/plugins.d.ts +1 -0
- package/dist/builtin/plugins.js +225 -0
- package/dist/builtin/skills/artifact.d.ts +1 -0
- package/dist/builtin/skills/artifact.js +18 -0
- package/dist/builtin/skills/code-review.d.ts +1 -0
- package/{builtin/skills/code-review/SKILL.md → dist/builtin/skills/code-review.js} +23 -19
- package/dist/builtin/skills/deep-research.d.ts +1 -0
- package/{builtin/skills/deep-research/SKILL.md → dist/builtin/skills/deep-research.js} +18 -14
- package/dist/builtin/skills/init.d.ts +1 -0
- package/{builtin/skills/init/SKILL.md → dist/builtin/skills/init.js} +6 -3
- package/dist/builtin/skills/loop.d.ts +1 -0
- package/dist/builtin/skills/loop.js +83 -0
- package/dist/builtin/skills/settings.d.ts +1 -0
- package/dist/builtin/skills/settings.js +1249 -0
- package/dist/builtin/skills/simplify.d.ts +1 -0
- package/{builtin/skills/simplify/SKILL.md → dist/builtin/skills/simplify.js} +7 -3
- package/dist/builtin/subagents.d.ts +1 -0
- package/dist/builtin/subagents.js +164 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/managers/aiManager.d.ts +10 -0
- package/dist/managers/aiManager.js +49 -5
- package/dist/managers/mcpManager.js +1 -1
- package/dist/managers/messageManager.d.ts +6 -0
- package/dist/managers/messageManager.js +33 -0
- package/dist/managers/subagentManager.d.ts +9 -0
- package/dist/managers/subagentManager.js +62 -3
- package/dist/services/hook.js +41 -7
- package/dist/services/initializationService.js +0 -21
- package/dist/services/jsonlHandler.d.ts +37 -2
- package/dist/services/jsonlHandler.js +55 -6
- package/dist/services/session.d.ts +35 -4
- package/dist/services/session.js +233 -36
- package/dist/services/worktreeHooks.d.ts +45 -0
- package/dist/services/worktreeHooks.js +133 -0
- package/dist/tools/agentTool.js +36 -1
- package/dist/tools/bashTool.js +120 -57
- package/dist/tools/editTool.js +1 -0
- package/dist/tools/enterWorktreeTool.d.ts +1 -1
- package/dist/tools/enterWorktreeTool.js +44 -31
- package/dist/tools/exitWorktreeTool.js +21 -26
- package/dist/tools/readTool.js +6 -3
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/writeTool.js +1 -0
- package/dist/types/agent.d.ts +5 -0
- package/dist/types/hooks.d.ts +3 -2
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/skills.d.ts +0 -1
- package/dist/types/skills.js +0 -1
- package/dist/utils/asyncWorkRegistry.d.ts +32 -0
- package/dist/utils/asyncWorkRegistry.js +81 -0
- package/dist/utils/builtinEmbed.d.ts +21 -0
- package/dist/utils/builtinEmbed.js +53 -0
- package/dist/utils/configPaths.d.ts +0 -1
- package/dist/utils/configPaths.js +5 -26
- package/dist/utils/containerSetup.js +5 -0
- package/dist/utils/convertMessagesForAPI.js +21 -2
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/skillParser.js +3 -6
- package/dist/utils/toolImagePersistence.d.ts +25 -0
- package/dist/utils/toolImagePersistence.js +56 -0
- package/dist/utils/windowsPaths.d.ts +28 -0
- package/dist/utils/windowsPaths.js +47 -0
- package/dist/utils/worktreeSession.d.ts +6 -0
- package/dist/utils/worktreeUtils.d.ts +7 -0
- package/dist/utils/worktreeUtils.js +88 -40
- package/package.json +5 -5
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +0 -8
- package/builtin/plugins/sdd/hooks/hooks.json +0 -14
- package/builtin/plugins/sdd/scripts/session-start.js +0 -24
- package/builtin/plugins/sdd/scripts/spec-count.js +0 -77
- package/builtin/plugins/sdd/skills/specify/SKILL.md +0 -47
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +0 -47
- package/builtin/skills/artifact/SKILL.md +0 -14
- package/builtin/skills/loop/SKILL.md +0 -79
- package/builtin/skills/settings/ENV.md +0 -78
- package/builtin/skills/settings/HOOKS.md +0 -227
- package/builtin/skills/settings/MCP.md +0 -137
- package/builtin/skills/settings/MEMORY.md +0 -76
- package/builtin/skills/settings/MODELS.md +0 -119
- package/builtin/skills/settings/PERMISSIONS.md +0 -88
- package/builtin/skills/settings/PLUGINS.md +0 -171
- package/builtin/skills/settings/SKILL.md +0 -132
- package/builtin/skills/settings/SKILLS.md +0 -107
- package/builtin/skills/settings/SUBAGENTS.md +0 -77
- package/builtin/subagents/bash.md +0 -19
- package/builtin/subagents/explore.md +0 -43
- package/builtin/subagents/general-purpose.md +0 -20
- package/builtin/subagents/plan.md +0 -56
- package/builtin/subagents/vision.md +0 -18
package/dist/agent.d.ts
CHANGED
|
@@ -23,7 +23,9 @@ export declare class Agent {
|
|
|
23
23
|
private messageQueue;
|
|
24
24
|
private dispatchPromise;
|
|
25
25
|
private isAborting;
|
|
26
|
+
private isDestroyed;
|
|
26
27
|
private dispatchAborted;
|
|
28
|
+
private asyncWorkRegistry;
|
|
27
29
|
private memoryRuleManager;
|
|
28
30
|
private liveConfigManager;
|
|
29
31
|
private taskManager;
|
|
@@ -122,6 +124,13 @@ export declare class Agent {
|
|
|
122
124
|
}>;
|
|
123
125
|
type?: "message" | "bang";
|
|
124
126
|
}): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Terminal-lifecycle guard for public APIs. destroy() is terminal: after it
|
|
129
|
+
* returns, no new work may start on this agent. Silent drops hid misuse until
|
|
130
|
+
* a ghost async side effect surfaced later (issue #1808); throwing surfaces
|
|
131
|
+
* the contract violation immediately at the call site.
|
|
132
|
+
*/
|
|
133
|
+
private assertNotDestroyed;
|
|
125
134
|
/**
|
|
126
135
|
* Unified dispatch trigger — checks state machine before processing.
|
|
127
136
|
* Handles user messages, bang commands, and background task notifications
|
|
@@ -244,12 +253,6 @@ export declare class Agent {
|
|
|
244
253
|
* Background the current foreground task
|
|
245
254
|
*/
|
|
246
255
|
backgroundCurrentTask(): Promise<void>;
|
|
247
|
-
/**
|
|
248
|
-
* Trigger WorktreeRemove hook before agent destruction.
|
|
249
|
-
* Called from CLI exit dialog when user chooses to remove the worktree.
|
|
250
|
-
* Non-blocking: errors logged but don't prevent removal.
|
|
251
|
-
*/
|
|
252
|
-
triggerWorktreeRemoveHook(worktreePath: string): Promise<void>;
|
|
253
256
|
/** Destroy managers, clean up resources */
|
|
254
257
|
destroy(): Promise<void>;
|
|
255
258
|
/**
|
package/dist/agent.js
CHANGED
|
@@ -53,7 +53,8 @@ export class Agent {
|
|
|
53
53
|
constructor(options) {
|
|
54
54
|
this.bangManager = null;
|
|
55
55
|
this.dispatchPromise = null; // Track current dispatch for teardown
|
|
56
|
-
this.isAborting = false; //
|
|
56
|
+
this.isAborting = false; // Transient guard: prevents tryDispatch from firing during abortMessage (reset when the abort completes)
|
|
57
|
+
this.isDestroyed = false; // Terminal guard: set in destroy(), never reset — no dispatch may ever start after destroy
|
|
57
58
|
this.dispatchAborted = false; // Set on abort while a dispatch is running: suppress the .finally re-check so preserved notifications don't get dispatched after abort
|
|
58
59
|
this.sessionStartTime = Date.now();
|
|
59
60
|
const { logger, workdir, systemPrompt, stream = true } = options;
|
|
@@ -113,6 +114,7 @@ export class Agent {
|
|
|
113
114
|
this.bangManager = this.container.get("BangManager");
|
|
114
115
|
this.cronManager = this.container.get("CronManager");
|
|
115
116
|
this.messageQueue = this.container.get("MessageQueue");
|
|
117
|
+
this.asyncWorkRegistry = this.container.get("AsyncWorkRegistry");
|
|
116
118
|
// Wire up CWD change callback from AIManager to sync Agent's workdir
|
|
117
119
|
this.aiManager.setOnCwdChange((newCwd) => {
|
|
118
120
|
this.workdir = newCwd;
|
|
@@ -256,6 +258,17 @@ export class Agent {
|
|
|
256
258
|
}
|
|
257
259
|
return updated;
|
|
258
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Terminal-lifecycle guard for public APIs. destroy() is terminal: after it
|
|
263
|
+
* returns, no new work may start on this agent. Silent drops hid misuse until
|
|
264
|
+
* a ghost async side effect surfaced later (issue #1808); throwing surfaces
|
|
265
|
+
* the contract violation immediately at the call site.
|
|
266
|
+
*/
|
|
267
|
+
assertNotDestroyed() {
|
|
268
|
+
if (this.isDestroyed) {
|
|
269
|
+
throw new Error("Agent destroyed");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
259
272
|
/**
|
|
260
273
|
* Unified dispatch trigger — checks state machine before processing.
|
|
261
274
|
* Handles user messages, bang commands, and background task notifications
|
|
@@ -263,6 +276,8 @@ export class Agent {
|
|
|
263
276
|
* onLoadingChange(false), and onCommandRunningChange(false).
|
|
264
277
|
*/
|
|
265
278
|
tryDispatch() {
|
|
279
|
+
if (this.isDestroyed)
|
|
280
|
+
return; // Terminal: agent destroyed, never dispatch again
|
|
266
281
|
if (this.isAborting)
|
|
267
282
|
return; // Suppress dispatch during abort to prevent queued notifications from being dispatched as a side-effect
|
|
268
283
|
if (this.dispatchAborted)
|
|
@@ -274,7 +289,7 @@ export class Agent {
|
|
|
274
289
|
if (this.aiManager.isLoading || this.isCommandRunning)
|
|
275
290
|
return;
|
|
276
291
|
this.messageQueue.transitionTo("dispatching");
|
|
277
|
-
this.dispatchPromise = this.processQueuedMessage()
|
|
292
|
+
this.dispatchPromise = this.asyncWorkRegistry.track(this.processQueuedMessage()
|
|
278
293
|
.catch((error) => {
|
|
279
294
|
this.logger?.error("Failed to process queued message:", error);
|
|
280
295
|
})
|
|
@@ -285,7 +300,7 @@ export class Agent {
|
|
|
285
300
|
this.tryDispatch(); // Re-check after processing
|
|
286
301
|
}
|
|
287
302
|
this.dispatchAborted = false;
|
|
288
|
-
});
|
|
303
|
+
}));
|
|
289
304
|
}
|
|
290
305
|
/**
|
|
291
306
|
* Process the next queued item when the agent becomes idle.
|
|
@@ -499,6 +514,7 @@ export class Agent {
|
|
|
499
514
|
}
|
|
500
515
|
/** Execute bash command (bang command) */
|
|
501
516
|
async bang(command) {
|
|
517
|
+
this.assertNotDestroyed();
|
|
502
518
|
// If the agent is busy, enqueue the bang command
|
|
503
519
|
if (this.aiManager.isLoading || this.isCommandRunning) {
|
|
504
520
|
this.messageQueue.enqueue({ type: "bang", content: command });
|
|
@@ -625,37 +641,14 @@ export class Agent {
|
|
|
625
641
|
await this.foregroundTaskManager.backgroundCurrentTask();
|
|
626
642
|
this.options.callbacks?.onBackgroundCurrentTask?.();
|
|
627
643
|
}
|
|
628
|
-
/**
|
|
629
|
-
* Trigger WorktreeRemove hook before agent destruction.
|
|
630
|
-
* Called from CLI exit dialog when user chooses to remove the worktree.
|
|
631
|
-
* Non-blocking: errors logged but don't prevent removal.
|
|
632
|
-
*/
|
|
633
|
-
async triggerWorktreeRemoveHook(worktreePath) {
|
|
634
|
-
if (!this.hookManager.hasHooks("WorktreeRemove")) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
try {
|
|
638
|
-
const sessionId = this.messageManager.getSessionId();
|
|
639
|
-
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
640
|
-
const hookResults = await this.hookManager.executeHooks("WorktreeRemove", {
|
|
641
|
-
event: "WorktreeRemove",
|
|
642
|
-
projectDir: this.workdir,
|
|
643
|
-
timestamp: new Date(),
|
|
644
|
-
sessionId,
|
|
645
|
-
transcriptPath,
|
|
646
|
-
cwd: this.workdir,
|
|
647
|
-
worktreePath,
|
|
648
|
-
env: Object.fromEntries(Object.entries(this.configurationService.getMergedEnv()).filter((e) => e[1] !== undefined)),
|
|
649
|
-
});
|
|
650
|
-
// Process results via messageManager (may not be visible during shutdown)
|
|
651
|
-
this.hookManager.processHookResults("WorktreeRemove", hookResults, this.messageManager);
|
|
652
|
-
}
|
|
653
|
-
catch (error) {
|
|
654
|
-
this.logger?.warn("WorktreeRemove hooks execution failed:", error);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
644
|
/** Destroy managers, clean up resources */
|
|
658
645
|
async destroy() {
|
|
646
|
+
// Terminal guard: suppress any dispatch triggered during teardown (e.g. the
|
|
647
|
+
// onLoadingChange(false) fired by abortAIMessage() below, or the dispatch
|
|
648
|
+
// .finally re-check). Without this, leftover queued messages would be
|
|
649
|
+
// dispatched fire-and-forget and outlive the agent. Never reset: destroy()
|
|
650
|
+
// is terminal, no dispatch should ever start afterwards.
|
|
651
|
+
this.isDestroyed = true;
|
|
659
652
|
// Log session_end event and shutdown telemetry
|
|
660
653
|
await logOTelEvent("session_end", {
|
|
661
654
|
duration: String(Math.round((Date.now() - this.sessionStartTime) / 1000)),
|
|
@@ -717,7 +710,13 @@ export class Agent {
|
|
|
717
710
|
}
|
|
718
711
|
// Cleanup remote settings polling
|
|
719
712
|
remoteSettingsService.shutdown();
|
|
720
|
-
//
|
|
713
|
+
// Drain live async work (dispatch, background subagents, fork subagents):
|
|
714
|
+
// abort steps above make them settle; wait for them so no async side
|
|
715
|
+
// effect outlives the agent. Timeout fallback keeps destroy bounded.
|
|
716
|
+
const drained = await this.asyncWorkRegistry.drain();
|
|
717
|
+
if (!drained) {
|
|
718
|
+
this.logger?.error(`Async work did not drain: ${this.asyncWorkRegistry.size} live work item(s) remain after destroy`);
|
|
719
|
+
}
|
|
721
720
|
}
|
|
722
721
|
/**
|
|
723
722
|
* Trigger the rewind UI callback
|
|
@@ -738,6 +737,7 @@ export class Agent {
|
|
|
738
737
|
* @returns Promise that resolves to the AI's answer
|
|
739
738
|
*/
|
|
740
739
|
async askBtw(question, abortSignal, onContent, onReasoning) {
|
|
740
|
+
this.assertNotDestroyed();
|
|
741
741
|
const result = await this.aiManager.runBtwFork(question, abortSignal, onContent, onReasoning);
|
|
742
742
|
return result.content ?? result.error ?? "No response received";
|
|
743
743
|
}
|
|
@@ -760,6 +760,7 @@ export class Agent {
|
|
|
760
760
|
* @returns Promise that resolves to the background task ID
|
|
761
761
|
*/
|
|
762
762
|
async forkSubagent(prompt, options, abortSignal) {
|
|
763
|
+
this.assertNotDestroyed();
|
|
763
764
|
return this.aiManager.runForkSubagent(prompt, options, abortSignal);
|
|
764
765
|
}
|
|
765
766
|
/**
|
|
@@ -789,6 +790,7 @@ export class Agent {
|
|
|
789
790
|
* ```
|
|
790
791
|
*/
|
|
791
792
|
async sendMessage(content, images) {
|
|
793
|
+
this.assertNotDestroyed();
|
|
792
794
|
// If the agent is busy, enqueue the message — unless it's an immediate
|
|
793
795
|
// slash command (e.g., /clear, /compact) that should execute
|
|
794
796
|
// right away even while AI is processing
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BUILTIN_CONTENT: Record<string, string>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { artifactSkill } from "./skills/artifact.js";
|
|
2
|
+
import { code_reviewSkill } from "./skills/code-review.js";
|
|
3
|
+
import { deep_researchSkill } from "./skills/deep-research.js";
|
|
4
|
+
import { initSkill } from "./skills/init.js";
|
|
5
|
+
import { loopSkill } from "./skills/loop.js";
|
|
6
|
+
import { simplifySkill } from "./skills/simplify.js";
|
|
7
|
+
import { settingsSkills } from "./skills/settings.js";
|
|
8
|
+
import { subagents } from "./subagents.js";
|
|
9
|
+
import { sddPlugin } from "./plugins.js";
|
|
10
|
+
export const BUILTIN_CONTENT = {
|
|
11
|
+
...artifactSkill,
|
|
12
|
+
...code_reviewSkill,
|
|
13
|
+
...deep_researchSkill,
|
|
14
|
+
...initSkill,
|
|
15
|
+
...loopSkill,
|
|
16
|
+
...simplifySkill,
|
|
17
|
+
...settingsSkills,
|
|
18
|
+
...subagents,
|
|
19
|
+
...sddPlugin,
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sddPlugin: Record<string, string>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export const sddPlugin = {
|
|
2
|
+
"plugins/sdd/.wave-plugin/plugin.json": `{
|
|
3
|
+
"name": "sdd",
|
|
4
|
+
"description": "Spec-first workflow: specify skill, SessionStart guidance, and spec-count validation.",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Wave Team"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
`,
|
|
11
|
+
"plugins/sdd/hooks/hooks.json": `{
|
|
12
|
+
"hooks": {
|
|
13
|
+
"SessionStart": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \\"\${WAVE_PLUGIN_ROOT}/scripts/session-start.js\\""
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
"plugins/sdd/scripts/session-start.js": `#!/usr/bin/env node
|
|
27
|
+
// SessionStart hook for the sdd built-in plugin.
|
|
28
|
+
// Emits the spec-first workflow guidance as additionalContext (JSON form),
|
|
29
|
+
// resolving the absolute path to the plugin's spec-count validator so the
|
|
30
|
+
// agent can run it from its bash tool (which does not carry WAVE_PLUGIN_ROOT).
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
const root =
|
|
34
|
+
process.env.WAVE_PLUGIN_ROOT ||
|
|
35
|
+
path.dirname(new URL("..", import.meta.url).pathname);
|
|
36
|
+
const specCount = \`node \${JSON.stringify(path.join(root, "scripts", "spec-count.js"))}\`;
|
|
37
|
+
|
|
38
|
+
const guidance = [
|
|
39
|
+
"Spec-First Workflow(规格优先工作流):",
|
|
40
|
+
"- 需求增加或变更时,优先更新 spec:先更新对应规格说明(新增用户故事、验收场景),待用户确认 spec 后再实现代码。spec 是功能设计的权威来源,不是 changelog。",
|
|
41
|
+
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
42
|
+
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
43
|
+
\`- 新增或修改 spec 后运行校验:\${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。\`,
|
|
44
|
+
].join("\\n");
|
|
45
|
+
|
|
46
|
+
// JSON form → parsed as hookSpecificOutput.additionalContext by the hook manager.
|
|
47
|
+
console.log(
|
|
48
|
+
JSON.stringify({ hookSpecificOutput: { additionalContext: guidance } }),
|
|
49
|
+
);
|
|
50
|
+
`,
|
|
51
|
+
"plugins/sdd/scripts/spec-count.js": `#!/usr/bin/env node
|
|
52
|
+
// Generic, self-contained spec validator. Counts user stories and acceptance
|
|
53
|
+
// scenarios under the project's specs directory and warns on missing sections.
|
|
54
|
+
// Detects the specs dir: prefers docs/specs/, else specs/, else exits gracefully.
|
|
55
|
+
// No dependency on any project's VitePress/docs-site modules.
|
|
56
|
+
import fs from "node:fs";
|
|
57
|
+
import path from "node:path";
|
|
58
|
+
|
|
59
|
+
function detectSpecsDir() {
|
|
60
|
+
for (const dir of ["docs/specs", "specs"]) {
|
|
61
|
+
const resolved = path.resolve(process.cwd(), dir);
|
|
62
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function walk(dir, out = []) {
|
|
68
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
69
|
+
const fullPath = path.join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) walk(fullPath, out);
|
|
71
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) out.push(fullPath);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countUserStories(content) {
|
|
77
|
+
const m = content.match(/^### 用户故事[::]/gm);
|
|
78
|
+
return m ? m.length : 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countAcceptanceScenarios(content) {
|
|
82
|
+
const m = content.match(/^\\d+\\.\\s+\\*\\*假设\\*\\*/gm);
|
|
83
|
+
return m ? m.length : 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const specsDir = detectSpecsDir();
|
|
87
|
+
if (!specsDir) {
|
|
88
|
+
console.log("未找到规格目录(docs/specs/ 或 specs/),跳过校验。");
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const files = walk(specsDir).sort();
|
|
93
|
+
const totals = { specs: 0, us: 0, ac: 0 };
|
|
94
|
+
const warnings = [];
|
|
95
|
+
|
|
96
|
+
// index.md is conventionally a directory listing page, not a spec — skip it.
|
|
97
|
+
const specFiles = files.filter(
|
|
98
|
+
(fp) => path.basename(fp).toLowerCase() !== "index.md",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (const fp of specFiles) {
|
|
102
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
103
|
+
const usCount = countUserStories(content);
|
|
104
|
+
const acCount = countAcceptanceScenarios(content);
|
|
105
|
+
const rel = path.relative(process.cwd(), fp);
|
|
106
|
+
totals.specs++;
|
|
107
|
+
totals.us += usCount;
|
|
108
|
+
totals.ac += acCount;
|
|
109
|
+
if (!content.match(/^## 用户场景与测试/m))
|
|
110
|
+
warnings.push(\`\${rel}: 缺少 "## 用户场景与测试" 章节\`);
|
|
111
|
+
if (usCount === 0)
|
|
112
|
+
warnings.push(\`\${rel}: 未找到用户故事(期望 \\\`### 用户故事:\\\`)\`);
|
|
113
|
+
if (acCount === 0)
|
|
114
|
+
warnings.push(
|
|
115
|
+
\`\${rel}: 未找到验收场景(期望 \\\`N. **假设** … **当** … **则** …\\\`)\`,
|
|
116
|
+
);
|
|
117
|
+
console.log(\`\${rel} 用户故事: \${usCount} 验收场景: \${acCount}\`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log("---");
|
|
121
|
+
console.log(
|
|
122
|
+
\`规格: \${totals.specs} 用户故事: \${totals.us} 验收场景: \${totals.ac}\`,
|
|
123
|
+
);
|
|
124
|
+
if (warnings.length) {
|
|
125
|
+
for (const w of warnings) console.warn(\`⚠ \${w}\`);
|
|
126
|
+
console.warn(\`⚠ \${warnings.length} 条模板警告——见上方。\`);
|
|
127
|
+
}
|
|
128
|
+
`,
|
|
129
|
+
"plugins/sdd/skills/specify/SKILL.md": `---
|
|
130
|
+
name: specify
|
|
131
|
+
description: 根据自然语言描述创建或更新功能规格说明,生成包含用户故事与验收场景的规格文件。
|
|
132
|
+
user-invocable: false
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 用户输入
|
|
136
|
+
|
|
137
|
+
\`\`\`text
|
|
138
|
+
$ARGUMENTS
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
## 流程
|
|
142
|
+
|
|
143
|
+
本技能由 AI 在会话中自动触发(不占用手动斜杠命令)。触发时机:用户提出新的需求、修改需求或涉及功能边界时,若对应规格尚未创建或已过期,则主动创建或更新规格说明。$ARGUMENTS 通常为空——需求描述直接来自对话上下文,不要让用户重复。
|
|
144
|
+
|
|
145
|
+
根据对话中的功能描述,执行以下步骤:
|
|
146
|
+
|
|
147
|
+
1. **确定规格文件路径**:
|
|
148
|
+
- **确定规格根目录**:优先复用项目中已有的规格目录——若 \`docs/specs/\` 存在则用之,否则若 \`specs/\` 存在则用之,否则默认 \`specs/\`(并在完成报告中说明所选目录,便于用户纠正)。
|
|
149
|
+
- **选择分组**:若规格目录下已有分组子目录,沿用其既有分组约定;否则默认扁平结构(直接放在规格根目录下)。
|
|
150
|
+
- 根据功能描述生成 2-4 个词的 slug(小写、连字符、保留缩写词),与组内已有文件名不冲突
|
|
151
|
+
- 规格文件路径:\`<规格根目录>/<分组>/<slug>.md\`(无分组时为 \`<规格根目录>/<slug>.md\`)
|
|
152
|
+
|
|
153
|
+
2. **加载模板** \`\${WAVE_SKILL_DIR}/templates/spec-template.md\`,了解必需章节。
|
|
154
|
+
|
|
155
|
+
3. **编写规格说明**:
|
|
156
|
+
- 解析用户描述,提取关键概念:角色、操作、数据、约束
|
|
157
|
+
- 对于不明确的部分,根据上下文和行业标准做出合理推断
|
|
158
|
+
- 仅在关键决策处标记 \`[待澄清:具体问题]\`(最多 3 处)
|
|
159
|
+
- 填写 frontmatter(\`name\` 为功能中文名、\`description\` 为一句话简述、\`order\` 为控制组内排序的数字)
|
|
160
|
+
- 填写「用户场景与测试」章节,包含按优先级排序的用户故事(P1、P2、P3...),每个故事以「作为…,我希望…,以便…」描述,附 \`**为什么是这个优先级**\` 与 \`**独立测试**\`(不适用的可省略)
|
|
161
|
+
- 为每个用户故事编写可测试的验收场景(**假设** … **当** … **则** …)
|
|
162
|
+
- 写入规格文件,替换所有占位符
|
|
163
|
+
|
|
164
|
+
4. **如果存在 \`[待澄清]\` 标记**(最多 3 处):
|
|
165
|
+
- 将每个标记作为问题展示,附带建议答案
|
|
166
|
+
- 等待用户回复后更新规格文件
|
|
167
|
+
|
|
168
|
+
5. 报告完成,输出规格文件路径。
|
|
169
|
+
|
|
170
|
+
## 指南
|
|
171
|
+
|
|
172
|
+
- 关注用户**需要什麼**和**为什么**,而非如何实现
|
|
173
|
+
- 不包含实现细节(不涉及技术栈、API、代码结构)
|
|
174
|
+
- 每个验收场景必须可测试、无歧义
|
|
175
|
+
- 删除不适用的可选章节(不要留 "N/A")
|
|
176
|
+
`,
|
|
177
|
+
"plugins/sdd/skills/specify/templates/spec-template.md": `---
|
|
178
|
+
name: "[功能名称]"
|
|
179
|
+
description: "[一句话简短描述]"
|
|
180
|
+
order: [数字,控制组内排序]
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
# 功能规格说明:[功能名称]
|
|
184
|
+
|
|
185
|
+
**创建日期**:[日期]
|
|
186
|
+
|
|
187
|
+
## 用户场景与测试 *(必填)*
|
|
188
|
+
|
|
189
|
+
### 用户故事:[简要标题](优先级:P1)
|
|
190
|
+
|
|
191
|
+
作为[角色],我希望[操作],以便[价值/目的]。
|
|
192
|
+
|
|
193
|
+
**为什么是这个优先级**:[解释其价值以及为何具有此优先级]
|
|
194
|
+
|
|
195
|
+
**独立测试**:[描述如何独立测试——例如,"可以通过 [具体操作] 进行完整测试,并交付 [具体价值]"]
|
|
196
|
+
|
|
197
|
+
**验收场景**:
|
|
198
|
+
|
|
199
|
+
1. **假设** [初始状态],**当** [操作],**则** [预期结果]
|
|
200
|
+
2. **假设** [初始状态],**当** [操作],**则** [预期结果]
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### 用户故事:[简要标题](优先级:P2)
|
|
205
|
+
|
|
206
|
+
作为[角色],我希望[操作],以便[价值/目的]。
|
|
207
|
+
|
|
208
|
+
**为什么是这个优先级**:[解释其价值以及为何具有此优先级]
|
|
209
|
+
|
|
210
|
+
**独立测试**:[描述如何独立测试]
|
|
211
|
+
|
|
212
|
+
**验收场景**:
|
|
213
|
+
|
|
214
|
+
1. **假设** [初始状态],**当** [操作],**则** [预期结果]
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
[根据需要添加更多用户故事,每个都分配优先级]
|
|
219
|
+
|
|
220
|
+
### 边界情况
|
|
221
|
+
|
|
222
|
+
- **[问题?]** [答案/处理方式]
|
|
223
|
+
- **[问题?]** [答案/处理方式]
|
|
224
|
+
`,
|
|
225
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const artifactSkill: Record<string, string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ARTIFACT_TOOL_NAME } from "../../constants/tools.js";
|
|
2
|
+
export const artifactSkill = {
|
|
3
|
+
"skills/artifact/SKILL.md": `---
|
|
4
|
+
name: artifact
|
|
5
|
+
description: Publish a local HTML or Markdown file as a shareable web page
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Artifact: Publish a File as a Shareable Web Page
|
|
10
|
+
|
|
11
|
+
Publish a local \`.html\` or \`.md\` file as a default-private, shareable web page.
|
|
12
|
+
|
|
13
|
+
- If a file path was provided ($ARGUMENTS / $1), use it directly as the \`file_path\`.
|
|
14
|
+
- Otherwise, infer which file to publish from the conversation context; if it is not clear, ask the user which file to publish.
|
|
15
|
+
|
|
16
|
+
Call the \`${ARTIFACT_TOOL_NAME}\` tool with the resolved \`file_path\` (and \`favicon\` if relevant), then report the resulting URL to the user.
|
|
17
|
+
`,
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const code_reviewSkill: Record<string, string>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { AGENT_TOOL_NAME, BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, } from "../../constants/tools.js";
|
|
2
|
+
export const code_reviewSkill = {
|
|
3
|
+
"skills/code-review/SKILL.md": `---
|
|
2
4
|
name: code-review
|
|
3
5
|
description: Review the current diff for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high/max: broader coverage, may include lower-confidence findings)
|
|
4
|
-
allowed-tools:
|
|
6
|
+
allowed-tools: ${BASH_TOOL_NAME}(git diff:*), ${BASH_TOOL_NAME}(git status:*), ${BASH_TOOL_NAME}(git log:*), ${BASH_TOOL_NAME}(git show:*), ${BASH_TOOL_NAME}(git blame:*), ${BASH_TOOL_NAME}(git remote:*), ${BASH_TOOL_NAME}(command -v:*), ${BASH_TOOL_NAME}(gh pr comment:*), ${BASH_TOOL_NAME}(gh pr view:*), ${BASH_TOOL_NAME}(glab mr note:*), ${BASH_TOOL_NAME}(glab mr view:*), ${READ_TOOL_NAME}, ${GLOB_TOOL_NAME}, ${GREP_TOOL_NAME}, ${AGENT_TOOL_NAME}
|
|
5
7
|
disable-model-invocation: true
|
|
6
8
|
---
|
|
7
9
|
|
|
@@ -13,25 +15,25 @@ Review the current branch's changes for correctness bugs and quality issues.
|
|
|
13
15
|
|
|
14
16
|
First, collect the diff and metadata:
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
\`\`\`
|
|
17
19
|
GIT STATUS:
|
|
18
|
-
|
|
20
|
+
!\`git status\`
|
|
19
21
|
|
|
20
22
|
FILES MODIFIED:
|
|
21
|
-
|
|
23
|
+
!\`git diff --name-only $(git merge-base HEAD main)...HEAD 2>/dev/null || git diff --name-only HEAD~1...HEAD\`
|
|
22
24
|
|
|
23
25
|
COMMITS:
|
|
24
|
-
|
|
26
|
+
!\`git log --no-decorate $(git merge-base HEAD main 2>/dev/null || echo HEAD~1)...HEAD\`
|
|
25
27
|
|
|
26
28
|
DIFF CONTENT:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
!\`git diff $(git merge-base HEAD main 2>/dev/null || echo HEAD~1)...HEAD\`
|
|
30
|
+
\`\`\`
|
|
29
31
|
|
|
30
32
|
If there are no changes, stop and tell the user.
|
|
31
33
|
|
|
32
34
|
## Phase 2: Determine Effort Level
|
|
33
35
|
|
|
34
|
-
Parse
|
|
36
|
+
Parse \`$ARGUMENTS\` for an effort level:
|
|
35
37
|
- **low**: Launch 2 agents, confidence threshold 90 (only near-certain findings)
|
|
36
38
|
- **medium** (default): Launch 3 agents, confidence threshold 80
|
|
37
39
|
- **high**: Launch 4 agents, confidence threshold 70
|
|
@@ -39,7 +41,7 @@ Parse `$ARGUMENTS` for an effort level:
|
|
|
39
41
|
|
|
40
42
|
## Phase 3: Launch Review Agents in Parallel
|
|
41
43
|
|
|
42
|
-
Use the
|
|
44
|
+
Use the ${AGENT_TOOL_NAME} tool to launch all agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
|
|
43
45
|
|
|
44
46
|
### Agent 1: Bug Scanner (all effort levels)
|
|
45
47
|
|
|
@@ -63,7 +65,7 @@ Review the changes for efficiency: unnecessary work, missed concurrency, hot-pat
|
|
|
63
65
|
|
|
64
66
|
## Phase 4: Confidence Scoring
|
|
65
67
|
|
|
66
|
-
For each issue found in Phase 3, launch a parallel
|
|
68
|
+
For each issue found in Phase 3, launch a parallel ${AGENT_TOOL_NAME} to independently score the issue. The scoring agent receives the PR diff, the issue description, and the list of AGENTS.md files (if any). It returns a confidence score from 0-100.
|
|
67
69
|
|
|
68
70
|
Give the scoring agent this rubric verbatim:
|
|
69
71
|
|
|
@@ -79,18 +81,18 @@ Give the scoring agent this rubric verbatim:
|
|
|
79
81
|
2. If no issues remain, say so and stop — do not post anything.
|
|
80
82
|
3. Otherwise, detect the platform and CLI availability:
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
\`\`\`
|
|
83
85
|
REMOTE URL:
|
|
84
|
-
|
|
86
|
+
!\`git remote get-url origin 2>/dev/null || echo "no-remote"\`
|
|
85
87
|
|
|
86
88
|
GH CLI:
|
|
87
|
-
|
|
89
|
+
!\`command -v gh 2>/dev/null || echo "not-installed"\`
|
|
88
90
|
|
|
89
91
|
GLAB CLI:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
!\`command -v glab 2>/dev/null || echo "not-installed"\`
|
|
93
|
+
\`\`\`
|
|
92
94
|
|
|
93
|
-
4. **Post as comment** (preferred): If the remote URL contains
|
|
95
|
+
4. **Post as comment** (preferred): If the remote URL contains \`github\` and \`gh\` is installed, check if a PR exists for the current branch (\`gh pr view --json number\`), then post the review as a comment: \`gh pr comment --body "<review content>"\`. If the remote URL contains \`gitlab\` and \`glab\` is installed, check if an MR exists for the current branch (\`glab mr view\`), then post the review as a note: \`glab mr note --message "<review content>"\`.
|
|
94
96
|
5. **Output directly** (fallback): If no CLI is installed, no PR/MR exists, or the platform is unrecognized, output the findings directly instead.
|
|
95
97
|
|
|
96
98
|
Whether posting or outputting, use this format:
|
|
@@ -103,11 +105,11 @@ Found N issues:
|
|
|
103
105
|
|
|
104
106
|
1. <brief description of bug> (AGENTS.md says "<...>")
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
\`<file>:<line range>\`
|
|
107
109
|
|
|
108
110
|
2. <brief description of bug> (bug due to <file and code snippet>)
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
\`<file>:<line range>\`
|
|
111
113
|
|
|
112
114
|
---
|
|
113
115
|
|
|
@@ -135,3 +137,5 @@ Examples of false positives to exclude:
|
|
|
135
137
|
## Input
|
|
136
138
|
|
|
137
139
|
$ARGUMENTS
|
|
140
|
+
`,
|
|
141
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const deep_researchSkill: Record<string, string>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { WORKFLOW_TOOL_NAME } from "../../constants/tools.js";
|
|
2
|
+
export const deep_researchSkill = {
|
|
3
|
+
"skills/deep-research/SKILL.md": `---
|
|
2
4
|
name: deep-research
|
|
3
5
|
description: Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report
|
|
4
6
|
context: fork
|
|
@@ -8,9 +10,9 @@ Deep research harness — fan-out web searches, fetch sources, adversarially ver
|
|
|
8
10
|
|
|
9
11
|
BEFORE invoking the workflow, check if the question is specific enough to research directly — if underspecified (e.g., "what car to buy" without budget/use-case/region), ask 2-3 clarifying questions to narrow scope. Then pass the refined question as args, weaving the answers in.
|
|
10
12
|
|
|
11
|
-
1. Call the
|
|
13
|
+
1. Call the ${WORKFLOW_TOOL_NAME} tool with the following script, passing the user's question as the \`args\` parameter:
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
\`\`\`
|
|
14
16
|
export const meta = {
|
|
15
17
|
name: 'deep-research',
|
|
16
18
|
description: 'Deep research harness — fan-out web searches, fetch sources, adversarially verify claims, synthesize a cited report',
|
|
@@ -27,12 +29,12 @@ const question = args
|
|
|
27
29
|
|
|
28
30
|
phase('Scope')
|
|
29
31
|
const angles = await agent(
|
|
30
|
-
'Decompose this research question into exactly 5 distinct search angles that together provide comprehensive coverage: "' + question + '"
|
|
32
|
+
'Decompose this research question into exactly 5 distinct search angles that together provide comprehensive coverage: "' + question + '"\\n\\nReturn a JSON array of 5 short angle labels. Example: ["overview and background", "technical details and specifications", "recent developments and news", "alternative perspectives and criticisms", "practical implications and case studies"]',
|
|
31
33
|
{ label: 'scope', phase: 'Scope' }
|
|
32
34
|
)
|
|
33
35
|
const angleList = (() => {
|
|
34
36
|
try {
|
|
35
|
-
const match = String(angles).match(
|
|
37
|
+
const match = String(angles).match(/\\[[\\s\\S]*\\]/)
|
|
36
38
|
return match ? JSON.parse(match[0]) : ['overview', 'technical details', 'recent developments', 'alternative perspectives', 'practical implications']
|
|
37
39
|
} catch { return ['overview', 'technical details', 'recent developments', 'alternative perspectives', 'practical implications'] }
|
|
38
40
|
})()
|
|
@@ -48,8 +50,8 @@ const searches = await parallel(angleList.map(angle => () =>
|
|
|
48
50
|
phase('Fetch')
|
|
49
51
|
const allUrls = searches.filter(Boolean).flatMap(r => {
|
|
50
52
|
if (typeof r !== 'string') return []
|
|
51
|
-
return r.split('
|
|
52
|
-
}).map(u => u.replace(
|
|
53
|
+
return r.split('\\n').filter(l => l.trim().startsWith('http') || l.trim().startsWith('- http'))
|
|
54
|
+
}).map(u => u.replace(/^-\\s*/, '').trim())
|
|
53
55
|
const sources = [...new Set(allUrls)].slice(0, 15)
|
|
54
56
|
const fetched = await pipeline(sources.length > 0 ? sources : ['No specific URLs found — synthesize from search results'], source =>
|
|
55
57
|
agent('Fetch and extract key falsifiable claims from: ' + source + '. For each claim, note the source URL and whether it is supported by evidence. Return each claim as a separate bullet.', {
|
|
@@ -59,12 +61,12 @@ const fetched = await pipeline(sources.length > 0 ? sources : ['No specific URLs
|
|
|
59
61
|
)
|
|
60
62
|
|
|
61
63
|
phase('Verify')
|
|
62
|
-
const claimsText = fetched.filter(Boolean).join('
|
|
63
|
-
const claimLines = claimsText.split('
|
|
64
|
+
const claimsText = fetched.filter(Boolean).join('\\n')
|
|
65
|
+
const claimLines = claimsText.split('\\n').filter(l => l.trim().startsWith('-') || l.trim().startsWith('•') || l.trim().startsWith('*'))
|
|
64
66
|
const claimsToVerify = claimLines.length > 0 ? claimLines : [claimsText]
|
|
65
67
|
const verified = await parallel(claimsToVerify.slice(0, 12).map((claim, i) => () =>
|
|
66
68
|
parallel([0, 1, 2].map(voteIdx => () =>
|
|
67
|
-
agent('You are a skeptical fact-checker (vote ' + (voteIdx + 1) + '/3). For the following claim, search for evidence that REFUTES it. Claim: "' + claim + '"
|
|
69
|
+
agent('You are a skeptical fact-checker (vote ' + (voteIdx + 1) + '/3). For the following claim, search for evidence that REFUTES it. Claim: "' + claim + '"\\n\\nIf you find strong evidence against it, respond REFUTED with the counter-evidence. If the claim holds up, respond CONFIRMED. Be rigorous — only refute if you find direct contradictory evidence.', {
|
|
68
70
|
label: 'verify:claim' + i + ':vote' + voteIdx,
|
|
69
71
|
phase: 'Verify'
|
|
70
72
|
})
|
|
@@ -77,14 +79,16 @@ const verifiedClaims = verified.flatMap((votes, i) => {
|
|
|
77
79
|
})
|
|
78
80
|
|
|
79
81
|
phase('Synthesize')
|
|
80
|
-
const confirmedClaims = verifiedClaims.filter(c => c.status === 'confirmed').map(c => c.claim).join('
|
|
81
|
-
const contestedClaims = verifiedClaims.filter(c => c.status === 'contested').map(c => c.claim + ' (contested)').join('
|
|
82
|
+
const confirmedClaims = verifiedClaims.filter(c => c.status === 'confirmed').map(c => c.claim).join('\\n')
|
|
83
|
+
const contestedClaims = verifiedClaims.filter(c => c.status === 'contested').map(c => c.claim + ' (contested)').join('\\n')
|
|
82
84
|
const report = await agent(
|
|
83
|
-
'Synthesize a comprehensive cited report answering: "' + question + '"
|
|
85
|
+
'Synthesize a comprehensive cited report answering: "' + question + '".\\n\\nConfirmed claims:\\n' + (confirmedClaims || 'None') + '\\n\\nContested claims (include with caveat):\\n' + (contestedClaims || 'None') + '\\n\\nMerge semantic duplicates. Rank findings by confidence level. Include: 1) Executive summary 2) Key findings with citations, ranked by confidence 3) Contested findings (with caveats) 4) Areas of uncertainty 5) Conclusion.',
|
|
84
86
|
{ label: 'synthesize', phase: 'Synthesize' }
|
|
85
87
|
)
|
|
86
88
|
|
|
87
89
|
return report
|
|
88
|
-
|
|
90
|
+
\`\`\`
|
|
89
91
|
|
|
90
92
|
2. After the workflow starts, inform the user that deep research is running and they can use /workflows to monitor progress.
|
|
93
|
+
`,
|
|
94
|
+
};
|