Skip to content

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

Open
jennyf19 wants to merge 20 commits into
github:mainfrom
jennyf19:add-the-workshop
Open

feat: add the-workshop plugin — multi-agent coordination with persistent desks#2343
jennyf19 wants to merge 20 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 14 changed file(s).

Severity Count
🔴 High 0
🟠 Medium 1
ℹ️ 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
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 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 12 out of 12 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

plugins/the-workshop/README.md:25

  • The manifest installs workshop-create, and the TA relies on it for the first setup workflow, but the plugin's component inventory omits it. Add it here so users can discover all six installed components and understand how to create the workshop root.
| Skill | [Bench Read](../../skills/bench-read/) | Read shared artifacts from the workspace where desks leave work for each other |

Comment thread skills/workshop-create/SKILL.md Outdated
Comment thread skills/bench-read/SKILL.md Outdated
The dashboard is the centerpiece — real-time agent coordination
view showing desk status, signal types, intent text, outcome
pairing with honesty gap, token usage, and stash/restore controls.

Includes all updates from the-workshop PRs #3-#7:
- Empty state guidance for new users
- Token usage display per desk
- TA partnership signals + Cairn awareness
- Intent-as-text (execution signals use descriptive text)
- Outcome signal pairing with honesty gap calibration
- Open desk button
- Subtype labels (done/checkpoint/blocked/hands-up)

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:04
@github-actions github-actions Bot added the canvas-extension PR touches canvas extensions label Jul 19, 2026

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 14 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (2)

skills/workshop-create/SKILL.md:52

  • These directories are empty, so Git will not include them in the commit from step 4. A cloned workshop therefore loses the advertised desks/ and bench/ scaffold. Create tracked sentinel files in both directories before committing.
     desks/
     bench/

plugins/the-workshop/README.md:25

  • The component table omits the new workshop-create skill even though this plugin manifest installs it. Users cannot discover the workshop setup flow from the plugin README; add it to the table.
| Skill | [Bench Read](../../skills/bench-read/) | Read shared artifacts from the workspace where desks leave work for each other |

Comment thread extensions/signals-dashboard/.github/plugin/plugin.json
Comment thread extensions/signals-dashboard/extension.mjs Outdated
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
Comment thread skills/signal-write/SKILL.md Outdated
Comment thread plugins/the-workshop/README.md Outdated
Comment thread extensions/signals-dashboard/extension.mjs
Comment thread extensions/signals-dashboard/extension.mjs
…nvas validates

The signals-dashboard extension was missing its required screenshot asset, so it failed awesome-copilot's validateExtensionManifest (logo must equal "assets/preview.png" and the file must exist) and never materialized -- meaning the Cairn canvas would not ship to the GHCP app. Adds the 1024x1024 preview.png and the convention logo field. Validated locally: node eng/validate-plugins.mjs -> extension signals-dashboard is valid; all 70 plugins + 19 extensions pass.

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:21

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 (10)

extensions/signals-dashboard/extension.mjs:192

  • esc only escapes HTML, but desk names are later interpolated inside single-quoted JavaScript in the inline onclick attributes. A directory name containing ' can terminate the argument and execute script when the button is clicked. Avoid inline JavaScript and pass the value through an escaped data-* attribute/event listener at all three call sites.
