Skip to content

feat(go): continue the sumctl port — occupancy/reservations fix + status/inbox (#94) - #99

Merged
douglasjarquin merged 17 commits into
mainfrom
sum-dev/issue94-go-port
Sep 11, 2026
Merged

feat(go): continue the sumctl port — occupancy/reservations fix + status/inbox (#94)#99
douglasjarquin merged 17 commits into
mainfrom
sum-dev/issue94-go-port

Conversation

@douglasjarquin

@douglasjarquin douglasjarquin commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Summary

Continues the full Go/Cobra port of sumctl from #94, on top of merged PR #95 (8 native commands: release-contract, settings show, preset list/show, graph config, metadata snippet, init, doctor). This branch adds:

  • Fix: occupancy/capacity semantics (go/internal/reservations): main moved since PR feat(go): first slice of full Go port of sumctl (#94) #95 merged — a new lib/execution_reservations.py module changed occupancy()/holds_slot() from a simple non-archived-task count to per-attempt held-reservation counting (a task can now hold 0, 1, or several execution slots at once: a worker attempt plus independent verifier attempts). This silently affected the already-merged settings show/preset list output. Ports execution_reservations.py's read path in full — the exact _attempt/_occupant/_observation validation (same error text on a malformed record) plus held(task) — and rewrites settings.occupancy/capacity_view to match, including the new middle error branch (occupancy itself can now fail independently of settings.json) and updated note text.
  • Test fix: the live doctor/init differential tests compare two separate process invocations that each embed a live timestamp; normalized ISO timestamps before comparing to remove a real (if rare) flake.
  • status/inbox (non-live): ports status(store, live=False, inbox=?) in full — the brief/version sidecar (go/internal/versions: read_versions/legacy_versions/versions_view/refresh_state/revision_view, including revision content-hash verification), the returns/obligations subsystem (go/internal/returns: open_attention/open_obligations/return_route/route_key/read_returns/notification_state/returns_view), cleanup_pending (go/internal/cleanup), and metadata_summary (added to the existing go/internal/metadata). statuscmd assembles all of it into the per-task row plus the top-level envelope.
  • brief list TASK_ID: trivial on top of the versions package — a task read plus versions.View with brief_path prepended. brief regenerate/request/adopt stay on the Python reference.
  • env show TASK_ID [--max-chars N] (go/internal/environment): ports env_show/environment_viewread_environment's symlink/schema-mismatch checks, redact's 8 secret-pattern regexes (GitHub/OpenAI/Slack/AWS token shapes, bearer tokens, PEM headers, generic key=/token=/secret=/password= assignments), bounded_view's truncation metadata (including its chars/truncated/redactions/text/note polymorphic-vs-plain-string call convention), and the discovery/endpoints/logs/resources/services reshaping (services_view, including the Python falsy-empty-dict readiness/stopNone quirk). env discover/record/inspect/start/stop stay on the Python reference — those observe or mutate live state.
  • release list/release show SHA (go/internal/release): ports release_list/release_show/verify_release's full ~20-check manifest verification (schema, source SHA, every manifest-listed file's content hash, required files, bin/sumctl's executable bit, no .sum/.codegraph in the tree, Mesh overlay marker deep-equality plus its dist/ file hashes, pinned tool symlink resolution, optional dependency-inventory + native-artifact validation) and installation_root's designated-state-home + git-toplevel check. A typed VerifyError marks every expected validation failure (mirroring raise SumError); release_list catches only that type per row (ok:false + error text) and lets an unexpected filesystem error propagate and fail the whole command, matching Python's except SumError letting other exception types through. release stage stays on the Python reference (it mutates).
  • project list/project show NAME (go/internal/project): ports project_list/project_show/observe_projectread_projects's symlink/schema checks, project_identity/parse_remote_identity/same_remote's Git-URL parsing (ssh-like and https forms, scp-style shorthand, host normalization, repo-part validation), and observe_project's live one-shot git inspection (toplevel check, remote comparison, HEAD, dirty status, linked worktrees) — each tolerant of git failures exactly like Python (falls back to None/false rather than raising). project enroll/migrate stay on the Python reference (they mutate or clone).
  • hook status (go/internal/hookstatus): ports hook_status/hook_summary/pending_summary/hook_manifest/observe_pluginread_health's symlink/schema checks, pending_summary's open-obligations aggregation across all tasks (reusing go/internal/returns), hook_manifest's TOML generation (for the expected_manifest_current hash comparison), and observe_plugin's bounded live registry probe via the existing herdrclient/toolpath packages when a Herdr session context is available. Faithfully reproduces a subtle Python quirk: hook_status reads health.json a second time, unguarded, after hook_summary already caught and degraded on the first read — so a malformed health.json still fails the whole command, not just one field (confirmed via a differential test). hook enable/disable/event stay on the Python reference (they mutate or require a live session). The live Herdr registry sub-path (ctx available + plugin actually registered) is implemented but not exercised by any test — verifying it needs a live Herdr session, the same category of gap as status --live.
  • show TASK_ID (go/internal/evidenceview): ports the full task record plus versions_view, evidence_view, and returns_view — reusing the already-ported versions.View/returns.View and adding evidence_view/current_candidate new. evidence_view is the closure-readiness computation: scoped evidence records tagged with candidate currency, a synthesized legacy report row when a task predates structured evidence, worker/root verification run selection, review status, and the "missing" prerequisite list (structured handoff, complete/matching PR identity, passing root verification, standardized-contract worker run + review + policy-review gates). current_candidate does one live git rev-parse HEAD in the task's worktree, tolerating a missing worktree or git failure exactly like Python.
  • context TASK_ID (go/internal/contextview): ports the default context_view path (no flags, which context_view itself always resolves to the single "outline" section) plus --section NAME (repeatable, deduplicated in first-occurrence order, matching list(dict.fromkeys(...))) for all 10 of 10 CONTEXT_SECTIONS: outline, decisions, returns, handoff, evidence, brief, notes, execution, environment, update. Ports cursor_of/cursor_counters/state_digest (the cursor token every read carries), paged (the stable append-only-list pagination helper several sections share), outstanding, latest_handoff/handoff_view/section_handoff, section_decisions, section_evidence, section_brief, section_execution, section_environment (skill_references, return_commands), section_update (default_runtime, reusing release.VerifyRelease/release.InstallationRoot), and the inline returns section body — reusing versions.View, evidenceview.View, returns.View, returns.OpenAttention, and cleanup.Pending. Also ports --role worker|reviewer|coordinator: it defaults the section list to ROLE_SECTIONS[role] when no --section is given, filters section_decisions by role (worker→answered-only, coordinator→open-only), adds the top-level contract/authority envelope (ROLE_CONTRACT), narrows skill_references to just that role's skills, and — for reviewer/coordinator — adds environment.artifacts via a new artifact_references/artifact_scope port (classifies worker-supplied handoff artifact strings as checkout/outside-checkout/unscoped by string inspection only, no filesystem calls). Four new small packages: go/internal/notes (notes_state, the task's optional timestamped-entry notes.md), environment.Outline/Stamp (environment_outline/environment_stamp, needed by the cursor and outline), go/internal/graphview (read_graph/graph_path/graph_summary/graph_view — the task-scoped graph.json sidecar, distinct from the existing go/internal/graph, which only handles graph config's MCP snippet), and the role/artifact helpers above. Also ports --since CURSOR cursor-based diffing: parse_cursor (the CURSOR regex, raising Python's exact error text for a malformed token) and changes_since (new questions, status-changed questions matching the cursor's timestamp, new evidence with report/publication change flags, refresh events, new attention, notes-entries delta, and the state-digest-based changed/unchanged verdict) — including the subtle early-return control flow: when --since is given alone (no --section/--role) and nothing changed, the command returns early with just the envelope, changes, and a note, rendering no section at all, exactly matching context_view's variable-reuse structure. Also adds ordjson.MarshalSortedCompact, a new encoder matching Python's json.dumps(sort_keys=True) exactly (sorts object keys at every level, leaves array order alone) — state_digest/environment_stamp hash this text directly, so it has to be byte-identical. Along the way, fixed two real bugs: (1) a latent bug in ordjson where a nil *ordjson.Object (a typed nil, distinct from an untyped nil) crashed all three encoders via a nil-pointer Len() call instead of encoding as JSON null — fixed in encodeCompact/encodeIndent/encodeSortedCompact, benefiting every port using ordjson, not just context; (2) a correctness bug introduced by the first --section commit in this same PR: validContextSections listed all 10 CONTEXT_SECTIONS names before contextview.View's switch had cases for all of them, so requesting an unimplemented section (e.g. --section update, at the time) was silently accepted and omitted that section's key from the output entirely instead of falling back to the Python reference — fixed by restricting validContextSections to only what's actually wired at each commit, adding each section's case and its validity entry in the same commit from then on.
  • context TASK_ID --after/--limit/--max-chars/--revision/--kind: the last unported flags on context. Bundles the growing flag list into a contextview.Options struct rather than continuing to grow View's positional parameter list. --limit/--after/--max-chars range validation (--limit 1..CONTEXT_MAX_LIMIT=200; --after/--max-chars non-negative) happens natively inside contextview.View, matching context_view's exact error text and code position; a malformed (non-integer) value for any of the three falls back to the Python reference so argparse's own type=int error text applies rather than replicating it. --revision reuses the already-ported versions.ReadVersions plus a newly-exported versions.RevisionView (was unexported, used only internally by brief list) to look up a recorded brief revision's verified content. --kind filters section_evidence's record list before paging. Fixed a third real bug found by differential testing while building this: section_evidence's kinds count-by-kind breakdown must be computed from all evidence records, not the --kind-filtered subset — Python's by_kind loop runs over view["records"] before filtering; the initial Go port filtered first, which would have under-counted every kind not requested. This completes native porting of 100% of sumctl's read-only command surface — every command/flag combination that only reads state now runs natively in Go; nothing read-only still falls back to the Python reference.

Scope

The --live path (status --live/inbox --live) is not ported: Snapshots' live agent fan-out across sessions/panes, attention_sweep, and pump's notification delivery are a separate, larger, stateful undertaking. status/inbox only go native for zero extra arguments; --live falls back to the Python reference unchanged.

Test plan

  • go build ./... && go vet ./... && gofmt -l . clean inside go/.
  • go test ./... — all packages pass, including differential tests against the real Python bin/sumctl:
    • settings show/preset list re-verified against the new occupancy model: a legacy running task, a legacy archived task, a real multi-reservation execution record (1 held worker + 1 released verifier + 1 held verifier → 2 slots), and a malformed reservation (invalid id prefix) exercising the new error path.
    • status/inbox against an empty store and against the same fixture set above (exercising occupancy through the full command, not just settings show).
    • doctor/init live against this dev checkout's own real --home (safe: both are read-only), now with timestamp normalization.
    • brief list across a legacy task (no versions.json), a real versions.json with one intact revision, and a report recorded under a superseded revision while a later revision is verification-affecting (the trickiest report_evidence branch).
    • env show across: no environment record; a full present record (discovery with a secret-shaped command exercising redaction, endpoints incl. a stale one with conflicts, logs, resources, services with both populated and empty-dict readiness/stop); --max-chars truncation; --max-chars 0 unbounded; and a schema-mismatch error path.
    • release list/release show against a synthetic git-repo "installation" fixture (a tempdir git init'd with a designating .sum/state.json — never the real installation this checkout is a worktree of): empty releases dir; a fully valid release built file-by-file to satisfy every verify_release check (real hashed files, Mesh overlay marker, resolving tool symlinks) alongside a broken (missing-manifest) release and an in-progress .staging-* dir, all in one release list; release show by full SHA and by a short prefix; and a no-match release show, exercising the command-level {"error": ...} failure path (Go's returned error text checked against Python's own stderr JSON).
    • project list/project show against synthetic git clones (never the real installation): an empty registry; a clean clone with a matching remote, a dirty clone, a clone with a mismatched recorded remote, and a missing clone, all in one project list; project show with active tasks (excluding an archived one and an unrelated-repository one) and an unknown-project command-level failure path.
    • hook status with Herdr env vars blanked (both sides take the records-only ctx=None path): no health.json; a disabled record with one pending obligation (exercising pending_summary's count/age); an enabled record with a plugin_id but no instance identity yet (the expected_manifest_current error-to-None path); an enabled record with a mismatched manifest hash; and a malformed-schema health.json exercising the command-level failure path (the double-read quirk above).
    • show across: a minimal task (no worktree/evidence/PR/reviewer); a legacy report with no structured evidence (exercises the synthesized report row); a real git worktree with a failing root verification run (exercises the "latest root run ... was fail" missing message); and a fully standardized contract with a real worktree, handoff, worker run, root run, and an approving policy review — all current against the live HEAD — reaching prerequisites_met: true end to end.
    • context (bare, outline-only) across: a minimal task with no worktree/evidence/notes/environment record; a real git worktree with a current handoff and a passing root verification (exercises evidence_view's closure/verification fields inside the outline); and a task with open/applied decisions, a notes.md with one entry, and a stale environment record with one endpoint and one running service (exercises environment_outline's per-state counts). The live read_at timestamp is normalized before comparing.
    • context --section: decisions + returns together on a task with one open and one applied question; handoff + evidence together against a real git worktree with a handoff (files, checks, next_action), a passing coordinator verification, and an approving review (three evidence kinds in one task); brief on a legacy task with no versions.json; notes with a present two-entry notes.md and with no notes.md at all; execution with no graph record/launch/parent/reviewer and with a ready graph record (index counts, a successful attempt) plus a full launch/admission/parent/reviewer; environment with no environment.json and with a present record; update on a legacy task outside any installation (exercises the try/except installation_root "unavailable" branch — the realistic case for a lab/dev --home).
    • context --role: worker role with default sections (decisions filtered to answered-only); coordinator role with default sections (decisions filtered to open-only); reviewer role with an explicit --section environment exercising artifact_references across all four scope outcomes (a checkout-relative path, a ../-escaping path, an absolute path, and a ~-prefixed path) in one handoff record.
    • context --since: an unchanged cursor (fetched from a real prior read) producing the early-return note-only response; a changed cursor (a question added between reads) with no section requested, rendering just the changes block; the same changed cursor with an explicit --section, rendering both changes and the section; and a malformed cursor string exercising the command-level failure path.
    • context --after/--limit/--max-chars/--revision/--kind: --after+--limit paging the decisions section; --kind filtering the evidence section (this is the case that caught the by_kind bug above); --max-chars truncating the brief section's approved text; --revision looking up a recorded brief revision by ID against a real versions.json+revision file; an unknown --revision ID as a command-level failure; --limit out of range (0 and 201) and negative --after/--max-chars as command-level failures matching context_view's exact SumError text. Plus fallback checks for a non-integer --max-chars (mirrors argparse's own type=int error), an unrecognized --role, and an unrecognized --section name.
  • Repository verification, run as commits landed:
    • MISE_ENABLE_TOOLS=go,python,node python3 .agents/skills/verify/scripts/verify_run.py --base 81dbdda96dd7c0b31696634f9f943b28981f511bverify: pass (candidate d5b2aa5c3bb8ce62da65cec03d93a0b1902d87c2, non-provisional, 87 scenarios pass, 9 not-run/manual). Record: .artifacts/verification/20260911T022710Z-e937b5fb/run.json.
    • verify: pass at candidate efeea569bfd8c59259b3f55c117a28d7e9ea470d (the brief list commit), clean. Record: .artifacts/verification/20260911T065122Z-06aa265e/run.json.
    • verify: pass at candidate ebecb0979128befadc74522944438dca660cb817 (the env show commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T075104Z-46e0e968/run.json.
    • verify: pass at candidate 21f23905e06f719ec6b4ba90b6a84fb66965c1bd (the release list/show commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T082115Z-109d3261/run.json.
    • verify: pass at candidate fbfb1a01bea055a75b003e0a6201c5a1ad2b43cb (the project list/show commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T084841Z-235b32dc/run.json.
    • verify: pass at candidate 822d13d1d75af15e9c9e6830d433dcb2d72864b0 (the hook status commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T091550Z-3bb6e83f/run.json.
    • verify: pass at candidate 24127e148d316af0dd09e179e610e7ad7125f732 (the show commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T094139Z-5934bdf5/run.json.
    • verify: pass at candidate dae51d4661e683b410db97717b8ebb5561e19f46 (the context outline commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T101436Z-8ddee53e/run.json.
    • verify: pass at candidate 51de4df21f2d34f3bdca47207bf02db0dcf96bb2 (the context --section commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T104124Z-1dd256e6/run.json.
    • verify: pass at candidate 2a2aad2878ab7919aecb88a91aca844ce23b7287 (the notes-section test fix), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T110438Z-0b839a92/run.json.
    • verify: pass at candidate 265fe4733f4a2e2d8542cb0fc81a680b4bfe9b87 (the execution section commit + validContextSections fix), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T112934Z-339ffa0b/run.json.
    • verify: pass at candidate 9b99e8f089e916d2a1516a4f68b10d6b121d51e9 (the environment/update sections commit, completing all 10 CONTEXT_SECTIONS), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T115739Z-3ba188e3/run.json.
    • verify: pass at candidate 32c724cfd6f97599f437c5a64b8f26a0b9ade845 (the --role support commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T122439Z-939c5b97/run.json.
    • verify: pass at candidate 0091b13121eea002e655422cdf4013cfa92951c2 (the --since support commit), clean, 87 scenarios pass, 9 not-run/manual. Record: .artifacts/verification/20260911T125207Z-c25f111b/run.json.
    • 02891f6 (this final --after/--limit/--max-chars/--revision/--kind commit, completing the read-only surface) is NOT yet independently verified through verify_run.py. Three consecutive attempts were killed by the OS for low memory on this shared host (free -h showed ~9.5-9.6Gi available each time, verify's own log empty each time — consistent with transient resource pressure from other concurrent sessions, but three in a row is outside the "check memory, retry once" pattern that resolved cleanly twice earlier in this same PR, so retries were paused rather than repeated blindly). go build ./... && go vet ./... && gofmt -l . && go test ./... are all clean locally for this commit (see Test plan above) — only the full repository-level verification run is outstanding. Will retry and record the result here once it completes cleanly.
  • Confirmed origin/main has not moved past this branch's base since verification ran (checked each time).

Review status

Independent review was not performed beyond the automated verification above — no separate reviewer pane was launched for this developer-checkout PR. Boss decides merge.

Limitations / what's not in this slice

  • 19 of ~35 sumctl subcommands now natively ported (adding status, inbox, brief list, env show, release list, release show, project list, project show, hook status, show, context to the 8 from feat(go): first slice of full Go port of sumctl (#94) #95); everything else still runs through the Python reference.
  • status --live/inbox --live (Snapshots, attention_sweep, pump) stay on the Python reference.
  • init's designated/coordinator branch is still not ported (unchanged from feat(go): first slice of full Go port of sumctl (#94) #95).
  • env discover/record/inspect/start/stop, release stage, project enroll/migrate, and hook enable/disable/event stay on the Python reference (observe, mutate, clone, or require a live session).
  • hook status's live-registry sub-path (a Herdr session context available and a plugin actually enrolled) is implemented but untested — no live Herdr session in this environment; same category of gap as status --live.
  • context TASK_ID now natively supports the bare shape, --section for all 10 sections, --role, --since, and --after/--limit/--max-chars/--revision/--kind. This was the last unported piece of the read-only surface: every read-only sumctl command is now natively ported in this branch. (Full repository verification of the final commit is still pending — see Test plan / verification log above.)
  • Next up is the mutating-commands phase (settings set, preset set/delete, dispatch, prepare, start, ask/answer/resolve/report, review, verify, pr reconcile/evidence, cleanup, pump, hook enable/disable/event, attention, bind, backup, project enroll/migrate, herdr bridge, graph init/status, dev prepare/list/remove, refresh request/status/adopt, release stage, update apply/rollback) — a qualitatively different risk class (store.lock()/atomic-write concurrency, not just reads) from everything ported so far. Holding for an explicit go-ahead from the boss before starting that phase, rather than proceeding autonomously as with the read-only work.

cs-test-runner added 17 commits September 11, 2026 02:26
…odel (#94)

Main moved since PR #95 merged: lib/sumctl.py's occupancy()/holds_slot()
no longer count one slot per non-archived task. A new
lib/execution_reservations.py module now tracks separate worker/
verifier attempt reservations per task (a task can hold 0, 1, or
several slots at once, one per held attempt), with a validated
"execution" record shape and a legacy fallback (no "execution" key:
one held slot unless archived) for tasks predating the change.

Ports execution_reservations.py's read path as go/internal/
reservations: the full _attempt/_occupant/_observation validation
(exact same error text on a malformed record) plus Held(task). Rewrites
settings.occupancy to sum held-reservation counts instead of a simple
archived check, and adds capacity_view's new middle error branch
(occupancy itself can now fail on a malformed record, independently of
settings.json) plus its updated note text. This affects the already-
merged `settings show`/`preset list` byte-for-byte.

Also exports Store.RegistrationKey (was unexported) for reuse.

Differential-tested against the real Python bin/sumctl across a
legacy running task, a legacy archived task, a real execution record
with one held worker + one released verifier + one held verifier
(2 slots), and a malformed reservation (invalid id prefix) exercising
the new error path.
…#94)

TestDoctor_matchesThePythonReferenceInThisDevCheckout and
TestInit_matchesThePythonReferenceInThisDevCheckout each run the Go
and Python implementations as two separate process invocations; both
embed a live now()/context() timestamp in their output, so a run that
straddles a second boundary flakes on an otherwise-correct candidate.
Normalize ISO timestamps to a placeholder before comparing.
Ports status(store, live=False, inbox=?) in full: the versions/brief
sidecar (go/internal/versions — read_versions/legacy_versions/
versions_view/refresh_state/revision_view, including revision content-
hash verification), the returns/obligations subsystem (go/internal/
returns — open_attention/open_obligations/return_route/route_key/
read_returns/notification_state/returns_view), cleanup_pending
(go/internal/cleanup), and metadata_summary (added to the existing
go/internal/metadata package alongside the snippet it already had).
statuscmd assembles all of it into the per-task row shape plus the
top-level tasks/capacity/guarantee/metadata envelope.

Scope: the `--live` path is NOT ported (Snapshots' live agent fan-out
across sessions/panes, attention_sweep, and pump's notification
delivery are a separate, much larger, stateful undertaking). `status`/
`inbox` only go native for zero extra arguments; `--live` falls back
to the Python reference unchanged.

Differential-tested against the real Python bin/sumctl: both commands
against an empty store, and against a fixture with a legacy running
task, a legacy archived task, and a task with a real multi-reservation
execution record (one held worker + one released verifier + one held
verifier) to exercise the occupancy fix above end-to-end through
`status`/`inbox` too.
Ports brief_command == "list": task read plus versions.View, with
brief_path prepended. Now trivial thanks to the already-ported
versions package. `brief regenerate`/`request`/`adopt` and anything
else stay on the Python reference.

Differential-tested against the real Python bin/sumctl across a
legacy task (no versions.json), a real versions.json with one intact
revision, and a report recorded under a superseded revision while a
later revision is verification-affecting (exercises report_evidence's
trickiest branch).
Ports env_show/environment_view (lib/sumctl.py) natively: read_environment's
symlink/schema-mismatch checks, redact's 8 secret patterns, bounded_view's
truncation metadata, and the discovery/endpoints/logs/resources/services
reshaping. `env discover/record/inspect/start/stop` stay on the Python
reference since they mutate.

Differential-tested against the real Python bin/sumctl: absent record,
a full present record (discovery with a secret-shaped command exercising
redaction, endpoints, logs, resources, services with both populated and
empty-dict readiness/stop), --max-chars truncation, --max-chars 0
(unbounded), and a schema-mismatch error path.
Ports release_list/release_show/verify_release (lib/sumctl.py): content_id/
sha256_file, the ~20-check manifest verification (schema, source SHA, every
manifest-listed file's hash, required files, bin/sumctl executable bit, no
.sum/.codegraph in the tree, Mesh overlay marker deep-equality plus its
dist file hashes, pinned tool symlink resolution, optional dependency
inventory + native-artifact validation), and installation_root's designated-
state-home + git-toplevel check. `release stage` stays on the Python
reference (it mutates).

A *VerifyError type marks every expected validation failure (mirroring
`raise SumError`); release_list catches only that type per release row
(ok:false + error text), matching Python's `except SumError` — an
unexpected filesystem error still propagates and fails the whole command,
as an uncaught Python exception would.

Differential-tested against the real Python bin/sumctl using a synthetic
git-repo "installation" fixture (state.json designates it, matching
installation_root's requirement, without touching the actual installation
this checkout is a worktree of): empty releases dir; a fully valid release
built file-by-file to satisfy every verify_release check plus a broken
(missing-manifest) release plus an in-progress .staging-* dir, all in one
`release list`; `release show` by full SHA and by a short prefix; and a
release show with no match, exercising the command-level `{"error": ...}`
failure path.
Ports project_list/project_show/observe_project (lib/sumctl.py): read_projects'
symlink/schema checks, project_identity/parse_remote_identity/same_remote's
Git-URL parsing (ssh-like and https forms, scp-style shorthand, host
normalization, repo-part validation), and observe_project's live one-shot
git inspection (toplevel check, remote comparison, HEAD, dirty status via
`git status --porcelain --untracked-files=all`, linked worktrees) — each
tolerant of git failures exactly like the Python reference (falls back to
None/false rather than raising). `project enroll`/`migrate` stay on the
Python reference (they mutate or clone).

Differential-tested against the real Python bin/sumctl using synthetic git
clones (never the real installation): an empty registry; a clean clone with
a matching remote, a dirty clone, a clone with a mismatched recorded remote,
and a missing clone, all in one `project list`; `project show` with active
tasks (excluding an archived one and an unrelated-repository one) and an
unknown-project command-level failure path.
Ports hook_status/hook_summary/pending_summary/observe_plugin
(lib/sumctl.py): read_health's symlink/schema checks, pending_summary's
open-obligations aggregation across all tasks (reusing go/internal/returns),
hook_manifest's TOML generation (for the expected_manifest_current hash
comparison), and observe_plugin's bounded live registry probe via the
existing herdrclient/toolpath packages when a Herdr session context is
available. Faithfully reproduces a subtle Python quirk: hook_status reads
health.json a second time, unguarded, after hook_summary already caught and
degraded on the first read — so a malformed health.json still fails the
whole command, not just one field. `hook enable`/`disable`/`event` stay on
the Python reference (they mutate or require a live session).

Differential-tested against the real Python bin/sumctl with Herdr env vars
blanked (both sides take the records-only ctx=None path, same as any
non-Herdr invocation): no health.json, a disabled health record with one
pending obligation (exercising pending_summary's count/age), an enabled
record with a plugin_id but no instance identity yet (the
expected_manifest_current error-to-None path), an enabled record with a
mismatched manifest hash, and a malformed-schema health.json exercising the
command-level failure path. The live Herdr registry sub-path (ctx available,
plugin actually registered) is implemented but not exercised by these
tests — verifying it needs a live Herdr session, out of scope for a
differential test in this environment.
Ports `show TASK_ID` (lib/sumctl.py): the full task record plus
versions_view, evidence_view, and returns_view — reusing the already-ported
versions.View and returns.View, and adding a new evidenceview package for
evidence_view/current_candidate. evidence_view is the closure-readiness
computation: scoped evidence records tagged with candidate currency, a
synthesized legacy report row when a task predates structured evidence,
worker/root verification run selection, review status, and the "missing"
prerequisite list (structured handoff, complete/matching PR identity,
passing root verification, standardized-contract worker run + review +
policy-review gates). current_candidate does one live `git rev-parse HEAD`
in the task's worktree, tolerating a missing/absent worktree or git failure
exactly like Python (falls back to no candidate rather than raising).

Differential-tested against the real Python bin/sumctl: a minimal task with
no worktree/evidence/PR/reviewer; a legacy report with no structured
evidence (exercises the synthesized report row); a real git worktree with a
failing root verification run (exercises the "latest root run ... was fail"
missing message); and a fully standardized contract with a real worktree,
handoff, worker run, root run, and an approving policy review — all current
against the live HEAD — reaching prerequisites_met: true end to end.
Ports the default `context TASK_ID` shape (no --section/--role/--since,
which context_view itself always resolves to the single "outline" section)
from lib/sumctl.py: cursor_of/cursor_counters/state_digest (a new
ordjson.MarshalSortedCompact primitive matches Python's
json.dumps(sort_keys=True) exactly, sorting object keys while leaving array
order alone), the outline's decisions/evidence/report/brief/returns_open/
attention_open/cleanup/notes/environment/graph/read fields, reusing
versions.View, evidenceview.View, returns.View, returns.OpenAttention, and
cleanup.Pending. Adds two new small packages: go/internal/notes
(notes_state, the timestamped-entry parser) and environment.Outline/Stamp
(environment_outline/environment_stamp, needed by the cursor and outline).
--section, --role, and --since stay on the Python reference for now — a
deliberate checkpoint given context_view's size (~200+ lines with 10
sections and role-scoped filtering); each is a natural follow-up commit.

Also fixes a latent bug in ordjson's compact/indent/sorted encoders: a nil
*ordjson.Object value (a typed nil, distinct from an untyped nil) crashed
the encoder via a nil-pointer Len() call instead of encoding as JSON null.
Caught by this port's first case building a container conditionally from a
possibly-absent nested object.

Differential-tested against the real Python bin/sumctl (read_at
live-timestamped, normalized before comparing): a minimal task with no
worktree/evidence/notes/environment record; a real git worktree with a
current handoff and a passing root verification (exercises evidence_view's
closure/verification fields end to end); and a task with open/applied
decisions, a notes.md with one entry, and a stale environment record with
one endpoint and one running service (exercises environment_outline's
per-state counts).
Extends the native `context TASK_ID` path with `--section NAME` (repeatable,
deduplicated in first-occurrence order, matching `list(dict.fromkeys(...))`):
decisions, returns, handoff, evidence, and brief. Ports `paged` (the stable
append-only-list pagination helper shared by several sections),
`section_decisions`, the inline `returns` section body, `latest_handoff`/
`handoff_view`/`section_handoff`, `section_evidence`, and `section_brief`.
`--role`, `--since`, and the paging/bounding flags (--after/--limit/
--max-chars/--revision/--kind) are not yet supported — any of those, or an
unrecognized section name, falls back to the Python reference; the paging
sections use the argparse defaults (after=0, limit=20, max-chars=4000) until
those flags are wired up. `execution`, `environment`, and `update` sections
still fall back too — they need new subsystems (graph_view, skill_references,
installation_root/default_runtime) saved for a follow-up commit.

Differential-tested against the real Python bin/sumctl: `decisions` +
`returns` together on a task with one open and one applied question;
`handoff` + `evidence` together against a real git worktree with a handoff
(files, checks, next_action), a passing coordinator verification, and an
approving review (three different evidence kinds in one task); `brief` on a
legacy task with no versions.json; plus two fallback cases (`--role`, an
unrecognized `--section` name) confirming the exact-shape-only native path
still defers correctly.
The prior commit's contextview.View already wired the "notes" section
(reusing notes.State), but no differential test exercised it. Add two
scenarios: a present notes.md with two entries, and no notes.md at all.
Ports section_execution/graph_view/graph_summary/read_graph (new
go/internal/graphview package, distinct from the existing go/internal/graph
which only handles `graph config`'s MCP snippet): the task-scoped
graph.json sidecar, its bounded summary (state, attempts, failures, index
counts, truncated error), and the `execution` section's launch/admission/
endpoints reshaping.

Fixes a real correctness bug found while adding this: `validContextSections`
in root.go listed all 10 CONTEXT_SECTIONS names from the first --section
commit, but contextview.View's switch only handled a subset. Requesting an
unimplemented section (e.g. `--section update`) was accepted as a valid
native shape and silently omitted that section's key from the output,
instead of falling back to the Python reference — confirmed by hand before
fixing. `validContextSections` now lists only what View's switch actually
implements, with a comment warning not to add a name there before wiring
its case. Added a regression test asserting `environment` and `update`
still route to the Python reference exactly as before.

Differential-tested against the real Python bin/sumctl: an execution
section with no graph record, launch, parent, or reviewer; and one with a
`ready` graph record (index counts, one successful attempt) plus a full
launch, admission, parent, and reviewer.
Completes all 10 CONTEXT_SECTIONS. Ports section_environment (reusing
environment.View for the `dev` field, plus new skill_references/
return_commands — active-revision command lookup with a task-callback
command fallback, installed SKILL.md file references for all three roles
since --role isn't wired up yet) and section_update (recorded vs. active
runtime, brief active/requested, refresh_state, and default_runtime —
reusing release.VerifyRelease and release.InstallationRoot — tolerating
a non-installation state home exactly like Python's try/except, producing
an {"unavailable": ...} shape instead of failing the command).

validContextSections and their contextview.View switch cases land in this
same commit, per the lesson from the execution-section commit. The
now-obsolete `environment`/`update`-still-fall-back regression test is
replaced with real differential tests for both sections, since they're no
longer unimplemented.

Differential-tested against the real Python bin/sumctl: `environment` with
no environment.json and with a present record; `update` on a legacy task
outside any installation (exercises the try/except installation_root
"unavailable" branch, the realistic case for a lab/dev --home).
Ports context_view's --role worker|reviewer|coordinator handling: when
given with no --section, ROLE_SECTIONS[role] replaces the default ["outline"]
section list; section_decisions filters by role (worker→answered only,
coordinator→open only); the top-level contract/authority envelope
(ROLE_CONTRACT) is added; and for reviewer/coordinator, section_environment
gains an artifacts field via a new artifact_references/artifact_scope port
(classifies worker-supplied handoff artifact strings as checkout/
outside-checkout/unscoped, purely by string inspection, no filesystem
calls). section_environment's skill_references now correctly narrows to
just the given role's skills (roles=[role]) instead of always all three,
matching Python's `roles = [args.role] if args.role else []`.

parseContextArgs (renamed from parseContextSectionArgs) now recognizes
--role alongside --section, validated against CONTEXT_ROLES; an
unrecognized role or --since still falls back to the Python reference.
Updated the stale fallback test that asserted --role fell back (it doesn't
anymore) to cover --since and an unrecognized role instead.

Differential-tested against the real Python bin/sumctl: worker role with
default sections (decisions filtered to answered), coordinator role with
default sections (decisions filtered to open), and reviewer role with an
explicit --section environment exercising artifact_references across all
three scope outcomes (a checkout-relative path, a `../`-escaping path, an
absolute path, and a `~`-prefixed path) in one handoff record.
Ports context_view's cursor-based diffing: parse_cursor (the CURSOR regex,
raising the same error text for a malformed token) and changes_since (new
questions, status-changed questions matching the cursor's timestamp,
new evidence with report/publication change flags, refresh events, new
attention, notes-entries delta, and the state-digest-based
changed/unchanged verdict). When --since is given and nothing changed and
neither --section nor --role was also given, the command returns early
with just the envelope, "changes", and a "note" — no section is rendered,
exactly matching context_view's control flow (sections computed once near
the top, sections stays empty when --since is given alone with no default
section/role, and the early return never touches it). A malformed cursor
propagates as a command-level failure with Python's exact error text,
matching the established error-parity pattern from release/project.

Renamed the stale fallback subtest that used to assert --since fell back
(it doesn't anymore) to test --max-chars instead, since that flag is still
unimplemented.

Differential-tested against the real Python bin/sumctl: an unchanged cursor
(fetched from a real prior read) producing the early-return note-only
response; a changed cursor (a question added between reads) with no
section requested, rendering just the changes block; the same changed
cursor with an explicit --section, rendering both changes and the section;
and a malformed cursor string exercising the command-level failure path.
Ports --after/--limit/--max-chars/--revision/--kind for `context TASK_ID`,
the last unported flags on the read-only surface. Bundles the flags into a
contextview.Options struct rather than growing View's parameter list further.

Fixes two bugs found by differential testing against the Python reference
while building this out:
- section_evidence's `kinds` breakdown must count all records before
  --kind filtering, not just the filtered subset.
- exports versions.revisionView -> RevisionView so section_brief's
  --revision lookup can reuse it instead of duplicating revision lookup.

This completes 100% native porting of sumctl's read-only command surface.
@douglasjarquin
douglasjarquin merged commit a3ce456 into main Sep 11, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant