Commit Graph

8 Commits

Author SHA1 Message Date
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