Skip to content

feat: add the-workshop plugin — multi-agent coordination with persistent desks#2343

Merged
codemillmatt merged 25 commits into
github:mainfrom
jennyf19:add-the-workshop
Jul 20, 2026
Merged

feat: add the-workshop plugin — multi-agent coordination with persistent desks#2343
codemillmatt merged 25 commits into
github:mainfrom
jennyf19:add-the-workshop

Conversation

@jennyf19

@jennyf19 jennyf19 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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

copilot plugin install the-workshop@awesome-copilot

Why it belongs here

The Workshop complements Ember (already on awesome-copilot):

  • Ember = partnership framework for ONE agent (how an AI shows up)
  • The Workshop = coordination framework for MANY agents (how a room works together)

Install both for the full stack.

Components

Type Name Description
Agent Workshop TA Room coordinator — sees all desks, routes work, tracks state
Skill desk-open Create a new desk with journal and folder structure
Skill desk-journal Read/write persistent memory across sessions
Skill signal-write Emit structured signals: hands-up, blocked, done, checkpoint
Skill bench-read Read shared artifacts from the workspace

Key concepts

  • Desks — long-running agents with persistent journals. Each has its own frame and equal standing to disagree with other desks.
  • The Bench — shared workspace where desks leave artifacts for each other.
  • Signals — structured state changes (hands-up, blocked, done, checkpoint).
  • The Cairn — operating disposition: stop is a valid finish, never bluff, equal standing. Read it →

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 agent
  • skills/{desk-open,desk-journal,signal-write,bench-read}/ — skills
  • .github/plugin/marketplace.json — marketplace entry added

…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
@jennyf19
jennyf19 requested a review from aaronpowell as a code owner July 17, 2026 18:16
Copilot AI review requested due to automatic review settings July 17, 2026 18:16
@github-actions github-actions Bot added agent PR touches agents new-submission PR adds at least one new contribution plugin PR touches plugins skills PR touches skills labels Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🟡 Contributor Reputation Check: MEDIUM risk

Check Risk
Profile MEDIUM
Credential audit NONE

Maintainers: please review this contributor before merging.
See the workflow run for full details.
Automated check powered by AGT.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor reputation check flagged MEDIUM risk label Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🔒 PR Risk Scan Results

Scanned 15 changed file(s).

Severity Count
🔴 High 0
🟠 Medium 2
ℹ️ Info 0
Severity Rule File Line Match
🟠 package-exec-command docs/README.skills.md 31 | [acreadiness-assess](../skills/acreadiness-assess/SKILL.md)&lt;br /&gt;`gh skills install github/awesome-copilot acreadiness-assess` | Run the AgentRC readiness assessment on the curre
🟠 unpinned-version-indicator extensions/signals-dashboard/package.json 7 "@​​github/copilot-sdk": "latest"
Skipped non-text or missing files
  • extensions/signals-dashboard/assets/preview.png

This is an automated soft-gate report. Findings indicate review targets and do not block merge by themselves.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🔍 Vally Lint Results

⚠️ Warnings or advisories found

Scope Checked
Skills 5
Agents 1
Total 6
Severity Count
❌ Errors 0
⚠️ Warnings 0
ℹ️ Advisories 1

Summary

