mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-17 14:46:56 +00:00
chore: publish from main
This commit is contained in:
@@ -1282,8 +1282,8 @@
|
||||
{
|
||||
"name": "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.",
|
||||
"version": "0.2.0"
|
||||
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
|
||||
"version": "0.3.0"
|
||||
},
|
||||
{
|
||||
"name": "site-studio",
|
||||
|
||||
@@ -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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
|
||||
@@ -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 { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
|
||||
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep } from "node:path";
|
||||
import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
parsePluginMcpNames,
|
||||
quoteWindowsCmdArgument,
|
||||
} from "./launch-profile.mjs";
|
||||
import {
|
||||
buildLocalDelegationLaunchEnv,
|
||||
deskOrientPrompt,
|
||||
formatLocalDelegationOpenNotice,
|
||||
localDelegationPreferencePath,
|
||||
normalizeLocalDelegationPreference,
|
||||
parseLocalDelegationState,
|
||||
resolveLocalDelegationAvailability,
|
||||
resolveLocalDelegationLaunch,
|
||||
serializeLocalDelegationState,
|
||||
windowsLocalDelegationCmdPrefix,
|
||||
} from "./local-delegation.mjs";
|
||||
|
||||
const servers = new Map();
|
||||
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
|
||||
// single-quoted literal inside the macOS Terminal command — never concatenated
|
||||
// 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. ` +
|
||||
`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.`;
|
||||
}
|
||||
//
|
||||
// Local Delegation is orthogonal to repo/connected: it never changes the tool
|
||||
// surface. When effective, only the orientation prompt and child env mark that
|
||||
// the frontier desk may use the installed local-agent-delegation skill.
|
||||
|
||||
// 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
|
||||
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
|
||||
} 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
|
||||
// below, and the resolved desk must still live inside the workshop root
|
||||
// (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;
|
||||
const agent = await deskAgentArgv(deskName, workshopDir, profile);
|
||||
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") {
|
||||
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
|
||||
const cmd = resolveSystem32Executable("cmd.exe");
|
||||
const direct = /\.(exe|com)$/i.test(run[0]);
|
||||
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true;
|
||||
|
||||
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every
|
||||
// argument is free of cmd metacharacters; otherwise fail closed and let
|
||||
// the UI copy the desk path rather than reparse an unsafe workshop path.
|
||||
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg));
|
||||
if (cmdSafe && wt && cmd &&
|
||||
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true;
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`,
|
||||
// still through cmd /k only when the arguments are safe for reparsing.
|
||||
return cmdSafe && cmd
|
||||
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath })
|
||||
: false;
|
||||
// wt.exe does not reliably forward Node's spawn env into a new tab when
|
||||
// Windows Terminal is already running. Always start through cmd.exe and
|
||||
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
|
||||
// Args are quoteWindowsCmdArgument'd, so only block expanders that still
|
||||
// fire inside quotes (% and !) — allow parentheses in workshop paths.
|
||||
const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
|
||||
if (!cmdSafe || !cmd) return false;
|
||||
const inner = windowsLocalDelegationCmdPrefix(effective)
|
||||
+ run.map(quoteWindowsCmdArgument).join(" ");
|
||||
if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
|
||||
return true;
|
||||
}
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`.
|
||||
return await trySpawn(
|
||||
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
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
|
||||
// single-quoted so the shell can't reinterpret it, and osascript itself
|
||||
// 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(" ");
|
||||
const script = 'tell application "Terminal"\n' +
|
||||
" activate\n" +
|
||||
" do script " + osaStringLiteral(line) + "\n" +
|
||||
"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
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
@@ -728,7 +803,42 @@ function avgScore(signals) {
|
||||
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 withSignals = activeSignals.filter(s => s.signalType !== "none").length;
|
||||
const awaiting = activeSignals.filter(s => s.signalType === "none").length;
|
||||
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
|
||||
|
||||
return `
|
||||
<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;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
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;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:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
|
||||
${tokenBadge}
|
||||
${calibrationBadge}
|
||||
${escBadge}
|
||||
${renderLocalDelegationControl(localDelegation)}
|
||||
</div>
|
||||
${avgBlock}
|
||||
</div>`;
|
||||
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
|
||||
</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 localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
|
||||
|
||||
const cards = activeSignals.length > 0
|
||||
? activeSignals.map(renderSignalCard).join("")
|
||||
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
</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 ? `
|
||||
<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();
|
||||
if (data.ok) {
|
||||
const path = data.deskPath || name;
|
||||
const notice = data.localDelegationNotice || {};
|
||||
const localTitle = notice.titleSuffix || '';
|
||||
const localDetail = notice.detail || '';
|
||||
if (data.launched) {
|
||||
// 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 {
|
||||
// No terminal launched from here, so copy the path as the
|
||||
// fallback handle, but only claim the copy when it actually
|
||||
// succeeded. The path shows in the toast either way.
|
||||
let copied = false;
|
||||
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 {
|
||||
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) => {
|
||||
const btn = e.target.closest('button[data-act]');
|
||||
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');
|
||||
if (!name) return;
|
||||
const act = btn.getAttribute('data-act');
|
||||
const profile = btn.getAttribute('data-profile');
|
||||
if (act === 'stash') stashDesk(name);
|
||||
else if (act === 'restore') restoreDesk(name);
|
||||
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
active.getAttribute('data-act'),
|
||||
active.getAttribute('data-desk'),
|
||||
active.getAttribute('data-profile') || '',
|
||||
active.getAttribute('data-preference') || '',
|
||||
]);
|
||||
}
|
||||
content.innerHTML = newContent.innerHTML;
|
||||
if (focusKey) {
|
||||
const [act, desk, profile] = JSON.parse(focusKey);
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
const target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
const [act, desk, profile, preference] = JSON.parse(focusKey);
|
||||
let target = null;
|
||||
if (act === 'local-delegation') {
|
||||
target = content.querySelector('button[data-act="local-delegation"]');
|
||||
} else {
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
}
|
||||
if (target) target.focus();
|
||||
void preference;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
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/")) {
|
||||
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
|
||||
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
|
||||
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
|
||||
return;
|
||||
}
|
||||
const profile = normalizeDeskProfile(profileInput);
|
||||
const preference = await readLocalDelegationPreference(workshopDir);
|
||||
const localDelegation = currentLocalDelegationLaunch(preference);
|
||||
for (const subdir of ["desks", "classroom"]) {
|
||||
const deskPath = join(workshopDir, subdir, deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile);
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, deskName, workshopDir, profile, localDelegation);
|
||||
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;
|
||||
}
|
||||
} catch {}
|
||||
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
|
||||
|
||||
const signals = await scanSignals(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.end(renderDashboard(signals, stashed, capabilityToken));
|
||||
res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
|
||||
} catch (err) {
|
||||
// Top-level boundary: never leave a request hanging or let a
|
||||
// rejection become an unhandled crash — e.g. malformed %-encoding
|
||||
@@ -1327,7 +1503,7 @@ const session = await joinSession({
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1337,6 +1513,11 @@ const session = await joinSession({
|
||||
enum: ["repo", "connected"],
|
||||
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"],
|
||||
},
|
||||
@@ -1347,13 +1528,21 @@ const session = await joinSession({
|
||||
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
|
||||
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
|
||||
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"]) {
|
||||
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, ctx.input.deskName, entry.workshopDir, profile);
|
||||
deskPath,
|
||||
ctx.input.deskName,
|
||||
entry.workshopDir,
|
||||
profile,
|
||||
localDelegation);
|
||||
return {
|
||||
ok: true,
|
||||
deskName: ctx.input.deskName,
|
||||
@@ -1361,6 +1550,8 @@ const session = await joinSession({
|
||||
launched,
|
||||
workshopDir: entry.workshopDir,
|
||||
profile,
|
||||
localDelegation,
|
||||
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(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) {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
|
||||
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
|
||||
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
|
||||
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", {
|
||||
|
||||
@@ -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",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"scripts": {
|
||||
"test": "node --test launch-profile.test.mjs"
|
||||
"test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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": [
|
||||
"agent-signals",
|
||||
"dashboard",
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
# 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
|
||||
|
||||
``bash
|
||||
```bash
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
## 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.
|
||||
+238
-47
@@ -5,20 +5,33 @@
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
|
||||
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep } from "node:path";
|
||||
import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
parsePluginMcpNames,
|
||||
quoteWindowsCmdArgument,
|
||||
} from "./launch-profile.mjs";
|
||||
import {
|
||||
buildLocalDelegationLaunchEnv,
|
||||
deskOrientPrompt,
|
||||
formatLocalDelegationOpenNotice,
|
||||
localDelegationPreferencePath,
|
||||
normalizeLocalDelegationPreference,
|
||||
parseLocalDelegationState,
|
||||
resolveLocalDelegationAvailability,
|
||||
resolveLocalDelegationLaunch,
|
||||
serializeLocalDelegationState,
|
||||
windowsLocalDelegationCmdPrefix,
|
||||
} from "./local-delegation.mjs";
|
||||
|
||||
const servers = new Map();
|
||||
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
|
||||
// single-quoted literal inside the macOS Terminal command — never concatenated
|
||||
// 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. ` +
|
||||
`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.`;
|
||||
}
|
||||
//
|
||||
// Local Delegation is orthogonal to repo/connected: it never changes the tool
|
||||
// surface. When effective, only the orientation prompt and child env mark that
|
||||
// the frontier desk may use the installed local-agent-delegation skill.
|
||||
|
||||
// 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
|
||||
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
|
||||
} 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
|
||||
// below, and the resolved desk must still live inside the workshop root
|
||||
// (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;
|
||||
const agent = await deskAgentArgv(deskName, workshopDir, profile);
|
||||
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") {
|
||||
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
|
||||
const cmd = resolveSystem32Executable("cmd.exe");
|
||||
const direct = /\.(exe|com)$/i.test(run[0]);
|
||||
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true;
|
||||
|
||||
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every
|
||||
// argument is free of cmd metacharacters; otherwise fail closed and let
|
||||
// the UI copy the desk path rather than reparse an unsafe workshop path.
|
||||
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg));
|
||||
if (cmdSafe && wt && cmd &&
|
||||
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true;
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`,
|
||||
// still through cmd /k only when the arguments are safe for reparsing.
|
||||
return cmdSafe && cmd
|
||||
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath })
|
||||
: false;
|
||||
// wt.exe does not reliably forward Node's spawn env into a new tab when
|
||||
// Windows Terminal is already running. Always start through cmd.exe and
|
||||
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
|
||||
// Args are quoteWindowsCmdArgument'd, so only block expanders that still
|
||||
// fire inside quotes (% and !) — allow parentheses in workshop paths.
|
||||
const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
|
||||
if (!cmdSafe || !cmd) return false;
|
||||
const inner = windowsLocalDelegationCmdPrefix(effective)
|
||||
+ run.map(quoteWindowsCmdArgument).join(" ");
|
||||
if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
|
||||
return true;
|
||||
}
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`.
|
||||
return await trySpawn(
|
||||
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
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
|
||||
// single-quoted so the shell can't reinterpret it, and osascript itself
|
||||
// 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(" ");
|
||||
const script = 'tell application "Terminal"\n' +
|
||||
" activate\n" +
|
||||
" do script " + osaStringLiteral(line) + "\n" +
|
||||
"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
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
@@ -728,7 +803,42 @@ function avgScore(signals) {
|
||||
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 withSignals = activeSignals.filter(s => s.signalType !== "none").length;
|
||||
const awaiting = activeSignals.filter(s => s.signalType === "none").length;
|
||||
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
|
||||
|
||||
return `
|
||||
<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;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
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;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:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
|
||||
${tokenBadge}
|
||||
${calibrationBadge}
|
||||
${escBadge}
|
||||
${renderLocalDelegationControl(localDelegation)}
|
||||
</div>
|
||||
${avgBlock}
|
||||
</div>`;
|
||||
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
|
||||
</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 localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
|
||||
|
||||
const cards = activeSignals.length > 0
|
||||
? activeSignals.map(renderSignalCard).join("")
|
||||
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
</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 ? `
|
||||
<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();
|
||||
if (data.ok) {
|
||||
const path = data.deskPath || name;
|
||||
const notice = data.localDelegationNotice || {};
|
||||
const localTitle = notice.titleSuffix || '';
|
||||
const localDetail = notice.detail || '';
|
||||
if (data.launched) {
|
||||
// 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 {
|
||||
// No terminal launched from here, so copy the path as the
|
||||
// fallback handle, but only claim the copy when it actually
|
||||
// succeeded. The path shows in the toast either way.
|
||||
let copied = false;
|
||||
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 {
|
||||
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) => {
|
||||
const btn = e.target.closest('button[data-act]');
|
||||
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');
|
||||
if (!name) return;
|
||||
const act = btn.getAttribute('data-act');
|
||||
const profile = btn.getAttribute('data-profile');
|
||||
if (act === 'stash') stashDesk(name);
|
||||
else if (act === 'restore') restoreDesk(name);
|
||||
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
active.getAttribute('data-act'),
|
||||
active.getAttribute('data-desk'),
|
||||
active.getAttribute('data-profile') || '',
|
||||
active.getAttribute('data-preference') || '',
|
||||
]);
|
||||
}
|
||||
content.innerHTML = newContent.innerHTML;
|
||||
if (focusKey) {
|
||||
const [act, desk, profile] = JSON.parse(focusKey);
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
const target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
const [act, desk, profile, preference] = JSON.parse(focusKey);
|
||||
let target = null;
|
||||
if (act === 'local-delegation') {
|
||||
target = content.querySelector('button[data-act="local-delegation"]');
|
||||
} else {
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
}
|
||||
if (target) target.focus();
|
||||
void preference;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
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/")) {
|
||||
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
|
||||
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
|
||||
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
|
||||
return;
|
||||
}
|
||||
const profile = normalizeDeskProfile(profileInput);
|
||||
const preference = await readLocalDelegationPreference(workshopDir);
|
||||
const localDelegation = currentLocalDelegationLaunch(preference);
|
||||
for (const subdir of ["desks", "classroom"]) {
|
||||
const deskPath = join(workshopDir, subdir, deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile);
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, deskName, workshopDir, profile, localDelegation);
|
||||
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;
|
||||
}
|
||||
} catch {}
|
||||
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
|
||||
|
||||
const signals = await scanSignals(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.end(renderDashboard(signals, stashed, capabilityToken));
|
||||
res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
|
||||
} catch (err) {
|
||||
// Top-level boundary: never leave a request hanging or let a
|
||||
// rejection become an unhandled crash — e.g. malformed %-encoding
|
||||
@@ -1327,7 +1503,7 @@ const session = await joinSession({
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1337,6 +1513,11 @@ const session = await joinSession({
|
||||
enum: ["repo", "connected"],
|
||||
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"],
|
||||
},
|
||||
@@ -1347,13 +1528,21 @@ const session = await joinSession({
|
||||
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
|
||||
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
|
||||
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"]) {
|
||||
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, ctx.input.deskName, entry.workshopDir, profile);
|
||||
deskPath,
|
||||
ctx.input.deskName,
|
||||
entry.workshopDir,
|
||||
profile,
|
||||
localDelegation);
|
||||
return {
|
||||
ok: true,
|
||||
deskName: ctx.input.deskName,
|
||||
@@ -1361,6 +1550,8 @@ const session = await joinSession({
|
||||
launched,
|
||||
workshopDir: entry.workshopDir,
|
||||
profile,
|
||||
localDelegation,
|
||||
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
+10
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(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) {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); }
|
||||
|
||||
+5
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
|
||||
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
|
||||
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
|
||||
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", {
|
||||
|
||||
+401
@@ -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(),
|
||||
};
|
||||
}
|
||||
+298
@@ -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
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "signals-dashboard",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"scripts": {
|
||||
"test": "node --test launch-profile.test.mjs"
|
||||
"test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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": [
|
||||
"agent-signals",
|
||||
"dashboard",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "signals-dashboard",
|
||||
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, and cost-aware repo or connected desk launch profiles.",
|
||||
"version": "0.2.0",
|
||||
"description": "Real-time Workshop dashboard with agent signals, honesty calibration, cost-aware desk profiles, and fail-closed Local Delegation.",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "jennyf19",
|
||||
"url": "https://github.com/jennyf19"
|
||||
@@ -12,7 +12,8 @@
|
||||
"dashboard",
|
||||
"multi-agent",
|
||||
"coordination",
|
||||
"canvas"
|
||||
"canvas",
|
||||
"local-delegation"
|
||||
],
|
||||
"extensions": {
|
||||
"com.github.copilot": {
|
||||
|
||||
@@ -45,6 +45,13 @@ Each desk card has two launch choices:
|
||||
- **connected** — the full configured tool surface for work that needs external
|
||||
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
|
||||
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.
|
||||
+238
-47
@@ -5,20 +5,33 @@
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { statSync, accessSync, realpathSync, constants as fsConstants } from "node:fs";
|
||||
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep } from "node:path";
|
||||
import { readdir, readFile, writeFile, stat, rename, unlink, mkdir } from "node:fs/promises";
|
||||
import { join, delimiter, isAbsolute, sep, dirname } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
parsePluginMcpNames,
|
||||
quoteWindowsCmdArgument,
|
||||
} from "./launch-profile.mjs";
|
||||
import {
|
||||
buildLocalDelegationLaunchEnv,
|
||||
deskOrientPrompt,
|
||||
formatLocalDelegationOpenNotice,
|
||||
localDelegationPreferencePath,
|
||||
normalizeLocalDelegationPreference,
|
||||
parseLocalDelegationState,
|
||||
resolveLocalDelegationAvailability,
|
||||
resolveLocalDelegationLaunch,
|
||||
serializeLocalDelegationState,
|
||||
windowsLocalDelegationCmdPrefix,
|
||||
} from "./local-delegation.mjs";
|
||||
|
||||
const servers = new Map();
|
||||
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
|
||||
// single-quoted literal inside the macOS Terminal command — never concatenated
|
||||
// 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. ` +
|
||||
`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.`;
|
||||
}
|
||||
//
|
||||
// Local Delegation is orthogonal to repo/connected: it never changes the tool
|
||||
// surface. When effective, only the orientation prompt and child env mark that
|
||||
// the frontier desk may use the installed local-agent-delegation skill.
|
||||
|
||||
// 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
|
||||
@@ -354,7 +366,62 @@ function isInsideRoot(root, target) {
|
||||
} 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
|
||||
// below, and the resolved desk must still live inside the workshop root
|
||||
// (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;
|
||||
const agent = await deskAgentArgv(deskName, workshopDir, profile);
|
||||
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") {
|
||||
const wt = resolveOnPath("wt", { directOnly: true, excludedRoot: workshopDir });
|
||||
const cmd = resolveSystem32Executable("cmd.exe");
|
||||
const direct = /\.(exe|com)$/i.test(run[0]);
|
||||
if (direct && wt && await trySpawn(wt, ["-d", deskPath, ...run])) return true;
|
||||
|
||||
// Older installs can expose .cmd/.bat shims. Only use cmd.exe when every
|
||||
// argument is free of cmd metacharacters; otherwise fail closed and let
|
||||
// the UI copy the desk path rather than reparse an unsafe workshop path.
|
||||
const cmdSafe = run.every((arg) => !/[&|<>^%!()\r\n]/.test(arg));
|
||||
if (cmdSafe && wt && cmd &&
|
||||
await trySpawn(wt, ["-d", deskPath, cmd, "/k", ...run])) return true;
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`,
|
||||
// still through cmd /k only when the arguments are safe for reparsing.
|
||||
return cmdSafe && cmd
|
||||
? await trySpawn(cmd, ["/c", "start", "", cmd, "/k", ...run], { cwd: deskPath })
|
||||
: false;
|
||||
// wt.exe does not reliably forward Node's spawn env into a new tab when
|
||||
// Windows Terminal is already running. Always start through cmd.exe and
|
||||
// set/clear WORKSHOP_LOCAL_DELEGATION in the command string itself.
|
||||
// Args are quoteWindowsCmdArgument'd, so only block expanders that still
|
||||
// fire inside quotes (% and !) — allow parentheses in workshop paths.
|
||||
const cmdSafe = run.every((arg) => isSafeQuotedWindowsCmdArg(arg));
|
||||
if (!cmdSafe || !cmd) return false;
|
||||
const inner = windowsLocalDelegationCmdPrefix(effective)
|
||||
+ run.map(quoteWindowsCmdArgument).join(" ");
|
||||
if (wt && await trySpawn(wt, ["-d", deskPath, cmd, "/d", "/s", "/k", inner], { env })) {
|
||||
return true;
|
||||
}
|
||||
// Fallback when wt.exe is absent: a fresh console window via `start`.
|
||||
return await trySpawn(
|
||||
cmd, ["/c", "start", "", cmd, "/d", "/s", "/k", inner], { cwd: deskPath, env });
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
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
|
||||
// single-quoted so the shell can't reinterpret it, and osascript itself
|
||||
// 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(" ");
|
||||
const script = 'tell application "Terminal"\n' +
|
||||
" activate\n" +
|
||||
" do script " + osaStringLiteral(line) + "\n" +
|
||||
"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
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
@@ -728,7 +803,42 @@ function avgScore(signals) {
|
||||
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 withSignals = activeSignals.filter(s => s.signalType !== "none").length;
|
||||
const awaiting = activeSignals.filter(s => s.signalType === "none").length;
|
||||
@@ -763,13 +873,14 @@ function renderSummaryBar(activeSignals) {
|
||||
|
||||
return `
|
||||
<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;">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
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;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:11px;color:#475569;">${withSignals} reporting · ${awaiting} awaiting</span>
|
||||
${tokenBadge}
|
||||
${calibrationBadge}
|
||||
${escBadge}
|
||||
${renderLocalDelegationControl(localDelegation)}
|
||||
</div>
|
||||
${avgBlock}
|
||||
</div>`;
|
||||
@@ -945,8 +1056,9 @@ function renderStashedCard(entry) {
|
||||
</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 localDelegationState = localDelegation || currentLocalDelegationLaunch("off");
|
||||
|
||||
const cards = activeSignals.length > 0
|
||||
? activeSignals.map(renderSignalCard).join("")
|
||||
@@ -975,7 +1087,9 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
</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 ? `
|
||||
<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();
|
||||
if (data.ok) {
|
||||
const path = data.deskPath || name;
|
||||
const notice = data.localDelegationNotice || {};
|
||||
const localTitle = notice.titleSuffix || '';
|
||||
const localDetail = notice.detail || '';
|
||||
if (data.launched) {
|
||||
// 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 {
|
||||
// No terminal launched from here, so copy the path as the
|
||||
// fallback handle, but only claim the copy when it actually
|
||||
// succeeded. The path shows in the toast either way.
|
||||
let copied = false;
|
||||
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 {
|
||||
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) => {
|
||||
const btn = e.target.closest('button[data-act]');
|
||||
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');
|
||||
if (!name) return;
|
||||
const act = btn.getAttribute('data-act');
|
||||
const profile = btn.getAttribute('data-profile');
|
||||
if (act === 'stash') stashDesk(name);
|
||||
else if (act === 'restore') restoreDesk(name);
|
||||
@@ -1102,19 +1244,26 @@ function renderDashboard(signals, stashed, capabilityToken) {
|
||||
active.getAttribute('data-act'),
|
||||
active.getAttribute('data-desk'),
|
||||
active.getAttribute('data-profile') || '',
|
||||
active.getAttribute('data-preference') || '',
|
||||
]);
|
||||
}
|
||||
content.innerHTML = newContent.innerHTML;
|
||||
if (focusKey) {
|
||||
const [act, desk, profile] = JSON.parse(focusKey);
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
const target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
const [act, desk, profile, preference] = JSON.parse(focusKey);
|
||||
let target = null;
|
||||
if (act === 'local-delegation') {
|
||||
target = content.querySelector('button[data-act="local-delegation"]');
|
||||
} else {
|
||||
const escDesk = (window.CSS && CSS.escape) ? CSS.escape(desk) : desk;
|
||||
const profileSelector = profile
|
||||
? '[data-profile="' + profile + '"]'
|
||||
: ':not([data-profile])';
|
||||
target = content.querySelector(
|
||||
'button[data-act="' + act + '"][data-desk="' + escDesk + '"]' +
|
||||
profileSelector);
|
||||
}
|
||||
if (target) target.focus();
|
||||
void preference;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -1180,6 +1329,20 @@ async function startServer(instanceId, workshopDir) {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
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/")) {
|
||||
const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
|
||||
const profileInput = url.searchParams.get("profile") || DEFAULT_DESK_PROFILE;
|
||||
@@ -1194,14 +1357,25 @@ async function startServer(instanceId, workshopDir) {
|
||||
return;
|
||||
}
|
||||
const profile = normalizeDeskProfile(profileInput);
|
||||
const preference = await readLocalDelegationPreference(workshopDir);
|
||||
const localDelegation = currentLocalDelegationLaunch(preference);
|
||||
for (const subdir of ["desks", "classroom"]) {
|
||||
const deskPath = join(workshopDir, subdir, deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(deskPath, deskName, workshopDir, profile);
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, deskName, workshopDir, profile, localDelegation);
|
||||
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;
|
||||
}
|
||||
} catch {}
|
||||
@@ -1213,8 +1387,10 @@ async function startServer(instanceId, workshopDir) {
|
||||
|
||||
const signals = await scanSignals(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.end(renderDashboard(signals, stashed, capabilityToken));
|
||||
res.end(renderDashboard(signals, stashed, capabilityToken, localDelegation));
|
||||
} catch (err) {
|
||||
// Top-level boundary: never leave a request hanging or let a
|
||||
// rejection become an unhandled crash — e.g. malformed %-encoding
|
||||
@@ -1327,7 +1503,7 @@ const session = await joinSession({
|
||||
},
|
||||
{
|
||||
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: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1337,6 +1513,11 @@ const session = await joinSession({
|
||||
enum: ["repo", "connected"],
|
||||
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"],
|
||||
},
|
||||
@@ -1347,13 +1528,21 @@ const session = await joinSession({
|
||||
const profileInput = ctx.input.profile || DEFAULT_DESK_PROFILE;
|
||||
if (!isDeskProfile(profileInput)) return { error: "Invalid desk profile" };
|
||||
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"]) {
|
||||
const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);
|
||||
try {
|
||||
const s = await stat(deskPath);
|
||||
if (s.isDirectory()) {
|
||||
const launched = await launchDeskConsole(
|
||||
deskPath, ctx.input.deskName, entry.workshopDir, profile);
|
||||
deskPath,
|
||||
ctx.input.deskName,
|
||||
entry.workshopDir,
|
||||
profile,
|
||||
localDelegation);
|
||||
return {
|
||||
ok: true,
|
||||
deskName: ctx.input.deskName,
|
||||
@@ -1361,6 +1550,8 @@ const session = await joinSession({
|
||||
launched,
|
||||
workshopDir: entry.workshopDir,
|
||||
profile,
|
||||
localDelegation,
|
||||
localDelegationNotice: formatLocalDelegationOpenNotice(localDelegation),
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
+10
@@ -25,6 +25,16 @@ export function isSafeWindowsCmdShim(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) {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(text); }
|
||||
|
||||
+5
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
||||
import {
|
||||
buildDeskAgentArgv,
|
||||
isDeskProfile,
|
||||
isSafeQuotedWindowsCmdArg,
|
||||
isSafeWindowsCmdShim,
|
||||
isWindowsAppExecutionAlias,
|
||||
normalizeDeskProfile,
|
||||
@@ -41,6 +42,10 @@ test("quotes trusted cmd shim arguments and rejects percent-bearing paths", () =
|
||||
assert.equal(quoteWindowsCmdArgument("--scope"), "\"--scope\"");
|
||||
assert.equal(isSafeWindowsCmdShim("C:\\Program Files\\Agency\\agency.cmd"), true);
|
||||
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", {
|
||||
|
||||
+401
@@ -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(),
|
||||
};
|
||||
}
|
||||
+298
@@ -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",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"main": "extension.mjs",
|
||||
"scripts": {
|
||||
"test": "node --test launch-profile.test.mjs"
|
||||
"test": "node --test launch-profile.test.mjs local-delegation.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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": [
|
||||
"agent-signals",
|
||||
"dashboard",
|
||||
|
||||
Reference in New Issue
Block a user