@mediadatafusion/pi-workflow-suite 0.0.11 → 0.0.12

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 (62) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +26 -17
  3. package/VERSION +1 -1
  4. package/agents/codebase-research.md +7 -5
  5. package/agents/general-worker.md +9 -7
  6. package/agents/implementation-planning.md +5 -3
  7. package/agents/quality-validation.md +9 -8
  8. package/agents/workflow-orchestrator.md +9 -7
  9. package/config/prompts/execute-approved-plan.md +12 -2
  10. package/config/prompts/mission-final-validation.md +38 -5
  11. package/config/prompts/mission-plan.md +17 -1
  12. package/config/prompts/mission-repair.md +16 -2
  13. package/config/prompts/mission-review-prompt.md +19 -6
  14. package/config/prompts/mission-run.md +18 -5
  15. package/config/prompts/validate-approved-plan.md +57 -3
  16. package/config/prompts/workflow-plan-prompt.md +11 -1
  17. package/config/prompts/workflow-repair.md +18 -2
  18. package/config/prompts/workflow-reviewer-prompt.md +25 -9
  19. package/config/prompts/workflow-summary.md +1 -4
  20. package/config/workflow-settings.example.json +13 -11
  21. package/docs/assets/mediadatafusion-logo.png +0 -0
  22. package/docs/assets/pi-workflow-suite-demo.gif +0 -0
  23. package/docs/assets/pi-workflow-suite-demo.mp4 +0 -0
  24. package/docs/assets/pi-workflow-suite-header.png +0 -0
  25. package/docs/assets/pi-workflow-suite-video-thumb.png +0 -0
  26. package/docs/assets/readme-link-commands.svg +10 -0
  27. package/docs/assets/readme-link-install.svg +10 -0
  28. package/docs/assets/readme-link-quick-start.svg +10 -0
  29. package/docs/assets/readme-link-settings.svg +10 -0
  30. package/docs/assets/screenshots/.gitkeep +1 -0
  31. package/docs/assets/screenshots/00-mission-home.png +0 -0
  32. package/docs/assets/screenshots/01-startup-Logo.png +0 -0
  33. package/docs/assets/screenshots/02-theme-settings.png +0 -0
  34. package/docs/assets/screenshots/03-GlobalSafetySettings.png +0 -0
  35. package/docs/assets/screenshots/04-SharedSubAgentsSettings.png +0 -0
  36. package/docs/assets/screenshots/05-mission-mode.png +0 -0
  37. package/docs/assets/screenshots/06-diagram-mermaid.png +0 -0
  38. package/extensions/subagent/index.ts +41 -18
  39. package/extensions/subagent/repolock-guard.ts +224 -4
  40. package/extensions/subagent/runner.ts +136 -12
  41. package/extensions/workflow-model-router.ts +124 -41
  42. package/extensions/workflow-modes.ts +3791 -967
  43. package/extensions/workflow-settings-capabilities.ts +10 -0
  44. package/extensions/workflow-state.ts +77 -10
  45. package/extensions/workflow-subagent-policy.ts +13 -1
  46. package/extensions/workflow-summary.ts +8 -19
  47. package/extensions/workflow-tool-guard.ts +326 -35
  48. package/extensions/workflow-validation-classifier.ts +46 -4
  49. package/extensions/workflow-web-tools.ts +361 -1
  50. package/package.json +9 -5
  51. package/scripts/audit-live.sh +1 -1
  52. package/scripts/build-package-export.mjs +8 -13
  53. package/scripts/check-clean-release-tree.sh +3 -2
  54. package/scripts/check-package-media.mjs +78 -0
  55. package/scripts/install-to-live.sh +2 -0
  56. package/scripts/package-media-config.mjs +28 -0
  57. package/scripts/prepare-package-readme.mjs +19 -18
  58. package/scripts/quarantine-live-junk.sh +1 -1
  59. package/scripts/verify-live.sh +9 -1
  60. package/skills/implementation-planning/SKILL.md +1 -1
  61. package/skills/safe-execution/SKILL.md +1 -1
  62. package/skills/validation-review/SKILL.md +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
