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:
parent
2f99ac5951
commit
0f92b2a1c3
5 changed files with 307 additions and 0 deletions
123
docs/cowork-probe.md
Normal file
123
docs/cowork-probe.md
Normal 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.
|
||||||
5
tests/fixtures/cowork-probe/.claude-plugin/plugin.json
vendored
Normal file
5
tests/fixtures/cowork-probe/.claude-plugin/plugin.json
vendored
Normal 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
8
tests/fixtures/cowork-probe/.mcp.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"probe-tools": {
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["${CLAUDE_PLUGIN_ROOT}/probe_tools.py"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
tests/fixtures/cowork-probe/probe_tools.py
vendored
Normal file
114
tests/fixtures/cowork-probe/probe_tools.py
vendored
Normal 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())
|
||||||
57
tests/fixtures/cowork-probe/skills/probe-versjon/SKILL.md
vendored
Normal file
57
tests/fixtures/cowork-probe/skills/probe-versjon/SKILL.md
vendored
Normal 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`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue