diff --git a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx index 1fadbd9fed..dc8a5f4327 100644 --- a/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx +++ b/apps/gittensory-ui/src/components/site/app-panels/maintainer-settings.tsx @@ -47,17 +47,11 @@ type MaintainerSettings = { agentDryRun: boolean; }; -type AutonomyLevel = "observe" | "suggest" | "propose" | "auto_with_approval" | "auto"; +type AutonomyLevel = "observe" | "auto_with_approval" | "auto"; type AgentActionClass = "review" | "request_changes" | "approve" | "merge" | "close" | "label"; type AutoMergeMethod = "merge" | "squash" | "rebase"; -const AUTONOMY_LEVELS: AutonomyLevel[] = [ - "observe", - "suggest", - "propose", - "auto_with_approval", - "auto", -]; +const AUTONOMY_LEVELS: AutonomyLevel[] = ["observe", "auto_with_approval", "auto"]; const AGENT_ACTION_CLASSES: AgentActionClass[] = [ "review", "request_changes", @@ -492,8 +486,6 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p

Auto-maintain (agent layer)

Per-action autonomy: observe (watch only) →{" "} - suggest →{" "} - propose →{" "} auto_with_approval →{" "} auto. Deny-by-default — anything left at{" "} observe never acts. diff --git a/packages/gittensory-engine/src/settings/autonomy.ts b/packages/gittensory-engine/src/settings/autonomy.ts index b88f97bd9c..2edaab2c62 100644 --- a/packages/gittensory-engine/src/settings/autonomy.ts +++ b/packages/gittensory-engine/src/settings/autonomy.ts @@ -2,7 +2,9 @@ import type { AgentActionClass, AutoMaintainPolicy, AutoMergeMethod, AutonomyLev // The graduated autonomy dial (#773), ordered least → most autonomous. Every later agent-layer phase reads // this BEFORE acting. `observe` is the deny-by-default floor — gittensory watches but never takes an action. -export const AUTONOMY_LEVELS = ["observe", "suggest", "propose", "auto_with_approval", "auto"] as const; +// (#4620: `suggest`/`propose` removed -- both were 100% behaviorally identical to `observe`, see +// AutonomyLevel's own doc comment.) +export const AUTONOMY_LEVELS = ["observe", "auto_with_approval", "auto"] as const; // The write-action classes the maintainer auto-maintain layer (#778) can take on a PR. `review_state_label` // (#label-scoping) is a separate class from `label`: it gates the planner's own disposition-communication diff --git a/packages/gittensory-engine/src/types/manifest-deps-types.ts b/packages/gittensory-engine/src/types/manifest-deps-types.ts index b180147d66..bca40f6e82 100644 --- a/packages/gittensory-engine/src/types/manifest-deps-types.ts +++ b/packages/gittensory-engine/src/types/manifest-deps-types.ts @@ -124,7 +124,8 @@ export type ContributorBlacklistEntry = { addedAt?: string | undefined; }; -export type AutonomyLevel = "observe" | "suggest" | "propose" | "auto_with_approval" | "auto"; +// (#4620: "suggest"/"propose" removed -- both were 100% behaviorally identical to "observe".) +export type AutonomyLevel = "observe" | "auto_with_approval" | "auto"; export type AgentActionClass = "review" | "request_changes" | "approve" | "merge" | "close" | "label" | "review_state_label" | "update_branch" | "assign"; diff --git a/src/api/routes.ts b/src/api/routes.ts index 28afde4c4b..76eb6a6995 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -761,7 +761,7 @@ const maintainerSettingsSchema = z }), // Agent-layer config (#773/#774). The DB layer normalizes both (autonomy: deny-by-default; autoMaintain: // defaults filled), so a loose record/object here is safe — invalid entries are dropped on persist. - autonomy: z.record(z.string().trim().min(1).max(32), z.enum(["observe", "suggest", "propose", "auto_with_approval", "auto"])), + autonomy: z.record(z.string().trim().min(1).max(32), z.enum(["observe", "auto_with_approval", "auto"])), autoMaintain: z.object({ requireApprovals: z.number().int().min(0).max(10).optional(), mergeMethod: z.enum(["merge", "squash", "rebase"]).optional() }), }) .partial(); diff --git a/src/settings/autonomy.ts b/src/settings/autonomy.ts index 594cf62172..881ea1e256 100644 --- a/src/settings/autonomy.ts +++ b/src/settings/autonomy.ts @@ -2,7 +2,9 @@ import type { AgentActionClass, AutoMaintainPolicy, AutoMergeMethod, AutonomyLev // The graduated autonomy dial (#773), ordered least → most autonomous. Every later agent-layer phase reads // this BEFORE acting. `observe` is the deny-by-default floor — gittensory watches but never takes an action. -export const AUTONOMY_LEVELS = ["observe", "suggest", "propose", "auto_with_approval", "auto"] as const; +// (#4620: `suggest`/`propose` removed -- both were 100% behaviorally identical to `observe`, see +// AutonomyLevel's own doc comment.) +export const AUTONOMY_LEVELS = ["observe", "auto_with_approval", "auto"] as const; // The write-action classes the maintainer auto-maintain layer (#778) can take on a PR. `review_state_label` // (#label-scoping) is a separate class from `label`: it gates the planner's own disposition-communication diff --git a/src/types.ts b/src/types.ts index 215caa9d05..78a04d5c38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -623,10 +623,9 @@ export type ReviewCheckMode = "required" | "visible" | "disabled"; /** Auto-project/milestone matching (#3183): detects when a PR is likely part of an open GitHub Milestone even * with no closing-keyword issue link, and posts a bot-comment suggestion. `"off"` (default) runs no matching - * at all; `"suggest"` matches and posts a single advisory comment, never mutating the PR; `"auto"` is accepted - * by config today but behaves identically to `"suggest"` until #3185 wires real milestone attachment -- no - * attach/auto-apply code exists yet, so treating it as inert-but-silent would be a worse failure mode than - * degrading to the safe, visible suggest behavior. */ + * at all; `"suggest"` matches and posts a single advisory comment, never mutating the PR; `"auto"` (#3185, + * shipped) actually calls `attachToMilestone`/`attachToProject` for a high-confidence match instead of only + * commenting -- see `maybeSuggestMilestoneMatchForPr` in `integrations/project-tracker-adapter.ts`. */ export type ProjectMilestoneMatchMode = "off" | "suggest" | "auto"; /** Which backend {@link ProjectMilestoneMatchMode} matches against (#3186). `"github"` (default) uses the @@ -1285,9 +1284,12 @@ export type ContributorBlacklistEntry = { }; /** Agent-layer graduated autonomy (#773), least → most autonomous. `observe` is the deny-by-default floor: - * gittensory watches but never acts. `suggest`/`propose` surface guidance/concrete proposals without - * executing; `auto_with_approval` executes behind a human approval gate (#779); `auto` executes directly. */ -export type AutonomyLevel = "observe" | "suggest" | "propose" | "auto_with_approval" | "auto"; + * gittensory watches but never acts. `auto_with_approval` executes behind a human approval gate (#779); + * `auto` executes directly. (#4620: `suggest`/`propose` were removed here -- the doc comment promised + * distinct "surface guidance/concrete proposals without executing" behavior, but every read site + * (`isActingAutonomyLevel`/`autonomyRequiresApproval`) only ever distinguished acting from non-acting, so + * both were 100% behaviorally identical to `observe` from day one. No stored config used either value.) */ +export type AutonomyLevel = "observe" | "auto_with_approval" | "auto"; /** The write-action classes the maintainer auto-maintain layer (#778) can take on a PR. `label` gates the * anti-abuse enforcement labels tied 1:1 to a `close` in the same disposition (blacklist/contributor-cap/ diff --git a/test/unit/agent-actions.test.ts b/test/unit/agent-actions.test.ts index 072fb56387..8054bec2ff 100644 --- a/test/unit/agent-actions.test.ts +++ b/test/unit/agent-actions.test.ts @@ -42,7 +42,7 @@ describe("planAgentMaintenanceActions (#778)", () => { }); it("plans nothing when every class is at a non-acting level", () => { - const plan = planAgentMaintenanceActions(input({ conclusion: "failure", autonomy: { review_state_label: "suggest", request_changes: "propose", close: "observe" }, blockerTitles: ["x"] })); + const plan = planAgentMaintenanceActions(input({ conclusion: "failure", autonomy: { review_state_label: "observe", request_changes: "observe", close: "observe" }, blockerTitles: ["x"] })); expect(plan).toEqual([]); }); diff --git a/test/unit/agent-execution.test.ts b/test/unit/agent-execution.test.ts index f98136483c..99c13092c1 100644 --- a/test/unit/agent-execution.test.ts +++ b/test/unit/agent-execution.test.ts @@ -146,7 +146,7 @@ describe("agent write-permission readiness (#775)", () => { expect(agentRequiresPrWrite({ request_changes: "auto_with_approval" })).toBe(true); expect(agentRequiresPrWrite({ close: "auto" })).toBe(true); // non-acting levels never demand write - expect(agentRequiresPrWrite({ merge: "propose", review: "suggest" })).toBe(false); + expect(agentRequiresPrWrite({ merge: "observe", review: "observe" })).toBe(false); expect(agentRequiresPrWrite({ merge: "observe" })).toBe(false); expect(agentRequiresPrWrite({})).toBe(false); expect(agentRequiresPrWrite(null)).toBe(false); diff --git a/test/unit/autonomy-engine.test.ts b/test/unit/autonomy-engine.test.ts index 25729a4119..2db630ce01 100644 --- a/test/unit/autonomy-engine.test.ts +++ b/test/unit/autonomy-engine.test.ts @@ -54,8 +54,6 @@ describe("autonomy level predicates", () => { it("isActingAutonomyLevel is true only for auto / auto_with_approval", () => { expect(isActingAutonomyLevel("auto")).toBe(true); expect(isActingAutonomyLevel("auto_with_approval")).toBe(true); - expect(isActingAutonomyLevel("propose")).toBe(false); - expect(isActingAutonomyLevel("suggest")).toBe(false); expect(isActingAutonomyLevel("observe")).toBe(false); }); @@ -68,13 +66,13 @@ describe("autonomy level predicates", () => { it("the level ladder is ordered observe → … → auto with observe at the floor", () => { expect(AUTONOMY_LEVELS[0]).toBe("observe"); expect(AUTONOMY_LEVELS[AUTONOMY_LEVELS.length - 1]).toBe("auto"); - expect(AUTONOMY_LEVELS).toEqual(["observe", "suggest", "propose", "auto_with_approval", "auto"]); + expect(AUTONOMY_LEVELS).toEqual(["observe", "auto_with_approval", "auto"]); }); }); describe("normalizeAutonomyPolicy", () => { it("keeps only known action classes mapped to known levels", () => { - expect(normalizeAutonomyPolicy({ merge: "auto", review: "suggest" })).toEqual({ merge: "auto", review: "suggest" }); + expect(normalizeAutonomyPolicy({ merge: "auto", review: "auto_with_approval" })).toEqual({ merge: "auto", review: "auto_with_approval" }); }); it("drops unknown action classes and unknown levels (deny-by-omission)", () => { @@ -91,7 +89,7 @@ describe("normalizeAutonomyPolicy", () => { }); it("round-trips a valid policy through normalization", () => { - const policy: AutonomyPolicy = { review: "propose", request_changes: "auto_with_approval", merge: "observe" }; + const policy: AutonomyPolicy = { review: "auto", request_changes: "auto_with_approval", merge: "observe" }; expect(normalizeAutonomyPolicy(policy)).toEqual(policy); }); }); @@ -124,11 +122,11 @@ describe("isAgentConfigured (#777 opt-in detection)", () => { it("is true when any action class has an acting level", () => { expect(isAgentConfigured({ merge: "auto" })).toBe(true); expect(isAgentConfigured({ label: "auto_with_approval" })).toBe(true); - expect(isAgentConfigured({ review: "suggest", close: "auto" })).toBe(true); + expect(isAgentConfigured({ review: "observe", close: "auto" })).toBe(true); }); it("is false for the deny-by-default floor (all observe / non-acting / empty / null)", () => { - expect(isAgentConfigured({ merge: "observe", review: "suggest", approve: "propose" })).toBe(false); + expect(isAgentConfigured({ merge: "observe", review: "observe", approve: "observe" })).toBe(false); expect(isAgentConfigured({})).toBe(false); expect(isAgentConfigured(null)).toBe(false); expect(isAgentConfigured(undefined)).toBe(false); diff --git a/test/unit/autonomy.test.ts b/test/unit/autonomy.test.ts index b8b3d79edb..7fb828d3dc 100644 --- a/test/unit/autonomy.test.ts +++ b/test/unit/autonomy.test.ts @@ -53,8 +53,6 @@ describe("autonomy level predicates", () => { it("isActingAutonomyLevel is true only for auto / auto_with_approval", () => { expect(isActingAutonomyLevel("auto")).toBe(true); expect(isActingAutonomyLevel("auto_with_approval")).toBe(true); - expect(isActingAutonomyLevel("propose")).toBe(false); - expect(isActingAutonomyLevel("suggest")).toBe(false); expect(isActingAutonomyLevel("observe")).toBe(false); }); @@ -67,13 +65,13 @@ describe("autonomy level predicates", () => { it("the level ladder is ordered observe → … → auto with observe at the floor", () => { expect(AUTONOMY_LEVELS[0]).toBe("observe"); expect(AUTONOMY_LEVELS[AUTONOMY_LEVELS.length - 1]).toBe("auto"); - expect(AUTONOMY_LEVELS).toEqual(["observe", "suggest", "propose", "auto_with_approval", "auto"]); + expect(AUTONOMY_LEVELS).toEqual(["observe", "auto_with_approval", "auto"]); }); }); describe("normalizeAutonomyPolicy", () => { it("keeps only known action classes mapped to known levels", () => { - expect(normalizeAutonomyPolicy({ merge: "auto", review: "suggest" })).toEqual({ merge: "auto", review: "suggest" }); + expect(normalizeAutonomyPolicy({ merge: "auto", review: "auto_with_approval" })).toEqual({ merge: "auto", review: "auto_with_approval" }); }); it("drops unknown action classes and unknown levels (deny-by-omission)", () => { @@ -90,7 +88,7 @@ describe("normalizeAutonomyPolicy", () => { }); it("round-trips a valid policy through normalization", () => { - const policy: AutonomyPolicy = { review: "propose", request_changes: "auto_with_approval", merge: "observe" }; + const policy: AutonomyPolicy = { review: "auto", request_changes: "auto_with_approval", merge: "observe" }; expect(normalizeAutonomyPolicy(policy)).toEqual(policy); }); }); @@ -123,11 +121,11 @@ describe("isAgentConfigured (#777 opt-in detection)", () => { it("is true when any action class has an acting level", () => { expect(isAgentConfigured({ merge: "auto" })).toBe(true); expect(isAgentConfigured({ label: "auto_with_approval" })).toBe(true); - expect(isAgentConfigured({ review: "suggest", close: "auto" })).toBe(true); + expect(isAgentConfigured({ review: "observe", close: "auto" })).toBe(true); }); it("is false for the deny-by-default floor (all observe / non-acting / empty / null)", () => { - expect(isAgentConfigured({ merge: "observe", review: "suggest", approve: "propose" })).toBe(false); + expect(isAgentConfigured({ merge: "observe", review: "observe", approve: "observe" })).toBe(false); expect(isAgentConfigured({})).toBe(false); expect(isAgentConfigured(null)).toBe(false); expect(isAgentConfigured(undefined)).toBe(false); diff --git a/test/unit/config-templates.test.ts b/test/unit/config-templates.test.ts index fd2e18fb18..c30e7eb3ed 100644 --- a/test/unit/config-templates.test.ts +++ b/test/unit/config-templates.test.ts @@ -227,10 +227,10 @@ describe("config/examples review templates (#1682)", () => { expect(resolveAutonomy(def.settings.autonomy, "review")).toBe("observe"); // Explicit per-action levels parse; each set action resolves to its level, unset actions stay "observe" — // the parity the config-generator's autonomy-level dial emits into. - const on = parseFocusManifest({ settings: { autonomy: { review: "suggest", merge: "auto" } } }); - expect(on.settings.autonomy).toEqual({ review: "suggest", merge: "auto" }); + const on = parseFocusManifest({ settings: { autonomy: { review: "auto_with_approval", merge: "auto" } } }); + expect(on.settings.autonomy).toEqual({ review: "auto_with_approval", merge: "auto" }); expect(isAgentConfigured(on.settings.autonomy)).toBe(true); - expect(resolveAutonomy(on.settings.autonomy, "review")).toBe("suggest"); + expect(resolveAutonomy(on.settings.autonomy, "review")).toBe("auto_with_approval"); expect(resolveAutonomy(on.settings.autonomy, "merge")).toBe("auto"); expect(resolveAutonomy(on.settings.autonomy, "close")).toBe("observe"); // unset action ⇒ default }); @@ -248,7 +248,7 @@ describe("config/examples review templates (#1682)", () => { " - dist/**", "settings:", " autonomy:", - " review: suggest", + " review: auto_with_approval", ].join("\n"); const imported = parseFocusManifestContent(yml, "repo_file"); expect(imported.warnings).toEqual([]); @@ -257,6 +257,6 @@ describe("config/examples review templates (#1682)", () => { expect(imported.gate.linkedIssue).toBe("block"); expect(imported.review.inlineComments).toBe(true); expect(imported.review.excludePaths).toEqual(["dist/**"]); - expect(resolveAutonomy(imported.settings.autonomy, "review")).toBe("suggest"); + expect(resolveAutonomy(imported.settings.autonomy, "review")).toBe("auto_with_approval"); }); }); diff --git a/test/unit/effective-config-summary.test.ts b/test/unit/effective-config-summary.test.ts index 172854288a..c6e0c99ad4 100644 --- a/test/unit/effective-config-summary.test.ts +++ b/test/unit/effective-config-summary.test.ts @@ -50,7 +50,7 @@ describe("summarizeEffectiveConfig", () => { it("never leaks a secret/reward/trust/wallet field (public-safe, #2168 house rule)", () => { const out = summarizeEffectiveConfig( - base({ autonomy: { review: "auto", request_changes: "propose", approve: "auto_with_approval", merge: "auto", close: "suggest" } }), + base({ autonomy: { review: "auto", request_changes: "observe", approve: "auto_with_approval", merge: "auto", close: "observe" } }), "live", ).toLowerCase(); for (const banned of ["reward", "payout", "emission", "wallet", "hotkey", "coldkey", "privatekey", "trustscore", "rawtrust", "coldkeys", "secret"]) {