From 89a2478e075d0450342dd50d70ccfeaa22beb33d Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Mon, 5 Oct 2026 21:54:47 +0300 Subject: [PATCH 1/4] fix(core,webui,extension): an edit that lowers a change's level turns Act off instead of failing (applying-a-configuration-turns-act-off) Reported by the owner: applying any named configuration to a change under the supervisor's Act failed, since every configuration sets Semi-autonomous and Act is accepted only under Autonomous. Applying one, or saving a change's Harness Settings with another level, now removes supervisor.mode, keeps the fallback agents and allowances, and says so where it says what was applied or saved. A file written by hand with both Act and another level is still refused. Co-Authored-By: Claude Opus 5.5 --- .../applying-a-configuration-turns-act-off.md | 8 +++ HARNESS.md | 11 ++++ .../.openspec.yaml | 4 ++ .../design.md | 42 ++++++++++++ .../proposal.md | 48 ++++++++++++++ .../specs/agentic-harness/spec.md | 26 ++++++++ .../specs/shared-ui/spec.md | 16 +++++ .../tasks.md | 58 +++++++++++++++++ packages/core/src/browser.ts | 2 +- packages/core/src/harness-config.test.ts | 16 ++++- packages/core/src/harness-templates.test.ts | 64 ++++++++++++++++++- packages/core/src/harness-templates.ts | 40 +++++++++++- packages/extension/src/commands.test.ts | 20 ++++++ packages/extension/src/commands.ts | 14 ++-- .../ChangeHarnessSettingsView.test.tsx | 42 ++++++++++-- .../components/ChangeHarnessSettingsView.tsx | 7 +- .../src/components/harness-settings-parts.tsx | 10 ++- .../webui/src/run-with-harness-dispatch.ts | 7 +- packages/webui/src/standalone-entry.tsx | 6 +- 19 files changed, 417 insertions(+), 24 deletions(-) create mode 100644 .changeset/applying-a-configuration-turns-act-off.md create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/.openspec.yaml create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/design.md create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/proposal.md create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md create mode 100644 openspec/changes/applying-a-configuration-turns-act-off/tasks.md diff --git a/.changeset/applying-a-configuration-turns-act-off.md b/.changeset/applying-a-configuration-turns-act-off.md new file mode 100644 index 00000000..d10e5081 --- /dev/null +++ b/.changeset/applying-a-configuration-turns-act-off.md @@ -0,0 +1,8 @@ +--- +"@openspec-ui/core": patch +"@openspec-ui/webui": patch +"@openspec-ui/server": patch +"openspec-ui-vscode": patch +--- + +Applying a named configuration to a change under the supervisor's Act no longer fails (applying-a-configuration-turns-act-off). Every named configuration sets Semi-autonomous, and Act acts only under Autonomous, so applying one - or saving a change's Harness Settings with another autonomy level - turns Act off instead of refusing the write, keeps the fallback agents and allowances, and says so where it says what was applied or saved. A file written by hand that sets both Act and another level is still refused. diff --git a/HARNESS.md b/HARNESS.md index c67e2af7..abf6e591 100644 --- a/HARNESS.md +++ b/HARNESS.md @@ -348,6 +348,17 @@ the change has one, and shows the fallback fields and the two allowances with a note on cost and providers. `openspec-ui-cli run` prints the move, and the reason under the next attempt's heading. +**An edit that lowers the level turns Act off** +(applying-a-configuration-turns-act-off). Applying a named configuration +to a change - every one sets Semi-autonomous - or saving a change's +Harness Settings with an autonomy level other than Autonomous removes +`supervisor.mode` where it was `act`, so the change follows the +workspace's mode, and says so where it says what was applied or saved. +The fallback agents and the two allowances stay in the file: choose +Autonomous and Act again to bring it back as it was. Only a file written +by hand that says both `act` and another level is refused, since nothing +chose between them. + ### `branches` `{ "rebaseWhenBehind"?: , "followMain"?: }`. Optional; diff --git a/openspec/changes/applying-a-configuration-turns-act-off/.openspec.yaml b/openspec/changes/applying-a-configuration-turns-act-off/.openspec.yaml new file mode 100644 index 00000000..b681b9ac --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/.openspec.yaml @@ -0,0 +1,4 @@ +schema: spec-driven +created: 2026-10-05 +follows: + - the-supervisor-changes-agents diff --git a/openspec/changes/applying-a-configuration-turns-act-off/design.md b/openspec/changes/applying-a-configuration-turns-act-off/design.md new file mode 100644 index 00000000..54595345 --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/design.md @@ -0,0 +1,42 @@ +## Context + +`the-supervisor-changes-agents` accepts `supervisor.mode: "act"` only in a +change's own file that also sets `autonomyLevel: "autonomous"` +(ADR 0018: what widens authority is per-change only). The rule is checked +when a file is written and when it resolves. Applying a named +configuration lays the configuration over the change's file and keeps +every other key (applying-a-template-keeps-the-rest); every named +configuration sets `semi-autonomous`; so the result kept Act and lost +Autonomous, and was refused. + +## Decisions + +1. **An edit that lowers the level turns Act off.** One function in core, + `withoutActItCannotUse(config)`, removes `supervisor.mode` where it is + `act` and the same config's `autonomyLevel` is not `autonomous`, and + says whether it did. A `supervisor` left empty is removed. The fallback + agents and the allowances stay: they do nothing without Act, and they + are what a person set. + - *Alternative: refuse with a clearer message.* Rejected by the owner: + applying a configuration is a decision to change the level, and the + configuration should apply. + - *Alternative: keep Autonomous instead.* Rejected: it would widen what + the configuration says, which is the direction ADR 0018 forbids + guessing in. +2. **Where it applies.** `changeTemplateConfigToWrite`, the one function + every surface applies a configuration to a change through (extension, + standalone settings view, standalone run dialog), and + `changeConfigToSave`, which a change's Harness Settings save through. +3. **It is said.** The extension's applied note and the settings view's + applied message add that the supervisor's Act is off, because it acts + only under Autonomous and the configuration sets another level. The + settings view's note under the supervisor, which said saving would be + refused, says Act turns off when the form is saved. +4. **A hand-written contradiction is still refused.** The validator does + not change: a file that says Act and a lower level in one breath states + two things, and the product does not pick one. + +## Risks / Trade-offs + +- **A person may not notice Act went off.** It is said on the surface that + applied or saved it, and the change's Harness Settings show the mode. diff --git a/openspec/changes/applying-a-configuration-turns-act-off/proposal.md b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md new file mode 100644 index 00000000..8264b3ed --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md @@ -0,0 +1,48 @@ +## Why + +Reported by the owner on 2026-10-05: in a workspace whose changes run +under `supervisor.mode: "act"`, applying any named configuration to a +change failed with "supervisor.mode "act" needs autonomyLevel +"autonomous" in the same file". Every named configuration sets +`autonomyLevel: "semi-autonomous"`, and applying one keeps the rest of the +change's file, `supervisor` included; the file it produced broke a rule +the product itself states, so the write was refused. The same happened in +a change's Harness Settings when the autonomy level was moved away from +Autonomous while Act was chosen: the view only warned that saving would +be refused. + +The owner's rule: configurations change freely, by rules a person can +follow, and are refused only where refusing is truly needed. + +## What Changes + +- Where a person's own edit takes a change's autonomy level below + Autonomous - a named configuration applied, or the level changed in the + change's Harness Settings - an Act the change had is turned off rather + than refused: the supervisor's mode is removed, so the change follows + the workspace's (Advise unless the workspace says Off). The fallback + agents and the two allowances are kept, so choosing Act again under + Autonomous restores what was set. +- Every surface that does this says so where it says what was applied or + saved. +- A file written by hand that sets Act without Autonomous is still + refused: nothing chose between the two, and guessing which was meant + would act on a guess. + +## Capabilities + +### Modified Capabilities + +- `agentic-harness`: applying a named configuration turns off an Act it + leaves without Autonomous. +- `shared-ui`: a change's Harness Settings turn Act off, and say so, when + the autonomy level leaves Autonomous. + +## Impact + +- `packages/core`: `harness-config.ts` (the rule as one function), + `harness-templates.ts` (`changeTemplateConfigToWrite`). +- `packages/webui`: `harness-settings-parts.tsx` (`changeConfigToSave`, the + applied message), `ChangeHarnessSettingsView.tsx` (the note). +- `packages/extension`: `commands.ts` (the applied note). +- `HARNESS.md`. diff --git a/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md b/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md new file mode 100644 index 00000000..66fefc86 --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Applying a configuration turns off an Act it leaves without Autonomous + +Applying a named configuration to a change SHALL NOT fail because the +change's file has `supervisor.mode: "act"` and the configuration sets an +autonomy level other than `autonomous`. The written file SHALL instead +carry no `supervisor.mode`, SHALL keep the change's `supervisor.fallback`, +`allowCostIncrease` and `allowProviderChange`, and the surface that +applied it SHALL say that Act was turned off and why. A change's file +written by hand with `act` and another level SHALL still be refused. + +#### Scenario: A named configuration over a change under Act + +- **WHEN** a change's file sets `autonomyLevel: "autonomous"`, + `maxStageAttempts: 2` and `supervisor` with `mode: "act"` and a fallback + for apply, and Balanced is applied to it +- **THEN** the file is written with `autonomyLevel: "semi-autonomous"`, a + `supervisor` with the fallback and no `mode`, and the applied note says + the supervisor's Act is off because Balanced sets Semi-autonomous + +#### Scenario: A hand-written contradiction + +- **WHEN** a change's `harness.json` is written with `supervisor.mode: + "act"` and `autonomyLevel: "semi-autonomous"` +- **THEN** the write is refused, naming the rule, as before diff --git a/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md b/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md new file mode 100644 index 00000000..e4f0eb35 --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: A change's Harness Settings turn Act off when the level leaves Autonomous + +When a change's Harness Settings have Act chosen and the autonomy level +is set to anything but Autonomous, by hand or by a named configuration, +the view SHALL say that Act turns off when the form is saved, and saving +SHALL write the change's file without `supervisor.mode`, keeping its +fallback agents and allowances. Saving SHALL NOT be refused for it. + +#### Scenario: The level moved away from Autonomous + +- **WHEN** a change under Act has its autonomy level set to Inherit in its + Harness Settings and is saved +- **THEN** the note under the supervisor said Act turns off on saving, and + the saved file has no `supervisor.mode` and the same fallback agents diff --git a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md new file mode 100644 index 00000000..74fafd67 --- /dev/null +++ b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md @@ -0,0 +1,58 @@ +Reported by the owner on 2026-10-05: applying any named configuration to +a change under Act failed. Rule: configurations change freely by rules a +person can follow, and are refused only where truly needed. + +## 1. Core + +- [x] 1.1 `harness-config.ts`: `withoutActItCannotUse` (design.md + decision 1). Tests in `harness-config.test.ts`: act with another level + loses its mode and keeps fallback and allowances; act under autonomous + unchanged; a `supervisor` left empty is removed; a hand-written file + with act and another level is still refused. + Placed in `harness-templates.ts`, beside the function that applies + configurations, since `harness-config.ts` is not browser-safe; with + `actTurnedOff` and `actTurnedOffNote` for the surfaces. Four cases in + `harness-templates.test.ts`; the hand-written refusal in + `harness-config.test.ts`. 266 passed across both. +- [x] 1.2 `harness-templates.ts`: `changeTemplateConfigToWrite` applies it. + Test in `harness-templates.test.ts`: every change-scope configuration + over a change under Act writes a file the validator accepts, with no + mode and the fallback kept. + Every change-scope configuration over a change under Act is written + and accepted by `writeChangeHarnessConfig` (`harness-config.test.ts`), + with no mode and the fallback kept (`harness-templates.test.ts`). + +## 2. Surfaces + +- [x] 2.1 `packages/webui`: `changeConfigToSave` applies it; the + supervisor's note says Act turns off on saving; the applied message + says so after a configuration. Tests in + `ChangeHarnessSettingsView.test.tsx`. + `changeConfigToSave` and the applied message; the standalone run + dialog's applied note too (`applyTemplateToChange` returns whether Act + went off). Two cases replace the old "will be refused" one; 31 passed + in the file, webui 713. +- [x] 2.2 `packages/extension`: the applied note says Act was turned off. + Test beside the command's existing tests. + One case in `commands.test.ts`, 163 passed. + +## 3. Documents + +- [x] 3.1 `HARNESS.md`, `supervisor`, Act: what turns it off. + "An edit that lowers the level turns Act off". +- [x] 3.2 A changeset: core, webui, server, extension, patch. + `.changeset/applying-a-configuration-turns-act-off.md`. + +## 4. Checks + +- [x] 4.1 `npm run typecheck && npm run lint`, and every test project, each + on its own where the root run would exceed a background limit. + Typecheck clean; lint 0 errors (3 warnings in lines this change did + not touch); seven script tests pass. Core 2101, core-git-subprocess 67, + cli 201, webui 713, server 122, extension 503 passed. +- [ ] 4.2 `openspec validate applying-a-configuration-turns-act-off + --strict`, and the merge gate with the worktree's absolute path as + `--cwd`. +- [ ] 4.3 **Human-only**: in the owner's workspace, apply a named + configuration to a change under Act, and change its autonomy level in + its Harness Settings; both apply, and say Act is off. diff --git a/packages/core/src/browser.ts b/packages/core/src/browser.ts index eba9f91a..c7e79e59 100644 --- a/packages/core/src/browser.ts +++ b/packages/core/src/browser.ts @@ -139,7 +139,7 @@ export type { ChangeDateEvidence, ChangeDateSource, ChangeDates, DatedFact } fro // Pure over a resolved config, with only type imports from the Node-side // modules — safe in the browser bundle, unlike harness-config.js itself. export { findHarnessConfigLimits, groupHarnessFindings } from "./harness-config-findings.js"; -export { agentForEveryStageToWrite, changeTemplateConfigToWrite, HARNESS_TEMPLATES, stepAgentsForTemplate, templateConfigToWrite, templatesForScope } from "./harness-templates.js"; +export { actTurnedOff, actTurnedOffNote, agentForEveryStageToWrite, changeTemplateConfigToWrite, HARNESS_TEMPLATES, stepAgentsForTemplate, templateConfigToWrite, templatesForScope, withoutActItCannotUse } from "./harness-templates.js"; export { HARNESS_EFFORT_LEVELS, effortLevelCollisions, resolveEffortLevel } from "./harness-effort-level.js"; export type { HarnessEffortLevel, ResolvedEffort } from "./harness-effort-level.js"; export { recommendTemplate } from "./harness-recommendation.js"; diff --git a/packages/core/src/harness-config.test.ts b/packages/core/src/harness-config.test.ts index 00300497..39f3403a 100644 --- a/packages/core/src/harness-config.test.ts +++ b/packages/core/src/harness-config.test.ts @@ -35,7 +35,7 @@ import { // merged a stage entry by spread and kept `customAgent`, `mergeStepAgent` // named three fields and dropped it, so the same entry came out of the two // differently. See a-stage-override-keeps-its-custom-agent. -import { HARNESS_TEMPLATES, templateConfigToWrite } from "./harness-templates.js"; +import { changeTemplateConfigToWrite, HARNESS_TEMPLATES, templateConfigToWrite, templatesForScope } from "./harness-templates.js"; // suite-survives-a-loaded-machine: // measured 2026-09-05 for this file alone at 734ms test time (3.91s wall) @@ -1104,6 +1104,20 @@ describe("supervisor act", () => { await expect(write({ apply: [] })).rejects.toThrow(/non-empty list/); }); + // applying-a-configuration-turns-act-off 1.1, 1.2: what a named + // configuration writes over a change under Act is a file that is accepted; + // the same contradiction written by hand is still refused. + it("accepts what a named configuration writes over Act, and refuses the contradiction by hand", async () => { + const root = await temporaryRoot(); + const own = { autonomyLevel: "autonomous" as const, maxStageAttempts: 2, supervisor: act }; + await writeChangeHarnessConfig(root, "demo", own); + for (const template of templatesForScope("change")) { + await writeChangeHarnessConfig(root, "demo", changeTemplateConfigToWrite(template, await resolveHarnessConfig(root), own)); + } + await expect(writeChangeHarnessConfig(root, "demo", { ...own, autonomyLevel: "semi-autonomous" })) + .rejects.toThrow(/needs autonomyLevel "autonomous" in the same file/); + }); + it("refuses the allowances in the workspace's file", async () => { const root = await temporaryRoot(); await expect(writeGlobalHarnessConfig(root, { supervisor: { allowCostIncrease: true } } as never)) diff --git a/packages/core/src/harness-templates.test.ts b/packages/core/src/harness-templates.test.ts index cdbed848..550e799d 100644 --- a/packages/core/src/harness-templates.test.ts +++ b/packages/core/src/harness-templates.test.ts @@ -4,7 +4,16 @@ import { DEFAULT_HARNESS_CONFIG, type HarnessConfig } from "./harness-config.js" import { HARNESS_AGENT_CAPABILITIES, normalizeStepAgent } from "./harness-step-agent.js"; import { HARNESS_EFFORT_LEVELS, resolveEffortLevel } from "./harness-effort-level.js"; import type { HarnessStepAgent, HarnessStepAgents } from "./harness-step-agent.js"; -import { agentForEveryStageToWrite, changeTemplateConfigToWrite, HARNESS_TEMPLATES, stepAgentsForTemplate, templateConfigToWrite, templatesForScope } from "./harness-templates.js"; +import { + actTurnedOff, + agentForEveryStageToWrite, + changeTemplateConfigToWrite, + HARNESS_TEMPLATES, + stepAgentsForTemplate, + templateConfigToWrite, + templatesForScope, + withoutActItCannotUse, +} from "./harness-templates.js"; // settings-templates: // pure over in-memory data — no files, no processes. Measured 2026-09-08 @@ -459,3 +468,56 @@ describe("agentForEveryStageToWrite — a recommendation from runs, applied", () expect(written.maxStageAttempts).toBe(3); }); }); + +// applying-a-configuration-turns-act-off 1.1, 1.2 +describe("withoutActItCannotUse — an edit that lowers the level turns Act off", () => { + const underAct: Partial = { + autonomyLevel: "autonomous", + maxStageAttempts: 2, + supervisor: { + mode: "act", + fallback: { apply: ["deepseek-cli-acp", "copilot-cli-acp"] }, + allowCostIncrease: true, + allowProviderChange: true, + }, + }; + + it("removes the mode under another level, and keeps the fallback and the allowances", () => { + expect(withoutActItCannotUse({ ...underAct, autonomyLevel: "semi-autonomous" })).toEqual({ + autonomyLevel: "semi-autonomous", + maxStageAttempts: 2, + supervisor: { + fallback: { apply: ["deepseek-cli-acp", "copilot-cli-acp"] }, + allowCostIncrease: true, + allowProviderChange: true, + }, + }); + const { autonomyLevel: _inherited, ...inheriting } = underAct; + expect(withoutActItCannotUse(inheriting).supervisor).not.toHaveProperty("mode"); + }); + + it("leaves Act under autonomous, and every other mode, as it was", () => { + expect(withoutActItCannotUse(underAct)).toBe(underAct); + const advising: Partial = { autonomyLevel: "assisted", supervisor: { mode: "off" } }; + expect(withoutActItCannotUse(advising)).toBe(advising); + }); + + it("removes a supervisor left with nothing in it", () => { + expect(withoutActItCannotUse({ autonomyLevel: "semi-autonomous", supervisor: { mode: "act" } })).toEqual({ autonomyLevel: "semi-autonomous" }); + }); + + it("is applied by every named configuration a change may be given, and says so", () => { + for (const template of templatesForScope("change")) { + const written = changeTemplateConfigToWrite(template, DEFAULT_HARNESS_CONFIG, underAct); + expect(written.autonomyLevel, template.id).not.toBe("autonomous"); + expect(written.supervisor, template.id).toEqual({ + fallback: { apply: ["deepseek-cli-acp", "copilot-cli-acp"] }, + allowCostIncrease: true, + allowProviderChange: true, + }); + expect(actTurnedOff(underAct, written), template.id).toBe(true); + } + expect(actTurnedOff(underAct, underAct)).toBe(false); + expect(actTurnedOff(undefined, {})).toBe(false); + }); +}); diff --git a/packages/core/src/harness-templates.ts b/packages/core/src/harness-templates.ts index 1f268ca3..4869afb5 100644 --- a/packages/core/src/harness-templates.ts +++ b/packages/core/src/harness-templates.ts @@ -260,11 +260,47 @@ export function changeTemplateConfigToWrite( base: HarnessConfig, override: Partial | undefined, ): Partial { - return templateConfigToWrite( + // Every named configuration sets a level below autonomous, and an Act the + // change kept would make the file one the validator refuses + // (applying-a-configuration-turns-act-off). + return withoutActItCannotUse(templateConfigToWrite( template, mergeStepAgents(base.stepAgents, override?.stepAgents), override ?? {}, - ); + )); +} + +/** A change's file with an Act it can no longer use turned off: where the + * supervisor's mode is `act` and the same file's autonomy level is not + * `autonomous`, the mode is removed, so the change follows the + * workspace's. The fallback agents and allowances stay, so choosing Act + * again under Autonomous restores what was set; a `supervisor` left empty + * is removed. Applied where a person's own edit lowered the level - a named + * configuration, the change's Harness Settings - and never to a file read + * as written, whose contradiction is refused rather than guessed at + * (applying-a-configuration-turns-act-off). */ +export function withoutActItCannotUse>(config: T): T { + if (config.supervisor?.mode !== "act" || config.autonomyLevel === "autonomous") return config; + const { mode: _off, ...rest } = config.supervisor; + const result = { ...config }; + if (Object.keys(rest).length === 0) delete result.supervisor; + else result.supervisor = rest; + return result; +} + +/** What a surface says where applying `template` turned a change's Act off: + * the same words in every host. */ +export function actTurnedOffNote(template: HarnessTemplate): string { + const level = template.config.autonomyLevel; + const named = level === undefined ? "another level" : `${level[0]?.toUpperCase() ?? ""}${level.slice(1)}`; + return `The supervisor's Act is off: it acts only under Autonomous, and "${template.title}" sets ${named}.` + + " The fallback agents and allowances are kept."; +} + +/** Whether an edit turned the change's Act off, for the surface that made + * it to say so. */ +export function actTurnedOff(before: Partial | null | undefined, after: Partial): boolean { + return before?.supervisor?.mode === "act" && after.supervisor?.mode !== "act"; } /** The override to write when one agent is put on every stage of a diff --git a/packages/extension/src/commands.test.ts b/packages/extension/src/commands.test.ts index 0b9374ae..244efbf5 100644 --- a/packages/extension/src/commands.test.ts +++ b/packages/extension/src/commands.test.ts @@ -2752,6 +2752,26 @@ describe("createRunChoiceHandler", () => { expect(deps.implementationSessions.start).not.toHaveBeenCalled(); }); + // applying-a-configuration-turns-act-off 2.2: a change's Act that the + // written level does not allow is turned off, kept fallback and all, and + // the note beside the Apply button says so. + it("turns off an Act the written level does not allow, and says so", async () => { + readGlobalHarnessConfigMock.mockResolvedValue({ stepAgents: {}, autonomyLevel: "assisted", reviewGate: { mode: "human-required" } }); + resolveHarnessConfigMock.mockResolvedValue({ stepAgents: {}, autonomyLevel: "assisted", reviewGate: { mode: "human-required" } }); + readChangeHarnessConfigMock.mockResolvedValue({ supervisor: { mode: "act", fallback: { apply: ["local-llm-acp"] } } }); + buildRunPlanMock.mockReturnValue({ resolved: "single-stage", because: "x", stageAgents: [], offered: [], findings: [] }); + const deps = makeDeps(); + + await createRunChoiceHandler(deps)({ kind: "apply-template", templateId: "balanced" }, context); + + expect(writeChangeHarnessConfigMock).toHaveBeenCalledWith("/workspace/repo", "demo-change", expect.objectContaining({ + supervisor: { fallback: { apply: ["local-llm-acp"] } }, + })); + expect(deps.revealAiPanel).toHaveBeenCalledWith(expect.objectContaining({ + appliedNote: expect.stringContaining("The supervisor's Act is off: it acts only under Autonomous"), + })); + }); + it("writes nothing for a configuration id it does not have", async () => { // The two sides disagreeing about what exists is worth a line in the // log; it is not worth writing something. diff --git a/packages/extension/src/commands.ts b/packages/extension/src/commands.ts index c9df4e4f..3322f948 100644 --- a/packages/extension/src/commands.ts +++ b/packages/extension/src/commands.ts @@ -26,6 +26,8 @@ import { describeChangeState, readChangeStandings, type DescribedChangeState, + actTurnedOff, + actTurnedOffNote, changeTemplateConfigToWrite, templatesForScope, type RecommendationInput, type RunStartFacts, @@ -678,18 +680,18 @@ export function createRunChoiceHandler(deps: CommandsDeps) { // and a-stage-override-keeps-its-custom-agent. const global = await readGlobalHarnessConfig(workspaceRoot); const existing = await readChangeHarnessConfig(workspaceRoot, changeName); - await writeChangeHarnessConfig( - workspaceRoot, - changeName, - changeTemplateConfigToWrite(template, global, existing), - ); + const written = changeTemplateConfigToWrite(template, global, existing); + await writeChangeHarnessConfig(workspaceRoot, changeName, written); // Re-read rather than re-render what was on screen: the dialog // should show what the file now resolves to, which is the same // thing the standalone shell does after applying one. await revealResolved({ appliedNote: `Applied "${template.title}" to openspec/changes/${changeName}/harness.json. ` - + "The dialog now shows what the change resolves to.", + + "The dialog now shows what the change resolves to." + // An Act the configuration's level does not allow was turned off + // (applying-a-configuration-turns-act-off). + + (actTurnedOff(existing, written) ? ` ${actTurnedOffNote(template)}` : ""), }); } catch (error) { await showCommandError("apply the named configuration", error); diff --git a/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx b/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx index 408cdb01..b02c6e44 100644 --- a/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx +++ b/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx @@ -347,16 +347,50 @@ describe("ChangeHarnessSettingsView — saving", () => { await waitFor(() => expect(api.writeChangeOverride).toHaveBeenCalledWith("demo", expect.objectContaining({ supervisor: { mode: "act" } }))); }); - it("says Act will be refused once the change is no longer autonomous", async () => { + // applying-a-configuration-turns-act-off 2.1 + it("turns Act off on saving once the change is no longer autonomous, and says so first", async () => { const api = createApi({ - readChangeOverride: vi.fn().mockResolvedValue({ stepAgents: {}, autonomyLevel: "autonomous", maxStageAttempts: 3, supervisor: { mode: "act" } }), + readChangeOverride: vi.fn().mockResolvedValue({ + stepAgents: {}, + autonomyLevel: "autonomous", + maxStageAttempts: 3, + supervisor: { mode: "act", fallback: { apply: ["local-llm-acp"] }, allowCostIncrease: true }, + }), }); await renderLoaded(api); expect(chosen("Change supervisor")).toBe("act"); - expect(screen.queryByTestId("change-supervisor-act-refused")).toBeNull(); + expect(screen.queryByTestId("change-supervisor-act-off")).toBeNull(); choose("Change autonomy level", ""); - expect(screen.getByTestId("change-supervisor-act-refused").textContent).toContain("saving will be refused"); + expect(screen.getByTestId("change-supervisor-act-off").textContent).toContain("Act turns off when this is saved"); + fireEvent.click(saveButton()); + + await waitFor(() => expect(api.writeChangeOverride).toHaveBeenCalled()); + const saved = (api.writeChangeOverride as ReturnType).mock.calls[0]![1] as Record; + expect(saved.autonomyLevel).toBeUndefined(); + expect(saved.supervisor).toEqual({ fallback: { apply: ["local-llm-acp"] }, allowCostIncrease: true }); + }); + + it("applies a named configuration over Act, and says Act is off", async () => { + const api = createApi({ + readChangeOverride: vi.fn().mockResolvedValue({ + stepAgents: { apply: "local-llm-acp" }, + autonomyLevel: "autonomous", + maxStageAttempts: 2, + supervisor: { mode: "act", fallback: { apply: ["deepseek-cli-acp"] }, allowProviderChange: true }, + }), + }); + await renderLoaded(api); + + choose("Named configuration", "balanced"); + fireEvent.click(screen.getByRole("button", { name: "Apply to the form" })); + expect(screen.getByText(/The supervisor's Act is off: it acts only under Autonomous, and "Balanced" sets Semi-autonomous/)).toBeTruthy(); + fireEvent.click(saveButton()); + + await waitFor(() => expect(api.writeChangeOverride).toHaveBeenCalled()); + const saved = (api.writeChangeOverride as ReturnType).mock.calls[0]![1] as Record; + expect(saved.autonomyLevel).toBe("semi-autonomous"); + expect(saved.supervisor).toEqual({ fallback: { apply: ["deepseek-cli-acp"] }, allowProviderChange: true }); }); it("still removes autonomyLevel when it is set back to inherit", async () => { diff --git a/packages/webui/src/components/ChangeHarnessSettingsView.tsx b/packages/webui/src/components/ChangeHarnessSettingsView.tsx index 7e9ec231..1d3111b7 100644 --- a/packages/webui/src/components/ChangeHarnessSettingsView.tsx +++ b/packages/webui/src/components/ChangeHarnessSettingsView.tsx @@ -1,5 +1,6 @@ import { useEffect, useId, useMemo, useRef, useState } from "react"; import { + actTurnedOff, changeTemplateConfigToWrite, findHarnessConfigLimits, isHarnessStepAgentStage, @@ -222,7 +223,7 @@ export function ChangeHarnessSettingsView( // The ceilings ride here, not in the form, and the save lays the form // over this rather than replacing it. setOverride(written); - setApplyStatus(changeTemplateAppliedMessage(template, writtenForms.effort, resolvedAgents, reset)); + setApplyStatus(changeTemplateAppliedMessage(template, writtenForms.effort, resolvedAgents, reset, actTurnedOff(override, written))); }; async function save() { @@ -417,8 +418,8 @@ export function ChangeHarnessSettingsView( : ""}