2
  import { type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+ import { execSync } from "node:child_process";
3
4
  import { Type } from "typebox";
4
5
 
5
6
  type RuntimeToolInfo = {
@@ -32,9 +33,44 @@ export interface WorkflowWebFetchDetails {
32
33
  fetchedAt: string;
33
34
  }
34
35
 
36
+ export interface WorkflowBrowserAction {
37
+ action: "click" | "type" | "wait" | "waitForSelector" | "select" | "evaluate" | "screenshot" | "reload" | "readText" | "readAttr";
38
+ selector?: string;
39
+ value?: string;
40
+ timeout?: number;
41
+ label?: string;
42
+ }
43
+
44
+ export interface WorkflowBrowserActionResult {
45
+ label?: string;
46
+ action: string;
47
+ ok: boolean;
48
+ error?: string;
49
+ text?: string;
50
+ attrValue?: string | null;
51
+ result?: unknown;
52
+ found?: boolean;
53
+ }
54
+
55
+ export interface WorkflowBrowserCheckDetails {
56
+ url: string;
57
+ title: string;
58
+ consoleMessages: string[];
59
+ pageErrors: string[];
60
+ elementCounts: Record<string, number>;
61
+ localStorageValues: Record<string, string | null>;
62
+ screenshotPath?: string;
63
+ loadTimeMs: number;
64
+ error?: string;
65
+ actionResults?: WorkflowBrowserActionResult[];
66
+ }
67
+
35
68
  const WORKFLOW_WEB_SEARCH_TOOL = "workflow_web_search";
36
69
  const WORKFLOW_WEB_FETCH_TOOL = "workflow_web_fetch";
37
- const WORKFLOW_WEB_TOOLS = [WORKFLOW_WEB_SEARCH_TOOL, WORKFLOW_WEB_FETCH_TOOL];
70
+ const WORKFLOW_BROWSER_CHECK_TOOL = "workflow_browser_check";
71
+ const WORKFLOW_STOP_SERVER_TOOL = "workflow_stop_server";
72
+ const WORKFLOW_WEB_TOOLS = [WORKFLOW_WEB_SEARCH_TOOL, WORKFLOW_WEB_FETCH_TOOL, WORKFLOW_BROWSER_CHECK_TOOL, WORKFLOW_STOP_SERVER_TOOL];
73
+ const workflowWebErrorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error ?? "");
38
74
  const SEARCH_TIMEOUT_MS = 12_000;
39
75
  const FETCH_TIMEOUT_MS = 12_000;
40
76
  const MAX_SEARCH_RESULTS = 10;
@@ -73,6 +109,24 @@ const WorkflowWebFetchParams = Type.Object({
73
109
  maxChars: Type.Optional(Type.Number({ description: "Maximum extracted text characters to return, 1000-18000. Default 12000.", minimum: 1000, maximum: 18000 })),
74
110
  });
75
111
 
112
+ const WorkflowBrowserCheckParams = Type.Object({
113
+ url: Type.String({ description: "Full URL to check in a headless browser, e.g. http://localhost:8017." }),
114
+ selectors: Type.Optional(Type.Array(Type.String(), { description: "CSS selectors to count matching elements." })),
115
+ localStorageKeys: Type.Optional(Type.Array(Type.String(), { description: "localStorage keys to retrieve values for." })),
116
+ screenshot: Type.Optional(Type.Boolean({ description: "Take a full-page screenshot saved to /tmp/validator_screenshot.png." })),
117
+ actions: Type.Optional(Type.Array(Type.Object({
118
+ action: Type.String({ description: "Interaction type: click, type, wait, waitForSelector, select, evaluate, screenshot, reload, readText, readAttr." }),
119
+ selector: Type.Optional(Type.String({ description: "CSS selector for the target element (required for click, type, waitForSelector, readText, readAttr)." })),
120
+ value: Type.Optional(Type.String({ description: "Text to type (type action), JS expression (evaluate action), or attribute name (readAttr action)." })),
121
+ timeout: Type.Optional(Type.Number({ description: "Timeout in ms. Default 5000." })),
122
+ label: Type.Optional(Type.String({ description: "Human-readable label for this step in results." })),
123
+ }), { description: "Sequential browser interaction steps to perform after page load." })),
124
+ });
125
+
126
+ const WorkflowStopServerParams = Type.Object({
127
+ port: Type.Number({ description: "Port number to kill any process listening on (1-65535).", minimum: 1, maximum: 65535 }),
128
+ });
129
+
76
130
  function normalizeToolName(name: string): string {
77
131
  return name.trim().toLowerCase();
78
132
  }
@@ -270,6 +324,211 @@ export async function workflowWebFetch(rawUrl: string, maxChars = 12_000): Promi
270
324
  }
271
325
  }
272
326
 
