docs(m1): record Cowork probe results and degradation branch

Plan Step 1. Builds the probe vehicle and the checklist; does NOT answer it.

The four questions all require the operator in a live Cowork session on this
Mac, and that is a fact about the measurement rather than a caveat: measured
2026-09-04, the session mode is not readable from disk (cowork_settings.json
carries only extraKnownMarketplaces; the desktop config carries
coworkNetworkMode and a trusted-folder list, neither states where a session
runs), and the other three each need a plugin installed in a live session.

The step's Verify is therefore RED on purpose and stays red until the operator
answers:
  test "$(grep -cE '...' docs/cowork-probe.md)" = 4   -> exit 1 (0 of 4 matched)
Validated in both directions: a fully answered copy exits 0, a copy with three
of four answered exits 1. The gate can pass, so its failure means something.

Probe vehicle: jobbsok-probe, exactly four files under
tests/fixtures/cowork-probe/ -- manifest, skills/probe-versjon/SKILL.md,
.mcp.json declaring the stdio server probe-tools, and probe_tools.py (standard
library only). Smoke-tested from Claude Code: initialize handshake completes,
tools/list returns probe_ping, tools/call returns an interpreter version, an
unknown tool name is rejected. What is NOT verified is whether Cowork ever
starts it -- that is the question.

Archive is built from an explicit four-file include list, never zip -r over the
repo root; verified to contain no .git, STATE.md, .venv or .claude/ entry.

The degradation branch for each possible nei is written down now, before any
answer is known, so it is not decided under pressure later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-04 17:22:59 +02:00
commit 0f92b2a1c3
5 changed files with 307 additions and 0 deletions

123
docs/cowork-probe.md Normal file
View file

@ -0,0 +1,123 @@
# Cowork probe
Plan Step 1 (M1). An empirical record of what this operator's Cowork
installation actually does, measured before any code depends on it.
Operator decision 7 — Python as a CLI plus a host-side stdio MCP server — is
void if host-side servers never appear in a Cowork session. That is not
knowable from Claude Code, so it is measured here first, and the milestone that
depends on it carries a written degradation branch either way.
**Status: unanswered.** All four questions require the operator sitting in a
Cowork session on this Mac. The executor session built the probe vehicle and
this checklist; it did not and could not answer them. The verify command for
this step is written so an unanswered checklist *fails*:
```bash
test "$(grep -cE '^- (Sesjonsmodus|Host-MCP|python3|CLAUDE_PLUGIN_ROOT): +(lokal VM|sky|ja|nei)\b' docs/cowork-probe.md)" = 4
```
A bullet still reading `ubesvart` does not match, the count drops below four,
and the command exits non-zero. That is the intended behaviour until the
operator has run the probe.
## Results
Overwrite `ubesvart` with one of the allowed answers, and put the date of the
measurement in the Measured column. A `nei` is a result, not a failure — it
selects the degradation branch below.
- Sesjonsmodus: ubesvart
- Host-MCP: ubesvart
- python3: ubesvart
- CLAUDE_PLUGIN_ROOT: ubesvart
| Bullet | What the operator does in Cowork | Allowed answer | Measured |
|---|---|---|---|
| `- Sesjonsmodus:` | Open a Cowork session and read whether it is a local VM or cloud | `lokal VM` or `sky` | |
| `- Host-MCP:` | Upload `jobbsok-probe.plugin` via Customize -> Plugins; does `probe-tools` appear as a connector, and does `probe_ping` return? | `ja` or `nei` | |
| `- python3:` | Run the `probe-versjon` skill; does it succeed in invoking `python3`, and which version does it report? | `ja` or `nei`, followed by the reported version | |
| `- CLAUDE_PLUGIN_ROOT:` | Run `probe-versjon`; does it print a resolved absolute path, or the literal variable name? | `ja` or `nei` | |
### What is already known, and what is not
Measured 2026-09-04 from Claude Code, before the probe was built:
- Cowork is installed and available on this Intel Mac.
- The **session mode is not readable from disk.** `cowork_settings.json` carries
only `extraKnownMarketplaces`; the desktop config carries `coworkNetworkMode`
and a trusted-folder list, and neither states where a session runs. So
`- Sesjonsmodus:` cannot be answered by reading files — it has to be read off
a live session.
- The other three each require a plugin installed in a live Cowork session.
Nothing about them is decidable from Claude Code.
## The probe vehicle
`jobbsok-probe` — a throwaway test plugin, deliberately **not** the `jobbsok`
plugin itself. `jobbsok`'s own `.mcp.json` (Step 12) and packaging script
(Step 13) land eleven steps later, and this probe must not wait for them.
It holds exactly four files, under `tests/fixtures/cowork-probe/`:
| File | What it is for |
|---|---|
| `.claude-plugin/plugin.json` | Manifest. Name `jobbsok-probe`, version `0.0.1`. The version is what `probe-versjon` echoes, so a mismatch reveals a cached build. |
| `skills/probe-versjon/SKILL.md` | A skill that reports the manifest version, the resolved `${CLAUDE_PLUGIN_ROOT}`, and whether it can invoke `python3`. Answers three of the four questions. |
| `.mcp.json` | Declares one stdio server, `probe-tools`. Its appearance in the session is the whole `- Host-MCP:` question. |
| `probe_tools.py` | Standard-library JSON-RPC server exposing one tool, `probe_ping`, returning the interpreter actually running it. No third-party imports: a server that failed on a missing dependency would answer a different question than the one asked. |
Verified from Claude Code 2026-09-04: the server completes an `initialize`
handshake, lists `probe_ping`, returns an interpreter version on
`tools/call`, and rejects an unknown tool name. What is unverified is whether
Cowork ever starts it.
### Building the archive
Run from the repository root. The file list is explicit, and that is the point:
```bash
cd tests/fixtures/cowork-probe && \
zip -X /tmp/jobbsok-probe.plugin \
.claude-plugin/plugin.json \
.mcp.json \
probe_tools.py \
skills/probe-versjon/SKILL.md
```
Never `zip -r` over the repository root. A recursive archive of the repo would
ship `.git`, `STATE.md`, `.claude/` and the virtualenv into an uploaded plugin.
The include list above names the four files and can carry nothing else.
Then upload `/tmp/jobbsok-probe.plugin` through Customize -> Plugins, run the
`probe-versjon` skill, and check whether `probe-tools` appears as a connector.
## Degradation branch
Written now, so the answer does not get decided under pressure later.
**If `- Host-MCP:` is `nei`** — host-side stdio servers do not appear in this
Cowork installation:
1. Operator decision 7's Cowork half is unavailable. Nothing changes for Claude
Code, where the scripts are invoked directly.
2. Every script-backed skill degrades to reading files that the Claude Code CLI
produced. The skills stay useful; they stop being able to compute.
3. `Verifiser i Cowork` for the affected milestones means "the skills load and
read files produced from Claude Code" — not "the connector answers".
4. The degradation is recorded in `STATE.md`.
This is risk H12's mitigation, and it is why the probe is the first step of the
build rather than a later check.
**If `- python3:` is `nei`** — a skill cannot invoke `python3` in a Cowork
session: the same degradation applies, and more broadly, since it removes the
CLI half too. Escalate to the operator before building further on decision 7.
**If `- CLAUDE_PLUGIN_ROOT:` is `nei`** — the variable does not resolve: every
intra-plugin path in the plan is affected, not just this milestone's. Escalate;
do not paper over it with hardcoded paths.
**If `- Sesjonsmodus:` is `sky`** — sessions run in the cloud, and host-side
servers cannot reach this machine at all. `- Host-MCP:` should then read `nei`,
and its branch applies. If the two disagree, the disagreement is the finding.

View file

@ -0,0 +1,5 @@
{
"name": "jobbsok-probe",
"version": "0.0.1",
"description": "Throwaway probe plugin: measures whether a plugin-declared stdio MCP server appears in a Cowork session, whether a skill can invoke python3, and whether ${CLAUDE_PLUGIN_ROOT} resolves. Not part of the jobbsok plugin."
}

8
tests/fixtures/cowork-probe/.mcp.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"mcpServers": {
"probe-tools": {
"command": "python3",
"args": ["${CLAUDE_PLUGIN_ROOT}/probe_tools.py"]
}
}
}

View file

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Minimal stdio MCP server for the Cowork probe (plan Step 1).
Standard library only, and deliberately so: the point of the probe is to learn
whether a plugin-declared stdio server appears in a Cowork session at all. A
server that failed because a dependency was missing would answer a different
question than the one being asked.
Exposes one tool, ``probe_ping``, which reports the interpreter that is actually
running it. Transport is newline-delimited JSON-RPC 2.0 over stdin/stdout, which
is what MCP stdio means; nothing is written to stdout except protocol messages.
"""
import json
import sys
SERVER_NAME = "probe-tools"
SERVER_VERSION = "0.0.1"
DEFAULT_PROTOCOL_VERSION = "2024-11-05"
TOOLS = [
{
"name": "probe_ping",
"description": (
"Return the interpreter version and executable path of the process "
"serving this tool. Used to establish whether a plugin-declared "
"stdio MCP server runs at all in a Cowork session."
),
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
}
]
def log(message):
"""Diagnostics go to stderr; stdout carries protocol traffic only."""
print("[probe-tools] %s" % message, file=sys.stderr, flush=True)
def ping_payload():
return {
"server": SERVER_NAME,
"server_version": SERVER_VERSION,
"python_version": sys.version,
"python_executable": sys.executable,
}
def handle(request):
"""Return a response dict, or None for a notification (no reply expected)."""
method = request.get("method")
req_id = request.get("id")
if req_id is None:
# A notification. `notifications/initialized` is the one we expect.
return None
if method == "initialize":
params = request.get("params") or {}
# Echo the client's protocol version when it offers one; guessing a
# newer version than the client speaks is how handshakes fail silently.
protocol_version = params.get("protocolVersion") or DEFAULT_PROTOCOL_VERSION
result = {
"protocolVersion": protocol_version,
"capabilities": {"tools": {}},
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
}
elif method == "tools/list":
result = {"tools": TOOLS}
elif method == "tools/call":
params = request.get("params") or {}
if params.get("name") != "probe_ping":
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32602, "message": "unknown tool: %r" % params.get("name")},
}
result = {
"content": [
{"type": "text", "text": json.dumps(ping_payload(), indent=2, sort_keys=True)}
],
"isError": False,
}
elif method == "ping":
result = {}
else:
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32601, "message": "method not found: %r" % method},
}
return {"jsonrpc": "2.0", "id": req_id, "result": result}
def main():
log("started on %s" % sys.version.replace("\n", " "))
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
request = json.loads(line)
except ValueError as exc:
log("undecodable line: %s" % exc)
continue
response = handle(request)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,57 @@
---
name: probe-versjon
description: Report what this probe plugin can actually see from inside a Cowork session - its own manifest version, the resolved value of ${CLAUDE_PLUGIN_ROOT}, and whether python3 can be invoked at all. Use when running the jobbsok Cowork probe, or when the operator asks to check Cowork session mode, host MCP visibility, python3 availability or plugin-root resolution.
---
# probe-versjon
This skill exists to answer three of the four questions in
`docs/cowork-probe.md`. It measures; it decides nothing.
Run all three checks and report each result verbatim. Do not summarise, do not
infer a result you did not observe, and do not fill in a value from memory of
how plugins usually behave — an unmeasured answer is worse than no answer here,
because the whole point of the probe is that these behaviours are unknown on
this machine.
## 1. Manifest version
Read `${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json` and report the `version`
field. Expected: `0.0.1`. A different value means the session served a cached or
different build, and every other result below is about that other build.
## 2. Plugin-root resolution
Report the value `${CLAUDE_PLUGIN_ROOT}` resolved to.
- If it is an absolute filesystem path: the answer to `- CLAUDE_PLUGIN_ROOT:` is `ja`.
- If it is the literal, unexpanded string `${CLAUDE_PLUGIN_ROOT}`, or empty: the answer is `nei`.
## 3. python3 invocation
Run exactly:
```bash
python3 -c 'import sys; print(sys.version)'
```
Report whether the command ran and what it printed.
- If it ran: the answer to `- python3:` is `ja`, followed by the reported version.
- If it could not run (no such command, blocked, sandbox refusal): the answer is
`nei`, followed by the error text.
## Reporting
Print the three results as a block the operator can copy into
`docs/cowork-probe.md`:
```
- python3: <ja|nei> <version or error>
- CLAUDE_PLUGIN_ROOT: <ja|nei> (<resolved value>)
manifest version: <value>
```
The fourth question (`- Host-MCP:`) is not this skill's to answer: it is about
whether the `probe-tools` connector appears in the session, which the operator
reads off the Cowork UI and confirms by calling `probe_ping`.