✨ forks: helper skeleton, state, where and init (block 1.1)
This commit is contained in:
@@ -63,3 +63,33 @@ Intake 2026-10-16 12:18 +0200, Athanor `claude-opus-5-5[1m]`. Start commit `83f6
|
||||
|
||||
| Block | Owner (model id) | Commit | Tokens (if known) |
|
||||
|---|---|---|---|
|
||||
|
||||
### Block 1.1 — Skeleton, state, where, init
|
||||
|
||||
- Model: `claude-opus-5-5[1m]`
|
||||
- Start / end: Fri Oct 16 12:20:48 +0200 2026 / Fri Oct 16 12:28:49 +0200 2026 (`date`; tests and gates finished by the end time, the note and commit followed)
|
||||
- Done: New `plugins/spire/skills/forks/forks.py` (plan §2 PEP 723 header, stdlib only, LF): argparse dispatch for all 13 commands; exit codes 0/2/3/4 as constants; stdout = exactly one JSON object per call (argparse errors, `--help` and uncaught exceptions included); path normalisation (relative, `G:\`, `G:/`, `/g/…`, `/c/…`; POSIX `abspath`); state dir `<run without .md>-forks` with `.gitignore` (`raw/`, `*.lock`), `mcp-empty.json`, `jobs/ briefs/ notes/ results/ logs/ raw/`; `registry.json` `"schema": 1`; `RegistryLock` (create-exclusive, stale after 30 s, token-checked release); atomic temp-then-`os.replace` with `PermissionError` retries (40 × 50 ms) for writers and readers; `ledger_append` (caller holds the lock); `_now()` / `_iso()`; `pid_record` / `pid_alive` / `proc_ctime` (Windows `OpenProcess` + `GetProcessTimes`, POSIX `/proc/<pid>/stat`); `git()` always `git --no-optional-locks`; scrub constants (`SCRUB_DROP`, `SCRUB_DROP_PATTERNS`, `SCRUB_KEEP`, `SCRUB_PINNED` + `CLAUDE_CODE_EXECPATH`) and `scrubbed_env(binary)`; `supervisor_interpreter()` (C6); `SPAWN_METHOD` (`cim-hidden` / `setsid`); `where`; `init` (all launch fields and defaults, fingerprint sha256 of canonical JSON, config extras incl. `orphan_after` 1200 and `pings` on, idempotency, C8 admission, all validation before any write). Launching and Phase 1 commands return `{"error":"not implemented","block":"1.2"}`, Phase 2 ones `{"phase":2}`, exit 2. New `preamble.md` (digest content, short). New `tests/test_forks.py` (PEP 723 + `unittest`, imports `forks.py` by path, 52 tests: path forms, state-dir derivation, scrub, lock contention in-process and across processes, stale-lock recovery by `at`, by mtime and under an injected `_now`, safe release after takeover, replace while a reader holds the target (retried, `attempts > 1` asserted on Windows), registry schema, ledger, pid + ctime, `init` defaults / idempotency / change before and after a fake base / bad model ids / `plan` / unknown mode and effort / missing binary / agent, C8 exact / `[1m]` strip / family alias / no list / unlisted → exit 2 naming the files with settings untouched, `where` via `uv run` with a stub and with the host binary, error-case stdout invariant via `uv run`, unknown command, not implemented, `--help`, internal error, N3 guard). Stub binary generated at test runtime in `tempfile` (`.cmd` on Windows); `USER_SETTINGS_PATH` monkeypatched to a temp file, so no test reads `~/.claude`.
|
||||
- Gates:
|
||||
- G-unit `uv run plugins/spire/skills/forks/tests/test_forks.py` → exit 0, `Ran 52 tests in 3.173s` / `OK`.
|
||||
- G-where `uv run plugins/spire/skills/forks/forks.py where` → exit 0; stdout 1 line, a JSON object, `ok: true` (binary `CLAUDE_CODE_EXECPATH` 2.1.299, Python 3.13.1, uv 0.5.13, git 2.34.1.windows.1, `spawn: cim-hidden`). uv's `Reading inline script metadata…` line goes to stderr.
|
||||
- Deviations from plan (digest):
|
||||
- Argument sets for the 1.2 commands are already declared in argparse from the digest (`train --base --brief [--block --phase]`, `fork --base --block --brief [--phase --mode --paths --max-minutes --max-budget-usd --force]`, `continue --job --brief [--max-minutes]`, `wait [jobs] --all --any --timeout`, `expire --base|--all`, `kill --job`, `_supervise --job`). 1.2 may change them.
|
||||
- The N3 guard is already in the dispatch for `train`, `fork`, `continue`, `rebase` (exit 2 before "not implemented" when `SPIRE_FORKS_CHILD` is set); `tick` left for Phase 2.
|
||||
- `--help` returns `{"help": …, "commands": […]}` on stdout with exit 0 (keeps the invariant). Unknown extra arguments are a usage error (exit 2), except on the Phase 2 stubs, which tolerate them until Phase 2.
|
||||
- Uncaught exceptions: exit 2, `{"error":"internal error","type","detail"}`, traceback on stderr (the digest defines no code for it).
|
||||
- `init` takes more flags than the digest lists: `--ttl-s`, `--stall-after-s`, and one per threshold (`--rebase-commits`, `--rebase-lines`, `--rebase-delta-tokens`, `--max-base-tokens`, `--max-rebases`). Thresholds sit nested under `config.thresholds`. Config also carries `binary_resolved_by` and `model_admission` (how, not what; ignored by the change comparison).
|
||||
- `where` takes an optional `--binary` (tests use a stub) and also reports `python.supervisor_interpreter`, `platform`, `scrub.drop_patterns`; on failure it carries both `reason` and `error`.
|
||||
- `preamble.md` adds two lines beyond the digest: "nobody can answer a question mid-run" and "never push, rebase or reset" (mirrors the deny list).
|
||||
- Flagged for review:
|
||||
- `--effort` is restricted to `low|medium|high|xhigh|max` and `--permission-mode` to `default|acceptEdits|dontAsk|bypassPermissions|auto` (`plan` refused). Both sets are assumed, not checked against `claude --help`.
|
||||
- Fingerprint launch dict: `model, effort, permission_mode, tools, allowed_tools, disallowed_tools, mcp {strict, config path}, append_system_prompt {path, sha256}, agent {name, path, sha256} | null, cwd, binary {path, version, size, mtime}, env.pinned` (only the two TTL/autoupdater vars; `SPIRE_FORKS_CHILD` and `CLAUDE_CODE_EXECPATH` are added at launch by `scrubbed_env`, outside the hash). The mcp config path is per run, so the hash differs between runs by construction.
|
||||
- `--allowed-tools` / `--disallowed-tools` are repeatable lists and each value is also split on commas outside parentheses (`Bash(a,b)` stays one rule).
|
||||
- Stale-lock breaking re-reads the lock and removes it only if unchanged; two breakers racing still have a narrow window. Release removes the lock only if its token is still ours. Lock wait is 45 s (longer than the 30 s stale age).
|
||||
- C8: an unreadable or invalid settings file is skipped with a stderr note; `availableModels: []` counts as a list that admits nothing.
|
||||
- Host env has `ANTHROPIC_BASE_URL`; it is on neither list, so it is kept for the child and listed under `scrub.unknown` by `where` (the P0 probes kept it too and auth worked). The coordinator may want it on a list.
|
||||
- `pid_alive` treats `ERROR_ACCESS_DENIED` from `OpenProcess` as alive with an unverifiable creation time.
|
||||
- Git Bash paths whose first segment is not a single letter (`/tmp/x`) are treated as rooted on the cwd's drive, not mapped to Git's install dir.
|
||||
- The model regex is the digest's; it admits ids like `claude-sonnet` with no version.
|
||||
- Left for later: everything the digest assigns to 1.2 (templates, launch, supervise, results, ledger lines, `wait`/`list`/`expire`/`kill`, no-visible-window test, `run.last_orchestrator_call_at`); Phase 2 commands.
|
||||
- Expected broken: `train`, `fork`, `continue`, `wait`, `list`, `expire`, `kill`, `_supervise` exit 2 "not implemented" (block 1.2); `rebase`, `usage`, `tick` (Phase 2).
|
||||
- Commits: see the follow-up hash commit below.
|
||||
|
||||
Reference in New Issue
Block a user