Level Finding
ℹ️ Vally currently lints SKILL.md content. Agent files were detected but skipped:
Full linter output
### Linting skills/bench-read
npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: 'commander@15.0.0',
npm warn EBADENGINE   required: { node: '>=22.12.0' },
npm warn EBADENGINE   current: { node: 'v20.20.2', npm: '10.8.2' }
npm warn EBADENGINE }
npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
✅ bench-read (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

### Linting skills/desk-journal
✅ desk-journal (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

### Linting skills/desk-open
✅ desk-open (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

### Linting skills/signal-write
✅ signal-write (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

### Linting skills/workshop-create
✅ workshop-create (2/2 checks passed)
    ✓ [spec-compliance] All 1 skill(s) are spec-compliant.
        ✓ spec-compliance: All spec checks passed.
    ✓ [valid-refs] All file references across 1 skill(s) are valid.
        ✓ valid-refs: All file references resolve to existing files within the skill directory.

1 skill(s) linted, 1 passed

### Agent files detected (not linted by vally)
ℹ️ Vally currently lints SKILL.md content. Agent files were detected but skipped:
agents/workshop-ta.agent.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread agents/workshop-ta.agent.md
Comment thread plugins/the-workshop/.github/plugin/plugin.json Outdated
Comment thread plugins/the-workshop/.github/plugin/plugin.json Outdated
Comment thread agents/workshop-ta.agent.md Outdated
Comment thread skills/signal-write/SKILL.md Outdated
Comment thread agents/workshop-ta.agent.md Outdated
Comment thread agents/workshop-ta.agent.md Outdated
Comment thread .github/plugin/marketplace.json Outdated
- 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
Copilot AI review requested due to automatic review settings July 17, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment thread plugins/the-workshop/.github/plugin/plugin.json Outdated
Comment thread agents/workshop-ta.agent.md Outdated
Comment thread skills/signal-write/SKILL.md
- 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
Copilot AI review requested due to automatic review settings July 17, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread skills/desk-open/SKILL.md
Comment thread skills/signal-write/SKILL.md
… 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
Copilot AI review requested due to automatic review settings July 17, 2026 19:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread skills/desk-open/SKILL.md Outdated
Comment thread agents/workshop-ta.agent.md Outdated
- 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
Copilot AI review requested due to automatic review settings July 17, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread skills/desk-open/SKILL.md Outdated
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
Copilot AI review requested due to automatic review settings July 19, 2026 02:56
…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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Host header. A DNS-rebinding request can send Host: attacker.example:<port> and Origin: http://attacker.example:<port>, pass this equality check, and invoke the state-changing stash/restore APIs. Compare against the canonical 127.0.0.1:<bound-port> origin and reject noncanonical Host headers; extensions/connector-namespaces/server.mjs:126-155 demonstrates 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 async HTTP listener's rejected promise is not handled by Node. Here malformed percent encoding in any decodeURIComponent call, 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 in extensions/connector-namespaces/server.mjs:576-584.
async function startServer(instanceId, workshopDir) {
    const server = createServer(async (req, res) => {
        try {

Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread extensions/signals-dashboard/extension.mjs
Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread extensions/signals-dashboard/extension.mjs
Copilot AI review requested due to automatic review settings July 19, 2026 03:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Host header, so a DNS-rebinding request with Host: attacker.example:<port> and the matching Origin is 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 at server.test.mjs:76-80). Capture this server's 127.0.0.1:<port> origin after binding and validate against that value instead of req.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);

Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread extensions/signals-dashboard/extension.mjs Outdated
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
Copilot AI review requested due to automatic review settings July 20, 2026 01:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Host header. A DNS-rebinding request can send matching Host/Origin values, pass this gate, fetch dashboard data, and invoke /api/open to expose local paths. Capture the canonical 127.0.0.1:<bound-port> origin after listening, reject noncanonical hosts, and compare Origin only to that value (as in extensions/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_at field 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_id or 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, latest remains 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);

Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread agents/workshop-ta.agent.md Outdated
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
Copilot AI review requested due to automatic review settings July 20, 2026 01:45
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Host header as its canonical origin. A DNS-rebound request can therefore send matching attacker-controlled Host/Origin values, read the default dashboard response (including local signal contents), and pass the POST CSRF check. Reject any host other than the actual 127.0.0.1:<listening-port> before routing, as extensions/connector-namespaces/server.mjs:138-155 does.
        const url = new URL(req.url, `http://${req.headers.host}`);

extensions/signals-dashboard/extension.mjs:134

  • A valid JSON file can still parse to null or an array. null is added to allSignals before parsed.signal_type throws; if another valid latest signal exists, the later outcome filters dereference that retained null, 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

  • withOutcomes can be positive while gaps is empty—for example, an outcome paired with a signal that has no confidence score. In that case the summary renders a green gap null badge because null <= 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.extensions causes materialization to copy signals-dashboard into the-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

Copilot AI review requested due to automatic review settings July 20, 2026 01:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 null is pushed into allSignals before property access fails. Later outcome filters dereference every parsed value, 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, avgGap is null; JavaScript then treats it as zero in the color comparison and the summary displays a green gap 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-dashboard via x-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:

Comment thread extensions/signals-dashboard/package.json
…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
Copilot AI review requested due to automatic review settings July 20, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • readStash is called by unlocked GET/refresh paths, but expiration cleanup writes the stash file here. That bypasses withStashLock, so a cleanup racing with stashDesk/restoreDesk can 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 null is appended to allSignals before parsed.signal_type throws. If the directory also contains a valid latest signal, the later outcome filters dereference that retained null, and the outer catch drops the desk from the dashboard entirely. Reject non-object JSON before adding it to allSignals.
                    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_id but 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 when run_id is 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.extensions causes materialization to bundle signals-dashboard with the-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>


Comment on lines +38 to +42
function toScore(v, max = 5) {
const n = Number(v);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(max, n));
}
Copilot AI review requested due to automatic review settings July 20, 2026 15:29
@codemillmatt
codemillmatt merged commit 9ae2bdc into github:main Jul 20, 2026
14 of 15 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (8)

extensions/signals-dashboard/extension.mjs:700

  • The loopback server trusts the request Host when constructing both the URL and the allowed Origin. A DNS-rebinding origin can therefore make Origin equal the attacker-controlled Host, bypass the cross-site check, and read dashboard signal data or call the API. Validate every request against the actual 127.0.0.1:<localPort> host (or require an unguessable per-instance token) before serving any route.
        const url = new URL(req.url, `http://${req.headers.host}`);

extensions/signals-dashboard/extension.mjs:164

  • JSON.parse can successfully return null, an array, or a primitive. Such a file is then added to allSignals; a later s.parsed.signal_type access can throw and the outer empty catch drops the entire desk from the dashboard. Reject non-object signal documents before adding them.
                    const parsed = JSON.parse(raw);
                    const emittedMs = signalTime(parsed, s.mtimeMs);
                    allSignals.push({ parsed, mtimeMs: emittedMs, path: fp });

extensions/signals-dashboard/extension.mjs:210

  • A score of 0 is valid according to signal-write, but the > 0 condition treats either zero score as missing and suppresses the honesty gap. Check whether both raw values are present and numeric, then compute from their clamped scores.
                if (outcome && sig.self_assessment) {
                    const selfConf = toScore(sig.self_assessment.confidence);
                    const outcomeRating = toScore(outcome.quality_rating);
                    if (selfConf > 0 && outcomeRating > 0) {

extensions/signals-dashboard/extension.mjs:237

  • The || null conversion turns the documented, valid quality_rating: 0 into “no outcome,” so the dashboard hides a zero-quality result entirely. Preserve zero while still rejecting missing or nonnumeric ratings.
                    outcomeRating: outcome ? (toScore(outcome.quality_rating) || null) : null,

extensions/signals-dashboard/extension.mjs:161

  • Every dashboard refresh stats, reads, and parses every historical signal file sequentially. Because signal-write creates a new file per signal and the UI repeats this scan every five seconds, I/O and refresh latency grow without bound for the long-running desks this feature targets. Keep a per-desk cache/index (invalidated by directory changes), persist a latest-signal summary, or introduce retention so refresh work is proportional to desks/new signals rather than all history.
            for (const f of jsonFiles) {
                const fp = join(sigDir, f);
                try {
                    const s = await stat(fp);
                    const raw = await readFile(fp, "utf-8");

plugins/the-workshop/README.md:40

  • This says users must install the dashboard alongside the plugin, but this plugin's manifest includes ./extensions/signals-dashboard/ in x-awesome-copilot.extensions, and the TA documentation says it ships bundled. Clarify that the standalone install is optional; otherwise users are instructed to install the same extension twice.
It ships as a separate extension. Install it alongside the plugin to get the live canvas:

extensions/signals-dashboard/extension.mjs:87

  • readStash is also called outside withStashLock for every dashboard render and refresh action, yet it writes expired-entry cleanup here. That write can race a locked stash/restore and overwrite its newer array, silently losing a user's mutation. Keep reads side-effect free; the next locked stash/restore already writes the filtered array and can persist cleanup safely.
        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);

extensions/signals-dashboard/extension.mjs:409

  • An escalation that supplies blocked_on (or only a recommendation) but leaves reason null is valid under the documented signal shape, yet this guard hides the entire escalation detail block. Render the block when any escalation detail is present so blocked desks do not lose the actionable information.
    if (isEscalation && sig.escalationReason) {
  • Files reviewed: 15/16 changed files
  • Comments generated: 1
  • Review effort level: Medium

Comment on lines +393 to +395
const stashBtn = `<button data-act="stash" data-desk="${esc(sig.deskName)}"
style="background:none;border:1px solid #1e293b;color:#475569;padding:2px 8px;border-radius:4px;
font-size:11px;cursor:pointer;transition:all .15s;"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent PR touches agents canvas-extension PR touches canvas extensions needs-review:MEDIUM Contributor reputation check flagged MEDIUM risk new-submission PR adds at least one new contribution plugin PR touches plugins skills PR touches skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants