@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.
Files changed (172) hide show
  1. package/README.md +140 -392
  2. package/build/cli/setup.d.ts +4 -2
  3. package/build/cli/setup.js +44 -18
  4. package/build/cli/setup.js.map +1 -1
  5. package/build/core/action-registry.d.ts +36 -0
  6. package/build/core/action-registry.js +53 -0
  7. package/build/core/action-registry.js.map +1 -0
  8. package/build/core/artifacts.d.ts +66 -0
  9. package/build/core/artifacts.js +178 -0
  10. package/build/core/artifacts.js.map +1 -0
  11. package/build/core/dispatch.d.ts +6 -2
  12. package/build/core/dispatch.js +12 -7
  13. package/build/core/dispatch.js.map +1 -1
  14. package/build/core/overrides.d.ts +19 -26
  15. package/build/core/overrides.js +32 -98
  16. package/build/core/overrides.js.map +1 -1
  17. package/build/core/plugin.d.ts +86 -0
  18. package/build/core/plugin.js +332 -0
  19. package/build/core/plugin.js.map +1 -0
  20. package/build/core/registry.d.ts +4 -5
  21. package/build/core/registry.js +8 -9
  22. package/build/core/registry.js.map +1 -1
  23. package/build/core/reviews.d.ts +11 -12
  24. package/build/core/reviews.js +18 -21
  25. package/build/core/reviews.js.map +1 -1
  26. package/build/core/schema.d.ts +43 -15
  27. package/build/core/schema.js +124 -20
  28. package/build/core/schema.js.map +1 -1
  29. package/build/core/state.d.ts +26 -27
  30. package/build/core/state.js +36 -90
  31. package/build/core/state.js.map +1 -1
  32. package/build/core/steps.d.ts +13 -14
  33. package/build/core/steps.js +26 -29
  34. package/build/core/steps.js.map +1 -1
  35. package/build/core/tree-utils.d.ts +52 -0
  36. package/build/core/tree-utils.js +226 -0
  37. package/build/core/tree-utils.js.map +1 -0
  38. package/build/core/types.d.ts +389 -118
  39. package/build/core/types.js +15 -1
  40. package/build/core/types.js.map +1 -1
  41. package/build/core/workflow-engine.d.ts +68 -0
  42. package/build/core/workflow-engine.js +821 -0
  43. package/build/core/workflow-engine.js.map +1 -0
  44. package/build/core/workflow-validator.d.ts +22 -0
  45. package/build/core/workflow-validator.js +489 -0
  46. package/build/core/workflow-validator.js.map +1 -0
  47. package/build/index.js +25 -26
  48. package/build/index.js.map +1 -1
  49. package/build/setup/inject.d.ts +4 -4
  50. package/build/setup/inject.js +6 -6
  51. package/build/setup/inject.js.map +1 -1
  52. package/build/setup/templates.d.ts +9 -7
  53. package/build/setup/templates.js +68 -172
  54. package/build/setup/templates.js.map +1 -1
  55. package/build/tools/artifact-approve.d.ts +8 -0
  56. package/build/tools/{approve-doc.js → artifact-approve.js} +24 -16
  57. package/build/tools/artifact-approve.js.map +1 -0
  58. package/build/tools/artifact-schema.d.ts +12 -0
  59. package/build/tools/artifact-schema.js +148 -0
  60. package/build/tools/artifact-schema.js.map +1 -0
  61. package/build/tools/artifact-tools.d.ts +18 -0
  62. package/build/tools/artifact-tools.js +465 -0
  63. package/build/tools/artifact-tools.js.map +1 -0
  64. package/build/tools/{report-dispatch.d.ts → dispatch-report.d.ts} +7 -3
  65. package/build/tools/{report-dispatch.js → dispatch-report.js} +106 -28
  66. package/build/tools/dispatch-report.js.map +1 -0
  67. package/build/tools/engine-helpers.d.ts +41 -0
  68. package/build/tools/engine-helpers.js +182 -0
  69. package/build/tools/engine-helpers.js.map +1 -0
  70. package/build/tools/get-project-status.d.ts +6 -4
  71. package/build/tools/get-project-status.js +265 -248
  72. package/build/tools/get-project-status.js.map +1 -1
  73. package/build/tools/get-role-prompt.d.ts +1 -1
  74. package/build/tools/get-role-prompt.js +7 -41
  75. package/build/tools/get-role-prompt.js.map +1 -1
  76. package/build/tools/iteration-start.d.ts +7 -4
  77. package/build/tools/iteration-start.js +45 -19
  78. package/build/tools/iteration-start.js.map +1 -1
  79. package/build/tools/loop-done.d.ts +11 -0
  80. package/build/tools/loop-done.js +109 -0
  81. package/build/tools/loop-done.js.map +1 -0
  82. package/build/tools/patch-start.d.ts +4 -2
  83. package/build/tools/patch-start.js +36 -11
  84. package/build/tools/patch-start.js.map +1 -1
  85. package/build/tools/project-init.d.ts +5 -5
  86. package/build/tools/project-init.js +41 -10
  87. package/build/tools/project-init.js.map +1 -1
  88. package/build/tools/role-dispatch.d.ts +55 -0
  89. package/build/tools/role-dispatch.js +508 -0
  90. package/build/tools/role-dispatch.js.map +1 -0
  91. package/build/tools/utils.d.ts +6 -0
  92. package/build/tools/utils.js +36 -0
  93. package/build/tools/utils.js.map +1 -1
  94. package/package.json +1 -1
  95. package/{build/hooks/claude-code.js → workflows/dev/hooks/claude.js} +34 -23
  96. package/{build → workflows/dev}/hooks/content.js +27 -18
  97. package/workflows/dev/hooks/index.js +52 -0
  98. package/{build → workflows/dev}/hooks/openclaw.js +31 -20
  99. package/{build → workflows/dev}/hooks/opencode.js +31 -20
  100. package/workflows/dev/roles/architect.md +68 -28
  101. package/workflows/dev/roles/coordinator.md +103 -0
  102. package/workflows/dev/roles/developer.md +5 -5
  103. package/workflows/dev/roles/tester.md +19 -19
  104. package/workflows/dev/schemas/api-contract.json +42 -0
  105. package/workflows/dev/schemas/api-design.json +30 -13
  106. package/workflows/dev/schemas/data-model.json +20 -7
  107. package/workflows/dev/schemas/prd.completeness-check.json +6 -5
  108. package/workflows/dev/schemas/prd.draft.json +13 -5
  109. package/workflows/dev/schemas/prd.final.json +34 -11
  110. package/workflows/dev/schemas/prd.json +29 -11
  111. package/workflows/dev/schemas/prd.requirements.json +6 -5
  112. package/workflows/dev/schemas/prototype.json +6 -2
  113. package/workflows/dev/schemas/task-breakdown.coarse.json +4 -3
  114. package/workflows/dev/schemas/task-breakdown.dependencies.json +5 -4
  115. package/workflows/dev/schemas/task-breakdown.detailed.json +8 -3
  116. package/workflows/dev/schemas/task-breakdown.final.json +8 -3
  117. package/workflows/dev/schemas/task-breakdown.json +8 -3
  118. package/workflows/dev/schemas/tech-design.analysis.json +6 -5
  119. package/workflows/dev/schemas/tech-design.draft.json +14 -5
  120. package/workflows/dev/schemas/tech-design.final.json +39 -13
  121. package/workflows/dev/schemas/tech-design.json +34 -13
  122. package/workflows/dev/schemas/tech-design.research.json +21 -0
  123. package/workflows/dev/schemas/test-plan.json +17 -7
  124. package/workflows/dev/schemas/test-report.json +26 -9
  125. package/workflows/dev/schemas/user-stories.json +7 -3
  126. package/workflows/dev/tools/index.js +23 -0
  127. package/workflows/dev/workflow.json +234 -101
  128. package/build/core/docs.d.ts +0 -36
  129. package/build/core/docs.js +0 -96
  130. package/build/core/docs.js.map +0 -1
  131. package/build/core/workflow.d.ts +0 -33
  132. package/build/core/workflow.js +0 -140
  133. package/build/core/workflow.js.map +0 -1
  134. package/build/hooks/claude-code.d.ts +0 -20
  135. package/build/hooks/claude-code.js.map +0 -1
  136. package/build/hooks/content.d.ts +0 -43
  137. package/build/hooks/content.js.map +0 -1
  138. package/build/hooks/install.d.ts +0 -40
  139. package/build/hooks/install.js +0 -63
  140. package/build/hooks/install.js.map +0 -1
  141. package/build/hooks/openclaw.d.ts +0 -24
  142. package/build/hooks/openclaw.js.map +0 -1
  143. package/build/hooks/opencode.d.ts +0 -29
  144. package/build/hooks/opencode.js.map +0 -1
  145. package/build/tools/approve-doc.d.ts +0 -6
  146. package/build/tools/approve-doc.js.map +0 -1
  147. package/build/tools/dispatch-role.d.ts +0 -16
  148. package/build/tools/dispatch-role.js +0 -266
  149. package/build/tools/dispatch-role.js.map +0 -1
  150. package/build/tools/doc-tools.d.ts +0 -16
  151. package/build/tools/doc-tools.js +0 -425
  152. package/build/tools/doc-tools.js.map +0 -1
  153. package/build/tools/override-tools.d.ts +0 -6
  154. package/build/tools/override-tools.js +0 -129
  155. package/build/tools/override-tools.js.map +0 -1
  156. package/build/tools/report-dispatch.js.map +0 -1
  157. package/build/tools/set-scale.d.ts +0 -6
  158. package/build/tools/set-scale.js +0 -95
  159. package/build/tools/set-scale.js.map +0 -1
  160. package/build/tools/setup-project.d.ts +0 -8
  161. package/build/tools/setup-project.js +0 -116
  162. package/build/tools/setup-project.js.map +0 -1
  163. package/build/tools/update-phase.d.ts +0 -12
  164. package/build/tools/update-phase.js +0 -148
  165. package/build/tools/update-phase.js.map +0 -1
  166. package/workflows/dev/roles/pm.md +0 -99
  167. package/workflows/dev/schemas/deploy.json +0 -20
  168. package/workflows/dev/schemas/fsd.json +0 -25
  169. package/workflows/dev/schemas/project-plan.json +0 -20
  170. package/workflows/dev/schemas/retrospective.json +0 -20
  171. package/workflows/dev/schemas/risk-assessment.json +0 -15
  172. package/workflows/dev/schemas/tech-design.api-contract.json +0 -20
