feat: add the-workshop plugin — multi-agent coordination with persistent desks#2343
feat: add the-workshop plugin — multi-agent coordination with persistent desks#2343jennyf19 wants to merge 24 commits into
Conversation
…ent desks The Workshop puts long-running AI agents (desks) in the same room, on the same work, each with its own memory and history, sharing one workspace so you direct the work instead of relaying it. Components: - Workshop TA agent (room coordinator) - Skills: desk-open, desk-journal, signal-write, bench-read - Marketplace entry for one-command install Install: copilot plugin install the-workshop@awesome-copilot Complements Ember (partnership for one agent) with coordination for many agents. Install both for the full stack. Source: https://github.com/jennyf19/the-workshop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
|
🟡 Contributor Reputation Check: MEDIUM risk
Maintainers: please review this contributor before merging. |
🔒 PR Risk Scan ResultsScanned 15 changed file(s).
Skipped non-text or missing files
|
🔍 Vally Lint Results
Summary
Full linter output |
There was a problem hiding this comment.
Pull request overview
Adds The Workshop plugin for coordinating persistent multi-agent desks through shared journals, artifacts, and signals.
Changes:
- Adds the Workshop TA coordinator agent.
- Adds four desk-management and signaling skills.
- Adds plugin metadata, documentation, and marketplace registration.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
agents/workshop-ta.agent.md |
Defines the coordinator agent. |
skills/desk-open/SKILL.md |
Creates persistent desks. |
skills/desk-journal/SKILL.md |
Manages desk journals. |
skills/signal-write/SKILL.md |
Defines structured signals. |
skills/bench-read/SKILL.md |
Reads shared artifacts. |
plugins/the-workshop/README.md |
Documents installation and concepts. |
plugins/the-workshop/.github/plugin/plugin.json |
Declares plugin components. |
.github/plugin/marketplace.json |
Registers the plugin. |
- Add YAML front matter to workshop-ta agent (name + description) - Fix agent path: use 'workshop-ta' not './agents/workshop-ta.agent.md' - Sort skills alphabetically in plugin.json - Make Cairn dashboard reference conditional (full plugin from source repo) - Update signal-write: write JSON to .signals/ AND note in journal - Add partnership signal type to signal-write skill - Inline CAIRN disposition in agent (treat external CAIRN.md as optional) - Run npm run build to regenerate marketplace.json and docs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
- plugin.json: use ./agents/workshop-ta.md path format (matches all other plugins) - workshop-ta front matter: name 'Workshop TA' preserves acronym (was 'workshop-ta') - signal-write: add subtype field (hands-up/blocked/done/checkpoint/partnership) so dashboard consumers can distinguish specific signal states - npm run build: regenerated docs/README.agents.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
… contract - desk-open: standard desk structure now creates .signals/ directory (prevents first signal-write from failing on missing parent dir) - signal-write: note that dashboard reads subtype field, falls back to signal_type for backward compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
- desk-open: add 'Session orientation' section explaining the session→journal→signals lifecycle. Desks are long-running in state (journal), not runtime (each session is independent). - workshop-ta: partnership signals write to desks/_ta/.signals/ so they appear on the dashboard without replacing any desk's latest signal. TA uses the _ta prefix to indicate coordinator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
Never initialize over existing journal.md — if the desk directory already exists, resume it instead. Operator must explicitly rename or archive before reusing a desk name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (6)
plugins/the-workshop/README.md:25
- The manifest installs
workshop-create, and the Workshop TA explicitly invokes it, but the plugin's component inventory omits it. Add the skill here so users can discover every installed component and understand how workshop roots are created.
| Skill | [Bench Read](../../skills/bench-read/) | Read shared artifacts from the workspace where desks leave work for each other |
extensions/signals-dashboard/extension.mjs:103
- The parsed signal is accepted without any schema or type validation, but its score, token, outcome-rating, and effort fields are later interpolated into HTML without consistent escaping. A crafted
.signals/*.jsonfile can therefore inject markup or script into the canvas (partnership scores are rendered even when their values are strings). Validate/coerce the complete signal schema and escape every dynamic render sink before retaining the object.
const raw = await readFile(fp, "utf-8");
const parsed = JSON.parse(raw);
allSignals.push({ parsed, mtimeMs: s.mtimeMs, path: fp });
extensions/signals-dashboard/extension.mjs:128
- When a latest signal has a
run_idbut no matching outcome, this fallback can attach an outcome belonging to a different run merely because it was written within an hour. That produces an incorrect quality rating and honesty gap. Legacy fallback should only pair records where both sides lackrun_id; otherwise leave the outcome unmatched.
// Also check for any recent outcome (within 1hr of latest signal) if no run_id match
if (!outcome) {
const recentOutcomes = allSignals
.filter(s => s.parsed.signal_type === "outcome" && s.mtimeMs >= latestTime && (s.mtimeMs - latestTime) < 3600000)
.sort((a, b) => a.mtimeMs - b.mtimeMs);
extensions/signals-dashboard/extension.mjs:101
- Every dashboard refresh stats, reads, and parses every historical signal file, while the client refreshes every five seconds and desks are designed to accumulate signals indefinitely. The scan cost therefore grows without bound and overlapping refreshes can amplify disk I/O on long-running workshops. Cache parsed files by path/mtime or maintain a bounded/indexed latest-signal view instead of rereading the full history.
for (const f of jsonFiles) {
const fp = join(sigDir, f);
try {
const s = await stat(fp);
const raw = await readFile(fp, "utf-8");
extensions/signals-dashboard/extension.mjs:590
- Replacing the entire content subtree every five seconds destroys the currently focused stash/path/restore button. Keyboard users lose their position repeatedly and may be unable to operate controls reliably. Preserve and restore focus by the button's action/desk identity, or update keyed cards without replacing focused nodes.
const newContent = doc.getElementById('content');
if (newContent) {
document.getElementById('content').innerHTML = newContent.innerHTML;
skills/bench-read/SKILL.md:43
workshop-createscaffoldsbench/as the shared workspace, but this reader instead defines the entire root and desk directories as the bench and never directs agents tobench/. Producers and consumers can therefore place and search for cross-desk artifacts in different locations. Definebench/as the canonical shared-artifact path and distinguish it from desk-local work.
The bench is the workshop directory itself and its subdirectories.
Common patterns:
desks// # each desk's own workspace
journal.md # the desk's memory
# work products from this desk
# cross-desk artifacts
…ite POSTs, preserve focus Second review round on the Cairn canvas extension: - XSS via numeric fields: self-assessment scores, quality rating, and token counts are read from unvalidated agent JSON and interpolated into HTML/style/title. Coerce them at the source in scanSignals via toScore (clamped 0..5) and toCount (finite nonnegative int), and esc() the effort label, so a nonnumeric value can neither inject markup nor break layout/width. - CSRF: /api/stash, /api/restore, /api/open are state-changing loopback POSTs that previously accepted any origin, so a web page that guessed the port could mutate .desk-stash.json. Add isCrossSiteRequest() and reject cross-site POST /api/* (Origin / Sec-Fetch-Site check), mirroring the loopback protection in connector-namespaces/server.mjs. - Accessibility: the 5s auto-refresh replaced the whole #content subtree, dropping keyboard focus. Skip the swap when markup is unchanged and restore focus to the same desk/action button when it does change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (7)
extensions/signals-dashboard/extension.mjs:652
- The same-origin check trusts the request's
Hostheader both here and inisCrossSiteRequest. A DNS-rebinding page can therefore sendHost: attacker.example:<port>and the matchingOrigin, pass the check, read workshop signals, and invoke stash/restore/open. Validate the Host against the canonical127.0.0.1:<actual-port>before serving requests (the hardened pattern is demonstrated inextensions/connector-namespaces/server.mjs:38-39,138-147).
const server = createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
extensions/signals-dashboard/extension.mjs:652
- The async HTTP listener has no top-level error boundary. Failures from
writeFile(for example a read-only workshop or full disk), malformed percent-encoding indecodeURIComponent, or another awaited operation reject the listener promise; Node's HTTP server does not turn that rejection into a response, so the extension can terminate instead of returning an error. Wrap request dispatch intry/catchand return a controlled 4xx/5xx response.
async function startServer(instanceId, workshopDir) {
const server = createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
skills/workshop-create/SKILL.md:47
- Path B creates and clones the repository before this skill tells the agent to check whether the destination parent is already in a Git worktree. Because
gh repo create --cloneclones into the current directory, invoking the skill from an existing repository creates exactly the nested repository the skill promises to prevent (and creates the remote before discovering the mistake). Select and validate the clone parent before runninggh repo create.
2. **Create the repo:** `gh repo create <owner>/<name> --private --clone`
- Use the operator's signed-in GitHub account as `<owner>`
- Clone it to a sensible location (ask the operator where, or use
their configured workshops directory)
plugins/the-workshop/README.md:22
- The installed manifest includes the new
workshop-createskill, and the TA directs users to it, but the plugin's component inventory omits it. Users reading the plugin README cannot discover the only documented workflow for creating the workshop root. Add it to this table.
| Skill | [Desk Open](../../skills/desk-open/) | Create a new desk with journal and folder structure |
extensions/signals-dashboard/extension.mjs:132
- Every dashboard refresh stats, reads, and parses every historical signal file for every desk, while the bundled client repeats this scan every five seconds and
signal-writecontinually creates timestamped files. For the advertised long-running desks, refresh cost and I/O therefore grow without bound. Keep an index/cache, read only the newest non-outcome signal plus relevant outcomes, or define a retention policy rather than rescanning the full history.
for (const f of jsonFiles) {
const fp = join(sigDir, f);
try {
const s = await stat(fp);
const raw = await readFile(fp, "utf-8");
skills/workshop-create/SKILL.md:52
desks/andbench/are empty, so Git will not include them in the commit made by step 4. The pushed repository—and any later clone—therefore lacks two directories that this flow claims to scaffold. Add tracked sentinel files (for example.gitkeep) or another initial tracked file in each directory before committing.
desks/
bench/
skills/bench-read/SKILL.md:34
- This location contract conflicts with
workshop-create, which explicitly createsbench/as the shared workspace. Calling the whole workshop root “the bench” and showing shared artifacts at the root gives desks two incompatible destinations and can make producers and readers look in different places. Definebench/as the cross-desk artifact location while retainingdesks/<name>/for desk-owned work.
The bench is the workshop directory itself and its subdirectories.
…ent list Address the remaining doc/skill review findings: - workshop-create: 'gh repo create --clone' clones into the CWD, which nests the new repo when run from inside a checkout. Add an explicit clone-parent selection + 'rev-parse --is-inside-work-tree' guard, and run the create from that parent. - workshop-create: desks/ and bench/ were scaffolded empty, so Git drops them and a later clone loses the scaffold. Scaffold .gitkeep in each and commit the placeholders. - bench-read: make <workshop>/bench/ the primary shared location (the directory workshop-create actually establishes) with desk-local artifacts as a secondary source, instead of treating the repo root as the bench. - the-workshop README: add the Workshop Create skill to the component table so all six packaged components are documented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
…n non-terminal Two follow-up review findings: - signals-dashboard: the async createServer callback had no top-level error boundary, so a malformed %-encoded path, a stash write on a read-only workshop, or a scan failure rejected a promise the server never awaits — hanging the request and risking an unhandled-rejection crash. Wrap the handler and return a controlled 500. - workshop-create: Path A treated finding a marker (desks/, CAIRN.md, etc.) as 'just use it', an early stop that left partially initialized workshops incomplete. Make it detection-only and continue scaffolding whatever is missing, per the 'only add what's missing' principle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (2)
extensions/signals-dashboard/extension.mjs:45
- The same-origin check trusts the request's
Hostheader. A DNS-rebinding request can sendHost: attacker.example:<port>andOrigin: http://attacker.example:<port>, pass this equality check, and invoke the state-changing stash/restore APIs. Compare against the canonical127.0.0.1:<bound-port>origin and reject noncanonical Host headers;extensions/connector-namespaces/server.mjs:126-155demonstrates both checks.
function isCrossSiteRequest(req) {
const origin = req.headers.origin;
if (origin) {
if (origin === `http://${req.headers.host}`) return false;
if (origin === "null") return true;
extensions/signals-dashboard/extension.mjs:652
- An
asyncHTTP listener's rejected promise is not handled by Node. Here malformed percent encoding in anydecodeURIComponentcall, or a failed stash write, rejects the listener and can terminate the extension instead of returning an HTTP error. Invoke an async request handler from a non-async listener and attach a catch that sends a 400/500 response, as done inextensions/connector-namespaces/server.mjs:576-584.
async function startServer(instanceId, workshopDir) {
const server = createServer(async (req, res) => {
try {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
extensions/signals-dashboard/extension.mjs:44
- The same-origin exception trusts the request's
Hostheader, so a DNS-rebinding request withHost: attacker.example:<port>and the matchingOriginis accepted as local and can invoke the stash/restore endpoints. The repository's canonical implementation explicitly compares against the captured loopback origin and rejects noncanonical hosts (extensions/connector-namespaces/server.mjs:126-155, tested atserver.test.mjs:76-80). Capture this server's127.0.0.1:<port>origin after binding and validate against that value instead ofreq.headers.host.
function isCrossSiteRequest(req) {
const origin = req.headers.origin;
if (origin) {
if (origin === `http://${req.headers.host}`) return false;
extensions/signals-dashboard/extension.mjs:76
- These stash updates are unprotected read-modify-write operations. Two quick stash/restore requests can both read the same array and then overwrite each other, losing one user's state change; the delegated click handler can issue exactly such concurrent requests. Serialize mutations per workshop (or use an atomic locked update) so each operation reads the result of the previous one.
async function stashDesk(workshopDir, deskName) {
const stash = await readStash(workshopDir);
if (stash.some(e => e.name === deskName)) return stash;
stash.push({ name: deskName, stashedAt: new Date().toISOString() });
await writeStash(workshopDir, stash);
signals-dashboard imports '@github/copilot-sdk/extension' but shipped without a package.json — the only SDK-importing extension in the repo missing one (every peer that imports the SDK declares it). Without the manifest the host has no dependency to resolve at load time. Add a package.json matching the peer shape (type: module, main: extension.mjs, '@github/copilot-sdk': latest — the modal peer convention). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (6)
extensions/signals-dashboard/extension.mjs:46
- The same-origin check derives trust from the attacker-controlled
Hostheader. A DNS-rebinding request can send matchingHost/Originvalues, pass this gate, fetch dashboard data, and invoke/api/opento expose local paths. Capture the canonical127.0.0.1:<bound-port>origin after listening, reject noncanonical hosts, and compareOriginonly to that value (as inextensions/connector-namespaces/server.mjs:38-39,141-154).
if (origin) {
if (origin === `http://${req.headers.host}`) return false;
if (origin === "null") return true;
if (/^https?:\/\//i.test(origin)) return true;
extensions/signals-dashboard/extension.mjs:134
- Filesystem mtime is not a persistent event timestamp: cloning or checking out a Git-backed workshop resets mtimes. The dashboard can then select an old signal as latest and report it as newly emitted. Add a validated
emitted_atfield to the signal schema (or a standardized filename timestamp fallback) and use that for ordering/display.
const s = await stat(fp);
const raw = await readFile(fp, "utf-8");
const parsed = JSON.parse(raw);
allSignals.push({ parsed, mtimeMs: s.mtimeMs, path: fp });
extensions/signals-dashboard/extension.mjs:160
- Time proximity does not establish correlation. If execution B is latest and an outcome for execution A arrives afterward within an hour, this pairs A's rating with B and computes a false honesty gap; it can also attach an outcome to an escalation. Require a matching
run_idor another explicit correlation ID, and leave uncorrelated outcomes unpaired.
if (!outcome) {
const recentOutcomes = allSignals
.filter(s => s.parsed.signal_type === "outcome" && s.mtimeMs >= latestTime && (s.mtimeMs - latestTime) < 3600000)
.sort((a, b) => a.mtimeMs - b.mtimeMs);
if (recentOutcomes.length > 0) outcome = recentOutcomes[0].parsed;
extensions/signals-dashboard/extension.mjs:132
- Every refresh stats, reads, and parses every historical signal for every desk. Since
refresh()runs every five seconds and signals accumulate for long-running desks, I/O grows without bound. Maintain a latest/outcome index or cache parsed files and bound retained history so refresh cost does not scale with the full archive.
for (const f of jsonFiles) {
const fp = join(sigDir, f);
try {
const s = await stat(fp);
const raw = await readFile(fp, "utf-8");
extensions/signals-dashboard/extension.mjs:141
- If every JSON file is malformed or is an outcome without a primary signal,
latestremains null and this removes the desk entirely. Empty or missing signal directories correctly render the desk as awaiting, so one bad agent-written file should not make it disappear. Emit the same no-signal record here, optionally with an invalid-signal status.
if (!latest) continue;
extensions/signals-dashboard/extension.mjs:76
- This read-modify-write is not serialized. Two quick stash requests can both read the same array and the last write drops the other desk; the click handlers launch these requests concurrently. Serialize stash mutations per workshop and use an atomic temp-file rename for persistence.
async function stashDesk(workshopDir, deskName) {
const stash = await readStash(workshopDir);
if (stash.some(e => e.name === deskName)) return stash;
stash.push({ name: deskName, stashedAt: new Date().toISOString() });
await writeStash(workshopDir, stash);
The dashboard is a canvas extension that ships standalone, not bundled in the plugin. Installing the-workshop delivers skills/agent/desks only; the signals-dashboard canvas installs separately and renders in the GitHub Copilot app. Replaces the false 'bundled... nothing else to install' claim with the real two-install path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
The signals-dashboard canvas is a standalone extension, not bundled in the-workshop plugin. Removes the same inaccurate 'bundled' phrasing the README fix corrected, keeping the subtype guidance intact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
extensions/signals-dashboard/extension.mjs:653
- The loopback server trusts the request's
Hostheader as its canonical origin. A DNS-rebound request can therefore send matching attacker-controlledHost/Originvalues, read the default dashboard response (including local signal contents), and pass the POST CSRF check. Reject any host other than the actual127.0.0.1:<listening-port>before routing, asextensions/connector-namespaces/server.mjs:138-155does.
const url = new URL(req.url, `http://${req.headers.host}`);
extensions/signals-dashboard/extension.mjs:134
- A valid JSON file can still parse to
nullor an array.nullis added toallSignalsbeforeparsed.signal_typethrows; if another valid latest signal exists, the later outcome filters dereference that retainednull, the outer catch drops the entire desk, and its card disappears. Skip non-object signal documents before retaining them.
const parsed = JSON.parse(raw);
allSignals.push({ parsed, mtimeMs: s.mtimeMs, path: fp });
extensions/signals-dashboard/extension.mjs:296
withOutcomescan be positive whilegapsis empty—for example, an outcome paired with a signal that has no confidence score. In that case the summary renders a greengap nullbadge becausenull <= 1. Base the badge count and average on outcomes that actually produced an honesty gap.
const withOutcomes = activeSignals.filter(s => s.outcomeRating !== null).length;
const avgGap = (() => {
const gaps = activeSignals.filter(s => s.honestyGap !== null).map(s => s.honestyGap);
return gaps.length ? (gaps.reduce((a, b) => a + b, 0) / gaps.length).toFixed(1) : null;
})();
plugins/the-workshop/README.md:44
- This install guidance contradicts the manifest and the TA documentation:
x-awesome-copilot.extensionscauses materialization to copysignals-dashboardintothe-workshop, so installing the plugin already includes it (eng/materialize-plugins.mjs:214-233). Requiring a second install misleads users; present the standalone command as optional instead.
It ships as a separate extension. Install it alongside the plugin to get the live canvas:
copilot plugin install signals-dashboard@awesome-copilot
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (6)
extensions/signals-dashboard/extension.mjs:136
- Filesystem mtime is used as the emission time for latest-signal selection, outcome matching, and displayed age. Git does not preserve mtimes, so cloning or checking out a repository-backed workshop can reorder historical signals and pair an outcome with the wrong execution. Persist a stable emission timestamp in the signal schema (or define a sortable filename format) and use that, with mtime only as a legacy fallback.
if ((parsed.signal_type || "execution") !== "outcome" && s.mtimeMs > latestTime) {
extensions/signals-dashboard/extension.mjs:134
- A valid JSON primitive such as
nullis pushed intoallSignalsbefore property access fails. Later outcome filters dereference everyparsedvalue, so that one malformed file triggers the outer catch and removes an otherwise valid desk from the dashboard. Reject non-object records before storing them.
const parsed = JSON.parse(raw);
allSignals.push({ parsed, mtimeMs: s.mtimeMs, path: fp });
extensions/signals-dashboard/extension.mjs:308
- When outcomes exist but none has a computable honesty gap,
avgGapisnull; JavaScript then treats it as zero in the color comparison and the summary displays a greengap null. Render this badge only when a gap was actually calculated.
const calibrationBadge = withOutcomes > 0
? `<span style="font-size:11px;color:${avgGap <= 1 ? '#22c55e' : avgGap <= 2 ? '#eab308' : '#ef4444'};" title="${withOutcomes} outcome signal${withOutcomes > 1 ? 's' : ''}, avg gap: ${avgGap}">🔍 gap ${avgGap}</span>`
: "";
extensions/signals-dashboard/extension.mjs:570
- This transient toast is the only confirmation that copying the desk path succeeded or failed, but dynamically inserted content without a live-region role is not announced by screen readers. Mark it as a polite status message.
function showToast(title, detail) {
const toast = document.createElement('div');
extensions/signals-dashboard/extension.mjs:792
- This action claims to open a new session, but its handler only validates the directory and returns a path; it never creates or navigates to a session. That can make callers assume the desk is active when no session was opened. Either perform the supported session action or describe this as path resolution.
description: "Open a desk as a new session in the GHCP app. Returns the desk path so the agent can create_session or navigate to it.",
plugins/the-workshop/README.md:40
- The plugin manifest bundles
signals-dashboardviax-awesome-copilot.extensions, so the main install already includes it. Telling users to install it alongside the plugin can register a duplicate copy; document the standalone command as an alternative instead.
It ships as a separate extension. Install it alongside the plugin to get the live canvas:
…a, error handling - extension.mjs: serialize stash read-modify-write behind a per-desk mutex; order signals by explicit ISO timestamp (clone-safe) not mtime; keep desks with malformed latest signal as 'awaiting'; gate calibration badge on a real gap value; add prefers-reduced-motion CSS; toast aria-live region; rename open_desk -> get_desk_path (returns path, no session); handle server.listen errors. - signal-write/SKILL.md: document timestamp + run_id, ordering guidance, and the outcome (calibration) signal schema. - workshop-ta.agent.md: a desk is a persistent workstream picked up by independent sessions, not one long-running process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (4)
extensions/signals-dashboard/extension.mjs:88
readStashis called by unlocked GET/refresh paths, but expiration cleanup writes the stash file here. That bypasseswithStashLock, so a cleanup racing withstashDesk/restoreDeskcan overwrite the newer mutation; a cleanup write failure also makes the catch return an empty stash. Keep reads side-effect free and let locked mutations persist their filtered result.
const now = Date.now();
const live = stash.filter(e => (now - new Date(e.stashedAt).getTime()) < STASH_TTL_MS);
if (live.length !== stash.length) await writeStash(workshopDir, live);
return live;
extensions/signals-dashboard/extension.mjs:164
- A valid JSON value such as
nullis appended toallSignalsbeforeparsed.signal_typethrows. If the directory also contains a valid latest signal, the later outcome filters dereference that retainednull, and the outer catch drops the desk from the dashboard entirely. Reject non-object JSON before adding it toallSignals.
const parsed = JSON.parse(raw);
const emittedMs = signalTime(parsed, s.mtimeMs);
allSignals.push({ parsed, mtimeMs: emittedMs, path: fp });
extensions/signals-dashboard/extension.mjs:203
- The recent-outcome fallback also runs when the latest signal has a
run_idbut no matching outcome, so it can attach another run's nearby outcome and report a false honesty gap. The skill contract says temporal fallback applies only whenrun_idis absent; gate this branch accordingly.
// Also check for any recent outcome (within 1hr of latest signal) if no run_id match
if (!outcome) {
const recentOutcomes = allSignals
.filter(s => s.parsed.signal_type === "outcome" && s.mtimeMs >= latestTime && (s.mtimeMs - latestTime) < 3600000)
.sort((a, b) => a.mtimeMs - b.mtimeMs);
if (recentOutcomes.length > 0) outcome = recentOutcomes[0].parsed;
}
plugins/the-workshop/README.md:43
- This contradicts both the plugin manifest and the TA instructions:
x-awesome-copilot.extensionscauses materialization to bundlesignals-dashboardwiththe-workshop(eng/materialize-plugins.mjs:214-233). Requiring a second install unnecessarily installs the same extension again; describe the standalone command as optional instead.
It ships as a separate extension. Install it alongside the plugin to get the live canvas:
copilot plugin install signals-dashboard@awesome-copilot
</details>
| function toScore(v, max = 5) { | ||
| const n = Number(v); | ||
| if (!Number.isFinite(n)) return 0; | ||
| return Math.max(0, Math.min(max, n)); | ||
| } |
What
Adds the-workshop — a multi-agent coordination framework where long-running AI agents (desks) work in the same room, on the same work, each with its own memory and history.
Install
Why it belongs here
The Workshop complements Ember (already on awesome-copilot):
Install both for the full stack.
Components
Key concepts
Source
jennyf19/the-workshop — includes a canvas extension (signals dashboard) for the GHCP app.
Files
plugins/the-workshop/— plugin entry (plugin.json+ README)agents/workshop-ta.agent.md— TA coordinator agentskills/{desk-open,desk-journal,signal-write,bench-read}/— skills.github/plugin/marketplace.json— marketplace entry added