jobbsok/CLAUDE.md
Kjell Tore Guttormsen dcd3eae534 feat(port): move five bash entry points to Python and drop bash from .mcp.json
jobbsok could not start on stock Windows. `.mcp.json` named `bash` as the
command, and the five entry points behind it reached for grep, sed, find, awk,
zip and unzip. `bootstrap.sh` built the virtualenv, so a Windows adopter could
not even reach an interpreter. Two adopters are waiting and neither is
guaranteed to be on macOS, so this is the install, not a rough edge.

The Python layer was already clean -- no /tmp, no /usr, no os.uname, no home
directory assumption -- so only the shell layer moved. Behaviour is carried
over unchanged; the deliberate exceptions are listed in docs/.

THE ONE OPEN DECISION, AND WHY IT WAS FORCED

How does .mcp.json start an interpreter without a POSIX shell, when it is
called python3 on macOS and python or py on Windows? Measured against the
installed CLI, not assumed:

  - The plugin mcpServers stdio schema has NO platform-conditional form. A
    config carrying invented windows/darwin/platform keys was accepted and the
    keys were silently discarded -- it fails quietly, not loudly.
  - ${VAR:-default} IS expanded, in command, args and env.
  - ${VAR} without a default is not safe: unset, it is passed through
    unexpanded, so the spawn would try to run a program named ${VAR}.
  - Windows spawns with shell:false, so a .py path as command is out.
  - No single literal works. On this Mac, python and py are not on PATH.

So the default form is the only lever the schema offers:
"${JOBBSOK_LAUNCH_PYTHON:-python3}". macOS and Linux keep working with nothing
set; Windows sets one variable and needs no shell.

A SECOND VARIABLE, NOT A REUSE OF JOBBSOK_PYTHON

JOBBSOK_PYTHON names the interpreter to SERVE on: the launcher treats it as an
explicit operator choice, so it wins over the bootstrapped virtualenv. A
Windows adopter setting it merely to spell `python` would silently bypass that
virtualenv and serve WITHOUT the ingestion guard. JOBBSOK_LAUNCH_PYTHON only
says how to start the launcher. A test asserts the two never collapse into one.

O4 IS LEFT STANDING

The launcher still gates on the interpreter's version rather than on the guard
being importable. That is the shell version's semantics carried over on
purpose: harmless while nothing writes, a defect from M2, and an M2 decision.

VERIFY

  - pytest tests/                      -> 124 passed, exit 0 (was 112)
  - grep -c '"command": "bash"' .mcp.json -> 0
  - git ls-files 'scripts/*.sh'        -> 0
  - README install block names Windows, and neither WSL nor Git Bash
  - server started end to end exactly as .mcp.json expands, and answered
    initialize and tools/list
  - the ported probe checker reproduces the shell version's output and exits 0

NOT MEASURED, AND NOT ASSUMED

Nothing here has ever run on Windows. Whether Cowork on Windows bridges to a
host-side stdio MCP as it does on this Mac is unmeasured -- docs/cowork-probe.md
covered macOS only. docs/cross-platform-port.md says what a Windows probe would
have to measure, and records two findings left deliberately untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:02:03 +02:00

70 lines
3.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# jobbsok
Local-first, read-only job search workspace for a single operator, packaged as
a marketplace plugin. The build brief is the contract:
`docs/build-brief.md` — read it whole before writing any file.
## Context
One plugin for Claude Code and Claude Cowork. Cowork is the day-to-day operator
surface; Claude Code is the development surface. Skills under `skills/*/SKILL.md`
(no `commands/`), deterministic logic in `scripts/*.py`, MCP servers declared in
`.mcp.json` (browser over CDP, read-only IMAP) — both optional; every skill
degrades to manual paste.
Operator-facing content (skill output, `sak.md`, drafts) is Norwegian bokmål.
Code, identifiers, file names and docs are English.
## Invariants
- **This repo has a PUBLIC remote (`open/jobbsok`).** `STATE.md` is LOCAL-ONLY
and gitignored from the first commit. Repos on a private remote track theirs.
- **Read-only against external sites.** No form submit, no send, no apply.
- **No credential handling.** Attaches to an already-authenticated browser
over CDP. Mail credentials only via environment variables.
- **Human approval before any profile mutation.** `laering` proposes diffs.
- **Local-only data.** Workspace never committed; no telemetry.
- **Untrusted content stays untrusted.** Every listing and email body passes
`scripts/guard_ingest.py` (llm-ingestion-guard, pinned tag) before persist.
The trust boundary is the write.
- **Append-only logs.** `beslutninger.jsonl`, `logg.jsonl`: corrections are new
entries, never edits.
- **No autonomous scheduling.** Nothing polls, nothing runs in background.
- **Build in milestone order (brief §11, M1–M6).** Do not skip ahead.
- **`${CLAUDE_PLUGIN_ROOT}` for every intra-plugin path.** Never hardcode.
- **No shell entry point, ever.** Every entry point is Python and the
standard library, and `.mcp.json` starts the server by naming an
interpreter, never a shell. The plugin runs on macOS, Linux and
Windows; a new `scripts/*.sh` breaks Windows and the suite says so.
The interpreter name is `${JOBBSOK_LAUNCH_PYTHON:-python3}` because
the plugin schema has no platform-conditional command, and that
variable is NOT `JOBBSOK_PYTHON` — see `docs/cross-platform-port.md`.
## Commands
```bash
python3 -m pytest tests/ # zero-network tests (brief §12)
```
## Release
Polyrepo rule: a version bump is not finished until the tag `vX.Y.Z` is pushed
**and** the catalog `ref` is bumped to it. Use `release-plugin.mjs`, never a
hand-edited `ref`. The catalog README entry for a new plugin is added by hand once.
## Communication patterns
### Linking to local files
When pointing to local files in responses, always use markdown link syntax with a descriptive name:
- Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks `<file://...>`.
- Always use absolute paths. Never `~/` or relative paths.
- For multiple files, render as a bullet list of named markdown links.
Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner.
Example:
- [Brief](file:///absolute/path/to/brief.html)
- [Research summary](file:///absolute/path/to/research/summary.md)