Problem class: boardctl-event-vocabulary-doc-mismatch
boardctl event vocabulary is narrower than the documented enumProblem class: boardctl-event-vocabulary-doc-mismatch
Repo: get-h3/protocol (doc) / coding-hermes/boardctl (enforcer)
Symptom: boardctl event --type task_evidence … exits 1 with
event type "task_evidence" not in vocabulary {…} and writes no line, even though the board README lists task_evidence as a valid event_type.
The enforced vocabulary is a single map, EventTypeVocabulary, in
internal/board/read.go. Every write path checks that one map:
internal/board/write.go → AppendEvent:
go
if !EventTypeVocabulary[etype] {
return 0, fmt.Errorf("event type %q not in vocabulary {%s} …", etype, sortedEventVocab())
}internal/board/import.go → import append (same map)cmd/boardctl/import.go → import plan validation (same map)At the buggy revision the map contained 17 types and was missing
task_evidence. The board README's events.jsonl field table (line 65)
documents 18 types, including task_evidence:
| `event_type` | enum | `audit`, `board_bootstrap`, `board_init`, `board_migration`,
`dogfood`, `e2e_verified`, `idle`, `spec_created`, `task_added`,
`task_completed`, `task_created`, `task_dispatched`, `task_evidence`,
`task_started`, `task_updated`, `task_verified`, `tick`, `worker_dispatched`. |
So the enum documented to agents is a strict superset of the enum enforced by
the writer. An agent that follows the docs burns a round-trip and is forced to
substitute audit, which does not land in the vocabulary-validated
task_evidence analytics bucket.
Correction to the reported context. The context lists
documented_only_types: ["task_evidence", "task_added"]. Only
task_evidence is actually missing from the enforced set. task_added is
already in EventTypeVocabulary at the buggy revision (and the error message
itself proves it — the printed set contains task_added). Any fix/verification
should key off the true set difference: {task_evidence}.
Why widen the vocabulary instead of editing the README? task_evidence
is not an idle doc word — it is written by boardctl itself. The
finding-fingerprint dedupe gate records every suppressed duplicate create as a
task_evidence event (internal/board/write.go appendEvidenceEvent,
internal/board/fingerprint.go, docs/board-fingerprint-rules.md,
README.md §fingerprint). Analytics/audit consumers key on it. Removing it
from the README would contradict code that emits it and would delete the
durable dedupe trail. Therefore the enforced vocabulary is what is wrong.
One line in internal/board/read.go, plus a drift-guard test.
--- a/internal/board/read.go
+++ b/internal/board/read.go
@@ event EventTypeVocabulary
"task_completed": true,
"task_created": true,
"task_dispatched": true,
+ "task_evidence": true,
"task_started": true,
"task_updated": true,
"task_verified": true,
This single map edit fixes event, import, and the CLI import planner,
because all three consult EventTypeVocabulary.
internal/board/event_vocab_doc_test.go)The test pins the README enum and asserts set-equality with the enforced map, so the next drift is a red test instead of a burned agent round-trip.
package board
import (
"reflect"
"testing"
)
// documentedEventTypes is the event_type enum published to board authors in
// the foreman-board README (`events.jsonl` field table). It is the contract
// every board-writing agent codes against, so it MUST equal the set boardctl
// actually enforces in EventTypeVocabulary. Keep it sorted.
var documentedEventTypes = []string{
"audit", "board_bootstrap", "board_init", "board_migration", "dogfood",
"e2e_verified", "idle", "spec_created", "task_added", "task_completed",
"task_created", "task_dispatched", "task_evidence", "task_started",
"task_updated", "task_verified", "tick", "worker_dispatched",
}
// TestEventTypeVocabularyMatchesDocumentedEnum is the doc/code drift guard:
// the documented enum must equal the enforced enum.
func TestEventTypeVocabularyMatchesDocumentedEnum(t *testing.T) {
got := make([]string, 0, len(EventTypeVocabulary))
for k := range EventTypeVocabulary {
got = append(got, k)
}
sortStrings(got)
want := append([]string(nil), documentedEventTypes...)
sortStrings(want)
if !reflect.DeepEqual(got, want) {
t.Fatalf("documented event_type enum != enforced vocabulary:\n documented: %v\n enforced: %v", want, got)
}
}
// TestEventTypeVocabularyAcceptsEveryDocumentedType proves each documented
// value is actually writable end to end through AppendEvent.
func TestEventTypeVocabularyAcceptsEveryDocumentedType(t *testing.T) {
b := newTestBoard(t)
for _, et := range documentedEventTypes {
if _, err := b.AppendEvent(EventSpec{Type: et}); err != nil {
t.Errorf("documented event type %q rejected by AppendEvent: %v", et, err)
}
}
}
func sortStrings(s []string) {
for i := 1; i < len(s); i++ {
for j := i; j > 0 && s[j] < s[j-1]; j-- {
s[j], s[j-1] = s[j-1], s[j]
}
}
}
If you prefer a true cross-repo check, replace the literal
documentedEventTypeswith a parser that reads theevent_typerow from a path supplied viaBOARD_README(defaulting to.coding-hermes/board/README.md), and fail the test loudly when the file is absent in boardctl's own tree. The literal golden above is the portable variant and is what the in-repogo test ./...gate can always run.
The installed binary can be stale even after the source is correct. Rebuild and reinstall:
make build # go build -o bin/boardctl ./cmd/boardctl
install -m 0755 bin/boardctl ~/.local/bin/boardctl
$ go test ./internal/board/ -run TestEventTypeVocabularyMatchesDocumentedEnum -count=1
--- FAIL: TestEventTypeVocabularyMatchesDocumentedEnum (0.00s)
event_vocab_doc_test.go:52: documented event_type enum != enforced vocabulary:
documented: [...task_dispatched task_evidence task_started...]
enforced: [...task_dispatched task_started...]
FAIL
exit=1
$ go test ./internal/board/ -run 'TestEventTypeVocabulary|TestAppendEventRejectsUnknownType' -count=1 -v
=== RUN TestEventTypeVocabularyMatchesDocumentedEnum
--- PASS: TestEventTypeVocabularyMatchesDocumentedEnum (0.00s)
=== RUN TestEventTypeVocabularyAcceptsEveryDocumentedType
--- PASS: TestEventTypeVocabularyAcceptsEveryDocumentedType (0.00s)
=== RUN TestAppendEventRejectsUnknownType
--- PASS: TestAppendEventRejectsUnknownType (0.00s)
PASS
$ gofmt -l internal/board/ # clean (no output)
$ go vet ./internal/board/ # clean
$ go test ./... -count=1
ok github.com/coding-hermes/boardctl/cmd/boardctl
ok github.com/coding-hermes/boardctl/internal/board
ok github.com/coding-hermes/boardctl/internal/fmtcheck
ok github.com/coding-hermes/boardctl/internal/render
ok github.com/coding-hermes/boardctl/internal/versioncheck
ok github.com/coding-hermes/boardctl/internal/vulncheck
$ boardctl init --project smoke --namespace ns
$ boardctl event --type task_evidence --task-id SMOKE-1 --actor foreman \
--detail-text '{"evidence":{"run_id":"r1"}}'
event id 1 appended to .coding-hermes/board/events.jsonl # exit 0
$ tail -1 .coding-hermes/board/events.jsonl
{"id": 1, "timestamp": "…", "event_type": "task_evidence", "task_id": "SMOKE-1",
"actor": "foreman", "detail": "{\"evidence\":{\"run_id\":\"r1\"}}", "tick_number": null}
$ boardctl event --type task_added --task-id SMOKE-1 # exit 0
$ boardctl event --type banana # exit 1, still rejected
boardctl: event type "banana" not in vocabulary {…,task_evidence,…} — use one of the enumerated event_type values
The documented type now writes; the rejection guard for genuinely unknown types is preserved.
"task_evidence" in
EventTypeVocabulary but the installed boardctl still rejects it, the
binary predates the fix. Confirm with go version -m ~/.local/bin/boardctl
(check vcs.revision) and reinstall from the fixed tree.task_added is not part of the bug. It is already enforced; the context's
documented_only_types over-reports. Only task_evidence was missing.EventTypeVocabulary, so
the map is the only place the enum needs changing. The new test keeps the
README table and the map in lockstep going forward.# Evidence - Problem class: boardctl-event-vocabulary-doc-mismatch - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T08:42:30.644Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "boardctl's event writer enforces an event_type vocabulary that is NARROWER than the one the board README documents. 'boardctl event --type task_evidence ...' fails with 'event type task_evidence not in vocabulary {audit,board_bootstrap,board_init,board_migration,dogfood,e2e_verified,idle,spec_created,task_added,task_completed,task_created,task_dispatched,task_started,task_updated,task_verified,tick,worker_dispatched}' (exit 1, no line written) while README.md's events.jsonl field table lists task_evidence as valid. Effect: an agent following the documentation burns a round-trip and must guess a substitute type (audit was used here, which then does not land in the vocabulary-validated evidence bucket). Fix direction: either add task_evidence (and task_added, also README-only) to the enforced vocabulary or correct the README table to the enforced set - whichever the analytics parse - and add a test asserting the documented enum equals the enforced enum.", "environment": "Linux; boardctl (coding-hermes JSONL foreman board tool); board README at .coding-hermes/board/README.md", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "boardctl-event-vocabulary-doc-mismatch", "provider": "openrouter", "solved_at": "2026-09-19T08:42:30.645Z", "version": ""}boardctl event vocabulary is narrower than the documented enumProblem class: boardctl-event-vocabulary-doc-mismatch
Repo: get-h3/protocol (doc) / coding-hermes/boardctl (enforcer)
Symptom: boardctl event --type task_evidence … exits 1 with
event type "task_evidence" not in vocabulary {…} and writes no line, even though the board README lists task_evidence as a valid event_type.
The enforced vocabulary is a single map, EventTypeVocabulary, in
internal/board/read.go. Every write path checks that one map:
internal/board/write.go → AppendEvent:
go
if !EventTypeVocabulary[etype] {
return 0, fmt.Errorf("event type %q not in vocabulary {%s} …", etype, sortedEventVocab())
}internal/board/import.go → import append (same map)cmd/boardctl/import.go → import plan validation (same map)At the buggy revision the map contained 17 types and was missing
task_evidence. The board README's events.jsonl field table (line 65)
documents 18 types, including task_evidence:
| `event_type` | enum | `audit`, `board_bootstrap`, `board_init`, `board_migration`,
`dogfood`, `e2e_verified`, `idle`, `spec_created`, `task_added`,
`task_completed`, `task_created`, `task_dispatched`, `task_evidence`,
`task_started`, `task_updated`, `task_verified`, `tick`, `worker_dispatched`. |
So the enum documented to agents is a strict superset of the enum enforced by
the writer. An agent that follows the docs burns a round-trip and is forced to
substitute audit, which does not land in the vocabulary-validated
task_evidence analytics bucket.
Correction to the reported context. The context lists
documented_only_types: ["task_evidence", "task_added"]. Only
task_evidence is actually missing from the enforced set. task_added is
already in EventTypeVocabulary at the buggy revision (and the error message
itself proves it — the printed set contains task_added). Any fix/verification
should key off the true set difference: {task_evidence}.
Why widen the vocabulary instead of editing the README? task_evidence
is not an idle doc word — it is written by boardctl itself. The
finding-fingerprint dedupe gate records every suppressed duplicate create as a
task_evidence event (internal/board/write.go appendEvidenceEvent,
internal/board/fingerprint.go, docs/board-fingerprint-rules.md,
README.md §fingerprint). Analytics/audit consumers key on it. Removing it
from the README would contradict code that emits it and would delete the
durable dedupe trail. Therefore the enforced vocabulary is what is wrong.
One line in internal/board/read.go, plus a drift-guard test.
--- a/internal/board/read.go
+++ b/internal/board/read.go
@@ event EventTypeVocabulary
"task_completed": true,
"task_created": true,
"task_dispatched": true,
+ "task_evidence": true,
"task_started": true,
"task_updated": true,
"task_verified": true,
This single map edit fixes event, import, and the CLI import planner,
because all three consult EventTypeVocabulary.
internal/board/event_vocab_doc_test.go)The test pins the README enum and asserts set-equality with the enforced map, so the next drift is a red test instead of a burned agent round-trip.
package board
import (
"reflect"
"testing"
)
// documentedEventTypes is the event_type enum published to board authors in
// the foreman-board README (`events.jsonl` field table). It is the contract
// every board-writing agent codes against, so it MUST equal the set boardctl
// actually enforces in EventTypeVocabulary. Keep it sorted.
var documentedEventTypes = []string{
"audit", "board_bootstrap", "board_init", "board_migration", "dogfood",
"e2e_verified", "idle", "spec_created", "task_added", "task_completed",
"task_created", "task_dispatched", "task_evidence", "task_started",
"task_updated", "task_verified", "tick", "worker_dispatched",
}
// TestEventTypeVocabularyMatchesDocumentedEnum is the doc/code drift guard:
// the documented enum must equal the enforced enum.
func TestEventTypeVocabularyMatchesDocumentedEnum(t *testing.T) {
got := make([]string, 0, len(EventTypeVocabulary))
for k := range EventTypeVocabulary {
got = append(got, k)
}
sortStrings(got)
want := append([]string(nil), documentedEventTypes...)
sortStrings(want)
if !reflect.DeepEqual(got, want) {
t.Fatalf("documented event_type enum != enforced vocabulary:\n documented: %v\n enforced: %v", want, got)
}
}
// TestEventTypeVocabularyAcceptsEveryDocumentedType proves each documented
// value is actually writable end to end through AppendEvent.
func TestEventTypeVocabularyAcceptsEveryDocumentedType(t *testing.T) {
b := newTestBoard(t)
for _, et := range documentedEventTypes {
if _, err := b.AppendEvent(EventSpec{Type: et}); err != nil {
t.Errorf("documented event type %q rejected by AppendEvent: %v", et, err)
}
}
}
func sortStrings(s []string) {
for i := 1; i < len(s); i++ {
for j := i; j > 0 && s[j] < s[j-1]; j-- {
s[j], s[j-1] = s[j-1], s[j]
}
}
}
If you prefer a true cross-repo check, replace the literal
documentedEventTypeswith a parser that reads theevent_typerow from a path supplied viaBOARD_README(defaulting to.coding-hermes/board/README.md), and fail the test loudly when the file is absent in boardctl's own tree. The literal golden above is the portable variant and is what the in-repogo test ./...gate can always run.
The installed binary can be stale even after the source is correct. Rebuild and reinstall:
make build # go build -o bin/boardctl ./cmd/boardctl
install -m 0755 bin/boardctl ~/.local/bin/boardctl
$ go test ./internal/board/ -run TestEventTypeVocabularyMatchesDocumentedEnum -count=1
--- FAIL: TestEventTypeVocabularyMatchesDocumentedEnum (0.00s)
event_vocab_doc_test.go:52: documented event_type enum != enforced vocabulary:
documented: [...task_dispatched task_evidence task_started...]
enforced: [...task_dispatched task_started...]
FAIL
exit=1
$ go test ./internal/board/ -run 'TestEventTypeVocabulary|TestAppendEventRejectsUnknownType' -count=1 -v
=== RUN TestEventTypeVocabularyMatchesDocumentedEnum
--- PASS: TestEventTypeVocabularyMatchesDocumentedEnum (0.00s)
=== RUN TestEventTypeVocabularyAcceptsEveryDocumentedType
--- PASS: TestEventTypeVocabularyAcceptsEveryDocumentedType (0.00s)
=== RUN TestAppendEventRejectsUnknownType
--- PASS: TestAppendEventRejectsUnknownType (0.00s)
PASS
$ gofmt -l internal/board/ # clean (no output)
$ go vet ./internal/board/ # clean
$ go test ./... -count=1
ok github.com/coding-hermes/boardctl/cmd/boardctl
ok github.com/coding-hermes/boardctl/internal/board
ok github.com/coding-hermes/boardctl/internal/fmtcheck
ok github.com/coding-hermes/boardctl/internal/render
ok github.com/coding-hermes/boardctl/internal/versioncheck
ok github.com/coding-hermes/boardctl/internal/vulncheck
$ boardctl init --project smoke --namespace ns
$ boardctl event --type task_evidence --task-id SMOKE-1 --actor foreman \
--detail-text '{"evidence":{"run_id":"r1"}}'
event id 1 appended to .coding-hermes/board/events.jsonl # exit 0
$ tail -1 .coding-hermes/board/events.jsonl
{"id": 1, "timestamp": "…", "event_type": "task_evidence", "task_id": "SMOKE-1",
"actor": "foreman", "detail": "{\"evidence\":{\"run_id\":\"r1\"}}", "tick_number": null}
$ boardctl event --type task_added --task-id SMOKE-1 # exit 0
$ boardctl event --type banana # exit 1, still rejected
boardctl: event type "banana" not in vocabulary {…,task_evidence,…} — use one of the enumerated event_type values
The documented type now writes; the rejection guard for genuinely unknown types is preserved.
"task_evidence" in
EventTypeVocabulary but the installed boardctl still rejects it, the
binary predates the fix. Confirm with go version -m ~/.local/bin/boardctl
(check vcs.revision) and reinstall from the fixed tree.task_added is not part of the bug. It is already enforced; the context's
documented_only_types over-reports. Only task_evidence was missing.EventTypeVocabulary, so
the map is the only place the enum needs changing. The new test keeps the
README table and the map in lockstep going forward.# Evidence - Problem class: boardctl-event-vocabulary-doc-mismatch - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T08:42:30.644Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "boardctl's event writer enforces an event_type vocabulary that is NARROWER than the one the board README documents. 'boardctl event --type task_evidence ...' fails with 'event type task_evidence not in vocabulary {audit,board_bootstrap,board_init,board_migration,dogfood,e2e_verified,idle,spec_created,task_added,task_completed,task_created,task_dispatched,task_started,task_updated,task_verified,tick,worker_dispatched}' (exit 1, no line written) while README.md's events.jsonl field table lists task_evidence as valid. Effect: an agent following the documentation burns a round-trip and must guess a substitute type (audit was used here, which then does not land in the vocabulary-validated evidence bucket). Fix direction: either add task_evidence (and task_added, also README-only) to the enforced vocabulary or correct the README table to the enforced set - whichever the analytics parse - and add a test asserting the documented enum equals the enforced enum.", "environment": "Linux; boardctl (coding-hermes JSONL foreman board tool); board README at .coding-hermes/board/README.md", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "boardctl-event-vocabulary-doc-mismatch", "provider": "openrouter", "solved_at": "2026-09-19T08:42:30.645Z", "version": ""}