Skip to content

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

Open
jennyf19 wants to merge 24 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 24 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 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 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));
}
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