From a61dea32ca5a388cd1a606bb36bed5fd4fd07858 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sun, 4 Oct 2026 18:31:52 +0200 Subject: [PATCH] fix(agentic-engineering): bind autonomy recommendations and retain known loss --- .claude-plugin/marketplace.json | 2 +- .github/plugin/marketplace.json | 2 +- .../.claude-plugin/plugin.json | 2 +- plugins/agentic-engineering/AUTONOMY.md | 5 +- plugins/agentic-engineering/CHANGELOG.md | 6 ++ plugins/agentic-engineering/plugin.json | 2 +- .../scripts/autonomy-contract-go/main.go | 27 ++++--- .../scripts/autonomy-contract-go/main_test.go | 77 +++++++++++++++++++ 8 files changed, 108 insertions(+), 15 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e56d2f68..ebd18ea5 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -27,7 +27,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "6.1.0", + "version": "6.1.1", "source": "./plugins/agentic-engineering" }, { diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index e56d2f68..ebd18ea5 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -27,7 +27,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "6.1.0", + "version": "6.1.1", "source": "./plugins/agentic-engineering" }, { diff --git a/plugins/agentic-engineering/.claude-plugin/plugin.json b/plugins/agentic-engineering/.claude-plugin/plugin.json index b3621982..93ddb04b 100644 --- a/plugins/agentic-engineering/.claude-plugin/plugin.json +++ b/plugins/agentic-engineering/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "6.1.0", + "version": "6.1.1", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/AUTONOMY.md b/plugins/agentic-engineering/AUTONOMY.md index 94bda982..f2adb609 100644 --- a/plugins/agentic-engineering/AUTONOMY.md +++ b/plugins/agentic-engineering/AUTONOMY.md @@ -82,7 +82,10 @@ and independently verify their hashes, scope and verdict before using this adapt does not parse the referenced calculator output or compute its measurements. Incomplete, unknown, future, foreign or unbound assertions yield a hold rather than replacement support. -A bound failure remains negative after expiry. Unknown observations never establish support or its loss. Loss of current evidence or runtime support can +A bound runtime failure remains negative even if its counterpart is missing or future-dated, and +a bound failure remains negative after expiry. Both runtime reports must be complete for replacement +support. Protected requests retain only the matching incumbent; foreign scope or an unsupported +current revision produces no recommendation. Unknown observations never establish support or its loss. Loss of current evidence or runtime support can recommend contraction when the candidate is current and scoped recovery is reported tested and already authorized. Pre-experiment and future observations cannot establish that loss. Missing recovery readiness yields a hold with the recovery owner; no fallback command is run. diff --git a/plugins/agentic-engineering/CHANGELOG.md b/plugins/agentic-engineering/CHANGELOG.md index abb0bac9..6ac97417 100644 --- a/plugins/agentic-engineering/CHANGELOG.md +++ b/plugins/agentic-engineering/CHANGELOG.md @@ -21,6 +21,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and new one before re-enabling unattended writes. Stopping early resumes writes with the retired FinOps schedule still armed, or with a schedule pointing at an entrypoint that no longer resolves. +## 6.1.1 — 2026-10-04 + +**Fixed** — autonomy assessments require distinct stage records, bind protected requests before +retaining the incumbent, and preserve independently bound runtime failures when their counterpart +report is incomplete. Recommendations remain advisory and never admit execution or run recovery. + ## 6.1.0 — 2026-10-04 **Added** — optional offline autonomy assessments join consumer classifications, exact capability diff --git a/plugins/agentic-engineering/plugin.json b/plugins/agentic-engineering/plugin.json index b3621982..93ddb04b 100644 --- a/plugins/agentic-engineering/plugin.json +++ b/plugins/agentic-engineering/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios — engineer, read-only surveyor, and meta-engineer agents; portfolio, product, spend, and improvement workflows; cross-tool instruction architecture and skill discovery; configured by the consumer AGENTS.md", - "version": "6.1.0", + "version": "6.1.1", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/scripts/autonomy-contract-go/main.go b/plugins/agentic-engineering/scripts/autonomy-contract-go/main.go index f8bc1fbb..707e65e5 100644 --- a/plugins/agentic-engineering/scripts/autonomy-contract-go/main.go +++ b/plugins/agentic-engineering/scripts/autonomy-contract-go/main.go @@ -316,10 +316,16 @@ func assess(in Input, now time.Time) Result { if c.Owner.Kind != "human" || !exact(c.Owner.ID) || !exact(c.Owner.Record) || !exact(c.ClassificationRecord) || !scopeValid(c.Scope) || !bindingsValid(c.Bindings) || !exact(c.RecoveryOwner) || c.Fallback != c.Bindings.Baseline || !unique(c.Protected) || !covers(c.Protected, domains) || !unique(c.RequiredEvidence) || !covers(c.RequiredEvidence, kinds) || !unique(c.RequiredOutcomes) || !unique(c.ProtectedOutcomes) || !covers(c.RequiredOutcomes, c.ProtectedOutcomes) || !unique(c.RequiredPaths) || (c.Classification != "protected" && c.Classification != "replaceable-default") { return finish("INVALID", "", "consumer classification, ownership, scope, required facts or fallback is incomplete") } + if !scopeValid(in.Request.Scope) || !scopeEqual(c.Scope, in.Request.Scope) || in.Request.Bindings != c.Bindings || (in.Request.Current != c.Bindings.Baseline && in.Request.Current != c.Bindings.Candidate) { + return finish("HOLD", "", "request scope or immutable provenance does not match") + } if c.Classification == "protected" { - return finish("RETAIN_DEFAULT", in.Request.Current, "protected method cannot be replaced by this assessment") + if in.Request.Current != c.Bindings.Baseline { + return finish("HOLD", "", "protected request does not name the incumbent") + } + return finish("RETAIN_DEFAULT", c.Bindings.Baseline, "protected method cannot be replaced by this assessment") } - if !scopeValid(in.Request.Scope) || !scopeEqual(c.Scope, in.Request.Scope) || in.Request.Bindings != c.Bindings || !scopeValid(p.Scope) || !scopeEqual(c.Scope, p.Scope) || p.Bindings != c.Bindings || (in.Request.Current != c.Bindings.Baseline && in.Request.Current != c.Bindings.Candidate) { + if !scopeValid(p.Scope) || !scopeEqual(c.Scope, p.Scope) || p.Bindings != c.Bindings { return finish("HOLD", "", "capability, scope or immutable provenance does not match") } // Only observations bound to the declared experiment can establish support or its loss. @@ -335,11 +341,13 @@ func assess(in Input, now time.Time) Result { expired := assessmentBound && a.Result != "unknown" && !ae.After(now) complete := assessmentBound && a.Result == "pass" && ae.After(now) ids, observedKinds := map[string]bool{}, []string{} + records := map[string]bool{} for _, e := range p.Evidence { - if !reportValid(e) || ids[e.ID] || !contains(c.RequiredEvidence, e.Kind) || e.Result == "intercepted" || len(e.Paths) != 0 { + if !reportValid(e) || ids[e.ID] || records[e.Record] || !contains(c.RequiredEvidence, e.Kind) || e.Result == "intercepted" || len(e.Paths) != 0 { return finish("INVALID", "", "ambiguous or invalid proof evidence") } ids[e.ID] = true + records[e.Record] = true observedKinds = append(observedKinds, e.Kind) o, _ := stamp(e.Observed) ex, _ := stamp(e.Expires) @@ -375,16 +383,15 @@ func assess(in Input, now time.Time) Result { complete = false } } - runtimeBound := planBound && scopeValid(r.Scope) && scopeEqual(c.Scope, r.Scope) && r.Revision == c.Bindings.Runtime && unique(r.Paths) && covers(r.Paths, c.RequiredPaths) && reportValid(r.Positive) && reportValid(r.Negative) && r.Positive.ID != r.Negative.ID && r.Positive.Record != r.Negative.Record && r.Positive.Kind == "runtime" && r.Negative.Kind == "runtime" && unique(r.Positive.Paths) && unique(r.Negative.Paths) && covers(r.Positive.Paths, c.RequiredPaths) && covers(r.Negative.Paths, c.RequiredPaths) - for _, e := range []Report{r.Positive, r.Negative} { + runtimeScopeBound := planBound && scopeValid(r.Scope) && scopeEqual(c.Scope, r.Scope) && r.Revision == c.Bindings.Runtime && unique(r.Paths) && covers(r.Paths, c.RequiredPaths) + reportBound := func(e Report) bool { o, ok := stamp(e.Observed) - if !ok || o.After(now) || o.Before(started) { - runtimeBound = false - } + return runtimeScopeBound && reportValid(e) && e.Kind == "runtime" && unique(e.Paths) && covers(e.Paths, c.RequiredPaths) && ok && !o.After(now) && !o.Before(started) } + runtimeBound := reportBound(r.Positive) && reportBound(r.Negative) && r.Positive.ID != r.Negative.ID && r.Positive.Record != r.Negative.Record runtimeReady := runtimeBound && r.Positive.Result == "pass" && r.Negative.Result == "intercepted" && fresh(r.Positive.Observed, r.Positive.Expires, now) && fresh(r.Negative.Observed, r.Negative.Expires, now) - if runtimeBound { - for _, e := range []Report{r.Positive, r.Negative} { + for _, e := range []Report{r.Positive, r.Negative} { + if reportBound(e) { ex, _ := stamp(e.Expires) if e.Result == "fail" { failed = true diff --git a/plugins/agentic-engineering/scripts/autonomy-contract-go/main_test.go b/plugins/agentic-engineering/scripts/autonomy-contract-go/main_test.go index 46b98fa2..04c53cef 100644 --- a/plugins/agentic-engineering/scripts/autonomy-contract-go/main_test.go +++ b/plugins/agentic-engineering/scripts/autonomy-contract-go/main_test.go @@ -223,6 +223,83 @@ func TestAssess(t *testing.T) { } } +func TestBoundRecommendations(t *testing.T) { + now := time.Date(2026, 10, 4, 0, 0, 0, 0, time.UTC) + baseline, candidate := strings.Repeat("4", 64), strings.Repeat("5", 64) + cases := []struct { + name, status, revision string + change func(map[string]any) + }{ + {"distinct stage records", "RECOMMEND_CANDIDATE", candidate, func(m map[string]any) {}}, + {"reused stage record", "INVALID", "", func(m map[string]any) { + for _, e := range nested(m, "observation", "proof")["evidence"].([]any) { + e.(map[string]any)["record"] = "artifact://one-report" + } + }}, + {"protected incumbent", "RETAIN_DEFAULT", baseline, func(m map[string]any) { nested(m, "contract")["candidateClassification"] = "protected" }}, + {"protected foreign scope", "HOLD", "", func(m map[string]any) { + nested(m, "contract")["candidateClassification"] = "protected" + nested(m, "request", "scope")["repository"] = "devantler-tech/foreign" + }}, + {"protected unknown current", "HOLD", "", func(m map[string]any) { + nested(m, "contract")["candidateClassification"] = "protected" + nested(m, "request")["currentRevision"] = strings.Repeat("9", 64) + }}, + {"protected candidate is not incumbent", "HOLD", "", func(m map[string]any) { + nested(m, "contract")["candidateClassification"] = "protected" + nested(m, "request")["currentRevision"] = candidate + }}, + {"failure without counterpart", "RECOMMEND_CONTRACTION", baseline, func(m map[string]any) { + nested(m, "request")["currentRevision"] = candidate + nested(m, "observation", "runtime", "positive")["result"] = "fail" + delete(nested(m, "observation", "runtime"), "interceptedNegative") + }}, + {"failure with future counterpart", "RECOMMEND_CONTRACTION", baseline, func(m map[string]any) { + nested(m, "request")["currentRevision"] = candidate + nested(m, "observation", "runtime", "positive")["result"] = "fail" + nested(m, "observation", "runtime", "interceptedNegative")["observedAt"] = "2026-10-05T00:00:00Z" + nested(m, "observation", "runtime", "interceptedNegative")["expiresAt"] = "2026-10-06T00:00:00Z" + }}, + {"negative failure without positive", "RECOMMEND_CONTRACTION", baseline, func(m map[string]any) { + nested(m, "request")["currentRevision"] = candidate + nested(m, "observation", "runtime", "interceptedNegative")["result"] = "fail" + delete(nested(m, "observation", "runtime"), "positive") + }}, + {"foreign failure", "HOLD", "", func(m map[string]any) { + nested(m, "request")["currentRevision"] = candidate + nested(m, "observation", "runtime", "positive")["result"] = "fail" + nested(m, "observation", "runtime", "scope")["repository"] = "devantler-tech/foreign" + delete(nested(m, "observation", "runtime"), "interceptedNegative") + }}, + {"future failure", "HOLD", "", func(m map[string]any) { + nested(m, "request")["currentRevision"] = candidate + p := nested(m, "observation", "runtime", "positive") + p["result"] = "fail" + p["observedAt"] = "2026-10-05T00:00:00Z" + p["expiresAt"] = "2026-10-06T00:00:00Z" + delete(nested(m, "observation", "runtime"), "interceptedNegative") + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + m := inputFixture(t) + tc.change(m) + raw, _ := json.Marshal(m) + in, err := decode(strings.NewReader(string(raw))) + if err != nil { + t.Fatal(err) + } + got := assess(in, now) + if got.Status != tc.status || got.RecommendedRevision != tc.revision { + t.Fatalf("got %+v, want %s/%s", got, tc.status, tc.revision) + } + if got.ExecutionAdmitted || got.MutationPerformed || got.ReportedEvidenceAuthenticated { + t.Fatal("assessment became authority") + } + }) + } +} + func TestDecode(t *testing.T) { for _, raw := range []string{fixture + fixture, `{"schemaVersion":1,"schemaVersion":1}`, `{"schemaVersion":1,"schem\u0061Version":1}`, `{"schemaVersion":1,"executionAdmitted":true}`, `null`, `[]`, "{", strings.Repeat(" ", 1<<20) + fixture, string([]byte{'{', '"', 'x', '"', ':', '"', 0xff, '"', '}'})} { if _, err := decode(strings.NewReader(raw)); err == nil {