Skip to content

Fresh install to the first Planner session - #2

Merged
vsov merged 4 commits into
vsov:mainfrom
vbanas:fix/fresh-install-onboarding
Oct 16, 2026
Merged

vsov merged 4 commits into
vsov:mainfrom
vbanas:fix/fresh-install-onboarding

Conversation

@vbanas

@vbanas vbanas commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Two fixes found by running /xcheck-plan on a new install. Together they're the whole route from install.sh to the first Planner session.

  • fix(decision): a fresh install with no state.json now routes to the Planner, instead of status, next
    and migrate refusing in a loop.
  • fix(onboarding): install.sh --type code|text picks the AUDIT.md template, and a new
    operator-profile.sh writes the operator profile. status shows a policy: line, and the refusal's hint is
    now a command that actually runs.

New tests: tests/test_fresh_install.py (17) and tests/test_operator_onboarding.py (26). Each commit message has the full detail.

vbanas and others added 4 commits October 16, 2026 12:41
On a project fresh from `install.sh`, `/xcheck-plan` could never start
the Planner. Three refusals met in a loop:

- `install.sh` writes no `audit/state.json`, by design (README §5.2).
- `xcheck status` and `xcheck next` refuse without one, and the
  refusal advised running `install.sh` again, which writes none.
- `xcheck migrate` refuses an AUDIT.md the Planner has not written.

So the first role of every audit had no orchestrated route at all, and
xcheck no longer ships a launcher for the uncontained one. Found by
running `/xcheck-plan` on a new install.

`decision.pre_state_decision` runs only when there is no state
document, and answers in one of three ways:

- `run-planner` for a proven fresh install whose AUDIT.md is not yet a
  complete plan. The session runs under the same eight controls and the
  same budget gates as any other, priced against an empty state.
- `stop-migrate` once AUDIT.md converts into a complete plan. This is a
  human gate: loading the plan is the operator's act, so the
  orchestrator lists the queued passes and names
  `xcheck --dry-run migrate` and `xcheck migrate` instead.
- Refusal, as before, for every other stateless tree, now with the
  reason.

"Fresh" is three facts, and each fails closed. `audit/.provenance.json`
must say `install.sh` made the tree. No findings, pass reports, plans,
construals or LEDGER.md rows may exist, because those make it an old
audit to convert. And the event journal must record no state
transition: a tree whose state document existed and is gone is told to
restore it, since planning again would start the audit over and orphan
its history.

`construal_gate=on` refuses the pre-state Planner rather than skipping
the gate. A construal is admitted into the state document, which a new
audit does not have, so the gate could only fail. The refusal says how
to plan with it off and turn it back on.

The same path had two smaller faults:

- A failed Planner session raised a traceback in the retry check, which
  read the last session from a state file that does not exist yet. It
  now stops on the session's own outcome, with exit 1.
- The no-progress message told the operator the charter was exactly
  where it was. Before migration the session's work is AUDIT.md, so it
  says that and points at `xcheck status`.

The state-file error now names `xcheck migrate` only. README §5.2, §6
Step 1 and §8 describe the route. XCHECK.md and the launcher skills are
unchanged, so installed trees need no upgrade.

`tests/test_fresh_install.py` runs the real `install.sh`, and a fake
agent for the dispatch cases: 17 tests, 15 of which fail on the parent.
The other two are controls that a too-wide route would break. selftest
169 -> 170 for the `stop-migrate` row in the gate-message table.

Full suite on this machine: 1,737 tests, 5 failures and 1 error, the
same set as on the parent. Each one is a machine dependency: no `build`
module, no `ruff`, and rows A2 and A3 of the SECURITY.md adversarial
matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new operator went from `install.sh` to `/xcheck-plan` on a real
codebase and met four problems in a row:

- `install.sh` always installed the text-corpus AUDIT.md. A codebase
  needed a hand `cp` that only a note under README §5.2 mentioned, so
  the plan started from `facts`, `style` and `currency`.
- Nothing on the install path said an operator profile is required.
  README §5 and §6 Step 1, bootstrap.md and the installer's output
  were silent; only the §8 reference explained it.
- `xcheck status` printed `decision: run-planner` and nothing about the
  profile, so `next` was the first command to say it would not run.
  The launcher skills already tell operators to read the profile there.
- The refusal's hint, "`xcheck --policy PATH` with a path that does not
  exist prints the template", names a command the grammar rejects:
  `--policy is not valid for no command`. The template is printed only
  after a command is parsed. The same message spelled the XDG location
  `$(XDG_CONFIG_HOME)`, which a shell reads as command substitution.

What changes:

- `install.sh --type code|text` picks the template, before or after the
  target. No flag still means `text`, so existing callers get what they
  got, and the output now says which template that was. An unknown type
  is refused before anything is written. An existing AUDIT.md is never
  replaced; if it is still an unfilled template, the installer prints
  the `cp` that switches it. The last lines are the next steps, the
  profile first.
- `operator-profile.sh` writes a profile from
  docs/operator.conf.example, which is already held byte-identical to
  the template. `--trust` is required and has no default. `untrusted`
  also sets `sandbox_profile=container`, without which every writing
  dispatch refuses. The path is named by the operator or by
  $XDG_CONFIG_HOME, never derived from the home directory. The file is
  validated by `policy.load` (with `--project`, the location gate too)
  and then hard-linked into place, so it never overwrites. It prints
  the line that points xcheck at the file, in fish syntax under fish.