327
+ export async function workflowBrowserCheck(
328
+ url: string,
329
+ selectors?: string[],
330
+ localStorageKeys?: string[],
331
+ screenshot?: boolean,
332
+ actions?: WorkflowBrowserAction[],
333
+ ): Promise<WorkflowBrowserCheckDetails> {
334
+ const startTime = Date.now();
335
+ const cleanUrl = url.trim();
336
+ if (!cleanUrl) throw new Error("URL is required.");
337
+ let puppeteer: typeof import("puppeteer");
338
+ try {
339
+ puppeteer = (await import("puppeteer")).default as typeof import("puppeteer");
340
+ } catch {
341
+ return {
342
+ url: cleanUrl, title: "", consoleMessages: [], pageErrors: [],
343
+ elementCounts: {}, localStorageValues: {}, loadTimeMs: Date.now() - startTime,
344
+ error: "Puppeteer is not available in this environment.",
345
+ };
346
+ }
347
+ const browser = await puppeteer.launch({ headless: "shell", args: ["--no-sandbox"] });
348
+ try {
349
+ const page = await browser.newPage();
350
+ const consoleMessages: string[] = [];
351
+ const pageErrors: string[] = [];
352
+ page.on("console", (msg) => consoleMessages.push(`${msg.type()}: ${msg.text()}`));
353
+ page.on("pageerror", (err) => pageErrors.push(workflowWebErrorMessage(err)));
354
+ await page.goto(cleanUrl, { waitUntil: "networkidle2", timeout: 15000 });
355
+ const title = await page.title();
356
+ const elementCounts: Record<string, number> = {};
357
+ if (selectors && selectors.length > 0) {
358
+ for (const sel of selectors) {
359
+ try {
360
+ elementCounts[sel] = await page.$$eval(sel, (els) => els.length);
361
+ } catch {
362
+ elementCounts[sel] = 0;
363
+ }
364
+ }
365
+ }
366
+ const localStorageValues: Record<string, string | null> = {};
367
+ if (localStorageKeys && localStorageKeys.length > 0) {
368
+ for (const key of localStorageKeys) {
369
+ try {
370
+ localStorageValues[key] = await page.evaluate((k) => localStorage.getItem(k), key);
371
+ } catch {
372
+ localStorageValues[key] = null;
373
+ }
374
+ }
375
+ }
376
+ let screenshotPath: string | undefined;
377
+ if (screenshot) {
378
+ screenshotPath = "/tmp/validator_screenshot.png";
379
+ await page.screenshot({ path: screenshotPath, fullPage: true });
380
+ }
381
+ const actionResults: WorkflowBrowserActionResult[] = [];
382
+ if (actions && actions.length > 0) {
383
+ for (const act of actions) {
384
+ const result: WorkflowBrowserActionResult = { label: act.label, action: act.action, ok: false };
385
+ const timeout = act.timeout ?? 5000;
386
+ try {
387
+ switch (act.action) {
388
+ case "click":
389
+ if (!act.selector) throw new Error("selector required for click");
390
+ await page.waitForSelector(act.selector, { timeout });
391
+ await page.click(act.selector);
392
+ result.ok = true;
393
+ break;
394
+ case "type":
395
+ if (!act.selector) throw new Error("selector required for type");
396
+ if (act.value === undefined) throw new Error("value required for type");
397
+ await page.waitForSelector(act.selector, { timeout });
398
+ await page.type(act.selector, act.value, { delay: 20 });
399
+ result.ok = true;
400
+ break;
401
+ case "wait":
402
+ await new Promise((r) => setTimeout(r, timeout));
403
+ result.ok = true;
404
+ break;
405
+ case "waitForSelector":
406
+ if (!act.selector) throw new Error("selector required for waitForSelector");
407
+ await page.waitForSelector(act.selector, { timeout });
408
+ result.found = true;
409
+ result.ok = true;
410
+ break;
411
+ case "select":
412
+ if (!act.selector) throw new Error("selector required for select");
413
+ if (act.value === undefined) throw new Error("value required for select");
414
+ await page.waitForSelector(act.selector, { timeout });
415
+ await page.select(act.selector, act.value);
416
+ result.ok = true;
417
+ break;
418
+ case "evaluate":
419
+ if (act.value === undefined) throw new Error("value (JS expression) required for evaluate");
420
+ result.result = await page.evaluate(act.value);
421
+ result.ok = true;
422
+ break;
423
+ case "screenshot": {
424
+ const stepPath = `/tmp/validator_step_${actionResults.length}.png`;
425
+ await page.screenshot({ path: stepPath, fullPage: true });
426
+ result.result = stepPath;
427
+ result.ok = true;
428
+ break;
429
+ }
430
+ case "reload":
431
+ await page.reload({ waitUntil: "networkidle2", timeout });
432
+ result.ok = true;
433
+ break;
434
+ case "readText":
435
+ if (!act.selector) throw new Error("selector required for readText");
436
+ await page.waitForSelector(act.selector, { timeout });
437
+ result.text = await page.$eval(act.selector, (el) => (el as HTMLElement).innerText?.trim() ?? el.textContent?.trim() ?? "");
438
+ result.ok = true;
439
+ break;
440
+ case "readAttr":
441
+ if (!act.selector) throw new Error("selector required for readAttr");
442
+ if (!act.value) throw new Error("value (attribute name) required for readAttr");
443
+ await page.waitForSelector(act.selector, { timeout });
444
+ result.attrValue = await page.$eval(act.selector, (el, attr) => el.getAttribute(attr as string), act.value);
445
+ result.ok = true;
446
+ break;
447
+ default:
448
+ result.error = `Unknown action type: ${act.action}`;
449
+ }
450
+ } catch (err) {
451
+ result.error = err instanceof Error ? err.message : String(err);
452
+ }
453
+ actionResults.push(result);
454
+ }
455
+ }
456
+ return {
457
+ url: cleanUrl, title, consoleMessages, pageErrors,
458
+ elementCounts, localStorageValues, screenshotPath,
459
+ loadTimeMs: Date.now() - startTime,
460
+ ...(actionResults.length > 0 ? { actionResults } : {}),
461
+ };
462
+ } finally {
463
+ await browser.close();
464
+ }
465
+ }
466
+
467
+
468
+ // ── Cross-platform port-kill utility ──────────────────────────
469
+
470
+ function killOnPortUnix(port: number): boolean {
471
+ // Primary: lsof — works on macOS (built-in) and most Linux systems
472
+ try {
473
+ const out = execSync("lsof -ti :" + port + " 2>/dev/null", { timeout: 3000, encoding: "utf8" });
474
+ const pids = out.trim().split("\n").map(Number).filter(pid => pid > 0);
475
+ if (pids.length > 0) {
476
+ for (const pid of pids) {
477
+ try { process.kill(pid, "SIGTERM"); } catch { /* already dead */ }
478
+ }
479
+ return true;
480
+ }
481
+ } catch { /* lsof unavailable or port free */ }
482
+ // Fallback 1: fuser — Linux psmisc
483
+ try {
484
+ execSync("fuser -k " + port + "/tcp 2>/dev/null || true", { timeout: 3000 });
485
+ return true;
486
+ } catch { /* fuser unavailable */ }
487
+ // Fallback 2: ss — Linux iproute2 (always available, even in minimal containers)
488
+ try {
489
+ const out = execSync("ss -tlnp 'sport = :" + port + "' 2>/dev/null", { timeout: 3000, encoding: "utf8" });
490
+ const match = out.match(/pid=(\d+)/);
491
+ if (match) {
492
+ try { process.kill(parseInt(match[1], 10), "SIGTERM"); } catch { /* already dead */ }
493
+ return true;
494
+ }
495
+ } catch { /* ss unavailable */ }
496
+ return false;
497
+ }
498
+
499
+ function killOnPortWindows(port: number): boolean {
500
+ try {
501
+ const out = execSync("netstat -ano | findstr :" + port, { timeout: 3000, encoding: "utf8", shell: "cmd.exe" });
502
+ const lines = out.split("\n");
503
+ let killed = false;
504
+ for (const line of lines) {
505
+ const parts = line.trim().split(/\s+/);
506
+ if (parts.length >= 5 && parts[1] && parts[1].endsWith(":" + port)) {
507
+ const pid = parseInt(parts[parts.length - 1], 10);
508
+ if (pid > 0) {
509
+ try {
510
+ execSync("taskkill /PID " + pid + " /F", { timeout: 3000, shell: "cmd.exe" });
511
+ killed = true;
512
+ } catch { /* taskkill failed */ }
513
+ }
514
+ }
515
+ }
516
+ return killed;
517
+ } catch { return false; }
518
+ }
519
+
520
+ function killProcessOnPort(port: number): boolean {
521
+ try {
522
+ if (process.platform === "win32") {
523
+ return killOnPortWindows(port);
524
+ } else {
525
+ return killOnPortUnix(port);
526
+ }
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
531
+
273
532
  export function refreshRuntimeWebTools(pi: ExtensionAPI): string[] {
274
533
  const tools = pi.getAllTools() as RuntimeToolInfo[];
275
534
  discoveredRuntimeWebTools = Array.from(new Set(
@@ -351,6 +610,107 @@ export function registerWorkflowWebTools(pi: ExtensionAPI): void {
351
610
  }
352
611
  },
353
612
  } as ToolDefinition<typeof WorkflowWebFetchParams, WorkflowWebFetchDetails>);
613
+
614
+ pi.registerTool({
615
+ name: WORKFLOW_BROWSER_CHECK_TOOL,
616
+ label: "Workflow Browser Check",
617
+ description: "Launch a headless browser to verify a web app. Supports passive checks (console errors, DOM elements, localStorage) and interactive actions (click, type, wait, read text, read attributes, screenshots, page reload). Use for browser-level validation evidence when dev server or static HTTP server is running.",
618
+ promptSnippet: "Launch headless browser for passive checks and interactive actions at a URL",
619
+ promptGuidelines: [
620
+ "Use workflow_browser_check to gather browser-level validation evidence. For passive checks, pass selectors and localStorageKeys. For interactive testing, pass an actions array with click, type, waitForSelector, readText, readAttr, screenshot, reload, evaluate, wait, or select steps.",
621
+ "Start a dev server or python3 http.server first, then pass the URL to workflow_browser_check.",
622
+ "After browser checks complete, stop the server with workflow_stop_server({ port: PORT }) — do not rely on platform-specific shell commands.",
623
+ "Actions execute sequentially after page load. Each action returns ok/error and type-specific results (text, attrValue, result, found). Failed actions do not abort remaining actions.",
624
+ "Use readText to extract visible text from elements. Use readAttr to read attribute values like 'value', 'class', 'disabled'. Use evaluate for arbitrary JS like document.title or localStorage.getItem('key').",
625
+ "Set screenshot: true for initial page, or use a screenshot action step after interactions.",
626
+ ],
627
+ parameters: WorkflowBrowserCheckParams,
628
+ executionMode: "sequential",
629
+ async execute(_toolCallId, params, signal): Promise<AgentToolResult<WorkflowBrowserCheckDetails>> {
630
+ if (signal?.aborted) throw new Error("Browser check aborted.");
631
+ try {
632
+ const p = params as { url?: unknown; selectors?: unknown; localStorageKeys?: unknown; screenshot?: unknown; actions?: unknown };
633
+ const rawActions = Array.isArray(p.actions) ? p.actions : undefined;
634
+ const parsedActions: WorkflowBrowserAction[] | undefined = rawActions?.map((a: unknown) => {
635
+ const item = a as Record<string, unknown>;
636
+ return {
637
+ action: String(item.action ?? ""),
638
+ selector: typeof item.selector === "string" ? item.selector : undefined,
639
+ value: typeof item.value === "string" ? item.value : undefined,
640
+ timeout: typeof item.timeout === "number" ? item.timeout : undefined,
641
+ label: typeof item.label === "string" ? item.label : undefined,
642
+ } as WorkflowBrowserAction;
643
+ });
644
+ const details = await workflowBrowserCheck(
645
+ String(p.url ?? ""),
646
+ Array.isArray(p.selectors) ? p.selectors.map(String) : undefined,
647
+ Array.isArray(p.localStorageKeys) ? p.localStorageKeys.map(String) : undefined,
648
+ Boolean(p.screenshot),
649
+ parsedActions,
650
+ );
651
+ const lines: string[] = [
652
+ `URL: ${details.url}`,
653
+ `Title: ${details.title || "(none)"}`,
654
+ `Load time: ${details.loadTimeMs}ms`,
655
+ ];
656
+ if (details.error) {
657
+ lines.push(`Error: ${details.error}`);
658
+ } else {
659
+ lines.push(`Console messages: ${details.consoleMessages.length}`);
660
+ for (const msg of details.consoleMessages.slice(0, 20)) lines.push(` ${msg}`);
661
+ if (details.consoleMessages.length > 20) lines.push(` ... and ${details.consoleMessages.length - 20} more`);
662
+ lines.push(`Page errors: ${details.pageErrors.length}`);
663
+ for (const err of details.pageErrors) lines.push(` ${err}`);
664
+ if (details.elementCounts && Object.keys(details.elementCounts).length > 0) {
665
+ lines.push("Element counts:");
666
+ for (const [sel, count] of Object.entries(details.elementCounts)) lines.push(` ${sel}: ${count}`);
667
+ }
668
+ if (details.localStorageValues && Object.keys(details.localStorageValues).length > 0) {
669
+ lines.push("localStorage:");
670
+ for (const [key, val] of Object.entries(details.localStorageValues)) lines.push(` ${key}: ${val ?? "(null)"}`);
671
+ }
672
+ if (details.screenshotPath) lines.push(`Screenshot: ${details.screenshotPath}`);
673
+ if (details.actionResults && details.actionResults.length > 0) {
674
+ lines.push(`Actions: ${details.actionResults.length} step(s)`);
675
+ for (const r of details.actionResults) {
676
+ const label = r.label ? `[${r.label}] ` : "";
677
+ const parts = [` ${label}${r.action}: ${r.ok ? "OK" : "FAILED"}`];
678
+ if (r.error) parts.push(`| error: ${r.error}`);
679
+ if (r.text !== undefined) parts.push(`| text: "${r.text.slice(0, 200)}"`);
680
+ if (r.attrValue !== undefined) parts.push(`| attr: ${r.attrValue}`);
681
+ if (r.found !== undefined) parts.push(`| found: ${r.found}`);
682
+ if (r.result !== undefined) parts.push(`| result: ${typeof r.result === "string" ? r.result : JSON.stringify(r.result)}`);
683
+ lines.push(parts.join(" "));
684
+ }
685
+ }
686
+ }
687
+ return { content: [{ type: "text", text: lines.join("\n") }], details };
688
+ } catch (error) {
689
+ const message = error instanceof Error ? error.message : String(error);
690
+ return { content: [{ type: "text", text: `Workflow browser check failed: ${message}` }], details: { url: String((params as { url?: unknown }).url ?? ""), title: "", consoleMessages: [], pageErrors: [], elementCounts: {}, localStorageValues: {}, loadTimeMs: 0, error: message } };
691
+ }
692
+ },
693
+ } as ToolDefinition<typeof WorkflowBrowserCheckParams, WorkflowBrowserCheckDetails>);
694
+
695
+ pi.registerTool({
696
+ name: WORKFLOW_STOP_SERVER_TOOL,
697
+ label: "Workflow Stop Server",
698
+ description: "Kill any process listening on the given port. Cross-platform (macOS, Windows, Linux). Use to stop dev servers, static HTTP servers, or any background process started for validation.",
699
+ promptSnippet: "Stop a server process on a port",
700
+ promptGuidelines: [
701
+ "Use workflow_stop_server to reliably stop dev servers or static servers after validation instead of shell commands.",
702
+ "Pass the port number the server is listening on (e.g., 3017 for a server started on port 3017).",
703
+ "This works cross-platform and does not require fuser, lsof, or other platform-specific tools installed.",
704
+ ],
705
+ parameters: WorkflowStopServerParams,
706
+ executionMode: "parallel",
707
+ async execute(_toolCallId, params, signal): Promise<AgentToolResult<{ port: number; killed: boolean }>> {
708
+ if (signal?.aborted) throw new Error("Server stop aborted.");
709
+ const port = (params as { port: number }).port;
710
+ const killed = killProcessOnPort(port);
711
+ return { content: [{ type: "text", text: killed ? "Stopped process on port " + port + "." : "No process found on port " + port + " (already free or unable to kill)." }], details: { port, killed } };
712
+ },
713
+ } as ToolDefinition<typeof WorkflowStopServerParams, { port: number; killed: boolean }>);
354
714
  }
355
715
 
356
716
  export default function workflowWebToolsNoopExtension(): void {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediadatafusion/pi-workflow-suite",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Structured workflow orchestration suite for Pi with Standard, Plan, Mission, compaction, diagrams, web access, repo lock, and safety gates.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,6 +41,7 @@
41
41
  "agents/",
42
42
  "config/",
43
43
  "themes/",
44
+ "docs/assets/",
44
45
  "scripts/install-to-live.sh",
45
46
  "scripts/verify-live.sh",
46
47
  "scripts/audit-live.sh",
@@ -50,6 +51,8 @@
50
51
  "scripts/bootstrap-project.sh",
51
52
  "scripts/check-clean-release-tree.sh",
52
53
  "scripts/check-package-size.mjs",
54
+ "scripts/check-package-media.mjs",
55
+ "scripts/package-media-config.mjs",
53
56
  "scripts/prepare-package-readme.mjs",
54
57
  "scripts/build-package-export.mjs",
55
58
  "README.md",
@@ -78,8 +81,8 @@
78
81
  "themes": [
79
82
  "./themes"
80
83
  ],
81
- "image": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.6/docs/assets/pi-workflow-suite-header.png",
82
- "video": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.6/docs/assets/pi-workflow-suite-demo.mp4"
84
+ "image": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.12/docs/assets/pi-workflow-suite-header.png",
85
+ "video": "https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@0.0.12/docs/assets/pi-workflow-suite-demo.mp4"
83
86
  },
84
87
  "peerDependencies": {
85
88
  "@earendil-works/pi-agent-core": "*",
@@ -104,9 +107,10 @@
104
107
  "typescript": "^6.0.3"
105
108
  },
106
109
  "scripts": {
107
- "check:ts": "tsc --noEmit --noCheck",
110
+ "check:ts": "tsc --noEmit",
108
111
  "typecheck": "tsc --noEmit",
109
- "validate": "npm run check:ts && ./scripts/test-workflow-forced-subagent-regression.sh && ./scripts/test-agent-skill-boundary-regression.sh && ./scripts/test-startup-visual-mode-entry-regression.sh && ./scripts/test-settings-health-regression.sh && ./scripts/test-handoff-visibility-regression.sh && ./scripts/test-mission-milestone-handoff-regression.sh && ./scripts/test-plan-handoff-chain-regression.sh && ./scripts/test-plan-step-progress-regression.sh && ./scripts/test-standard-mode-regression.sh && ./scripts/test-final-handoff-summary-regression.sh && ./scripts/test-clarification-answer-handoff-regression.sh && ./scripts/test-validation-evidence-contract-regression.sh && ./scripts/test-mermaid-guidance-regression.sh && ./scripts/test-runtime-web-tools-regression.sh && ./scripts/test-repolock-scope-regression.sh && ./scripts/test-repo-lock-version-regression.sh && ./scripts/test-package-menu-surface.sh && npm run check:package-size && git diff --check",
112
+ "validate": "npm run check:ts && npm run check:package-media && npm run check:package-size && git diff --check",
113
+ "check:package-media": "node scripts/check-package-media.mjs",
110
114
  "check:package-size": "node scripts/check-package-size.mjs",
111
115
  "prepack": "node scripts/prepare-package-readme.mjs apply",
112
116
  "postpack": "node scripts/prepare-package-readme.mjs restore --pack",
@@ -28,7 +28,7 @@ while IFS= read -r rel; do
28
28
  printf 'MISSING: %s\n' "$rel"
29
29
  missing=1
30
30
  fi
31
- done < <(cd "$REPO_DIR" && find agents skills extensions config -type f ! -name '.DS_Store' ! -name '*.backup.*' ! -name '*.broken.*' | sort)
31
+ done < <(cd "$REPO_DIR" && find agents skills extensions config -type f ! -name '.DS_Store' ! -name '*.bak' ! -name '*.backup.*' ! -name '*.broken.*' | sort)
32
32
  if [[ "$missing" -eq 0 ]]; then
33
33
  printf 'OK: no missing canonical managed files\n'
34
34
  fi
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { spawnSync } from 'node:child_process';
8
+ import { packageMediaUrl, packageMediaUrls } from './package-media-config.mjs';
8
9
 
9
10
  const scriptDir = dirname(fileURLToPath(import.meta.url));
10
11
  const repoRoot = resolve(scriptDir, '..');
@@ -39,20 +40,14 @@ function parseArgs(argv) {
39
40
  return args;
40
41
  }
41
42
 
42
- const mediaVersion = '0.0.6';
43
-
44
- function mediaCdn(assetPath) {
45
- return `https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@${mediaVersion}/${assetPath}`;
46
- }
47
-
48
43
  function buildPackageReadme(sourceReadme, version) {
49
44
  const headerBlock = `# Pi Workflow Suite\n\n${
50
- `![Pi Workflow Suite — structured workflow orchestration for Pi](${mediaCdn('docs/assets/pi-workflow-suite-header.png')})`
45
+ `![Pi Workflow Suite — structured workflow orchestration for Pi](${packageMediaUrls.header})`
51
46
  }\n\n${[
52
- `[![Install](${mediaCdn('docs/assets/readme-link-install.svg')})](#installation)`,
53
- `[![Quick Start](${mediaCdn('docs/assets/readme-link-quick-start.svg')})](#quick-start)`,
54
- `[![Commands](${mediaCdn('docs/assets/readme-link-commands.svg')})](#core-commands)`,
55
- `[![Settings](${mediaCdn('docs/assets/readme-link-settings.svg')})](#settings-reference)`,
47
+ `[![Install](${packageMediaUrls.readmeInstall})](#installation)`,
48
+ `[![Quick Start](${packageMediaUrls.readmeQuickStart})](#quick-start)`,
49
+ `[![Commands](${packageMediaUrls.readmeCommands})](#core-commands)`,
50
+ `[![Settings](${packageMediaUrls.readmeSettings})](#settings-reference)`,
56
51
  ].join(' ')}\n\n**Workflow Suite Version:** `;
57
52
 
58
53
  let readme = sourceReadme.replace(
@@ -60,7 +55,7 @@ function buildPackageReadme(sourceReadme, version) {
60
55
  headerBlock,
61
56
  );
62
57
 
63
- const packageMediaBlock = `## Quick Demo\n\nSee Pi Workflow Suite in action: structured workflow modes, settings, runtime status, and guided execution inside Pi.\n\n[![Watch the Pi Workflow Suite quick demo](${mediaCdn('docs/assets/pi-workflow-suite-demo.gif')})](${mediaCdn('docs/assets/pi-workflow-suite-demo.mp4')})\n\n## Screenshots\n\n${[
58
+ const packageMediaBlock = `## Quick Demo\n\nSee Pi Workflow Suite in action: structured workflow modes, settings, runtime status, and guided execution inside Pi.\n\n[![Watch the Pi Workflow Suite quick demo](${packageMediaUrls.demoGif})](${packageMediaUrls.demoMp4})\n\n## Screenshots\n\n${[
64
59
  ['Pi Workflow Suite Mission Home with workflow graphs', 'docs/assets/screenshots/00-mission-home.png'],
65
60
  ['Pi Workflow Suite startup logo', 'docs/assets/screenshots/01-startup-Logo.png'],
66
61
  ['Workflow Suite theme settings', 'docs/assets/screenshots/02-theme-settings.png'],
@@ -68,7 +63,7 @@ function buildPackageReadme(sourceReadme, version) {
68
63
  ['Workflow Suite shared sub-agent settings', 'docs/assets/screenshots/04-SharedSubAgentsSettings.png'],
69
64
  ['Mission Mode milestone progress', 'docs/assets/screenshots/05-mission-mode.png'],
70
65
  ['Workflow Suite Mermaid diagram output', 'docs/assets/screenshots/06-diagram-mermaid.png'],
71
- ].map(([alt, path]) => `![${alt}](${mediaCdn(path)})`).join('\n\n')}\n\n`;
66
+ ].map(([alt, path]) => `![${alt}](${packageMediaUrl(path)})`).join('\n\n')}\n\n`;
72
67
 
73
68
  readme = readme.replace(/## Quick Demo[\s\S]*?## Contents\n/, `${packageMediaBlock}## Contents\n`);
