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