Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -492,8 +486,6 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p
<h3 className={LABEL_CLASS}>Auto-maintain (agent layer)</h3>
<p className="mt-1 text-token-2xs text-muted-foreground">
Per-action autonomy: <code className="font-mono">observe</code> (watch only) →{" "}
<code className="font-mono">suggest</code> →{" "}
<code className="font-mono">propose</code> →{" "}
<code className="font-mono">auto_with_approval</code> →{" "}
<code className="font-mono">auto</code>. Deny-by-default — anything left at{" "}
<code className="font-mono">observe</code> never acts.
Expand Down
4 changes: 3 additions & 1 deletion packages/gittensory-engine/src/settings/autonomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 3 additions & 1 deletion src/settings/autonomy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion test/unit/agent-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});

Expand Down
2 changes: 1 addition & 1 deletion test/unit/agent-execution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 5 additions & 7 deletions test/unit/autonomy-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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)", () => {
Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 5 additions & 7 deletions test/unit/autonomy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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)", () => {
Expand All @@ -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);
});
});
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions test/unit/config-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand All @@ -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([]);
Expand All @@ -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");
});
});
2 changes: 1 addition & 1 deletion test/unit/effective-config-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]) {
Expand Down
Loading