74
69
  return readme;
@@ -11,6 +11,7 @@ report() {
11
11
  }
12
12
 
13
13
  if [[ -e AGENTS.md && -n "$(git ls-files AGENTS.md)" ]]; then report 'AGENTS.md must not be present on clean release main'; fi
14
+ if [[ -e CLAUDE.md && -n "$(git ls-files CLAUDE.md)" ]]; then report 'CLAUDE.md must not be present on clean release main'; fi
14
15
  if [[ -e .github && -n "$(git ls-files .github)" ]]; then report '.github/ must not be tracked on clean release main'; fi
15
16
  if [[ -e .factory && -n "$(git ls-files .factory)" ]]; then report '.factory/ must not be tracked on clean release main'; fi
16
17
  if [[ -e .kilo && -n "$(git ls-files .kilo)" ]]; then report '.kilo/ must not be tracked on clean release main'; fi
@@ -18,7 +19,7 @@ if [[ -e .cursor && -n "$(git ls-files .cursor)" ]]; then report '.cursor/ must
18
19
 
19
20
  while IFS= read -r path; do
20
21
  case "$path" in
21
- agents/*|config/*|extensions/*|skills/*|docs/assets/*|themes/*|scripts/install-to-live.sh|scripts/verify-live.sh|scripts/audit-live.sh|scripts/quarantine-live-junk.sh|scripts/backup-live.sh|scripts/audit-settings.sh|scripts/bootstrap-project.sh|scripts/check-clean-release-tree.sh|scripts/check-package-size.mjs|scripts/prepare-package-readme.mjs|scripts/build-package-export.mjs|README.md|CHANGELOG.md|CONTRIBUTING.md|LICENSE.md|NOTICE|SECURITY.md|SUPPORT.md|TRADEMARKS.md|VERSION|package.json|package-lock.json|tsconfig.json|.gitignore)
22
+ agents/*|config/*|extensions/*|skills/*|docs/assets/*|themes/*|scripts/install-to-live.sh|scripts/verify-live.sh|scripts/audit-live.sh|scripts/quarantine-live-junk.sh|scripts/backup-live.sh|scripts/audit-settings.sh|scripts/bootstrap-project.sh|scripts/check-clean-release-tree.sh|scripts/check-package-size.mjs|scripts/check-package-media.mjs|scripts/package-media-config.mjs|scripts/prepare-package-readme.mjs|scripts/build-package-export.mjs|README.md|CHANGELOG.md|CONTRIBUTING.md|LICENSE.md|NOTICE|SECURITY.md|SUPPORT.md|TRADEMARKS.md|VERSION|package.json|package-lock.json|tsconfig.json|.gitignore)
22
23
  ;;
23
24
  docs/*)
24
25
  report "non-asset docs file is not allowed: $path"
@@ -26,7 +27,7 @@ while IFS= read -r path; do
26
27
  scripts/test-*|scripts/sync-from-live.sh)
27
28
  report "internal script is not allowed: $path"
28
29
  ;;
29
- .github/*|AGENTS.md|.factory/*|.kilo/*|.cursor/*)
30
+ .github/*|AGENTS.md|CLAUDE.md|.factory/*|.kilo/*|.cursor/*)
30
31
  report "internal path is not allowed: $path"
31
32
  ;;
32
33
  *auth.json|*settings.json|*workflow-settings.json|*active.json|workflows/*|*sessions/*|*missions/*|*plans/*|*logs/*|*.env|*.env.*|*.DS_Store)
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { packageMediaBaseUrl, packageMediaUrls, packageMediaVersion } from './package-media-config.mjs';
6
+
7
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = resolve(scriptDir, '..');
9
+ const packageJson = JSON.parse(readFileSync(resolve(repoRoot, 'package.json'), 'utf8'));
10
+
11
+ const forbiddenNeedles = [
12
+ '@mediadatafusion/pi-workflow-suite@0.0.6/docs/assets',
13
+ 'raw.githubusercontent.com/MediaDataFusion/pi-workflow-suite/v0.0.12/docs/assets',
14
+ ];
15
+
16
+ const expectedPiManifest = {
17
+ extensions: ['./extensions/workflow-modes.ts', './extensions/subagent/index.ts'],
18
+ skills: ['./skills'],
19
+ prompts: ['./config/prompts', '!*.md'],
20
+ themes: ['./themes'],
21
+ };
22
+
23
+ const requiredPackageFiles = [
24
+ 'extensions/',
25
+ 'skills/',
26
+ 'config/',
27
+ 'themes/',
28
+ 'scripts/check-package-media.mjs',
29
+ 'scripts/package-media-config.mjs',
30
+ 'scripts/prepare-package-readme.mjs',
31
+ 'scripts/build-package-export.mjs',
32
+ 'README.md',
33
+ 'package-lock.json',
34
+ ];
35
+
36
+ function assert(condition, message) {
37
+ if (!condition) {
38
+ console.error(`ERROR: ${message}`);
39
+ process.exitCode = 1;
40
+ }
41
+ }
42
+
43
+ function assertArrayEquals(actual, expected, label) {
44
+ assert(Array.isArray(actual), `package.json ${label} must be an array`);
45
+ if (!Array.isArray(actual)) return;
46
+ const sameLength = actual.length === expected.length;
47
+ const sameEntries = expected.every((entry, index) => actual[index] === entry);
48
+ assert(sameLength && sameEntries, `package.json ${label} changed from expected manifest surface`);
49
+ }
50
+
51
+ assertArrayEquals(packageJson.pi?.extensions, expectedPiManifest.extensions, 'pi.extensions');
52
+ assertArrayEquals(packageJson.pi?.skills, expectedPiManifest.skills, 'pi.skills');
53
+ assertArrayEquals(packageJson.pi?.prompts, expectedPiManifest.prompts, 'pi.prompts');
54
+ assertArrayEquals(packageJson.pi?.themes, expectedPiManifest.themes, 'pi.themes');
55
+ assert(packageJson.pi?.image === packageMediaUrls.header, 'package.json pi.image does not match package media config');
56
+ assert(packageJson.pi?.video === packageMediaUrls.demoMp4, 'package.json pi.video does not match package media config');
57
+ assert(packageJson.pi?.video?.endsWith('.mp4'), 'package.json pi.video must be an MP4 URL');
58
+ assert(packageJson.pi?.image?.match(/\.(png|jpg|jpeg|webp|gif)$/), 'package.json pi.image must be an image URL');
59
+ assert(
60
+ packageMediaBaseUrl ===
61
+ `https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@${packageMediaVersion}/docs/assets`,
62
+ 'package media must use the established jsDelivr npm CDN media base',
63
+ );
64
+
65
+ for (const requiredFile of requiredPackageFiles) {
66
+ assert(packageJson.files?.includes(requiredFile), `package.json files must include ${requiredFile}`);
67
+ }
68
+ assert(!packageJson.files?.includes('docs/assets/'), 'package.json files must not include docs/assets/');
69
+
70
+ const packageJsonText = JSON.stringify(packageJson);
71
+ for (const needle of forbiddenNeedles) {
72
+ assert(!packageJsonText.includes(needle), `package.json still contains forbidden media URL ${needle}`);
73
+ }
74
+
75
+ assert(packageJsonText.includes(packageMediaBaseUrl), 'package.json does not contain the package media base URL');
76
+
77
+ if (process.exitCode) process.exit(process.exitCode);
78
+ console.log(`OK: package media points to ${packageMediaBaseUrl}`);
@@ -72,6 +72,7 @@ install_dir() {
72
72
  ! -name '.DS_Store' \
73
73
  ! -name '*.log' \
74
74
  ! -name '*.tmp' \
75
+ ! -name '*.bak' \
75
76
  ! -name '*.backup.*' \
76
77
  ! -name '*.broken.*' \
77
78
  -print0)
@@ -79,6 +80,7 @@ install_dir() {
79
80
 
80
81
  atomic_install_file "package.json"
81
82
  atomic_install_file "package-lock.json"
83
+ atomic_install_file "VERSION"
82
84
  install_dir "extensions"
83
85
  install_dir "agents"
84
86
  install_dir "skills"
@@ -0,0 +1,28 @@
1
+ export const packageMediaVersion = '0.0.12';
2
+
3
+ export const packageMediaBaseUrl =
4
+ `https://cdn.jsdelivr.net/npm/@mediadatafusion/pi-workflow-suite@${packageMediaVersion}/docs/assets`;
5
+
6
+ export function packageMediaUrl(assetPath) {
7
+ const normalizedPath = assetPath.replace(/^docs\/assets\//, '').replace(/^\/+/, '');
8
+ return `${packageMediaBaseUrl}/${normalizedPath}`;
9
+ }
10
+
11
+ export const packageMediaUrls = {
12
+ header: packageMediaUrl('pi-workflow-suite-header.png'),
13
+ demoGif: packageMediaUrl('pi-workflow-suite-demo.gif'),
14
+ demoMp4: packageMediaUrl('pi-workflow-suite-demo.mp4'),
15
+ readmeInstall: packageMediaUrl('readme-link-install.svg'),
16
+ readmeQuickStart: packageMediaUrl('readme-link-quick-start.svg'),
17
+ readmeCommands: packageMediaUrl('readme-link-commands.svg'),
18
+ readmeSettings: packageMediaUrl('readme-link-settings.svg'),
19
+ screenshots: {
20
+ missionHome: packageMediaUrl('screenshots/00-mission-home.png'),
21
+ startupLogo: packageMediaUrl('screenshots/01-startup-Logo.png'),
22
+ themeSettings: packageMediaUrl('screenshots/02-theme-settings.png'),
23
+ globalSafetySettings: packageMediaUrl('screenshots/03-GlobalSafetySettings.png'),
24
+ sharedSubAgentsSettings: packageMediaUrl('screenshots/04-SharedSubAgentsSettings.png'),
25
+ missionMode: packageMediaUrl('screenshots/05-mission-mode.png'),
26
+ diagramMermaid: packageMediaUrl('screenshots/06-diagram-mermaid.png'),
27
+ },
28
+ };