@s_s/harmonia 1.3.0 → 1.4.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/README.md +140 -392
- package/build/cli/setup.d.ts +4 -2
- package/build/cli/setup.js +44 -18
- package/build/cli/setup.js.map +1 -1
- package/build/core/action-registry.d.ts +36 -0
- package/build/core/action-registry.js +53 -0
- package/build/core/action-registry.js.map +1 -0
- package/build/core/artifacts.d.ts +66 -0
- package/build/core/artifacts.js +178 -0
- package/build/core/artifacts.js.map +1 -0
- package/build/core/dispatch.d.ts +6 -2
- package/build/core/dispatch.js +12 -7
- package/build/core/dispatch.js.map +1 -1
- package/build/core/overrides.d.ts +19 -26
- package/build/core/overrides.js +32 -98
- package/build/core/overrides.js.map +1 -1
- package/build/core/plugin.d.ts +86 -0
- package/build/core/plugin.js +332 -0
- package/build/core/plugin.js.map +1 -0
- package/build/core/registry.d.ts +4 -5
- package/build/core/registry.js +8 -9
- package/build/core/registry.js.map +1 -1
- package/build/core/reviews.d.ts +11 -12
- package/build/core/reviews.js +18 -21
- package/build/core/reviews.js.map +1 -1
- package/build/core/schema.d.ts +43 -15
- package/build/core/schema.js +124 -20
- package/build/core/schema.js.map +1 -1
- package/build/core/state.d.ts +26 -27
- package/build/core/state.js +36 -90
- package/build/core/state.js.map +1 -1
- package/build/core/steps.d.ts +13 -14
- package/build/core/steps.js +26 -29
- package/build/core/steps.js.map +1 -1
- package/build/core/tree-utils.d.ts +52 -0
- package/build/core/tree-utils.js +226 -0
- package/build/core/tree-utils.js.map +1 -0
- package/build/core/types.d.ts +389 -118
- package/build/core/types.js +15 -1
- package/build/core/types.js.map +1 -1
- package/build/core/workflow-engine.d.ts +68 -0
- package/build/core/workflow-engine.js +821 -0
- package/build/core/workflow-engine.js.map +1 -0
- package/build/core/workflow-validator.d.ts +22 -0
- package/build/core/workflow-validator.js +489 -0
- package/build/core/workflow-validator.js.map +1 -0
- package/build/index.js +25 -26
- package/build/index.js.map +1 -1
- package/build/setup/inject.d.ts +4 -4
- package/build/setup/inject.js +6 -6
- package/build/setup/inject.js.map +1 -1
- package/build/setup/templates.d.ts +9 -7
- package/build/setup/templates.js +68 -172
- package/build/setup/templates.js.map +1 -1
- package/build/tools/artifact-approve.d.ts +8 -0
- package/build/tools/{approve-doc.js → artifact-approve.js} +24 -16
- package/build/tools/artifact-approve.js.map +1 -0
- package/build/tools/artifact-schema.d.ts +12 -0
- package/build/tools/artifact-schema.js +148 -0
- package/build/tools/artifact-schema.js.map +1 -0
- package/build/tools/artifact-tools.d.ts +18 -0
- package/build/tools/artifact-tools.js +465 -0
- package/build/tools/artifact-tools.js.map +1 -0
- package/build/tools/{report-dispatch.d.ts → dispatch-report.d.ts} +7 -3
- package/build/tools/{report-dispatch.js → dispatch-report.js} +106 -28
- package/build/tools/dispatch-report.js.map +1 -0
- package/build/tools/engine-helpers.d.ts +41 -0
- package/build/tools/engine-helpers.js +182 -0
- package/build/tools/engine-helpers.js.map +1 -0
- package/build/tools/get-project-status.d.ts +6 -4
- package/build/tools/get-project-status.js +265 -248
- package/build/tools/get-project-status.js.map +1 -1
- package/build/tools/get-role-prompt.d.ts +1 -1
- package/build/tools/get-role-prompt.js +7 -41
- package/build/tools/get-role-prompt.js.map +1 -1
- package/build/tools/iteration-start.d.ts +7 -4
- package/build/tools/iteration-start.js +45 -19
- package/build/tools/iteration-start.js.map +1 -1
- package/build/tools/loop-done.d.ts +11 -0
- package/build/tools/loop-done.js +109 -0
- package/build/tools/loop-done.js.map +1 -0
- package/build/tools/patch-start.d.ts +4 -2
- package/build/tools/patch-start.js +36 -11
- package/build/tools/patch-start.js.map +1 -1
- package/build/tools/project-init.d.ts +5 -5
- package/build/tools/project-init.js +41 -10
- package/build/tools/project-init.js.map +1 -1
- package/build/tools/role-dispatch.d.ts +55 -0
- package/build/tools/role-dispatch.js +508 -0
- package/build/tools/role-dispatch.js.map +1 -0
- package/build/tools/utils.d.ts +6 -0
- package/build/tools/utils.js +36 -0
- package/build/tools/utils.js.map +1 -1
- package/package.json +1 -1
- package/{build/hooks/claude-code.js → workflows/dev/hooks/claude.js} +34 -23
- package/{build → workflows/dev}/hooks/content.js +27 -18
- package/workflows/dev/hooks/index.js +52 -0
- package/{build → workflows/dev}/hooks/openclaw.js +31 -20
- package/{build → workflows/dev}/hooks/opencode.js +31 -20
- package/workflows/dev/roles/architect.md +68 -28
- package/workflows/dev/roles/coordinator.md +103 -0
- package/workflows/dev/roles/developer.md +5 -5
- package/workflows/dev/roles/tester.md +19 -19
- package/workflows/dev/schemas/api-contract.json +42 -0
- package/workflows/dev/schemas/api-design.json +30 -13
- package/workflows/dev/schemas/data-model.json +20 -7
- package/workflows/dev/schemas/prd.completeness-check.json +6 -5
- package/workflows/dev/schemas/prd.draft.json +13 -5
- package/workflows/dev/schemas/prd.final.json +34 -11
- package/workflows/dev/schemas/prd.json +29 -11
- package/workflows/dev/schemas/prd.requirements.json +6 -5
- package/workflows/dev/schemas/prototype.json +6 -2
- package/workflows/dev/schemas/task-breakdown.coarse.json +4 -3
- package/workflows/dev/schemas/task-breakdown.dependencies.json +5 -4
- package/workflows/dev/schemas/task-breakdown.detailed.json +8 -3
- package/workflows/dev/schemas/task-breakdown.final.json +8 -3
- package/workflows/dev/schemas/task-breakdown.json +8 -3
- package/workflows/dev/schemas/tech-design.analysis.json +6 -5
- package/workflows/dev/schemas/tech-design.draft.json +14 -5
- package/workflows/dev/schemas/tech-design.final.json +39 -13
- package/workflows/dev/schemas/tech-design.json +34 -13
- package/workflows/dev/schemas/tech-design.research.json +21 -0
- package/workflows/dev/schemas/test-plan.json +17 -7
- package/workflows/dev/schemas/test-report.json +26 -9
- package/workflows/dev/schemas/user-stories.json +7 -3
- package/workflows/dev/tools/index.js +23 -0
- package/workflows/dev/workflow.json +234 -101
- package/build/core/docs.d.ts +0 -36
- package/build/core/docs.js +0 -96
- package/build/core/docs.js.map +0 -1
- package/build/core/workflow.d.ts +0 -33
- package/build/core/workflow.js +0 -140
- package/build/core/workflow.js.map +0 -1
- package/build/hooks/claude-code.d.ts +0 -20
- package/build/hooks/claude-code.js.map +0 -1
- package/build/hooks/content.d.ts +0 -43
- package/build/hooks/content.js.map +0 -1
- package/build/hooks/install.d.ts +0 -40
- package/build/hooks/install.js +0 -63
- package/build/hooks/install.js.map +0 -1
- package/build/hooks/openclaw.d.ts +0 -24
- package/build/hooks/openclaw.js.map +0 -1
- package/build/hooks/opencode.d.ts +0 -29
- package/build/hooks/opencode.js.map +0 -1
- package/build/tools/approve-doc.d.ts +0 -6
- package/build/tools/approve-doc.js.map +0 -1
- package/build/tools/dispatch-role.d.ts +0 -16
- package/build/tools/dispatch-role.js +0 -266
- package/build/tools/dispatch-role.js.map +0 -1
- package/build/tools/doc-tools.d.ts +0 -16
- package/build/tools/doc-tools.js +0 -425
- package/build/tools/doc-tools.js.map +0 -1
- package/build/tools/override-tools.d.ts +0 -6
- package/build/tools/override-tools.js +0 -129
- package/build/tools/override-tools.js.map +0 -1
- package/build/tools/report-dispatch.js.map +0 -1
- package/build/tools/set-scale.d.ts +0 -6
- package/build/tools/set-scale.js +0 -95
- package/build/tools/set-scale.js.map +0 -1
- package/build/tools/setup-project.d.ts +0 -8
- package/build/tools/setup-project.js +0 -116
- package/build/tools/setup-project.js.map +0 -1
- package/build/tools/update-phase.d.ts +0 -12
- package/build/tools/update-phase.js +0 -148
- package/build/tools/update-phase.js.map +0 -1
- package/workflows/dev/roles/pm.md +0 -99
- package/workflows/dev/schemas/deploy.json +0 -20
- package/workflows/dev/schemas/fsd.json +0 -25
- package/workflows/dev/schemas/project-plan.json +0 -20
- package/workflows/dev/schemas/retrospective.json +0 -20
- package/workflows/dev/schemas/risk-assessment.json +0 -15
- package/workflows/dev/schemas/tech-design.api-contract.json +0 -20
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Tool: project_init
|
|
3
|
-
* Register a new Harmonia project in the global registry.
|
|
3
|
+
* Register a new Harmonia project in the global registry and install workflow hooks.
|
|
4
4
|
*
|
|
5
|
-
* This tool
|
|
6
|
-
*
|
|
7
|
-
* first iteration.
|
|
5
|
+
* This tool registers the project AND installs workflow-defined hooks for the
|
|
6
|
+
* detected agent. It does NOT create iteration directories or initialize state.
|
|
7
|
+
* After registration, call `iteration_start` to begin the first iteration.
|
|
8
8
|
*
|
|
9
9
|
* Supports an optional `workflow` parameter. When multiple workflows are
|
|
10
10
|
* available and none is specified, returns an error with the available list
|
|
11
11
|
* so the agent can re-call with a specific choice.
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
14
|
+
import { createKit, defineHooks } from '@s_s/agent-kit';
|
|
15
|
+
import { loadWorkflow, listWorkflows } from '../core/plugin.js';
|
|
16
|
+
import { registerProject, getProject, getGlobalDir } from '../core/registry.js';
|
|
17
|
+
import { detectHostAgent } from '../setup/inject.js';
|
|
18
|
+
/** Shared kit instance for hook installation */
|
|
19
|
+
const kit = createKit('harmonia');
|
|
20
|
+
export function registerProjectInit(server, workflowsDir) {
|
|
17
21
|
server.tool('project_init', 'Register a new Harmonia project. Creates the project entry in the global registry and the project source directory. Does NOT start an iteration — call iteration_start after registration to begin the first iteration.', {
|
|
18
22
|
project_name: z
|
|
19
23
|
.string()
|
|
@@ -59,7 +63,7 @@ export function registerProjectInit(server, builtinDir, customDir) {
|
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
// Resolve workflow name
|
|
62
|
-
const available = await listWorkflows(
|
|
66
|
+
const available = await listWorkflows(workflowsDir);
|
|
63
67
|
if (available.length === 0) {
|
|
64
68
|
return {
|
|
65
69
|
content: [
|
|
@@ -97,7 +101,7 @@ export function registerProjectInit(server, builtinDir, customDir) {
|
|
|
97
101
|
const descriptions = [];
|
|
98
102
|
for (const name of available) {
|
|
99
103
|
try {
|
|
100
|
-
const wf = await loadWorkflow(
|
|
104
|
+
const wf = await loadWorkflow(workflowsDir, name);
|
|
101
105
|
descriptions.push(`- ${name}: ${wf.definition.description}`);
|
|
102
106
|
}
|
|
103
107
|
catch {
|
|
@@ -121,9 +125,35 @@ export function registerProjectInit(server, builtinDir, customDir) {
|
|
|
121
125
|
};
|
|
122
126
|
}
|
|
123
127
|
// Load workflow definition (validate it loads correctly)
|
|
124
|
-
const wf = await loadWorkflow(
|
|
128
|
+
const wf = await loadWorkflow(workflowsDir, workflowName);
|
|
125
129
|
// Register project (creates global data dir + project source dir)
|
|
126
130
|
await registerProject(project_name, project_dir, workflowName);
|
|
131
|
+
// Install workflow hooks if the plugin provides a hook creator
|
|
132
|
+
let hookMessage = '';
|
|
133
|
+
if (wf.hooks) {
|
|
134
|
+
try {
|
|
135
|
+
const agentType = await detectHostAgent(project_dir);
|
|
136
|
+
const context = {
|
|
137
|
+
defineHooks,
|
|
138
|
+
dataDir: getGlobalDir(),
|
|
139
|
+
projectName: project_name,
|
|
140
|
+
};
|
|
141
|
+
const hookSet = wf.hooks(agentType, context);
|
|
142
|
+
const hookResult = await kit.installHooks(agentType, hookSet);
|
|
143
|
+
if (hookResult.success) {
|
|
144
|
+
hookMessage = `\nHooks 已安装 (${hookResult.filesWritten.length} 个文件)`;
|
|
145
|
+
if (hookResult.warnings.length > 0) {
|
|
146
|
+
hookMessage += `\n警告: ${hookResult.warnings.join('; ')}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
hookMessage = `\n[warn] Hook 安装失败: ${hookResult.error ?? '未知错误'}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
hookMessage = `\n[warn] Hook 安装出错: ${err instanceof Error ? err.message : String(err)}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
127
157
|
return {
|
|
128
158
|
content: [
|
|
129
159
|
{
|
|
@@ -134,6 +164,7 @@ export function registerProjectInit(server, builtinDir, customDir) {
|
|
|
134
164
|
`源代码目录: ${project_dir}`,
|
|
135
165
|
`工作流: ${wf.definition.name} (${wf.definition.description})`,
|
|
136
166
|
`可用角色: ${Object.keys(wf.roles).join(', ')}`,
|
|
167
|
+
hookMessage,
|
|
137
168
|
``,
|
|
138
169
|
`下一步: 调用 iteration_start(project_name="${project_name}") 开始第一轮迭代。`,
|
|
139
170
|
].join('\n'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-init.js","sourceRoot":"","sources":["../../src/tools/project-init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"project-init.js","sourceRoot":"","sources":["../../src/tools/project-init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool: role_dispatch
|
|
3
|
+
*
|
|
4
|
+
* Prepare all data needed to hand off a task to a team member role.
|
|
5
|
+
* Returns: role prompt (with overrides injected), session guidance,
|
|
6
|
+
* input artifacts, task brief, and dispatch tracking info.
|
|
7
|
+
*
|
|
8
|
+
* Node-based architecture: validates against workflow node states.
|
|
9
|
+
* Accepts an optional node_id parameter to target a specific task node.
|
|
10
|
+
*
|
|
11
|
+
* Session/parallel behavior is enforced by Core:
|
|
12
|
+
* - session: none → never searches for idle sessions
|
|
13
|
+
* - session: persistent → searches for idle sessions, directs reuse
|
|
14
|
+
* - session: optional → searches for idle sessions, suggests reuse
|
|
15
|
+
* - parallel: true + running dispatch → forces new session
|
|
16
|
+
*
|
|
17
|
+
* Automatically:
|
|
18
|
+
* - Creates a dispatch record for tracking
|
|
19
|
+
* - Enforces session/parallel strategy from role frontmatter
|
|
20
|
+
* - Triggers a dispatch_requested engine event
|
|
21
|
+
* - Returns nextAction from the workflow engine
|
|
22
|
+
*
|
|
23
|
+
* This tool does NOT launch agents — it only prepares the data.
|
|
24
|
+
* The coordinator decides how to pass this to the team member.
|
|
25
|
+
*/
|
|
26
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
27
|
+
import type { ArtifactIOContext } from '../core/artifacts.js';
|
|
28
|
+
import type { WorkflowPlugin } from '../core/types.js';
|
|
29
|
+
/**
|
|
30
|
+
* Get file extension from artifact definition format.
|
|
31
|
+
* - 'html' → '.html'
|
|
32
|
+
* - 'json' → '.json'
|
|
33
|
+
* - 'md' or undefined → '.md'
|
|
34
|
+
*/
|
|
35
|
+
export declare function getFormatExtension(format?: 'md' | 'html' | 'json'): string;
|
|
36
|
+
/** A resolved input artifact reference */
|
|
37
|
+
export interface InputReference {
|
|
38
|
+
/** Artifact ID */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Human-readable name from artifact definition */
|
|
41
|
+
name: string;
|
|
42
|
+
/** Resolved file path (managed) or directory path (unmanaged) */
|
|
43
|
+
path: string;
|
|
44
|
+
/** Whether this artifact was found on disk */
|
|
45
|
+
found: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve an artifact ID to a name + path reference.
|
|
49
|
+
*
|
|
50
|
+
* - Managed artifacts: resolves to full file path via format → extension mapping
|
|
51
|
+
* - Unmanaged artifacts: resolves to the output directory
|
|
52
|
+
* - Unknown artifacts: returns not-found
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveInputReference(artifactId: string, wf: WorkflowPlugin, ioCtx: ArtifactIOContext, existingArtifacts: Set<string>): InputReference;
|
|
55
|
+
export declare function registerDispatchRole(server: McpServer, workflowsDir: string): void;
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool: role_dispatch
|
|
3
|
+
*
|
|
4
|
+
* Prepare all data needed to hand off a task to a team member role.
|
|
5
|
+
* Returns: role prompt (with overrides injected), session guidance,
|
|
6
|
+
* input artifacts, task brief, and dispatch tracking info.
|
|
7
|
+
*
|
|
8
|
+
* Node-based architecture: validates against workflow node states.
|
|
9
|
+
* Accepts an optional node_id parameter to target a specific task node.
|
|
10
|
+
*
|
|
11
|
+
* Session/parallel behavior is enforced by Core:
|
|
12
|
+
* - session: none → never searches for idle sessions
|
|
13
|
+
* - session: persistent → searches for idle sessions, directs reuse
|
|
14
|
+
* - session: optional → searches for idle sessions, suggests reuse
|
|
15
|
+
* - parallel: true + running dispatch → forces new session
|
|
16
|
+
*
|
|
17
|
+
* Automatically:
|
|
18
|
+
* - Creates a dispatch record for tracking
|
|
19
|
+
* - Enforces session/parallel strategy from role frontmatter
|
|
20
|
+
* - Triggers a dispatch_requested engine event
|
|
21
|
+
* - Returns nextAction from the workflow engine
|
|
22
|
+
*
|
|
23
|
+
* This tool does NOT launch agents — it only prepares the data.
|
|
24
|
+
* The coordinator decides how to pass this to the team member.
|
|
25
|
+
*/
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { readArtifact, listArtifacts, resolveArtifactDir } from '../core/artifacts.js';
|
|
28
|
+
import { getMergedOverrides, resolveRoleConfig } from '../core/overrides.js';
|
|
29
|
+
import { createDispatch, findIdleSession, hasRunningDispatch } from '../core/dispatch.js';
|
|
30
|
+
import { loadArtifactSchema, formatSchemaGuidance } from '../core/schema.js';
|
|
31
|
+
import { resolveActive, isError, buildOverrideSection } from './utils.js';
|
|
32
|
+
import { loadWorkflowForContext, processWorkflowEvent, formatNextAction, findTaskNode } from './engine-helpers.js';
|
|
33
|
+
import { collectTaskNodes, findAncestorLoopId } from '../core/tree-utils.js';
|
|
34
|
+
/**
|
|
35
|
+
* Find task nodes for a given role that are active or pending.
|
|
36
|
+
*/
|
|
37
|
+
function findDispatchableNodes(wf, state, role) {
|
|
38
|
+
const allTasks = collectTaskNodes(wf.definition.root);
|
|
39
|
+
// Include floating nodes
|
|
40
|
+
if (wf.definition.floatingNodes) {
|
|
41
|
+
allTasks.push(...wf.definition.floatingNodes);
|
|
42
|
+
}
|
|
43
|
+
return allTasks.filter((t) => {
|
|
44
|
+
if (t.role !== role)
|
|
45
|
+
return false;
|
|
46
|
+
const nodeState = state.nodes[t.id];
|
|
47
|
+
return nodeState && (nodeState.status === 'active' || nodeState.status === 'pending');
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build session guidance text.
|
|
52
|
+
*
|
|
53
|
+
* Incorporates session type behavior, parallel status, model, and agent info.
|
|
54
|
+
* This replaces the former separate "## Configuration" section — all dispatch
|
|
55
|
+
* configuration info is now part of Session Guidance.
|
|
56
|
+
*/
|
|
57
|
+
function buildSessionGuidance(params) {
|
|
58
|
+
const { idleSession, sessionType, model, agent, parallelForced } = params;
|
|
59
|
+
const lines = [];
|
|
60
|
+
// Consistent agent descriptor used across all Action lines
|
|
61
|
+
const agentDesc = agent ? `拉起 ${agent} 作为子 agent 执行任务` : '拉起子 agent 执行任务';
|
|
62
|
+
// Model/agent header
|
|
63
|
+
if (model) {
|
|
64
|
+
const agentSuffix = agent ? `,使用 ${agent}` : '';
|
|
65
|
+
lines.push(`**Model**: \`${model}\`${agentSuffix}`);
|
|
66
|
+
}
|
|
67
|
+
else if (agent) {
|
|
68
|
+
lines.push(`**Agent**: ${agent}`);
|
|
69
|
+
}
|
|
70
|
+
if (lines.length > 0)
|
|
71
|
+
lines.push('');
|
|
72
|
+
// Session guidance based on type and findings
|
|
73
|
+
if (parallelForced) {
|
|
74
|
+
// parallel=true and same role already has a running dispatch → force new session
|
|
75
|
+
lines.push(`**Session**: 该角色已有运行中的 dispatch,强制启动新会话(parallel 模式)`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`**Action**: ${agentDesc}。`);
|
|
78
|
+
}
|
|
79
|
+
else if (idleSession) {
|
|
80
|
+
const agentId = idleSession.agentSessionId
|
|
81
|
+
? `Agent session ID: \`${idleSession.agentSessionId}\``
|
|
82
|
+
: 'Agent session ID: not recorded';
|
|
83
|
+
const label = idleSession.label ? ` (${idleSession.label})` : '';
|
|
84
|
+
lines.push(`**Reusable session found**: ${idleSession.id}${label}`);
|
|
85
|
+
lines.push(`- ${agentId}`);
|
|
86
|
+
lines.push(`- Agent type: ${idleSession.agentType ?? 'unknown'}`);
|
|
87
|
+
lines.push(`- Last active: ${idleSession.lastActiveAt}`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
if (sessionType === 'persistent') {
|
|
90
|
+
lines.push(`**Action**: 复用已有会话,而非启动新 agent。`);
|
|
91
|
+
lines.push(idleSession.agentSessionId
|
|
92
|
+
? `使用 \`--resume ${idleSession.agentSessionId}\` 或 \`--session ${idleSession.agentSessionId}\` 恢复会话。`
|
|
93
|
+
: `注意: 该会话未记录 agent session ID,可能需要${agentDesc}。`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// optional — suggestion, not directive
|
|
97
|
+
lines.push(`**Suggestion**: 存在空闲会话,可复用也可启动新会话,由你决定。`);
|
|
98
|
+
if (idleSession.agentSessionId) {
|
|
99
|
+
lines.push(`如需复用: \`--resume ${idleSession.agentSessionId}\` 或 \`--session ${idleSession.agentSessionId}\``);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// No idle session found (or session type is 'none')
|
|
105
|
+
if (sessionType === 'none') {
|
|
106
|
+
lines.push(`**Session**: 每次 dispatch 启动全新会话(session: none)`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push(`**未找到可复用会话**`);
|
|
110
|
+
lines.push(`Session type: ${sessionType}`);
|
|
111
|
+
}
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push(`**Action**: ${agentDesc}。`);
|
|
114
|
+
}
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get file extension from artifact definition format.
|
|
119
|
+
* - 'html' → '.html'
|
|
120
|
+
* - 'json' → '.json'
|
|
121
|
+
* - 'md' or undefined → '.md'
|
|
122
|
+
*/
|
|
123
|
+
export function getFormatExtension(format) {
|
|
124
|
+
switch (format) {
|
|
125
|
+
case 'html':
|
|
126
|
+
return '.html';
|
|
127
|
+
case 'json':
|
|
128
|
+
return '.json';
|
|
129
|
+
default:
|
|
130
|
+
return '.md';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve an artifact ID to a name + path reference.
|
|
135
|
+
*
|
|
136
|
+
* - Managed artifacts: resolves to full file path via format → extension mapping
|
|
137
|
+
* - Unmanaged artifacts: resolves to the output directory
|
|
138
|
+
* - Unknown artifacts: returns not-found
|
|
139
|
+
*/
|
|
140
|
+
export function resolveInputReference(artifactId, wf, ioCtx, existingArtifacts) {
|
|
141
|
+
const artifactDef = wf.artifactDefinitions[artifactId];
|
|
142
|
+
if (!artifactDef) {
|
|
143
|
+
return {
|
|
144
|
+
id: artifactId,
|
|
145
|
+
name: artifactId,
|
|
146
|
+
path: '',
|
|
147
|
+
found: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (artifactDef.unmanaged) {
|
|
151
|
+
// Unmanaged: resolve to directory
|
|
152
|
+
const dir = resolveArtifactDir(artifactDef.output, ioCtx);
|
|
153
|
+
return {
|
|
154
|
+
id: artifactId,
|
|
155
|
+
name: artifactDef.name,
|
|
156
|
+
path: dir,
|
|
157
|
+
found: true, // unmanaged dirs are always "available"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Managed: resolve to full file path via format → extension
|
|
161
|
+
const dir = resolveArtifactDir(artifactDef.output, ioCtx);
|
|
162
|
+
const ext = getFormatExtension(artifactDef.format);
|
|
163
|
+
const filePath = `${dir}/${artifactId}${ext}`;
|
|
164
|
+
return {
|
|
165
|
+
id: artifactId,
|
|
166
|
+
name: artifactDef.name,
|
|
167
|
+
path: filePath,
|
|
168
|
+
found: existingArtifacts.has(artifactId),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build Artifact Requirements section for the dispatch data package.
|
|
173
|
+
* Only includes schemas for artifacts associated with the dispatched role
|
|
174
|
+
* (via the role's capabilities).
|
|
175
|
+
*/
|
|
176
|
+
async function buildArtifactRequirements(wf, workflowsDir, workflowName, role) {
|
|
177
|
+
const artifactDefs = wf.artifactDefinitions;
|
|
178
|
+
// Extract artifact IDs from role capabilities
|
|
179
|
+
const roleDef = wf.roles[role];
|
|
180
|
+
const roleArtifactIds = new Set();
|
|
181
|
+
if (roleDef?.frontmatter.capabilities) {
|
|
182
|
+
for (const cap of roleDef.frontmatter.capabilities) {
|
|
183
|
+
if (cap.artifact) {
|
|
184
|
+
roleArtifactIds.add(cap.artifact);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// If role has no artifact capabilities, skip
|
|
189
|
+
if (roleArtifactIds.size === 0)
|
|
190
|
+
return '';
|
|
191
|
+
const sections = [];
|
|
192
|
+
for (const artifactId of roleArtifactIds) {
|
|
193
|
+
const artifactDef = artifactDefs[artifactId];
|
|
194
|
+
if (!artifactDef || artifactDef.unmanaged)
|
|
195
|
+
continue;
|
|
196
|
+
// Load main schema
|
|
197
|
+
const schema = await loadArtifactSchema(workflowsDir, workflowName, artifactId);
|
|
198
|
+
// Load step schemas if artifact has steps
|
|
199
|
+
let stepSchemas;
|
|
200
|
+
if (artifactDef.steps && artifactDef.steps.length > 0) {
|
|
201
|
+
stepSchemas = [];
|
|
202
|
+
for (const step of artifactDef.steps) {
|
|
203
|
+
const stepSchema = await loadArtifactSchema(workflowsDir, workflowName, `${artifactId}.${step.id}`);
|
|
204
|
+
stepSchemas.push({ step, schema: stepSchema });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Skip if no schema at all
|
|
208
|
+
if (!schema && (!stepSchemas || stepSchemas.every((s) => !s.schema)))
|
|
209
|
+
continue;
|
|
210
|
+
sections.push(formatSchemaGuidance(artifactId, artifactDef, schema, stepSchemas));
|
|
211
|
+
}
|
|
212
|
+
if (sections.length === 0)
|
|
213
|
+
return '';
|
|
214
|
+
return ['## Artifact Requirements', '', ...sections].join('\n');
|
|
215
|
+
}
|
|
216
|
+
export function registerDispatchRole(server, workflowsDir) {
|
|
217
|
+
server.tool('role_dispatch', "Prepare all data needed to dispatch a task to a team member. Returns the role's prompt (with capability overrides), configuration, input artifacts, task brief, and a dispatch tracking ID. Automatically searches for reusable sessions and provides guidance. Does NOT launch agents — you (coordinator) decide how to pass this to the team member. After launching, call dispatch_report to register the session.", {
|
|
218
|
+
project_name: z.string().describe('Project name'),
|
|
219
|
+
role: z.string().describe('Role ID to dispatch (e.g. architect, developer, tester)'),
|
|
220
|
+
task_brief: z
|
|
221
|
+
.string()
|
|
222
|
+
.describe('Task description for the team member — what they need to do, which tasks from the breakdown, specific instructions, etc.'),
|
|
223
|
+
node_id: z
|
|
224
|
+
.string()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe('Target task node ID from the workflow tree. If omitted, automatically finds an active/pending task node for the specified role.'),
|
|
227
|
+
input_artifact_ids: z
|
|
228
|
+
.array(z.string())
|
|
229
|
+
.optional()
|
|
230
|
+
.describe('Supplementary artifact IDs to include as input references (name + path) for the team member. These are merged with the node-level inputArtifacts declaration. Note: only path references are provided, not content.'),
|
|
231
|
+
}, async ({ project_name, role, task_brief, node_id, input_artifact_ids }) => {
|
|
232
|
+
try {
|
|
233
|
+
const ctx = await resolveActive(project_name);
|
|
234
|
+
if (isError(ctx))
|
|
235
|
+
return ctx;
|
|
236
|
+
// Load project state and workflow plugin
|
|
237
|
+
const { wf, state } = await loadWorkflowForContext(workflowsDir, project_name, ctx);
|
|
238
|
+
// Validate role exists
|
|
239
|
+
const roleDef = wf.roles[role];
|
|
240
|
+
if (!roleDef) {
|
|
241
|
+
const available = Object.keys(wf.roles).join(', ');
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: 'text',
|
|
246
|
+
text: `Role "${role}" not found. Available: ${available}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
isError: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Resolve target task node
|
|
253
|
+
let targetNode;
|
|
254
|
+
if (node_id) {
|
|
255
|
+
// Explicit node_id — validate it
|
|
256
|
+
targetNode = findTaskNode(wf, node_id);
|
|
257
|
+
if (!targetNode) {
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: 'text',
|
|
262
|
+
text: `Node "${node_id}" not found or is not a task node in the workflow.`,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
isError: true,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (targetNode.role !== role) {
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: `Node "${node_id}" is assigned to role "${targetNode.role}", not "${role}".`,
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
isError: true,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// Validate node state allows dispatch
|
|
280
|
+
const nodeState = state.nodes[node_id];
|
|
281
|
+
if (nodeState && nodeState.status !== 'active' && nodeState.status !== 'pending') {
|
|
282
|
+
return {
|
|
283
|
+
content: [
|
|
284
|
+
{
|
|
285
|
+
type: 'text',
|
|
286
|
+
text: `Node "${node_id}" is in status "${nodeState.status}" — cannot dispatch. Only active or pending nodes can be dispatched.`,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
isError: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Auto-find: look for active/pending task nodes for this role
|
|
295
|
+
const candidates = findDispatchableNodes(wf, state, role);
|
|
296
|
+
if (candidates.length === 0) {
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: `No active or pending task nodes found for role "${role}". Check project_status to see the current workflow state.`,
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Prefer active over pending
|
|
308
|
+
targetNode = candidates.find((t) => state.nodes[t.id]?.status === 'active') ?? candidates[0];
|
|
309
|
+
}
|
|
310
|
+
const targetNodeId = targetNode.id;
|
|
311
|
+
// Build I/O context for artifact path resolution
|
|
312
|
+
const ioCtx = {
|
|
313
|
+
contextDir: ctx.dir,
|
|
314
|
+
projectDir: ctx.entry.dir,
|
|
315
|
+
contextLabel: ctx.activeContext,
|
|
316
|
+
};
|
|
317
|
+
// Get merged overrides
|
|
318
|
+
const overrides = await getMergedOverrides(project_name);
|
|
319
|
+
// Build the full prompt with overrides injected
|
|
320
|
+
const overrideSection = buildOverrideSection(role, overrides);
|
|
321
|
+
let fullPrompt = overrideSection ? `${roleDef.prompt}\n${overrideSection}` : roleDef.prompt;
|
|
322
|
+
// Execute beforeDispatch hooks (if defined on the target node)
|
|
323
|
+
const hookInjections = [];
|
|
324
|
+
if (targetNode.beforeDispatch) {
|
|
325
|
+
// Collect static inject text
|
|
326
|
+
if (targetNode.beforeDispatch.inject) {
|
|
327
|
+
hookInjections.push(...targetNode.beforeDispatch.inject);
|
|
328
|
+
}
|
|
329
|
+
// Execute registered actions
|
|
330
|
+
if (targetNode.beforeDispatch.actions && wf.actions) {
|
|
331
|
+
const nodeState = state.nodes[targetNodeId];
|
|
332
|
+
// Resolve loopIteration: find ancestor loop node and read its current iteration
|
|
333
|
+
let loopIteration;
|
|
334
|
+
const ancestorLoopId = findAncestorLoopId(wf.definition.root, targetNodeId);
|
|
335
|
+
if (ancestorLoopId) {
|
|
336
|
+
const loopState = state.nodes[ancestorLoopId];
|
|
337
|
+
if (loopState) {
|
|
338
|
+
loopIteration = loopState.currentIteration;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const actionCtx = {
|
|
342
|
+
nodeId: targetNodeId,
|
|
343
|
+
role,
|
|
344
|
+
retryCount: nodeState?.retryCount ?? 0,
|
|
345
|
+
projectName: project_name,
|
|
346
|
+
pluginConfig: wf.config,
|
|
347
|
+
workflowState: state,
|
|
348
|
+
artifacts: {
|
|
349
|
+
read: (artifactId) => readArtifact(artifactId, ioCtx, wf.artifactDefinitions[artifactId]),
|
|
350
|
+
list: () => listArtifacts(ioCtx, wf.artifactDefinitions),
|
|
351
|
+
},
|
|
352
|
+
loopIteration,
|
|
353
|
+
};
|
|
354
|
+
for (const actionName of targetNode.beforeDispatch.actions) {
|
|
355
|
+
const handler = wf.actions[actionName];
|
|
356
|
+
if (handler) {
|
|
357
|
+
try {
|
|
358
|
+
const result = await handler(actionCtx);
|
|
359
|
+
if (result.inject) {
|
|
360
|
+
hookInjections.push(...result.inject);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
console.warn(`[harmonia] beforeDispatch action "${actionName}" failed:`, err);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
console.warn(`[harmonia] beforeDispatch action "${actionName}" not registered`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Append hook injections to the role prompt
|
|
374
|
+
if (hookInjections.length > 0) {
|
|
375
|
+
fullPrompt += '\n\n' + hookInjections.join('\n\n');
|
|
376
|
+
}
|
|
377
|
+
// Resolve input artifact references (merge node declaration + coordinator parameter)
|
|
378
|
+
const nodeInputIds = targetNode.inputArtifacts ?? [];
|
|
379
|
+
const paramInputIds = input_artifact_ids ?? [];
|
|
380
|
+
const mergedInputIds = [...new Set([...nodeInputIds, ...paramInputIds])];
|
|
381
|
+
// List existing artifacts for reference resolution
|
|
382
|
+
const existingArtifacts = new Set(await listArtifacts(ioCtx, wf.artifactDefinitions));
|
|
383
|
+
// Resolve all input references
|
|
384
|
+
const inputRefs = mergedInputIds.map((id) => resolveInputReference(id, wf, ioCtx, existingArtifacts));
|
|
385
|
+
const foundRefs = inputRefs.filter((r) => r.found);
|
|
386
|
+
const missingRefs = inputRefs.filter((r) => !r.found);
|
|
387
|
+
// Resolve agent/model: override takes precedence over frontmatter
|
|
388
|
+
const roleConfig = resolveRoleConfig(role, overrides);
|
|
389
|
+
const modelDisplay = roleConfig.model ?? roleDef.frontmatter.model;
|
|
390
|
+
const agentDisplay = roleConfig.agent ?? roleDef.frontmatter.agent;
|
|
391
|
+
const sessionType = roleDef.frontmatter.session;
|
|
392
|
+
// Determine session strategy based on session type + parallel field
|
|
393
|
+
let idleSession = null;
|
|
394
|
+
let parallelForced = false;
|
|
395
|
+
if (sessionType === 'none') {
|
|
396
|
+
// session: none → never look for idle sessions
|
|
397
|
+
idleSession = null;
|
|
398
|
+
}
|
|
399
|
+
else if (roleDef.frontmatter.parallel) {
|
|
400
|
+
// parallel=true → check if same role has running dispatches
|
|
401
|
+
const hasRunning = await hasRunningDispatch(project_name, ctx.number, role, ctx.dir);
|
|
402
|
+
if (hasRunning) {
|
|
403
|
+
// Force new session — don't look for idle ones
|
|
404
|
+
parallelForced = true;
|
|
405
|
+
idleSession = null;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// No running dispatch → follow normal session behavior
|
|
409
|
+
idleSession = await findIdleSession(project_name, ctx.number, role, ctx.dir);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// session: persistent or optional → look for idle sessions
|
|
414
|
+
idleSession = await findIdleSession(project_name, ctx.number, role, ctx.dir);
|
|
415
|
+
}
|
|
416
|
+
// Create dispatch record
|
|
417
|
+
const dispatch = await createDispatch(project_name, ctx.number, role, task_brief, [], // expectedOutputs — determined dynamically by workflow engine
|
|
418
|
+
idleSession?.id, ctx.dir, targetNodeId);
|
|
419
|
+
// Trigger engine event: dispatch_requested
|
|
420
|
+
const engineResult = await processWorkflowEvent(workflowsDir, project_name, ctx, {
|
|
421
|
+
type: 'dispatch_requested',
|
|
422
|
+
nodeId: targetNodeId,
|
|
423
|
+
});
|
|
424
|
+
const nextActionText = formatNextAction(engineResult.nextAction);
|
|
425
|
+
// Build session guidance (now includes model/agent info — replaces ## Configuration)
|
|
426
|
+
const sessionGuidance = buildSessionGuidance({
|
|
427
|
+
idleSession,
|
|
428
|
+
sessionType,
|
|
429
|
+
model: modelDisplay,
|
|
430
|
+
agent: agentDisplay,
|
|
431
|
+
parallelForced,
|
|
432
|
+
});
|
|
433
|
+
// Build artifact requirements for expected outputs
|
|
434
|
+
const artifactRequirements = await buildArtifactRequirements(wf, workflowsDir, state.workflow, role);
|
|
435
|
+
const summary = [
|
|
436
|
+
`# Dispatch: ${role}`,
|
|
437
|
+
``,
|
|
438
|
+
`## Dispatch Tracking`,
|
|
439
|
+
`- Dispatch ID: \`${dispatch.id}\``,
|
|
440
|
+
`- Status: ${dispatch.status}`,
|
|
441
|
+
`- Target Node: ${targetNodeId}`,
|
|
442
|
+
``,
|
|
443
|
+
`## Session Guidance`,
|
|
444
|
+
sessionGuidance,
|
|
445
|
+
``,
|
|
446
|
+
`## Task Brief`,
|
|
447
|
+
task_brief,
|
|
448
|
+
``,
|
|
449
|
+
`## Project Context`,
|
|
450
|
+
`- Project: ${project_name}`,
|
|
451
|
+
`- Directory: ${state.projectDir}`,
|
|
452
|
+
`- Workflow: ${state.workflow}`,
|
|
453
|
+
];
|
|
454
|
+
// Inject unmanaged artifact output path hints
|
|
455
|
+
const unmanagedHints = [];
|
|
456
|
+
for (const cap of roleDef.frontmatter.capabilities ?? []) {
|
|
457
|
+
if (!cap.artifact)
|
|
458
|
+
continue;
|
|
459
|
+
const def = wf.artifactDefinitions[cap.artifact];
|
|
460
|
+
if (!def?.unmanaged)
|
|
461
|
+
continue;
|
|
462
|
+
const outputDir = resolveArtifactDir(def.output, ioCtx);
|
|
463
|
+
unmanagedHints.push(`- **${cap.artifact}** (${def.name}): \`${outputDir}\``);
|
|
464
|
+
}
|
|
465
|
+
if (unmanagedHints.length > 0) {
|
|
466
|
+
summary.push(``, `## Unmanaged Artifact Output Paths`, `以下 artifact 由 agent 直接输出(非 artifact_write),请将产出写入对应路径:`, ...unmanagedHints);
|
|
467
|
+
}
|
|
468
|
+
summary.push(``, `## Input References (${foundRefs.length}${missingRefs.length > 0 ? `, ${missingRefs.length} missing` : ''})`);
|
|
469
|
+
if (foundRefs.length > 0) {
|
|
470
|
+
for (const ref of foundRefs) {
|
|
471
|
+
summary.push(`- **${ref.id}** (${ref.name}): \`${ref.path}\``);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (missingRefs.length > 0) {
|
|
475
|
+
summary.push(``, `### Missing`);
|
|
476
|
+
for (const ref of missingRefs) {
|
|
477
|
+
summary.push(`- **${ref.id}** (${ref.name}): 未找到`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
summary.push(``, `## Next Step`, `After launching the agent, call \`dispatch_report\` with dispatch_id="${dispatch.id}" and the agent's session ID.`, `When the agent finishes, call \`dispatch_report\` again with status="completed" (or "failed").`);
|
|
481
|
+
if (artifactRequirements) {
|
|
482
|
+
summary.push(``, artifactRequirements);
|
|
483
|
+
}
|
|
484
|
+
summary.push(``, `## Role Prompt`, ``, fullPrompt);
|
|
485
|
+
summary.push(nextActionText);
|
|
486
|
+
return {
|
|
487
|
+
content: [
|
|
488
|
+
{
|
|
489
|
+
type: 'text',
|
|
490
|
+
text: summary.join('\n'),
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: 'text',
|
|
500
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
isError: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
//# sourceMappingURL=role-dispatch.js.map
|