function esc(s) {
    return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

extensions/signals-dashboard/extension.mjs:537

  • The desk name and returned path come from filesystem/API data and are inserted with innerHTML. A desk directory containing HTML can therefore execute markup when opened. Use text nodes/textContent for this toast.
                toast.innerHTML = '📂 <b>' + name + '</b> desk ready' +
                    '<div style="font-size:10px;color:#475569;margin-top:4px;">Path: ' + (data.deskPath || name) + '</div>';

extensions/signals-dashboard/extension.mjs:584

  • The decoded route value is joined directly to the workshop path. Values such as %2e%2e%2f%2e%2e%2f.ssh escape desks/, allowing this endpoint to probe arbitrary directories and return their absolute paths. Reject separators and dot segments before resolving the desk path.
            const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
            for (const subdir of ["desks", "classroom"]) {
                const deskPath = join(workshopDir, subdir, deskName);

extensions/signals-dashboard/extension.mjs:680

  • The canvas action has the same path traversal as the HTTP route: a deskName containing ../ escapes the workshop and discloses arbitrary directory paths. Validate the name before joining it to either desk root.
                        for (const subdir of ["desks", "classroom"]) {
                            const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);

extensions/signals-dashboard/extension.mjs:634

  • Subtracting the stash length is incorrect when a stashed desk has been deleted, was stashed through the action without existing, or shares a name across the two scanned roots; it can report the wrong count or even a negative value. Count the same filtered signal set used by renderDashboard.
                        return { signals, stashed, activeCount: signals.length - stashed.length };

skills/signal-write/SKILL.md:93

  • The plugin manifest now bundles ./extensions/signals-dashboard/, so this note incorrectly tells installed users that the extension is unavailable from the awesome-copilot package. Document the bundled consumer instead.
> **Note:** The signals-dashboard extension (shipped with the full
> source repo, not the awesome-copilot package) reads `subtype`
> when present and falls back to `signal_type` for display. If
> consuming signals in your own tooling, prefer `subtype` for the
> specific state.

plugins/the-workshop/README.md:37

  • The manifest bundles the Cairn extension into the-workshop, so requiring a separate upstream plugin install is now inaccurate and may lead users to install a second copy. State that the dashboard is included with this plugin.
The Workshop ships a **canvas extension** (🪨 Cairn) that shows the pulse of every desk — score bars, patterns, escalations — auto-refreshing in the GHCP app. Install the full plugin from [jennyf19/the-workshop](https://github.com/jennyf19/the-workshop) to get the dashboard.

agents/workshop-ta.agent.md:110

  • This still directs the TA to the upstream plugin, but plugins/the-workshop/.github/plugin/plugin.json now includes ./extensions/signals-dashboard/. Installed users already have the canvas, so the coordinator should describe it as bundled.
If the Workshop's canvas extension (🪨 Cairn) is installed — it
ships with the full plugin at
[jennyf19/the-workshop](https://github.com/jennyf19/the-workshop)
— the operator can open a live dashboard showing every desk's

plugins/the-workshop/README.md:25

  • The manifest installs workshop-create, but the component inventory omits it, leaving the documented plugin contents incomplete. Add the bundled workshop-creation skill to this table.
| Skill | [Bench Read](../../skills/bench-read/) | Read shared artifacts from the workspace where desks leave work for each other |

extensions/signals-dashboard/extension.mjs:158

  • Outcome fields are copied from JSON without type checks, then outcomeEffort/outcomeRating are interpolated as HTML and outcomeIssues is assumed to support .map(). A malformed file can inject markup or crash the whole dashboard. Normalize the rating and enum, and accept issues only as an array.
                    outcomeRating: outcome?.quality_rating || null,
                    outcomeEffort: outcome?.effort_to_merge || null,
                    outcomeIssues: outcome?.issues_found || [],

Comment thread extensions/signals-dashboard/extension.mjs
Comment thread extensions/signals-dashboard/extension.mjs Outdated
Comment thread extensions/signals-dashboard/extension.mjs
Comment thread extensions/signals-dashboard/extension.mjs Outdated
…19 repo

The signals-dashboard canvas extension bundles with the-workshop
plugin (x-awesome-copilot.extensions), so installing the plugin from
awesome-copilot includes the dashboard. Drop the pointers telling
users to install from jennyf19/the-workshop.

Addresses PR review comments on README (Cairn Dashboard section),
signal-write SKILL note, and workshop-ta agent viewing-signals note.

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:36

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 1 comment.

Comments suppressed due to low confidence (12)

extensions/signals-dashboard/extension.mjs:145

  • Signal JSON is trusted without type validation, but these values are later interpolated into HTML and CSS. For example, a partnership signal can put HTML in self_assessment.confidence, which scoreBar renders unescaped, causing script execution when the dashboard loads. Validate the signal schema and clamp numeric fields before adding them to results.
                    confidence: sig.self_assessment?.confidence || 0,
                    accuracy: sig.self_assessment?.accuracy || 0,
                    completeness: sig.self_assessment?.completeness || 0,
                    whatWorked: sig.patterns?.what_worked || "",
                    whatWasHard: sig.patterns?.what_was_hard || "",

extensions/signals-dashboard/extension.mjs:311

  • HTML escaping does not make a desk name safe inside a JavaScript string. A directory named x');alert(1);// breaks out of this inline handler when the card is rendered. Serialize the argument as JavaScript before HTML-escaping the attribute.
    const stashBtn = `<button onclick="stashDesk('${esc(sig.deskName)}')"

extensions/signals-dashboard/extension.mjs:320

  • This repeats the JavaScript-context injection in the Open handler: a quote in a filesystem desk name can escape the argument and execute code. Serialize the argument instead of placing HTML-escaped text in a JavaScript string literal.
    const openBtn = noSignal ? "" : `<button onclick="openDesk('${esc(sig.deskName)}')"

extensions/signals-dashboard/extension.mjs:443

  • Stashed names are persisted data and can also contain quotes, so HTML escaping alone leaves this inline JavaScript handler injectable. Serialize the argument before embedding it.
        <button onclick="restoreDesk('${esc(entry.name)}')"

extensions/signals-dashboard/extension.mjs:537

  • Both name and data.deskPath originate from filesystem/API data and are concatenated into innerHTML. A crafted desk name therefore executes markup when Open is clicked. Build these nodes with textContent instead.
                toast.innerHTML = '📂 <b>' + name + '</b> desk ready' +
                    '<div style="font-size:10px;color:#475569;margin-top:4px;">Path: ' + (data.deskPath || name) + '</div>';

extensions/signals-dashboard/extension.mjs:584

  • The decoded URL segment is passed directly to join, so an encoded separator such as %2F..%2F.. can escape desks/ and make this endpoint report arbitrary directories as desks. Reject path components before resolving the candidate.
            const deskName = decodeURIComponent(url.pathname.split("/api/open/")[1]);
            for (const subdir of ["desks", "classroom"]) {
                const deskPath = join(workshopDir, subdir, deskName);

extensions/signals-dashboard/extension.mjs:680

  • The action input has the same path-escape issue as the HTTP route: ../.. can resolve outside the workshop and be returned as a desk path for session navigation. Require a single safe path component before calling join.
                        // Check both desks/ and classroom/
                        for (const subdir of ["desks", "classroom"]) {
                            const deskPath = join(entry.workshopDir, subdir, ctx.input.deskName);

extensions/signals-dashboard/extension.mjs:530

  • Clicking Open only fetches the desk path and later displays a toast; it never creates or navigates to a session. This makes the advertised Open control a no-op. Either integrate the host's session-opening mechanism or relabel the control to describe what it actually does.
        async function openDesk(name) {
            const res = await fetch('/api/open/' + encodeURIComponent(name), { method: 'POST' });
            const data = await res.json();

skills/workshop-create/SKILL.md:47

  • gh repo create --clone clones into the current working directory, but this workflow does not select or validate the requested destination until after that command. Running it from an existing repository can therefore create the nested repository that the skill promises never to create. Obtain the parent directory, run the rev-parse check, and execute the clone from that verified parent before scaffolding.
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)

skills/workshop-create/SKILL.md:54

  • These two directories are empty, so the subsequent Git commit will not track them and a later clone will not contain the advertised workshop structure. Add tracked placeholders (for example .gitkeep files) or initial documentation files inside both directories.
   <name>/
     desks/
     bench/
     CAIRN.md
     README.md

skills/bench-read/SKILL.md:35

  • Workshop creation establishes bench/ as the shared workspace, but this skill instead defines the entire workshop root as the bench and never directs readers to bench/. That leaves agents with conflicting artifact locations. Define one canonical bench path and update the creator, TA, and reader consistently.
## Where to look

The bench is the workshop directory itself and its subdirectories.
Common patterns:

plugins/the-workshop/README.md:25

  • The manifest installs workshop-create and the TA instructs users to invoke it, but the component table omits it. Add the fifth skill so the plugin README accurately documents all installed capabilities.
| Skill | [Bench Read](../../skills/bench-read/) | Read shared artifacts from the workspace where desks leave work for each other |

Comment on lines +89 to +95
for (const f of jsonFiles) {
const fp = join(sigDir, f);
try {
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 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Parking this for a follow-up rather than fixing in this PR, and flagging it for a maintainer call. This is a local, single-user dashboard scoped to one workshop: signal files are small JSON and the poll runs every 5s, so real-world I/O is modest. f9061f6 also removes the 5s DOM churn when nothing changed. A proper fix (mtime-indexed signal cache, or scanning only the latest candidate + its paired outcome) is worthwhile but a larger change than the rest of this review round — happy to do it as a separate PR, or fold it in here if you would prefer it in scope.

…d malformed signals

Address PR review findings on the Cairn canvas extension:
- XSS: replace inline onclick handlers (which used HTML-escape that
  does not escape single quotes) with event delegation via
  data-act/data-desk attributes and one document click listener that
  survives the innerHTML auto-refresh.
- Path traversal: add isValidDeskName() and enforce it in every HTTP
  and canvas-action handler that takes a desk name (reject empty, /,
  \\, null byte, '.' and '..').
- Crash safety: String()-coerce in esc()/truncate() and guard
  outcomeIssues with Array.isArray so a malformed signal cannot take
  down the whole dashboard render.
- Honest UI: the per-desk button no longer claims to 'open' a desk;
  it is relabeled 'path' and copies the desk's filesystem path to the
  clipboard with an accurate toast built via textContent (not innerHTML).
- Correctness: outcome signals only pair with a signal when emitted at
  or after it (within 1hr), and activeCount is computed by excluding
  stashed desks instead of subtracting counts (no longer goes negative).

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:44

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 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/*.json file 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_id but 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 lack run_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-create scaffolds bench/ as the shared workspace, but this reader instead defines the entire root and desk directories as the bench and never directs agents to bench/. Producers and consumers can therefore place and search for cross-desk artifacts in different locations. Define bench/ 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

Comment thread skills/workshop-create/SKILL.md Outdated
Comment thread extensions/signals-dashboard/extension.mjs
…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
Copilot AI review requested due to automatic review settings July 19, 2026 02: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 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 Host header both here and in isCrossSiteRequest. A DNS-rebinding page can therefore send Host: attacker.example:<port> and the matching Origin, pass the check, read workshop signals, and invoke stash/restore/open. Validate the Host against the canonical 127.0.0.1:<actual-port> before serving requests (the hardened pattern is demonstrated in extensions/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 in decodeURIComponent, 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 in try/catch and 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 --clone clones 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 running gh 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-create skill, 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-write continually 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/ and bench/ 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 creates bench/ 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. Define bench/ as the cross-desk artifact location while retaining desks/<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
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 on lines +72 to +76
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 on lines +542 to +546
@keyframes pulse {
0%, 100% { border-color: #dc2626; }
50% { border-color: #7f1d1d; }
}
#content { transition: opacity .15s; }
Comment on lines +778 to +779
name: "open_desk",
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.",
Comment on lines +569 to +573
function showToast(title, detail) {
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);' +
'background:#1e3a5f;color:#7dd3fc;padding:10px 20px;border-radius:8px;font-size:13px;' +
'border:1px solid #3b82f6;z-index:999;max-width:90%;text-align:center;';
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 on lines +131 to +137
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 });
// Latest non-outcome signal (execution, partnership, escalation)
if ((parsed.signal_type || "execution") !== "outcome" && s.mtimeMs > latestTime) {
latestTime = s.mtimeMs; latest = { parsed, mtimeMs: s.mtimeMs };
}
} catch {}
}
if (!latest) continue;
Comment on lines +306 to +308
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>`
: "";
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.

2 participants