@@ -1,153 +1,156 @@
1
1
  /**
2
2
  * MCP Tool: project_status
3
- * Read the current project status with rich context for PM decision-making.
4
- * Includes phase progress, documents, pending reviews, dispatch records,
5
- * active sessions, and intelligent next-step suggestions.
3
+ * Read the current project status with rich context for coordinator decision-making.
4
+ *
5
+ * Node-based architecture: displays workflow tree with node states instead of phases.
6
+ * Includes artifacts, pending reviews, dispatch records, active sessions,
7
+ * workflow engine nextAction, and intelligent next-step suggestions.
6
8
  *
7
9
  * When called without project_name, returns a summary list of all projects.
8
10
  */
9
11
  import { z } from 'zod';
10
12
  import { readState } from '../core/state.js';
11
- import { loadWorkflow } from '../core/workflow.js';
12
- import { listDocs } from '../core/docs.js';
13
+ import { loadWorkflow } from '../core/plugin.js';
14
+ import { listArtifacts } from '../core/artifacts.js';
13
15
  import { readReviews } from '../core/reviews.js';
14
- import { getMergedOverrides } from '../core/overrides.js';
15
16
  import { readDispatches, readSessions } from '../core/dispatch.js';
16
17
  import { readSteps, getCompletedStepIds } from '../core/steps.js';
