@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.
- 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 +18 -11
- package/build/core/dispatch.js +43 -33
- package/build/core/dispatch.js.map +1 -1
- package/build/core/issues.d.ts +37 -0
- package/build/core/issues.js +100 -0
- package/build/core/issues.js.map +1 -0
- 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 +36 -3
- package/build/core/registry.js +63 -5
- package/build/core/registry.js.map +1 -1
- package/build/core/reviews.d.ts +13 -13
- package/build/core/reviews.js +31 -32
- 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 +29 -22
- package/build/core/state.js +49 -81
- package/build/core/state.js.map +1 -1
- package/build/core/steps.d.ts +15 -15
- package/build/core/steps.js +32 -33
- 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 +417 -117
- 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 +28 -25
- 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 -103
- package/build/setup/templates.js.map +1 -1
- package/build/tools/artifact-approve.d.ts +8 -0
- package/build/tools/artifact-approve.js +94 -0
- 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/dispatch-report.js +261 -0
- 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 +308 -246
- 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/issue-tools.d.ts +10 -0
- package/build/tools/issue-tools.js +169 -0
- package/build/tools/issue-tools.js.map +1 -0
- package/build/tools/iteration-start.d.ts +7 -4
- package/build/tools/iteration-start.js +51 -20
- 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 +16 -0
- package/build/tools/patch-start.js +122 -0
- package/build/tools/patch-start.js.map +1 -0
- package/build/tools/project-init.d.ts +5 -5
- package/build/tools/project-init.js +47 -18
- 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 +40 -0
- package/build/tools/utils.js +97 -0
- package/build/tools/utils.js.map +1 -0
- 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 -32
- package/build/core/docs.js +0 -91
- 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 +0 -108
- 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 -277
- 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 -389
- 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 +0 -194
- 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 -107
- 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 -159
- 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,148 +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
|
-
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
|
-
*
|
|
23
|
+
* Get status icon for a node.
|
|
20
24
|
*/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
120
|
-
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 : '';
|
|
121
121
|
const brief = d.taskBrief.length > 60 ? d.taskBrief.slice(0, 57) + '...' : d.taskBrief;
|
|
122
|
-
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);
|
|
123
135
|
}
|
|
124
136
|
/**
|
|
125
137
|
* Format a session record for display.
|
|
126
138
|
*/
|
|
127
139
|
function formatSession(s) {
|
|
128
|
-
const agentInfo = s.agentSessionId ?
|
|
129
|
-
const label = s.label ?
|
|
130
|
-
const agentType = s.agentType ?
|
|
131
|
-
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;
|
|
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
|
|
137
|
-
* Returns lines like:
|
|
138
|
-
* Steps: [✓] 需求结构化 → [✓] 完整性校验 → [→] PRD 文档草稿 → [ ] PRD 最终版
|
|
146
|
+
* Format step progress for a sequential artifact.
|
|
139
147
|
*/
|
|
140
|
-
function formatStepProgress(
|
|
141
|
-
const 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
|
|
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
|
|
164
|
+
return '[✓] ' + s.name;
|
|
157
165
|
if (i === firstIncomplete)
|
|
158
|
-
return
|
|
159
|
-
return
|
|
166
|
+
return '[→] ' + s.name;
|
|
167
|
+
return '[ ] ' + s.name;
|
|
160
168
|
});
|
|
161
|
-
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') : '';
|
|
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
|
-
'
|
|
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.
|
|
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,
|
|
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
|
|
189
|
-
rows.push(`| ${name} | ${state.projectDir} | ${state.
|
|
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
|
-
|
|
256
|
+
`Total: ${projectNames.length} projects`,
|
|
199
257
|
'',
|
|
200
|
-
'|
|
|
201
|
-
'
|
|
258
|
+
'| Project | Directory | Workflow | Active Node | Context | Updated |',
|
|
259
|
+
'|---------|-----------|----------|-------------|---------|---------|',
|
|
202
260
|
...rows,
|
|
203
261
|
'',
|
|
204
|
-
'
|
|
262
|
+
'Use project_status(project_name) to view project details.',
|
|
205
263
|
].join('\n');
|
|
206
264
|
}
|
|
207
|
-
export function registerGetProjectStatus(server,
|
|
208
|
-
server.tool('project_status', '
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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:
|
|
298
|
+
`Iterations: ${entry.totalIterations}`,
|
|
299
|
+
`Patches: ${entry.totalPatches}`,
|
|
300
|
+
`Active context: (none)`,
|
|
242
301
|
``,
|
|
243
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
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.
|
|
356
|
+
.map((r) => `- ${r.artifactId} (submitted: ${r.submittedAt.split('T')[0]})`)
|
|
275
357
|
.join('\n')
|
|
276
358
|
: '(none)';
|
|
277
|
-
//
|
|
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
|
-
//
|
|
362
|
+
// Dispatches
|
|
315
363
|
const dispatchesSection = dispatches.length > 0 ? dispatches.map((d) => formatDispatch(d, sessions)).join('\n') : '(none)';
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
391
|
+
nextActionText = formatNextAction(engineResult.nextAction);
|
|
328
392
|
}
|
|
329
|
-
|
|
330
|
-
|
|
393
|
+
catch {
|
|
394
|
+
nextActionText = '\n[Next Action] (could not compute \u2014 engine error)';
|
|
331
395
|
}
|
|
332
|
-
//
|
|
333
|
-
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
|
+
];
|
|
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:
|
|
445
|
+
text: `Failed to read project "${project_name}" status: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
446
|
},
|
|
385
447
|
],
|
|
386
448
|
isError: true,
|