mesh/merge-gate pass: builds new: modules/mesh-knowledge, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compo…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
An agent learns what a decision says only by reading whole records. This module holds every decision record and research effort whole in a database in the store, with clauses that carry their own status and source, typed links read from both ends, and every version. It answers ask, get and holds within a stated bound, and takes a record through reserve, propose (refused on any of the 29 per-record rules), review by someone other than the author, and acceptance, on the operator's proven warrant when the record needs it. The 4 whole-set rules make the module unhealthy when they fail. The import reads hq at a commit through the records module, never overwrites, and changes nothing when run again. Prose decisions get drafted clauses that a second judgement must confirm before they are served.
464 lines
17 KiB
Go
464 lines
17 KiB
Go
package main
|
|
|
|
// Drafting and confirming clauses at the import (novox/hq ADR 0290 rule 8, to-be 55 §7). For a decision whose
|
|
// Decision section is prose, one judgement drafts its clauses: the section in, numbered statements out, each one
|
|
// sentence saying nothing the section does not. They are kept `drafted`, and never served as in force. A second
|
|
// judgement, a separate call that sees only the record and the drafts, answers per clause whether the record says
|
|
// exactly that: a clause it accepts becomes `confirmed`, one it rejects stays `drafted` with the reason. Which
|
|
// clause of an older record a supersedes-in-part changes is judged and confirmed the same way. When every record
|
|
// of a topic has been through both, the operator is told once, with the counts. `return` sends a record back to
|
|
// be drafted again.
|
|
//
|
|
// The judgements are the agent's on this node (claude-code.claude_code_judge, ADR 0278): no tools, no session,
|
|
// at most 25 seconds each, one at a time. The worker takes one step at a time in the background.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DefaultJudgeModel is the model the judgements ask unless the settings name another.
|
|
const DefaultJudgeModel = "sonnet"
|
|
|
|
// MostDraftTries is how many times a record's drafting is tried before it is left failed, said by its get.
|
|
const MostDraftTries = 3
|
|
|
|
var draftSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clauses": map[string]any{"type": "array", "items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"lead": map[string]any{"type": "string", "description": "a few words naming what is decided, ending with a full stop"},
|
|
"text": map[string]any{"type": "string", "description": "the one sentence of the statement, after the lead"},
|
|
},
|
|
"required": []string{"lead", "text"},
|
|
}},
|
|
},
|
|
"required": []string{"clauses"},
|
|
}
|
|
|
|
var confirmSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"verdicts": map[string]any{"type": "array", "items": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clause": map[string]any{"type": "integer"},
|
|
"exact": map[string]any{"type": "boolean", "description": "true when the record says exactly this, nothing more"},
|
|
"reason": map[string]any{"type": "string", "description": "when not exact: what the record says instead"},
|
|
},
|
|
"required": []string{"clause", "exact"},
|
|
}},
|
|
},
|
|
"required": []string{"verdicts"},
|
|
}
|
|
|
|
var whichSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"clauses": map[string]any{"type": "array", "items": map[string]any{"type": "integer"},
|
|
"description": "the numbers of the older record's clauses the newer record replaces part of"},
|
|
"reason": map[string]any{"type": "string"},
|
|
},
|
|
"required": []string{"clauses", "reason"},
|
|
}
|
|
|
|
var whichConfirmSchema = map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"exact": map[string]any{"type": "boolean"},
|
|
"reason": map[string]any{"type": "string"},
|
|
},
|
|
"required": []string{"exact", "reason"},
|
|
}
|
|
|
|
// DraftQuestion is the first judgement's question: the record's Decision section, nothing else of the mesh.
|
|
func DraftQuestion(r *Record, section, returned string) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Below is the Decision section of a decision record, %s: %q. It is written as prose. Write what it decides as numbered "+
|
|
"clauses: each one statement, one sentence, with a lead of a few words naming what is decided. A clause says nothing the section "+
|
|
"does not say: no inference, no context from elsewhere, nothing added. Every decision the section makes is in one clause; a "+
|
|
"sentence that only explains is in none. Keep the section's own words where you can.\n", r.Key(), r.Title)
|
|
if returned != "" {
|
|
fmt.Fprintf(&b, "\nAn earlier draft was sent back, with this reason: %s\n", returned)
|
|
}
|
|
b.WriteString("\nThe Decision section:\n\n" + section)
|
|
return b.String()
|
|
}
|
|
|
|
// ConfirmQuestion is the second judgement's question: the record and the drafts, nothing else.
|
|
func ConfirmQuestion(r *Record, section string, drafts []Clause) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Below are the Decision section of a decision record, %s: %q, and clauses drafted from it by someone else. For each "+
|
|
"clause, answer whether the section says exactly that: exact is true only when the clause says nothing the section does not, "+
|
|
"and leaves out nothing that changes its meaning. When it is not exact, say in reason what the section says instead.\n\n"+
|
|
"The Decision section:\n\n%s\n\nThe drafted clauses:\n\n", r.Key(), r.Title, section)
|
|
for _, c := range drafts {
|
|
fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (k *KB) judgeModel() string {
|
|
if m := strings.TrimSpace(k.settings().JudgeModel); m != "" {
|
|
return m
|
|
}
|
|
return DefaultJudgeModel
|
|
}
|
|
|
|
// DraftStep takes one step of the drafting and confirming: the first record waiting to be drafted or confirmed,
|
|
// then the first changes link whose clause is not yet judged. It answers what it did, or "" when nothing waits.
|
|
func (k *KB) DraftStep() (string, error) {
|
|
if k.Judge == nil || strings.EqualFold(strings.TrimSpace(k.settings().Drafting), "off") {
|
|
return "", nil
|
|
}
|
|
ctx := context.Background()
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, e := range all {
|
|
d := e.Current.Drafting
|
|
if d == nil || d.State == DraftDone || (d.State == DraftFailed && d.Tries >= MostDraftTries) {
|
|
continue
|
|
}
|
|
if d.State == DraftFailed && k.now().Sub(d.At) < time.Duration(d.Tries)*10*time.Minute {
|
|
continue
|
|
}
|
|
return k.draftOne(ctx, e.Current.Key())
|
|
}
|
|
for _, e := range all {
|
|
if !e.Current.Accepted() {
|
|
continue
|
|
}
|
|
for i, l := range e.Current.Links {
|
|
if l.Kind == LinkChanges && (l.Source == LinkPending || l.Source == SourceDrafted) && !l.Unresolved {
|
|
return k.judgeChange(ctx, e.Current.Key(), i)
|
|
}
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
// draftOne drafts or confirms one record's clauses, under the write lock only to read and to write.
|
|
func (k *KB) draftOne(ctx context.Context, key Key) (string, error) {
|
|
k.mu.Lock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
k.mu.Unlock()
|
|
if err != nil || e == nil {
|
|
return "", err
|
|
}
|
|
r := e.Current.Clone()
|
|
section, ok := decisionSection(r)
|
|
if !ok {
|
|
r.Drafting = &Drafting{State: DraftDone, Reason: "it has no Decision section", At: k.now()}
|
|
return k.saveDraft(ctx, e, r, EventDrafted, "no Decision section to draft from")
|
|
}
|
|
model := k.judgeModel()
|
|
fail := func(step string, err error) (string, error) {
|
|
// Tried again after a pause that grows with each try, at the step it failed at, up to MostDraftTries.
|
|
r.Drafting.State, r.Drafting.Reason, r.Drafting.At = DraftFailed, step+": "+err.Error(), k.now()
|
|
r.Drafting.Tries++
|
|
_, serr := k.saveDraft(ctx, e, r, EventDrafted, step+" failed: "+err.Error())
|
|
if serr != nil {
|
|
return "", serr
|
|
}
|
|
return "", fmt.Errorf("%s %s: %v", step, key, err)
|
|
}
|
|
if r.Drafting == nil {
|
|
r.Drafting = &Drafting{State: DraftWaiting}
|
|
}
|
|
// A failed record goes back to the step it failed at: confirming when it holds drafts, drafting otherwise.
|
|
confirming := r.Drafting.State == DraftDrafted || (r.Drafting.State == DraftFailed && hasDrafts(r))
|
|
if !confirming {
|
|
answer, by, err := k.Judge(DraftQuestion(r, section, r.Drafting.Returned), draftSchema, model)
|
|
if err != nil {
|
|
return fail("drafting", err)
|
|
}
|
|
var drafted struct {
|
|
Clauses []struct {
|
|
Lead string `json:"lead"`
|
|
Text string `json:"text"`
|
|
} `json:"clauses"`
|
|
}
|
|
if err := json.Unmarshal(answer, &drafted); err != nil || len(drafted.Clauses) == 0 {
|
|
return fail("drafting", errors.New("the judgement drafted no clause"))
|
|
}
|
|
r.Clauses = nil
|
|
for i, c := range drafted.Clauses {
|
|
lead := strings.TrimSpace(strings.Trim(c.Lead, "* "))
|
|
if !strings.HasSuffix(lead, ".") {
|
|
lead += "."
|
|
}
|
|
r.Clauses = append(r.Clauses, Clause{N: i + 1, Text: "**" + lead + "** " + strings.TrimSpace(c.Text),
|
|
Status: clauseStatusOf(r.Status), Source: SourceDrafted})
|
|
}
|
|
r.Drafting = &Drafting{State: DraftDrafted, Drafter: by, Returned: r.Drafting.Returned, Tries: r.Drafting.Tries, At: k.now()}
|
|
return k.saveDraft(ctx, e, r, EventDrafted, fmt.Sprintf("%d clauses drafted by %s", len(r.Clauses), by))
|
|
}
|
|
answer, by, err := k.Judge(ConfirmQuestion(r, section, r.Clauses), confirmSchema, model)
|
|
if err != nil {
|
|
return fail("confirming", err)
|
|
}
|
|
var verdicts struct {
|
|
Verdicts []struct {
|
|
Clause int `json:"clause"`
|
|
Exact bool `json:"exact"`
|
|
Reason string `json:"reason"`
|
|
} `json:"verdicts"`
|
|
}
|
|
if err := json.Unmarshal(answer, &verdicts); err != nil {
|
|
return fail("confirming", errors.New("the judgement answered no verdicts"))
|
|
}
|
|
confirmed := 0
|
|
for i := range r.Clauses {
|
|
r.Clauses[i].Unconfirmed = "the second judgement gave no verdict on it"
|
|
for _, v := range verdicts.Verdicts {
|
|
if v.Clause != r.Clauses[i].N {
|
|
continue
|
|
}
|
|
if v.Exact {
|
|
r.Clauses[i].Source, r.Clauses[i].Unconfirmed = SourceConfirmed, ""
|
|
confirmed++
|
|
} else {
|
|
r.Clauses[i].Unconfirmed = strings.TrimSpace(v.Reason)
|
|
if r.Clauses[i].Unconfirmed == "" {
|
|
r.Clauses[i].Unconfirmed = "not exact"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
r.Drafting.State, r.Drafting.Checker, r.Drafting.At, r.Drafting.Reason = DraftDone, by, k.now(), ""
|
|
said, err := k.saveDraft(ctx, e, r, EventConfirmed, fmt.Sprintf("%d of %d drafted clauses confirmed by %s", confirmed, len(r.Clauses), by))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
k.tellTopic(ctx, r.Topic)
|
|
return said, nil
|
|
}
|
|
|
|
func hasDrafts(r *Record) bool {
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceDrafted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (k *KB) saveDraft(ctx context.Context, e *Entry, r *Record, event, why string) (string, error) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
by := "the knowledge base"
|
|
if r.Drafting != nil && event == EventDrafted && r.Drafting.Drafter != "" {
|
|
by = r.Drafting.Drafter
|
|
}
|
|
if r.Drafting != nil && event == EventConfirmed && r.Drafting.Checker != "" {
|
|
by = r.Drafting.Checker
|
|
}
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
|
return "", fmt.Errorf("%s could not be kept (%v)", r.Key(), err)
|
|
}
|
|
k.emit(event, r, by, why, e.Versions+1)
|
|
return r.Key().String() + ": " + why, nil
|
|
}
|
|
|
|
// tellTopic tells the operator, once, when every prose record of a topic has been drafted and confirmed.
|
|
func (k *KB) tellTopic(ctx context.Context, topic string) {
|
|
if k.Notify == nil || topic == "" {
|
|
return
|
|
}
|
|
all, err := k.Store.All(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
records, confirmed, drafted := 0, 0, 0
|
|
for _, e := range all {
|
|
r := e.Current
|
|
if r.Topic != topic || r.Drafting == nil {
|
|
continue
|
|
}
|
|
if r.Drafting.State != DraftDone {
|
|
return
|
|
}
|
|
records++
|
|
for _, c := range r.Clauses {
|
|
if c.Source == SourceConfirmed {
|
|
confirmed++
|
|
} else if c.Source == SourceDrafted {
|
|
drafted++
|
|
}
|
|
}
|
|
}
|
|
if records == 0 {
|
|
return
|
|
}
|
|
first, err := k.Store.Told(ctx, topic)
|
|
if err != nil || !first {
|
|
return
|
|
}
|
|
summary := fmt.Sprintf("The clauses of %d prose decisions on %s are drafted and checked: %d confirmed, %d still drafted.", records, topic, confirmed, drafted)
|
|
explanation := "A second judgement confirmed each clause against its record; a clause still drafted is never served as in force. " +
|
|
"You may send any record back with the knowledge base's return, and its clauses are drafted again. Nothing needs you otherwise."
|
|
if err := k.Notify("knowledge.clauses."+strings.ReplaceAll(topic, " ", "-"), summary, "Clauses drafted for "+topic, explanation); err != nil {
|
|
k.logf("[mesh-knowledge] the operator could not be told about %s's clauses: %v", topic, err)
|
|
}
|
|
}
|
|
|
|
// judgeChange judges, then confirms, which clause of an older record a supersedes-in-part of hq changes.
|
|
func (k *KB) judgeChange(ctx context.Context, from Key, i int) (string, error) {
|
|
k.mu.Lock()
|
|
e, err := k.Store.Get(ctx, from)
|
|
var t *Entry
|
|
if err == nil && e != nil && i < len(e.Current.Links) {
|
|
if to, ok := e.Current.Links[i].To.Record(); ok {
|
|
t, err = k.Store.Get(ctx, to)
|
|
}
|
|
}
|
|
k.mu.Unlock()
|
|
if err != nil || e == nil || t == nil {
|
|
return "", err
|
|
}
|
|
r := e.Current.Clone()
|
|
l := &r.Links[i]
|
|
older := t.Current
|
|
served := 0
|
|
for _, c := range older.Clauses {
|
|
if c.Source != SourceDrafted {
|
|
served++
|
|
}
|
|
}
|
|
if served == 0 {
|
|
return "", nil // the older record's clauses are not confirmed yet: judged once they are
|
|
}
|
|
model := k.judgeModel()
|
|
newer, _ := decisionSection(r)
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "%s says it replaces part of an older record, %s. Below are the clauses of %s and the Decision section of %s.\n\n", r.Key(), older.Key(), older.Key(), r.Key())
|
|
for _, c := range older.Clauses {
|
|
fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text)
|
|
}
|
|
fmt.Fprintf(&b, "\nThe Decision section of %s:\n\n%s\n", r.Key(), newer)
|
|
if l.Source == LinkPending {
|
|
answer, by, err := k.Judge(b.String()+"\nWhich of the older record's clauses does the newer record replace part of? Name their numbers.", whichSchema, model)
|
|
if err != nil {
|
|
return "", fmt.Errorf("judging which clause %s changes: %v", r.Key(), err)
|
|
}
|
|
var which struct {
|
|
Clauses []int `json:"clauses"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if json.Unmarshal(answer, &which) != nil || len(which.Clauses) == 0 || older.Clause(which.Clauses[0]) == nil {
|
|
return "", fmt.Errorf("judging which clause %s changes: the judgement named no clause of %s", r.Key(), older.Key())
|
|
}
|
|
base := *l
|
|
l.To.Clause, l.Source = which.Clauses[0], SourceDrafted
|
|
for _, n := range which.Clauses[1:] {
|
|
if older.Clause(n) != nil {
|
|
extra := base
|
|
extra.To.Clause, extra.Source = n, SourceDrafted
|
|
r.Links = append(r.Links, extra)
|
|
}
|
|
}
|
|
return k.saveLinks(ctx, e, r, by, fmt.Sprintf("which clause of %s it changes drafted: §%s (%s)", older.Key(), joinInts(which.Clauses), which.Reason))
|
|
}
|
|
answer, by, err := k.Judge(b.String()+fmt.Sprintf("\nA first judgement said %s replaces part of clause %d of %s. Is that exact?", r.Key(), l.To.Clause, older.Key()), whichConfirmSchema, model)
|
|
if err != nil {
|
|
return "", fmt.Errorf("confirming which clause %s changes: %v", r.Key(), err)
|
|
}
|
|
var v struct {
|
|
Exact bool `json:"exact"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
if json.Unmarshal(answer, &v) != nil {
|
|
return "", fmt.Errorf("confirming which clause %s changes: no verdict", r.Key())
|
|
}
|
|
if !v.Exact {
|
|
clause := l.To.Clause
|
|
l.To.Clause, l.Source = 0, SourceDrafted+": not confirmed"
|
|
return k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d was not confirmed: %s", older.Key(), clause, v.Reason))
|
|
}
|
|
l.Source = SourceConfirmed
|
|
said, err := k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d confirmed by %s", older.Key(), l.To.Clause, by))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// The older clause is changed now, as an acceptance would have made it.
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
t, err = k.Store.Get(ctx, older.Key())
|
|
if err != nil || t == nil {
|
|
return said, err
|
|
}
|
|
c := t.Current.Clone()
|
|
if cl := c.Clause(l.To.Clause); cl != nil && cl.Status == ClauseInForce {
|
|
cl.Status = ClauseChanged
|
|
cl.ChangedBy = append(cl.ChangedBy, r.Key().String())
|
|
what := fmt.Sprintf("§%d changed by %s (as hq said, its clause judged and confirmed)", l.To.Clause, r.Key())
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: what, Record: c}, true, false, t.Revision); err != nil {
|
|
return said, err
|
|
}
|
|
k.emit(EventChanged, c, by, what, t.Versions+1)
|
|
}
|
|
return said, nil
|
|
}
|
|
|
|
func (k *KB) saveLinks(ctx context.Context, e *Entry, r *Record, by, why string) (string, error) {
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
|
return "", fmt.Errorf("%s could not be kept (%v)", r.Key(), err)
|
|
}
|
|
event := EventDrafted
|
|
if strings.Contains(why, "confirmed") {
|
|
event = EventConfirmed
|
|
}
|
|
k.emit(event, r, by, why, e.Versions+1)
|
|
return r.Key().String() + ": " + why, nil
|
|
}
|
|
|
|
func joinInts(ns []int) string {
|
|
s := make([]string, len(ns))
|
|
for i, n := range ns {
|
|
s[i] = strconv.Itoa(n)
|
|
}
|
|
return strings.Join(s, ", §")
|
|
}
|
|
|
|
// Return sends a record's drafted or confirmed clauses back to be drafted again, with the reason, which the next
|
|
// drafting reads.
|
|
func (k *KB) Return(caller string, key Key, reason string) (map[string]any, error) {
|
|
ctx := context.Background()
|
|
reason = strings.TrimSpace(reason)
|
|
if reason == "" {
|
|
return nil, errors.New("return needs reason: what is wrong with the clauses drafted")
|
|
}
|
|
k.mu.Lock()
|
|
defer k.mu.Unlock()
|
|
e, err := k.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e == nil {
|
|
return nil, fmt.Errorf("the knowledge base holds no %s", key)
|
|
}
|
|
r := e.Current.Clone()
|
|
if r.Drafting == nil {
|
|
return nil, fmt.Errorf("%s's clauses were written by its author, not drafted: a written clause is changed by a new record", key)
|
|
}
|
|
r.Clauses = nil
|
|
r.Drafting = &Drafting{State: DraftWaiting, Returned: reason, At: k.now()}
|
|
why := "returned to be drafted again: " + reason
|
|
if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil {
|
|
return nil, err
|
|
}
|
|
k.emit(EventReturned, r, caller, why, e.Versions+1)
|
|
return map[string]any{"record": key.String(), "says": fmt.Sprintf("%s's clauses are taken back and will be drafted again, with your reason; until then it serves none.", key)}, nil
|
|
}
|