17
18
  import { listProjects, getProject, resolveContextDir } from '../core/registry.js';
18
19
  import { readIssues } from '../core/issues.js';
20
+ import { processWorkflowEvent, formatNextAction } from './engine-helpers.js';
21
+ // --- Formatting Helpers ---
19
22
  /**
20
- * Derive next-step suggestions based on current project state, including dispatch awareness.
23
+ * Get status icon for a node.
21
24
  */
22
- function deriveNextSteps(state, wf, existingDocs, pendingReviews, dispatches, sessions, openIssueCount) {
23
- const suggestions = [];
24
- const currentPhaseDef = wf.definition.phases.find((p) => p.id === state.currentPhase);
25
- if (!currentPhaseDef)
26
- return ['Unknown phase — check project state.'];
27
- // If scale is not set, that's the most important next step
28
- if (state.scale === null) {
29
- // Check if PRD exists and is approved
30
- const hasPrdApproved = pendingReviews.length === 0; // simplified check
31
- suggestions.push(`项目规模 (scale) 尚未设定。请先完成 PRD 编写和审批,然后调用 project_set_scale 设定规模。`);
32
- }
33
- // If there are pending reviews, those block progress
34
- if (pendingReviews.length > 0) {
35
- const docNames = pendingReviews.map((r) => r.docId).join(', ');
36
- suggestions.push(`Review pending documents: ${docNames}. Present them to the user and call doc_approve after user feedback.`);
37
- }
38
- // Check dispatch states — find dispatches that need attention
39
- const activeDispatches = dispatches.filter((d) => d.status === 'dispatched' || d.status === 'running');
40
- const dispatchedNotRunning = dispatches.filter((d) => d.status === 'dispatched');
41
- const runningDispatches = dispatches.filter((d) => d.status === 'running');
42
- const failedDispatches = dispatches.filter((d) => d.status === 'failed');
43
- if (dispatchedNotRunning.length > 0) {
44
- for (const d of dispatchedNotRunning) {
45
- suggestions.push(`Dispatch ${d.id} (${d.role}) is created but not yet launched. Launch the agent and call dispatch_report to register it.`);
46
- }
47
- }
48
- if (runningDispatches.length > 0) {
49
- for (const d of runningDispatches) {
50
- const session = sessions.find((s) => s.id === d.sessionId);
51
- const agentInfo = session?.agentSessionId ? ` (agent session: ${session.agentSessionId})` : '';
52
- suggestions.push(`Dispatch ${d.id} (${d.role}) is running${agentInfo}. Check if the agent has finished, then call dispatch_report with status="completed" or "failed".`);
53
- }
25
+ function statusIcon(status) {
26
+ switch (status) {
27
+ case 'completed':
28
+ return '✓';
29
+ case 'active':
30
+ return '●';
31
+ case 'failed':
32
+ return '✗';
33
+ case 'cancelled':
34
+ return '—';
35
+ case 'skipped':
36
+ return '⊘';
37
+ default:
38
+ return '';
54
39
  }
55
- if (failedDispatches.length > 0) {
56
- for (const d of failedDispatches) {
57
- const reason = d.note ? ` Reason: ${d.note}` : '';
58
- suggestions.push(`Dispatch ${d.id} (${d.role}) failed.${reason} Consider re-dispatching with role_dispatch.`);
40
+ }
41
+ /**
42
+ * Get dispatch info string for a node (if any active dispatches target it).
43
+ */
44
+ function getNodeDispatchInfo(nodeId, dispatches) {
45
+ const nodeDispatches = dispatches.filter((d) => d.nodeId === nodeId && (d.status === 'dispatched' || d.status === 'running'));
46
+ if (nodeDispatches.length === 0)
47
+ return '';
48
+ const info = nodeDispatches.map((d) => d.id + ':' + d.status).join(', ');
49
+ return ' [' + info + ']';
50
+ }
51
+ /**
52
+ * Format the workflow tree as an indented status view.
53
+ */
54
+ function formatNodeTree(node, nodes, dispatches, depth = 0) {
55
+ const indent = ' '.repeat(depth);
56
+ const lines = [];
57
+ const state = nodes[node.id];
58
+ const status = state?.status ?? 'pending';
59
+ const icon = statusIcon(status);
60
+ switch (node.type) {
61
+ case 'task': {
62
+ const dispatchInfo = getNodeDispatchInfo(node.id, dispatches);
63
+ lines.push(indent + icon + ' ' + node.id + ' (task, ' + node.role + ') — ' + status + dispatchInfo);
64
+ break;
59
65
  }
60
- }
61
- // Check which phase outputs are still missing (only when scale is set)
62
- if (state.scale !== null) {
63
- const scale = state.scale;
64
- const missingOutputs = currentPhaseDef.outputs.filter((o) => {
65
- const docDef = wf.definition.docs[o];
66
- if (docDef?.external)
67
- return false;
68
- const scaleVal = docDef?.scale[scale];
69
- if (scaleVal === 'skip' || scaleVal === 'optional')
70
- return false;
71
- return !existingDocs.includes(o);
72
- });
73
- if (missingOutputs.length > 0) {
74
- const alreadyDispatched = activeDispatches.length > 0;
75
- const nonPmRoles = currentPhaseDef.roles.filter((r) => r !== 'pm');
76
- if (nonPmRoles.length > 0 && !alreadyDispatched) {
77
- suggestions.push(`Dispatch ${nonPmRoles.join(', ')} to produce: ${missingOutputs.join(', ')}. Use role_dispatch to prepare task data.`);
66
+ case 'sequence':
67
+ lines.push(indent + icon + ' ' + node.id + ' (sequence) ' + status);
68
+ for (const child of node.children) {
69
+ lines.push(...formatNodeTree(child, nodes, dispatches, depth + 1));
78
70
  }
79
- else if (nonPmRoles.length === 0) {
80
- suggestions.push(`Produce remaining documents: ${missingOutputs.join(', ')}. Use doc_write for each.`);
71
+ break;
72
+ case 'parallel':
73
+ lines.push(indent + icon + ' ' + node.id + ' (parallel, ' + node.failStrategy + ') — ' + status);
74
+ for (const child of node.children) {
75
+ lines.push(...formatNodeTree(child, nodes, dispatches, depth + 1));
81
76
  }
77
+ break;
78
+ case 'gate': {
79
+ const gateStatus = status === 'completed' ? 'passed' : status === 'failed' ? 'failed' : status;
80
+ lines.push(indent + icon + ' ' + node.id + ' (gate) — ' + gateStatus);
81
+ lines.push(...formatNodeTree(node.pass, nodes, dispatches, depth + 1));
82
+ if ('type' in node.fail) {
83
+ lines.push(...formatNodeTree(node.fail, nodes, dispatches, depth + 1));
84
+ }
85
+ else {
86
+ const failTarget = node.fail;
87
+ lines.push(indent + ' ↩ fail → goto ' + failTarget.goto);
88
+ }
89
+ break;
82
90
  }
83
- else if (pendingReviews.length === 0 && activeDispatches.length === 0) {
84
- suggestions.push(`All outputs for "${currentPhaseDef.name}" are complete. Advance with: phase_update(project_name, "${state.currentPhase}", "completed")`);
85
- }
86
- }
87
- // Check for lost sessions
88
- const lostSessions = sessions.filter((s) => s.status === 'lost');
89
- if (lostSessions.length > 0) {
90
- for (const s of lostSessions) {
91
- suggestions.push(`Session ${s.id} (${s.role}) is marked as lost. The agent may have crashed. Consider re-dispatching this role.`);
92
- }
93
- }
94
- if (suggestions.length === 0) {
95
- // Check if ALL phases are completed → recommend iteration_start for next iteration
96
- const allPhasesCompleted = state.phases.every((p) => p.status === 'completed' || p.status === 'skipped');
97
- if (allPhasesCompleted) {
98
- suggestions.push(`本次${state.type === 'patch' ? '补丁' : '迭代'}所有阶段已完成。如需开始新一轮迭代,请调用 iteration_start(project_name="${state.projectName}")。`);
99
- }
100
- else {
101
- suggestions.push(`Continue working on the "${currentPhaseDef.name}" phase.`);
91
+ case 'loop': {
92
+ const loopState = state;
93
+ const iteration = loopState?.currentIteration ?? 0;
94
+ const done = loopState?.done ? ', done marked' : '';
95
+ lines.push(indent +
96
+ icon +
97
+ ' ' +
98
+ node.id +
99
+ ' (loop, ' +
100
+ iteration +
101
+ '/' +
102
+ node.maxIterations +
103
+ done +
104
+ ') ' +
105
+ status);
106
+ lines.push(...formatNodeTree(node.body, nodes, dispatches, depth + 1));
107
+ break;
102
108
  }
103
109
  }
104
- // Always mention open issues if there are any
105
- if (openIssueCount > 0) {
106
- suggestions.push(`有 ${openIssueCount} 个未关闭的 issue。可调用 patch_start 开始补丁修复,或 issue_list 查看详情。`);
107
- }
108
- return suggestions;
110
+ return lines;
109
111
  }
110
112
  /**
111
113
  * Format a dispatch record for display.
112
114
  */
113
115
  function formatDispatch(d, sessions) {
114
- const statusIcon = d.status === 'completed'
115
- ? '✓'
116
- : d.status === 'running'
117
- ? '→'
118
- : d.status === 'failed'
119
- ? '✗'
120
- : d.status === 'cancelled'
121
- ? '—'
122
- : '○';
116
+ const icon = statusIcon(d.status === 'dispatched' ? 'pending' : d.status === 'running' ? 'active' : d.status);
123
117
  const session = sessions.find((s) => s.id === d.sessionId);
124
- const sessionInfo = session?.agentSessionId ? ` session:${session.agentSessionId}` : '';
125
- const note = d.note ? ` (${d.note})` : '';
118
+ const sessionInfo = session?.agentSessionId ? ' session:' + session.agentSessionId : '';
119
+ const note = d.note ? ' (' + d.note + ')' : '';
120
+ const nodeInfo = d.nodeId ? ' node:' + d.nodeId : '';
126
121
  const brief = d.taskBrief.length > 60 ? d.taskBrief.slice(0, 57) + '...' : d.taskBrief;
127
- return ` ${statusIcon} ${d.id} ${d.role.padEnd(12)} [${d.status}] ${brief}${sessionInfo}${note}`;
122
+ return (' ' +
123
+ icon +
124
+ ' ' +
125
+ d.id +
126
+ ' ' +
127
+ d.role.padEnd(12) +
128
+ ' [' +
129
+ d.status +
130
+ '] ' +
131
+ brief +
132
+ nodeInfo +
133
+ sessionInfo +
134
+ note);
128
135
  }
129
136
  /**
130
137
  * Format a session record for display.
131
138
  */
132
139
  function formatSession(s) {
133
- const agentInfo = s.agentSessionId ? `agent:${s.agentSessionId}` : 'no agent ID';
134
- const label = s.label ? ` (${s.label})` : '';
135
- const agentType = s.agentType ? ` via ${s.agentType}` : '';
136
- return ` ${s.id} ${s.role.padEnd(12)} [${s.status}] ${agentInfo}${agentType}${label}`;
140
+ const agentInfo = s.agentSessionId ? 'agent:' + s.agentSessionId : 'no agent ID';
141
+ const label = s.label ? ' (' + s.label + ')' : '';
142
+ const agentType = s.agentType ? ' via ' + s.agentType : '';
143
+ return ' ' + s.id + ' ' + s.role.padEnd(12) + ' [' + s.status + '] ' + agentInfo + agentType + label;
137
144
  }
138
- /** Scales that activate sequential mode */
139
- const SEQUENTIAL_SCALES = new Set(['medium', 'large']);
140
145
  /**
141
- * Format step progress for a sequential document.
142
- * Returns lines like:
143
- * Steps: [✓] 需求结构化 → [✓] 完整性校验 → [→] PRD 文档草稿 → [ ] PRD 最终版
146
+ * Format step progress for a sequential artifact.
144
147
  */
145
- function formatStepProgress(docDef, stepState) {
146
- const steps = docDef.steps;
148
+ function formatStepProgress(artifactDef, stepState) {
149
+ const steps = artifactDef.steps;
147
150
  const completedIds = stepState ? getCompletedStepIds(stepState) : new Set();
148
151
  const finalized = stepState?.finalized ?? false;
149
152
  if (finalized) {
150
- return ` Steps: all completed ✓ (finalized)`;
153
+ return ' Steps: all completed ✓ (finalized)';
151
154
  }
152
155
  let firstIncomplete = steps.length;
153
156
  for (let i = 0; i < steps.length; i++) {
@@ -158,12 +161,58 @@ function formatStepProgress(docDef, stepState) {
158
161
  }
159
162
  const parts = steps.map((s, i) => {
160
163
  if (completedIds.has(s.id))
161
- return `[✓] ${s.name}`;
164
+ return '[✓] ' + s.name;
162
165
  if (i === firstIncomplete)
163
- return `[→] ${s.name}`;
164
- return `[ ] ${s.name}`;
166
+ return '[→] ' + s.name;
167
+ return '[ ] ' + s.name;
165
168
  });
166
- return ` Steps: ${parts.join(' → ')}`;
169
+ return ' Steps: ' + parts.join(' → ');
170
+ }
171
+ /**
172
+ * Format artifacts summary.
173
+ */
174
+ function formatArtifacts(existingArtifacts, artifactDefs, reviews, stepsData) {
175
+ if (existingArtifacts.length === 0)
176
+ return '(none yet)';
177
+ return existingArtifacts
178
+ .map((id) => {
179
+ const review = reviews[id];
180
+ const reviewTag = review ? ' [' + review.status + ']' : '';
181
+ const def = artifactDefs[id];
182
+ const hasSteps = def?.steps && def.steps.length > 0;
183
+ let line = '- ' + id + reviewTag;
184
+ if (hasSteps) {
185
+ line += '\n' + formatStepProgress(def, stepsData[id]);
186
+ }
187
+ return line;
188
+ })
189
+ .join('\n');
190
+ }
191
+ /**
192
+ * Format in-progress artifacts (steps started but artifact not yet finalized).
193
+ */
194
+ function formatInProgressArtifacts(existingArtifacts, artifactDefs, stepsData) {
195
+ const inProgress = Object.keys(stepsData)
196
+ .filter((id) => !existingArtifacts.includes(id))
197
+ .map((id) => {
198
+ const def = artifactDefs[id];
199
+ if (!def?.steps?.length)
200
+ return null;
201
+ const stepState = stepsData[id];
202
+ const completedCount = stepState?.completedSteps.length ?? 0;
203
+ if (completedCount === 0)
204
+ return null;
205
+ return ('- ' +
206
+ id +
207
+ ' (in progress, ' +
208
+ completedCount +
209
+ '/' +
210
+ def.steps.length +
211
+ ' steps)\n' +
212
+ formatStepProgress(def, stepState));
213
+ })
214
+ .filter(Boolean);
215
+ return inProgress.length > 0 ? inProgress.join('\n') : '';
167
216
  }
168
217
  /**
169
218
  * Build the project list summary (when project_name is not provided).
@@ -174,9 +223,9 @@ async function buildProjectList() {
174
223
  return [
175
224
  '# Harmonia Projects',
176
225
  '',
177
- '(无已注册项目)',
226
+ '(no registered projects)',
178
227
  '',
179
- '使用 project_init(project_name, project_dir) 创建新项目。',
228
+ 'Use project_init(project_name, project_dir) to create a new project.',
180
229
  ].join('\n');
181
230
  }
182
231
  const rows = [];
@@ -184,55 +233,53 @@ async function buildProjectList() {
184
233
  try {
185
234
  const entry = await getProject(name);
186
235
  if (!entry || !entry.activeContext) {
187
- rows.push(`| ${name} | ${entry?.dir ?? '?'} | (无活跃上下文) | - | - | - |`);
236
+ rows.push(`| ${name} | ${entry?.dir ?? '?'} | (no active context) | - | - |`);
188
237
  continue;
189
238
  }
190
239
  const resolved = resolveContextDir(name, entry.activeContext);
191
240
  if (!resolved) {
192
- rows.push(`| ${name} | ${entry.dir} | (上下文异常) | - | - | - |`);
241
+ rows.push(`| ${name} | ${entry.dir} | (context error) | - | - |`);
193
242
  continue;
194
243
  }
195
244
  const state = await readState(name, resolved.number, resolved.dir);
196
- const scaleDisplay = state.scale ?? '(未设定)';
197
245
  const updated = state.updatedAt.split('T')[0];
198
- const contextDisplay = entry.activeContext;
199
- rows.push(`| ${name} | ${state.projectDir} | ${state.currentPhase} | ${scaleDisplay} | ${contextDisplay} | ${updated} |`);
246
+ const activeNode = state.activeNodeId ?? '(none)';
247
+ rows.push(`| ${name} | ${state.projectDir} | ${state.workflow} | ${activeNode} | ${entry.activeContext} | ${updated} |`);
200
248
  }
201
249
  catch {
202
- rows.push(`| ${name} | (无法读取状态) | - | - | - | - |`);
250
+ rows.push(`| ${name} | (cannot read state) | - | - | - | - |`);
203
251
  }
204
252
  }
205
253
  return [
206
254
  '# Harmonia Projects',
207
255
  '',
208
- `共 ${projectNames.length} 个项目:`,
256
+ `Total: ${projectNames.length} projects`,
209
257
  '',
210
- '| 项目 | 目录 | 阶段 | 规模 | 上下文 | 更新时间 |',
211
- '|------|------|------|------|--------|----------|',
258
+ '| Project | Directory | Workflow | Active Node | Context | Updated |',
259
+ '|---------|-----------|----------|-------------|---------|---------|',
212
260
  ...rows,
213
261
  '',
214
- '使用 project_status(project_name) 查看项目详情。',
262
+ 'Use project_status(project_name) to view project details.',
215
263
  ].join('\n');
216
264
  }
217
- export function registerGetProjectStatus(server, builtinDir, customDir) {
218
- server.tool('project_status', '查看项目状态。不传 project_name 则返回所有项目的摘要列表;传入 project_name 则返回该项目的详细状态(阶段、文档、dispatch、session、下一步建议)。', {
219
- project_name: z.string().optional().describe('项目名称。不传则返回所有项目的摘要列表。'),
265
+ export function registerGetProjectStatus(server, workflowsDir) {
266
+ server.tool('project_status', 'View project status. Without project_name: returns summary of all projects. With project_name: returns detailed status including workflow tree, artifacts, dispatches, sessions, and next action.', {
267
+ project_name: z.string().optional().describe('Project name. Omit to list all projects.'),
220
268
  }, async ({ project_name }) => {
221
- // List mode — no project_name
269
+ // List mode
222
270
  if (!project_name) {
223
271
  const text = await buildProjectList();
224
272
  return { content: [{ type: 'text', text }] };
225
273
  }
226
- // Detail mode — specific project
274
+ // Detail mode
227
275
  try {
228
- // Resolve project entry
229
276
  const entry = await getProject(project_name);
230
277
  if (!entry) {
231
278
  return {
232
279
  content: [
233
280
  {
234
281
  type: 'text',
235
- text: `项目 "${project_name}" 未注册。使用 project_status() 查看所有项目,或 project_init 创建新项目。`,
282
+ text: `Project "${project_name}" not registered. Use project_status() to list all projects, or project_init to create a new one.`,
236
283
  },
237
284
  ],
238
285
  isError: true,
@@ -252,7 +299,7 @@ export function registerGetProjectStatus(server, builtinDir, customDir) {
252
299
  `Patches: ${entry.totalPatches}`,
253
300
  `Active context: (none)`,
254
301
  ``,
255
- `项目已注册但尚未开始迭代或补丁。请调用 iteration_start(project_name="${project_name}") 开始第一次迭代,或 patch_start 开始补丁。`,
302
+ `Project is registered but has no active iteration or patch. Call iteration_start(project_name="${project_name}") to begin.`,
256
303
  ].join('\n'),
257
304
  },
258
305
  ],
@@ -264,7 +311,7 @@ export function registerGetProjectStatus(server, builtinDir, customDir) {
264
311
  content: [
265
312
  {
266
313
  type: 'text',
267
- text: `项目 "${project_name}" activeContext "${entry.activeContext}" 无法解析。数据可能已损坏。`,
314
+ text: `Project "${project_name}" activeContext "${entry.activeContext}" cannot be resolved. Data may be corrupted.`,
268
315
  },
269
316
  ],
270
317
  isError: true,
@@ -273,73 +320,48 @@ export function registerGetProjectStatus(server, builtinDir, customDir) {
273
320
  const contextDir = resolved.dir;
274
321
  const contextNumber = resolved.number;
275
322
  const state = await readState(project_name, contextNumber, contextDir);
276
- const wf = await loadWorkflow(builtinDir, customDir, state.workflow);
277
- const docs = await listDocs(project_name, contextNumber, contextDir);
323
+ const wf = await loadWorkflow(workflowsDir, state.workflow);
324
+ const statusIoCtx = {
325
+ contextDir,
326
+ projectDir: entry.dir,
327
+ contextLabel: entry.activeContext,
328
+ };
329
+ const artifactIds = await listArtifacts(statusIoCtx, wf.artifactDefinitions);
278
330
  const reviews = await readReviews(project_name, contextNumber, contextDir);
279
- const overrides = await getMergedOverrides(project_name);
280
331
  const dispatches = await readDispatches(project_name, contextNumber, contextDir);
281
332
  const sessions = await readSessions(project_name, contextNumber, contextDir);
282
333
  const stepsData = await readSteps(project_name, contextNumber, contextDir);
283
334
  const issues = await readIssues(project_name);
284
- const scaleDisplay = state.scale ?? '(未设定)';
285
- // Phase summary
286
- const phasesSummary = state.phases
287
- .map((p) => {
288
- const def = wf.definition.phases.find((pd) => pd.id === p.id);
289
- const marker = p.id === state.currentPhase ? ' <-- current' : '';
290
- const name = def ? ` (${def.name})` : '';
291
- return ` ${p.id}${name}: ${p.status}${p.blockedReason ? ` [blocked: ${p.blockedReason}]` : ''}${marker}`;
292
- })
293
- .join('\n');
294
- const currentPhaseDef = wf.definition.phases.find((p) => p.id === state.currentPhase);
295
- // Categorize documents
335
+ // Workflow tree view
336
+ const treeLines = formatNodeTree(wf.definition.root, state.nodes, dispatches);
337
+ // Floating nodes
338
+ if (wf.definition.floatingNodes && wf.definition.floatingNodes.length > 0) {
339
+ treeLines.push('');
340
+ treeLines.push('Floating nodes:');
341
+ for (const fn of wf.definition.floatingNodes) {
342
+ const fnState = state.nodes[fn.id];
343
+ const fnStatus = fnState?.status ?? 'pending';
344
+ const fnIcon = statusIcon(fnStatus);
345
+ treeLines.push(` ${fnIcon} ${fn.id} (task, ${fn.role}) \u2014 ${fnStatus}`);
346
+ }
347
+ }
348
+ // Artifacts
349
+ const artifactDefs = wf.artifactDefinitions;
350
+ const artifactsSection = formatArtifacts(artifactIds, artifactDefs, reviews, stepsData);
351
+ const inProgressSection = formatInProgressArtifacts(artifactIds, artifactDefs, stepsData);
352
+ // Pending reviews
296
353
  const pendingReviews = Object.values(reviews).filter((r) => r.status === 'pending');
297
- // Build pending reviews section
298
354
  const pendingSection = pendingReviews.length > 0
299
355
  ? pendingReviews
300
- .map((r) => `- ${r.docId} (submitted: ${r.submittedAt.split('T')[0]})`)
356
+ .map((r) => `- ${r.artifactId} (submitted: ${r.submittedAt.split('T')[0]})`)
301
357
  .join('\n')
302
358
  : '(none)';
303
- // Build documents section with status and step progress
304
- const docsSection = docs.length > 0
305
- ? docs
306
- .map((d) => {
307
- const review = reviews[d];
308
- const reviewTag = review ? ` [${review.status}]` : '';
309
- const docDef = wf.definition.docs[d];
310
- const hasSteps = docDef?.steps?.length &&
311
- state.scale !== null &&
312
- SEQUENTIAL_SCALES.has(state.scale);
313
- let line = `- ${d}${reviewTag}`;
314
- if (hasSteps) {
315
- line += '\n' + formatStepProgress(docDef, stepsData[d]);
316
- }
317
- return line;
318
- })
319
- .join('\n')
320
- : '(none yet)';
321
- // Show step progress for docs not yet written but with active steps
322
- const inProgressStepDocs = Object.keys(stepsData).filter((docId) => !docs.includes(docId));
323
- const inProgressSection = inProgressStepDocs
324
- .map((docId) => {
325
- const docDef = wf.definition.docs[docId];
326
- if (!docDef?.steps?.length)
327
- return null;
328
- const stepState = stepsData[docId];
329
- const completedCount = stepState?.completedSteps.length ?? 0;
330
- if (completedCount === 0)
331
- return null;
332
- return (`- ${docId} (in progress, ${completedCount}/${docDef.steps.length} steps)\n` +
333
- formatStepProgress(docDef, stepState));
334
- })
335
- .filter(Boolean)
336
- .join('\n');
337
- // Build sessions section
359
+ // Sessions
338
360
  const activeSessions = sessions.filter((s) => s.status !== 'closed');
339
361
  const sessionsSection = activeSessions.length > 0 ? activeSessions.map((s) => formatSession(s)).join('\n') : '(none)';
340
- // Build dispatches section
362
+ // Dispatches
341
363
  const dispatchesSection = dispatches.length > 0 ? dispatches.map((d) => formatDispatch(d, sessions)).join('\n') : '(none)';
342
- // Build issues section
364
+ // Issues
343
365
  const openIssues = issues.filter((i) => i.status === 'open');
344
366
  const closedIssues = issues.filter((i) => i.status === 'closed');
345
367
  const issuesSummary = issues.length > 0
@@ -347,75 +369,70 @@ export function registerGetProjectStatus(server, builtinDir, customDir) {
347
369
  `Total: ${issues.length} (${openIssues.length} open, ${closedIssues.length} closed)`,
348
370
  ...openIssues.map((i) => {
349
371
  const resolvedBy = i.resolvedBy
350
- ? ` ${i.resolvedBy.type}-${i.resolvedBy.number}`
372
+ ? ` \u2192 ${i.resolvedBy.type}-${i.resolvedBy.number}`
351
373
  : '';
352
374
  return ` [OPEN] ${i.id}: ${i.title} (iter-${i.iteration}, ${i.source})${resolvedBy}`;
353
375
  }),
354
376
  ].join('\n')
355
377
  : '(none)';
356
- // Expected outputs — only when scale is set
357
- let expectedOutputsLine = '';
358
- if (currentPhaseDef?.outputs && state.scale !== null) {
359
- const scale = state.scale;
360
- const filtered = currentPhaseDef.outputs.filter((o) => {
361
- const d = wf.definition.docs[o];
362
- if (!d)
363
- return true;
364
- const sv = d.scale[scale];
365
- return sv !== 'skip' && sv !== 'optional';
378
+ // Engine nextAction
379
+ let nextActionText = '';
380
+ try {
381
+ const ctx = {
382
+ entry,
383
+ number: contextNumber,
384
+ dir: contextDir,
385
+ type: resolved.type,
386
+ activeContext: entry.activeContext,
387
+ };
388
+ const engineResult = await processWorkflowEvent(workflowsDir, project_name, ctx, {
389
+ type: 'query_status',
366
390
  });
367
- expectedOutputsLine = `Expected outputs: ${filtered.join(', ')}`;
391
+ nextActionText = formatNextAction(engineResult.nextAction);
368
392
  }
369
- else if (currentPhaseDef?.outputs) {
370
- expectedOutputsLine = `Expected outputs: (需先设定 scale 才能确定)`;
393
+ catch {
394
+ nextActionText = '\n[Next Action] (could not compute \u2014 engine error)';
371
395
  }
372
- // Derive next steps (now dispatch-aware)
373
- const nextSteps = deriveNextSteps(state, wf, docs, pendingReviews, dispatches, sessions, openIssues.length);
396
+ // Build response
397
+ const output = [
398
+ `# Project Status: ${state.projectName}`,
399
+ ``,
400
+ `Source directory: ${state.projectDir}`,
401
+ `Workflow: ${state.workflow}`,
402
+ `Active context: ${entry.activeContext} (${resolved.type} #${contextNumber})`,
403
+ `Iterations: ${entry.currentIteration} / ${entry.totalIterations}`,
404
+ `Patches: ${entry.currentPatch} / ${entry.totalPatches}`,
405
+ `Active node: ${state.activeNodeId ?? '(none)'}`,
406
+ `Created: ${state.createdAt}`,
407
+ `Updated: ${state.updatedAt}`,
408
+ ``,
409
+ `## Workflow Tree`,
410
+ ...treeLines,
411
+ ``,
412
+ `## Sessions`,
413
+ sessionsSection,
414
+ ``,
415
+ `## Dispatches`,
416
+ dispatchesSection,
417
+ ``,
418
+ `## Issues`,
419
+ issuesSummary,
420
+ ``,
421
+ `## Pending Reviews`,
422
+ pendingSection,
423
+ ``,
424
+ `## Artifacts`,
425
+ artifactsSection,
426
+ ...(inProgressSection ? [``, `## In-Progress Artifacts`, inProgressSection] : []),
427
+ ``,
428
+ `## Next Action`,
429
+ nextActionText || '(none)',
430
+ ];
374
431
  return {
375
432
  content: [
376
433
  {
377
434
  type: 'text',
378
- text: [
379
- `# Project Status: ${state.projectName}`,
380
- ``,
381
- `Source directory: ${state.projectDir}`,
382
- `Workflow: ${state.workflow}`,
383
- `Scale: ${scaleDisplay}`,
384
- `Active context: ${entry.activeContext} (${resolved.type} #${contextNumber})`,
385
- `Iterations: ${entry.currentIteration} / ${entry.totalIterations}`,
386
- `Patches: ${entry.currentPatch} / ${entry.totalPatches}`,
387
- `Created: ${state.createdAt}`,
388
- `Updated: ${state.updatedAt}`,
389
- ``,
390
- `## Phases`,
391
- phasesSummary,
392
- ``,
393
- `## Current Phase`,
394
- currentPhaseDef
395
- ? `${currentPhaseDef.name} (${currentPhaseDef.id}): ${currentPhaseDef.description}`
396
- : 'Unknown',
397
- currentPhaseDef?.roles ? `Roles: ${currentPhaseDef.roles.join(', ')}` : '',
398
- expectedOutputsLine,
399
- ``,
400
- `## Sessions`,
401
- sessionsSection,
402
- ``,
403
- `## Dispatches`,
404
- dispatchesSection,
405
- ``,
406
- `## Issues`,
407
- issuesSummary,
408
- ``,
409
- `## Pending Reviews`,
410
- pendingSection,
411
- ``,
412
- `## Documents`,
413
- docsSection,
414
- ...(inProgressSection ? [``, `## In-Progress Steps`, inProgressSection] : []),
415
- ``,
416
- `## Next Steps`,
417
- nextSteps.map((s, i) => `${i + 1}. ${s}`).join('\n'),
418
- ].join('\n'),
435
+ text: output.join('\n'),
419
436
  },
420
437
  ],
421
438
  };
@@ -425,7 +442,7 @@ export function registerGetProjectStatus(server, builtinDir, customDir) {
425
442
  content: [
426
443
  {
427
444
  type: 'text',
428
- text: `项目 "${project_name}" 状态读取失败: ${err instanceof Error ? err.message : String(err)}`,
445
+ text: `Failed to read project "${project_name}" status: ${err instanceof Error ? err.message : String(err)}`,
429
446
  },
430
447
  ],
431
448
  isError: true,