mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
51b0bb4de930b0bb24e0929cf8fcd60b87f961d0
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e598bc4f64 |
feat(decision): effort, effort_medium and high_stakes on the attention set, staged; drain-paced backfill (TASK-3921) (#2236)
* feat(decision): effort, effort_medium and high_stakes on the attention set, staged; one-shot backfill (TASK-3921)
- attention set gains effort (Score), effort_medium (Choice) and high_stakes
(Noul) in the same call; needs_human_decision's text is unchanged.
- QuestionSet.Unsurfaced names the staged keys: WorkspaceFlags and
SurfacedDecisions skip them (no flag, no state read, no counter); the
decisions list and endpoint still return every row.
- attention chips only render keys that have a label.
- store: per-set enqueue body extracted (owedDecisionJobTx), OweDecisionJobs
(chunked, under the installed debounce) and ItemsMissingDecisionKey.
- Runner.BackfillEffort owes the attention set once for eligible items with a
needs_human_decision answer and no effort answer, marker
effort_decisions_backfilled written after every chunk; hooked at the end of
reconfigureDecisionsLocked (boot and later admin enable); makes no provider call.
- eval harness: titlebody now sends the live set; RESULTS.md rows pending.
- docs/api.md and tests updated.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* test(decision-eval): keep titlebody as the day-73 control, add titlebody-live; record the TASK-3957 and TASK-3921 runs
- titlebody again sends the day-73 pair (needs_human_decision + blocked), so it
stays comparable with its 09-22 row as the model-drift control.
- titlebody-live sends the live attention set on the same {title, body} state:
the TASK-3921 gate (needs_human_decision AUC 0.936, within .02 of 0.939).
- Every run prints the question keys it sends, before -dry too, so a row's
call shape is in its own log.
- RESULTS.md: rows for the one-question call (TASK-3957) and the
four-question call (TASK-3921).
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* fix(decision): effort backfill lifecycle, staged keys off item reads, dry-run guard (TASK-3921; #r1 F1-F4)
- F1: startEffortBackfill checks stopped and does bg.Add under one
decisionTick.mu hold; BackfillEffort/EffortBackfillCandidates take a ctx
checked before each candidate read and each chunk; cancel returns ctx.Err()
and never writes the marker. stopDecisionTick cancels the run.
- F2: reconfigure cancels a running backfill; only a non-nil runner starts a
new one. A per-run generation replaces the atomic latch so a cancelled run
cannot clobber its successor.
- F3: item show / --agent / MCP item get use Runner.ItemReadDecisions, which
drops the set's Unsurfaced keys; the decisions endpoint stays raw. docs/api.md
updated.
- F4: the file dry run refuses a path with a -wal/-shm sibling or under
$HOME/.pad (refuseLiveDatabase, tested without the env var).
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* test(decision): a chunk write that fails in the store writes no marker (TASK-3921; mutant M6)
The two failure tests fail the per-chunk test hook, so the path where
OweDecisionJobs itself returns an error was never exercised: a mutant that
ignores that error and marks anyway survived. The new test breaks the
decision_jobs table from the hook so the error comes back through the store,
asserts no marker, then restores the table and re-runs to completion.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* refactor(decision): drop the pre-marker cancel check; a cancel after the last chunk may mark (TASK-3921; M5, lead ruling)
Mutant M5 (that check removed) survived every test, and could not do
otherwise: a cancel can land there only after every chunk was owed, and the
marker means every candidate was ENQUEUED, not answered, so marking then is
true. The check is removed rather than kept as a guard no instrument can
turn red. The doc comments on BackfillEffort, cancelEffortBackfill and
startEffortBackfill now say a cancel before the last chunk writes no
marker and one after it may.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* docs(api): a backfill chunk in flight when decisions are disabled may still commit (TASK-3921; r2 L1, lead ruling bdce2116)
docs/api.md said that with no provider "no job row is ever written". That is
false for one window: a disable that lands while the one-shot effort
backfill is inside a chunk's write lets that chunk commit, and its jobs wait
until a provider returns. Ruled docs-only. The disable test's comment now
says it covers a backfill parked before its first chunk, not that window.
Sweep for the claim (CONVE-23): grep of docs, internal, cmd, scripts and
skills (.go, .md) for "no job row", "job row is", "no job is", "no jobs
are written/owed/enqueued" and "never written/owed/enqueued". Two hits are
about this mechanism: api.md:72 (fixed) and the test comment (narrowed).
Unchanged, still true:
- store/decisions.go:27 is about the WRITE DOORS, which still enqueue
nothing without a resolver.
- runner_test.go:238 is about the nil-provider runner, which never starts a
backfill.
- effort_backfill_lifecycle_test.go:128 covers a run parked before its
first chunk.
The other hits are unrelated mechanisms.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* fix(decision): pace the effort backfill on drain; dry run opens a copy; docs eligibility (TASK-3921; r3 M1, L2, L4)
- M1: the backfill owes one tick pass of items per chunk (20 default, wired
from the tick limit) and owes chunk k+1 only once chunk k's own jobs have
drained: none claimable now, by ClaimDecisionJobs' own predicate
(new Store.CountClaimableDecisionJobs). Debounced jobs do not hold it.
The wait polls at the tick interval, honours ctx, and a cancel writes
no marker; the marker still follows the last chunk being owed.
- L2: TestEffortBackfillDryRunOnFile copies the named file under GOTMPDIR
and opens only the copy.
- L4: docs/api.md states the real eligibility and the drain pacing.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* docs(decision): the Unsurfaced comment names ItemReadDecisions (TASK-3921; r4 L2)
The field's comment listed WorkspaceFlags and SurfacedDecisions as the
readers that skip staged keys, but not ItemReadDecisions, which item show
(and so --agent and MCP item get) has used since the r1 F3 fix. Comment only.
Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF
* test(decision): break the table before the LAST chunk so the store-failure test pins M6 (TASK-3921; lead ruling)
Since the drain pacing (
|
||
|
|
b1785f8952 |
fix(decision): retire text-derived blocked, waiting_on_external and conventions_comments; keep a pending answer on the dashboard (TASK-3915, TASK-3924) (#2229)
* fix(decision): retire text-derived blocked, waiting_on_external and conventions_comments; keep a pending answer on the dashboard (TASK-3915, TASK-3924) IDEA-3906 half B, units 1 and 3. - The attention set asks one question, needs_human_decision. The text-derived blocked Noul is gone: blocked is the dependency graph's alone, and the dashboard no longer appends a "its text says it is blocked" entry. waiting_on_external flagged nothing on 10-17 and goes too. - conventions_comments is no longer registered in production. It took 42% of provider calls on 10-17 for 7 chip-level answers. The set definition and the PerComment machinery stay, since tests and the conventions eval gates use them. The web drops the comment-card chips and their store. - Stored answers to the retired keys and set need no migration: no registered question carries their keys, so they read current=false and the brief counts them as stale questions. - WorkspaceFlags pending tolerance: an above-threshold answer whose item has moved on is kept while a job is owed for it with no recorded failure and the set still applies. Without that, the dashboard entry vanishes for the whole wait for the re-ask, which TASK-3916's debounce will stretch to an hour. - The day-73 titlebody eval control keeps the retired blocked question text locally, so it still re-runs that shape. - docs/api.md and README updated. Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF * test(decision): a claimed re-ask still counts as pending; rewording one question keeps the others (TASK-3915 r1) r1 hunter Lows, all FIXED: the per-key rewording rule had lost its coverage with the one-question set, so a two-question test set keeps it; a claimed-but-in-flight job leg is added to the pending test; the ProductionRegistry comment's run-on join is split. Claude-Session: https://claude.ai/code/session_018oQ5EErQKHtBJK8yLpcDNF |
||
|
|
dab3acd1f1 |
feat(store): a read-only diagnostic counts version tie groups migration 054 may have misordered (BUG-3751) (#2136)
* feat(store): a read-only diagnostic counts version tie groups whose order breaks history (BUG-3751) Postgres migration 054 backfilled item_versions.version_seq ordering same-second ties by ctid, a physical location rather than chronology. No column records finer chronology (created_at is second-precision, id a random UUIDv4), so whether a tie group was numbered out of order shows only in whether its reverse patches apply in the order history reads them. DiagnoseVersionTies walks each item with a tie group from its current content and classifies every group: current order only, ambiguous, misordered (another order applies, the current one does not), unresolvable, skipped (over 5 rows), plus items whose chain broke outside any tie. Counts only, no ids or content, inside a read-only transaction. `pad db diagnose-version-ties --pg URL | --sqlite PATH` runs it without the store's migrate-on-open and prints JSON. It is measurement first, by the lead's ruling, before any corrective rewrite of history. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * refactor(store): the version tie diagnostic is a standalone script, read-only by construction (BUG-3751) Per the lead, for running against production Postgres: - No product surface: `pad db diagnose-version-ties` is removed. The operator runs `go run ./scripts/diagnose-version-ties -pg DSN`. - Read-only by construction: one transaction opened READ ONLY through TxOptions and again with SET TRANSACTION READ ONLY. A test shows a write inside it refused (SQLSTATE 25006); with both removed (mutant on test-pg) the write goes through and the test fails. SQLite opens query_only. - Bounded: SET LOCAL statement_timeout (-timeout, default 30s), one item at a time, progress on stderr. - Counts only as JSON on stdout: no ids, titles or content. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): the tie diagnostic bounds its whole run and does not judge groups a snapshot masks (BUG-3751) Codex round 1: - Per-statement timeouts did not bound the run, or the read-only snapshot it holds on a production database. -max-duration (default 15m) stops the run and reports what it scanned, with truncated: true. - A full-content row in a tie group resets the content whatever the order, so such groups are counted as with_snapshot and never as ordered or misordered. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): the tie diagnostic's deadline is a context from the first statement, and permutations are checked against the rest of the chain (BUG-3751) Codex round 2: - The whole-run bound was checked only between items, after the first scan. It is now a context deadline set before the first statement, so it cancels any query (SQLite included) and ends the snapshot. Reaching it, wherever, returns a partial report with truncated set. - An order was judged against only the next older version. It is now checked against every older version in stored order, up to the next tie group or full snapshot. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): the tie diagnostic's deadline bounds classification too, and a sub-millisecond timeout is never 0 (BUG-3751) Codex on the diagnostic: the run deadline was checked only between items, so one item with many tie groups could run past -max-duration and still report a complete result; and a positive -timeout under 1ms truncated to statement_timeout = 0, which PostgreSQL reads as no limit. Classification now checks the context per group, inside the permutation search and before judging, and each item is folded into the report only when it finishes. The timeout rounds up to whole milliseconds. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): the tie diagnostic's deadline bounds every version it applies (BUG-3751) Codex on the diagnostic: applyOrder walked and applied versions with no deadline check, and the singleton, snapshot and oversized paths could cross the deadline on their last step and still report the item complete. applyOrder now checks the context before each version, and every successful return of classifyTieGroups re-checks it. The checks in the permutation search and before judging a group are removed: once the deadline passes applyOrder applies nothing, and the return re-check keeps the item out of the counts, so no mutant of them can be observed. The test now ties the deadline to the work: it expires during the n-th version application, for every n, across histories that take every path. An earlier form tripped on the n-th Err call and could not see a removed check. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): the tie diagnostic sets its statement timeout first and requires a run deadline (BUG-3751) Codex on the diagnostic: BEGIN and SET TRANSACTION READ ONLY ran before statement_timeout was set. The timeout is now the first statement in the transaction. BEGIN and that SET cannot be capped by statement_timeout; they are bounded by the run deadline, which the operator entry point now requires (a max duration of zero or less is refused). On Postgres the test reads back statement_timeout and transaction_read_only from inside the transaction. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(store): a deadline that closes the tie diagnostic's rows before a scan is a truncated report (BUG-3751) Codex on the diagnostic: database/sql closes the rows when the deadline passes, and if that lands between Next and Scan the scan fails. Both row loops returned that as an error; they now report it as truncated, like every other deadline path. A test hook runs before each scan so a test can put the deadline there. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS |
||
|
|
1d20ae56c8 |
fix(release): the release smoke stops the server it boots, and checks that it did (TASK-2035) (#1983)
#1980's script started the server through clean(), a shell FUNCTION, in the background. That runs in a subshell, so $! was the subshell's pid: the startup liveness check, the SIGTERM and the 'did not stop within 5s' assertion were all aimed at the subshell, and the real server kept running. The stop check could never fail, and every run left a server behind (found as seven stray pad servers, two under qemu, from the snapshot verification, while chasing a low-memory reap). The server now starts through env directly, so $! IS the server, and 'stopped' also means nothing answers on the port afterwards. Measured against a real binary: the old script passes and leaves 1 server running; this one passes and leaves 0. |
||
|
|
2c1706fbbb |
build(release): a shipped binary that does not start fails the release before it publishes (TASK-2035) (#1980)
A GoReleaser builds[].hooks.post runs scripts/release-smoke.sh on every binary right after its build and before anything is archived, signed, notarized, pushed or uploaded; a non-zero exit fails 'release' at the build stage with nothing published. No skip switch (lead ruling): a flake gets a re-run, not a bypass. - Every target, statically (go version -m reads any GOOS/GOARCH): a Go executable whose main is github.com/PerpetualSoftware/pad/cmd/pad, with the GOOS/GOARCH it claims and the version and commit the release sets in its -ldflags (GoReleaser builds without VCS stamping). - linux/amd64 natively and linux/arm64 under qemu-user also BOOT: --version names the version and commit, and 'server start' on a free loopback port with an empty HOME/data dir answers /api/v1/health with status ok, the same version and commit, and an embedded web bundle built from the same commit; then it must stop on SIGTERM. - darwin and windows cannot run on the ubuntu runner; ci.yml's native-smoke job boots them from source on every PR. The binary runs with an empty environment: the hook inherits the GoReleaser step's env, which carries the macOS signing and notary secrets. qemu-user comes from Ubuntu's own package, not a new third-party action, since this job holds contents:write and the GHCR token. Verified with goreleaser v2.15.4 --snapshot (publish, sign, notarize, docker and sbom skipped): all six pass; a hook expecting the wrong version fails all six at the build stage, and a binary that exits at 'server start' fails linux/amd64 and linux/arm64 there; both produce no archive. |
||
|
|
0bdf539455 |
feat(release): the Claude Code plugin follows stable releases; the release workflow pins the marketplace (TASK-3487) (#1890)
* feat(release): the Claude Code plugin follows stable releases; the release workflow pins the marketplace (TASK-3487) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * fix(release): the plugin pin never moves backwards; force the tag fetch (TASK-3487 codex r1) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS |
||
|
|
4532eecffd |
feat(decision): conventions ask about the item without its comment trail, 12 questions a call (TASK-3119 U2a) (#1871)
* chore(eval): TASK-3119 U2 gates in one command; decision.BuildCommentState -gates runs three passes through the production question path: A U0's population on the U2a item state (links in, trail out), gated on precision not dropping below U0's on any convention that fired; B comment-borne breaks, each comment asked alone with its item's title and collection, gated on precision >= 0.9; C a measurement of 12+ questions in one call (MaxPerCall 12). BuildCommentState is the comment subject's one serialisation, shared by the harness now and the runner in U2b. Comments are clipped at the body bound (16k runes), not the trail's 2k. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * feat(decision): the conventions set asks about the item without its comment trail, 12 questions a call (TASK-3119 U2a) The conventions state carried the last 10 comments, clipped at 2,000 runes each: on docapp that was most of every call's input (15.7k tokens for one write across 3 calls), 720 items had a clipped comment, and a break past the clip was invisible anyway. Comments get their own subject in U2b. - QuestionSet.NoTrail leaves the trail out; the conventions set sets it. The state is exactly what the U2 gates measured (recent_trail [], links in): precision held at 100% on every convention that fires, recall rose (1286 40->80%, 2693 70->90%), 0 truncated states, ~954 input tokens per call. - MaxPerCall 5 -> 12, from measure C: up to 13 questions in one call gave identical precision and recall, 0 of 116 answers moved by more than 0.2. - A comment write no longer changes this set's state, so it costs no call. - Every conventions answer computed before this reads not-current until the item's next write (the state bytes changed): a fleet-wide re-ask, by design. - CLAUDE.md: the registered sets (it still said attention was the only one, since U1a) and the per-set state. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|