{supervisorMode === "act" && !ownAutonomous ? ( -

- Act needs this change's own autonomy level to be Autonomous: saving will be refused until it is, or until another mode is chosen. +

+ Act turns off when this is saved: it acts only where this change's own autonomy level is Autonomous. The fallback agents and allowances are kept.

) : null} {supervisorMode === "act" ? ( diff --git a/packages/webui/src/components/harness-settings-parts.tsx b/packages/webui/src/components/harness-settings-parts.tsx index d8259d13..618cb56c 100644 --- a/packages/webui/src/components/harness-settings-parts.tsx +++ b/packages/webui/src/components/harness-settings-parts.tsx @@ -10,6 +10,8 @@ import { resolveEffortLevel, STAGE_PURPOSES, VSCODE_CHAT_STEP_AGENT_ID, + actTurnedOffNote, + withoutActItCannotUse, type HarnessAutonomyLevel, type HarnessConfig, type HarnessEffort, @@ -266,12 +268,14 @@ export function changeTemplateAppliedMessage( effort: StepEffortForm, agents: StepAgentsForm, reset: readonly HarnessStepAgentStage[], + actOff = false, ): string { + const actNote = actOff ? ` ${actTurnedOffNote(template)}` : ""; const agentNote = reset.length === 0 ? " The agents and models on screen are unchanged." : ` ${reset.join(", ")} now name${reset.length === 1 ? "s" : ""} the agent the change resolves to,` + " because an effort without its agent means nothing."; - return `Filled from "${template.title}". ${templateEffortNote(template, effort, agents)}${agentNote}` + return `Filled from "${template.title}". ${templateEffortNote(template, effort, agents)}${agentNote}${actNote}` + " Nothing is saved until you save."; } @@ -349,7 +353,9 @@ export function changeConfigToSave( else delete config.autonomyLevel; if (reviewGateMode !== INHERIT) config.reviewGate = { mode: reviewGateMode }; else delete config.reviewGate; - return withRunBudget(config, runBudget); + // An Act the level no longer allows is turned off rather than refused + // (applying-a-configuration-turns-act-off). + return withoutActItCannotUse(withRunBudget(config, runBudget)); } /** The run budget as the field shows it: the chain's cost ceiling, or empty. */ diff --git a/packages/webui/src/run-with-harness-dispatch.ts b/packages/webui/src/run-with-harness-dispatch.ts index 4732dcaf..5e8d1af4 100644 --- a/packages/webui/src/run-with-harness-dispatch.ts +++ b/packages/webui/src/run-with-harness-dispatch.ts @@ -7,7 +7,7 @@ // code from entry-point wiring (`harness-config-client.ts`, // `change-editor-client.ts`). -import { agentForEveryStageToWrite, buildRunPlan, changeTemplateConfigToWrite, openTaskCount, resolveRunWithHarnessTarget, runStartFactsFrom, type HarnessBudget, type HarnessTemplate, type RunPlan, type RunWithHarnessTarget } from "@openspec-ui/core/browser"; +import { actTurnedOff, agentForEveryStageToWrite, buildRunPlan, changeTemplateConfigToWrite, openTaskCount, resolveRunWithHarnessTarget, runStartFactsFrom, type HarnessBudget, type HarnessTemplate, type RunPlan, type RunWithHarnessTarget } from "@openspec-ui/core/browser"; import { readChangeHarnessOverride, resolveHarnessConfig, writeHarnessConfig } from "./harness-config-client.js"; import { loadChangeTimeline } from "./change-timeline-client.js"; import type { ChangeEditorRequest } from "./change-editor-client.js"; @@ -118,7 +118,7 @@ export async function applyTemplateToChange( cwd: string, changeName: string, template: HarnessTemplate, -): Promise { +): Promise<{ actTurnedOff: boolean }> { const [existing, global] = await Promise.all([ readChangeHarnessOverride(request, cwd, changeName), // The global file, not the change's own override: the effort has to @@ -129,6 +129,9 @@ export async function applyTemplateToChange( ]); const config = changeTemplateConfigToWrite(template, global, existing ?? undefined); await writeHarnessConfig(request, cwd, config, changeName); + // Said by the caller beside what it applied + // (applying-a-configuration-turns-act-off). + return { actTurnedOff: actTurnedOff(existing, config) }; } /** Puts one agent on every stage of a change, keeping what the change's diff --git a/packages/webui/src/standalone-entry.tsx b/packages/webui/src/standalone-entry.tsx index 753651a4..bb64b321 100644 --- a/packages/webui/src/standalone-entry.tsx +++ b/packages/webui/src/standalone-entry.tsx @@ -116,6 +116,7 @@ import { } from "./run-with-harness-dispatch.js"; import { fireDueSchedule, type ScheduleFiringHost } from "./scheduled-run-firing.js"; import { + actTurnedOffNote, comparisonRows, comparisonWindow, DEFAULT_COMPARISON_PERIOD, @@ -928,14 +929,15 @@ function StandaloneApp() { if (!runDispatch) return; setRunHarnessLoading(true); try { - await applyTemplateToChangeApi(apiFetch, cwd, runChangeName, template); + const applied = await applyTemplateToChangeApi(apiFetch, cwd, runChangeName, template); setRunDispatch(await resolveRunWithHarnessDispatch(apiFetch, cwd, runChangeName)); // Beside the Apply button, in the dialog. It used to be set above // the dialog, where the person who pressed Apply could not see it // (a-change-is-configured-from-the-change). setRunAppliedNote( `Applied "${template.title}" to openspec/changes/${runChangeName}/harness.json. ` - + "The dialog now shows what the change resolves to.", + + "The dialog now shows what the change resolves to." + + (applied.actTurnedOff ? ` ${actTurnedOffNote(template)}` : ""), ); } catch (error) { const message = error instanceof Error ? error.message : String(error); From 476a944e237061861da433c1a92c849b852e7635 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Mon, 5 Oct 2026 21:56:20 +0300 Subject: [PATCH 2/4] docs(openspec): record the checks of applying-a-configuration-turns-act-off Co-Authored-By: Claude Opus 5.5 --- .../applying-a-configuration-turns-act-off/proposal.md | 4 ++-- .../changes/applying-a-configuration-turns-act-off/tasks.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openspec/changes/applying-a-configuration-turns-act-off/proposal.md b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md index 8264b3ed..796d125b 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/proposal.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md @@ -40,8 +40,8 @@ follow, and are refused only where refusing is truly needed. ## Impact -- `packages/core`: `harness-config.ts` (the rule as one function), - `harness-templates.ts` (`changeTemplateConfigToWrite`). +- `packages/core`: `harness-templates.ts` (the rule as one function, and + `changeTemplateConfigToWrite` applying it). - `packages/webui`: `harness-settings-parts.tsx` (`changeConfigToSave`, the applied message), `ChangeHarnessSettingsView.tsx` (the note). - `packages/extension`: `commands.ts` (the applied note). diff --git a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md index 74fafd67..1f608549 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md @@ -50,9 +50,11 @@ person can follow, and are refused only where truly needed. Typecheck clean; lint 0 errors (3 warnings in lines this change did not touch); seven script tests pass. Core 2101, core-git-subprocess 67, cli 201, webui 713, server 122, extension 503 passed. -- [ ] 4.2 `openspec validate applying-a-configuration-turns-act-off +- [x] 4.2 `openspec validate applying-a-configuration-turns-act-off --strict`, and the merge gate with the worktree's absolute path as `--cwd`. + Valid; the gate with `--cwd C:/Prog/.worktrees/OpenSpec-UI/applying-a-configuration-turns-act-off` + reports only 4.3 open (Human-only). - [ ] 4.3 **Human-only**: in the owner's workspace, apply a named configuration to a change under Act, and change its autonomy level in its Harness Settings; both apply, and say Act is off. From eef681733137377bf5f87cef30154392c34511fd Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Mon, 5 Oct 2026 22:32:00 +0300 Subject: [PATCH 3/4] fix(core,webui): a hand-written Act without Autonomous opens in its Harness Settings (applying-a-configuration-turns-act-off) Found in the owner's check: the rule that Act needs autonomyLevel "autonomous" in the same file was part of reading a change's file, so a file written by hand with both could not even open in its Harness Settings ("Load failed"), the one place that would put it right. It is now checked where a change's file is written and where it resolves for a run - which names both ways out - and not where it is read. The settings view opens such a file, says Act turns off on saving, and offers Save with nothing else changed. Co-Authored-By: Claude Opus 5.5 --- .../applying-a-configuration-turns-act-off.md | 2 +- HARNESS.md | 5 ++-- .../design.md | 13 +++++++--- .../proposal.md | 7 ++--- .../specs/agentic-harness/spec.md | 21 +++++++++++---- .../specs/shared-ui/spec.md | 7 +++++ .../tasks.md | 12 ++++++++- packages/core/src/harness-config.test.ts | 13 ++++++++++ packages/core/src/harness-config.ts | 26 +++++++++++++++---- .../ChangeHarnessSettingsView.test.tsx | 22 ++++++++++++++++ .../components/ChangeHarnessSettingsView.tsx | 6 ++++- 11 files changed, 113 insertions(+), 21 deletions(-) diff --git a/.changeset/applying-a-configuration-turns-act-off.md b/.changeset/applying-a-configuration-turns-act-off.md index d10e5081..39fc40fb 100644 --- a/.changeset/applying-a-configuration-turns-act-off.md +++ b/.changeset/applying-a-configuration-turns-act-off.md @@ -5,4 +5,4 @@ "openspec-ui-vscode": patch --- -Applying a named configuration to a change under the supervisor's Act no longer fails (applying-a-configuration-turns-act-off). Every named configuration sets Semi-autonomous, and Act acts only under Autonomous, so applying one - or saving a change's Harness Settings with another autonomy level - turns Act off instead of refusing the write, keeps the fallback agents and allowances, and says so where it says what was applied or saved. A file written by hand that sets both Act and another level is still refused. +Applying a named configuration to a change under the supervisor's Act no longer fails (applying-a-configuration-turns-act-off). Every named configuration sets Semi-autonomous, and Act acts only under Autonomous, so applying one - or saving a change's Harness Settings with another autonomy level - turns Act off instead of refusing the write, keeps the fallback agents and allowances, and says so where it says what was applied or saved. A file written by hand that sets both Act and another level is not run, but it now opens in the change's Harness Settings, which put it right on Save. diff --git a/HARNESS.md b/HARNESS.md index abf6e591..1cc3c0c1 100644 --- a/HARNESS.md +++ b/HARNESS.md @@ -356,8 +356,9 @@ Harness Settings with an autonomy level other than Autonomous removes workspace's mode, and says so where it says what was applied or saved. The fallback agents and the two allowances stay in the file: choose Autonomous and Act again to bring it back as it was. Only a file written -by hand that says both `act` and another level is refused, since nothing -chose between them. +by hand that says both `act` and another level is not run and is not +written, since nothing chose between them; it still opens in the change's +Harness Settings, which say Act turns off and put it right on Save. ### `branches` diff --git a/openspec/changes/applying-a-configuration-turns-act-off/design.md b/openspec/changes/applying-a-configuration-turns-act-off/design.md index 54595345..7a32518a 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/design.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/design.md @@ -32,9 +32,16 @@ Autonomous, and was refused. only under Autonomous and the configuration sets another level. The settings view's note under the supervisor, which said saving would be refused, says Act turns off when the form is saved. -4. **A hand-written contradiction is still refused.** The validator does - not change: a file that says Act and a lower level in one breath states - two things, and the product does not pick one. +4. **A hand-written contradiction is refused where it would do something, + and opens where it can be put right.** Found in the owner's check: the + rule was part of reading a file, so a file written by hand with Act and + a lower level could not even open in its Harness Settings, the one + place that would fix it. It moves to `actWithoutAutonomousProblem`, + checked where a change's file is written and where it resolves for a + run (which names both ways out), and not where it is read. The + settings view then opens it, says Act turns off on saving, and offers + Save with nothing else changed. The product still does not pick for a + file that says both: it refuses to run it and to write it. ## Risks / Trade-offs diff --git a/openspec/changes/applying-a-configuration-turns-act-off/proposal.md b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md index 796d125b..18bd04fa 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/proposal.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/proposal.md @@ -25,9 +25,10 @@ follow, and are refused only where refusing is truly needed. Autonomous restores what was set. - Every surface that does this says so where it says what was applied or saved. -- A file written by hand that sets Act without Autonomous is still - refused: nothing chose between the two, and guessing which was meant - would act on a guess. +- A file written by hand that sets Act without Autonomous is refused + where it would act - a run - and where it is written, but it opens in + its Harness Settings, which put it right on saving: nothing chose + between the two, and only a person's save does. ## Capabilities diff --git a/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md b/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md index 66fefc86..94fd14b7 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/specs/agentic-harness/spec.md @@ -7,8 +7,12 @@ change's file has `supervisor.mode: "act"` and the configuration sets an autonomy level other than `autonomous`. The written file SHALL instead carry no `supervisor.mode`, SHALL keep the change's `supervisor.fallback`, `allowCostIncrease` and `allowProviderChange`, and the surface that -applied it SHALL say that Act was turned off and why. A change's file -written by hand with `act` and another level SHALL still be refused. +applied it SHALL say that Act was turned off and why. + +A change's file that sets `act` with another level SHALL be refused where +it is written and where it resolves for a run, naming how to put it +right, and SHALL NOT be refused where it is read, so that its Harness +Settings open. #### Scenario: A named configuration over a change under Act @@ -21,6 +25,13 @@ written by hand with `act` and another level SHALL still be refused. #### Scenario: A hand-written contradiction -- **WHEN** a change's `harness.json` is written with `supervisor.mode: - "act"` and `autonomyLevel: "semi-autonomous"` -- **THEN** the write is refused, naming the rule, as before +- **WHEN** a change's `harness.json` with `supervisor.mode: "act"` and + `autonomyLevel: "semi-autonomous"` is written through the product +- **THEN** the write is refused, naming the rule + +#### Scenario: The same contradiction, written by hand + +- **WHEN** such a file is already on disk +- **THEN** reading it succeeds, and resolving it for a run fails, saying + to save the change's Harness Settings to turn Act off or to set + `autonomyLevel: "autonomous"` diff --git a/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md b/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md index e4f0eb35..7293370b 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/specs/shared-ui/spec.md @@ -14,3 +14,10 @@ fallback agents and allowances. Saving SHALL NOT be refused for it. Harness Settings and is saved - **THEN** the note under the supervisor said Act turns off on saving, and the saved file has no `supervisor.mode` and the same fallback agents + +#### Scenario: A file written by hand with Act and another level + +- **WHEN** a change's Harness Settings are opened on a file that sets + `act` with `semi-autonomous` +- **THEN** they open, say Act turns off on saving, and offer Save without + any other edit; saving writes the file without `supervisor.mode` diff --git a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md index 1f608549..4b8d9b1b 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md @@ -21,6 +21,14 @@ person can follow, and are refused only where truly needed. Every change-scope configuration over a change under Act is written and accepted by `writeChangeHarnessConfig` (`harness-config.test.ts`), with no mode and the fallback kept (`harness-templates.test.ts`). +- [x] 1.3 Found in the owner's check of 4.3: a file written by hand with + Act and another level did not open in its Harness Settings ("Load + failed"), since the rule was part of reading it. `harness-config.ts`: + `actWithoutAutonomousProblem`, checked on write and on resolve, not on + read (design.md decision 4); the settings view offers Save on such a + file with nothing else changed. Tests: `harness-config.test.ts` (read + succeeds, resolve refuses and names the way out), 267 passed with + `harness-templates`; `ChangeHarnessSettingsView.test.tsx`, 32 passed. ## 2. Surfaces @@ -49,7 +57,9 @@ person can follow, and are refused only where truly needed. on its own where the root run would exceed a background limit. Typecheck clean; lint 0 errors (3 warnings in lines this change did not touch); seven script tests pass. Core 2101, core-git-subprocess 67, - cli 201, webui 713, server 122, extension 503 passed. + cli 201, webui 713, server 122, extension 503 passed. After 1.3: core + 2102, core-git-subprocess 67, cli 201, webui 714, server 122, extension + 503 passed; typecheck and lint as before. - [x] 4.2 `openspec validate applying-a-configuration-turns-act-off --strict`, and the merge gate with the worktree's absolute path as `--cwd`. diff --git a/packages/core/src/harness-config.test.ts b/packages/core/src/harness-config.test.ts index 39f3403a..eb186d42 100644 --- a/packages/core/src/harness-config.test.ts +++ b/packages/core/src/harness-config.test.ts @@ -1118,6 +1118,19 @@ describe("supervisor act", () => { .rejects.toThrow(/needs autonomyLevel "autonomous" in the same file/); }); + // applying-a-configuration-turns-act-off: a hand-written contradiction + // opens, so it can be put right in its Harness Settings, and nothing runs + // on it. + it("reads a hand-written Act without Autonomous, and refuses to resolve it for a run", async () => { + const root = await temporaryRoot(); + const file = path.join(root, "openspec", "changes", "demo", "harness.json"); + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(file, JSON.stringify({ autonomyLevel: "semi-autonomous", maxStageAttempts: 2, supervisor: act }), "utf8"); + + expect((await readChangeHarnessConfig(root, "demo"))?.supervisor?.mode).toBe("act"); + await expect(resolveHarnessConfig(root, "demo")).rejects.toThrow(/Open the change's Harness Settings and save to turn Act off/); + }); + it("refuses the allowances in the workspace's file", async () => { const root = await temporaryRoot(); await expect(writeGlobalHarnessConfig(root, { supervisor: { allowCostIncrease: true } } as never)) diff --git a/packages/core/src/harness-config.ts b/packages/core/src/harness-config.ts index 5c0db640..77199427 100644 --- a/packages/core/src/harness-config.ts +++ b/packages/core/src/harness-config.ts @@ -150,6 +150,18 @@ export function resolveSupervisor(value: HarnessSupervisor | undefined): Resolve /** Why a resolved configuration's `act` cannot act, or `undefined`. Checked * where the configuration resolves, because the attempts it spends may come * from the other file (the-supervisor-changes-agents). */ +/** Why a change's own file sets an `act` it cannot have, or `undefined`: + * Act needs `autonomyLevel: "autonomous"` in the same file (ADR 0018). + * Refused where the file is written and where it resolves for a run, and + * not where it is read: a file written by hand that says both has to open + * in its Harness Settings to be put right there + * (applying-a-configuration-turns-act-off). */ +export function actWithoutAutonomousProblem(own: Partial | undefined): string | undefined { + if (own?.supervisor?.mode !== "act" || own.autonomyLevel === "autonomous") return undefined; + return 'supervisor.mode "act" needs autonomyLevel "autonomous" in the same file: under a level that asks a person, the person decides.' + + " Open the change's Harness Settings and save to turn Act off, or set autonomyLevel \"autonomous\""; +} + export function supervisorActProblem(config: Pick): string | undefined { if (config.supervisor?.mode !== "act") return undefined; const attempts = config.maxStageAttempts ?? 1; @@ -909,7 +921,6 @@ const SUPERVISOR_KEYS = ["mode", "silentAfterSeconds", "waitingAfterSeconds", "f function assertValidSupervisor( value: unknown, isPerChangeFile: boolean, - autonomyLevel: HarnessAutonomyLevel | undefined, ): asserts value is HarnessSupervisor | undefined { if (value === undefined) return; if (typeof value !== "object" || value === null || Array.isArray(value)) { @@ -928,9 +939,6 @@ function assertValidSupervisor( if (!isPerChangeFile) { throw new InvalidHarnessConfigError('supervisor.mode "act" is set in a change\'s own harness.json, never for the whole workspace: it may start another agent unattended'); } - if (autonomyLevel !== "autonomous") { - throw new InvalidHarnessConfigError('supervisor.mode "act" needs autonomyLevel "autonomous" in the same file: under a level that asks a person, the person decides'); - } } for (const key of ["silentAfterSeconds", "waitingAfterSeconds"] as const) { const seconds = record[key]; @@ -1182,7 +1190,7 @@ function assertValidHarnessConfigInput( assertValidReviewGate(input.reviewGate, isPerChangeFile); assertValidCheckpoints(input.checkpoints, isPerChangeFile); assertValidHints((input as { hints?: unknown }).hints); - assertValidSupervisor((input as { supervisor?: unknown }).supervisor, isPerChangeFile, input.autonomyLevel); + assertValidSupervisor((input as { supervisor?: unknown }).supervisor, isPerChangeFile); assertValidMaxCost((input as { budget?: { maxCost?: unknown } }).budget?.maxCost); const allowAgentMessages = (input as { allowAgentMessages?: unknown }).allowAgentMessages; if (allowAgentMessages !== undefined && typeof allowAgentMessages !== "boolean") { @@ -1490,6 +1498,12 @@ export async function resolveHarnessConfig(workspaceRoot: string, changeName?: s } }); + // Act without Autonomous in the change's own file is read, so its + // settings can open, and refused here, so nothing runs on it + // (applying-a-configuration-turns-act-off). + const contradiction = actWithoutAutonomousProblem(override); + if (contradiction !== undefined) throw new InvalidHarnessConfigError(contradiction); + // `act` spends attempts, which may be set in either file // (the-supervisor-changes-agents). const actProblem = supervisorActProblem(merged); @@ -1525,5 +1539,7 @@ export async function writeChangeHarnessConfig( ): Promise { const migrated = migrateLegacyDispatchInConfig(config); assertValidHarnessConfigInput(migrated.value, true); + const contradiction = actWithoutAutonomousProblem(migrated.value as Partial); + if (contradiction !== undefined) throw new InvalidHarnessConfigError(contradiction); await writeJsonFile(changeHarnessConfigPath(workspaceRoot, changeName), migrated.value); } diff --git a/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx b/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx index b02c6e44..5f004f28 100644 --- a/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx +++ b/packages/webui/src/components/ChangeHarnessSettingsView.test.tsx @@ -371,6 +371,28 @@ describe("ChangeHarnessSettingsView — saving", () => { expect(saved.supervisor).toEqual({ fallback: { apply: ["local-llm-acp"] }, allowCostIncrease: true }); }); + it("opens a file written by hand with Act and another level, and saving puts it right", async () => { + const api = createApi({ + readChangeOverride: vi.fn().mockResolvedValue({ + stepAgents: {}, + autonomyLevel: "semi-autonomous", + maxStageAttempts: 2, + supervisor: { mode: "act", fallback: { apply: ["local-llm-acp"] } }, + }), + }); + await renderLoaded(api); + + expect(chosen("Change supervisor")).toBe("act"); + expect(screen.getByTestId("change-supervisor-act-off").textContent).toContain("Act turns off when this is saved"); + // Nothing else to change: saving it as it stands is what puts it right. + expect(saveButton()).not.toBeDisabled(); + fireEvent.click(saveButton()); + + await waitFor(() => expect(api.writeChangeOverride).toHaveBeenCalled()); + const saved = (api.writeChangeOverride as ReturnType).mock.calls[0]![1] as Record; + expect(saved.supervisor).toEqual({ fallback: { apply: ["local-llm-acp"] } }); + }); + it("applies a named configuration over Act, and says Act is off", async () => { const api = createApi({ readChangeOverride: vi.fn().mockResolvedValue({ diff --git a/packages/webui/src/components/ChangeHarnessSettingsView.tsx b/packages/webui/src/components/ChangeHarnessSettingsView.tsx index 1d3111b7..28548091 100644 --- a/packages/webui/src/components/ChangeHarnessSettingsView.tsx +++ b/packages/webui/src/components/ChangeHarnessSettingsView.tsx @@ -204,8 +204,12 @@ export function ChangeHarnessSettingsView( return findHarnessConfigLimits({ ...global, ...ownRest, stepAgents: resolvedStepAgents }); }, [global, override, resolvedStepAgents]); + // A saved file that says Act under another level - written by hand - is + // put right by saving it as it stands, so there is something to save + // (applying-a-configuration-turns-act-off). + const actToTurnOff = override?.supervisor?.mode === "act" && autonomyLevel !== "autonomous"; const dirty = savedSnapshot !== null && override !== undefined - && snapshotOf(override, forms, autonomyLevel, reviewGateMode, runBudget) !== savedSnapshot; + && (actToTurnOff || snapshotOf(override, forms, autonomyLevel, reviewGateMode, runBudget) !== savedSnapshot); /** Applies a named configuration against what the change resolves to, * through the one core function every surface applies one through, and From f79376a23d02c8c90cfc538d42ccbf294704cead Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Tue, 6 Oct 2026 09:37:25 +0300 Subject: [PATCH 4/4] docs(openspec): close 4.3 of applying-a-configuration-turns-act-off (checked by the owner) Co-Authored-By: Claude Opus 5.5 --- .../applying-a-configuration-turns-act-off/tasks.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md index 4b8d9b1b..093b31f4 100644 --- a/openspec/changes/applying-a-configuration-turns-act-off/tasks.md +++ b/openspec/changes/applying-a-configuration-turns-act-off/tasks.md @@ -65,6 +65,12 @@ person can follow, and are refused only where truly needed. `--cwd`. Valid; the gate with `--cwd C:/Prog/.worktrees/OpenSpec-UI/applying-a-configuration-turns-act-off` reports only 4.3 open (Human-only). -- [ ] 4.3 **Human-only**: in the owner's workspace, apply a named +- [x] 4.3 **Human-only**: in the owner's workspace, apply a named configuration to a change under Act, and change its autonomy level in its Harness Settings; both apply, and say Act is off. + Closed by the owner on 2026-10-06 in an Extension Development Host on a + copy of their workspace's changes: a named configuration applied and an + autonomy level changed over Act, both saved and said Act is off; Act + came back with its fallbacks; the hand-written contradiction opened and + was put right on Save. Ticked on the owner's word in chat: everything + works, close it.