chore: publish from main

This commit is contained in:
github-actions[bot]
2026-08-17 05:43:12 +00:00
parent 4b887e09a2
commit 3e8da0a7c0
26 changed files with 3329 additions and 159 deletions
+2 -2
View File
@@ -1282,8 +1282,8 @@
{ {
"name": "signals-dashboard", "name": "signals-dashboard",
"source": "plugins/signals-dashboard", "source": "plugins/signals-dashboard",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
"version": "0.2.0" "version": "0.3.0"
}, },
{ {
"name": "site-studio", "name": "site-studio",
+1 -1
View File
@@ -102,7 +102,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t
| [rust-mcp-development](../plugins/rust-mcp-development/README.md) | Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations. | 0 items | rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp | | [rust-mcp-development](../plugins/rust-mcp-development/README.md) | Build high-performance Model Context Protocol servers in Rust using the official rmcp SDK with async/await, procedural macros, and type-safe implementations. | 0 items | rust, mcp, model-context-protocol, server-development, sdk, tokio, async, macros, rmcp |
| [salesforce-development](../plugins/salesforce-development/README.md) | Complete Salesforce agentic development environment covering Apex & Triggers, Flow automation, Lightning Web Components, Aura components, and Visualforce pages. | 0 items | salesforce, apex, triggers, lwc, aura, flow, visualforce, crm, salesforce-dx | | [salesforce-development](../plugins/salesforce-development/README.md) | Complete Salesforce agentic development environment covering Apex & Triggers, Flow automation, Lightning Web Components, Aura components, and Visualforce pages. | 0 items | salesforce, apex, triggers, lwc, aura, flow, visualforce, crm, salesforce-dx |
| [security-best-practices](../plugins/security-best-practices/README.md) | Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications. | 0 items | security, accessibility, performance, code-quality, owasp, a11y, optimization, best-practices | | [security-best-practices](../plugins/security-best-practices/README.md) | Security frameworks, accessibility guidelines, performance optimization, and code quality best practices for building secure, maintainable, and high-performance applications. | 0 items | security, accessibility, performance, code-quality, owasp, a11y, optimization, best-practices |
| [signals-dashboard](../plugins/signals-dashboard/README.md) | Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles. | 1 items | agent-signals, dashboard, multi-agent, coordination, canvas | | [signals-dashboard](../plugins/signals-dashboard/README.md) | Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation. | 1 items | agent-signals, dashboard, multi-agent, coordination, canvas, local-delegation |
| [site-studio](../plugins/site-studio/README.md) | Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change. | 1 items | agent-collaboration, content-authoring, personal-website, progress-tracking, section-planning, site-builder | | [site-studio](../plugins/site-studio/README.md) | Plan, draft, and track a personal website section by section — a shared canvas where you and your agent author content, watch progress, and review every change. | 1 items | agent-collaboration, content-authoring, personal-website, progress-tracking, section-planning, site-builder |
| [skill-image-gen](../plugins/skill-image-gen/README.md) | Generate images using AI directly from your coding workflow. Supports OpenAI (gpt-image-2) and Google Gemini. BYO API key — the skill guides you through setup on first use. | 0 items | image-generation, openai, gemini, ai, art, sprites, textures, icons | | [skill-image-gen](../plugins/skill-image-gen/README.md) | Generate images using AI directly from your coding workflow. Supports OpenAI (gpt-image-2) and Google Gemini. BYO API key — the skill guides you through setup on first use. | 0 items | image-generation, openai, gemini, ai, art, sprites, textures, icons |
| [software-engineering-team](../plugins/software-engineering-team/README.md) | 7 specialized agents covering the full software development lifecycle from UX design and architecture to security and DevOps. | 0 items | team, enterprise, security, devops, ux, architecture, product, ai-ethics | | [software-engineering-team](../plugins/software-engineering-team/README.md) | 7 specialized agents covering the full software development lifecycle from UX design and architecture to security and DevOps. | 0 items | team, enterprise, security, devops, ux, architecture, product, ai-ethics |
+145
View File
@@ -0,0 +1,145 @@
# 🪨 Cairn — Signals Dashboard
A live GitHub Copilot CLI **canvas extension** that shows the pulse of every
desk in your Workshop. It reads the agent signals your desks emit and renders
them as a compact, always-current dashboard in a side panel — so you can direct
the work instead of polling each desk by hand.
It replaces the old Blazor **WorkshopRoom** dashboard (`src/WorkshopRoom/`) with
a native canvas that runs inside GHCP, with no separate web app to launch.
## What it is
Each desk in the Workshop leaves signals behind — small stones on the trail —
by writing JSON files into its `.signals/` folder. Cairn scans those folders,
picks the most recent signal per desk, and renders:
- **Score bars** for the desk's self-assessment (intent, confidence, accuracy,
completeness).
- **Patterns** the desk reported: what worked ✓, what was hard △, and skill
gaps ✗.
- **Escalations** — desks that raised their hand, with what they're blocked on
and their recommendation, pinned to the top and pulsing red.
## How to open it
The dashboard is registered as the **🪨 Cairn** canvas (`signals-dashboard`).
Ask Copilot to open it and pass your workshop root as `workshopDir`:
> Open the 🪨 Cairn canvas with `workshopDir` set to the folder that contains
> my `desks/` directory.
`workshopDir` must be the **absolute path to the workshop root** — the folder
that holds `desks/` (and optionally `classroom/`). If omitted, it falls back to
the current working directory.
## Features
- **Signal scanning** — walks `desks/*/.signals/` and `classroom/*/.signals/`,
reading the newest `*.json` per desk (mirrors `SignalReader.cs`).
- **Score bars** — color-coded intent / confidence / accuracy / completeness,
scored out of 5.
- **Escalation alerts** — escalation signals sort to the very top, render with a
pulsing red border, and surface the blocker + recommendation.
- **Active desks first** — sorted escalations → recent signals → desks with no
signal yet, then by recency.
- **Stash / restore** — pause a workstream by stashing its desk. Stashed desks
drop off the active view and auto-expire after a **48-hour TTL**; restore any
time before then. Stash state lives in `.desk-stash.json` at the workshop
root.
- **Auto-refresh** — the panel refreshes every 5 seconds using a background
fetch (no full page reload), so scores and escalations stay current smoothly.
- **Summary bar** — desk count, how many are reporting vs. awaiting, an
escalation badge, and average scores across the room.
- **Cost-aware desk launch** — **open** starts a repo-profile desk that keeps the
verified Workshop root available while suppressing ambient plugin MCPs.
**connected** preserves every configured MCP for work that needs external
systems. Agency remains the preferred wrapper when installed.
- **Local Delegation** — orthogonal off/on control. When available and on, the
frontier desk may use the installed [`local-agent-delegation`](https://github.com/jennyf19/sealed-delegation)
skill for bounded, independently gated read/evidence work. Fail-closed: no
skill or no qualified route receipt means the toggle cannot take effect, and
no local-savings credit is awarded.
## Agent actions
The canvas also exposes actions Copilot can invoke directly:
- `refresh` — force a rescan and return current signal data as JSON.
- `stash` — stash a desk by `deskName`.
- `restore` — restore a stashed desk by `deskName`.
- `open_desk` — open a desk with optional `profile: "repo" | "connected"` and
optional `localDelegation: "off" | "on"`.
## Desk launch profiles
`repo` is the default. At launch, Cairn asks Copilot for the enabled
plugin-scoped MCP inventory and disables those ambient servers for the topic
desk. User-, workspace-, organization-, and built-in resources are left alone.
If discovery fails, Copilot plugin MCP suppression fails open; Agency repo mode
still omits Agency's own default MCPs.
When Agency is installed, Cairn keeps the existing `agency copilot` launch and
adds Agency's `--no-default-mcps` in repo mode. Outside Agency, the same profile
is applied directly to Copilot CLI.
Both profiles pass `--add-dir <workshop-root>` so a desk can intentionally read
another desk's journal or artifact without receiving access outside the room.
Set `WORKSHOP_DESK_PROFILE=connected` to retain the historical default for the
main **open** button. The separate **connected** button is always available when
repo mode is the default.
## Local Delegation
Local Delegation is **not** a third desk profile and does not replace the frontier
model. It is a separate permission bit:
```text
repo / connected = which MCPs and tools the frontier desk can see
Local Delegation off/on = whether the frontier desk may invoke a bounded local worker
```
Availability is fail-closed. Cairn enables the lane only when:
1. the `local-agent-delegation` skill is installed, and
2. a qualified route is declared via `WORKSHOP_LOCAL_DELEGATION_ROUTE_ID` or a
receipt at `~/.copilot/local-agent-runs/qualified-route.json`
(`status: "qualified"`, safe `route_id`).
Operator preference is stored **user-locally** under
`~/.copilot/workshop-local-delegation/` (keyed by the canonical workshop path),
never in the cloned workshop — a repo cannot ship `preference: on`.
When the preference is on but availability fails, opens still launch as frontier
desks and surface the reason — they never silently fall back with savings credit.
When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched
process, shows an open toast/badge (`Local Delegation effective · route …`), and
may append one short ASCII line to `-i` (`Local Delegation env is enabled.`) when
that combined prompt stays quote-free and under the length guard. Full policy
still lives in the env flag plus the installed `local-agent-delegation` skill —
never a long multi-sentence `-i` appendix (Windows Terminal reparse).
The runtime, launcher, and gates remain owned by
[Sealed Delegation](https://github.com/jennyf19/sealed-delegation).
## Signal shape
Cairn reads the agent-signals protocol used across the Workshop:
```json
{
"signal_type": "execution",
"agent_name": "desk-name",
"self_assessment": { "intent": 5, "confidence": 4, "accuracy": 4, "completeness": 3 },
"patterns": { "what_worked": "...", "what_was_hard": "...", "skill_gap": "..." },
"escalation": { "reason": "...", "blocked_on": "...", "recommendation": "..." }
}
```
`escalation` is only present on `signal_type: "escalation"` signals.
## Replaces the Blazor WorkshopRoom
This canvas supersedes the standalone Blazor dashboard in `src/WorkshopRoom/`.
The data is the truth and the UI is just a view — Cairn renders the same signal
data natively inside GHCP, so there's no separate server to run.
+238 -47
View File
@@ -5,20 +5,33 @@
import { createServer } from "node:http"; import { createServer } from "node:http";
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs"; import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
import { readdir, readFile, writeFile, stat } from "node:fs/promises"; import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
import { join, delimiter, isAbsolute, sep } from "node:path"; import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
parsePluginMcpNames, parsePluginMcpNames,
quoteWindowsCmdArgument, quoteWindowsCmdArgument,
} from "./launch-profile.mjs"; } from "./launch-profile.mjs";
import {
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
formatLocalDelegationOpenNotice,
localDelegationPreferencePath,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
const servers = new Map(); const servers = new Map();
const STASH_TTL_MS = 48 * 60 * 60 * 1000; const STASH_TTL_MS = 48 * 60 * 60 * 1000;
@@ -60,11 +73,10 @@ function isValidDeskName(name) {
// path is then only ever passed as a spawn cwd, an argv element, or a // path is then only ever passed as a spawn cwd, an argv element, or a
// single-quoted literal inside the macOS Terminal command — never concatenated // single-quoted literal inside the macOS Terminal command — never concatenated
// raw onto a command line — so no character filtering of the path is required. // raw onto a command line — so no character filtering of the path is required.
function deskOrientPrompt(deskName) { //
return `You are sitting down at the ${deskName} desk in this workshop. ` + // Local Delegation is orthogonal to repo/connected: it never changes the tool
`Read journal.md in this folder first to pick up where the last session ` + // surface. When effective, only the orientation prompt and child env mark that
`left off, then continue the desk's work. Write your journal before you stop.`; // the frontier desk may use the installed local-agent-delegation skill.
}
// Spawn detached and resolve true only once the OS confirms the process // Spawn detached and resolve true only once the OS confirms the process
// started ('spawn'), false on failure ('error', e.g. the binary is missing) so // started ('spawn'), false on failure ('error', e.g. the binary is missing) so
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
} catch { return false; } } catch { return false; }
} }
async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) { function preferenceStatePath(workshopDir) {
return localDelegationPreferencePath(workshopDir, {
resolvePath: (p) => {
try { return realpathSync(p); } catch { return p; }
},
});
}
async function readLocalDelegationPreference(workshopDir) {
// Never read preference from the workshop repo — a clone can ship
// preference:on. Only user-local state (or explicit env) counts.
try {
const raw = JSON.parse(await readFile(preferenceStatePath(workshopDir), "utf8"));
return parseLocalDelegationState(raw).preference;
} catch {
return normalizeLocalDelegationPreference(
process.env.WORKSHOP_LOCAL_DELEGATION_PREFERENCE, "off");
}
}
async function writeLocalDelegationPreference(workshopDir, preference) {
const state = serializeLocalDelegationState({ preference });
const target = preferenceStatePath(workshopDir);
await mkdir(dirname(target), { recursive: true });
// Atomic replace in the user-local dir (temp + rename).
const tmp = join(
dirname(target),
`.pref.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
const body = JSON.stringify(state, null, 2) + "\n";
try {
await writeFile(tmp, body, { encoding: "utf8", flag: "wx" });
try {
await rename(tmp, target);
} catch {
await unlink(target).catch(() => {});
await rename(tmp, target);
}
} catch (err) {
await unlink(tmp).catch(() => {});
throw err;
}
return state;
}
function currentLocalDelegationLaunch(preference) {
const availability = resolveLocalDelegationAvailability();
return resolveLocalDelegationLaunch({ preference, availability });
}
async function launchDeskConsole(
deskPath,
deskName,
workshopDir,
profile = DEFAULT_DESK_PROFILE,
localDelegation = { effective: false },
) {
// deskName must be a plain slug so it is safe on every command line and shell // 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 // below, and the resolved desk must still live inside the workshop root
// (which defeats a symlinked desk that escapes the repo). deskPath itself is // (which defeats a symlinked desk that escapes the repo). deskPath itself is
@@ -367,24 +434,27 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
if (!isInsideRoot(workshopDir, deskPath)) return false; if (!isInsideRoot(workshopDir, deskPath)) return false;
const agent = await deskAgentArgv(deskName, workshopDir, profile); const agent = await deskAgentArgv(deskName, workshopDir, profile);
if (!agent) return false; if (!agent) return false;
const run = [...agent, "-i", deskOrientPrompt(deskName)]; const effective = Boolean(localDelegation?.effective);
const run = [...agent, "-i", deskOrientPrompt(deskName, { localDelegationEffective: effective })];
const env = buildLocalDelegationLaunchEnv(process.env, { localDelegationEffective: effective });
if (process.platform === "win32") { if (process.platform === "win32") {
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir }); const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
const cmd = resolveSystem32Executable("cmd.exe"); const cmd = resolveSystem32Executable("cmd.exe");
const direct = /\.(exe|com)$/i.test(run[0]); // wt.exe does not reliably forward Node's spawn env into a new tab when
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true; // Windows Terminal is already running. Always start through cmd.exe and
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every // Args are quoteWindowsCmdArgument'd, so only block expanders that still
// argument is free of cmd metacharacters; otherwise fail closed and let // fire inside quotes (% and !) — allow parentheses in workshop paths.
// the UI copy the desk path rather than reparse an unsafe workshop path. const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)); if (!cmdSafe || !cmd) return false;
if (cmdSafe && wt && cmd && const inner = windowsLocalDelegationCmdPrefix(effective)
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true; + run.map(quoteWindowsCmdArgument).join(" ");
// Fallback when wt.exe is absent: a fresh console window via `start`, if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
// still through cmd /k only when the arguments are safe for reparsing. return true;
return cmdSafe && cmd }
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath }) // Fallback when wt.exe is absent: a fresh console window via `start`.
: false; return await trySpawn(
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
} }
if (process.platform === "darwin") { if (process.platform === "darwin") {
const osascript = "/usr/bin/osascript"; const osascript = "/usr/bin/osascript";
@@ -393,13 +463,18 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
// to cd into the desk and exec the agent. Each argv element is POSIX // 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 // single-quoted so the shell can't reinterpret it, and osascript itself
// is spawned via argv (no shell). // is spawned via argv (no shell).
const line = "cd " + shSingleQuote(deskPath) + " && exec " + // Local-delegation env is exported in-line so the Terminal session sees it
// without inheriting a polluted parent shell forever.
const envPrefix = effective
? "export WORKSHOP_LOCAL_DELEGATION=enabled; "
: "unset WORKSHOP_LOCAL_DELEGATION; ";
const line = "cd " + shSingleQuote(deskPath) + " && " + envPrefix + "exec " +
run.map(shSingleQuote).join(" "); run.map(shSingleQuote).join(" ");
const script = 'tell application "Terminal"\n' + const script = 'tell application "Terminal"\n' +
" activate\n" + " activate\n" +
" do script " + osaStringLiteral(line) + "\n" + " do script " + osaStringLiteral(line) + "\n" +
"end tell"; "end tell";
return await trySpawn(osascript, ["-e", script]); return await trySpawn(osascript, ["-e", script], { env });
} }
// Linux/other: best-effort across common terminal emulators. Each is spawned // 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, // via argv (no shell) with the agent command after the emulator's exec flag,
@@ -412,7 +487,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
]; ];
for (const [term, args] of linuxTerms) { for (const [term, args] of linuxTerms) {
const executable = resolveOnPath(term, { excludedRoot: workshopDir }); const executable = resolveOnPath(term, { excludedRoot: workshopDir });
if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true; if (executable && await trySpawn(executable, args, { cwd: deskPath, env })) return true;
} }
return false; return false;
} }
@@ -728,7 +803,42 @@ function avgScore(signals) {
return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") }; return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") };
} }
function renderSummaryBar(activeSignals) { function renderLocalDelegationControl(localDelegation) {
const pref = localDelegation?.preference || "off";
const available = Boolean(localDelegation?.availability?.available);
const effective = Boolean(localDelegation?.effective);
const reason = localDelegation?.availability?.reason || "Local Delegation unavailable";
const routeId = localDelegation?.availability?.routeId || null;
const next = pref === "on" ? "off" : "on";
const label = effective ? "On" : (pref === "on" ? "On*" : "Off");
const color = effective ? "#86efac" : (pref === "on" ? "#fbbf24" : "#94a3b8");
const border = effective ? "#166534" : (pref === "on" ? "#854d0e" : "#334155");
const title = available
? (effective
? `Local Delegation effective${routeId ? ` · route ${routeId}` : ""}`
: "Local Delegation available but currently off")
: reason;
const note = effective && routeId
? `<span style="font-size:10px;color:#86efac;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(title)}">effective · ${esc(truncate(routeId, 28))}</span>`
: !available
? `<span style="font-size:10px;color:#64748b;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(reason)}">${esc(truncate(reason, 48))}</span>`
: (pref === "on" && !effective
? `<span style="font-size:10px;color:#fbbf24;">requested, unavailable</span>`
: "");
return `
<div style="display:flex;align-items:center;gap:6px;" title="${esc(title)}">
<span style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.04em;">Local</span>
<button data-act="local-delegation" data-preference="${esc(next)}"
aria-label="Local Delegation ${esc(label)}${effective && routeId ? ` route ${esc(routeId)}` : ""}"
aria-pressed="${pref === "on" ? "true" : "false"}"
style="background:#020617;border:1px solid ${border};color:${color};padding:2px 8px;border-radius:999px;
font-size:11px;cursor:pointer;font-weight:600;min-width:42px;"
title="${esc(title)}">${esc(label)}</button>
${note}
</div>`;
}
function renderSummaryBar(activeSignals, localDelegation) {
const escalations = activeSignals.filter(s => s.signalType === "escalation").length; const escalations = activeSignals.filter(s => s.signalType === "escalation").length;
const withSignals = activeSignals.filter(s => s.signalType !== "none").length; const withSignals = activeSignals.filter(s => s.signalType !== "none").length;
const awaiting = activeSignals.filter(s => s.signalType === "none").length; const awaiting = activeSignals.filter(s => s.signalType === "none").length;
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
return ` return `
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px; <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;
background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;"> background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;gap:12px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span> <span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span>
<span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span> <span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
${tokenBadge} ${tokenBadge}
${calibrationBadge} ${calibrationBadge}
${escBadge} ${escBadge}
${renderLocalDelegationControl(localDelegation)}
</div> </div>
${avgBlock} ${avgBlock}
</div>`; </div>`;
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
</div>`; </div>`;
} }
function renderDashboard(signals, stashed, capabilityToken) { function renderDashboard(signals, stashed, capabilityToken, localDelegation) {
const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName))); const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName)));
const localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
const cards = activeSignals.length > 0 const cards = activeSignals.length > 0
? activeSignals.map(renderSignalCard).join("") ? activeSignals.map(renderSignalCard).join("")
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
</div> </div>
</div>`; </div>`;
const summaryBar = activeSignals.length > 0 ? renderSummaryBar(activeSignals) : ""; // Always show the Local Delegation control so operators can see availability
// even before the first desk signal arrives.
const summaryBar = renderSummaryBar(activeSignals, localDelegationState);
const stashedSection = stashed.length > 0 ? ` const stashedSection = stashed.length > 0 ? `
<div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;"> <div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;">
@@ -1058,27 +1172,55 @@ function renderDashboard(signals, stashed, capabilityToken) {
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
const path = data.deskPath || name; const path = data.deskPath || name;
const notice = data.localDelegationNotice || {};
const localTitle = notice.titleSuffix || '';
const localDetail = notice.detail || '';
if (data.launched) { if (data.launched) {
// A successful open shouldn't hijack the user's clipboard. // A successful open shouldn't hijack the user's clipboard.
showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path); // Surface LD state in the toast — operators cannot rely on -i alone.
showToast('opening ' + name + ' desk (' + selectedProfile + localTitle + ')…',
localDetail || path);
} else { } else {
// No terminal launched from here, so copy the path as the // No terminal launched from here, so copy the path as the
// fallback handle, but only claim the copy when it actually // fallback handle, but only claim the copy when it actually
// succeeded. The path shows in the toast either way. // succeeded. The path shows in the toast either way.
let copied = false; let copied = false;
try { await navigator.clipboard.writeText(path); copied = true; } catch {} try { await navigator.clipboard.writeText(path); copied = true; } catch {}
showToast(copied ? (name + ' · path copied') : (name + ' · copy this path'), path); const copyTitle = copied ? (name + ' · path copied') : (name + ' · copy this path');
showToast(copyTitle + localTitle, localDetail || path);
} }
} else { } else {
showToast(name + ' · not found', ''); showToast(name + ' · not found', '');
} }
} }
async function setLocalDelegation(preference) {
const res = await fetch('/api/local-delegation?preference=' +
encodeURIComponent(preference || 'off'), POST_OPTS);
const data = await res.json();
if (data.ok) {
const ld = data.localDelegation || {};
const routeId = ld.availability && ld.availability.routeId;
const label = ld.effective
? ('Local Delegation effective' + (routeId ? (' · route ' + routeId) : ''))
: (ld.preference === 'on'
? 'Local Delegation requested (unavailable)'
: 'Local Delegation off');
showToast(label, ld.availability?.reason || '');
refresh();
} else {
showToast('Local Delegation · not updated', data.error || '');
}
}
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-act]'); const btn = e.target.closest('button[data-act]');
if (!btn) return; if (!btn) return;
const act = btn.getAttribute('data-act');
if (act === 'local-delegation') {
setLocalDelegation(btn.getAttribute('data-preference') || 'off');
return;
}
const name = btn.getAttribute('data-desk'); const name = btn.getAttribute('data-desk');
if (!name) return; if (!name) return;
const act = btn.getAttribute('data-act');
const profile = btn.getAttribute('data-profile'); const profile = btn.getAttribute('data-profile');
if (act === 'stash') stashDesk(name); if (act === 'stash') stashDesk(name);
else if (act === 'restore') restoreDesk(name); else if (act === 'restore') restoreDesk(name);
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
active.getAttribute('data-act'), active.getAttribute('data-act'),
active.getAttribute('data-desk'), active.getAttribute('data-desk'),
active.getAttribute('data-profile') || '', active.getAttribute('data-profile') || '',
active.getAttribute('data-preference') || '',
]); ]);
} }
content.innerHTML = newContent.innerHTML; content.innerHTML = newContent.innerHTML;
if (focusKey) { if (focusKey) {
const [act, desk, profile] = JSON.parse(focusKey); const [act, desk, profile, preference] = JSON.parse(focusKey);
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk; let target = null;
const profileSelector = profile if (act === 'local-delegation') {
? '[data-profile="' + profile + '"]' target = content.querySelector('button[data-act="local-delegation"]');
: ':not([data-profile])'; } else {
const target = content.querySelector( const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' + const profileSelector = profile
profileSelector); ? '[data-profile="' + profile + '"]'
: ':not([data-profile])';
target = content.querySelector(
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
profileSelector);
}
if (target) target.focus(); if (target) target.focus();
void preference;
} }
} }
} catch {} } catch {}
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
return; return;
} }
if (req.method === "POST" && url.pathname === "/api/local-delegation") {
const preferenceInput = url.searchParams.get("preference") || "off";
if (!["off", "on"].includes(String(preferenceInput).toLowerCase())) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid local delegation preference" }));
return;
}
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
await writeLocalDelegationPreference(workshopDir, preference);
const localDelegation = currentLocalDelegationLaunch(preference);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, localDelegation }));
return;
}
if (req.method === "POST" && url.pathname.startsWith("/api/open/")) { if (req.method === "POST" && url.pathname.startsWith("/api/open/")) {
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]); const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE; const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
return; return;
} }
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(workshopDir, subdir, deskName); const deskPath = join(workshopDir, subdir, deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile); const launched = await launchDeskConsole(
deskPath, deskName, workshopDir, profile, localDelegation);
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile })); res.end(JSON.stringify({
ok: true,
deskName,
deskPath,
launched,
profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}));
return; return;
} }
} catch {} } catch {}
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
const signals = await scanSignals(workshopDir); const signals = await scanSignals(workshopDir);
const stashed = await readStash(workshopDir); const stashed = await readStash(workshopDir);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderDashboard(signals, stashed, capabilityToken)); res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
} catch (err) { } catch (err) {
// Top-level boundary: never leave a request hanging or let a // Top-level boundary: never leave a request hanging or let a
// rejection become an unhandled crash — e.g. malformed %-encoding // rejection become an unhandled crash — e.g. malformed %-encoding
@@ -1327,7 +1503,7 @@ const session = await joinSession({
}, },
{ {
name: "open_desk", name: "open_desk",
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.", description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Local Delegation is orthogonal and fail-closed: when available and preferred on, the frontier desk may use sealed local-agent-delegation for bounded read/evidence work. Returns the desk path, profile, localDelegation state, and whether a terminal was launched.",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
@@ -1337,6 +1513,11 @@ const session = await joinSession({
enum: ["repo", "connected"], enum: ["repo", "connected"],
description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`, description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`,
}, },
localDelegation: {
type: "string",
enum: ["off", "on"],
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
},
}, },
required: ["deskName"], required: ["deskName"],
}, },
@@ -1347,13 +1528,21 @@ const session = await joinSession({
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE; const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" }; if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preferenceInput = ctx.input.localDelegation
?? await readLocalDelegationPreference(entry.workshopDir);
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName); const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole( const launched = await launchDeskConsole(
deskPath, ctx.input.deskName, entry.workshopDir, profile); deskPath,
ctx.input.deskName,
entry.workshopDir,
profile,
localDelegation);
return { return {
ok: true, ok: true,
deskName: ctx.input.deskName, deskName: ctx.input.deskName,
@@ -1361,6 +1550,8 @@ const session = await joinSession({
launched, launched,
workshopDir: entry.workshopDir, workshopDir: entry.workshopDir,
profile, profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}; };
} }
} catch {} } catch {}
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value); return typeof value === "string" && !/[%\r\n]/.test(value);
} }
/**
* Args that will be wrapped with quoteWindowsCmdArgument. Inside double quotes,
* cmd still expands %VAR% and !VAR! (delayed expansion). Other metacharacters
* like & | < > ^ ( ) are literal when quoted, so paths such as
* C:\Work\Project (1) must be allowed.
*/
export function isSafeQuotedWindowsCmdArg(value) {
return typeof value === "string" && !/[%!\r\n]/.test(value);
}
export function parsePluginMcpNames(text) { export function parsePluginMcpNames(text) {
let parsed; let parsed;
try { parsed = JSON.parse(text); } try { parsed = JSON.parse(text); }
@@ -7,6 +7,7 @@ import { join } from "node:path";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\""); assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true); assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false); assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
// Quoted args may contain parentheses (common workshop folders).
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Work\\Project (1)"), true);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\%USERNAME%\\w"), false);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\!DELAY!\\w"), false);
}); });
test("executes a Windows cmd shim with safe quoting", { test("executes a Windows cmd shim with safe quoting", {
@@ -0,0 +1,401 @@
import { createHash } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const PREFERENCES = new Set(["off", "on"]);
const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
/** @deprecated Repo-root state is rejected; kept only for docs/migration mentions. */
export const LOCAL_DELEGATION_STATE_FILE = ".local-delegation.json";
export const LOCAL_DELEGATION_ENV = "WORKSHOP_LOCAL_DELEGATION";
export const LOCAL_DELEGATION_SKILL_NAME = "local-agent-delegation";
export const LOCAL_DELEGATION_USER_STATE_DIR = join(".copilot", "workshop-local-delegation");
/**
* Stable user-local preference path for a workshop root.
* Permission state must NOT live in the cloned workshop (a repo can ship
* preference:on). Key by a hash of the canonical workshop path under ~/.copilot.
*/
export function localDelegationPreferencePath(workshopDir, {
home = homedir(),
resolvePath = (p) => p,
} = {}) {
if (typeof workshopDir !== "string" || !workshopDir) {
throw new Error("workshopDir is required");
}
let canonical = workshopDir;
try { canonical = resolvePath(workshopDir); } catch { /* keep input */ }
// Normalize separators only. Do not lowercase: on case-sensitive filesystems
// /work/Foo and /work/foo are distinct workshops and must not share state.
const key = createHash("sha256")
.update(String(canonical).replaceAll("\\", "/"))
.digest("hex")
.slice(0, 32);
return join(home, LOCAL_DELEGATION_USER_STATE_DIR, `${key}.json`);
}
export function isLocalDelegationPreference(value) {
return typeof value === "string" && PREFERENCES.has(value.toLowerCase());
}
export function normalizeLocalDelegationPreference(value, fallback = "off") {
return isLocalDelegationPreference(value) ? value.toLowerCase() : fallback;
}
// Windows Terminal / cmd reparse cannot safely carry long multi-space -i strings.
// Keep the orientation prompt short, ASCII, and quote-free. Local Delegation
// policy lives in WORKSHOP_LOCAL_DELEGATION=enabled + the installed skill — not
// on the CLI.
const MAX_ORIENT_PROMPT_CHARS = 280;
// Allow apostrophes (desk's). Ban double quotes, backticks, dashes that WT/cmd
// have split on, and classic cmd metacharacters.
const UNSAFE_ORIENT_CHARS = /["`—–|&<>^%!()\r\n]/;
export function isSafeDeskOrientPrompt(prompt) {
return typeof prompt === "string"
&& prompt.length > 0
&& prompt.length <= MAX_ORIENT_PROMPT_CHARS
&& !UNSAFE_ORIENT_CHARS.test(prompt);
}
/** One short ASCII notice operators can see in the session start prompt. */
export const LOCAL_DELEGATION_ORIENT_LINE = " Local Delegation env is enabled.";
export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) {
// deskName is already constrained to a slug by the launcher; still keep the
// prompt free of punctuation that cmd/wt have historically mis-parsed.
let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` +
`Read journal.md in this folder first to pick up where the last session ` +
`left off, then continue the desk's work. Write your journal before you stop.`;
// Only a single short ASCII line may ride on -i. Full policy stays in env + skill.
if (localDelegationEffective) {
const withNotice = prompt + LOCAL_DELEGATION_ORIENT_LINE;
if (isSafeDeskOrientPrompt(withNotice)) prompt = withNotice;
}
return prompt;
}
/**
* Operator-visible summary for open toasts and badges.
* Never claims savings; only reports effective state + route id when known.
*/
export function formatLocalDelegationOpenNotice(localDelegation) {
if (!localDelegation || typeof localDelegation !== "object") {
return { titleSuffix: "", detail: "" };
}
const routeId = localDelegation.availability?.routeId || null;
if (localDelegation.effective) {
const routePart = routeId ? ` · route ${routeId}` : "";
return {
titleSuffix: " · Local Delegation effective",
detail: `Local Delegation effective${routePart}`,
};
}
if (localDelegation.requested) {
return {
titleSuffix: " · local unavailable",
detail: localDelegation.warning
|| localDelegation.availability?.reason
|| "Local Delegation requested but unavailable",
};
}
return { titleSuffix: "", detail: "" };
}
export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) {
const env = { ...baseEnv };
// Windows env names are case-insensitive; spreading process.env yields a
// case-sensitive object, so clear every spelling before optionally setting.
const target = LOCAL_DELEGATION_ENV.toLowerCase();
for (const key of Object.keys(env)) {
if (key.toLowerCase() === target) delete env[key];
}
if (localDelegationEffective) {
env[LOCAL_DELEGATION_ENV] = "enabled";
}
return env;
}
/**
* cmd.exe prefix that forces WORKSHOP_LOCAL_DELEGATION on or off inside a new
* Windows Terminal / console session. wt.exe does not reliably forward the
* caller's process env into a new tab when Terminal is already running.
*/
export function windowsLocalDelegationCmdPrefix(localDelegationEffective = false) {
return localDelegationEffective
? 'set "WORKSHOP_LOCAL_DELEGATION=enabled"&& '
: 'set "WORKSHOP_LOCAL_DELEGATION="&& ';
}
/**
* Savings credit is utilization accounting, not a price claim.
* Failed, unaccepted, redone, or escalated local work earns zero.
*/
export function localSavingsCredit({
attempted = false,
gateAccepted = false,
redone = false,
escalated = false,
} = {}) {
if (!attempted || !gateAccepted || redone || escalated) {
return {
credit: 0,
utilization: attempted ? "handled_locally_unaccepted" : "not_attempted",
reason: !attempted
? "not_attempted"
: escalated
? "escalated"
: redone
? "redone"
: "gate_not_accepted",
};
}
return {
credit: 0, // dollar savings are never claimed by Cairn
utilization: "handled_locally_accepted",
reason: "accepted_utilization_only",
};
}
function isReadableFile(path) {
try {
if (!statSync(path).isFile()) return false;
accessSync(path, fsConstants.R_OK);
return true;
} catch {
return false;
}
}
function skillMarkerPath(dir) {
return join(dir, "SKILL.md");
}
function looksLikeSkillDir(dir) {
return isReadableFile(skillMarkerPath(dir));
}
/**
* Discover the installed local-agent-delegation skill.
* Injectable probes keep unit tests filesystem-free.
*/
export function findLocalDelegationSkillDir({
env = process.env,
home = homedir(),
exists = existsSync,
isSkillDir = looksLikeSkillDir,
} = {}) {
const explicit = (env.WORKSHOP_LOCAL_DELEGATION_SKILL_DIR || "").trim();
if (explicit) {
return isSkillDir(explicit) ? explicit : null;
}
const candidates = [
join(home, ".copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(home, ".agents", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const candidate of candidates) {
if (isSkillDir(candidate)) return candidate;
}
// Marketplace: ~/.copilot/installed-plugins/<marketplace>/<plugin>/
// Direct: ~/.copilot/installed-plugins/_direct/<plugin>/
// Skill dirs may live at skills/, .github/skills/, or com.github.copilot/skills/.
const pluginsRoot = join(home, ".copilot", "installed-plugins");
if (exists(pluginsRoot)) {
try {
for (const market of readdirSync(pluginsRoot, { withFileTypes: true })) {
if (!market.isDirectory()) continue;
const marketRoot = join(pluginsRoot, market.name);
let pluginEntries;
try {
pluginEntries = readdirSync(marketRoot, { withFileTypes: true });
} catch {
continue;
}
for (const plugin of pluginEntries) {
if (!plugin.isDirectory()) continue;
const pluginRoot = join(marketRoot, plugin.name);
const nestedCandidates = [
join(pluginRoot, "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, ".github", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.awesome-copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const nested of nestedCandidates) {
if (isSkillDir(nested)) return nested;
}
}
}
} catch {
// fail closed on scan errors
}
}
return null;
}
function readJsonFile(path, readFile = readFileSync) {
try {
return JSON.parse(readFile(path, "utf8"));
} catch {
return null;
}
}
function isRouteId(value) {
return typeof value === "string" && SAFE_ROUTE_ID.test(value);
}
/**
* Fail-closed availability. Enable only when skill + qualified route receipt
* (or explicit env route id) are present. Never invent availability.
*/
export function resolveLocalDelegationAvailability({
env = process.env,
home = homedir(),
now = Date.now(),
findSkill = findLocalDelegationSkillDir,
readFile = readFileSync,
exists = existsSync,
} = {}) {
const forced = (env.WORKSHOP_LOCAL_DELEGATION_AVAILABLE || "").trim().toLowerCase();
if (forced === "0" || forced === "false" || forced === "unavailable") {
return {
available: false,
reason: "Forced unavailable by WORKSHOP_LOCAL_DELEGATION_AVAILABLE",
skillDir: null,
routeId: null,
};
}
const skillDir = findSkill({ env, home, exists });
if (!skillDir) {
return {
available: false,
reason: "local-agent-delegation skill is not installed",
skillDir: null,
routeId: null,
};
}
const envRoute = (env.WORKSHOP_LOCAL_DELEGATION_ROUTE_ID || "").trim();
if (envRoute) {
if (!isRouteId(envRoute)) {
return {
available: false,
reason: "WORKSHOP_LOCAL_DELEGATION_ROUTE_ID is not a safe route id",
skillDir,
routeId: null,
};
}
return {
available: true,
reason: "Skill installed; route id provided by environment",
skillDir,
routeId: envRoute,
};
}
const receiptPath = (env.WORKSHOP_LOCAL_DELEGATION_RECEIPT || "").trim()
|| join(home, ".copilot", "local-agent-runs", "qualified-route.json");
if (!exists(receiptPath)) {
return {
available: false,
reason: "No qualified route receipt (set WORKSHOP_LOCAL_DELEGATION_ROUTE_ID or write ~/.copilot/local-agent-runs/qualified-route.json)",
skillDir,
routeId: null,
};
}
const receipt = readJsonFile(receiptPath, readFile);
if (!receipt || typeof receipt !== "object") {
return {
available: false,
reason: "Qualified route receipt is unreadable",
skillDir,
routeId: null,
};
}
const status = String(receipt.status || "").toLowerCase();
if (status !== "qualified") {
return {
available: false,
reason: `Route receipt status is '${receipt.status || "missing"}', not qualified`,
skillDir,
routeId: isRouteId(receipt.route_id || receipt.routeId) ? (receipt.route_id || receipt.routeId) : null,
};
}
const routeId = receipt.route_id || receipt.routeId || null;
if (!isRouteId(routeId)) {
return {
available: false,
reason: "Route receipt is missing a safe route_id",
skillDir,
routeId: null,
};
}
if (receipt.expires_at || receipt.expiresAt) {
const expires = Date.parse(receipt.expires_at || receipt.expiresAt);
if (!Number.isFinite(expires) || expires <= now) {
return {
available: false,
reason: "Qualified route receipt has expired",
skillDir,
routeId,
};
}
}
return {
available: true,
reason: "Skill installed; qualified route receipt present",
skillDir,
routeId,
};
}
export function resolveLocalDelegationLaunch({
preference = "off",
availability,
} = {}) {
const pref = normalizeLocalDelegationPreference(preference, "off");
const available = Boolean(availability?.available);
if (pref === "on" && !available) {
return {
preference: pref,
requested: true,
effective: false,
availability,
warning: availability?.reason || "Local Delegation unavailable",
};
}
return {
preference: pref,
requested: pref === "on",
effective: pref === "on" && available,
availability,
warning: null,
};
}
export function parseLocalDelegationState(raw) {
if (!raw || typeof raw !== "object") {
return { preference: "off" };
}
return {
preference: normalizeLocalDelegationPreference(raw.preference, "off"),
};
}
export function serializeLocalDelegationState(state) {
return {
preference: normalizeLocalDelegationPreference(state?.preference, "off"),
updatedAt: new Date().toISOString(),
};
}
@@ -0,0 +1,298 @@
import test from "node:test";
import assert from "node:assert/strict";
import { join, sep } from "node:path";
import {
LOCAL_DELEGATION_ORIENT_LINE,
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
findLocalDelegationSkillDir,
formatLocalDelegationOpenNotice,
isLocalDelegationPreference,
isSafeDeskOrientPrompt,
localDelegationPreferencePath,
localSavingsCredit,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
import {
buildDeskAgentArgv,
normalizeDeskProfile,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
test("normalizes local-delegation preference independently of desk profile", () => {
assert.equal(isLocalDelegationPreference("on"), true);
assert.equal(isLocalDelegationPreference("OFF"), true);
assert.equal(isLocalDelegationPreference("maybe"), false);
assert.equal(normalizeLocalDelegationPreference("ON"), "on");
assert.equal(normalizeLocalDelegationPreference("nope"), "off");
// Profile axis remains orthogonal and untouched.
assert.equal(normalizeDeskProfile("connected"), "connected");
assert.equal(normalizeDeskProfile("repo"), "repo");
});
test("availability is fail-closed without skill or route receipt", () => {
const missingSkill = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => null,
exists: () => false,
});
assert.equal(missingSkill.available, false);
assert.match(missingSkill.reason, /not installed/i);
const skillOnly = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => "C:\\home\\.copilot\\skills\\local-agent-delegation",
exists: () => false,
});
assert.equal(skillOnly.available, false);
assert.match(skillOnly.reason, /No qualified route receipt/i);
});
test("availability accepts env route id or a qualified receipt", () => {
const home = join("home");
const skillDir = join("skills", "local-agent-delegation");
const viaEnv = resolveLocalDelegationAvailability({
env: { WORKSHOP_LOCAL_DELEGATION_ROUTE_ID: "foundry-qwen25-7b-qualified" },
home,
findSkill: () => skillDir,
exists: () => false,
});
assert.equal(viaEnv.available, true);
assert.equal(viaEnv.routeId, "foundry-qwen25-7b-qualified");
// Match host path.join separators so Linux CI exercises the receipt branch.
const receiptPath = join(home, ".copilot", "local-agent-runs", "qualified-route.json");
const viaReceipt = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2026-08-14T12:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(viaReceipt.available, true);
assert.equal(viaReceipt.routeId, "foundry-qwen25-7b-qualified");
const expired = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2027-01-01T00:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(expired.available, false);
assert.match(expired.reason, /expired/i);
});
test("requested on + unavailable stays ineffective with a warning", () => {
const launch = resolveLocalDelegationLaunch({
preference: "on",
availability: {
available: false,
reason: "local-agent-delegation skill is not installed",
},
});
assert.equal(launch.requested, true);
assert.equal(launch.effective, false);
assert.match(launch.warning, /not installed/i);
});
test("launch env enables when effective; -i prompt stays short and quote-free", () => {
const base = {
PATH: "/usr/bin",
WORKSHOP_LOCAL_DELEGATION: "enabled",
workshop_local_delegation: "enabled",
};
const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false });
assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false);
assert.equal(Object.hasOwn(offEnv, "workshop_local_delegation"), false);
assert.equal(offEnv.PATH, "/usr/bin");
const onEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: true });
assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled");
assert.equal(Object.hasOwn(onEnv, "workshop_local_delegation"), false);
const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false });
const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true });
assert.match(offPrompt, /cost-desk/);
assert.equal(offPrompt.includes("Local Delegation"), false);
// One short ASCII line only — never the long policy appendix.
assert.equal(onPrompt, offPrompt + LOCAL_DELEGATION_ORIENT_LINE);
assert.match(onPrompt, /Local Delegation env is enabled\./);
assert.equal(onPrompt.includes("do not delegate"), false);
assert.equal(isSafeDeskOrientPrompt(onPrompt), true);
assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false);
assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false);
assert.match(windowsLocalDelegationCmdPrefix(true), /enabled/);
assert.match(windowsLocalDelegationCmdPrefix(false), /WORKSHOP_LOCAL_DELEGATION="&&/);
const run = ["C:\\tools\\copilot.exe", "-i", onPrompt];
const cmdLine = windowsLocalDelegationCmdPrefix(true) + run.map(quoteWindowsCmdArgument).join(" ");
assert.match(cmdLine, /^set "WORKSHOP_LOCAL_DELEGATION=enabled"&& /);
assert.match(cmdLine, /copilot\.exe/);
});
test("open notice reports effective route without savings claims", () => {
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: true,
requested: true,
availability: { available: true, routeId: "foundry-qwen25-7b-qualified" },
}), {
titleSuffix: " · Local Delegation effective",
detail: "Local Delegation effective · route foundry-qwen25-7b-qualified",
});
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: true,
warning: "local-agent-delegation skill is not installed",
availability: { available: false, reason: "local-agent-delegation skill is not installed" },
}).detail, "local-agent-delegation skill is not installed");
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: false,
}), { titleSuffix: "", detail: "" });
});
test("repo/connected argv stays orthogonal to local-delegation preference", () => {
const repo = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "repo",
pluginMcpNames: ["teams"],
});
const connected = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "connected",
pluginMcpNames: ["teams"],
});
assert.deepEqual(repo, [
"copilot", "--name", "cost-desk",
"--disable-mcp-server", "teams",
"--add-dir", "/workshop",
]);
assert.deepEqual(connected, [
"copilot", "--name", "cost-desk",
"--add-dir", "/workshop",
]);
// Local delegation never injects into argv — only env/prompt.
assert.equal(repo.includes("local"), false);
assert.equal(connected.includes("local"), false);
});
test("failed or unaccepted local work earns zero savings credit", () => {
assert.equal(localSavingsCredit({ attempted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, redone: true }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, escalated: true }).credit, 0);
const accepted = localSavingsCredit({ attempted: true, gateAccepted: true });
assert.equal(accepted.credit, 0);
assert.equal(accepted.utilization, "handled_locally_accepted");
});
test("state parse/serialize defaults to off", () => {
assert.deepEqual(parseLocalDelegationState(null), { preference: "off" });
assert.equal(parseLocalDelegationState({ preference: "ON" }).preference, "on");
const serialized = serializeLocalDelegationState({ preference: "on" });
assert.equal(serialized.preference, "on");
assert.equal(typeof serialized.updatedAt, "string");
});
test("preference path is user-local and keyed by workshop path, not the repo root", () => {
const home = join("user-home");
const a = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
const b = localDelegationPreferencePath(join("repos", "workshop-b"), {
home,
resolvePath: (p) => p,
});
const again = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
assert.match(a, /workshop-local-delegation/);
assert.equal(a.startsWith(home), true);
assert.equal(a.includes(`${sep}repos${sep}`), false);
assert.notEqual(a, b);
assert.equal(a, again);
// Repo-shipped .local-delegation.json is never the preference path.
assert.equal(a.endsWith(".local-delegation.json"), false);
// Case-sensitive filesystems: Foo and foo must not share permission state.
const upper = localDelegationPreferencePath("/work/Foo", {
home,
resolvePath: (p) => p,
});
const lower = localDelegationPreferencePath("/work/foo", {
home,
resolvePath: (p) => p,
});
assert.notEqual(upper, lower);
});
test("skill discovery respects explicit dir and common install roots", () => {
const found = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\sealed\\.github\\skills\\local-agent-delegation" },
home: "C:\\home",
exists: () => false,
isSkillDir: (p) => p === "D:\\sealed\\.github\\skills\\local-agent-delegation",
});
assert.equal(found, "D:\\sealed\\.github\\skills\\local-agent-delegation");
const missingExplicit = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\missing" },
home: "C:\\home",
exists: () => false,
isSkillDir: () => false,
});
assert.equal(missingExplicit, null);
});
test("skill discovery walks marketplace/plugin and _direct install layouts", async () => {
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const home = mkdtempSync(join(tmpdir(), "ld-skill-"));
try {
const skillDir = join(
home, ".copilot", "installed-plugins", "awesome-copilot", "sealed-delegation",
".github", "skills", "local-agent-delegation");
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, "SKILL.md"), "# local-agent-delegation\n");
const found = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(found, skillDir);
rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true });
const direct = join(
home, ".copilot", "installed-plugins", "_direct", "sealed-delegation",
"skills", "local-agent-delegation");
mkdirSync(direct, { recursive: true });
writeFileSync(join(direct, "SKILL.md"), "# local-agent-delegation\n");
const foundDirect = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(foundDirect, direct);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
+3 -3
View File
@@ -1,15 +1,15 @@
{ {
"name": "signals-dashboard", "name": "signals-dashboard",
"version": "0.2.0", "version": "0.3.0",
"type": "module", "type": "module",
"main": "extension.mjs", "main": "extension.mjs",
"scripts": { "scripts": {
"test": "node --test launch-profile.test.mjs" "test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
}, },
"dependencies": { "dependencies": {
"@github/copilot-sdk": "latest" "@github/copilot-sdk": "latest"
}, },
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
"keywords": [ "keywords": [
"agent-signals", "agent-signals",
"dashboard", "dashboard",
+15 -3
View File
@@ -1,15 +1,27 @@
# Signals Dashboard Plugin # Signals Dashboard Plugin
Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles. Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware
desk profiles (`repo` / `connected`), and fail-closed Local Delegation.
## Installation ## Installation
``bash ```bash
copilot plugin install signals-dashboard@awesome-copilot copilot plugin install signals-dashboard@awesome-copilot
`` ```
## Features
- Live desk signals, score bars, patterns, and escalations
- Cost-aware **open** (repo) and **connected** desk launch profiles
- **Local Delegation** toggle: when available, the frontier desk may use the
installed `local-agent-delegation` skill for bounded read/evidence work
- Fail-closed availability (skill + qualified route receipt); no silent savings credit
## Source ## Source
Canonical implementation: [jennyf19/the-workshop](https://github.com/jennyf19/the-workshop).
Local worker runtime: [jennyf19/sealed-delegation](https://github.com/jennyf19/sealed-delegation).
This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot). This plugin is part of [Awesome Copilot](https://github.com/github/awesome-copilot).
## License ## License
@@ -0,0 +1,145 @@
# 🪨 Cairn — Signals Dashboard
A live GitHub Copilot CLI **canvas extension** that shows the pulse of every
desk in your Workshop. It reads the agent signals your desks emit and renders
them as a compact, always-current dashboard in a side panel — so you can direct
the work instead of polling each desk by hand.
It replaces the old Blazor **WorkshopRoom** dashboard (`src/WorkshopRoom/`) with
a native canvas that runs inside GHCP, with no separate web app to launch.
## What it is
Each desk in the Workshop leaves signals behind — small stones on the trail —
by writing JSON files into its `.signals/` folder. Cairn scans those folders,
picks the most recent signal per desk, and renders:
- **Score bars** for the desk's self-assessment (intent, confidence, accuracy,
completeness).
- **Patterns** the desk reported: what worked ✓, what was hard △, and skill
gaps ✗.
- **Escalations** — desks that raised their hand, with what they're blocked on
and their recommendation, pinned to the top and pulsing red.
## How to open it
The dashboard is registered as the **🪨 Cairn** canvas (`signals-dashboard`).
Ask Copilot to open it and pass your workshop root as `workshopDir`:
> Open the 🪨 Cairn canvas with `workshopDir` set to the folder that contains
> my `desks/` directory.
`workshopDir` must be the **absolute path to the workshop root** — the folder
that holds `desks/` (and optionally `classroom/`). If omitted, it falls back to
the current working directory.
## Features
- **Signal scanning** — walks `desks/*/.signals/` and `classroom/*/.signals/`,
reading the newest `*.json` per desk (mirrors `SignalReader.cs`).
- **Score bars** — color-coded intent / confidence / accuracy / completeness,
scored out of 5.
- **Escalation alerts** — escalation signals sort to the very top, render with a
pulsing red border, and surface the blocker + recommendation.
- **Active desks first** — sorted escalations → recent signals → desks with no
signal yet, then by recency.
- **Stash / restore** — pause a workstream by stashing its desk. Stashed desks
drop off the active view and auto-expire after a **48-hour TTL**; restore any
time before then. Stash state lives in `.desk-stash.json` at the workshop
root.
- **Auto-refresh** — the panel refreshes every 5 seconds using a background
fetch (no full page reload), so scores and escalations stay current smoothly.
- **Summary bar** — desk count, how many are reporting vs. awaiting, an
escalation badge, and average scores across the room.
- **Cost-aware desk launch****open** starts a repo-profile desk that keeps the
verified Workshop root available while suppressing ambient plugin MCPs.
**connected** preserves every configured MCP for work that needs external
systems. Agency remains the preferred wrapper when installed.
- **Local Delegation** — orthogonal off/on control. When available and on, the
frontier desk may use the installed [`local-agent-delegation`](https://github.com/jennyf19/sealed-delegation)
skill for bounded, independently gated read/evidence work. Fail-closed: no
skill or no qualified route receipt means the toggle cannot take effect, and
no local-savings credit is awarded.
## Agent actions
The canvas also exposes actions Copilot can invoke directly:
- `refresh` — force a rescan and return current signal data as JSON.
- `stash` — stash a desk by `deskName`.
- `restore` — restore a stashed desk by `deskName`.
- `open_desk` — open a desk with optional `profile: "repo" | "connected"` and
optional `localDelegation: "off" | "on"`.
## Desk launch profiles
`repo` is the default. At launch, Cairn asks Copilot for the enabled
plugin-scoped MCP inventory and disables those ambient servers for the topic
desk. User-, workspace-, organization-, and built-in resources are left alone.
If discovery fails, Copilot plugin MCP suppression fails open; Agency repo mode
still omits Agency's own default MCPs.
When Agency is installed, Cairn keeps the existing `agency copilot` launch and
adds Agency's `--no-default-mcps` in repo mode. Outside Agency, the same profile
is applied directly to Copilot CLI.
Both profiles pass `--add-dir <workshop-root>` so a desk can intentionally read
another desk's journal or artifact without receiving access outside the room.
Set `WORKSHOP_DESK_PROFILE=connected` to retain the historical default for the
main **open** button. The separate **connected** button is always available when
repo mode is the default.
## Local Delegation
Local Delegation is **not** a third desk profile and does not replace the frontier
model. It is a separate permission bit:
```text
repo / connected = which MCPs and tools the frontier desk can see
Local Delegation off/on = whether the frontier desk may invoke a bounded local worker
```
Availability is fail-closed. Cairn enables the lane only when:
1. the `local-agent-delegation` skill is installed, and
2. a qualified route is declared via `WORKSHOP_LOCAL_DELEGATION_ROUTE_ID` or a
receipt at `~/.copilot/local-agent-runs/qualified-route.json`
(`status: "qualified"`, safe `route_id`).
Operator preference is stored **user-locally** under
`~/.copilot/workshop-local-delegation/` (keyed by the canonical workshop path),
never in the cloned workshop — a repo cannot ship `preference: on`.
When the preference is on but availability fails, opens still launch as frontier
desks and surface the reason — they never silently fall back with savings credit.
When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched
process, shows an open toast/badge (`Local Delegation effective · route …`), and
may append one short ASCII line to `-i` (`Local Delegation env is enabled.`) when
that combined prompt stays quote-free and under the length guard. Full policy
still lives in the env flag plus the installed `local-agent-delegation` skill —
never a long multi-sentence `-i` appendix (Windows Terminal reparse).
The runtime, launcher, and gates remain owned by
[Sealed Delegation](https://github.com/jennyf19/sealed-delegation).
## Signal shape
Cairn reads the agent-signals protocol used across the Workshop:
```json
{
"signal_type": "execution",
"agent_name": "desk-name",
"self_assessment": { "intent": 5, "confidence": 4, "accuracy": 4, "completeness": 3 },
"patterns": { "what_worked": "...", "what_was_hard": "...", "skill_gap": "..." },
"escalation": { "reason": "...", "blocked_on": "...", "recommendation": "..." }
}
```
`escalation` is only present on `signal_type: "escalation"` signals.
## Replaces the Blazor WorkshopRoom
This canvas supersedes the standalone Blazor dashboard in `src/WorkshopRoom/`.
The data is the truth and the UI is just a view — Cairn renders the same signal
data natively inside GHCP, so there's no separate server to run.
@@ -5,20 +5,33 @@
import { createServer } from "node:http"; import { createServer } from "node:http";
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs"; import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
import { readdir, readFile, writeFile, stat } from "node:fs/promises"; import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
import { join, delimiter, isAbsolute, sep } from "node:path"; import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
parsePluginMcpNames, parsePluginMcpNames,
quoteWindowsCmdArgument, quoteWindowsCmdArgument,
} from "./launch-profile.mjs"; } from "./launch-profile.mjs";
import {
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
formatLocalDelegationOpenNotice,
localDelegationPreferencePath,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
const servers = new Map(); const servers = new Map();
const STASH_TTL_MS = 48 * 60 * 60 * 1000; const STASH_TTL_MS = 48 * 60 * 60 * 1000;
@@ -60,11 +73,10 @@ function isValidDeskName(name) {
// path is then only ever passed as a spawn cwd, an argv element, or a // path is then only ever passed as a spawn cwd, an argv element, or a
// single-quoted literal inside the macOS Terminal command — never concatenated // single-quoted literal inside the macOS Terminal command — never concatenated
// raw onto a command line — so no character filtering of the path is required. // raw onto a command line — so no character filtering of the path is required.
function deskOrientPrompt(deskName) { //
return `You are sitting down at the ${deskName} desk in this workshop. ` + // Local Delegation is orthogonal to repo/connected: it never changes the tool
`Read journal.md in this folder first to pick up where the last session ` + // surface. When effective, only the orientation prompt and child env mark that
`left off, then continue the desk's work. Write your journal before you stop.`; // the frontier desk may use the installed local-agent-delegation skill.
}
// Spawn detached and resolve true only once the OS confirms the process // Spawn detached and resolve true only once the OS confirms the process
// started ('spawn'), false on failure ('error', e.g. the binary is missing) so // started ('spawn'), false on failure ('error', e.g. the binary is missing) so
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
} catch { return false; } } catch { return false; }
} }
async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) { function preferenceStatePath(workshopDir) {
return localDelegationPreferencePath(workshopDir, {
resolvePath: (p) => {
try { return realpathSync(p); } catch { return p; }
},
});
}
async function readLocalDelegationPreference(workshopDir) {
// Never read preference from the workshop repo — a clone can ship
// preference:on. Only user-local state (or explicit env) counts.
try {
const raw = JSON.parse(await readFile(preferenceStatePath(workshopDir), "utf8"));
return parseLocalDelegationState(raw).preference;
} catch {
return normalizeLocalDelegationPreference(
process.env.WORKSHOP_LOCAL_DELEGATION_PREFERENCE, "off");
}
}
async function writeLocalDelegationPreference(workshopDir, preference) {
const state = serializeLocalDelegationState({ preference });
const target = preferenceStatePath(workshopDir);
await mkdir(dirname(target), { recursive: true });
// Atomic replace in the user-local dir (temp + rename).
const tmp = join(
dirname(target),
`.pref.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
const body = JSON.stringify(state, null, 2) + "\n";
try {
await writeFile(tmp, body, { encoding: "utf8", flag: "wx" });
try {
await rename(tmp, target);
} catch {
await unlink(target).catch(() => {});
await rename(tmp, target);
}
} catch (err) {
await unlink(tmp).catch(() => {});
throw err;
}
return state;
}
function currentLocalDelegationLaunch(preference) {
const availability = resolveLocalDelegationAvailability();
return resolveLocalDelegationLaunch({ preference, availability });
}
async function launchDeskConsole(
deskPath,
deskName,
workshopDir,
profile = DEFAULT_DESK_PROFILE,
localDelegation = { effective: false },
) {
// deskName must be a plain slug so it is safe on every command line and shell // 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 // below, and the resolved desk must still live inside the workshop root
// (which defeats a symlinked desk that escapes the repo). deskPath itself is // (which defeats a symlinked desk that escapes the repo). deskPath itself is
@@ -367,24 +434,27 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
if (!isInsideRoot(workshopDir, deskPath)) return false; if (!isInsideRoot(workshopDir, deskPath)) return false;
const agent = await deskAgentArgv(deskName, workshopDir, profile); const agent = await deskAgentArgv(deskName, workshopDir, profile);
if (!agent) return false; if (!agent) return false;
const run = [...agent, "-i", deskOrientPrompt(deskName)]; const effective = Boolean(localDelegation?.effective);
const run = [...agent, "-i", deskOrientPrompt(deskName, { localDelegationEffective: effective })];
const env = buildLocalDelegationLaunchEnv(process.env, { localDelegationEffective: effective });
if (process.platform === "win32") { if (process.platform === "win32") {
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir }); const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
const cmd = resolveSystem32Executable("cmd.exe"); const cmd = resolveSystem32Executable("cmd.exe");
const direct = /\.(exe|com)$/i.test(run[0]); // wt.exe does not reliably forward Node's spawn env into a new tab when
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true; // Windows Terminal is already running. Always start through cmd.exe and
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every // Args are quoteWindowsCmdArgument'd, so only block expanders that still
// argument is free of cmd metacharacters; otherwise fail closed and let // fire inside quotes (% and !) — allow parentheses in workshop paths.
// the UI copy the desk path rather than reparse an unsafe workshop path. const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)); if (!cmdSafe || !cmd) return false;
if (cmdSafe && wt && cmd && const inner = windowsLocalDelegationCmdPrefix(effective)
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true; + run.map(quoteWindowsCmdArgument).join(" ");
// Fallback when wt.exe is absent: a fresh console window via `start`, if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
// still through cmd /k only when the arguments are safe for reparsing. return true;
return cmdSafe && cmd }
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath }) // Fallback when wt.exe is absent: a fresh console window via `start`.
: false; return await trySpawn(
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
} }
if (process.platform === "darwin") { if (process.platform === "darwin") {
const osascript = "/usr/bin/osascript"; const osascript = "/usr/bin/osascript";
@@ -393,13 +463,18 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
// to cd into the desk and exec the agent. Each argv element is POSIX // 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 // single-quoted so the shell can't reinterpret it, and osascript itself
// is spawned via argv (no shell). // is spawned via argv (no shell).
const line = "cd " + shSingleQuote(deskPath) + " && exec " + // Local-delegation env is exported in-line so the Terminal session sees it
// without inheriting a polluted parent shell forever.
const envPrefix = effective
? "export WORKSHOP_LOCAL_DELEGATION=enabled; "
: "unset WORKSHOP_LOCAL_DELEGATION; ";
const line = "cd " + shSingleQuote(deskPath) + " && " + envPrefix + "exec " +
run.map(shSingleQuote).join(" "); run.map(shSingleQuote).join(" ");
const script = 'tell application "Terminal"\n' + const script = 'tell application "Terminal"\n' +
" activate\n" + " activate\n" +
" do script " + osaStringLiteral(line) + "\n" + " do script " + osaStringLiteral(line) + "\n" +
"end tell"; "end tell";
return await trySpawn(osascript, ["-e", script]); return await trySpawn(osascript, ["-e", script], { env });
} }
// Linux/other: best-effort across common terminal emulators. Each is spawned // 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, // via argv (no shell) with the agent command after the emulator's exec flag,
@@ -412,7 +487,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
]; ];
for (const [term, args] of linuxTerms) { for (const [term, args] of linuxTerms) {
const executable = resolveOnPath(term, { excludedRoot: workshopDir }); const executable = resolveOnPath(term, { excludedRoot: workshopDir });
if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true; if (executable && await trySpawn(executable, args, { cwd: deskPath, env })) return true;
} }
return false; return false;
} }
@@ -728,7 +803,42 @@ function avgScore(signals) {
return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") }; return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") };
} }
function renderSummaryBar(activeSignals) { function renderLocalDelegationControl(localDelegation) {
const pref = localDelegation?.preference || "off";
const available = Boolean(localDelegation?.availability?.available);
const effective = Boolean(localDelegation?.effective);
const reason = localDelegation?.availability?.reason || "Local Delegation unavailable";
const routeId = localDelegation?.availability?.routeId || null;
const next = pref === "on" ? "off" : "on";
const label = effective ? "On" : (pref === "on" ? "On*" : "Off");
const color = effective ? "#86efac" : (pref === "on" ? "#fbbf24" : "#94a3b8");
const border = effective ? "#166534" : (pref === "on" ? "#854d0e" : "#334155");
const title = available
? (effective
? `Local Delegation effective${routeId ? ` · route ${routeId}` : ""}`
: "Local Delegation available but currently off")
: reason;
const note = effective && routeId
? `<span style="font-size:10px;color:#86efac;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(title)}">effective · ${esc(truncate(routeId, 28))}</span>`
: !available
? `<span style="font-size:10px;color:#64748b;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(reason)}">${esc(truncate(reason, 48))}</span>`
: (pref === "on" && !effective
? `<span style="font-size:10px;color:#fbbf24;">requested, unavailable</span>`
: "");
return `
<div style="display:flex;align-items:center;gap:6px;" title="${esc(title)}">
<span style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.04em;">Local</span>
<button data-act="local-delegation" data-preference="${esc(next)}"
aria-label="Local Delegation ${esc(label)}${effective && routeId ? ` route ${esc(routeId)}` : ""}"
aria-pressed="${pref === "on" ? "true" : "false"}"
style="background:#020617;border:1px solid ${border};color:${color};padding:2px 8px;border-radius:999px;
font-size:11px;cursor:pointer;font-weight:600;min-width:42px;"
title="${esc(title)}">${esc(label)}</button>
${note}
</div>`;
}
function renderSummaryBar(activeSignals, localDelegation) {
const escalations = activeSignals.filter(s => s.signalType === "escalation").length; const escalations = activeSignals.filter(s => s.signalType === "escalation").length;
const withSignals = activeSignals.filter(s => s.signalType !== "none").length; const withSignals = activeSignals.filter(s => s.signalType !== "none").length;
const awaiting = activeSignals.filter(s => s.signalType === "none").length; const awaiting = activeSignals.filter(s => s.signalType === "none").length;
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
return ` return `
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px; <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;
background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;"> background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;gap:12px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span> <span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span>
<span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span> <span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
${tokenBadge} ${tokenBadge}
${calibrationBadge} ${calibrationBadge}
${escBadge} ${escBadge}
${renderLocalDelegationControl(localDelegation)}
</div> </div>
${avgBlock} ${avgBlock}
</div>`; </div>`;
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
</div>`; </div>`;
} }
function renderDashboard(signals, stashed, capabilityToken) { function renderDashboard(signals, stashed, capabilityToken, localDelegation) {
const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName))); const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName)));
const localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
const cards = activeSignals.length > 0 const cards = activeSignals.length > 0
? activeSignals.map(renderSignalCard).join("") ? activeSignals.map(renderSignalCard).join("")
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
</div> </div>
</div>`; </div>`;
const summaryBar = activeSignals.length > 0 ? renderSummaryBar(activeSignals) : ""; // Always show the Local Delegation control so operators can see availability
// even before the first desk signal arrives.
const summaryBar = renderSummaryBar(activeSignals, localDelegationState);
const stashedSection = stashed.length > 0 ? ` const stashedSection = stashed.length > 0 ? `
<div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;"> <div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;">
@@ -1058,27 +1172,55 @@ function renderDashboard(signals, stashed, capabilityToken) {
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
const path = data.deskPath || name; const path = data.deskPath || name;
const notice = data.localDelegationNotice || {};
const localTitle = notice.titleSuffix || '';
const localDetail = notice.detail || '';
if (data.launched) { if (data.launched) {
// A successful open shouldn't hijack the user's clipboard. // A successful open shouldn't hijack the user's clipboard.
showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path); // Surface LD state in the toast — operators cannot rely on -i alone.
showToast('opening ' + name + ' desk (' + selectedProfile + localTitle + ')…',
localDetail || path);
} else { } else {
// No terminal launched from here, so copy the path as the // No terminal launched from here, so copy the path as the
// fallback handle, but only claim the copy when it actually // fallback handle, but only claim the copy when it actually
// succeeded. The path shows in the toast either way. // succeeded. The path shows in the toast either way.
let copied = false; let copied = false;
try { await navigator.clipboard.writeText(path); copied = true; } catch {} try { await navigator.clipboard.writeText(path); copied = true; } catch {}
showToast(copied ? (name + ' · path copied') : (name + ' · copy this path'), path); const copyTitle = copied ? (name + ' · path copied') : (name + ' · copy this path');
showToast(copyTitle + localTitle, localDetail || path);
} }
} else { } else {
showToast(name + ' · not found', ''); showToast(name + ' · not found', '');
} }
} }
async function setLocalDelegation(preference) {
const res = await fetch('/api/local-delegation?preference=' +
encodeURIComponent(preference || 'off'), POST_OPTS);
const data = await res.json();
if (data.ok) {
const ld = data.localDelegation || {};
const routeId = ld.availability && ld.availability.routeId;
const label = ld.effective
? ('Local Delegation effective' + (routeId ? (' · route ' + routeId) : ''))
: (ld.preference === 'on'
? 'Local Delegation requested (unavailable)'
: 'Local Delegation off');
showToast(label, ld.availability?.reason || '');
refresh();
} else {
showToast('Local Delegation · not updated', data.error || '');
}
}
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-act]'); const btn = e.target.closest('button[data-act]');
if (!btn) return; if (!btn) return;
const act = btn.getAttribute('data-act');
if (act === 'local-delegation') {
setLocalDelegation(btn.getAttribute('data-preference') || 'off');
return;
}
const name = btn.getAttribute('data-desk'); const name = btn.getAttribute('data-desk');
if (!name) return; if (!name) return;
const act = btn.getAttribute('data-act');
const profile = btn.getAttribute('data-profile'); const profile = btn.getAttribute('data-profile');
if (act === 'stash') stashDesk(name); if (act === 'stash') stashDesk(name);
else if (act === 'restore') restoreDesk(name); else if (act === 'restore') restoreDesk(name);
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
active.getAttribute('data-act'), active.getAttribute('data-act'),
active.getAttribute('data-desk'), active.getAttribute('data-desk'),
active.getAttribute('data-profile') || '', active.getAttribute('data-profile') || '',
active.getAttribute('data-preference') || '',
]); ]);
} }
content.innerHTML = newContent.innerHTML; content.innerHTML = newContent.innerHTML;
if (focusKey) { if (focusKey) {
const [act, desk, profile] = JSON.parse(focusKey); const [act, desk, profile, preference] = JSON.parse(focusKey);
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk; let target = null;
const profileSelector = profile if (act === 'local-delegation') {
? '[data-profile="' + profile + '"]' target = content.querySelector('button[data-act="local-delegation"]');
: ':not([data-profile])'; } else {
const target = content.querySelector( const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' + const profileSelector = profile
profileSelector); ? '[data-profile="' + profile + '"]'
: ':not([data-profile])';
target = content.querySelector(
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
profileSelector);
}
if (target) target.focus(); if (target) target.focus();
void preference;
} }
} }
} catch {} } catch {}
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
return; return;
} }
if (req.method === "POST" && url.pathname === "/api/local-delegation") {
const preferenceInput = url.searchParams.get("preference") || "off";
if (!["off", "on"].includes(String(preferenceInput).toLowerCase())) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid local delegation preference" }));
return;
}
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
await writeLocalDelegationPreference(workshopDir, preference);
const localDelegation = currentLocalDelegationLaunch(preference);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, localDelegation }));
return;
}
if (req.method === "POST" && url.pathname.startsWith("/api/open/")) { if (req.method === "POST" && url.pathname.startsWith("/api/open/")) {
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]); const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE; const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
return; return;
} }
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(workshopDir, subdir, deskName); const deskPath = join(workshopDir, subdir, deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile); const launched = await launchDeskConsole(
deskPath, deskName, workshopDir, profile, localDelegation);
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile })); res.end(JSON.stringify({
ok: true,
deskName,
deskPath,
launched,
profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}));
return; return;
} }
} catch {} } catch {}
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
const signals = await scanSignals(workshopDir); const signals = await scanSignals(workshopDir);
const stashed = await readStash(workshopDir); const stashed = await readStash(workshopDir);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderDashboard(signals, stashed, capabilityToken)); res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
} catch (err) { } catch (err) {
// Top-level boundary: never leave a request hanging or let a // Top-level boundary: never leave a request hanging or let a
// rejection become an unhandled crash — e.g. malformed %-encoding // rejection become an unhandled crash — e.g. malformed %-encoding
@@ -1327,7 +1503,7 @@ const session = await joinSession({
}, },
{ {
name: "open_desk", name: "open_desk",
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.", description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Local Delegation is orthogonal and fail-closed: when available and preferred on, the frontier desk may use sealed local-agent-delegation for bounded read/evidence work. Returns the desk path, profile, localDelegation state, and whether a terminal was launched.",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
@@ -1337,6 +1513,11 @@ const session = await joinSession({
enum: ["repo", "connected"], enum: ["repo", "connected"],
description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`, description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`,
}, },
localDelegation: {
type: "string",
enum: ["off", "on"],
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
},
}, },
required: ["deskName"], required: ["deskName"],
}, },
@@ -1347,13 +1528,21 @@ const session = await joinSession({
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE; const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" }; if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preferenceInput = ctx.input.localDelegation
?? await readLocalDelegationPreference(entry.workshopDir);
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName); const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole( const launched = await launchDeskConsole(
deskPath, ctx.input.deskName, entry.workshopDir, profile); deskPath,
ctx.input.deskName,
entry.workshopDir,
profile,
localDelegation);
return { return {
ok: true, ok: true,
deskName: ctx.input.deskName, deskName: ctx.input.deskName,
@@ -1361,6 +1550,8 @@ const session = await joinSession({
launched, launched,
workshopDir: entry.workshopDir, workshopDir: entry.workshopDir,
profile, profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}; };
} }
} catch {} } catch {}
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value); return typeof value === "string" && !/[%\r\n]/.test(value);
} }
/**
* Args that will be wrapped with quoteWindowsCmdArgument. Inside double quotes,
* cmd still expands %VAR% and !VAR! (delayed expansion). Other metacharacters
* like & | < > ^ ( ) are literal when quoted, so paths such as
* C:\Work\Project (1) must be allowed.
*/
export function isSafeQuotedWindowsCmdArg(value) {
return typeof value === "string" && !/[%!\r\n]/.test(value);
}
export function parsePluginMcpNames(text) { export function parsePluginMcpNames(text) {
let parsed; let parsed;
try { parsed = JSON.parse(text); } try { parsed = JSON.parse(text); }
@@ -7,6 +7,7 @@ import { join } from "node:path";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\""); assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true); assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false); assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
// Quoted args may contain parentheses (common workshop folders).
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Work\\Project (1)"), true);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\%USERNAME%\\w"), false);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\!DELAY!\\w"), false);
}); });
test("executes a Windows cmd shim with safe quoting", { test("executes a Windows cmd shim with safe quoting", {
@@ -0,0 +1,401 @@
import { createHash } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const PREFERENCES = new Set(["off", "on"]);
const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
/** @deprecated Repo-root state is rejected; kept only for docs/migration mentions. */
export const LOCAL_DELEGATION_STATE_FILE = ".local-delegation.json";
export const LOCAL_DELEGATION_ENV = "WORKSHOP_LOCAL_DELEGATION";
export const LOCAL_DELEGATION_SKILL_NAME = "local-agent-delegation";
export const LOCAL_DELEGATION_USER_STATE_DIR = join(".copilot", "workshop-local-delegation");
/**
* Stable user-local preference path for a workshop root.
* Permission state must NOT live in the cloned workshop (a repo can ship
* preference:on). Key by a hash of the canonical workshop path under ~/.copilot.
*/
export function localDelegationPreferencePath(workshopDir, {
home = homedir(),
resolvePath = (p) => p,
} = {}) {
if (typeof workshopDir !== "string" || !workshopDir) {
throw new Error("workshopDir is required");
}
let canonical = workshopDir;
try { canonical = resolvePath(workshopDir); } catch { /* keep input */ }
// Normalize separators only. Do not lowercase: on case-sensitive filesystems
// /work/Foo and /work/foo are distinct workshops and must not share state.
const key = createHash("sha256")
.update(String(canonical).replaceAll("\\", "/"))
.digest("hex")
.slice(0, 32);
return join(home, LOCAL_DELEGATION_USER_STATE_DIR, `${key}.json`);
}
export function isLocalDelegationPreference(value) {
return typeof value === "string" && PREFERENCES.has(value.toLowerCase());
}
export function normalizeLocalDelegationPreference(value, fallback = "off") {
return isLocalDelegationPreference(value) ? value.toLowerCase() : fallback;
}
// Windows Terminal / cmd reparse cannot safely carry long multi-space -i strings.
// Keep the orientation prompt short, ASCII, and quote-free. Local Delegation
// policy lives in WORKSHOP_LOCAL_DELEGATION=enabled + the installed skill — not
// on the CLI.
const MAX_ORIENT_PROMPT_CHARS = 280;
// Allow apostrophes (desk's). Ban double quotes, backticks, dashes that WT/cmd
// have split on, and classic cmd metacharacters.
const UNSAFE_ORIENT_CHARS = /["`—–|&<>^%!()\r\n]/;
export function isSafeDeskOrientPrompt(prompt) {
return typeof prompt === "string"
&& prompt.length > 0
&& prompt.length <= MAX_ORIENT_PROMPT_CHARS
&& !UNSAFE_ORIENT_CHARS.test(prompt);
}
/** One short ASCII notice operators can see in the session start prompt. */
export const LOCAL_DELEGATION_ORIENT_LINE = " Local Delegation env is enabled.";
export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) {
// deskName is already constrained to a slug by the launcher; still keep the
// prompt free of punctuation that cmd/wt have historically mis-parsed.
let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` +
`Read journal.md in this folder first to pick up where the last session ` +
`left off, then continue the desk's work. Write your journal before you stop.`;
// Only a single short ASCII line may ride on -i. Full policy stays in env + skill.
if (localDelegationEffective) {
const withNotice = prompt + LOCAL_DELEGATION_ORIENT_LINE;
if (isSafeDeskOrientPrompt(withNotice)) prompt = withNotice;
}
return prompt;
}
/**
* Operator-visible summary for open toasts and badges.
* Never claims savings; only reports effective state + route id when known.
*/
export function formatLocalDelegationOpenNotice(localDelegation) {
if (!localDelegation || typeof localDelegation !== "object") {
return { titleSuffix: "", detail: "" };
}
const routeId = localDelegation.availability?.routeId || null;
if (localDelegation.effective) {
const routePart = routeId ? ` · route ${routeId}` : "";
return {
titleSuffix: " · Local Delegation effective",
detail: `Local Delegation effective${routePart}`,
};
}
if (localDelegation.requested) {
return {
titleSuffix: " · local unavailable",
detail: localDelegation.warning
|| localDelegation.availability?.reason
|| "Local Delegation requested but unavailable",
};
}
return { titleSuffix: "", detail: "" };
}
export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) {
const env = { ...baseEnv };
// Windows env names are case-insensitive; spreading process.env yields a
// case-sensitive object, so clear every spelling before optionally setting.
const target = LOCAL_DELEGATION_ENV.toLowerCase();
for (const key of Object.keys(env)) {
if (key.toLowerCase() === target) delete env[key];
}
if (localDelegationEffective) {
env[LOCAL_DELEGATION_ENV] = "enabled";
}
return env;
}
/**
* cmd.exe prefix that forces WORKSHOP_LOCAL_DELEGATION on or off inside a new
* Windows Terminal / console session. wt.exe does not reliably forward the
* caller's process env into a new tab when Terminal is already running.
*/
export function windowsLocalDelegationCmdPrefix(localDelegationEffective = false) {
return localDelegationEffective
? 'set "WORKSHOP_LOCAL_DELEGATION=enabled"&& '
: 'set "WORKSHOP_LOCAL_DELEGATION="&& ';
}
/**
* Savings credit is utilization accounting, not a price claim.
* Failed, unaccepted, redone, or escalated local work earns zero.
*/
export function localSavingsCredit({
attempted = false,
gateAccepted = false,
redone = false,
escalated = false,
} = {}) {
if (!attempted || !gateAccepted || redone || escalated) {
return {
credit: 0,
utilization: attempted ? "handled_locally_unaccepted" : "not_attempted",
reason: !attempted
? "not_attempted"
: escalated
? "escalated"
: redone
? "redone"
: "gate_not_accepted",
};
}
return {
credit: 0, // dollar savings are never claimed by Cairn
utilization: "handled_locally_accepted",
reason: "accepted_utilization_only",
};
}
function isReadableFile(path) {
try {
if (!statSync(path).isFile()) return false;
accessSync(path, fsConstants.R_OK);
return true;
} catch {
return false;
}
}
function skillMarkerPath(dir) {
return join(dir, "SKILL.md");
}
function looksLikeSkillDir(dir) {
return isReadableFile(skillMarkerPath(dir));
}
/**
* Discover the installed local-agent-delegation skill.
* Injectable probes keep unit tests filesystem-free.
*/
export function findLocalDelegationSkillDir({
env = process.env,
home = homedir(),
exists = existsSync,
isSkillDir = looksLikeSkillDir,
} = {}) {
const explicit = (env.WORKSHOP_LOCAL_DELEGATION_SKILL_DIR || "").trim();
if (explicit) {
return isSkillDir(explicit) ? explicit : null;
}
const candidates = [
join(home, ".copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(home, ".agents", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const candidate of candidates) {
if (isSkillDir(candidate)) return candidate;
}
// Marketplace: ~/.copilot/installed-plugins/<marketplace>/<plugin>/
// Direct: ~/.copilot/installed-plugins/_direct/<plugin>/
// Skill dirs may live at skills/, .github/skills/, or com.github.copilot/skills/.
const pluginsRoot = join(home, ".copilot", "installed-plugins");
if (exists(pluginsRoot)) {
try {
for (const market of readdirSync(pluginsRoot, { withFileTypes: true })) {
if (!market.isDirectory()) continue;
const marketRoot = join(pluginsRoot, market.name);
let pluginEntries;
try {
pluginEntries = readdirSync(marketRoot, { withFileTypes: true });
} catch {
continue;
}
for (const plugin of pluginEntries) {
if (!plugin.isDirectory()) continue;
const pluginRoot = join(marketRoot, plugin.name);
const nestedCandidates = [
join(pluginRoot, "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, ".github", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.awesome-copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const nested of nestedCandidates) {
if (isSkillDir(nested)) return nested;
}
}
}
} catch {
// fail closed on scan errors
}
}
return null;
}
function readJsonFile(path, readFile = readFileSync) {
try {
return JSON.parse(readFile(path, "utf8"));
} catch {
return null;
}
}
function isRouteId(value) {
return typeof value === "string" && SAFE_ROUTE_ID.test(value);
}
/**
* Fail-closed availability. Enable only when skill + qualified route receipt
* (or explicit env route id) are present. Never invent availability.
*/
export function resolveLocalDelegationAvailability({
env = process.env,
home = homedir(),
now = Date.now(),
findSkill = findLocalDelegationSkillDir,
readFile = readFileSync,
exists = existsSync,
} = {}) {
const forced = (env.WORKSHOP_LOCAL_DELEGATION_AVAILABLE || "").trim().toLowerCase();
if (forced === "0" || forced === "false" || forced === "unavailable") {
return {
available: false,
reason: "Forced unavailable by WORKSHOP_LOCAL_DELEGATION_AVAILABLE",
skillDir: null,
routeId: null,
};
}
const skillDir = findSkill({ env, home, exists });
if (!skillDir) {
return {
available: false,
reason: "local-agent-delegation skill is not installed",
skillDir: null,
routeId: null,
};
}
const envRoute = (env.WORKSHOP_LOCAL_DELEGATION_ROUTE_ID || "").trim();
if (envRoute) {
if (!isRouteId(envRoute)) {
return {
available: false,
reason: "WORKSHOP_LOCAL_DELEGATION_ROUTE_ID is not a safe route id",
skillDir,
routeId: null,
};
}
return {
available: true,
reason: "Skill installed; route id provided by environment",
skillDir,
routeId: envRoute,
};
}
const receiptPath = (env.WORKSHOP_LOCAL_DELEGATION_RECEIPT || "").trim()
|| join(home, ".copilot", "local-agent-runs", "qualified-route.json");
if (!exists(receiptPath)) {
return {
available: false,
reason: "No qualified route receipt (set WORKSHOP_LOCAL_DELEGATION_ROUTE_ID or write ~/.copilot/local-agent-runs/qualified-route.json)",
skillDir,
routeId: null,
};
}
const receipt = readJsonFile(receiptPath, readFile);
if (!receipt || typeof receipt !== "object") {
return {
available: false,
reason: "Qualified route receipt is unreadable",
skillDir,
routeId: null,
};
}
const status = String(receipt.status || "").toLowerCase();
if (status !== "qualified") {
return {
available: false,
reason: `Route receipt status is '${receipt.status || "missing"}', not qualified`,
skillDir,
routeId: isRouteId(receipt.route_id || receipt.routeId) ? (receipt.route_id || receipt.routeId) : null,
};
}
const routeId = receipt.route_id || receipt.routeId || null;
if (!isRouteId(routeId)) {
return {
available: false,
reason: "Route receipt is missing a safe route_id",
skillDir,
routeId: null,
};
}
if (receipt.expires_at || receipt.expiresAt) {
const expires = Date.parse(receipt.expires_at || receipt.expiresAt);
if (!Number.isFinite(expires) || expires <= now) {
return {
available: false,
reason: "Qualified route receipt has expired",
skillDir,
routeId,
};
}
}
return {
available: true,
reason: "Skill installed; qualified route receipt present",
skillDir,
routeId,
};
}
export function resolveLocalDelegationLaunch({
preference = "off",
availability,
} = {}) {
const pref = normalizeLocalDelegationPreference(preference, "off");
const available = Boolean(availability?.available);
if (pref === "on" && !available) {
return {
preference: pref,
requested: true,
effective: false,
availability,
warning: availability?.reason || "Local Delegation unavailable",
};
}
return {
preference: pref,
requested: pref === "on",
effective: pref === "on" && available,
availability,
warning: null,
};
}
export function parseLocalDelegationState(raw) {
if (!raw || typeof raw !== "object") {
return { preference: "off" };
}
return {
preference: normalizeLocalDelegationPreference(raw.preference, "off"),
};
}
export function serializeLocalDelegationState(state) {
return {
preference: normalizeLocalDelegationPreference(state?.preference, "off"),
updatedAt: new Date().toISOString(),
};
}
@@ -0,0 +1,298 @@
import test from "node:test";
import assert from "node:assert/strict";
import { join, sep } from "node:path";
import {
LOCAL_DELEGATION_ORIENT_LINE,
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
findLocalDelegationSkillDir,
formatLocalDelegationOpenNotice,
isLocalDelegationPreference,
isSafeDeskOrientPrompt,
localDelegationPreferencePath,
localSavingsCredit,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
import {
buildDeskAgentArgv,
normalizeDeskProfile,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
test("normalizes local-delegation preference independently of desk profile", () => {
assert.equal(isLocalDelegationPreference("on"), true);
assert.equal(isLocalDelegationPreference("OFF"), true);
assert.equal(isLocalDelegationPreference("maybe"), false);
assert.equal(normalizeLocalDelegationPreference("ON"), "on");
assert.equal(normalizeLocalDelegationPreference("nope"), "off");
// Profile axis remains orthogonal and untouched.
assert.equal(normalizeDeskProfile("connected"), "connected");
assert.equal(normalizeDeskProfile("repo"), "repo");
});
test("availability is fail-closed without skill or route receipt", () => {
const missingSkill = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => null,
exists: () => false,
});
assert.equal(missingSkill.available, false);
assert.match(missingSkill.reason, /not installed/i);
const skillOnly = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => "C:\\home\\.copilot\\skills\\local-agent-delegation",
exists: () => false,
});
assert.equal(skillOnly.available, false);
assert.match(skillOnly.reason, /No qualified route receipt/i);
});
test("availability accepts env route id or a qualified receipt", () => {
const home = join("home");
const skillDir = join("skills", "local-agent-delegation");
const viaEnv = resolveLocalDelegationAvailability({
env: { WORKSHOP_LOCAL_DELEGATION_ROUTE_ID: "foundry-qwen25-7b-qualified" },
home,
findSkill: () => skillDir,
exists: () => false,
});
assert.equal(viaEnv.available, true);
assert.equal(viaEnv.routeId, "foundry-qwen25-7b-qualified");
// Match host path.join separators so Linux CI exercises the receipt branch.
const receiptPath = join(home, ".copilot", "local-agent-runs", "qualified-route.json");
const viaReceipt = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2026-08-14T12:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(viaReceipt.available, true);
assert.equal(viaReceipt.routeId, "foundry-qwen25-7b-qualified");
const expired = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2027-01-01T00:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(expired.available, false);
assert.match(expired.reason, /expired/i);
});
test("requested on + unavailable stays ineffective with a warning", () => {
const launch = resolveLocalDelegationLaunch({
preference: "on",
availability: {
available: false,
reason: "local-agent-delegation skill is not installed",
},
});
assert.equal(launch.requested, true);
assert.equal(launch.effective, false);
assert.match(launch.warning, /not installed/i);
});
test("launch env enables when effective; -i prompt stays short and quote-free", () => {
const base = {
PATH: "/usr/bin",
WORKSHOP_LOCAL_DELEGATION: "enabled",
workshop_local_delegation: "enabled",
};
const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false });
assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false);
assert.equal(Object.hasOwn(offEnv, "workshop_local_delegation"), false);
assert.equal(offEnv.PATH, "/usr/bin");
const onEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: true });
assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled");
assert.equal(Object.hasOwn(onEnv, "workshop_local_delegation"), false);
const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false });
const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true });
assert.match(offPrompt, /cost-desk/);
assert.equal(offPrompt.includes("Local Delegation"), false);
// One short ASCII line only — never the long policy appendix.
assert.equal(onPrompt, offPrompt + LOCAL_DELEGATION_ORIENT_LINE);
assert.match(onPrompt, /Local Delegation env is enabled\./);
assert.equal(onPrompt.includes("do not delegate"), false);
assert.equal(isSafeDeskOrientPrompt(onPrompt), true);
assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false);
assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false);
assert.match(windowsLocalDelegationCmdPrefix(true), /enabled/);
assert.match(windowsLocalDelegationCmdPrefix(false), /WORKSHOP_LOCAL_DELEGATION="&&/);
const run = ["C:\\tools\\copilot.exe", "-i", onPrompt];
const cmdLine = windowsLocalDelegationCmdPrefix(true) + run.map(quoteWindowsCmdArgument).join(" ");
assert.match(cmdLine, /^set "WORKSHOP_LOCAL_DELEGATION=enabled"&& /);
assert.match(cmdLine, /copilot\.exe/);
});
test("open notice reports effective route without savings claims", () => {
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: true,
requested: true,
availability: { available: true, routeId: "foundry-qwen25-7b-qualified" },
}), {
titleSuffix: " · Local Delegation effective",
detail: "Local Delegation effective · route foundry-qwen25-7b-qualified",
});
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: true,
warning: "local-agent-delegation skill is not installed",
availability: { available: false, reason: "local-agent-delegation skill is not installed" },
}).detail, "local-agent-delegation skill is not installed");
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: false,
}), { titleSuffix: "", detail: "" });
});
test("repo/connected argv stays orthogonal to local-delegation preference", () => {
const repo = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "repo",
pluginMcpNames: ["teams"],
});
const connected = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "connected",
pluginMcpNames: ["teams"],
});
assert.deepEqual(repo, [
"copilot", "--name", "cost-desk",
"--disable-mcp-server", "teams",
"--add-dir", "/workshop",
]);
assert.deepEqual(connected, [
"copilot", "--name", "cost-desk",
"--add-dir", "/workshop",
]);
// Local delegation never injects into argv — only env/prompt.
assert.equal(repo.includes("local"), false);
assert.equal(connected.includes("local"), false);
});
test("failed or unaccepted local work earns zero savings credit", () => {
assert.equal(localSavingsCredit({ attempted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, redone: true }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, escalated: true }).credit, 0);
const accepted = localSavingsCredit({ attempted: true, gateAccepted: true });
assert.equal(accepted.credit, 0);
assert.equal(accepted.utilization, "handled_locally_accepted");
});
test("state parse/serialize defaults to off", () => {
assert.deepEqual(parseLocalDelegationState(null), { preference: "off" });
assert.equal(parseLocalDelegationState({ preference: "ON" }).preference, "on");
const serialized = serializeLocalDelegationState({ preference: "on" });
assert.equal(serialized.preference, "on");
assert.equal(typeof serialized.updatedAt, "string");
});
test("preference path is user-local and keyed by workshop path, not the repo root", () => {
const home = join("user-home");
const a = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
const b = localDelegationPreferencePath(join("repos", "workshop-b"), {
home,
resolvePath: (p) => p,
});
const again = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
assert.match(a, /workshop-local-delegation/);
assert.equal(a.startsWith(home), true);
assert.equal(a.includes(`${sep}repos${sep}`), false);
assert.notEqual(a, b);
assert.equal(a, again);
// Repo-shipped .local-delegation.json is never the preference path.
assert.equal(a.endsWith(".local-delegation.json"), false);
// Case-sensitive filesystems: Foo and foo must not share permission state.
const upper = localDelegationPreferencePath("/work/Foo", {
home,
resolvePath: (p) => p,
});
const lower = localDelegationPreferencePath("/work/foo", {
home,
resolvePath: (p) => p,
});
assert.notEqual(upper, lower);
});
test("skill discovery respects explicit dir and common install roots", () => {
const found = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\sealed\\.github\\skills\\local-agent-delegation" },
home: "C:\\home",
exists: () => false,
isSkillDir: (p) => p === "D:\\sealed\\.github\\skills\\local-agent-delegation",
});
assert.equal(found, "D:\\sealed\\.github\\skills\\local-agent-delegation");
const missingExplicit = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\missing" },
home: "C:\\home",
exists: () => false,
isSkillDir: () => false,
});
assert.equal(missingExplicit, null);
});
test("skill discovery walks marketplace/plugin and _direct install layouts", async () => {
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const home = mkdtempSync(join(tmpdir(), "ld-skill-"));
try {
const skillDir = join(
home, ".copilot", "installed-plugins", "awesome-copilot", "sealed-delegation",
".github", "skills", "local-agent-delegation");
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, "SKILL.md"), "# local-agent-delegation\n");
const found = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(found, skillDir);
rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true });
const direct = join(
home, ".copilot", "installed-plugins", "_direct", "sealed-delegation",
"skills", "local-agent-delegation");
mkdirSync(direct, { recursive: true });
writeFileSync(join(direct, "SKILL.md"), "# local-agent-delegation\n");
const foundDirect = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(foundDirect, direct);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
@@ -1,15 +1,15 @@
{ {
"name": "signals-dashboard", "name": "signals-dashboard",
"version": "0.2.0", "version": "0.3.0",
"type": "module", "type": "module",
"main": "extension.mjs", "main": "extension.mjs",
"scripts": { "scripts": {
"test": "node --test launch-profile.test.mjs" "test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
}, },
"dependencies": { "dependencies": {
"@github/copilot-sdk": "latest" "@github/copilot-sdk": "latest"
}, },
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
"keywords": [ "keywords": [
"agent-signals", "agent-signals",
"dashboard", "dashboard",
+4 -3
View File
@@ -1,8 +1,8 @@
{ {
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "signals-dashboard", "name": "signals-dashboard",
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
"version": "0.2.0", "version": "0.3.0",
"author": { "author": {
"name": "jennyf19", "name": "jennyf19",
"url": "https://github.com/jennyf19" "url": "https://github.com/jennyf19"
@@ -12,7 +12,8 @@
"dashboard", "dashboard",
"multi-agent", "multi-agent",
"coordination", "coordination",
"canvas" "canvas",
"local-delegation"
], ],
"extensions": { "extensions": {
"com.github.copilot": { "com.github.copilot": {
+7
View File
@@ -45,6 +45,13 @@ Each desk card has two launch choices:
- **connected** — the full configured tool surface for work that needs external - **connected** — the full configured tool surface for work that needs external
systems such as issue trackers, mail, or service APIs. systems such as issue trackers, mail, or service APIs.
The summary bar also has a **Local** toggle. It is orthogonal to repo/connected:
when available and on, the frontier desk may use
[Sealed Delegation](https://github.com/jennyf19/sealed-delegation)'s
`local-agent-delegation` skill for bounded read/evidence work. Availability is
fail-closed (installed skill + qualified route receipt). Local output is a
proposal and never earns savings credit on its own.
Set `WORKSHOP_DESK_PROFILE=connected` to make the main **open** button use the 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. full tool surface; in that mode the separate **connected** button is omitted.
@@ -0,0 +1,145 @@
# 🪨 Cairn — Signals Dashboard
A live GitHub Copilot CLI **canvas extension** that shows the pulse of every
desk in your Workshop. It reads the agent signals your desks emit and renders
them as a compact, always-current dashboard in a side panel — so you can direct
the work instead of polling each desk by hand.
It replaces the old Blazor **WorkshopRoom** dashboard (`src/WorkshopRoom/`) with
a native canvas that runs inside GHCP, with no separate web app to launch.
## What it is
Each desk in the Workshop leaves signals behind — small stones on the trail —
by writing JSON files into its `.signals/` folder. Cairn scans those folders,
picks the most recent signal per desk, and renders:
- **Score bars** for the desk's self-assessment (intent, confidence, accuracy,
completeness).
- **Patterns** the desk reported: what worked ✓, what was hard △, and skill
gaps ✗.
- **Escalations** — desks that raised their hand, with what they're blocked on
and their recommendation, pinned to the top and pulsing red.
## How to open it
The dashboard is registered as the **🪨 Cairn** canvas (`signals-dashboard`).
Ask Copilot to open it and pass your workshop root as `workshopDir`:
> Open the 🪨 Cairn canvas with `workshopDir` set to the folder that contains
> my `desks/` directory.
`workshopDir` must be the **absolute path to the workshop root** — the folder
that holds `desks/` (and optionally `classroom/`). If omitted, it falls back to
the current working directory.
## Features
- **Signal scanning** — walks `desks/*/.signals/` and `classroom/*/.signals/`,
reading the newest `*.json` per desk (mirrors `SignalReader.cs`).
- **Score bars** — color-coded intent / confidence / accuracy / completeness,
scored out of 5.
- **Escalation alerts** — escalation signals sort to the very top, render with a
pulsing red border, and surface the blocker + recommendation.
- **Active desks first** — sorted escalations → recent signals → desks with no
signal yet, then by recency.
- **Stash / restore** — pause a workstream by stashing its desk. Stashed desks
drop off the active view and auto-expire after a **48-hour TTL**; restore any
time before then. Stash state lives in `.desk-stash.json` at the workshop
root.
- **Auto-refresh** — the panel refreshes every 5 seconds using a background
fetch (no full page reload), so scores and escalations stay current smoothly.
- **Summary bar** — desk count, how many are reporting vs. awaiting, an
escalation badge, and average scores across the room.
- **Cost-aware desk launch****open** starts a repo-profile desk that keeps the
verified Workshop root available while suppressing ambient plugin MCPs.
**connected** preserves every configured MCP for work that needs external
systems. Agency remains the preferred wrapper when installed.
- **Local Delegation** — orthogonal off/on control. When available and on, the
frontier desk may use the installed [`local-agent-delegation`](https://github.com/jennyf19/sealed-delegation)
skill for bounded, independently gated read/evidence work. Fail-closed: no
skill or no qualified route receipt means the toggle cannot take effect, and
no local-savings credit is awarded.
## Agent actions
The canvas also exposes actions Copilot can invoke directly:
- `refresh` — force a rescan and return current signal data as JSON.
- `stash` — stash a desk by `deskName`.
- `restore` — restore a stashed desk by `deskName`.
- `open_desk` — open a desk with optional `profile: "repo" | "connected"` and
optional `localDelegation: "off" | "on"`.
## Desk launch profiles
`repo` is the default. At launch, Cairn asks Copilot for the enabled
plugin-scoped MCP inventory and disables those ambient servers for the topic
desk. User-, workspace-, organization-, and built-in resources are left alone.
If discovery fails, Copilot plugin MCP suppression fails open; Agency repo mode
still omits Agency's own default MCPs.
When Agency is installed, Cairn keeps the existing `agency copilot` launch and
adds Agency's `--no-default-mcps` in repo mode. Outside Agency, the same profile
is applied directly to Copilot CLI.
Both profiles pass `--add-dir <workshop-root>` so a desk can intentionally read
another desk's journal or artifact without receiving access outside the room.
Set `WORKSHOP_DESK_PROFILE=connected` to retain the historical default for the
main **open** button. The separate **connected** button is always available when
repo mode is the default.
## Local Delegation
Local Delegation is **not** a third desk profile and does not replace the frontier
model. It is a separate permission bit:
```text
repo / connected = which MCPs and tools the frontier desk can see
Local Delegation off/on = whether the frontier desk may invoke a bounded local worker
```
Availability is fail-closed. Cairn enables the lane only when:
1. the `local-agent-delegation` skill is installed, and
2. a qualified route is declared via `WORKSHOP_LOCAL_DELEGATION_ROUTE_ID` or a
receipt at `~/.copilot/local-agent-runs/qualified-route.json`
(`status: "qualified"`, safe `route_id`).
Operator preference is stored **user-locally** under
`~/.copilot/workshop-local-delegation/` (keyed by the canonical workshop path),
never in the cloned workshop — a repo cannot ship `preference: on`.
When the preference is on but availability fails, opens still launch as frontier
desks and surface the reason — they never silently fall back with savings credit.
When effective, Cairn sets `WORKSHOP_LOCAL_DELEGATION=enabled` on the launched
process, shows an open toast/badge (`Local Delegation effective · route …`), and
may append one short ASCII line to `-i` (`Local Delegation env is enabled.`) when
that combined prompt stays quote-free and under the length guard. Full policy
still lives in the env flag plus the installed `local-agent-delegation` skill —
never a long multi-sentence `-i` appendix (Windows Terminal reparse).
The runtime, launcher, and gates remain owned by
[Sealed Delegation](https://github.com/jennyf19/sealed-delegation).
## Signal shape
Cairn reads the agent-signals protocol used across the Workshop:
```json
{
"signal_type": "execution",
"agent_name": "desk-name",
"self_assessment": { "intent": 5, "confidence": 4, "accuracy": 4, "completeness": 3 },
"patterns": { "what_worked": "...", "what_was_hard": "...", "skill_gap": "..." },
"escalation": { "reason": "...", "blocked_on": "...", "recommendation": "..." }
}
```
`escalation` is only present on `signal_type: "escalation"` signals.
## Replaces the Blazor WorkshopRoom
This canvas supersedes the standalone Blazor dashboard in `src/WorkshopRoom/`.
The data is the truth and the UI is just a view — Cairn renders the same signal
data natively inside GHCP, so there's no separate server to run.
@@ -5,20 +5,33 @@
import { createServer } from "node:http"; import { createServer } from "node:http";
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs"; import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
import { readdir, readFile, writeFile, stat } from "node:fs/promises"; import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
import { join, delimiter, isAbsolute, sep } from "node:path"; import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension"; import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
parsePluginMcpNames, parsePluginMcpNames,
quoteWindowsCmdArgument, quoteWindowsCmdArgument,
} from "./launch-profile.mjs"; } from "./launch-profile.mjs";
import {
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
formatLocalDelegationOpenNotice,
localDelegationPreferencePath,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
const servers = new Map(); const servers = new Map();
const STASH_TTL_MS = 48 * 60 * 60 * 1000; const STASH_TTL_MS = 48 * 60 * 60 * 1000;
@@ -60,11 +73,10 @@ function isValidDeskName(name) {
// path is then only ever passed as a spawn cwd, an argv element, or a // path is then only ever passed as a spawn cwd, an argv element, or a
// single-quoted literal inside the macOS Terminal command — never concatenated // single-quoted literal inside the macOS Terminal command — never concatenated
// raw onto a command line — so no character filtering of the path is required. // raw onto a command line — so no character filtering of the path is required.
function deskOrientPrompt(deskName) { //
return `You are sitting down at the ${deskName} desk in this workshop. ` + // Local Delegation is orthogonal to repo/connected: it never changes the tool
`Read journal.md in this folder first to pick up where the last session ` + // surface. When effective, only the orientation prompt and child env mark that
`left off, then continue the desk's work. Write your journal before you stop.`; // the frontier desk may use the installed local-agent-delegation skill.
}
// Spawn detached and resolve true only once the OS confirms the process // Spawn detached and resolve true only once the OS confirms the process
// started ('spawn'), false on failure ('error', e.g. the binary is missing) so // started ('spawn'), false on failure ('error', e.g. the binary is missing) so
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
} catch { return false; } } catch { return false; }
} }
async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFAULT_DESK_PROFILE) { function preferenceStatePath(workshopDir) {
return localDelegationPreferencePath(workshopDir, {
resolvePath: (p) => {
try { return realpathSync(p); } catch { return p; }
},
});
}
async function readLocalDelegationPreference(workshopDir) {
// Never read preference from the workshop repo — a clone can ship
// preference:on. Only user-local state (or explicit env) counts.
try {
const raw = JSON.parse(await readFile(preferenceStatePath(workshopDir), "utf8"));
return parseLocalDelegationState(raw).preference;
} catch {
return normalizeLocalDelegationPreference(
process.env.WORKSHOP_LOCAL_DELEGATION_PREFERENCE, "off");
}
}
async function writeLocalDelegationPreference(workshopDir, preference) {
const state = serializeLocalDelegationState({ preference });
const target = preferenceStatePath(workshopDir);
await mkdir(dirname(target), { recursive: true });
// Atomic replace in the user-local dir (temp + rename).
const tmp = join(
dirname(target),
`.pref.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
const body = JSON.stringify(state, null, 2) + "\n";
try {
await writeFile(tmp, body, { encoding: "utf8", flag: "wx" });
try {
await rename(tmp, target);
} catch {
await unlink(target).catch(() => {});
await rename(tmp, target);
}
} catch (err) {
await unlink(tmp).catch(() => {});
throw err;
}
return state;
}
function currentLocalDelegationLaunch(preference) {
const availability = resolveLocalDelegationAvailability();
return resolveLocalDelegationLaunch({ preference, availability });
}
async function launchDeskConsole(
deskPath,
deskName,
workshopDir,
profile = DEFAULT_DESK_PROFILE,
localDelegation = { effective: false },
) {
// deskName must be a plain slug so it is safe on every command line and shell // 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 // below, and the resolved desk must still live inside the workshop root
// (which defeats a symlinked desk that escapes the repo). deskPath itself is // (which defeats a symlinked desk that escapes the repo). deskPath itself is
@@ -367,24 +434,27 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
if (!isInsideRoot(workshopDir, deskPath)) return false; if (!isInsideRoot(workshopDir, deskPath)) return false;
const agent = await deskAgentArgv(deskName, workshopDir, profile); const agent = await deskAgentArgv(deskName, workshopDir, profile);
if (!agent) return false; if (!agent) return false;
const run = [...agent, "-i", deskOrientPrompt(deskName)]; const effective = Boolean(localDelegation?.effective);
const run = [...agent, "-i", deskOrientPrompt(deskName, { localDelegationEffective: effective })];
const env = buildLocalDelegationLaunchEnv(process.env, { localDelegationEffective: effective });
if (process.platform === "win32") { if (process.platform === "win32") {
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir }); const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
const cmd = resolveSystem32Executable("cmd.exe"); const cmd = resolveSystem32Executable("cmd.exe");
const direct = /\.(exe|com)$/i.test(run[0]); // wt.exe does not reliably forward Node's spawn env into a new tab when
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true; // Windows Terminal is already running. Always start through cmd.exe and
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every // Args are quoteWindowsCmdArgument'd, so only block expanders that still
// argument is free of cmd metacharacters; otherwise fail closed and let // fire inside quotes (% and !) — allow parentheses in workshop paths.
// the UI copy the desk path rather than reparse an unsafe workshop path. const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg)); if (!cmdSafe || !cmd) return false;
if (cmdSafe && wt && cmd && const inner = windowsLocalDelegationCmdPrefix(effective)
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true; + run.map(quoteWindowsCmdArgument).join(" ");
// Fallback when wt.exe is absent: a fresh console window via `start`, if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
// still through cmd /k only when the arguments are safe for reparsing. return true;
return cmdSafe && cmd }
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath }) // Fallback when wt.exe is absent: a fresh console window via `start`.
: false; return await trySpawn(
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
} }
if (process.platform === "darwin") { if (process.platform === "darwin") {
const osascript = "/usr/bin/osascript"; const osascript = "/usr/bin/osascript";
@@ -393,13 +463,18 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
// to cd into the desk and exec the agent. Each argv element is POSIX // 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 // single-quoted so the shell can't reinterpret it, and osascript itself
// is spawned via argv (no shell). // is spawned via argv (no shell).
const line = "cd " + shSingleQuote(deskPath) + " && exec " + // Local-delegation env is exported in-line so the Terminal session sees it
// without inheriting a polluted parent shell forever.
const envPrefix = effective
? "export WORKSHOP_LOCAL_DELEGATION=enabled; "
: "unset WORKSHOP_LOCAL_DELEGATION; ";
const line = "cd " + shSingleQuote(deskPath) + " && " + envPrefix + "exec " +
run.map(shSingleQuote).join(" "); run.map(shSingleQuote).join(" ");
const script = 'tell application "Terminal"\n' + const script = 'tell application "Terminal"\n' +
" activate\n" + " activate\n" +
" do script " + osaStringLiteral(line) + "\n" + " do script " + osaStringLiteral(line) + "\n" +
"end tell"; "end tell";
return await trySpawn(osascript, ["-e", script]); return await trySpawn(osascript, ["-e", script], { env });
} }
// Linux/other: best-effort across common terminal emulators. Each is spawned // 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, // via argv (no shell) with the agent command after the emulator's exec flag,
@@ -412,7 +487,7 @@ async function launchDeskConsole(deskPath, deskName, workshopDir, profile = DEFA
]; ];
for (const [term, args] of linuxTerms) { for (const [term, args] of linuxTerms) {
const executable = resolveOnPath(term, { excludedRoot: workshopDir }); const executable = resolveOnPath(term, { excludedRoot: workshopDir });
if (executable && await trySpawn(executable, args, { cwd: deskPath })) return true; if (executable && await trySpawn(executable, args, { cwd: deskPath, env })) return true;
} }
return false; return false;
} }
@@ -728,7 +803,42 @@ function avgScore(signals) {
return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") }; return { confidence: avg("confidence"), accuracy: avg("accuracy"), completeness: avg("completeness"), intent: avg("intentScore") };
} }
function renderSummaryBar(activeSignals) { function renderLocalDelegationControl(localDelegation) {
const pref = localDelegation?.preference || "off";
const available = Boolean(localDelegation?.availability?.available);
const effective = Boolean(localDelegation?.effective);
const reason = localDelegation?.availability?.reason || "Local Delegation unavailable";
const routeId = localDelegation?.availability?.routeId || null;
const next = pref === "on" ? "off" : "on";
const label = effective ? "On" : (pref === "on" ? "On*" : "Off");
const color = effective ? "#86efac" : (pref === "on" ? "#fbbf24" : "#94a3b8");
const border = effective ? "#166534" : (pref === "on" ? "#854d0e" : "#334155");
const title = available
? (effective
? `Local Delegation effective${routeId ? ` · route ${routeId}` : ""}`
: "Local Delegation available but currently off")
: reason;
const note = effective && routeId
? `<span style="font-size:10px;color:#86efac;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(title)}">effective · ${esc(truncate(routeId, 28))}</span>`
: !available
? `<span style="font-size:10px;color:#64748b;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(reason)}">${esc(truncate(reason, 48))}</span>`
: (pref === "on" && !effective
? `<span style="font-size:10px;color:#fbbf24;">requested, unavailable</span>`
: "");
return `
<div style="display:flex;align-items:center;gap:6px;" title="${esc(title)}">
<span style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.04em;">Local</span>
<button data-act="local-delegation" data-preference="${esc(next)}"
aria-label="Local Delegation ${esc(label)}${effective && routeId ? ` route ${esc(routeId)}` : ""}"
aria-pressed="${pref === "on" ? "true" : "false"}"
style="background:#020617;border:1px solid ${border};color:${color};padding:2px 8px;border-radius:999px;
font-size:11px;cursor:pointer;font-weight:600;min-width:42px;"
title="${esc(title)}">${esc(label)}</button>
${note}
</div>`;
}
function renderSummaryBar(activeSignals, localDelegation) {
const escalations = activeSignals.filter(s => s.signalType === "escalation").length; const escalations = activeSignals.filter(s => s.signalType === "escalation").length;
const withSignals = activeSignals.filter(s => s.signalType !== "none").length; const withSignals = activeSignals.filter(s => s.signalType !== "none").length;
const awaiting = activeSignals.filter(s => s.signalType === "none").length; const awaiting = activeSignals.filter(s => s.signalType === "none").length;
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
return ` return `
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px; <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 14px;
background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;"> background:#0f172a;border:1px solid #1e293b;border-radius:8px;margin-bottom:14px;gap:12px;flex-wrap:wrap;">
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span> <span style="font-size:13px;color:#cbd5e1;"><b style="color:#f1f5f9;">${activeSignals.length}</b> desk${activeSignals.length !== 1 ? "s" : ""}</span>
<span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span> <span style="font-size:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
${tokenBadge} ${tokenBadge}
${calibrationBadge} ${calibrationBadge}
${escBadge} ${escBadge}
${renderLocalDelegationControl(localDelegation)}
</div> </div>
${avgBlock} ${avgBlock}
</div>`; </div>`;
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
</div>`; </div>`;
} }
function renderDashboard(signals, stashed, capabilityToken) { function renderDashboard(signals, stashed, capabilityToken, localDelegation) {
const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName))); const activeSignals = sortSignals(signals.filter(s => !stashed.some(e => e.name === s.deskName)));
const localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
const cards = activeSignals.length > 0 const cards = activeSignals.length > 0
? activeSignals.map(renderSignalCard).join("") ? activeSignals.map(renderSignalCard).join("")
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
</div> </div>
</div>`; </div>`;
const summaryBar = activeSignals.length > 0 ? renderSummaryBar(activeSignals) : ""; // Always show the Local Delegation control so operators can see availability
// even before the first desk signal arrives.
const summaryBar = renderSummaryBar(activeSignals, localDelegationState);
const stashedSection = stashed.length > 0 ? ` const stashedSection = stashed.length > 0 ? `
<div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;"> <div style="margin-top:20px;padding-top:12px;border-top:1px solid #1a1a1a;">
@@ -1058,27 +1172,55 @@ function renderDashboard(signals, stashed, capabilityToken) {
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
const path = data.deskPath || name; const path = data.deskPath || name;
const notice = data.localDelegationNotice || {};
const localTitle = notice.titleSuffix || '';
const localDetail = notice.detail || '';
if (data.launched) { if (data.launched) {
// A successful open shouldn't hijack the user's clipboard. // A successful open shouldn't hijack the user's clipboard.
showToast('opening ' + name + ' desk (' + selectedProfile + ')…', path); // Surface LD state in the toast — operators cannot rely on -i alone.
showToast('opening ' + name + ' desk (' + selectedProfile + localTitle + ')…',
localDetail || path);
} else { } else {
// No terminal launched from here, so copy the path as the // No terminal launched from here, so copy the path as the
// fallback handle, but only claim the copy when it actually // fallback handle, but only claim the copy when it actually
// succeeded. The path shows in the toast either way. // succeeded. The path shows in the toast either way.
let copied = false; let copied = false;
try { await navigator.clipboard.writeText(path); copied = true; } catch {} try { await navigator.clipboard.writeText(path); copied = true; } catch {}
showToast(copied ? (name + ' · path copied') : (name + ' · copy this path'), path); const copyTitle = copied ? (name + ' · path copied') : (name + ' · copy this path');
showToast(copyTitle + localTitle, localDetail || path);
} }
} else { } else {
showToast(name + ' · not found', ''); showToast(name + ' · not found', '');
} }
} }
async function setLocalDelegation(preference) {
const res = await fetch('/api/local-delegation?preference=' +
encodeURIComponent(preference || 'off'), POST_OPTS);
const data = await res.json();
if (data.ok) {
const ld = data.localDelegation || {};
const routeId = ld.availability && ld.availability.routeId;
const label = ld.effective
? ('Local Delegation effective' + (routeId ? (' · route ' + routeId) : ''))
: (ld.preference === 'on'
? 'Local Delegation requested (unavailable)'
: 'Local Delegation off');
showToast(label, ld.availability?.reason || '');
refresh();
} else {
showToast('Local Delegation · not updated', data.error || '');
}
}
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-act]'); const btn = e.target.closest('button[data-act]');
if (!btn) return; if (!btn) return;
const act = btn.getAttribute('data-act');
if (act === 'local-delegation') {
setLocalDelegation(btn.getAttribute('data-preference') || 'off');
return;
}
const name = btn.getAttribute('data-desk'); const name = btn.getAttribute('data-desk');
if (!name) return; if (!name) return;
const act = btn.getAttribute('data-act');
const profile = btn.getAttribute('data-profile'); const profile = btn.getAttribute('data-profile');
if (act === 'stash') stashDesk(name); if (act === 'stash') stashDesk(name);
else if (act === 'restore') restoreDesk(name); else if (act === 'restore') restoreDesk(name);
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
active.getAttribute('data-act'), active.getAttribute('data-act'),
active.getAttribute('data-desk'), active.getAttribute('data-desk'),
active.getAttribute('data-profile') || '', active.getAttribute('data-profile') || '',
active.getAttribute('data-preference') || '',
]); ]);
} }
content.innerHTML = newContent.innerHTML; content.innerHTML = newContent.innerHTML;
if (focusKey) { if (focusKey) {
const [act, desk, profile] = JSON.parse(focusKey); const [act, desk, profile, preference] = JSON.parse(focusKey);
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk; let target = null;
const profileSelector = profile if (act === 'local-delegation') {
? '[data-profile="' + profile + '"]' target = content.querySelector('button[data-act="local-delegation"]');
: ':not([data-profile])'; } else {
const target = content.querySelector( const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' + const profileSelector = profile
profileSelector); ? '[data-profile="' + profile + '"]'
: ':not([data-profile])';
target = content.querySelector(
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
profileSelector);
}
if (target) target.focus(); if (target) target.focus();
void preference;
} }
} }
} catch {} } catch {}
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
res.end(JSON.stringify({ ok: true })); res.end(JSON.stringify({ ok: true }));
return; return;
} }
if (req.method === "POST" && url.pathname === "/api/local-delegation") {
const preferenceInput = url.searchParams.get("preference") || "off";
if (!["off", "on"].includes(String(preferenceInput).toLowerCase())) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid local delegation preference" }));
return;
}
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
await writeLocalDelegationPreference(workshopDir, preference);
const localDelegation = currentLocalDelegationLaunch(preference);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, localDelegation }));
return;
}
if (req.method === "POST" && url.pathname.startsWith("/api/open/")) { if (req.method === "POST" && url.pathname.startsWith("/api/open/")) {
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]); const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE; const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
return; return;
} }
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(workshopDir, subdir, deskName); const deskPath = join(workshopDir, subdir, deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile); const launched = await launchDeskConsole(
deskPath, deskName, workshopDir, profile, localDelegation);
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, deskName, deskPath, launched, profile })); res.end(JSON.stringify({
ok: true,
deskName,
deskPath,
launched,
profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}));
return; return;
} }
} catch {} } catch {}
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
const signals = await scanSignals(workshopDir); const signals = await scanSignals(workshopDir);
const stashed = await readStash(workshopDir); const stashed = await readStash(workshopDir);
const preference = await readLocalDelegationPreference(workshopDir);
const localDelegation = currentLocalDelegationLaunch(preference);
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderDashboard(signals, stashed, capabilityToken)); res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
} catch (err) { } catch (err) {
// Top-level boundary: never leave a request hanging or let a // Top-level boundary: never leave a request hanging or let a
// rejection become an unhandled crash — e.g. malformed %-encoding // rejection become an unhandled crash — e.g. malformed %-encoding
@@ -1327,7 +1503,7 @@ const session = await joinSession({
}, },
{ {
name: "open_desk", name: "open_desk",
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.", description: "Open a desk as an in-place Copilot CLI session. Repo profile suppresses ambient plugin MCPs; connected keeps every configured tool. Local Delegation is orthogonal and fail-closed: when available and preferred on, the frontier desk may use sealed local-agent-delegation for bounded read/evidence work. Returns the desk path, profile, localDelegation state, and whether a terminal was launched.",
inputSchema: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
@@ -1337,6 +1513,11 @@ const session = await joinSession({
enum: ["repo", "connected"], enum: ["repo", "connected"],
description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`, description: `Tool profile. Defaults to ${DEFAULT_DESK_PROFILE}.`,
}, },
localDelegation: {
type: "string",
enum: ["off", "on"],
description: "Optional Local Delegation preference for this launch. Defaults to the workshop Cairn toggle (.local-delegation.json).",
},
}, },
required: ["deskName"], required: ["deskName"],
}, },
@@ -1347,13 +1528,21 @@ const session = await joinSession({
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE; const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" }; if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
const profile = normalizeDeskProfile(profileInput); const profile = normalizeDeskProfile(profileInput);
const preferenceInput = ctx.input.localDelegation
?? await readLocalDelegationPreference(entry.workshopDir);
const preference = normalizeLocalDelegationPreference(preferenceInput, "off");
const localDelegation = currentLocalDelegationLaunch(preference);
for (const subdir of ["desks", "classroom"]) { for (const subdir of ["desks", "classroom"]) {
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName); const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
try { try {
const s = await stat(deskPath); const s = await stat(deskPath);
if (s.isDirectory()) { if (s.isDirectory()) {
const launched = await launchDeskConsole( const launched = await launchDeskConsole(
deskPath, ctx.input.deskName, entry.workshopDir, profile); deskPath,
ctx.input.deskName,
entry.workshopDir,
profile,
localDelegation);
return { return {
ok: true, ok: true,
deskName: ctx.input.deskName, deskName: ctx.input.deskName,
@@ -1361,6 +1550,8 @@ const session = await joinSession({
launched, launched,
workshopDir: entry.workshopDir, workshopDir: entry.workshopDir,
profile, profile,
localDelegation,
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
}; };
} }
} catch {} } catch {}
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(value) {
return typeof value === "string" && !/[%\r\n]/.test(value); return typeof value === "string" && !/[%\r\n]/.test(value);
} }
/**
* Args that will be wrapped with quoteWindowsCmdArgument. Inside double quotes,
* cmd still expands %VAR% and !VAR! (delayed expansion). Other metacharacters
* like & | < > ^ ( ) are literal when quoted, so paths such as
* C:\Work\Project (1) must be allowed.
*/
export function isSafeQuotedWindowsCmdArg(value) {
return typeof value === "string" && !/[%!\r\n]/.test(value);
}
export function parsePluginMcpNames(text) { export function parsePluginMcpNames(text) {
let parsed; let parsed;
try { parsed = JSON.parse(text); } try { parsed = JSON.parse(text); }
@@ -7,6 +7,7 @@ import { join } from "node:path";
import { import {
buildDeskAgentArgv, buildDeskAgentArgv,
isDeskProfile, isDeskProfile,
isSafeQuotedWindowsCmdArg,
isSafeWindowsCmdShim, isSafeWindowsCmdShim,
isWindowsAppExecutionAlias, isWindowsAppExecutionAlias,
normalizeDeskProfile, normalizeDeskProfile,
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\""); assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true); assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false); assert.equal(isSafeWindowsCmdShim("C:\\Users\\%USERNAME%\\agency.cmd"), false);
// Quoted args may contain parentheses (common workshop folders).
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Work\\Project (1)"), true);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\%USERNAME%\\w"), false);
assert.equal(isSafeQuotedWindowsCmdArg("C:\\Users\\!DELAY!\\w"), false);
}); });
test("executes a Windows cmd shim with safe quoting", { test("executes a Windows cmd shim with safe quoting", {
@@ -0,0 +1,401 @@
import { createHash } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const PREFERENCES = new Set(["off", "on"]);
const SAFE_ROUTE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
/** @deprecated Repo-root state is rejected; kept only for docs/migration mentions. */
export const LOCAL_DELEGATION_STATE_FILE = ".local-delegation.json";
export const LOCAL_DELEGATION_ENV = "WORKSHOP_LOCAL_DELEGATION";
export const LOCAL_DELEGATION_SKILL_NAME = "local-agent-delegation";
export const LOCAL_DELEGATION_USER_STATE_DIR = join(".copilot", "workshop-local-delegation");
/**
* Stable user-local preference path for a workshop root.
* Permission state must NOT live in the cloned workshop (a repo can ship
* preference:on). Key by a hash of the canonical workshop path under ~/.copilot.
*/
export function localDelegationPreferencePath(workshopDir, {
home = homedir(),
resolvePath = (p) => p,
} = {}) {
if (typeof workshopDir !== "string" || !workshopDir) {
throw new Error("workshopDir is required");
}
let canonical = workshopDir;
try { canonical = resolvePath(workshopDir); } catch { /* keep input */ }
// Normalize separators only. Do not lowercase: on case-sensitive filesystems
// /work/Foo and /work/foo are distinct workshops and must not share state.
const key = createHash("sha256")
.update(String(canonical).replaceAll("\\", "/"))
.digest("hex")
.slice(0, 32);
return join(home, LOCAL_DELEGATION_USER_STATE_DIR, `${key}.json`);
}
export function isLocalDelegationPreference(value) {
return typeof value === "string" && PREFERENCES.has(value.toLowerCase());
}
export function normalizeLocalDelegationPreference(value, fallback = "off") {
return isLocalDelegationPreference(value) ? value.toLowerCase() : fallback;
}
// Windows Terminal / cmd reparse cannot safely carry long multi-space -i strings.
// Keep the orientation prompt short, ASCII, and quote-free. Local Delegation
// policy lives in WORKSHOP_LOCAL_DELEGATION=enabled + the installed skill — not
// on the CLI.
const MAX_ORIENT_PROMPT_CHARS = 280;
// Allow apostrophes (desk's). Ban double quotes, backticks, dashes that WT/cmd
// have split on, and classic cmd metacharacters.
const UNSAFE_ORIENT_CHARS = /["`—–|&<>^%!()\r\n]/;
export function isSafeDeskOrientPrompt(prompt) {
return typeof prompt === "string"
&& prompt.length > 0
&& prompt.length <= MAX_ORIENT_PROMPT_CHARS
&& !UNSAFE_ORIENT_CHARS.test(prompt);
}
/** One short ASCII notice operators can see in the session start prompt. */
export const LOCAL_DELEGATION_ORIENT_LINE = " Local Delegation env is enabled.";
export function deskOrientPrompt(deskName, { localDelegationEffective = false } = {}) {
// deskName is already constrained to a slug by the launcher; still keep the
// prompt free of punctuation that cmd/wt have historically mis-parsed.
let prompt = `You are sitting down at the ${deskName} desk in this workshop. ` +
`Read journal.md in this folder first to pick up where the last session ` +
`left off, then continue the desk's work. Write your journal before you stop.`;
// Only a single short ASCII line may ride on -i. Full policy stays in env + skill.
if (localDelegationEffective) {
const withNotice = prompt + LOCAL_DELEGATION_ORIENT_LINE;
if (isSafeDeskOrientPrompt(withNotice)) prompt = withNotice;
}
return prompt;
}
/**
* Operator-visible summary for open toasts and badges.
* Never claims savings; only reports effective state + route id when known.
*/
export function formatLocalDelegationOpenNotice(localDelegation) {
if (!localDelegation || typeof localDelegation !== "object") {
return { titleSuffix: "", detail: "" };
}
const routeId = localDelegation.availability?.routeId || null;
if (localDelegation.effective) {
const routePart = routeId ? ` · route ${routeId}` : "";
return {
titleSuffix: " · Local Delegation effective",
detail: `Local Delegation effective${routePart}`,
};
}
if (localDelegation.requested) {
return {
titleSuffix: " · local unavailable",
detail: localDelegation.warning
|| localDelegation.availability?.reason
|| "Local Delegation requested but unavailable",
};
}
return { titleSuffix: "", detail: "" };
}
export function buildLocalDelegationLaunchEnv(baseEnv = {}, { localDelegationEffective = false } = {}) {
const env = { ...baseEnv };
// Windows env names are case-insensitive; spreading process.env yields a
// case-sensitive object, so clear every spelling before optionally setting.
const target = LOCAL_DELEGATION_ENV.toLowerCase();
for (const key of Object.keys(env)) {
if (key.toLowerCase() === target) delete env[key];
}
if (localDelegationEffective) {
env[LOCAL_DELEGATION_ENV] = "enabled";
}
return env;
}
/**
* cmd.exe prefix that forces WORKSHOP_LOCAL_DELEGATION on or off inside a new
* Windows Terminal / console session. wt.exe does not reliably forward the
* caller's process env into a new tab when Terminal is already running.
*/
export function windowsLocalDelegationCmdPrefix(localDelegationEffective = false) {
return localDelegationEffective
? 'set "WORKSHOP_LOCAL_DELEGATION=enabled"&& '
: 'set "WORKSHOP_LOCAL_DELEGATION="&& ';
}
/**
* Savings credit is utilization accounting, not a price claim.
* Failed, unaccepted, redone, or escalated local work earns zero.
*/
export function localSavingsCredit({
attempted = false,
gateAccepted = false,
redone = false,
escalated = false,
} = {}) {
if (!attempted || !gateAccepted || redone || escalated) {
return {
credit: 0,
utilization: attempted ? "handled_locally_unaccepted" : "not_attempted",
reason: !attempted
? "not_attempted"
: escalated
? "escalated"
: redone
? "redone"
: "gate_not_accepted",
};
}
return {
credit: 0, // dollar savings are never claimed by Cairn
utilization: "handled_locally_accepted",
reason: "accepted_utilization_only",
};
}
function isReadableFile(path) {
try {
if (!statSync(path).isFile()) return false;
accessSync(path, fsConstants.R_OK);
return true;
} catch {
return false;
}
}
function skillMarkerPath(dir) {
return join(dir, "SKILL.md");
}
function looksLikeSkillDir(dir) {
return isReadableFile(skillMarkerPath(dir));
}
/**
* Discover the installed local-agent-delegation skill.
* Injectable probes keep unit tests filesystem-free.
*/
export function findLocalDelegationSkillDir({
env = process.env,
home = homedir(),
exists = existsSync,
isSkillDir = looksLikeSkillDir,
} = {}) {
const explicit = (env.WORKSHOP_LOCAL_DELEGATION_SKILL_DIR || "").trim();
if (explicit) {
return isSkillDir(explicit) ? explicit : null;
}
const candidates = [
join(home, ".copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(home, ".agents", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const candidate of candidates) {
if (isSkillDir(candidate)) return candidate;
}
// Marketplace: ~/.copilot/installed-plugins/<marketplace>/<plugin>/
// Direct: ~/.copilot/installed-plugins/_direct/<plugin>/
// Skill dirs may live at skills/, .github/skills/, or com.github.copilot/skills/.
const pluginsRoot = join(home, ".copilot", "installed-plugins");
if (exists(pluginsRoot)) {
try {
for (const market of readdirSync(pluginsRoot, { withFileTypes: true })) {
if (!market.isDirectory()) continue;
const marketRoot = join(pluginsRoot, market.name);
let pluginEntries;
try {
pluginEntries = readdirSync(marketRoot, { withFileTypes: true });
} catch {
continue;
}
for (const plugin of pluginEntries) {
if (!plugin.isDirectory()) continue;
const pluginRoot = join(marketRoot, plugin.name);
const nestedCandidates = [
join(pluginRoot, "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, ".github", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
join(pluginRoot, "com.github.awesome-copilot", "skills", LOCAL_DELEGATION_SKILL_NAME),
];
for (const nested of nestedCandidates) {
if (isSkillDir(nested)) return nested;
}
}
}
} catch {
// fail closed on scan errors
}
}
return null;
}
function readJsonFile(path, readFile = readFileSync) {
try {
return JSON.parse(readFile(path, "utf8"));
} catch {
return null;
}
}
function isRouteId(value) {
return typeof value === "string" && SAFE_ROUTE_ID.test(value);
}
/**
* Fail-closed availability. Enable only when skill + qualified route receipt
* (or explicit env route id) are present. Never invent availability.
*/
export function resolveLocalDelegationAvailability({
env = process.env,
home = homedir(),
now = Date.now(),
findSkill = findLocalDelegationSkillDir,
readFile = readFileSync,
exists = existsSync,
} = {}) {
const forced = (env.WORKSHOP_LOCAL_DELEGATION_AVAILABLE || "").trim().toLowerCase();
if (forced === "0" || forced === "false" || forced === "unavailable") {
return {
available: false,
reason: "Forced unavailable by WORKSHOP_LOCAL_DELEGATION_AVAILABLE",
skillDir: null,
routeId: null,
};
}
const skillDir = findSkill({ env, home, exists });
if (!skillDir) {
return {
available: false,
reason: "local-agent-delegation skill is not installed",
skillDir: null,
routeId: null,
};
}
const envRoute = (env.WORKSHOP_LOCAL_DELEGATION_ROUTE_ID || "").trim();
if (envRoute) {
if (!isRouteId(envRoute)) {
return {
available: false,
reason: "WORKSHOP_LOCAL_DELEGATION_ROUTE_ID is not a safe route id",
skillDir,
routeId: null,
};
}
return {
available: true,
reason: "Skill installed; route id provided by environment",
skillDir,
routeId: envRoute,
};
}
const receiptPath = (env.WORKSHOP_LOCAL_DELEGATION_RECEIPT || "").trim()
|| join(home, ".copilot", "local-agent-runs", "qualified-route.json");
if (!exists(receiptPath)) {
return {
available: false,
reason: "No qualified route receipt (set WORKSHOP_LOCAL_DELEGATION_ROUTE_ID or write ~/.copilot/local-agent-runs/qualified-route.json)",
skillDir,
routeId: null,
};
}
const receipt = readJsonFile(receiptPath, readFile);
if (!receipt || typeof receipt !== "object") {
return {
available: false,
reason: "Qualified route receipt is unreadable",
skillDir,
routeId: null,
};
}
const status = String(receipt.status || "").toLowerCase();
if (status !== "qualified") {
return {
available: false,
reason: `Route receipt status is '${receipt.status || "missing"}', not qualified`,
skillDir,
routeId: isRouteId(receipt.route_id || receipt.routeId) ? (receipt.route_id || receipt.routeId) : null,
};
}
const routeId = receipt.route_id || receipt.routeId || null;
if (!isRouteId(routeId)) {
return {
available: false,
reason: "Route receipt is missing a safe route_id",
skillDir,
routeId: null,
};
}
if (receipt.expires_at || receipt.expiresAt) {
const expires = Date.parse(receipt.expires_at || receipt.expiresAt);
if (!Number.isFinite(expires) || expires <= now) {
return {
available: false,
reason: "Qualified route receipt has expired",
skillDir,
routeId,
};
}
}
return {
available: true,
reason: "Skill installed; qualified route receipt present",
skillDir,
routeId,
};
}
export function resolveLocalDelegationLaunch({
preference = "off",
availability,
} = {}) {
const pref = normalizeLocalDelegationPreference(preference, "off");
const available = Boolean(availability?.available);
if (pref === "on" && !available) {
return {
preference: pref,
requested: true,
effective: false,
availability,
warning: availability?.reason || "Local Delegation unavailable",
};
}
return {
preference: pref,
requested: pref === "on",
effective: pref === "on" && available,
availability,
warning: null,
};
}
export function parseLocalDelegationState(raw) {
if (!raw || typeof raw !== "object") {
return { preference: "off" };
}
return {
preference: normalizeLocalDelegationPreference(raw.preference, "off"),
};
}
export function serializeLocalDelegationState(state) {
return {
preference: normalizeLocalDelegationPreference(state?.preference, "off"),
updatedAt: new Date().toISOString(),
};
}
@@ -0,0 +1,298 @@
import test from "node:test";
import assert from "node:assert/strict";
import { join, sep } from "node:path";
import {
LOCAL_DELEGATION_ORIENT_LINE,
buildLocalDelegationLaunchEnv,
deskOrientPrompt,
findLocalDelegationSkillDir,
formatLocalDelegationOpenNotice,
isLocalDelegationPreference,
isSafeDeskOrientPrompt,
localDelegationPreferencePath,
localSavingsCredit,
normalizeLocalDelegationPreference,
parseLocalDelegationState,
resolveLocalDelegationAvailability,
resolveLocalDelegationLaunch,
serializeLocalDelegationState,
windowsLocalDelegationCmdPrefix,
} from "./local-delegation.mjs";
import {
buildDeskAgentArgv,
normalizeDeskProfile,
quoteWindowsCmdArgument,
} from "./launch-profile.mjs";
test("normalizes local-delegation preference independently of desk profile", () => {
assert.equal(isLocalDelegationPreference("on"), true);
assert.equal(isLocalDelegationPreference("OFF"), true);
assert.equal(isLocalDelegationPreference("maybe"), false);
assert.equal(normalizeLocalDelegationPreference("ON"), "on");
assert.equal(normalizeLocalDelegationPreference("nope"), "off");
// Profile axis remains orthogonal and untouched.
assert.equal(normalizeDeskProfile("connected"), "connected");
assert.equal(normalizeDeskProfile("repo"), "repo");
});
test("availability is fail-closed without skill or route receipt", () => {
const missingSkill = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => null,
exists: () => false,
});
assert.equal(missingSkill.available, false);
assert.match(missingSkill.reason, /not installed/i);
const skillOnly = resolveLocalDelegationAvailability({
env: {},
home: "C:\\home",
findSkill: () => "C:\\home\\.copilot\\skills\\local-agent-delegation",
exists: () => false,
});
assert.equal(skillOnly.available, false);
assert.match(skillOnly.reason, /No qualified route receipt/i);
});
test("availability accepts env route id or a qualified receipt", () => {
const home = join("home");
const skillDir = join("skills", "local-agent-delegation");
const viaEnv = resolveLocalDelegationAvailability({
env: { WORKSHOP_LOCAL_DELEGATION_ROUTE_ID: "foundry-qwen25-7b-qualified" },
home,
findSkill: () => skillDir,
exists: () => false,
});
assert.equal(viaEnv.available, true);
assert.equal(viaEnv.routeId, "foundry-qwen25-7b-qualified");
// Match host path.join separators so Linux CI exercises the receipt branch.
const receiptPath = join(home, ".copilot", "local-agent-runs", "qualified-route.json");
const viaReceipt = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2026-08-14T12:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(viaReceipt.available, true);
assert.equal(viaReceipt.routeId, "foundry-qwen25-7b-qualified");
const expired = resolveLocalDelegationAvailability({
env: {},
home,
now: Date.parse("2027-01-01T00:00:00Z"),
findSkill: () => skillDir,
exists: (p) => p === receiptPath,
readFile: () => JSON.stringify({
status: "qualified",
route_id: "foundry-qwen25-7b-qualified",
expires_at: "2026-12-01T00:00:00Z",
}),
});
assert.equal(expired.available, false);
assert.match(expired.reason, /expired/i);
});
test("requested on + unavailable stays ineffective with a warning", () => {
const launch = resolveLocalDelegationLaunch({
preference: "on",
availability: {
available: false,
reason: "local-agent-delegation skill is not installed",
},
});
assert.equal(launch.requested, true);
assert.equal(launch.effective, false);
assert.match(launch.warning, /not installed/i);
});
test("launch env enables when effective; -i prompt stays short and quote-free", () => {
const base = {
PATH: "/usr/bin",
WORKSHOP_LOCAL_DELEGATION: "enabled",
workshop_local_delegation: "enabled",
};
const offEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: false });
assert.equal(Object.hasOwn(offEnv, "WORKSHOP_LOCAL_DELEGATION"), false);
assert.equal(Object.hasOwn(offEnv, "workshop_local_delegation"), false);
assert.equal(offEnv.PATH, "/usr/bin");
const onEnv = buildLocalDelegationLaunchEnv(base, { localDelegationEffective: true });
assert.equal(onEnv.WORKSHOP_LOCAL_DELEGATION, "enabled");
assert.equal(Object.hasOwn(onEnv, "workshop_local_delegation"), false);
const offPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: false });
const onPrompt = deskOrientPrompt("cost-desk", { localDelegationEffective: true });
assert.match(offPrompt, /cost-desk/);
assert.equal(offPrompt.includes("Local Delegation"), false);
// One short ASCII line only — never the long policy appendix.
assert.equal(onPrompt, offPrompt + LOCAL_DELEGATION_ORIENT_LINE);
assert.match(onPrompt, /Local Delegation env is enabled\./);
assert.equal(onPrompt.includes("do not delegate"), false);
assert.equal(isSafeDeskOrientPrompt(onPrompt), true);
assert.equal(isSafeDeskOrientPrompt('bad "quote"'), false);
assert.equal(isSafeDeskOrientPrompt("em dash — bad"), false);
assert.match(windowsLocalDelegationCmdPrefix(true), /enabled/);
assert.match(windowsLocalDelegationCmdPrefix(false), /WORKSHOP_LOCAL_DELEGATION="&&/);
const run = ["C:\\tools\\copilot.exe", "-i", onPrompt];
const cmdLine = windowsLocalDelegationCmdPrefix(true) + run.map(quoteWindowsCmdArgument).join(" ");
assert.match(cmdLine, /^set "WORKSHOP_LOCAL_DELEGATION=enabled"&& /);
assert.match(cmdLine, /copilot\.exe/);
});
test("open notice reports effective route without savings claims", () => {
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: true,
requested: true,
availability: { available: true, routeId: "foundry-qwen25-7b-qualified" },
}), {
titleSuffix: " · Local Delegation effective",
detail: "Local Delegation effective · route foundry-qwen25-7b-qualified",
});
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: true,
warning: "local-agent-delegation skill is not installed",
availability: { available: false, reason: "local-agent-delegation skill is not installed" },
}).detail, "local-agent-delegation skill is not installed");
assert.deepEqual(formatLocalDelegationOpenNotice({
effective: false,
requested: false,
}), { titleSuffix: "", detail: "" });
});
test("repo/connected argv stays orthogonal to local-delegation preference", () => {
const repo = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "repo",
pluginMcpNames: ["teams"],
});
const connected = buildDeskAgentArgv({
deskName: "cost-desk",
workshopDir: "/workshop",
useAgency: false,
copilotCommand: "copilot",
profile: "connected",
pluginMcpNames: ["teams"],
});
assert.deepEqual(repo, [
"copilot", "--name", "cost-desk",
"--disable-mcp-server", "teams",
"--add-dir", "/workshop",
]);
assert.deepEqual(connected, [
"copilot", "--name", "cost-desk",
"--add-dir", "/workshop",
]);
// Local delegation never injects into argv — only env/prompt.
assert.equal(repo.includes("local"), false);
assert.equal(connected.includes("local"), false);
});
test("failed or unaccepted local work earns zero savings credit", () => {
assert.equal(localSavingsCredit({ attempted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: false }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, redone: true }).credit, 0);
assert.equal(localSavingsCredit({ attempted: true, gateAccepted: true, escalated: true }).credit, 0);
const accepted = localSavingsCredit({ attempted: true, gateAccepted: true });
assert.equal(accepted.credit, 0);
assert.equal(accepted.utilization, "handled_locally_accepted");
});
test("state parse/serialize defaults to off", () => {
assert.deepEqual(parseLocalDelegationState(null), { preference: "off" });
assert.equal(parseLocalDelegationState({ preference: "ON" }).preference, "on");
const serialized = serializeLocalDelegationState({ preference: "on" });
assert.equal(serialized.preference, "on");
assert.equal(typeof serialized.updatedAt, "string");
});
test("preference path is user-local and keyed by workshop path, not the repo root", () => {
const home = join("user-home");
const a = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
const b = localDelegationPreferencePath(join("repos", "workshop-b"), {
home,
resolvePath: (p) => p,
});
const again = localDelegationPreferencePath(join("repos", "workshop-a"), {
home,
resolvePath: (p) => p,
});
assert.match(a, /workshop-local-delegation/);
assert.equal(a.startsWith(home), true);
assert.equal(a.includes(`${sep}repos${sep}`), false);
assert.notEqual(a, b);
assert.equal(a, again);
// Repo-shipped .local-delegation.json is never the preference path.
assert.equal(a.endsWith(".local-delegation.json"), false);
// Case-sensitive filesystems: Foo and foo must not share permission state.
const upper = localDelegationPreferencePath("/work/Foo", {
home,
resolvePath: (p) => p,
});
const lower = localDelegationPreferencePath("/work/foo", {
home,
resolvePath: (p) => p,
});
assert.notEqual(upper, lower);
});
test("skill discovery respects explicit dir and common install roots", () => {
const found = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\sealed\\.github\\skills\\local-agent-delegation" },
home: "C:\\home",
exists: () => false,
isSkillDir: (p) => p === "D:\\sealed\\.github\\skills\\local-agent-delegation",
});
assert.equal(found, "D:\\sealed\\.github\\skills\\local-agent-delegation");
const missingExplicit = findLocalDelegationSkillDir({
env: { WORKSHOP_LOCAL_DELEGATION_SKILL_DIR: "D:\\missing" },
home: "C:\\home",
exists: () => false,
isSkillDir: () => false,
});
assert.equal(missingExplicit, null);
});
test("skill discovery walks marketplace/plugin and _direct install layouts", async () => {
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const home = mkdtempSync(join(tmpdir(), "ld-skill-"));
try {
const skillDir = join(
home, ".copilot", "installed-plugins", "awesome-copilot", "sealed-delegation",
".github", "skills", "local-agent-delegation");
mkdirSync(skillDir, { recursive: true });
writeFileSync(join(skillDir, "SKILL.md"), "# local-agent-delegation\n");
const found = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(found, skillDir);
rmSync(join(home, ".copilot", "installed-plugins", "awesome-copilot"), { recursive: true, force: true });
const direct = join(
home, ".copilot", "installed-plugins", "_direct", "sealed-delegation",
"skills", "local-agent-delegation");
mkdirSync(direct, { recursive: true });
writeFileSync(join(direct, "SKILL.md"), "# local-agent-delegation\n");
const foundDirect = findLocalDelegationSkillDir({ home, env: {} });
assert.equal(foundDirect, direct);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
@@ -1,15 +1,15 @@
{ {
"name": "signals-dashboard", "name": "signals-dashboard",
"version": "0.2.0", "version": "0.3.0",
"type": "module", "type": "module",
"main": "extension.mjs", "main": "extension.mjs",
"scripts": { "scripts": {
"test": "node --test launch-profile.test.mjs" "test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
}, },
"dependencies": { "dependencies": {
"@github/copilot-sdk": "latest" "@github/copilot-sdk": "latest"
}, },
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.", "description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
"keywords": [ "keywords": [
"agent-signals", "agent-signals",
"dashboard", "dashboard",