chore: publish from main

This commit is contained in:
github-actions[bot]
2026-08-05 03:59:30 +00:00
parent aa152fdb37
commit aeb70304ee
12 changed files with 1079 additions and 130 deletions
+2 -2
View File
@@ -1146,8 +1146,8 @@
{
"name": "signals-dashboard",
"source": "extensions/signals-dashboard",
"description": "Real-time agent coordination dashboard for The Workshop. Shows desk status, signal types (done, checkpoint, blocked, hands-up, partnership), intent text, outcome pairing with honesty gap, token usage, and stash/restore controls.",
"version": "0.1.0"
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
"version": "0.2.0"
},
{
"name": "site-studio",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "signals-dashboard",
"description": "Real-time agent coordination dashboard for The Workshop. Shows desk status, signal types (done, checkpoint, blocked, hands-up, partnership), intent text, outcome pairing with honesty gap, token usage, and stash/restore controls.",
"version": "0.1.0",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
"version": "0.2.0",
"author": {
"name": "jennyf19",
"url": "https://github.com/jennyf19"
@@ -6,13 +6,26 @@
import { createServer } from "node:http";
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
import { join, delimiter, sep } from "node:path";
import { join, delimiter, isAbsolute, sep } from "node:path";
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
const servers = new Map();
const STASH_TTL_MS = 48 * 60 * 60 * 1000;
const MCP_DISCOVERY_TTL_MS = 5 * 60 * 1000;
const MCP_DISCOVERY_MAX_BYTES = 1024 * 1024;
const DEFAULT_DESK_PROFILE = normalizeDeskProfile(process.env.WORKSHOP_DESK_PROFILE);
const mcpDiscoveryCache = new Map();
// Serialize stash read-modify-write per workshop. The UI fires stash/restore
// POSTs without awaiting each other, so two overlapping mutations could both
@@ -83,48 +96,217 @@ function trySpawn(cmd, args, opts = {}) {
// match, so auto-detection would pick the wrapper and the terminal would then
// fail to run it with no fallback.
function isExecutableFile(p) {
try {
if (!statSync(p).isFile()) return false;
if (process.platform !== "win32") accessSync(p, fsConstants.X_OK);
return true;
} catch { return false; }
return probeExecutableFile(p).ok;
}
function isOnPath(command) {
function probeExecutableFile(p) {
try {
if (!statSync(p).isFile()) return { ok: false, errorCode: null };
if (process.platform !== "win32") accessSync(p, fsConstants.X_OK);
return { ok: true, errorCode: null };
} catch (error) {
return { ok: false, errorCode: error?.code || null };
}
}
function resolveOnPath(command, { directOnly = false, excludedRoot = null } = {}) {
try {
const dirs = (process.env.PATH || "").split(delimiter);
const exts = process.platform === "win32"
? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean)
: [];
for (const dir of dirs) {
if (!dir) continue;
for (const rawDir of dirs) {
const dir = rawDir.replace(/^"(.*)"$/, "$1");
if (!dir || !isAbsolute(dir)) continue;
// On Windows only a PATHEXT match is runnable; on POSIX check the bare
// name, and isExecutableFile confirms the execute bit either way.
if (exts.length) {
for (const ext of exts) if (isExecutableFile(join(dir, command + ext))) return true;
} else if (isExecutableFile(join(dir, command))) {
return true;
for (const ext of exts) {
if (directOnly && ![".EXE", ".COM"].includes(ext.toUpperCase())) continue;
const candidate = join(dir, command + ext);
const probe = probeExecutableFile(candidate);
const appAlias = directOnly && probe.errorCode === "EACCES" &&
isWindowsAppExecutionAlias(candidate, process.env.LOCALAPPDATA);
if (!appAlias && !probe.ok) continue;
const resolved = appAlias ? candidate : realpathSync(candidate);
if (excludedRoot && isInsideRoot(excludedRoot, resolved)) continue;
return resolved;
}
} else {
const candidate = join(dir, command);
if (!isExecutableFile(candidate)) continue;
const resolved = realpathSync(candidate);
if (excludedRoot && isInsideRoot(excludedRoot, resolved)) continue;
return resolved;
}
}
} catch {}
return false;
return null;
}
// The agent argv a desk opens with. Default: prefer Agency (the internal
// wrapper around Copilot) when it's installed, so a desk comes up with its
// MCPs/plugin already configured instead of bare GHCP; otherwise vanilla
// Copilot. Agency can't take Copilot's --name (it clashes with Agency's own
// --resume), matching AgentClis. Override with WORKSHOP_DESK_AGENT=copilot to
// force vanilla, or =agency to insist on the wrapper.
function deskAgentArgv(deskName) {
function resolveDeskAgent(workshopDir) {
const pref = (process.env.WORKSHOP_DESK_AGENT || "").trim().toLowerCase();
// An explicit override is authoritative: =agency insists on the wrapper even
// when it isn't detected on PATH, and =copilot forces vanilla. Only when the
// override is unset do we auto-detect and prefer Agency if it's installed.
const useAgency = pref === "agency" ? true
: pref === "copilot" ? false
: isOnPath("agency");
return useAgency ? ["agency", "copilot"] : ["copilot", "--name", deskName];
const agencyCommand = resolveOnPath("agency", { excludedRoot: workshopDir });
const copilotCommand = resolveOnPath("copilot", { excludedRoot: workshopDir });
// Explicit overrides are authoritative and fail closed when unavailable.
if (pref === "agency") {
return agencyCommand
? { useAgency: true, agencyCommand, copilotCommand }
: null;
}
if (pref === "copilot") {
return copilotCommand
? { useAgency: false, agencyCommand, copilotCommand }
: null;
}
if (agencyCommand) return { useAgency: true, agencyCommand, copilotCommand };
if (copilotCommand) return { useAgency: false, agencyCommand, copilotCommand };
return null;
}
function resolveSystem32Executable(name) {
if (process.platform !== "win32") return null;
const root = process.env.SystemRoot || process.env.WINDIR;
if (!root || !isAbsolute(root)) return null;
try {
const candidate = join(root, "System32", name);
return isExecutableFile(candidate) ? realpathSync(candidate) : null;
} catch {
return null;
}
}
function terminateProcessTree(child) {
if (!child || child.exitCode !== null) return;
if (process.platform === "win32" && child.pid) {
const taskkill = resolveSystem32Executable("taskkill.exe");
if (!taskkill) return;
try {
const killer = spawn(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
windowsHide: true,
stdio: "ignore",
});
killer.unref();
} catch {}
return;
}
try {
if (child.pid) process.kill(-child.pid, "SIGKILL");
else child.kill("SIGKILL");
} catch {
try { child.kill("SIGKILL"); } catch {}
}
}
// Capture the underlying Copilot plugin inventory directly. Agency repo mode
// separately suppresses its own default/config plugins, so discovery does not
// need a wrapper process that can leave inherited pipes or descendants behind.
function capturePluginMcpJson(workshopDir, agent) {
return new Promise((resolve) => {
const command = agent.copilotCommand;
if (!command) {
resolve(null);
return;
}
const args = ["plugins", "list", "--kind", "mcp", "--scope", "plugin", "--json"];
const shim = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
if (shim && !isSafeWindowsCmdShim(command)) {
resolve(null);
return;
}
const spawnCommand = shim ? resolveSystem32Executable("cmd.exe") : command;
if (!spawnCommand) {
resolve(null);
return;
}
const spawnArgs = shim
? ["/d", "/s", "/c",
`"${[command, ...args].map(quoteWindowsCmdArgument).join(" ")}"`]
: args;
let settled = false;
let stdout = "";
const done = (value, child, timer, terminate = false) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (terminate) {
try { child.stdout.destroy(); } catch {}
terminateProcessTree(child);
}
resolve(value);
};
let child;
try {
child = spawn(spawnCommand, spawnArgs, {
cwd: workshopDir,
detached: process.platform !== "win32",
windowsHide: true,
windowsVerbatimArguments: shim,
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
resolve(null);
return;
}
const timer = setTimeout(() => done(null, child, timer, true), 30000);
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
if (settled) return;
stdout += chunk;
if (Buffer.byteLength(stdout, "utf8") > MCP_DISCOVERY_MAX_BYTES) {
done(null, child, timer, true);
return;
}
if (parsePluginMcpNames(stdout) !== null) done(stdout, child, timer, true);
});
child.on("error", () => done(null, child, timer));
child.on("close", (code) => done(code === 0 ? stdout : null, child, timer));
});
}
async function discoverPluginMcpNames(workshopDir, agent) {
const cacheKey = `${workshopDir}\0${agent.copilotCommand || "missing"}`;
const cached = mcpDiscoveryCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) return cached.value;
const output = await capturePluginMcpJson(workshopDir, agent);
const names = output === null ? null : parsePluginMcpNames(output);
const value = names === null
? { ok: false, names: [] }
: { ok: true, names };
mcpDiscoveryCache.set(cacheKey, {
expiresAt: Date.now() + MCP_DISCOVERY_TTL_MS,
value,
});
return value;
}
// Preserve the existing Agency-aware launch and layer the repo profile on top.
// Repo mode suppresses ambient plugin MCPs; connected mode keeps today's tool
// surface. Plugin discovery fails open, while Agency repo mode still suppresses
// Agency's own default MCPs.
async function deskAgentArgv(deskName, workshopDir, profile) {
const resolved = resolveDeskAgent(workshopDir);
if (!resolved) return null;
const { useAgency, agencyCommand, copilotCommand } = resolved;
if (useAgency && !agencyCommand) return null;
if (!useAgency && !copilotCommand) return null;
const discovery = profile === "repo"
? await discoverPluginMcpNames(workshopDir, resolved)
: { ok: true, names: [] };
return buildDeskAgentArgv({
deskName,
workshopDir,
useAgency,
agencyCommand,
copilotCommand,
profile,
pluginMcpNames: discovery.names,
discoverySucceeded: discovery.ok,
});
}
// A desk name flows onto a command line, and on the no-wt Windows fallback
@@ -172,7 +354,7 @@ function isInsideRoot(root, target) {
} catch { return false; }
}
async function launchDeskConsole(deskPath, deskName, workshopDir) {
async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) {
// deskName must be a plain slug so it is safe on every command line and shell
// below, and the resolved desk must still live inside the workshop root
// (which defeats a symlinked desk that escapes the repo). deskPath itself is
@@ -183,22 +365,30 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
if (!deskPath) return false;
if (!isSafeDeskNameForLaunch(deskName)) return false;
if (!isInsideRoot(workshopDir, deskPath)) return false;
const run = [...deskAgentArgv(deskName), "-i", deskOrientPrompt(deskName)];
const agent = await deskAgentArgv(deskName, workshopDir, profile);
if (!agent) return false;
const run = [...agent, "-i", deskOrientPrompt(deskName)];
if (process.platform === "win32") {
// Run the agent through cmd.exe (/k) so PATHEXT is applied: globally
// installed CLIs like `copilot`/`agency` are usually .cmd shims that
// Windows Terminal or a bare CreateProcess would fail to launch (they
// expect a literal executable, not a PATHEXT name). Windows Terminal is a
// GUI app, so it still surfaces its own window from the windowless host.
// Each element of run is its own argv token — deskName is a slug and the
// orientation prompt has no cmd metacharacters — and the desk path is
// passed via -d/cwd, so nothing untrusted is reparsed by a shell.
if (await trySpawn("wt.exe", ["-d", deskPath, "cmd", "/k", ...run])) return true;
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
const cmd = resolveSystem32Executable("cmd.exe");
const direct = /\.(exe|com)$/i.test(run[0]);
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true;
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every
// argument is free of cmd metacharacters; otherwise fail closed and let
// the UI copy the desk path rather than reparse an unsafe workshop path.
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg));
if (cmdSafe && wt && cmd &&
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true;
// Fallback when wt.exe is absent: a fresh console window via `start`,
// still through cmd /k for the same PATHEXT resolution.
return await trySpawn("cmd.exe", ["/c", "start", "", "cmd", "/k", ...run], { cwd: deskPath });
// still through cmd /k only when the arguments are safe for reparsing.
return cmdSafe && cmd
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath })
: false;
}
if (process.platform === "darwin") {
const osascript = "/usr/bin/osascript";
if (!isExecutableFile(osascript)) return false;
// macOS: `open` can't inject a command, so drive Terminal via AppleScript
// to cd into the desk and exec the agent. Each argv element is POSIX
// single-quoted so the shell can't reinterpret it, and osascript itself
@@ -209,7 +399,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
" activate\n" +
" do script " + osaStringLiteral(line) + "\n" +
"end tell";
return await trySpawn("osascript", ["-e", script]);
return await trySpawn(osascript, ["-e", script]);
}
// Linux/other: best-effort across common terminal emulators. Each is spawned
// via argv (no shell) with the agent command after the emulator's exec flag,
@@ -221,7 +411,8 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
["xterm", ["-e", ...run]],
];
for (const [term, args] of linuxTerms) {
if (await trySpawn(term, args, { cwd: deskPath })) return true;
const executable = resolveOnPath(term, { excludedRoot: workshopDir });
if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true;
}
return false;
}
@@ -612,11 +803,20 @@ function renderSignalCard(sig) {
const openBtnStyle = isEscalation
? "background:#7f1d1d;border:1px solid #dc2626;color:#fca5a5;padding:2px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;transition:all .15s;"
: "background:none;border:1px solid #1e3a5f;color:#7dd3fc;padding:2px 8px;border-radius:4px;font-size:11px;cursor:pointer;transition:all .15s;";
const openBtn = `<button data-act="open" data-desk="${esc(sig.deskName)}"
const openBtn = `<button data-act="open" data-profile="${esc(DEFAULT_DESK_PROFILE)}" data-desk="${esc(sig.deskName)}"
aria-label="Open ${esc(sig.deskName)} desk with ${esc(DEFAULT_DESK_PROFILE)} profile"
style="${openBtnStyle}"
onmouseover="this.style.background='#1e3a5f'"
onmouseout="this.style.background='${isEscalation ? '#7f1d1d' : 'transparent'}'"
title="Open this desk as a Copilot CLI session in its folder">open</button>`;
title="Open this desk with the ${esc(DEFAULT_DESK_PROFILE)} tool profile">open</button>`;
const connectedBtn = DEFAULT_DESK_PROFILE === "connected" ? "" : `
<button data-act="open" data-profile="connected" data-desk="${esc(sig.deskName)}"
aria-label="Open ${esc(sig.deskName)} desk with connected profile"
style="background:none;border:1px solid #262626;color:#94a3b8;padding:2px 7px;border-radius:4px;
font-size:10px;cursor:pointer;transition:all .15s;"
onmouseover="this.style.borderColor='#475569';this.style.color='#cbd5e1'"
onmouseout="this.style.borderColor='#262626';this.style.color='#94a3b8'"
title="Open with every configured MCP and tool">connected</button>`;
let escalationBlock = "";
if (isEscalation && sig.escalationReason) {
@@ -719,6 +919,7 @@ function renderSignalCard(sig) {
${(sig.tokensIn || sig.tokensOut) ? `<span style="font-size:10px;color:#334155;background:#0f172a;border:1px solid #1e293b;padding:1px 6px;border-radius:3px;" title="in: ${sig.tokensIn} · out: ${sig.tokensOut}${sig.model ? ' · ' + esc(sig.model) : ''}">🪙 ${formatTokens(sig.tokensIn + sig.tokensOut)}</span>` : ""}
<span style="font-size:11px;color:#475569;">${timeSince(sig.emittedAt)}${sig.signalCount ? ` · ${sig.signalCount}` : ""}</span>
${openBtn}
${connectedBtn}
${stashBtn}
</div>
</div>
@@ -850,14 +1051,16 @@ function renderDashboard(signals, stashed, capabilityToken) {
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
async function openDesk(name) {
const res = await fetch('/api/open/' + encodeURIComponent(name), POST_OPTS);
async function openDesk(name, profile) {
const selectedProfile = profile || ${JSON.stringify(DEFAULT_DESK_PROFILE)};
const res = await fetch('/api/open/' + encodeURIComponent(name) +
'?profile=' + encodeURIComponent(selectedProfile), POST_OPTS);
const data = await res.json();
if (data.ok) {
const path = data.deskPath || name;
if (data.launched) {
// A successful open shouldn't hijack the user's clipboard.
showToast('opening ' + name + ' desk…', path);
showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path);
} else {
// No terminal launched from here, so copy the path as the
// fallback handle, but only claim the copy when it actually
@@ -876,9 +1079,10 @@ function renderDashboard(signals, stashed, capabilityToken) {
const name = btn.getAttribute('data-desk');
if (!name) return;
const act = btn.getAttribute('data-act');
const profile = btn.getAttribute('data-profile');
if (act === 'stash') stashDesk(name);
else if (act === 'restore') restoreDesk(name);
else if (act === 'open') openDesk(name);
else if (act === 'open') openDesk(name, profile);
});
async function refresh() {
try {
@@ -894,15 +1098,22 @@ function renderDashboard(signals, stashed, capabilityToken) {
const active = document.activeElement;
let focusKey = null;
if (active && active.matches && active.matches('button[data-act]')) {
focusKey = active.getAttribute('data-act') + '|' + active.getAttribute('data-desk');
focusKey = JSON.stringify([
active.getAttribute('data-act'),
active.getAttribute('data-desk'),
active.getAttribute('data-profile') || '',
]);
}
content.innerHTML = newContent.innerHTML;
if (focusKey) {
const bar = focusKey.indexOf('|');
const act = focusKey.slice(0, bar);
const desk = focusKey.slice(bar + 1);
const [act, desk, profile] = JSON.parse(focusKey);
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
const target = content.querySelector('button[data-act="' + act + '"][data-desk="' + escDesk + '"]');
const profileSelector = profile
? '[data-profile="' + profile + '"]'
: ':not([data-profile])';
const target = content.querySelector(
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
profileSelector);
if (target) target.focus();
}
}
@@ -971,19 +1182,26 @@ async function startServer(instanceId, workshopDir) {
}
if (req.method === "POST" && url.pathname.startsWith("/api/open/")) {
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
if (!isValidDeskName(deskName)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid desk name" }));
return;
}
if (!isDeskProfile(profileInput)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid desk profile" }));
return;
}
const profile = normalizeDeskProfile(profileInput);
for (const subdir of ["desks", "classroom"]) {
const deskPath = join(workshopDir, subdir, deskName);
try {
const s = await stat(deskPath);
if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, deskName, workshopDir);
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched }));
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile }));
return;
}
} catch {}
@@ -1109,23 +1327,41 @@ const session = await joinSession({
},
{
name: "open_desk",
description: "Open a desk as an in-place Copilot CLI session: launches a terminal in the desk's folder (inside the workshop repo) running copilot, oriented to read the desk journal and continue. This is the Model A 'sit down at the desk' — no new worktree, no session spun off elsewhere. Returns the desk path and whether a terminal was launched.",
description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Returns the desk path, profile, and whether a terminal was launched.",
inputSchema: {
type: "object",
properties: { deskName: { type: "string", description: "Name of the desk to open" } },
properties: {
deskName: { type: "string", description: "Name of the desk to open" },
profile: {
type: "string",
enum: ["repo", "connected"],
description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`,
},
},
required: ["deskName"],
},
handler: async (ctx) => {
const entry = servers.get(ctx.instanceId);
if (!entry) return { error: "Dashboard not open" };
if (!isValidDeskName(ctx.input.deskName)) return { error: "Invalid desk name" };
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
const profile = normalizeDeskProfile(profileInput);
for (const subdir of ["desks", "classroom"]) {
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
try {
const s = await stat(deskPath);
if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, ctx.input.deskName, entry.workshopDir);
return { ok: true, deskName: ctx.input.deskName, deskPath, launched, workshopDir: entry.workshopDir };
const launched = await launchDeskConsole(
deskPath, ctx.input.deskName, entry.workshopDir, profile);
return {
ok: true,
deskName: ctx.input.deskName,
deskPath,
launched,
workshopDir: entry.workshopDir,
profile,
};
}
} catch {}
}
@@ -0,0 +1,88 @@
const PROFILES = new Set(["repo", "connected"]);
const SAFE_MCP_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export function isDeskProfile(value) {
return typeof value === "string" && PROFILES.has(value.toLowerCase());
}
export function normalizeDeskProfile(value, fallback = "repo") {
return isDeskProfile(value) ? value.toLowerCase() : fallback;
}
export function isWindowsAppExecutionAlias(candidate, localAppData) {
if (typeof candidate !== "string" || typeof localAppData !== "string") return false;
const normalized = candidate.replaceAll("/", "\\").toLowerCase();
const root = `${localAppData.replaceAll("/", "\\").replace(/\\+$/, "")}` +
"\\microsoft\\windowsapps\\";
return normalized.startsWith(root.toLowerCase()) && normalized.endsWith(".exe");
}
export function quoteWindowsCmdArgument(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value);
}
export function parsePluginMcpNames(text) {
let parsed;
try { parsed = JSON.parse(text); }
catch {
// Agency may prefix its pass-through command with human-readable startup
// lines. Its underlying Copilot JSON is the final object in stdout.
let start = text.lastIndexOf("{");
while (start >= 0) {
try {
parsed = JSON.parse(text.slice(start));
break;
} catch {
start = text.lastIndexOf("{", start - 1);
}
}
if (start < 0) return null;
}
if (!Array.isArray(parsed?.plugins)) return null;
const names = [];
const seen = new Set();
for (const entry of parsed.plugins) {
if (entry?.kind !== "mcp" || entry.enabled === false) continue;
if (entry.scope !== "plugin" && entry.source !== "plugin") continue;
if (typeof entry.name !== "string" || !SAFE_MCP_NAME.test(entry.name)) continue;
if (!seen.has(entry.name)) {
seen.add(entry.name);
names.push(entry.name);
}
}
return names;
}
export function buildDeskAgentArgv({
deskName,
workshopDir,
useAgency,
agencyCommand = "agency",
copilotCommand = "copilot",
profile = "repo",
pluginMcpNames = [],
discoverySucceeded = true,
}) {
const argv = useAgency
? [agencyCommand, "copilot"]
: [copilotCommand, "--name", deskName];
if (profile === "repo") {
if (useAgency) argv.push("--no-default-mcps");
}
if (profile === "repo" && discoverySucceeded) {
for (const name of pluginMcpNames) {
if (SAFE_MCP_NAME.test(name)) argv.push("--disable-mcp-server", name);
}
}
argv.push("--add-dir", workshopDir);
return argv;
}
@@ -0,0 +1,142 @@
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
test("normalizes supported profiles and defaults unknown values to repo", () => {
assert.equal(isDeskProfile("repo"), true);
assert.equal(isDeskProfile("CONNECTED"), true);
assert.equal(isDeskProfile("other"), false);
assert.equal(normalizeDeskProfile("CONNECTED"), "connected");
assert.equal(normalizeDeskProfile("other"), "repo");
});
test("recognizes Windows App Execution Alias paths without trusting repository executables", () => {
assert.equal(isWindowsAppExecutionAlias(
"C:\\Users\\person\\AppData\\Local\\Microsoft\\WindowsApps\\wt.exe",
"C:\\Users\\person\\AppData\\Local"), true);
assert.equal(isWindowsAppExecutionAlias(
"C:\\repo\\wt.exe",
"C:\\Users\\person\\AppData\\Local"), false);
assert.equal(isWindowsAppExecutionAlias(
"C:\\Users\\person\\AppData\\Local\\Microsoft\\WindowsApps\\wt.cmd",
"C:\\Users\\person\\AppData\\Local"), false);
});
test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () => {
assert.equal(
quoteWindowsCmdArgument("C:\\Program Files\\Agency\\agency.cmd"),
"\"C:\\Program Files\\Agency\\agency.cmd\"");
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
});
test("executes a Windows cmd shim with safe quoting", {
skip: process.platform !== "win32",
}, () => {
const root = mkdtempSync(join(tmpdir(), "workshop-profile-"));
const shimDir = join(root, "Shim Name");
mkdirSync(shimDir);
const shim = join(shimDir, "copilot.cmd");
writeFileSync(shim, "@echo off\r\necho {\"plugins\":[]}\r\n");
const cmd = join(process.env.SystemRoot, "System32", "cmd.exe");
const commandLine = `"${[shim, "plugins", "list"]
.map(quoteWindowsCmdArgument)
.join(" ")}"`;
const result = spawnSync(cmd, ["/d", "/s", "/c", commandLine], {
encoding: "utf8",
windowsHide: true,
windowsVerbatimArguments: true,
});
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\{"plugins":\[\]\}/);
});
test("extracts enabled plugin-scoped MCP names and rejects unsafe names", () => {
const names = parsePluginMcpNames(JSON.stringify({
plugins: [
{ kind: "mcp", name: "teams", scope: "plugin", enabled: true },
{ kind: "mcp", name: "repo-mcp", source: "plugin", enabled: true },
{ kind: "mcp", name: "teams", scope: "plugin", enabled: true },
{ kind: "mcp", name: "disabled", scope: "plugin", enabled: false },
{ kind: "mcp", name: "workspace", scope: "repository", enabled: true },
{ kind: "skill", name: "not-an-mcp", scope: "plugin", enabled: true },
{ kind: "mcp", name: "bad;name", scope: "plugin", enabled: true },
],
}));
assert.deepEqual(names, ["teams", "repo-mcp"]);
assert.deepEqual(parsePluginMcpNames(`Agency startup\n${JSON.stringify({
plugins: [{ kind: "mcp", name: "ado", scope: "plugin", enabled: true }],
})}`), ["ado"]);
assert.equal(parsePluginMcpNames("not json"), null);
assert.equal(parsePluginMcpNames("{}"), null);
});
test("builds an Agency repo profile on top of the existing wrapper", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "C:\\workshop",
useAgency: true,
agencyCommand: "C:\\tools\\agency.exe",
profile: "repo",
pluginMcpNames: ["teams", "ado"],
}), [
"C:\\tools\\agency.exe", "copilot", "--no-default-mcps",
"--disable-mcp-server", "teams",
"--disable-mcp-server", "ado",
"--add-dir", "C:\\workshop",
]);
});
test("builds a plain Copilot repo profile without Agency-only flags", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "/usr/local/bin/copilot",
profile: "repo",
pluginMcpNames: ["calendar"],
}), [
"/usr/local/bin/copilot", "--name", "cost-desk",
"--disable-mcp-server", "calendar",
"--add-dir", "/workshop",
]);
});
test("connected preserves tools while Agency discovery failure still removes defaults", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: true,
profile: "connected",
pluginMcpNames: ["teams"],
}), [
"agency", "copilot", "--add-dir", "/workshop",
]);
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: true,
profile: "repo",
pluginMcpNames: [],
discoverySucceeded: false,
}), [
"agency", "copilot", "--no-default-mcps", "--add-dir", "/workshop",
]);
});
@@ -1,12 +1,15 @@
{
"name": "signals-dashboard",
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"main": "extension.mjs",
"scripts": {
"test": "node --test launch-profile.test.mjs"
},
"dependencies": {
"@github/copilot-sdk": "latest"
},
"description": "Real-time agent coordination dashboard for The Workshop. Shows desk status, signal types (done, checkpoint, blocked, hands-up, partnership), intent text, outcome pairing with honesty gap, token usage, and stash/restore controls.",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
"keywords": [
"agent-signals",
"dashboard",
+13 -2
View File
@@ -37,8 +37,19 @@ A **desk** isn't a sub-agent — it's a peer with a history. Sub-agents inherit
The Workshop's live view is a **canvas extension** (🪨 Cairn) — `signals-dashboard` — that shows the pulse of every desk (score bars, patterns, escalations), auto-refreshing in the GitHub Copilot app.
Each desk card also has an **open** button that launches a Copilot CLI right in
that desk's folder, so you can sit down at a desk straight from the board.
Each desk card has two launch choices:
- **open** — the cost-aware repo profile. The desk keeps access to the Workshop
root for cross-desk journals and artifacts while ambient plugin MCPs are
suppressed.
- **connected** — the full configured tool surface for work that needs external
systems such as issue trackers, mail, or service APIs.
Set `WORKSHOP_DESK_PROFILE=connected` to make the main **open** button use the
full tool surface; in that mode the separate **connected** button is omitted.
Both launch a CLI in the desk's folder. If Agency is installed, Cairn keeps
using the existing Agency wrapper; otherwise it launches plain Copilot CLI.
It ships as a separate extension. Install it alongside the plugin to get the live canvas:
@@ -1,7 +1,7 @@
{
"name": "signals-dashboard",
"description": "Real-time agent coordination dashboard for The Workshop. Shows desk status, signal types (done, checkpoint, blocked, hands-up, partnership), intent text, outcome pairing with honesty gap, token usage, and stash/restore controls.",
"version": "0.1.0",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
"version": "0.2.0",
"author": {
"name": "jennyf19",
"url": "https://github.com/jennyf19"
@@ -6,13 +6,26 @@
import { createServer } from "node:http";
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
import { join, delimiter, sep } from "node:path";
import { join, delimiter, isAbsolute, sep } from "node:path";
import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
const servers = new Map();
const STASH_TTL_MS = 48 * 60 * 60 * 1000;
const MCP_DISCOVERY_TTL_MS = 5 * 60 * 1000;
const MCP_DISCOVERY_MAX_BYTES = 1024 * 1024;
const DEFAULT_DESK_PROFILE = normalizeDeskProfile(process.env.WORKSHOP_DESK_PROFILE);
const mcpDiscoveryCache = new Map();
// Serialize stash read-modify-write per workshop. The UI fires stash/restore
// POSTs without awaiting each other, so two overlapping mutations could both
@@ -83,48 +96,217 @@ function trySpawn(cmd, args, opts = {}) {
// match, so auto-detection would pick the wrapper and the terminal would then
// fail to run it with no fallback.
function isExecutableFile(p) {
try {
if (!statSync(p).isFile()) return false;
if (process.platform !== "win32") accessSync(p, fsConstants.X_OK);
return true;
} catch { return false; }
return probeExecutableFile(p).ok;
}
function isOnPath(command) {
function probeExecutableFile(p) {
try {
if (!statSync(p).isFile()) return { ok: false, errorCode: null };
if (process.platform !== "win32") accessSync(p, fsConstants.X_OK);
return { ok: true, errorCode: null };
} catch (error) {
return { ok: false, errorCode: error?.code || null };
}
}
function resolveOnPath(command, { directOnly = false, excludedRoot = null } = {}) {
try {
const dirs = (process.env.PATH || "").split(delimiter);
const exts = process.platform === "win32"
? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean)
: [];
for (const dir of dirs) {
if (!dir) continue;
for (const rawDir of dirs) {
const dir = rawDir.replace(/^"(.*)"$/, "$1");
if (!dir || !isAbsolute(dir)) continue;
// On Windows only a PATHEXT match is runnable; on POSIX check the bare
// name, and isExecutableFile confirms the execute bit either way.
if (exts.length) {
for (const ext of exts) if (isExecutableFile(join(dir, command + ext))) return true;
} else if (isExecutableFile(join(dir, command))) {
return true;
for (const ext of exts) {
if (directOnly && ![".EXE", ".COM"].includes(ext.toUpperCase())) continue;
const candidate = join(dir, command + ext);
const probe = probeExecutableFile(candidate);
const appAlias = directOnly && probe.errorCode === "EACCES" &&
isWindowsAppExecutionAlias(candidate, process.env.LOCALAPPDATA);
if (!appAlias && !probe.ok) continue;
const resolved = appAlias ? candidate : realpathSync(candidate);
if (excludedRoot && isInsideRoot(excludedRoot, resolved)) continue;
return resolved;
}
} else {
const candidate = join(dir, command);
if (!isExecutableFile(candidate)) continue;
const resolved = realpathSync(candidate);
if (excludedRoot && isInsideRoot(excludedRoot, resolved)) continue;
return resolved;
}
}
} catch {}
return false;
return null;
}
// The agent argv a desk opens with. Default: prefer Agency (the internal
// wrapper around Copilot) when it's installed, so a desk comes up with its
// MCPs/plugin already configured instead of bare GHCP; otherwise vanilla
// Copilot. Agency can't take Copilot's --name (it clashes with Agency's own
// --resume), matching AgentClis. Override with WORKSHOP_DESK_AGENT=copilot to
// force vanilla, or =agency to insist on the wrapper.
function deskAgentArgv(deskName) {
function resolveDeskAgent(workshopDir) {
const pref = (process.env.WORKSHOP_DESK_AGENT || "").trim().toLowerCase();
// An explicit override is authoritative: =agency insists on the wrapper even
// when it isn't detected on PATH, and =copilot forces vanilla. Only when the
// override is unset do we auto-detect and prefer Agency if it's installed.
const useAgency = pref === "agency" ? true
: pref === "copilot" ? false
: isOnPath("agency");
return useAgency ? ["agency", "copilot"] : ["copilot", "--name", deskName];
const agencyCommand = resolveOnPath("agency", { excludedRoot: workshopDir });
const copilotCommand = resolveOnPath("copilot", { excludedRoot: workshopDir });
// Explicit overrides are authoritative and fail closed when unavailable.
if (pref === "agency") {
return agencyCommand
? { useAgency: true, agencyCommand, copilotCommand }
: null;
}
if (pref === "copilot") {
return copilotCommand
? { useAgency: false, agencyCommand, copilotCommand }
: null;
}
if (agencyCommand) return { useAgency: true, agencyCommand, copilotCommand };
if (copilotCommand) return { useAgency: false, agencyCommand, copilotCommand };
return null;
}
function resolveSystem32Executable(name) {
if (process.platform !== "win32") return null;
const root = process.env.SystemRoot || process.env.WINDIR;
if (!root || !isAbsolute(root)) return null;
try {
const candidate = join(root, "System32", name);
return isExecutableFile(candidate) ? realpathSync(candidate) : null;
} catch {
return null;
}
}
function terminateProcessTree(child) {
if (!child || child.exitCode !== null) return;
if (process.platform === "win32" && child.pid) {
const taskkill = resolveSystem32Executable("taskkill.exe");
if (!taskkill) return;
try {
const killer = spawn(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
windowsHide: true,
stdio: "ignore",
});
killer.unref();
} catch {}
return;
}
try {
if (child.pid) process.kill(-child.pid, "SIGKILL");
else child.kill("SIGKILL");
} catch {
try { child.kill("SIGKILL"); } catch {}
}
}
// Capture the underlying Copilot plugin inventory directly. Agency repo mode
// separately suppresses its own default/config plugins, so discovery does not
// need a wrapper process that can leave inherited pipes or descendants behind.
function capturePluginMcpJson(workshopDir, agent) {
return new Promise((resolve) => {
const command = agent.copilotCommand;
if (!command) {
resolve(null);
return;
}
const args = ["plugins", "list", "--kind", "mcp", "--scope", "plugin", "--json"];
const shim = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
if (shim && !isSafeWindowsCmdShim(command)) {
resolve(null);
return;
}
const spawnCommand = shim ? resolveSystem32Executable("cmd.exe") : command;
if (!spawnCommand) {
resolve(null);
return;
}
const spawnArgs = shim
? ["/d", "/s", "/c",
`"${[command, ...args].map(quoteWindowsCmdArgument).join(" ")}"`]
: args;
let settled = false;
let stdout = "";
const done = (value, child, timer, terminate = false) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (terminate) {
try { child.stdout.destroy(); } catch {}
terminateProcessTree(child);
}
resolve(value);
};
let child;
try {
child = spawn(spawnCommand, spawnArgs, {
cwd: workshopDir,
detached: process.platform !== "win32",
windowsHide: true,
windowsVerbatimArguments: shim,
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
resolve(null);
return;
}
const timer = setTimeout(() => done(null, child, timer, true), 30000);
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
if (settled) return;
stdout += chunk;
if (Buffer.byteLength(stdout, "utf8") > MCP_DISCOVERY_MAX_BYTES) {
done(null, child, timer, true);
return;
}
if (parsePluginMcpNames(stdout) !== null) done(stdout, child, timer, true);
});
child.on("error", () => done(null, child, timer));
child.on("close", (code) => done(code === 0 ? stdout : null, child, timer));
});
}
async function discoverPluginMcpNames(workshopDir, agent) {
const cacheKey = `${workshopDir}\0${agent.copilotCommand || "missing"}`;
const cached = mcpDiscoveryCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) return cached.value;
const output = await capturePluginMcpJson(workshopDir, agent);
const names = output === null ? null : parsePluginMcpNames(output);
const value = names === null
? { ok: false, names: [] }
: { ok: true, names };
mcpDiscoveryCache.set(cacheKey, {
expiresAt: Date.now() + MCP_DISCOVERY_TTL_MS,
value,
});
return value;
}
// Preserve the existing Agency-aware launch and layer the repo profile on top.
// Repo mode suppresses ambient plugin MCPs; connected mode keeps today's tool
// surface. Plugin discovery fails open, while Agency repo mode still suppresses
// Agency's own default MCPs.
async function deskAgentArgv(deskName, workshopDir, profile) {
const resolved = resolveDeskAgent(workshopDir);
if (!resolved) return null;
const { useAgency, agencyCommand, copilotCommand } = resolved;
if (useAgency && !agencyCommand) return null;
if (!useAgency && !copilotCommand) return null;
const discovery = profile === "repo"
? await discoverPluginMcpNames(workshopDir, resolved)
: { ok: true, names: [] };
return buildDeskAgentArgv({
deskName,
workshopDir,
useAgency,
agencyCommand,
copilotCommand,
profile,
pluginMcpNames: discovery.names,
discoverySucceeded: discovery.ok,
});
}
// A desk name flows onto a command line, and on the no-wt Windows fallback
@@ -172,7 +354,7 @@ function isInsideRoot(root, target) {
} catch { return false; }
}
async function launchDeskConsole(deskPath, deskName, workshopDir) {
async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) {
// deskName must be a plain slug so it is safe on every command line and shell
// below, and the resolved desk must still live inside the workshop root
// (which defeats a symlinked desk that escapes the repo). deskPath itself is
@@ -183,22 +365,30 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
if (!deskPath) return false;
if (!isSafeDeskNameForLaunch(deskName)) return false;
if (!isInsideRoot(workshopDir, deskPath)) return false;
const run = [...deskAgentArgv(deskName), "-i", deskOrientPrompt(deskName)];
const agent = await deskAgentArgv(deskName, workshopDir, profile);
if (!agent) return false;
const run = [...agent, "-i", deskOrientPrompt(deskName)];
if (process.platform === "win32") {
// Run the agent through cmd.exe (/k) so PATHEXT is applied: globally
// installed CLIs like `copilot`/`agency` are usually .cmd shims that
// Windows Terminal or a bare CreateProcess would fail to launch (they
// expect a literal executable, not a PATHEXT name). Windows Terminal is a
// GUI app, so it still surfaces its own window from the windowless host.
// Each element of run is its own argv token — deskName is a slug and the
// orientation prompt has no cmd metacharacters — and the desk path is
// passed via -d/cwd, so nothing untrusted is reparsed by a shell.
if (await trySpawn("wt.exe", ["-d", deskPath, "cmd", "/k", ...run])) return true;
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
const cmd = resolveSystem32Executable("cmd.exe");
const direct = /\.(exe|com)$/i.test(run[0]);
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true;
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every
// argument is free of cmd metacharacters; otherwise fail closed and let
// the UI copy the desk path rather than reparse an unsafe workshop path.
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg));
if (cmdSafe && wt && cmd &&
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true;
// Fallback when wt.exe is absent: a fresh console window via `start`,
// still through cmd /k for the same PATHEXT resolution.
return await trySpawn("cmd.exe", ["/c", "start", "", "cmd", "/k", ...run], { cwd: deskPath });
// still through cmd /k only when the arguments are safe for reparsing.
return cmdSafe && cmd
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath })
: false;
}
if (process.platform === "darwin") {
const osascript = "/usr/bin/osascript";
if (!isExecutableFile(osascript)) return false;
// macOS: `open` can't inject a command, so drive Terminal via AppleScript
// to cd into the desk and exec the agent. Each argv element is POSIX
// single-quoted so the shell can't reinterpret it, and osascript itself
@@ -209,7 +399,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
" activate\n" +
" do script " + osaStringLiteral(line) + "\n" +
"end tell";
return await trySpawn("osascript", ["-e", script]);
return await trySpawn(osascript, ["-e", script]);
}
// Linux/other: best-effort across common terminal emulators. Each is spawned
// via argv (no shell) with the agent command after the emulator's exec flag,
@@ -221,7 +411,8 @@ async function launchDeskConsole(deskPath, deskName, workshopDir) {
["xterm", ["-e", ...run]],
];
for (const [term, args] of linuxTerms) {
if (await trySpawn(term, args, { cwd: deskPath })) return true;
const executable = resolveOnPath(term, { excludedRoot: workshopDir });
if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true;
}
return false;
}
@@ -612,11 +803,20 @@ function renderSignalCard(sig) {
const openBtnStyle = isEscalation
? "background:#7f1d1d;border:1px solid #dc2626;color:#fca5a5;padding:2px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;transition:all .15s;"
: "background:none;border:1px solid #1e3a5f;color:#7dd3fc;padding:2px 8px;border-radius:4px;font-size:11px;cursor:pointer;transition:all .15s;";
const openBtn = `<button data-act="open" data-desk="${esc(sig.deskName)}"
const openBtn = `<button data-act="open" data-profile="${esc(DEFAULT_DESK_PROFILE)}" data-desk="${esc(sig.deskName)}"
aria-label="Open ${esc(sig.deskName)} desk with ${esc(DEFAULT_DESK_PROFILE)} profile"
style="${openBtnStyle}"
onmouseover="this.style.background='#1e3a5f'"
onmouseout="this.style.background='${isEscalation ? '#7f1d1d' : 'transparent'}'"
title="Open this desk as a Copilot CLI session in its folder">open</button>`;
title="Open this desk with the ${esc(DEFAULT_DESK_PROFILE)} tool profile">open</button>`;
const connectedBtn = DEFAULT_DESK_PROFILE === "connected" ? "" : `
<button data-act="open" data-profile="connected" data-desk="${esc(sig.deskName)}"
aria-label="Open ${esc(sig.deskName)} desk with connected profile"
style="background:none;border:1px solid #262626;color:#94a3b8;padding:2px 7px;border-radius:4px;
font-size:10px;cursor:pointer;transition:all .15s;"
onmouseover="this.style.borderColor='#475569';this.style.color='#cbd5e1'"
onmouseout="this.style.borderColor='#262626';this.style.color='#94a3b8'"
title="Open with every configured MCP and tool">connected</button>`;
let escalationBlock = "";
if (isEscalation && sig.escalationReason) {
@@ -719,6 +919,7 @@ function renderSignalCard(sig) {
${(sig.tokensIn || sig.tokensOut) ? `<span style="font-size:10px;color:#334155;background:#0f172a;border:1px solid #1e293b;padding:1px 6px;border-radius:3px;" title="in: ${sig.tokensIn} · out: ${sig.tokensOut}${sig.model ? ' · ' + esc(sig.model) : ''}">🪙 ${formatTokens(sig.tokensIn + sig.tokensOut)}</span>` : ""}
<span style="font-size:11px;color:#475569;">${timeSince(sig.emittedAt)}${sig.signalCount ? ` · ${sig.signalCount}` : ""}</span>
${openBtn}
${connectedBtn}
${stashBtn}
</div>
</div>
@@ -850,14 +1051,16 @@ function renderDashboard(signals, stashed, capabilityToken) {
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
async function openDesk(name) {
const res = await fetch('/api/open/' + encodeURIComponent(name), POST_OPTS);
async function openDesk(name, profile) {
const selectedProfile = profile || ${JSON.stringify(DEFAULT_DESK_PROFILE)};
const res = await fetch('/api/open/' + encodeURIComponent(name) +
'?profile=' + encodeURIComponent(selectedProfile), POST_OPTS);
const data = await res.json();
if (data.ok) {
const path = data.deskPath || name;
if (data.launched) {
// A successful open shouldn't hijack the user's clipboard.
showToast('opening ' + name + ' desk…', path);
showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path);
} else {
// No terminal launched from here, so copy the path as the
// fallback handle, but only claim the copy when it actually
@@ -876,9 +1079,10 @@ function renderDashboard(signals, stashed, capabilityToken) {
const name = btn.getAttribute('data-desk');
if (!name) return;
const act = btn.getAttribute('data-act');
const profile = btn.getAttribute('data-profile');
if (act === 'stash') stashDesk(name);
else if (act === 'restore') restoreDesk(name);
else if (act === 'open') openDesk(name);
else if (act === 'open') openDesk(name, profile);
});
async function refresh() {
try {
@@ -894,15 +1098,22 @@ function renderDashboard(signals, stashed, capabilityToken) {
const active = document.activeElement;
let focusKey = null;
if (active && active.matches && active.matches('button[data-act]')) {
focusKey = active.getAttribute('data-act') + '|' + active.getAttribute('data-desk');
focusKey = JSON.stringify([
active.getAttribute('data-act'),
active.getAttribute('data-desk'),
active.getAttribute('data-profile') || '',
]);
}
content.innerHTML = newContent.innerHTML;
if (focusKey) {
const bar = focusKey.indexOf('|');
const act = focusKey.slice(0, bar);
const desk = focusKey.slice(bar + 1);
const [act, desk, profile] = JSON.parse(focusKey);
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
const target = content.querySelector('button[data-act="' + act + '"][data-desk="' + escDesk + '"]');
const profileSelector = profile
? '[data-profile="' + profile + '"]'
: ':not([data-profile])';
const target = content.querySelector(
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
profileSelector);
if (target) target.focus();
}
}
@@ -971,19 +1182,26 @@ async function startServer(instanceId, workshopDir) {
}
if (req.method === "POST" && url.pathname.startsWith("/api/open/")) {
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
if (!isValidDeskName(deskName)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid desk name" }));
return;
}
if (!isDeskProfile(profileInput)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid desk profile" }));
return;
}
const profile = normalizeDeskProfile(profileInput);
for (const subdir of ["desks", "classroom"]) {
const deskPath = join(workshopDir, subdir, deskName);
try {
const s = await stat(deskPath);
if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, deskName, workshopDir);
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched }));
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile }));
return;
}
} catch {}
@@ -1109,23 +1327,41 @@ const session = await joinSession({
},
{
name: "open_desk",
description: "Open a desk as an in-place Copilot CLI session: launches a terminal in the desk's folder (inside the workshop repo) running copilot, oriented to read the desk journal and continue. This is the Model A 'sit down at the desk' — no new worktree, no session spun off elsewhere. Returns the desk path and whether a terminal was launched.",
description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Returns the desk path, profile, and whether a terminal was launched.",
inputSchema: {
type: "object",
properties: { deskName: { type: "string", description: "Name of the desk to open" } },
properties: {
deskName: { type: "string", description: "Name of the desk to open" },
profile: {
type: "string",
enum: ["repo", "connected"],
description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`,
},
},
required: ["deskName"],
},
handler: async (ctx) => {
const entry = servers.get(ctx.instanceId);
if (!entry) return { error: "Dashboard not open" };
if (!isValidDeskName(ctx.input.deskName)) return { error: "Invalid desk name" };
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
const profile = normalizeDeskProfile(profileInput);
for (const subdir of ["desks", "classroom"]) {
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
try {
const s = await stat(deskPath);
if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, ctx.input.deskName, entry.workshopDir);
return { ok: true, deskName: ctx.input.deskName, deskPath, launched, workshopDir: entry.workshopDir };
const launched = await launchDeskConsole(
deskPath, ctx.input.deskName, entry.workshopDir, profile);
return {
ok: true,
deskName: ctx.input.deskName,
deskPath,
launched,
workshopDir: entry.workshopDir,
profile,
};
}
} catch {}
}
@@ -0,0 +1,88 @@
const PROFILES = new Set(["repo", "connected"]);
const SAFE_MCP_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
export function isDeskProfile(value) {
return typeof value === "string" && PROFILES.has(value.toLowerCase());
}
export function normalizeDeskProfile(value, fallback = "repo") {
return isDeskProfile(value) ? value.toLowerCase() : fallback;
}
export function isWindowsAppExecutionAlias(candidate, localAppData) {
if (typeof candidate !== "string" || typeof localAppData !== "string") return false;
const normalized = candidate.replaceAll("/", "\\").toLowerCase();
const root = `${localAppData.replaceAll("/", "\\").replace(/\\+$/, "")}` +
"\\microsoft\\windowsapps\\";
return normalized.startsWith(root.toLowerCase()) && normalized.endsWith(".exe");
}
export function quoteWindowsCmdArgument(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value);
}
export function parsePluginMcpNames(text) {
let parsed;
try { parsed = JSON.parse(text); }
catch {
// Agency may prefix its pass-through command with human-readable startup
// lines. Its underlying Copilot JSON is the final object in stdout.
let start = text.lastIndexOf("{");
while (start >= 0) {
try {
parsed = JSON.parse(text.slice(start));
break;
} catch {
start = text.lastIndexOf("{", start - 1);
}
}
if (start < 0) return null;
}
if (!Array.isArray(parsed?.plugins)) return null;
const names = [];
const seen = new Set();
for (const entry of parsed.plugins) {
if (entry?.kind !== "mcp" || entry.enabled === false) continue;
if (entry.scope !== "plugin" && entry.source !== "plugin") continue;
if (typeof entry.name !== "string" || !SAFE_MCP_NAME.test(entry.name)) continue;
if (!seen.has(entry.name)) {
seen.add(entry.name);
names.push(entry.name);
}
}
return names;
}
export function buildDeskAgentArgv({
deskName,
workshopDir,
useAgency,
agencyCommand = "agency",
copilotCommand = "copilot",
profile = "repo",
pluginMcpNames = [],
discoverySucceeded = true,
}) {
const argv = useAgency
? [agencyCommand, "copilot"]
: [copilotCommand, "--name", deskName];
if (profile === "repo") {
if (useAgency) argv.push("--no-default-mcps");
}
if (profile === "repo" && discoverySucceeded) {
for (const name of pluginMcpNames) {
if (SAFE_MCP_NAME.test(name)) argv.push("--disable-mcp-server", name);
}
}
argv.push("--add-dir", workshopDir);
return argv;
}
@@ -0,0 +1,142 @@
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
buildDeskAgentArgv,
isDeskProfile,
isSafeWindowsCmdShim,
isWindowsAppExecutionAlias,
normalizeDeskProfile,
parsePluginMcpNames,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
test("normalizes supported profiles and defaults unknown values to repo", () => {
assert.equal(isDeskProfile("repo"), true);
assert.equal(isDeskProfile("CONNECTED"), true);
assert.equal(isDeskProfile("other"), false);
assert.equal(normalizeDeskProfile("CONNECTED"), "connected");
assert.equal(normalizeDeskProfile("other"), "repo");
});
test("recognizes Windows App Execution Alias paths without trusting repository executables", () => {
assert.equal(isWindowsAppExecutionAlias(
"C:\\Users\\person\\AppData\\Local\\Microsoft\\WindowsApps\\wt.exe",
"C:\\Users\\person\\AppData\\Local"), true);
assert.equal(isWindowsAppExecutionAlias(
"C:\\repo\\wt.exe",
"C:\\Users\\person\\AppData\\Local"), false);
assert.equal(isWindowsAppExecutionAlias(
"C:\\Users\\person\\AppData\\Local\\Microsoft\\WindowsApps\\wt.cmd",
"C:\\Users\\person\\AppData\\Local"), false);
});
test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () => {
assert.equal(
quoteWindowsCmdArgument("C:\\Program Files\\Agency\\agency.cmd"),
"\"C:\\Program Files\\Agency\\agency.cmd\"");
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
});
test("executes a Windows cmd shim with safe quoting", {
skip: process.platform !== "win32",
}, () => {
const root = mkdtempSync(join(tmpdir(), "workshop-profile-"));
const shimDir = join(root, "Shim Name");
mkdirSync(shimDir);
const shim = join(shimDir, "copilot.cmd");
writeFileSync(shim, "@echo off\r\necho {\"plugins\":[]}\r\n");
const cmd = join(process.env.SystemRoot, "System32", "cmd.exe");
const commandLine = `"${[shim, "plugins", "list"]
.map(quoteWindowsCmdArgument)
.join(" ")}"`;
const result = spawnSync(cmd, ["/d", "/s", "/c", commandLine], {
encoding: "utf8",
windowsHide: true,
windowsVerbatimArguments: true,
});
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\{"plugins":\[\]\}/);
});
test("extracts enabled plugin-scoped MCP names and rejects unsafe names", () => {
const names = parsePluginMcpNames(JSON.stringify({
plugins: [
{ kind: "mcp", name: "teams", scope: "plugin", enabled: true },
{ kind: "mcp", name: "repo-mcp", source: "plugin", enabled: true },
{ kind: "mcp", name: "teams", scope: "plugin", enabled: true },
{ kind: "mcp", name: "disabled", scope: "plugin", enabled: false },
{ kind: "mcp", name: "workspace", scope: "repository", enabled: true },
{ kind: "skill", name: "not-an-mcp", scope: "plugin", enabled: true },
{ kind: "mcp", name: "bad;name", scope: "plugin", enabled: true },
],
}));
assert.deepEqual(names, ["teams", "repo-mcp"]);
assert.deepEqual(parsePluginMcpNames(`Agency startup\n${JSON.stringify({
plugins: [{ kind: "mcp", name: "ado", scope: "plugin", enabled: true }],
})}`), ["ado"]);
assert.equal(parsePluginMcpNames("not json"), null);
assert.equal(parsePluginMcpNames("{}"), null);
});
test("builds an Agency repo profile on top of the existing wrapper", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "C:\\workshop",
useAgency: true,
agencyCommand: "C:\\tools\\agency.exe",
profile: "repo",
pluginMcpNames: ["teams", "ado"],
}), [
"C:\\tools\\agency.exe", "copilot", "--no-default-mcps",
"--disable-mcp-server", "teams",
"--disable-mcp-server", "ado",
"--add-dir", "C:\\workshop",
]);
});
test("builds a plain Copilot repo profile without Agency-only flags", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "/usr/local/bin/copilot",
profile: "repo",
pluginMcpNames: ["calendar"],
}), [
"/usr/local/bin/copilot", "--name", "cost-desk",
"--disable-mcp-server", "calendar",
"--add-dir", "/workshop",
]);
});
test("connected preserves tools while Agency discovery failure still removes defaults", () => {
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: true,
profile: "connected",
pluginMcpNames: ["teams"],
}), [
"agency", "copilot", "--add-dir", "/workshop",
]);
assert.deepEqual(buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: true,
profile: "repo",
pluginMcpNames: [],
discoverySucceeded: false,
}), [
"agency", "copilot", "--no-default-mcps", "--add-dir", "/workshop",
]);
});
@@ -1,12 +1,15 @@
{
"name": "signals-dashboard",
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"main": "extension.mjs",
"scripts": {
"test": "node --test launch-profile.test.mjs"
},
"dependencies": {
"@github/copilot-sdk": "latest"
},
"description": "Real-time agent coordination dashboard for The Workshop. Shows desk status, signal types (done, checkpoint, blocked, hands-up, partnership), intent text, outcome pairing with honesty gap, token usage, and stash/restore controls.",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
"keywords": [
"agent-signals",
"dashboard",