Commit graph

18 commits

Author SHA1 Message Date
58db24ba24 fix(package): include pyproject.toml so an installed copy can bootstrap
scripts/bootstrap.py resolves --group against <repo-root>/pyproject.toml,
but package_plugin.py's INCLUDE tuple never shipped that file. An archive
built and unpacked elsewhere could never bootstrap (F1). Verify: built an
archive with package_plugin.py, unpacked it to a tmp dir, ran
scripts/bootstrap.py against the unpacked copy -- "bootstrap: guard 1.3.0"
/ "bootstrap: ok", exit 0. Also fixed two stale pyproject.toml comments
still naming the pre-port scripts/bootstrap.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:43:25 +02:00
c7db9f16a0 feat(m2): expose status and daily tools through jobbsok-tools 2026-09-05 22:09:17 +02:00
bdb6291042 feat(m2): add dagens text view with golden output 2026-09-05 22:04:36 +02:00
06b45e9168 feat(m2): add append-only decision log and beslutning skill 2026-09-05 22:00:13 +02:00
12125c1932 feat(m2): add deterministic sak_status cli with golden output
--workspace is required with no environment fall-back: this process runs
unsandboxed, and a status run that quietly picked a workspace is one nobody
can audit. --check exits 1 on a stale cache, distinct from 2 for a failed run.

Byte-stability is measured, not asserted: the CLI is run twice under two
PYTHONHASHSEED values and the bytes compared, in both output formats. The
write boundary gets two digest tests rather than one -- without --oppdater
every byte stays put, with it exactly the diverging case's four cached keys
move and logg.jsonl, the body and every other file are byte-identical.

Co-Authored-By: Claude <claude-opus-5>
2026-09-05 21:40:32 +02:00
4669cc363d feat(m2): implement event enum and status transition table
Status derives from logg.jsonl alone; sak.md frontmatter is a cache and the
log wins where they disagree (risk H3). --oppdater is the only writer of the
four cached keys, and it leaves a key alone when the derivation is None --
neste_frist in vurderer/soker is the operator's application deadline and no
refresh may eat it.

Two rows of the plan's table are not 5.3 events. Decision ja and operator
close reach the case log as the decision-log records build-brief 5.7 already
mirrors there, so the event enum stays closed at eleven.

Co-Authored-By: Claude <claude-opus-5>
2026-09-05 21:34:04 +02:00
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
0e3295f52b test(m1): assert plugin manifest and pin a build stamp 2026-09-05 06:52:40 +02:00
89bcd1b038 feat(m1): add jobbsok-tools stdio mcp server and launcher 2026-09-05 06:48:55 +02:00
28a950bcea fix(m1): replace transcribed operator values in fixtures with invented ones 2026-09-04 21:35:24 +02:00
3c74968ec1 feat(m1): compute scores from sub-scores and weights in a script 2026-09-04 21:27:38 +02:00
ac7de63dc1 feat(m1): validate kandidat.md against the section 5.1 contract 2026-09-04 21:18:33 +02:00
aa7fe4b553 feat(m1): add append-only jsonl writer with folding reader 2026-09-04 19:28:28 +02:00
a0874a5e17 feat(m1): add strict frontmatter parser with documented grammar 2026-09-04 19:26:18 +02:00
a4e6b44df4 fix(m1): correct two wrong queries in the Cowork probe check
Both were caught by running the check against a state whose answer was already
known from the screen, which is the only reason they were caught at all. A
check that reports PENDING for a thing that has plainly happened is worse than
no check: it invites the reader to distrust the instrument or, worse, to
believe it.

1. It looked for a directory named after the plugin. Cowork installs into
   rpm/plugin_<opaque id>/, so the name appears in no path -- only in
   rpm/manifest.json, which is now what gets read. The old code also used an
   unquoted $(find ...) in a for-loop over a path containing "Application
   Support", so the space split every path in half. That is the exact
   word-splitting failure this machine's shell rules warn about, and it made
   the check report "not installed" while the server was running.

2. It expected ~/Library/Logs/Claude/mcp-server-<name>.log. That naming is for
   Claude Desktop's own connectors; a plugin's MCP server is logged by
   LocalMcpServerManager into main.log. An absent file at a guessed path is a
   statement about the guess, not about the world.

What the corrected check now reads, and what it establishes:

  rpm/manifest.json  -> jobbsok-probe, plugin_01NxHfqM495jtJzjVjRvgWDm,
                        marketplace "My Uploads", installed 2026-09-04T16:48:20Z
  installed copy     -> 4 files, manifest version 0.0.1 (no stale cache)
  main.log 18:48:28  -> Connecting to plugin:jobbsok-probe:probe-tools
                        negotiated protocol version: 2025-11-25
                        Connected ... (1 tools)
  process            -> pid 12997, /usr/local/bin/python3, a child of Claude.app

So a plugin-declared stdio MCP server does spawn and connect on this Intel Mac,
on the host interpreter rather than in the sandbox. The negotiated version also
vindicates one design choice: probe_tools.py echoes the client's protocolVersion
instead of asserting its own default, and the client asked for 2025-11-25, which
is newer than the default the server would otherwise have claimed.

Still open, and deliberately not inferred from the above: whether a tools/call
to probe_ping actually returns. main.log records the connection lifecycle, not
individual calls, so that one is confirmed in the chat or not at all -- the
script says so rather than treating a missing line as an answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:51:45 +02:00
e1ad1b688f test(m1): add a host-side progress check for the Cowork probe
The probe's four questions are answered by the operator in Cowork, but three of
them leave traces on this Mac, and a trace is a measurement where "what did you
see in the UI" is a recollection. This reads the traces so each step can be
verified before the next one starts.

What it can actually see, discovered by measurement rather than assumed:

- Claude Desktop writes one log per MCP server, named mcp-server-<name>.log,
  and it records the full JSON-RPC handshake (verified against the existing
  mcp-server-filesystem.log: initialize, notifications/initialized, tools/list
  all appear as "Message from client: method=..."). So if Cowork spawns
  probe-tools, the entire exchange is readable from disk -- including the
  server's own stderr line, which carries the interpreter version.
- The absence of that log file is itself informative: it means Cowork never
  attempted to start the server, which is a different finding from starting
  and failing.
- pgrep on probe_tools.py confirms a live process, but its absence is not a
  nei: a server may be spawned on demand. The log is the record; the process
  check is corroboration. The script says so rather than over-claiming.

The Cowork store search is bounded to the session directory by name plus the
small manifests. The first version ran a recursive content grep over the whole
Claude support directory and did not finish inside two minutes -- it walks
caches and VM images. A check that hangs is not a check. Bounded version runs
in 0.17s.

It reports, it does not gate: exit 0 when everything has landed, 1 while
anything is pending. Baseline now: vehicle and archive done, three pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 18:43:22 +02:00
1d631b7c94 feat(m1): add pinned workspace root and ASCII slug helpers
Plan Step 3. Verify green: `.venv/bin/python -m pytest tests/test_paths.py` ->
8 passed, exit 0. Full suite: 11 passed.

Test first, and RED was observed before a line of paths.py existed
(ModuleNotFoundError on collection).

Two measured risks drive the design, not one.

Risk C6 -- the host-side server runs unsandboxed with caller-supplied paths.
safe_join resolves BOTH operands with os.path.realpath before comparing, so
lexical traversal and symlink escape fall to the same check; a lexical check on
the joined string would have called the symlink case safe. The comparison is
component-aware, so a sibling whose name merely prefixes the root is outside.
Escape raises; it never clamps to an adjacent path.

Risk H8 -- macOS hands back NFD. slug normalises to NFC first, so a decomposed
name and its composed twin produce one slug rather than two directories. The
test asserts that equality on a real NFC/NFD pair, not on a lookalike.

No implicit default workspace: --workspace, else JOBBSOK_WORKSPACE, else a
`workspace:` line in ${CLAUDE_PLUGIN_DATA}/jobbsok.conf, else WorkspaceUnresolved.
Guessing at the operator's home directory is the one behaviour this module must
not have. This tightens build-brief section 5's `~/jobbsok-workspace` default
into an explicit order; the deviation is recorded in the plan's Assumptions.

scaffold is idempotent by only creating what is absent -- an existing
beslutninger.jsonl is never truncated. It is append-only, and a scaffold that
emptied it would destroy the decision log. The test proves this by populating
the workspace and asserting a byte-identical snapshot across a second run.

Collision rule is documented and tested: -2, then -3, counting up.

sak_id carries no `taken` parameter. An earlier draft had one; no step and no
test requires it, so it was unproven code and was removed rather than kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:29:52 +02:00
24768ef66b build(m1): pin toolchain and guard v1.3.0 in pyproject
Plan Step 2. Verify green: `bash scripts/bootstrap.sh && .venv/bin/python -m
pytest tests/test_toolchain.py` -> 3 passed, exit 0.

Test first, and the RED was demonstrated rather than assumed: run against a
venv carrying pytest and the guard but not pytest-socket, the third test fails
with ModuleNotFoundError while the other two pass. The test discriminates.

pyproject.toml declares the dependencies as a PEP 735 group rather than an
installable distribution -- nothing here needs a build backend, since the tests
import from scripts/ via pytest's pythonpath. Measured before choosing it: pip
26.2.1 on this machine supports `install --group`, and bootstrap upgrades pip
first, so a pip too old fails loudly instead of silently skipping the guard.

The guard is pinned to the exact tag v1.3.0 and is not vendored. Verified the
tag resolves and the package exposes __version__ == "1.3.0"; that equality is
what the test asserts, not the pip metadata.

addopts carries --disable-socket and --strict-markers, which is why the
`network` marker is declared: with strict markers an undeclared marker would
make `-m 'not network'` an error rather than a filter, and the offline
guarantee would quietly stop being tested.

bootstrap.sh builds under ${CLAUDE_PLUGIN_DATA} when set (survives a plugin
update) and falls back to the repo-root .venv for development -- risk H2. It
refuses an interpreter below 3.10 instead of building on it, because python3
here can resolve to 3.9.6 under a GUI-spawned process.

Exercised, not just written: re-running is idempotent (exit 0); --med-xlsx
installs the extra (openpyxl 3.1.5 importable); an unknown argument exits 2
with usage; a missing JOBBSOK_PYTHON interpreter refuses loudly. bash 3.2-clean
(system /bin/bash 3.2.57 -n passes) and ASCII-only.

.gitignore needed no change: `.venv/` was already on line 12 from the initial
commit, and `git check-ignore -v .venv` confirms it matches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 17:26:20 +02:00