- `status` prints a `policy:` line: the resolved path and digest in the
  words `require_policy` uses at dispatch, or "none found" with the
  helper named.
- The refusal names the helper and `xcheck --policy PATH status`, and
  spells `$XDG_CONFIG_HOME`.
- README: a new §5.3 for the profile (5.3-5.6 become 5.4-5.7, and their
  three references move with them). §5.2 documents `--type`, §6 Step 1
  points at §5.3, §8 names the helper, and §15 has a row for the
  refusal. bootstrap.md: the installer command, a new §1a for the
  profile; the old §1a is now §1b.

Three existing checks move with this:

- selftest F-0127 inverted the AUDIT.md guard by matching the old
  one-line form, so the mutation stopped applying and the check went
  red. It now inverts the new `if [ ! -f ... ]` guard; 169/169.
- `test_parallel_retry` compares two `status` outputs whose profiles
  differ by design (the flag absent, then written as `off`). Only the
  16-digit digest on the `policy:` line is masked.
- The containment-claim pin goes 209 -> 211 with its ledger entry: both
  new claims are in README §5.3, and unscoped stays at 0 because the
  section names `orchestrated` before the first of them.

XCHECK.md and the launcher skills are unchanged, so installed trees
need no upgrade. This builds on the previous commit, which lets
`status` and `next` run on a fresh install that has no state.json yet;
the two together are the whole route from install to the first Planner
session.

`tests/test_operator_onboarding.py` (PR tier) runs the real scripts and
CLI: 26 tests, all of which fail on the parent. The refusal test parses
the hint and runs it, so a reworded hint is held to the same standard.

Full suite on this machine: 1,746 tests, 5 failures and 1 error, the
same set as on the parent. Each one is a machine dependency: no `build`
module, no `ruff`, and rows A2 and A3 of the SECURITY.md adversarial
matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Planner given templates/AUDIT-code.md wrote a full plan for a real
codebase (19 norms, 10 dimensions, 74 units, 90 passes) that `xcheck
migrate` refused:

    the Unit map table has no 'unit' column (header: id | material |
    size | responsibility)

The converter finds each catalog column by its exact header name, but
the templates showed the header of only one of the three tables it
reads. §1 Norms catalog and §3 Unit map held a comment and nothing
else, so a Planner had to guess `id | source | scope` and `unit |
material | responsibility`. This one got the first right and named the
unit column `id`. It could not find out: `xcheck` is not on PATH in the
Planner's sandbox. The comment above _UNITMAP_COLS in
migrate_readers.py already described the templates as carrying `unit |
material | approx. size | responsibility`.

- Both templates show `| id | source | scope |` and `| unit | material
  | approx. size | responsibility |`, with a note that the converter
  reads these names and that norm ids are N1, N2, ... A fresh
  install's `--dry-run migrate` goes from 9 defects to the 7 dimension
  rows a Planner fills in; `status` still routes it to the Planner.
- The selftest's template check (F-0130) holds all three catalog
  headers, not only Dimensions. A live mutation names the unit column
  `id`, as the real plan did, and expects it caught. 170/170.
- _NORMS_RE and _UNITMAP_RE move from migrate_readers to md_prose,
  beside _DIMENSIONS_RE, because nothing outside the converter may
  import migrate_readers (test_no_bypass).
- test_operator_onboarding fills each table under the header the
  template shows and expects the plan to convert, with one norm,
  dimension, unit and pass. On the parent it fails with "no Norms
  catalog table" and "no Unit map table".

A plan already written keeps its header. One with `| id |` in its Unit
map loads once that cell reads `unit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ca6897d's comment beside _NORMS_RE and _UNITMAP_RE put the module name
migrate_readers and the word "import" on one line. test_no_bypass greps
xcheck/ for that pair outside the converter, so it failed on a comment.
The comment says the same thing without naming the module there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vsov pushed a commit that referenced this pull request Oct 16, 2026
PR #2 failed CI at its first real step, ruff F841 at cli.py:516. The
contributor's machine had no ruff, which the PR says, so nothing local
could show it.

`checked` was never read in cmd_next before the PR either; the old
single unpack of a call result is exempt from F841. The PR's new branch
assigns a literal tuple, which ruff reads as plain assignments, and that
made the dead name visible. Both branches now use `_checked`, like
`_state` and `_report` on the same lines. No behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vsov pushed a commit that referenced this pull request Oct 16, 2026
The development gate failed on PR #2 with
`test_every_place_that_names_the_stream_is_declared`: the new
`migrate.not_fresh_install` names `events.jsonl`, and the 0.9.5 boundary
gate requires every such site to say what it does there. Its use is
benign: it counts `state_transition` events through `ledger.read_stream`,
the validated reader, and the name appears only in message text. That is
the shape of the existing `bundle.py` row, and it is declared the same
way.

Neither the contributor nor this repository's public CI could see it.
`OneValidatedReadBoundary` carried a CLASS-level `@needs_live_corpus`
from the 0.9.5 export work, and only one of its five tests reads the
corpus. The other four run on temp fixtures or scan the package's own
source. In the exported tree all five skipped, so the boundary check was
disarmed exactly where an outside contribution is checked.

The gate moves to the one method that reads the real stream, and
`setUpClass` reads its digest only when the corpus is there. Exported
tree: 5 skipped -> 1 skipped, 4 running. Counterfactual in the exported
tree, with the new row removed: the check fails naming
`migrate.py not_fresh_install` (failures=1, skipped=0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vsov
vsov merged commit 55bb6f3 into vsov:main Oct 16, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants