From 8ca0688993bf777ae74f7e11028932e33aee3e9a Mon Sep 17 00:00:00 2001 From: "linh.doan" Date: Sun, 13 Sep 2026 04:24:20 +0700 Subject: [PATCH] fix(ledger): release-created rows carry tag/sha again (regressed in #349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #349 added a Revision field to the Go ReleaseRecord but dropped Tag/SHA from the `record release` literal while still printing "(tag @ sha)" to stdout, so every Go-written release row persisted empty tag/sha — the exact Q24 fields the record exists to carry. No test covered the CLI write site, so it shipped green. - restore Tag/SHA in the ReleaseRecord literal (actions_ledger.go) - add internal/cli/actions_release_test.go pinning the CLI row shape (proven red on the regressed literal, green after) - update docs/PRD.md footer + regenerate docs/PRD.html (pandoc) so the styled mirror matches PRD.md and gains #349's build-revision line too --- docs/PRD.html | 56 ++++++++++++++++++++------ docs/PRD.md | 1 + internal/cli/actions_ledger.go | 2 + internal/cli/actions_release_test.go | 59 ++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 internal/cli/actions_release_test.go diff --git a/docs/PRD.html b/docs/PRD.html index ea0b69e..882248c 100644 --- a/docs/PRD.html +++ b/docs/PRD.html @@ -4945,8 +4945,20 @@

23.1 Requirements

chaos schedule) so "the driver works perfectly" is a checkable claim, not a hope.


-

*Last updated: 2026-09-13 (herdr orphan-pane reaping: both probes -were dead, repaired as one change) — +

