@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,153 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP Tool: project_status
|
|
3
|
-
* Read the current project status with rich context for
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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/
|
|
12
|
-
import {
|
|
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
|
-
*
|
|
23
|
+
* Get status icon for a node.
|
|
21
24
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
125
|
-
const 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
|
|
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 ?
|
|
134
|
-
const label = s.label ?
|
|
135
|
-
const agentType = s.agentType ?
|
|
136
|
-
return
|
|
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
|
|
142
|
-
* Returns lines like:
|
|
143
|
-
* Steps: [✓] 需求结构化 → [✓] 完整性校验 → [→] PRD 文档草稿 → [ ] PRD 最终版
|
|
146
|
+
* Format step progress for a sequential artifact.
|
|
144
147
|
*/
|
|
145
|
-
function formatStepProgress(
|
|
146
|
-
const 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
|
|
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
|
|
164
|
+
return '[✓] ' + s.name;
|
|
162
165
|
if (i === firstIncomplete)
|
|
163
|
-
return
|
|
164
|
-
return
|
|
166
|
+
return '[→] ' + s.name;
|
|
167
|
+
return '[ ] ' + s.name;
|
|
165
168
|
});
|
|
166
|
-
return
|
|
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
|
-
'
|
|
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
|
|
199
|
-
rows.push(`| ${name} | ${state.projectDir} | ${state.
|
|
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
|
-
|
|
256
|
+
`Total: ${projectNames.length} projects`,
|
|
209
257
|
'',
|
|
210
|
-
'|
|
|
211
|
-
'
|
|
258
|
+
'| Project | Directory | Workflow | Active Node | Context | Updated |',
|
|
259
|
+
'|---------|-----------|----------|-------------|---------|---------|',
|
|
212
260
|
...rows,
|
|
213
261
|
'',
|
|
214
|
-
'
|
|
262
|
+
'Use project_status(project_name) to view project details.',
|
|
215
263
|
].join('\n');
|
|
216
264
|
}
|
|
217
|
-
export function registerGetProjectStatus(server,
|
|
218
|
-
server.tool('project_status', '
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
277
|
-
const
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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.
|
|
356
|
+
.map((r) => `- ${r.artifactId} (submitted: ${r.submittedAt.split('T')[0]})`)
|
|
301
357
|
.join('\n')
|
|
302
358
|
: '(none)';
|
|
303
|
-
//
|
|
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
|
-
//
|
|
362
|
+
// Dispatches
|
|
341
363
|
const dispatchesSection = dispatches.length > 0 ? dispatches.map((d) => formatDispatch(d, sessions)).join('\n') : '(none)';
|
|
342
|
-
//
|
|
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
|
-
? `
|
|
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
|
-
//
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
391
|
+
nextActionText = formatNextAction(engineResult.nextAction);
|
|
368
392
|
}
|
|
369
|
-
|
|
370
|
-
|
|
393
|
+
catch {
|
|
394
|
+
nextActionText = '\n[Next Action] (could not compute \u2014 engine error)';
|
|
371
395
|
}
|
|
372
|
-
//
|
|
373
|
-
const
|
|
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:
|
|
445
|
+
text: `Failed to read project "${project_name}" status: ${err instanceof Error ? err.message : String(err)}`,
|
|
429
446
|
},
|
|
430
447
|
],
|
|
431
448
|
isError: true,
|