Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/hetaoBackend/mcode-dynamic-workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Turn a complex task into a reviewable multi-agent workflow. Inspect and edit the topology before execution, follow each agent's progress and output, then repair a failed script without discarding valid completed work.

Version **0.8.0** · Apache-2.0 · one English Skill and one local MCP server with 11 tools.
Version **0.8.0** · Apache-2.0 · one English Skill and one local MCP server with 14 tools.

## Try it

Expand Down Expand Up @@ -82,6 +82,6 @@ Verification covers isolated demo execution, approval gating, selective reuse an

## MCP tools

`workflow_validate`, `workflow_start`, `workflow_update`, `workflow_repair`, `workflow_status`, `workflow_results`, `workflow_wait`, `workflow_pause`, `workflow_cancel`, `workflow_resume`, `workflow_dashboard`.
`workflow_validate`, `workflow_start`, `workflow_update`, `workflow_repair`, `workflow_status`, `workflow_results`, `workflow_wait`, `workflow_pause`, `workflow_cancel`, `workflow_resume`, `workflow_delete`, `workflow_restore`, `workflow_rerun`, `workflow_dashboard`.

Read the [Skill](skills/dynamic-workflow/SKILL.md), [English example](examples/audit-en.js) and [Chinese example](examples/audit.js). The dashboard is a local web page, not a native Mini App or TUI extension.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import test from 'node:test';import assert from 'node:assert/strict';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';import {mkdtemp,rm,writeFile} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';
import test from 'node:test';import assert from 'node:assert/strict';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';import {mkdtemp,rm,writeFile} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';import {execFile} from 'node:child_process';import {promisify} from 'node:util';
// win32 EBUSY on tmpdir cleanup has two layers. (1) --stdio spawns a detached daemon (src/main.mjs: spawn with cwd:workspace, service.log and the DB in dataDir, child.unref(); "the service, workers and dashboard outlive" the chat transport by design), so closing client/transport leaves that daemon alive with its cwd and handles inside the mkdtemp dir — win32 refuses to rmdir a tree a live process sits in, and retrying alone cannot fix it: run 35506404071 burned the full ~6s retry budget and still failed EBUSY. Stop the service first, same as checks/fail-loud.check.mjs (--stop-service matches this test's --settings identity because workspace=dataDir=dir in both). (2) Even after the daemon is gone, StdioClientTransport.close() resolving does not mean the child has fully exited, so handle release can lag an instant: run 35506090983. Hence stop-service, then rmWithRetry as a release-lag fallback, then fail loud — cleanup failures stay visible. Linux/darwin unlink open files, so none of this bites locally.
const exec=promisify(execFile);
const rmWithRetry=async(dir)=>{for(let i=0;;i++){try{return await rm(dir,{recursive:true,force:true})}catch(e){if(!['EBUSY','ENOTEMPTY','EPERM'].includes(e?.code)||i>=5)throw e;await new Promise(res=>setTimeout(res,200*2**i))}}};
test('packaged MCP advertises reuseAcrossRuns and accepts it through the public tool surface',async()=>{
const dir=await mkdtemp(join(tmpdir(),'wf-cross-mcp-'));await writeFile(join(dir,'settings.json'),JSON.stringify({workspace:dir,dataDir:dir}));
const client=new Client({name:'cross-reuse-mcp-test',version:'1'});const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/main.mjs'),'--stdio','--settings',join(dir,'settings.json')],stderr:'pipe'});
Expand All @@ -10,5 +13,5 @@ test('packaged MCP advertises reuseAcrossRuns and accepts it through the public
assert.equal(run.reuseAcrossRuns,true,'the flag must survive the public tool surface');
const rejected=await client.callTool({name:'workflow_start',arguments:{requestId:'cross-mcp-bad',name:'Bad flag type',executor:'demo',reuseAcrossRuns:'yes',script:'return 1;'}});
assert.ok(rejected.isError,'a non-boolean flag must be rejected by the public surface');
}finally{await client.close();await transport.close();await rm(dir,{recursive:true,force:true});}
}finally{let closeErr;try{await client.close();await transport.close();}catch(e){closeErr=e}try{await exec(process.execPath,[resolve('dist/main.mjs'),'--stop-service','--workspace',dir,'--data-dir',dir]);}catch{}await rmWithRetry(dir);if(closeErr)throw closeErr;}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Regression nail for the lineage panel render-idempotence bugs found by the
// egolite interaction pass: polling over an unchanged family must keep the
// user's compare pair and open diff (identity signature stable), while live
// member data (status, duration, trash/archive flags) must stay refreshable
// through the member cards without forcing a control rebuild.
import test from 'node:test';import assert from 'node:assert/strict';
import {compareSignature,shouldRebuildCompare} from '../web/lineage-model.mjs';
const member=(id,rerunSeq,name,extra={})=>({id,rerunSeq,name,status:'succeeded',createdAt:1,durationMs:1000,resultPreview:null,deleted:null,archived:null,...extra});
const family=[member('run-root',0,'Family root'),member('run-rerun-1',1,'First rerun',{status:'running',durationMs:400})];
test('poll-shaped refresh (fresh objects, moved live data, fixed identity) keeps the compare controls',()=>{
const before=compareSignature('zh',family);
const poll=family.map(m=>({...m,status:'succeeded',durationMs:m.durationMs+9000,resultPreview:'done'}));
assert.equal(shouldRebuildCompare(before,compareSignature('zh',poll)),false);
assert.equal(compareSignature('zh',family),compareSignature('zh',[...family]));
});
test('trash restore clears deleted flags without rebuilding the compare pair',()=>{
const trashed=family.map(m=>m.id==='run-rerun-1'?{...m,deleted:{at:2,by:'studio'}}:m);
const restored=trashed.map(m=>({...m,deleted:null}));
assert.equal(shouldRebuildCompare(compareSignature('en',trashed),compareSignature('en',restored)),false);
});
test('identity changes (member added, removed or reordered) rebuild the controls',()=>{
const before=compareSignature('zh',family);
assert.equal(shouldRebuildCompare(before,compareSignature('zh',[...family,member('run-rerun-2',2,'Second rerun')])),true);
assert.equal(shouldRebuildCompare(before,compareSignature('zh',family.slice(0,1))),true);
assert.equal(shouldRebuildCompare(before,compareSignature('zh',[...family].reverse())),true);
});
test('language switch and member renames rebuild so option labels stay faithful',()=>{
assert.equal(shouldRebuildCompare(compareSignature('zh',family),compareSignature('en',family)),true);
assert.equal(shouldRebuildCompare(compareSignature('zh',family),compareSignature('zh',family.map(m=>m.id==='run-root'?{...m,name:'Renamed'}:m))),true);
});
test('first render of any family rebuilds and empty/missing members are safe',()=>{
assert.equal(shouldRebuildCompare('',compareSignature('zh',family)),true);
assert.equal(compareSignature('zh',[]),compareSignature('zh',null));
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import test from 'node:test';import assert from 'node:assert/strict';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';import {mkdtemp,rm,writeFile} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';
test('clean packaged MCP lists tools and completes a sandbox workflow',async()=>{const dir=await mkdtemp(join(tmpdir(),'wf-package-'));await writeFile(join(dir,'settings.json'),JSON.stringify({workspace:dir,dataDir:dir}));const client=new Client({name:'test',version:'1'});const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/main.mjs'),'--stdio','--settings',join(dir,'settings.json')],stderr:'pipe'});let stderr='';transport.stderr?.on('data',c=>stderr+=c);try{await client.connect(transport);const tools=await client.listTools();assert.equal(tools.tools.length,11);assert.equal(tools.tools.find(t=>t.name==='workflow_start').inputSchema.properties.maxSteps.maximum,1000);const dashboard=await client.callTool({name:'workflow_dashboard',arguments:{}});assert.match(dashboard.content[0].text,/127\.0\.0\.1/);const start=await client.callTool({name:'workflow_start',arguments:{requestId:'package',name:'Packaged',executor:'demo',maxSteps:160,stepTimeoutMs:1800000,script:'return await ctx.agent({id:"a",prompt:"p"});'}});assert.ok(!start.isError,start.content?.[0]?.text);const started=JSON.parse(start.content[0].text);assert.equal(started.maxSteps,160);assert.equal(started.stepTimeoutMs,1800000);const id=started.id;assert.equal(started.status,'pending_review');const u=new URL(JSON.parse(dashboard.content[0].text).url);const approved=await fetch(u.origin+`/api/runs/${id}/approve`,{method:'POST',headers:{'X-Workflow-Client':'1','Content-Type':'application/json'},body:JSON.stringify({revision:started.revision})});assert.equal(approved.status,200);let status;for(let i=0;i<15;i++){const r=await client.callTool({name:'workflow_wait',arguments:{runId:id,timeoutMs:500}});status=JSON.parse(r.content[0].text).status;if(status==='succeeded')break;await new Promise(r=>setTimeout(r,100));}assert.equal(status,'succeeded',stderr);const detail=JSON.parse((await client.callTool({name:'workflow_results',arguments:{runId:id,includeDefinition:true}})).content[0].text);const repair=await client.callTool({name:'workflow_repair',arguments:{runId:id,sourceUpdatedAt:detail.updatedAt,requestId:'package-repair',reason:'Correct synthesis',script:detail.definition.script,reuseStepIds:['a']}});assert.ok(!repair.isError,repair.content?.[0]?.text);assert.equal(JSON.parse(repair.content[0].text).status,'pending_review');}finally{await client.close();await transport.close();await stopService(dir);await rm(dir,{recursive:true,force:true});}});
test('clean packaged MCP lists tools and completes a sandbox workflow',async()=>{const dir=await mkdtemp(join(tmpdir(),'wf-package-'));await writeFile(join(dir,'settings.json'),JSON.stringify({workspace:dir,dataDir:dir}));const client=new Client({name:'test',version:'1'});const transport=new StdioClientTransport({command:process.execPath,args:[resolve('dist/main.mjs'),'--stdio','--settings',join(dir,'settings.json')],stderr:'pipe'});let stderr='';transport.stderr?.on('data',c=>stderr+=c);try{await client.connect(transport);const tools=await client.listTools();assert.equal(tools.tools.length,14);assert.equal(tools.tools.find(t=>t.name==='workflow_start').inputSchema.properties.maxSteps.maximum,1000);const dashboard=await client.callTool({name:'workflow_dashboard',arguments:{}});assert.match(dashboard.content[0].text,/127\.0\.0\.1/);const start=await client.callTool({name:'workflow_start',arguments:{requestId:'package',name:'Packaged',executor:'demo',maxSteps:160,stepTimeoutMs:1800000,script:'return await ctx.agent({id:"a",prompt:"p"});'}});assert.ok(!start.isError,start.content?.[0]?.text);const started=JSON.parse(start.content[0].text);assert.equal(started.maxSteps,160);assert.equal(started.stepTimeoutMs,1800000);const id=started.id;assert.equal(started.status,'pending_review');const u=new URL(JSON.parse(dashboard.content[0].text).url);const approved=await fetch(u.origin+`/api/runs/${id}/approve`,{method:'POST',headers:{'X-Workflow-Client':'1','Content-Type':'application/json'},body:JSON.stringify({revision:started.revision})});assert.equal(approved.status,200);let status;for(let i=0;i<15;i++){const r=await client.callTool({name:'workflow_wait',arguments:{runId:id,timeoutMs:500}});status=JSON.parse(r.content[0].text).status;if(status==='succeeded')break;await new Promise(r=>setTimeout(r,100));}assert.equal(status,'succeeded',stderr);const detail=JSON.parse((await client.callTool({name:'workflow_results',arguments:{runId:id,includeDefinition:true}})).content[0].text);const repair=await client.callTool({name:'workflow_repair',arguments:{runId:id,sourceUpdatedAt:detail.updatedAt,requestId:'package-repair',reason:'Correct synthesis',script:detail.definition.script,reuseStepIds:['a']}});assert.ok(!repair.isError,repair.content?.[0]?.text);assert.equal(JSON.parse(repair.content[0].text).status,'pending_review');}finally{await client.close();await transport.close();await stopService(dir);await rm(dir,{recursive:true,force:true});}});
test('second MCP connection proxies the existing owner, child workers expose no tools',async()=>{
const dir=await mkdtemp(join(tmpdir(),'wf-proxy-'));const clients=[],transports=[];
const connect=async(env)=>{const c=new Client({name:'proxy-test',version:'1'});const t=new StdioClientTransport({command:process.execPath,args:[resolve('dist/main.mjs'),'--stdio','--data-dir',dir,'--workspace',dir],stderr:'pipe',...(env?{env}:{})});clients.push(c);transports.push(t);await c.connect(t);return c;};
try{const owner=await connect();const proxy=await connect();const a=await owner.callTool({name:'workflow_dashboard',arguments:{}}),b=await proxy.callTool({name:'workflow_dashboard',arguments:{}});assert.deepEqual(a,b);await proxy.close();assert.equal((await owner.listTools()).tools.length,11);const worker=await connect({...process.env,MCODE_WORKFLOW_CHILD:'1'});assert.equal((await worker.listTools()).tools.length,0);}finally{for(const c of clients.reverse())await c.close();for(const t of transports)await t.close();await stopService(dir);await rm(dir,{recursive:true,force:true});}
try{const owner=await connect();const proxy=await connect();const a=await owner.callTool({name:'workflow_dashboard',arguments:{}}),b=await proxy.callTool({name:'workflow_dashboard',arguments:{}});assert.deepEqual(a,b);await proxy.close();assert.equal((await owner.listTools()).tools.length,14);const worker=await connect({...process.env,MCODE_WORKFLOW_CHILD:'1'});assert.equal((await worker.listTools()).tools.length,0);}finally{for(const c of clients.reverse())await c.close();for(const t of transports)await t.close();await stopService(dir);await rm(dir,{recursive:true,force:true});}
});

async function stopService(dir){const {execFile}=await import('node:child_process');const {promisify}=await import('node:util');await promisify(execFile)(process.execPath,[resolve('dist/main.mjs'),'--stop-service','--data-dir',dir,'--workspace',dir]);}
Loading
Loading