*Last updated: 2026-09-13 (release-created rows carry +tag/sha again: PR #349's Revision stamp landed +as a silent data-loss regression) — #349 added a Revision +field to the Go ReleaseRecord but dropped +Tag/SHA from the record release +literal while still printing (tag @ sha) to stdout, so +every Go-written release row persisted "tag":"" +"sha":"" (the Q24 fields the record exists to carry) and no +test covered the write site, so it shipped green. Restored both fields; +internal/cli/actions_release_test.go now pins the CLI row +shape (proven red on the regressed literal, green after); regenerated +docs/PRD.html so the styled mirror matches +PRD.md. *Last updated: 2026-09-13 (herdr orphan-pane +reaping: both probes were dead, repaired as one change) — devagent herdr-sweep --orphans had never reaped a live orphan: pane process-info went out without --session, so herdr answered for its own default session @@ -4995,15 +5007,37 @@

23.1 Requirements

split against real pgrep exit codes: 1 means no collector (reap), 2 means the probe could not run (spare). Docs corrected: FR-VIS-02/07/10, §18 Q23, docs/HERDR.md sweep safety. *Last -updated: 2026-09-12 (the rescue reaches CLOSED-but-unmerged pull -requests: a goal-named closed PR is reopened and merged, or lands -nothing) — the driver-side verify-and-merge rescue, and the goal-text -derivation feeding it, only accepted subjects that read -OPEN before the dispatch, so the false closes were -unreachable: PR #346/#347 (auto-closed by the zombie sweep's -BaseBranchGone(main) transport-noise bug minutes after they -opened, green and mergeable) and loop 282's #345 each recorded -no-pr with a landable branch left closed. +updated: 2026-09-12 (build-revision self-identification: a stale driver +binary is loud, and Go-written release rows name their writer) — loops +290/291 burned implement-and-gate cycles while the driver seat ran a +binary built from an older commit, and nothing announced the mismatch. +internal/version.Revision() (new, memoized) reads +debug.ReadBuildInfo's vcs.revision and returns +version.RevisionUnknown ("unknown") when the +build carries no stamp — go-test binaries carry none (probed +2026-09-12), so version.RevisionOverride is the injection +seam. RunLoop compares Revision() against +git rev-parse HEAD before the first iteration and warns +[loop] WARN: stale binary when the revision is unknown or +differs, naming both SHAs (advisory only: the loop still runs). +ledger.ReleaseRecord gains revision (stamped +at the record release write site; omitempty +keeps unstamped Go rows byte-compatible with the TS writer — Node +readers tolerate the extra key, pinned by +TestSchemaDriftUnknownFieldsTolerated), and +devagent --version prints +0.1.0 (rev <sha>). Pinned by +TestRevision (sentinel, memoization, override) and +TestRunLoopStaleBinaryWarns (unknown warn, mismatch warn, +matching-revision silence, rc 0 on every arm). *Last updated: 2026-09-12 +(the rescue reaches CLOSED-but-unmerged pull requests: a goal-named +closed PR is reopened and merged, or lands nothing) — the driver-side +verify-and-merge rescue, and the goal-text derivation feeding it, only +accepted subjects that read OPEN before the dispatch, so +the false closes were unreachable: PR #346/#347 (auto-closed by the +zombie sweep's BaseBranchGone(main) transport-noise bug +minutes after they opened, green and mergeable) and loop 282's #345 each +recorded no-pr with a landable branch left closed. prView now decodes baseRefName/headRefName, and a goal-named pull request that is CLOSED, unmerged, based on main with its diff --git a/docs/PRD.md b/docs/PRD.md index 6e70186..3c2a5fc 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1695,6 +1695,7 @@ existing driver with validation surfaces (test, command, telemetry, chaos schedule) so "the driver works perfectly" is a checkable claim, not a hope. --- +*Last updated: 2026-09-13 (release-created rows carry `tag`/`sha` again: PR #349's Revision stamp landed as a silent data-loss regression) — #349 added a `Revision` field to the Go `ReleaseRecord` but dropped `Tag`/`SHA` from the `record release` literal while still printing `(tag @ sha)` to stdout, so every Go-written release row persisted `"tag":""` `"sha":""` (the Q24 fields the record exists to carry) and no test covered the write site, so it shipped green. Restored both fields; `internal/cli/actions_release_test.go` now pins the CLI row shape (proven red on the regressed literal, green after); regenerated `docs/PRD.html` so the styled mirror matches `PRD.md`. *Last updated: 2026-09-13 (herdr orphan-pane reaping: both probes were dead, repaired as one change) — `devagent herdr-sweep --orphans` had never reaped a live orphan: `pane process-info` went out without `--session`, so herdr answered for its own default session and "no such pane" was every devagent pane's liveness, which left the FR-VIS-07 in-flight guard, the FR-VIS-02 roster upgrade (#317) and the orphan class — gated on a live foreground worker — inert at once; the owner probe was `herdr.*pane run .*`, a process that does not exist while a pane runs (`pane run` types the script into the pane's shell and exits; verified live parent chain `omp -> -zsh -> herdr --session server -> launchd`), so `PaneRunOwnerOrphaned` always took its "no owner CLI -> orphaned" exit and the driver spare behind it was unreachable. Each half alone is a regression — a working owner probe over an inert liveness probe closes live workers — so the fix lands atomically: `PaneForegroundWorker(cli, session, paneID)` scopes the probe (roster and sweep share it), the owner is now the surviving dispatcher (argv0-anchored `devagent`/`devagent-go` `task`/`pane-run`, optionally behind the driver's `timeout` wrapper, so a prompt quoting devagent cannot impersonate its own collector), non-worktree panes need positive per-pane dispatch evidence (the capture contract on the worker's fd 1/2), and the orphan class moved ahead of the roster spare. Reaping also fails closed: `psPidsMatching` and `pidAncestryCommands` now report whether the probe answered at all — pgrep's exit-1 "no match" is an answer (no collector, reap), an absent binary / the 5s cap / a rejected pattern / an unfinished ancestry walk is not (spare) — because a reaper that read "could not inspect" as "no collector" would close every live pane on such a host. Test seams: `DEVAGENT_SWEEP_OWNER_PIDS_JSON` is keyed by the pattern asked for and a miss now counts as an unanswered probe (never a clean no-match), `DEVAGENT_SWEEP_PANE_CAPTURE_JSON` supplies pane-keyed evidence — and because the 2026-09-13 bug survived a green stub suite, both shapes are pinned from outside: `TestProcFdsCaptureSeesRealCaptureContract` runs the real `lsof` probe against a live child holding `/devagent-herdr-/out` on fd 1/2 (skipped where lsof is absent), `TestPaneRunOwnerPatternMatchesRealDispatchShapes` checks `paneRunOwnerPattern` against the command lines `internal/loopdriver` really builds and against the transient `herdr pane run` shape it used to search for, and `TestOrphanClassGoesInertWhenProbesCannotAnswer` pins the spare-on-no-answer direction at the matrix level. `TestPsPidsMatchingAnswersOnlyWhenPgrepAnswers` pins the same split against real `pgrep` exit codes: 1 means no collector (reap), 2 means the probe could not run (spare). Docs corrected: FR-VIS-02/07/10, §18 Q23, `docs/HERDR.md` sweep safety. *Last updated: 2026-09-12 (build-revision self-identification: a stale driver binary is loud, and Go-written release rows name their writer) — loops 290/291 burned implement-and-gate cycles while the driver seat ran a binary built from an older commit, and nothing announced the mismatch. `internal/version.Revision()` (new, memoized) reads `debug.ReadBuildInfo`'s `vcs.revision` and returns `version.RevisionUnknown` (`"unknown"`) when the build carries no stamp — go-test binaries carry none (probed 2026-09-12), so `version.RevisionOverride` is the injection seam. `RunLoop` compares `Revision()` against `git rev-parse HEAD` before the first iteration and warns `[loop] WARN: stale binary` when the revision is unknown or differs, naming both SHAs (advisory only: the loop still runs). `ledger.ReleaseRecord` gains `revision` (stamped at the `record release` write site; `omitempty` keeps unstamped Go rows byte-compatible with the TS writer — Node readers tolerate the extra key, pinned by `TestSchemaDriftUnknownFieldsTolerated`), and `devagent --version` prints `0.1.0 (rev )`. Pinned by `TestRevision` (sentinel, memoization, override) and `TestRunLoopStaleBinaryWarns` (unknown warn, mismatch warn, matching-revision silence, rc 0 on every arm). *Last updated: 2026-09-12 (the rescue reaches CLOSED-but-unmerged pull requests: a goal-named closed PR is reopened and merged, or lands nothing) — the driver-side verify-and-merge rescue, and the goal-text derivation feeding it, only accepted subjects that read `OPEN` before the dispatch, so the false closes were unreachable: PR #346/#347 (auto-closed by the zombie sweep's `BaseBranchGone(main)` transport-noise bug minutes after they opened, green and mergeable) and loop 282's #345 each recorded `no-pr` with a landable branch left closed. `prView` now decodes `baseRefName`/`headRefName`, and a goal-named pull request that is CLOSED, unmerged, based on `main` with its head ref still on origin becomes the merge subject: `verifyAndMergeRescue` reopens it (`gh pr reopen`, repo-scoped, 60s wall, drain-tolerant), re-reads the state (gh's reopen is verified, never believed) and only then merges through the existing `mergePRBounded` pipeline. Everything else lands nothing — another/superseded base, a head ref gone from origin, a merge stamp, a reopen gh refuses — so no ship is ever fabricated. Pinned by `TestRunLoopClosedUnmergedGoalPRReopenedAndMerged`, `TestRunLoopClosedGoalPRLandsNothingWhenNotReopenable` and `TestVerifyAndMergeRescueRefusesNonReopenablePR` (`internal/loopdriver/run.go`, `issues.go`). diff --git a/internal/cli/actions_ledger.go b/internal/cli/actions_ledger.go index 9147b8a..cfc8bab 100644 --- a/internal/cli/actions_ledger.go +++ b/internal/cli/actions_ledger.go @@ -237,6 +237,8 @@ func recordCommand() *cobra.Command { TaskID: "release/" + version, Attempt: 1, Event: "release-created", + Tag: tag, + SHA: sha, Version: version, Revision: versionpkg.Revision(), Source: source, diff --git a/internal/cli/actions_release_test.go b/internal/cli/actions_release_test.go new file mode 100644 index 0000000..d578b87 --- /dev/null +++ b/internal/cli/actions_release_test.go @@ -0,0 +1,59 @@ +package cli + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestRecordReleasePersistsTagAndSHA pins the row shape at the `record +// release` CLI write site: every Q24 field the command reads from flags +// must land in the appended release-created row. PR #349 added a Revision +// field but silently dropped Tag/SHA from the ReleaseRecord literal while +// still printing "(tag @ sha)" to stdout, so rows persisted "tag":"" +// "sha":"" — the exact fields the record exists to carry. No test covered +// this write path, which is why it stayed green; this closes that gap. +func TestRecordReleasePersistsTagAndSHA(t *testing.T) { + repo := t.TempDir() + + root := NewRoot() + root.SetArgs([]string{ + "record", "release", + "--tag", "v1.2.3", + "--sha", "deadbeefcafe", + "--repo", repo, + "--source", "cli", + }) + root.SilenceUsage = true + if err := root.Execute(); err != nil { + t.Fatalf("record release: %v", err) + } + + data, err := os.ReadFile(filepath.Join(repo, ".devagent", "runs", "orchestration", "events.jsonl")) + if err != nil { + t.Fatalf("read events.jsonl: %v", err) + } + line := strings.TrimSpace(string(data)) + if line == "" { + t.Fatal("no release-created row written") + } + + var row map[string]any + if err := json.Unmarshal([]byte(line), &row); err != nil { + t.Fatalf("unmarshal row %q: %v", line, err) + } + + for key, want := range map[string]string{ + "event": "release-created", + "tag": "v1.2.3", + "sha": "deadbeefcafe", + "version": "1.2.3", + "source": "cli", + } { + if got, _ := row[key].(string); got != want { + t.Errorf("row[%q] = %q, want %q", key, got, want) + } + } +}