Commit Graph

11 Commits

Author SHA1 Message Date
xarmian deec62235e feat(decision): executable conventions, the server half (TASK-3119 U1a) (#1859)
* wip(decision): conventions set (TASK-3119 U1a)

* chore(eval): executable-conventions eval through the production question path (TASK-3119 U0)

Measures, before any chip exists, how often the decision provider says a
hand-labelled item breaks a convention at the 0.9 threshold: per
convention precision and recall (all cases and real-only), the false
'Breaks' rate of a behaviour convention, and the calls and tokens spent.
State is decision.BuildItemState, the question a decision.Noul worded as
the U1 contract says, the model decision.DefaultModel.

The hand labels were posted to TASK-3119 before any score existed; the
trail comment carrying them is dropped from every state, and the dry run
refuses a state that still carries them. Reads TYPESAFE_API_KEY only.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* chore(eval): -links and -only for the conventions eval (TASK-3119 U1a)

-links builds each state as the conventions set does, with the item's
links (from the dump: the parent link only; blocking links are not in
pad item show's JSON). -only runs one convention. Used for the lead's
U1a gate: CONVE-1286 rerun with links.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(decision): liveness before EACH split call (TASK-3119 U1a, codex r1)

A split set makes several provider calls; the liveness check ran once
before them, so an item or workspace deleted between calls was still
sent in the later ones. It now runs immediately before every call. A
test deletes the item from inside the first call and the second is not
sent; checking only once fails it.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(decision): decision_check has no default; allow-list the eval's decode (TASK-3119 U1a)

A default "on" was written into every new convention: imports gained a
decision_check field (artifact import parity) and an activated built-in
no longer matched its seed (TASK-3462 would read every one as diverged).
Absent already reads as on, so the field carries no default, in the
template and both migrations.

The conventions eval's readJSON is a raw decode the reserved-field guard
counts; allow-listed with its reason, like the attention harness.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-07 12:42:32 -04:00
xarmian 9e6791d4f1 fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403) (#1790)
* fix(scripts): install-refresh asks git in the caller's checkout, not the server's cwd (BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS

* fix(scripts): anchor the web/ pathspec at the repo root (codex r1 on BUG-3403)

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-05 05:52:30 -04:00
xarmian d15b31077a feat(chatgpt): a reset-and-verify script for the reviewer's demo workspace (TASK-3321 G8) (#1718)
scripts/chatgpt-review-seed.sh resets the Acme Launch workspace to the
state the plugin's review test cases expect (DOC-3328 section 4),
through the pad CLI as whichever account it is signed in as. It
creates no account. It archives live items outside Conventions and
Playbooks, creates the workspace and a Bugs collection if missing,
re-creates the seed items, links and comments, then checks every test
case's precondition; --verify-only runs just the check. It refuses
without --yes, and refuses any slug but acme-launch without
--allow-other-slug.

Running it against a throwaway server corrected two assumptions:
full-text search does not stem, so the login bug's body says "timing
out" (the query "login timing out" found nothing before), and
what_next recommends the in-progress onboarding task before the open
high-priority one, so P1's expected behaviour now names both, and the
Dark mode idea is seeded "new" (an exploring idea ranks as in
progress).

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 03:08:07 -04:00
xarmian 2d8aedc9a4 fix(release): the changelog starts at the previous stable release by version, and its exclude filters match GitHub's hash-prefixed entries (TASK-3268) (#1626)
* fix(release): the changelog starts at the previous stable release by version, and its exclude filters match GitHub's hash-prefixed entries (TASK-3268)

Two release-notes defects, measured with the pinned GoReleaser (v2.15.4) in a
changelog-only dry run whose output for v0.16.0 is byte-identical to the
published notes.

Base: GoReleaser took the previous tag from git describe on the new tag's
parent, which sees only tags on that ancestry. Releases are cut on
release-X.Y.x branches, so v0.16.0 was built from v0.15.0-rc.1, and a tag cut
from main now would describe back past v0.16.0: GitHub's compare reports 547
commits from there against 301 from v0.16.0, re-listing all of v0.16.0.
release.yml now pins GORELEASER_PREVIOUS_TAG through
scripts/release-previous-tag.sh (the highest stable tag below the current one
by version; an RC gets the previous stable) and fails the run if none exists.

Filters: with `use: github` every entry is "<sha>: <message>", so the
message-anchored excludes never matched and v0.16.0 shipped 30 docs, test, ci
and chore lines. The rule now allows the prefix, a scope, and a type list made
only of excluded types (`docs,test(store):`).

Dry run of v0.16.0 with both fixes: 230 entries become 200. Every one of the 30
removed is an excluded type, and nothing is added.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9

* fix(release): a breaking change stays in the notes whatever its type (TASK-3268 codex r1)

The exclude accepted `!` before the colon, so `chore!: ...` would have been
dropped. Codex round 1, P2. The v0.16.0 dry run is unchanged at 200 entries;
an RE2 matrix of 15 messages, each with and without the hash prefix, answers
as intended.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9
2026-09-27 19:10:20 -04:00
xarmian 7512f5500b docs(decision): README decision-provider section, and the day-73 eval scripts with their recorded baseline (TASK-3123) (#1593)
* docs(decision): README decision-provider section, and the day-73 eval scripts with their recorded baseline (TASK-3123)

Housekeeping for PLAN-3114 tranche 1; no behaviour change.

- README: a Decision provider (optional) section covering what uses it,
  that answers are asynchronous, configuration precedence (config file,
  then the admin setting, then the environment; PAD_DECISION_PROVIDER=none
  as the operator off-switch), and exactly what leaves the box and where.
- scripts/decision-eval/eval.py and eval2.py: the day-73 scripts, as run
  (standard library, raw systemone API, key read from TYPESAFE_API_KEY). A
  read and a pattern scan found no key or token literal.
- scripts/decision-eval/README.md: what each script measures, how to run
  it, how to regenerate the corpora, and the day-73 baseline. A, B and D
  were recomputed from the saved result files with the scripts' own
  summary logic and match PLAN-3114 (D AUC 0.939; the B >=0.8 bucket
  86.8%). C comes from PLAN-3114's trail. eval.py --dry-run over the saved
  corpora queues the same 360 items the day-73 run did.
- scripts/decision-eval/.gitignore: the corpora are private workspace
  exports and the results embed their content, so neither can be
  committed; neither was.

The CHANGELOG line is queued as DOC-3242 (draft highlights for the
release after v0.15.0, per PLAYB-1160), since releases take their
changelog from goreleaser and a highlights DOC.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9

* docs(decision): the README section states what off, the triggers, server info and disabling actually do (TASK-3123, codex round 1)

Codex fact-checked every claim against the code, and four were too broad.
Off is not "every surface unchanged": the decisions list is empty and
playbook match answers 404. Jobs are also queued by restore and by comment
edit and delete (verified at items.go:614/3207/3437/5459 and
comments.go:133/196/416), and a closed item's job is dropped at the tick.
pad server info cannot see the admin setting. Disabling stops new calls
without a restart, but an in-flight request may finish. The scripts README
now says eval2.py runs at the top level.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9
2026-09-26 22:09:21 -04:00
xarmian bf0631d22f feat(ops): health reports which web bundle is served, and install-refresh warns when it is not a clean build of the commit (TASK-3233) (#1583)
The binary's commit stamp says nothing about web/build: it is generated,
gitignored and embedded as it stands. A bundle built from uncommitted edits
(a negative-control build, BUG-3129) or left over from an older commit
therefore ships under a clean commit stamp, and nothing afterwards could say
which bundle a server served.

- The server digests the web FS it embeds (sha256 over each file's path and
  content hash) once in SetWebUI. /api/v1/health reports it as an additive
  `web: {sha256, files, source_commit?, source_dirty?}`.
- `vite build` writes build-source.json (the commit, and the count of
  `git status` entries under web/) into static/, which SvelteKit copies into
  the bundle. It never fails a build: with no git, both fields are null
  ("unknown"), never clean. The stamp is gitignored.
- install-refresh.sh step 7 logs the served bundle and WARNS when the stamp
  is missing, when it was built with web/ dirty, or when web/ changed
  between the stamp's commit and the expected one. A backend-only commit on
  top of a bundle is correct and is not flagged. It warns after the
  restart; it does not prevent it.

Claude-Session: https://claude.ai/code/session_016PXRzXhokr61CLbbu3Txj9
2026-09-26 16:54:20 -04:00
xarmian d4ec587de9 fix(cli): a CLI call during an install-refresh bounce waits for the refresh instead of auto-starting a server over it (BUG-3215) (#1544)
install-refresh.sh stops the server, installs, and restarts it with the
captured argv. Any pad CLI call in between found nothing answering and
auto-started a server of its own: the caller's binary, cwd and default
flags. That server held the port against the refresh's restart (day 79:
127.0.0.1:7777 went dark and the script exited 1).

- The script writes <DataDir>/refresh.lock {pid, port, deadline} by rename
  before the stop and removes it in its EXIT trap, only while it is still
  its own. The deadline is 10s + PAD_PROBE_TIMEOUT (30s by default).
- EnsureServer defers to a live marker for its own port (writer pid alive
  AND deadline not passed): it polls health and never spawns. If the
  writer is still alive past the deadline it errors naming the pid. A dead
  writer, a passed deadline, another port or an unparseable file means
  the marker is ignored.
- The script names a holder that took the port before the restart spawns
  (and does not spawn it), and refuses success when the listener after
  the probe is not the restart it spawned. A health probe answers for
  whoever holds the address.
- The script-test dependency guard now reads every install_refresh*_test.go.

Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m
2026-09-25 09:55:48 -04:00
xarmian b2bee3242f fix(scripts): install-refresh restores the server's own cwd and matches the running server by identity (BUG-3196) (#1512)
* fix(scripts): install-refresh restores the server's own cwd with its argv, refuses a deleted one, and matches the running server by identity rather than the built binary's name (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): one existence check refuses a deleted server cwd, naming the path without the kernel's suffix (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* test(scripts): the no-/proc refresh test declares lsof, which now reads the server's cwd there (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* test(scripts): the lsof dependency sits on the requireScriptDeps line the guard reads (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(make): the install target's guarantees include the restored cwd (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): the server's cwd is re-checked immediately before the stop, and the capture's limits are stated (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): a failed restart cd says what state it leaves, not that no server runs (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): an unreadable server cwd is refused naming what was read and its error, and the case is tested (BUG-3196)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
2026-09-24 10:38:16 -04:00
xarmian 45dddb8f8f fix(scripts): install-refresh replaces the server listening on its port, by pid, and leaves other pad servers running (BUG-3194) (#1504)
* fix(scripts): install-refresh replaces the server listening on its port, by pid, and leaves other pad servers running (BUG-3194)

The script counted every `pad server start` on the box and refused on
more than one, so a sibling seat's e2e server (a worktree binary on its
own port) made the scripted refresh unusable whenever anyone ran e2e.
Its stop was a system-wide `pkill -x`, which would also have killed
that server and any `pad` CLI call in flight.

The target is now the `server start` process listening on the port the
server itself would resolve without a flag (PAD_PORT, config, 7777),
read with ss or lsof. It is stopped by pid, after its argv is re-read
and matched. Still refused: the port held by something that is not a
captured server, several servers on it, or several servers and none on
it. One server with nothing on the port is still that server.

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(make): the install caution names npm ci, the hazard that remains; the stop is no longer system-wide (BUG-3194)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): refuse a foreign or unreadable listener on the port, re-check before each signal, keep empty trailing arguments (BUG-3194, codex round 1)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* fix(scripts): the one-server fallback needs an explicit --port, so a lone sibling server is never replaced (BUG-3194, codex round 2)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N

* docs(make): reflow the install comment (BUG-3194)

Claude-Session: https://claude.ai/code/session_01RYiqbqXmxQMoKHsGm8B77N
2026-09-24 06:56:26 -04:00
xarmian 3b7665a469 chore(decision-eval): attention threshold eval — no threshold passes the pre-registered rule (TASK-3137) (#1424)
* chore(decision-eval): attention threshold eval through the production state builder (TASK-3137)

The instrument and its recorded results. The pre-registered rule is met by
no threshold on the production state, so AttentionThreshold is unchanged;
the ablation shows the 70/70 labels are historical while production asks
about open items in the present. See RESULTS.md.

Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd

* chore(decision-eval): replay each item to its last open moment (TASK-3137)

The lead-ruled instrument: BuildItemState over the item as of just before
its first logged terminal transition (r1), and at filing (r0), with the same
140 labels. The pre-registered rule (P >= 90%, R >= 60% on r1) is met by no
threshold, so AttentionThreshold is unchanged. RESULTS.md records the
finding: collection-membership labels cannot see the transient human waits
that a task at its last open moment really has.

Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd

* test(models): classify the eval tool's generic decode site (TASK-3137)

The BUG-2685 guard found the new scripts/decision-eval readJSON helper as an
unclassified interface-destination decode. It decodes item, comment, history
and activity dumps, never a collection definition.

Claude-Session: https://claude.ai/code/session_01ByMjAhvdWgbAWWeGE9jwWd
2026-09-22 01:04:24 -04:00
xarmian 110045578c fix(build): make install proves what it installed and what it restarted (BUG-2897, TASK-2787) (#1272)
`make install` made three claims it did not check.

1. It brought the server back BY SIDE EFFECT -- `pad auth whoami` triggers
   an auto-start, which does not know the killed process's argv. A server
   running `--host 0.0.0.0` came back bound to the default host alone:
   curl 127.0.0.1:7777 -> 000 while the LAN address -> 200, with the
   process count and the version both reading correct (BUG-2897).

2. `cp` copied whatever was at the repo path, not what the invocation
   built. Two sessions sharing the checkout interleave and the loser's
   build is installed by the winner, every exit code green (TASK-2787).

3. "Server restarted." was printed after a command ending in `|| true`,
   with no probe of any kind. Not "the wrong address went unverified" --
   nothing was verified. Found reading the recipe; neither filing names it,
   and it is what made the other two invisible.

The logic moves to scripts/install-refresh.sh for one reason above
readability: a script can be TESTED. internal/buildtools drives it against
a compiled stub `pad`, including the branches where a check must FAIL.
Recipe-inline logic is only exercisable by running `make install`, which
stops the developer's server -- a test nobody runs twice, which is how this
target accumulated three unverified claims.

The script checks OUTCOMES rather than steps: what got installed, and what
is answering afterwards. Two commit checks, deliberately, answering
different questions -- the ARTIFACT before the kill, so a wrong build costs
an error instead of an outage, and the DESTINATION after the copy, which is
the shared-path race TASK-2787 names. The restart uses the argv read from
/proc before the kill, and nothing is printed about a restart until the
server answers on BOTH 127.0.0.1 and the configured host (`--host 0.0.0.0`
resolving to loopback plus the primary LAN address, since 0.0.0.0 is a bind
spec, not something to curl).

Three defects were found in the fix itself, by its own tests and by the
first real run:

- The restart redirected to $HOME/.pad/server.log with nothing creating
  that directory. On a fresh HOME the redirect fails, the server never
  starts, and the probe reports "did not answer" -- true, and three steps
  downstream. Invisible to every hand-run this script could have had,
  because a developer's box has ~/.pad by luck of history.
- The post-copy check SURVIVED its mutant: both fixtures reported the same
  version from source and destination, so the guard and its absence were
  indistinguishable. Fixed by making the stub's version depend on its path.
- Comparing commits by equality rejected a healthy build. `git rev-parse
  --short` returns the shortest UNAMBIGUOUS prefix, so its width grows with
  the object database: the binary embedded `a3a1d58` and the Makefile
  produced `a3a1d586` minutes later. Now a prefix comparison in either
  direction, with a negative leg pinning that a genuinely different commit
  of the same width is still refused.

Five mutants, each verified to compile, each detected by its own leg.
Verified end to end on the real box: captured `--host 0.0.0.0`, installed
a3a1d586, restarted with that argv, both addresses answering, /proc/exe
matching the installed binary.

CONVE-2687's manual sibling-safe recipe is unchanged and remains the path
to use when another session's worktree is live; this makes the plain target
honest, it does not replace the convention.

NINE adversarial rounds ran before one returned nothing new. They found
twelve more defects, each verified in the code before being accepted:

- The probe used a fixed port while the restart preserved `--port N`.
- Only `--flag value` was parsed; Cobra accepts `--flag=value` too.
- The artifact check and the copy were separated by a window another
  session could write into, so a swap was caught only AFTER the kill. The
  copy is now STAGED beside the destination, verified there, and moved into
  place with a rename.
- `setsid` is Linux-only and would have failed on macOS after the stop.
- A host or port configured only in ~/.pad/config.toml was ignored, so such
  a user would have seen every install fail.
- A TOML inline comment (`port = 8080 # dev`) was swallowed into the value.
- $INSTALLED could be replaced between the outcome check and the exec.
- Comparing abbreviations by PREFIX accepts two different commits sharing
  seven characters -- the same mechanism that makes abbreviations grow. Ids
  are now resolved with `git rev-parse` and compared in full; an id that
  cannot be resolved is REFUSED rather than prefix-matched.
- The argv capture read /proc only, so on macOS it captured nothing and the
  restart fell back to defaults -- the fix silently absent on the platform
  the setsid fallback had just been added for.
- A wildcard bind whose LAN address could not be determined was reported as
  a success having verified only loopback. It now FAILS CLOSED.
- `localhost` was mapped to 127.0.0.1 unconditionally, failing a healthy
  IPv6 bind.
- `local` was used at top level, so bash printed an error to stderr on every
  normal refresh while the script kept working.
- More than one running server is now REFUSED rather than guessed: the stop
  is a system-wide pkill and the restart can only restore one argv.

Twenty mutants, each verified to compile, each detected by its own leg.
SEVEN survived first -- the destination check, the `--flag=value` parse, the
setsid guard, the /proc fallback, the wildcard loopback probe, the
prefix-collision refusal, and the multi-server refusal. Every one survived
for the same reason: the fixtures were drawn from my own model of the input
(equal-width commits, one flag spelling, one bind shape, a machine with
/proc), so they tested the model rather than the input. Two of them are
unreachable on this platform by construction and are reached through
documented PAD_NO_SETSID / PAD_NO_PROC knobs, because an untestable branch
is how the macOS half would have shipped broken twice.

One residual is named rather than closed: a concurrent `make install` can
still swap the binary between the final verification and the exec. The
complete answer is an exclusive lock across the whole sequence; flock is
Linux-only and a lock that silently does nothing on macOS is worse than a
named gap. Filed as IDEA-2925.

Claude-Session: https://claude.ai/code/session_01HeChkgZVYb3NTgTcckF5KR
2026-09-07 11:48:58 -04:00