@timurproko/a1 0.1.8-dev.447 → 0.1.8-dev.459

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +9 -0
  2. package/dist/composition/owned-ui.js +12 -0
  3. package/dist/contracts/owned-ui/model.d.ts +12 -0
  4. package/dist/features/owned-ui/settings-app.d.ts +1 -0
  5. package/dist/features/owned-ui/settings-app.js +129 -17
  6. package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -0
  7. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +2 -0
  8. package/dist/integrations/pi/components/upstream/components/owned-editor.js +17 -0
  9. package/dist/integrations/pi/engine/adapter.d.ts +2 -0
  10. package/dist/integrations/pi/engine/adapter.js +25 -6
  11. package/dist/integrations/pi/engine/settings-effects.d.ts +1 -1
  12. package/dist/integrations/pi/engine/settings-effects.js +1 -1
  13. package/dist/integrations/pi/session-ui/quit-outro-effects.d.ts +29 -0
  14. package/dist/integrations/pi/session-ui/quit-outro-effects.js +242 -0
  15. package/dist/integrations/pi/session-ui/quit-outro.d.ts +38 -0
  16. package/dist/integrations/pi/session-ui/quit-outro.js +98 -0
  17. package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
  18. package/dist/integrations/pi/session-ui/session-shell.js +65 -5
  19. package/dist/integrations/pi/tui-runtime/adapter.d.ts +10 -2
  20. package/dist/integrations/pi/tui-runtime/adapter.js +53 -4
  21. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
  22. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +12 -0
  23. package/dist/native/darwin-arm64/manifest.json +1 -1
  24. package/dist/native/linux-x64/manifest.json +1 -1
  25. package/dist/native/win32-x64/manifest.json +2 -2
  26. package/dist/native/win32-x64/process-guardian.exe +0 -0
  27. package/dist/ui/components/list-view.js +5 -6
  28. package/dist/ui/components/surface.d.ts +7 -2
  29. package/dist/ui/components/surface.js +13 -3
  30. package/dist/ui/settings/declarations.d.ts +3 -1
  31. package/dist/ui/settings/declarations.js +33 -1
  32. package/dist/ui/settings/migrations.js +20 -0
  33. package/docs/architecture/ui-reference-provenance.md +2 -2
  34. package/docs/ci-release-runbook.md +5 -5
  35. package/docs/local-worktree-cleanup.md +40 -9
  36. package/docs/manual-owned-ui-checkpoint.md +3 -2
  37. package/docs/openspec-archive-automation.md +22 -9
  38. package/docs/validation.md +4 -2
  39. package/package.json +1 -1
@@ -63,6 +63,13 @@ export declare class DamageAwareTerminalAdapter implements PiTuiTerminalPort {
63
63
  get kittyProtocolActive(): boolean;
64
64
  get lastDecision(): PiTuiDamageDecision;
65
65
  get hyperlinkCleanupPending(): boolean;
66
+ /**
67
+ * The rows as last forwarded to the terminal, top to bottom, with styling
68
+ * intact. A row the adapter has not seen since its last invalidation is empty.
69
+ * The quit outro animates over this snapshot because it is what the terminal
70
+ * shows, not what a fresh render would produce.
71
+ */
72
+ presentedRows(): readonly string[];
66
73
  /** Latches the former link rows, including rows whose replacement contains no link. */
67
74
  requestHyperlinkCleanup(rows?: readonly number[]): void;
68
75
  arm(descriptor: PiTuiDamageFrameDescriptor, safety: PiTuiDamageFrameSafety): void;
@@ -40,6 +40,18 @@ export class DamageAwareTerminalAdapter {
40
40
  get kittyProtocolActive() { return this.inner.kittyProtocolActive; }
41
41
  get lastDecision() { return this.#decision; }
42
42
  get hyperlinkCleanupPending() { return this.#cleanupRevision > this.#cleanedRevision; }
43
+ /**
44
+ * The rows as last forwarded to the terminal, top to bottom, with styling
45
+ * intact. A row the adapter has not seen since its last invalidation is empty.
46
+ * The quit outro animates over this snapshot because it is what the terminal
47
+ * shows, not what a fresh render would produce.
48
+ */
49
+ presentedRows() {
50
+ const presented = [];
51
+ for (let row = 1; row <= this.rows; row += 1)
52
+ presented.push(this.#rows.get(row) ?? "");
53
+ return presented;
54
+ }
43
55
  /** Latches the former link rows, including rows whose replacement contains no link. */
44
56
  requestHyperlinkCleanup(rows) {
45
57
  this.#cleanupRevision += 1;
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-17T07:00:33.413Z",
8
+ "builtAt": "2026-09-17T13:26:08.181Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-17T07:00:55.073Z",
8
+ "builtAt": "2026-09-17T13:26:00.541Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-09-17T07:01:11.858Z",
8
+ "builtAt": "2026-09-17T13:27:04.398Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "41a34706be8b325fe7bc342a0e8d62e55962addf7cd22c2a6b37553a9b8c579c",
11
+ "sha256": "f7e5072d6fddb8e0672d8274699e8e47f515d98776ba3bd79b00391fcd38e667",
12
12
  "size": 177664
13
13
  },
14
14
  "provenance": {
@@ -29,14 +29,13 @@ export function renderListRow(row, state, valueColumn, width, theme) {
29
29
  ? `${theme.fg("accent", cursor)}${theme.fg("accent", labelPadded)}`
30
30
  : `${cursor}${theme.plain(labelPadded)}`;
31
31
  const gap = Math.max(2, valueColumn - displayWidth(leftRaw));
32
- // Compatibility: pinned SettingsList gives the selected label and value the same accent
33
- // role. Pointer hover may brighten an unselected value without changing the
34
- // keyboard selection.
32
+ // Rationale: a declared difference from pinned SettingsList, which paints the selected
33
+ // value in the accent too. Here only the cursor and label carry the selection; the
34
+ // value reads the same on every row, and pointer hover brightens it without moving
35
+ // the keyboard selection.
35
36
  const valueHovered = state.hovered && state.region !== "label";
36
37
  const stepper = row.stepper !== undefined && valueHovered;
37
- const value = state.selected
38
- ? theme.fg("accent", row.value)
39
- : valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
38
+ const value = valueHovered ? theme.plain(row.value) : theme.fg("muted", row.value);
40
39
  const minus = stepper
41
40
  ? row.stepper?.lower === true
42
41
  ? state.region === "minus" ? theme.plain("- ") : theme.fg("dim", "- ")
@@ -1,12 +1,17 @@
1
- import { type ScrollbarGeometry } from "./scrollbar.js";
1
+ import { type ScrollbarGeometry, type ScrollbarPresentation } from "./scrollbar.js";
2
2
  import type { UiTheme } from "./theme.js";
3
3
  /** Columns the rail occupies: its own, plus the gap before it. */
4
4
  export declare const RAIL_COLUMNS = 2;
5
5
  export interface RailOptions {
6
6
  /** Rows at the top the rail does not run beside, such as a sticky header. */
7
7
  readonly topInset?: number;
8
+ /** How the rail shows. Absent draws the thin rail whenever the content overflows. */
9
+ readonly presentation?: ScrollbarPresentation;
8
10
  }
9
- /** Draws the rail beside each row, padding the rows to a common width first. */
11
+ /**
12
+ * Draws the rail beside each row, padding the rows to a common width first.
13
+ * A presentation that reserves no space returns the rows untouched.
14
+ */
10
15
  export declare function withScrollbarRail(lines: readonly string[], geometry: ScrollbarGeometry | null, contentWidth: number, theme: UiTheme, options?: RailOptions): readonly string[];
11
16
  /**
12
17
  * What a list shows instead of rows: a mark and a line, both quiet, sitting in
@@ -3,13 +3,23 @@ import { displayWidth } from "./text.js";
3
3
  // Rationale: shared list chrome belongs here rather than in individual screens.
4
4
  /** Columns the rail occupies: its own, plus the gap before it. */
5
5
  export const RAIL_COLUMNS = 2;
6
- /** Draws the rail beside each row, padding the rows to a common width first. */
6
+ /**
7
+ * Draws the rail beside each row, padding the rows to a common width first.
8
+ * A presentation that reserves no space returns the rows untouched.
9
+ */
7
10
  export function withScrollbarRail(lines, geometry, contentWidth, theme, options = {}) {
8
11
  const inset = options.topInset ?? 0;
12
+ const presentation = options.presentation
13
+ ?? { visible: geometry !== null, reservesSpace: true, trackGlyph: "│", thumbGlyph: "│" };
14
+ if (!presentation.reservesSpace)
15
+ return lines;
16
+ const drawn = presentation.visible && geometry !== null;
9
17
  return lines.map((line, offset) => {
10
- const cell = offset < inset || geometry === null
18
+ const cell = offset < inset || !drawn
11
19
  ? " "
12
- : isThumbRow(geometry, offset - inset) ? theme.fg("accent", "│") : theme.fg("dim", "│");
20
+ : isThumbRow(geometry, offset - inset)
21
+ ? theme.fg("accent", presentation.thumbGlyph)
22
+ : theme.fg("dim", presentation.trackGlyph);
13
23
  return `${pad(line, contentWidth)} ${cell}`;
14
24
  });
15
25
  }
@@ -1,4 +1,4 @@
1
- export declare const OWNED_UI_SETTINGS_VERSION = 4;
1
+ export declare const OWNED_UI_SETTINGS_VERSION = 6;
2
2
  export type OwnedUiSettingValue = string | number | boolean;
3
3
  export type OwnedUiSettingApplication = "live" | "restart";
4
4
  export interface OwnedUiSettingDeclaration {
@@ -15,6 +15,8 @@ export interface OwnedUiSettingDeclaration {
15
15
  readonly defaultValue: OwnedUiSettingValue;
16
16
  readonly allowedValues: readonly OwnedUiSettingValue[];
17
17
  }
18
+ /** Playback lengths the quit outro offers, in milliseconds. */
19
+ export declare const QUIT_EFFECT_DURATIONS_MS: readonly number[];
18
20
  export declare const OWNED_UI_SETTING_DECLARATIONS: readonly OwnedUiSettingDeclaration[];
19
21
  export declare function assertOwnedUiSettingDeclarations(declarations: readonly OwnedUiSettingDeclaration[]): void;
20
22
  export declare function findOwnedUiSettingDeclaration(declarations: readonly OwnedUiSettingDeclaration[], id: string): OwnedUiSettingDeclaration | null;
@@ -1,8 +1,22 @@
1
- export const OWNED_UI_SETTINGS_VERSION = 4;
1
+ export const OWNED_UI_SETTINGS_VERSION = 6;
2
2
  const MAX_ID_LENGTH = 64;
3
3
  const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
4
+ /** Declared first so the settings screen opens on it; sections follow first-declaration order. */
5
+ const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
4
6
  const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
7
+ const QUIT_SECTION = Object.freeze({ id: "quit", title: "Quit" });
8
+ /** Playback lengths the quit outro offers, in milliseconds. */
9
+ export const QUIT_EFFECT_DURATIONS_MS = Object.freeze(Array.from({ length: 18 }, (_, index) => 300 + index * 100));
5
10
  export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
11
+ Object.freeze({
12
+ id: "quitAnimation",
13
+ label: "Exit animation",
14
+ section: GENERIC_SECTION,
15
+ description: "Play the quit effect when the session quits. Off returns to the terminal immediately.",
16
+ application: "live",
17
+ defaultValue: true,
18
+ allowedValues: Object.freeze([true, false]),
19
+ }),
6
20
  Object.freeze({
7
21
  id: "scrollbarAppearance",
8
22
  label: "Scrollbar mode",
@@ -48,6 +62,24 @@ export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
48
62
  defaultValue: 100,
49
63
  allowedValues: Object.freeze([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]),
50
64
  }),
65
+ Object.freeze({
66
+ id: "quitEffect",
67
+ label: "Effect",
68
+ section: QUIT_SECTION,
69
+ description: "Animation played over the last screen when the session quits.",
70
+ application: "live",
71
+ defaultValue: "fall",
72
+ allowedValues: Object.freeze(["fall", "dissolve", "starburst", "waves"]),
73
+ }),
74
+ Object.freeze({
75
+ id: "quitEffectDurationMs",
76
+ label: "Duration",
77
+ section: QUIT_SECTION,
78
+ description: "Milliseconds the quit animation plays before the terminal is restored.",
79
+ application: "live",
80
+ defaultValue: 800,
81
+ allowedValues: QUIT_EFFECT_DURATIONS_MS,
82
+ }),
51
83
  Object.freeze({
52
84
  id: "promptSuggestions",
53
85
  label: "Prompt suggestions",
@@ -29,6 +29,26 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
29
29
  return { ...values };
30
30
  },
31
31
  }),
32
+ Object.freeze({
33
+ to: 5,
34
+ description: "Introduce the quit outro effect and duration with prototype defaults.",
35
+ migrate(values) {
36
+ return { ...values };
37
+ },
38
+ }),
39
+ Object.freeze({
40
+ to: 6,
41
+ description: "Move the disabled quit effect into the exit-animation toggle.",
42
+ migrate(values) {
43
+ // Invariant: a profile that chose off keeps quitting without an animation; the
44
+ // effect it stored is no longer a choice, so it resolves to the default.
45
+ if (values.quitEffect !== "off")
46
+ return { ...values };
47
+ const migrated = { ...values, quitAnimation: false };
48
+ delete migrated.quitEffect;
49
+ return migrated;
50
+ },
51
+ }),
32
52
  ]);
33
53
  export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
34
54
  const firstProduced = currentVersion - migrations.length + 1;
@@ -29,7 +29,7 @@ its own `core` facade layer; A1 is a product, so the port adapts imports and kee
29
29
  | `ui-components/list-block.ts` — sticky scroll | `settings/impl.ts` — `stickyHeaderGroup`, `topPaddingRows`, `visibleRowCountAt`, `clampScrollForView` | Same reservation arithmetic and two-pass reveal. |
30
30
  | `ui-components/mouse.ts` | `core/panes/sgr-mouse.ts` | Same SGR decoding and per-call regex reset; A1 emits its own event shape. |
31
31
  | `ui-components/mouse.ts` — tracking sequences | `core/host/pi/providers/host-bridge-surface.ts` | Mouse modes only. A1 does not take the alternate screen, because the Pi TUI owns the screen A1 renders through. |
32
- | `features/owned-ui/settings-app.ts` — section layout, pointer controls, and scrolling | `settings/impl.ts` — `settingsValueColumn`, block navigation, sticky sections, and pointer hit regions | Setting discovery is A1's own A1/Agent section model. Section navigation and pointer-only numeric controls follow the A1 reference; explicit `/` search, ruled shared input, shortcut-derived hints, hidden description rows, configured wheel cadence, and distinct floating scalar menus are reviewed owned interactions. |
32
+ | `features/owned-ui/settings-app.ts` — section layout, pointer controls, and scrolling | `settings/impl.ts` — `settingsValueColumn`, block navigation, sticky sections, and pointer hit regions | Setting discovery is A1's own A1/Agent section model. Section navigation and pointer-only numeric controls follow the A1 reference; explicit `/` search, ruled shared input, shortcut-derived hints, hidden description rows, configured wheel cadence, a rail presented through the shared scrollbar settings, `Ctrl+Home`/`Ctrl+End` boundary jumps, and distinct floating scalar menus are reviewed owned interactions. |
33
33
 
34
34
  ## Ported from the pinned engine
35
35
 
@@ -39,7 +39,7 @@ its own `core` facade layer; A1 is a product, so the port adapts imports and kee
39
39
  | `pi-engine-adapter/adapter.ts`, `engine/workflows.ts`, and `pi-session-ui/session-shell.ts` — interactive command outcomes | Pi 0.84.2 `modes/interactive/interactive-mode.ts`, `core/model-resolver.ts`, and `config.ts` at commit `914cf1472e715297caa30db4b9535d534a9eb718` | Preserves route-specific status/warning/error semantics, authentication partial-success sequences, stored-credential logout wording, fork/clone empty states, import context/recovery, share URL and failure/cancellation behavior, and post-login warning lifetime. A1 deliberately returns recoverable failed workflow results instead of adopting Pi's process-owning shutdown/exit for fatal `/new`, `/resume`, and `/import` outcomes. Focused outcome evidence is in `test/integrations/pi/engine/workflows.test.ts` and `test/integrations/pi/session-ui/session-shell.test.ts`; terminal-cell geometry remains a separate milestone. |
40
40
  | `pi-engine-adapter/settings-integration.ts` — `SETTING_LABELS` | pinned Pi settings selector | Labels and descriptions transcribed so an owned screen reads as the vanilla route words it. Ids are mapped from the selector kebab-case to the exposed camelCase keys. |
41
41
  | `pi-engine/session-integration.ts` and `pi-components/shell-footer-status.ts` — steering queue | pinned Pi interactive mode `onSubmit` and `updatePendingMessagesDisplay` | Steering/follow-up uses `prompt(..., { streamingBehavior })`, allowing Pi to emit the accepted user row, while remaining steering rows preserve Pi's opening spacer, dim `Steering:` labels, dequeue hint, and order before `Working`. |
42
- | `ui-components/list-view.ts`, `dialog-panel.ts`, `value-menu.ts`, and `features/owned-ui/settings-app.ts` — setting presentation | pinned Pi `SettingsSelectorComponent`, Pi TUI `SettingsList`, `SelectList`, and `Input` at `0.84.2` | Cursor, selected label/value accents, unselected muted values, the 30-column label cap, dialog styling, notices, and narrow-width geometry retain pinned semantics. Scalar-menu placement and input remain shared, while A1/Agent grouping, pointer steppers, explicit `/` search, ruled shared input, shortcut-derived status hints, suppressed selected descriptions, `scrollbarSpeed`-driven wheel movement, and the dark floating menu with lighter active row and effective-value check mark are declared product-owned differences. Independent row evidence: `test/features/owned-ui/pinned-settings-presentation-parity.test.ts`; owned interaction evidence: `test/features/owned-ui/settings-app.test.ts`, `test/ui/components/value-menu.test.ts`, and `test/composition/settings-route-host.test.ts`. |
42
+ | `ui-components/list-view.ts`, `dialog-panel.ts`, `value-menu.ts`, and `features/owned-ui/settings-app.ts` — setting presentation | pinned Pi `SettingsSelectorComponent`, Pi TUI `SettingsList`, `SelectList`, and `Input` at `0.84.2` | Cursor and selected label accents, unselected muted values, the 30-column label cap, dialog styling, notices, and narrow-width geometry retain pinned semantics. Scalar-menu placement and input remain shared, while the muted, hover-brightened selected value, A1/Agent grouping, pointer steppers, explicit `/` search, ruled shared input, shortcut-derived status hints, suppressed selected descriptions, `scrollbarSpeed`-driven wheel movement, a list rail that follows `scrollbarAppearance` and `scrollbarStyle` with transcript-style hover and drag, `Ctrl+Home`/`Ctrl+End` boundary jumps, and the dark floating menu with lighter active row and effective-value check mark are declared product-owned differences. Independent row evidence: `test/features/owned-ui/pinned-settings-presentation-parity.test.ts`; owned interaction evidence: `test/features/owned-ui/settings-app.test.ts`, `test/ui/components/value-menu.test.ts`, and `test/composition/settings-route-host.test.ts`. |
43
43
  | `features/owned-ui/project-trust-prompt.ts` — pre-resource selector | pinned Pi `cli/startup-ui.ts`, `cli/project-trust.ts`, and `core/project-trust.ts` at commit `914cf1472e715297caa30db4b9535d534a9eb718` | Uses a fixed, dependency-bounded A1 startup selector rather than importing private CLI modules. It preserves selected-option accent, navigation/accept/reject/cancel semantics, fail-closed behavior, raw-mode restoration, clearing, cursor restoration, and parent-screen restoration before diagnostics. |
44
44
  | `pi-session-ui/session-shell-root.ts` and `session-shell.ts` — fullscreen exit | pinned Pi `InteractiveMode.formatResumeCommand()` and shutdown output at commit `914cf1472e715297caa30db4b9535d534a9eb718` | Re-renders authoritative transcript components with semantic SGR intact, excludes inline-image control payloads and fullscreen-only chrome, restores the terminal first, then emits pinned dim `To resume this session:` wording with `a1`, compact session id, and conditional quoted `--session-dir`. |
45
45
  | `pi-tui-runtime/input-presentation-coordinator.ts` and custom-viewport dock reuse | pinned Pi TUI `TuiBase` input dispatch and immediate-render pending guard at `0.84.2` | Preserves each original terminal delivery and invokes the existing Pi handlers exactly once in order. Bare A1 alone drains finite-grammar text/edit/navigation bursts in one immediate event-loop opportunity so Pi's existing pending guard paints the newest state once; effectful, unknown, protocol, paste, and extension-owned input remains an immediate barrier. Geometry-stable dock frames reuse A1's established transcript viewport, while uncertain geometry and replacement-surface damage still fail closed. Independent evidence is under `test/support/input-responsiveness/`. |
@@ -58,9 +58,9 @@ effect of stable publication, not a trigger.
58
58
 
59
59
  Ordinary type, architecture, unit/contract, and dist checks always run for code changes. Changed-file documentation and rendering run as independent parallel jobs. Rendered shell/component changes select `smoke`; viewport, scheduler, terminal adapter, evidence harness, package identity, and selector changes select `full`; unrelated changes select `none`. The aggregate accepts a skipped modular job only when the current selector requested the skip.
60
60
 
61
- Integration owners declare one cadence in `config/integration-owners.json`. Impact mode selects affected `pull-request` owners; an invalidator, unknown operational path, or manual Development dispatch selects every `pull-request` owner. `exhaustive` owners are never silently skipped or reported as passed: impact and aggregate evidence list them as cadence-deferred, and malformed cadence blocks selection. Full regression and nightly/stable release still execute both cadence classes.
61
+ Integration owners declare one cadence in `config/integration-owners.json`. Impact mode selects affected `pull-request` owners: a changed production path selects its coarse owner, a changed test selects its owner, and a changed file under `test/support/` or `test/fixtures/` selects the owners of the retained tests that import it directly or through other support files (the selection lists those tests as its `shared-support` reason). A support file that no retained test imports, or a test tree the scanner cannot read, falls back to every owner the shared rule declares and records `shared-support-declared`; an invalidator, unknown operational path, or manual Development dispatch selects every `pull-request` owner. An implementation-bound PR body does not force conservative selection; it only removes the documentation-only and version-only shortcuts. The modular matrix itself is derived from the selection by `scripts/release/validation-matrix.mjs`, so inactive jobs are not scheduled at all rather than checking out and exiting early. `exhaustive` owners are never silently skipped or reported as passed: impact and aggregate evidence list them as cadence-deferred, and malformed cadence blocks selection. Full regression and nightly/stable release still execute both cadence classes.
62
62
 
63
- The real three-release `update-predecessor` scenario is exhaustive because four fresh npm installations dominated recent PR critical paths. PR validation retains deterministic predecessor command, lifecycle, fault, fixture, materialization, warmup, package, and update contracts. This permits a real published-history incompatibility to reach `develop` before nightly detects it; nightly failure still blocks publication. For a high-risk release/update change, explicitly dispatch `.github/workflows/full-regression.yml` before merge instead of adding the exhaustive owner back to ordinary Development.
63
+ The real three-release `update-predecessor` scenario is exhaustive because four fresh npm installations dominated recent PR critical paths, and `update-performance` is exhaustive because its assertion is wall-clock timing on a shared runner; the update path's deterministic contracts stay on pull requests through `pi-release-resume` and `package-contracts`. PR validation retains deterministic predecessor command, lifecycle, fault, fixture, materialization, warmup, package, and update contracts. This permits a real published-history incompatibility or update slowdown to reach `develop` before the next exhaustive run detects it. `.github/workflows/full-regression.yml` runs every night at `02:47 UTC` against the `develop` tip with every owner and enforced budgets, independent of publication, so such a regression shows as a failed Full regression run the next morning; nightly publication's own complete validation still blocks publication. For a high-risk release/update change, dispatch Full regression before merge instead of adding an exhaustive owner back to ordinary Development.
64
64
 
65
65
  Development outcomes report each owner/scope invocation separately while sharing authenticated build/package preparation. The aggregate reports setup, scope, job, aggregate-processing, total runner, and runner-critical-path durations. The acceptance targets are at most eight minutes of runner critical path and five minutes for one PR-required scope; an over-target result remains unmet without retries, timeout increases, workload reduction, or mutable installation caches. Hosted queue time is reported separately when available and is not counted as test execution.
66
66
 
@@ -86,9 +86,9 @@ Rendering evidence captures each selected producer/mode/workload matrix once and
86
86
 
87
87
  ## Resource-sensitive fast validation
88
88
 
89
- The authoritative fast-tier declaration identifies tests that repeatedly create temporary repositories, launch child processes, mutate storage, or coordinate release cohorts. The planner removes those files from the parallel remainder and runs each exactly once in its own `vitest-fast-resource-sensitive-*` process with file parallelism disabled. Per-file process isolation prevents one resource-heavy file from consuming another file's unchanged five-second test budget; no timeout override or retry is added. Pull-request, development-package, and complete release plans use the same partition on every platform.
89
+ The authoritative fast-tier declaration identifies tests that repeatedly create temporary repositories, launch child processes, mutate storage, or coordinate release cohorts. The planner removes those files from the parallel remainder and runs them exactly once in one serial `vitest-fast-resource-sensitive` process with file parallelism disabled, on an isolated runner that no other partition shares. One process instead of one per file removes about twenty cold starts from the partition; each file still gets a fresh module context. Pull-request, development-package, and complete release plans use the same partition on every platform.
90
90
 
91
- The partition retains Vitest's five-second default test timeout. It does not add a test, suite, platform, or workflow timeout, and a failure is not retried or converted to success. If a serialized test still approaches five seconds, use `scripts/release/report-resource-sensitive-validation.mjs` to record repeated per-file and test-body timing, then optimize repository setup, subprocess count, storage operations, or release fixtures. Do not increase a timeout to create margin.
91
+ The partition runs under an explicit `--testTimeout=30000`, the same hang bound the other explicit fast-tier invocations use. That bound is a hang detector, not a performance gate: shared Windows runners vary by a factor of two or more for identical work, and a fixed five-second wall-clock limit failed passing suites on runner noise. Per-test durations remain in the reporter evidence; `scripts/release/report-resource-sensitive-validation.mjs` records repeated executions and lists every test body above five seconds under `slowTests` so a real slowdown is visible without failing the pull request. A failure is still not retried or converted to success, and the bound is not raised to create margin.
92
92
 
93
93
  Inspect the partition without running tests:
94
94
 
@@ -147,7 +147,7 @@ validation; malformed, mixed, renamed, stale, or unavailable inputs fail closed.
147
147
  A legitimate generated baseline update remains outside the allowlist and follows the
148
148
  manually accepted mixed/code path.
149
149
 
150
- A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, the same branch conservatively synchronizes deltas and stages the dated archive plus conditional acceptance manifest. Add one to three plain implementation-specific bullets under `## Acceptance`, mark the finalized PR ready, and require exact-head CI. Auto-merge remains disabled: an authorized maintainer's manual merge accepts the listed scenarios and atomically integrates implementation, specs, and archive. No acceptance or archive follow-up PR is created. Closing an unmerged draft integrates nothing; cleanup still needs separate approval. See [delivery and archive handoff](openspec-archive-automation.md) for commands and legacy compatibility.
150
+ A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, add one to three plain implementation-specific bullets under `## Acceptance` and mark the PR ready; the trusted `OpenSpec finalization` workflow conservatively synchronizes deltas, stages the dated archive plus conditional acceptance manifest, and commits them to the same branch, and exact-head CI validates that head. Auto-merge remains disabled: an authorized maintainer's manual merge accepts the listed scenarios and atomically integrates implementation, specs, and archive. No acceptance or archive follow-up PR is created. Closing an unmerged draft integrates nothing; cleanup still needs separate approval. See [delivery and archive handoff](openspec-archive-automation.md) for commands and legacy compatibility.
151
151
 
152
152
  ## Numbered development previews
153
153
 
@@ -4,9 +4,35 @@ Local cleanup completes the delivery order in [the archive runbook](openspec-arc
4
4
 
5
5
  The implementation is repository tooling, not part of the installed A1 product. It requires Node, Git, and GitHub read access; the explicit closed-unmerged discard operation additionally requires authenticated permission to delete its exact remote topic ref. No product build, dependency installation, interactive UI, or OS-service provisioning is needed. It supports this repository's `origin` on github.com, via HTTPS or SSH.
6
6
 
7
+ ## Hand-off and sweep: the ordinary agent path
8
+
9
+ The delivering agent's session usually ends before the maintainer merges, so cleanup is split into two commands that never need the same session. When the validated PR is handed to the maintainer, and again after any repair push, the owning agent parks the worktree from the primary checkout:
10
+
11
+ ```bash
12
+ node scripts/governance/local-worktree-cleanup.mjs handoff --repo D:/Git/a1 --path D:/Git/a1/.worktrees/example-task --change example-change --pr 123
13
+ ```
14
+
15
+ `handoff` registers the exact worktree if it is not registered yet (or reclaims the existing released entry), records its current HEAD and branch, applies the central disposable policy, and releases it. It evaluates nothing, deletes nothing, and does not enable the queue. That release is the candidate-scoped cleanup authorization for this worktree and its topic ref, exercised only once the merge, archive, validation, and remote-ref gates verify later; it never becomes discard authority for a PR that is later closed unmerged. It is idempotent per worktree: repeating it updates the recorded head instead of adding a second registration. Tracked, staged, unstaged, or untracked content outside the disposable roots blocks with `worktree-content` and the affected paths, because such content means unpushed work; unknown ignored content is judged at removal time, as it is for `complete`. A primary, foreign, replaced, or branch-changed path blocks with a named reason. A worktree that a session registered with the low-level `register` command and still owns is released by `handoff` (or `complete`) when `LOCAL_CLEANUP_OWNER_TOKEN` holds that registration's token; without it both block with `owned-worktree`, and a separate `release` is needed.
16
+
17
+ Every delivery session then starts, before it creates a worktree, with one bounded sweep from the primary checkout, and runs it again when it verifies or is told of a merge:
18
+
19
+ ```bash
20
+ node scripts/governance/local-worktree-cleanup.mjs sweep --repo D:/Git/a1
21
+ ```
22
+
23
+ `sweep` evaluates every released registration in one pass under the queue limits (100 registrations, 500 remote requests, a durable round-robin cursor) with a 180-second elapsed budget, since each merged candidate costs three evidence loads, and completes each candidate whose PR is verified merged using exactly the `complete` safeguards. It needs no `enable` and starts no process: its authority is each candidate's release. A candidate whose PR is open, draft, or not yet finalized reports `pending`; a candidate whose PR closed without merge reports `awaiting-discard` and is never touched; blockers report their exact reason. An old stop sentinel does not prevent a sweep, but `disable` run while a sweep is executing stops it before its next destructive step. If another session holds the mutation lock the sweep reports `mutation-busy` and the agent relays it as deferred rather than waiting. The JSON report carries a `lines` array with one relayable line per candidate and pruned branch, for example `#458 close-absent-and-skewed-cleanup: removed [worktree-removed, local-ref-removed]`. Pending or blocked results never delay the new delivery.
24
+
25
+ ### Accepted ancestry
26
+
27
+ Since the `OpenSpec finalization` workflow pushes its commit onto the PR branch after the agent's last push, the registered head is normally one commit behind the merged head. Cleanup therefore accepts a registered head, live worktree HEAD, or local topic-ref tip that equals the merged PR head or is one of its ancestors on GitHub, verified with the compare API: every commit reachable from such a tip is reachable from a head the maintainer accepted, so nothing is lost. A tip that holds a commit outside the merged head, or a commit GitHub does not know, still blocks with `candidate-head-association`, `worktree-identity-changed`, or `local-ref-advanced`, and the branch attachment must still be the registered one. Ref deletion reads the tip immediately before deleting, requires it to be accepted, and uses it as the compare-and-delete expectation. Ancestry of `develop` is never used, because the repository squash-merges.
28
+
29
+ ### Merged branch pruning
30
+
31
+ The sweep also prunes local topic branches that have no live registration, such as the branch left behind when a worktree was deleted by hand. A branch is deleted only when all of these hold: its name follows `type/short-description` and is not `develop` or another protected or reserved name; no worktree has it checked out; the same-repository pull-request lookup by that head ref name returns at least one PR merged into `develop` and no open PR; the tip equals or is an ancestor of the most recent such merged PR's head; and `origin` no longer has the ref. Deletion is compare-and-delete against the tip reread immediately before. Everything else is reported and kept with `branch-no-pull-request`, `branch-open-pull-request`, `branch-closed-pull-request`, `branch-unmerged-commits`, `branch-checked-out`, or `branch-remote-present`. Branch pruning never deletes remote refs, worktrees, or registrations, and a preview never prunes.
32
+
7
33
  ## Standard completed-delivery command
8
34
 
9
- After authorized merge, accepted/archive verification, and remote topic-ref removal, the owning agent runs one command from the primary checkout:
35
+ The exact-candidate form remains for a session that is still alive at merge time or wants to finish one candidate by name. After authorized merge, accepted/archive verification, and remote topic-ref removal, the owning agent runs one command from the primary checkout:
10
36
 
11
37
  ```bash
12
38
  node scripts/governance/local-worktree-cleanup.mjs complete \
@@ -18,9 +44,9 @@ node scripts/governance/local-worktree-cleanup.mjs complete \
18
44
 
19
45
  `complete` is explicit cleanup authorization for that exact candidate. It creates and releases an exact registration when needed, applies the repository-owned generated-path policy, verifies live merge/archive/CI/ref evidence, evaluates only that candidate, uses journaled non-force Git removal, deletes only the unchanged local topic ref, and leaves persistent watcher authority unchanged. Repeating it reports the completed candidate as already absent. Existing conflicting ownership, identity drift, unavailable evidence, or unknown content remains blocking.
20
46
 
21
- The central disposable policy is `node_modules`, `dist`, `.builds`, `.artifacts/openspec-archive`, `.artifacts/validation`, `native/process-guardian/target`, and `native/terminal-host/target`. The artifact roots contain repository-generated finalization and validation reports; the two native roots contain repository-generated Cargo output. Each encountered path must be ignored and stay inside the exact worktree with no link, special file, or nested repository boundary. Authority is component-exact: `.artifacts`, sibling directories such as `.artifacts/other`, near matches such as `.artifacts/validation-user`, arbitrary `target` directories, and sibling native projects remain blocking. Tracked/staged/unstaged/untracked content and every unknown ignored path still block. A tracked regular `.gitmodules` file alone is ordinary content; actual nested `.git` metadata, gitlinks, configured submodules, and submodule changes block. Ordinary content and these approved generated roots are traversed under separate finite entry allowances, so a normal dependency installation does not consume the ordinary source-tree allowance; both allowances retain the same deadline and content-boundary checks.
47
+ The central disposable policy is `node_modules`, `dist`, `.builds`, `.artifacts`, `native/process-guardian/target`, and `native/terminal-host/target`. The `.artifacts` root is the repository's ignored generated-artifact root (finalization and validation reports, packed candidates, agent-written logs and diffs); the two native roots contain repository-generated Cargo output. Registrations that still name `.artifacts/openspec-archive` or `.artifacts/validation` stay valid and are widened to the root on the next `complete`. Each encountered path must be ignored and stay inside the exact worktree with no link, special file, or nested repository boundary. Authority is component-exact: near matches such as `.artifacts-user` or `artifacts`, arbitrary `target` directories, and sibling native projects remain blocking. Tracked/staged/unstaged/untracked content and every unknown ignored path still block. A tracked regular `.gitmodules` file alone is ordinary content; actual nested `.git` metadata, gitlinks, configured submodules, and submodule changes block. Ordinary content and these approved generated roots are traversed under separate finite entry allowances, so a normal dependency installation does not consume the ordinary source-tree allowance; both allowances retain the same deadline and content-boundary checks. Once those checks pass, cleanup deletes the declared disposable roots itself with a bounded retry for transient Windows sharing violations before handing the worktree to Git, so non-force Git removal only has to delete tracked content. A root that stays locked after the retry budget reports `blocked` with `disposable-path-locked` and the root's path; the worktree, its `.git` pointer, and its journal are untouched, so stop the process holding the handle and rerun the same command.
22
48
 
23
- Agents do not manually remove generated content, call `git worktree remove`, or delete the local branch after delivery. The JSON result is authoritative: report success only for `removed` or verified `already-absent`; otherwise retain the worktree and report the exact blocker. Legacy roles can supply separate `--source-pr`, `--candidate-pr`, and `--role` values.
49
+ Agents do not manually remove generated content, call `git worktree remove`, or delete the local branch after delivery. The JSON result is authoritative: report success only for `removed` or verified `already-absent`; otherwise retain the worktree and report the exact blocker. A handed-off entry whose worktree was deleted by hand completes through its journal, and its branch is deleted under the accepted-ancestry rule. Legacy roles can supply separate `--source-pr`, `--candidate-pr`, and `--role` values.
24
50
 
25
51
  ## Explicit closed-unmerged discard
26
52
 
@@ -54,7 +80,7 @@ Remote reads use `GH_TOKEN`/`GITHUB_TOKEN`, otherwise existing `gh auth token --
54
80
 
55
81
  ## Explicit queue/watch enablement
56
82
 
57
- The exact-candidate `complete` command does not enable or scan the persistent queue. Only after the maintainer separately authorizes queue/watch activation, use the reviewed tool in the primary/stable checkout outside `.worktrees/`:
83
+ Neither `handoff`, `sweep`, nor the exact-candidate `complete` command enables the persistent queue; `sweep` scans released registrations under its own authority, while `once`/`watch` remain the separately enabled background route. Only after the maintainer separately authorizes queue/watch activation, use the reviewed tool in the primary/stable checkout outside `.worktrees/`:
58
84
 
59
85
  ```bash
60
86
  node scripts/governance/local-worktree-cleanup.mjs enable --repo D:/Git/a1
@@ -123,24 +149,29 @@ A session crash does not release ownership. After separately confirming that the
123
149
  node scripts/governance/local-worktree-cleanup.mjs recover --repo D:/Git/a1 --id REGISTRATION_ID --generation CURRENT_GENERATION --confirm-stopped
124
150
  ```
125
151
 
126
- Recovery never releases or deletes. The generation must still match. A Git worktree lock remains a veto. A leftover `mutation.lock` from a crashed cleanup process also remains a veto: stop all cleanup/claim processes, inspect the lock and `state.json`, and obtain explicit approval before manually removing that identified lock. There is no automatic stale-lock eviction or PID-based deletion.
152
+ Recovery never releases or deletes. The generation must still match. A Git worktree lock remains a veto. A leftover `mutation.lock` from a killed cleanup process is evicted only on proof: the holder writes its PID and refreshes a heartbeat in the lock every five seconds, and a later `handoff`, `sweep`, `complete`, `discard`, claim, or queue pass evicts the lock only when that heartbeat (or, for an old lock without one, the file's modification time) is more than two minutes old and the PID no longer exists. A live, unprobeable, or own PID keeps the lock, as does a fresh or unreadable-but-fresh file, and the operation reports `mutation-busy`. Each eviction is journaled as `lock-evicted-<time>-<id>.json` beside `state.json` with the evicted record; it releases no ownership and advances no journal step. A lock that stays `mutation-busy` therefore has a live holder: wait for it or stop that process first.
127
153
 
128
154
  ## Outcomes and interruption handling
129
155
 
130
156
  State, journals, stop controls, and execution reports live in `<git-common-dir>/local-worktree-cleanup/`, outside removable checkouts. `status` omits owner-token hashes. Reports contain identities, local blockers, performed steps, and coverage, never file contents or credentials. Completed reports are retained for 30 days subject to a 10 MiB cap; unresolved queue/journal records are not evicted.
131
157
 
132
158
  - `eligible`: preview passed the gates; nothing was removed.
133
- - `pending`: archive/ref prerequisites have not finished.
159
+ - `pending`: archive/ref prerequisites have not finished, or the handed-off PR is still open.
160
+ - `awaiting-discard`: the handed-off PR closed without merge; only the explicit `discard` command may act.
134
161
  - `blocked`: ownership, content, path, provenance, authentication, or identity needs attention.
135
162
  - `unmanaged`: no local registration; no automatic adoption.
136
163
  - `removed`: worktree and eligible local-ref operations were verified.
137
164
  - `already-absent`: a completed journal's path/ref are still absent.
138
- - `partial`: a destructive step began but all cleanup could not be verified.
165
+ - `partial`: a destructive step began but all cleanup could not be verified; the same command resumes it.
139
166
  - `deferred`: a bounded pass or concurrent mutation owner prevented evaluation.
140
167
 
141
- Non-force Git removal is the only worktree deletion operation. Local topic-ref deletion then compares the exact old SHA and refuses refs checked out elsewhere. `develop`, primary/current directories, changed heads, and active sessions are protected. Normal removal retires its own Git worktree registration; unrelated stale/missing registrations are never globally pruned.
168
+ Non-force Git removal is the only operation that deletes tracked content from an intact worktree. Local topic-ref deletion then compares the tip read immediately before, which must be the journaled head or an accepted ancestor of the merged head, and refuses refs checked out elsewhere. `develop`, primary/current directories, changed heads, and active sessions are protected. Normal removal retires its own Git worktree registration; unrelated stale/missing registrations are never globally pruned.
169
+
170
+ A released worktree whose directory was deleted by hand before cleanup ran is finished through its journal rather than failing on the missing directory: the same merge/archive evidence is verified, Git may hold no registration for the path or only this candidate's own dangling one, that registration is retired, the step is recorded as `worktree-already-absent`, and the unchanged local topic ref is deleted under the usual compare-and-delete rule. A `complete` invocation for an absent path that was never registered blocks with `worktree-absent-unregistered`, because there is no journaled head to compare the ref against.
171
+
172
+ Post-merge provenance accepts a `merged` timeline event whose time is within five seconds of the pull request's `merged_at`; GitHub stamps the two from different services and has reported them one second apart. The single-event, human-actor, no-App, and same-commit checks are unchanged.
142
173
 
143
- Windows file locks can leave a directory after Git removes part of a worktree. `partial` retains that residue and its journal; there is no `rm -rf`, force discard, or automatic directory-repair fallback. Stop the locking process and review the exact residual data before separately authorized manual repair. A recreated path is not the old checkout. If a prior run fully removed the worktree and only ref cleanup remains, a subsequent enabled pass rechecks evidence and safely resumes the branch-only step.
174
+ Windows file locks can interrupt Git after it has already unlinked the `.git` pointer and part of the tree. That pass reports `partial` with `git-operation-failed` and keeps the journal at `remove-intent`; rerunning the same command resumes it. If the exact registered worktree is still intact, the retry re-inspects it and repeats non-force Git removal. Otherwise the retry verifies the residue: Git must no longer list the path as a valid worktree, the residue may contain no `.git` entry, link, special file, or nested repository, and every remaining regular file must either sit below a declared disposable root or match the exact tracked path and blob hash of the journaled head (`git hash-object` with the repository's filters). Only residue verified that way is removed, with the same bounded retry, after which this candidate's own dangling Git registration is retired and ref cleanup continues. A residue with any unknown, changed, or boundary-violating path reports `residual-content` and lists the offending paths for manual review; a residue that is still locked reports `residual-locked` and is retried on a later pass. `git worktree prune` is never run. A recreated or foreign worktree at the path reports `residual-or-reused-path` and needs a new registration. If a prior run fully removed the worktree and only ref cleanup remains, a subsequent pass rechecks evidence and safely resumes the branch-only step.
144
175
 
145
176
  Ownership is cooperative: managed sessions must claim before use. It cannot police arbitrary external editors. Remote ref checks and local deletion also are not one distributed transaction; refs are read immediately before destructive steps, and uncertain identities always block.
146
177
 
@@ -57,8 +57,9 @@ Use equivalent values in bare A1 and the Pi comparison profile. Exercise both a
57
57
  - [ ] Trust startup: from an undecided project, compare selected Trust/Do not trust rows, arrow navigation, Enter, Escape/Ctrl+C, clearing, cursor state, and restoration. No project extension/theme/prompt/skill may run before selection, and a fail-closed diagnostic must appear once on the restored parent rather than above a blank fullscreen frame.
58
58
  - [ ] Terminal lifecycle: toggle hardware cursor, clear-on-shrink, and terminal progress; resize smaller/larger; open/close selectors; select and copy transcript text; verify no duplicate rows, stale OSC progress, leaked mouse mode, misplaced cursor, or broken parent input.
59
59
  - [ ] Images: in Kitty or iTerm2 verify inline width and clipping; in Windows Terminal verify the textual fallback and absence of image protocol bytes without hiding `showImages`.
60
- - [ ] Fullscreen exit `transcript`: verify the parent is restored before styled user, assistant Markdown, thinking, tool, notice, warning, error, and spacing rows are printed. No overlay, draft, animation, scrollbar, or inline-image payload may appear.
61
- - [ ] Fullscreen exit `resume-hint`: verify only dim `To resume this session:` plus `a1 --session <compact-id>` is printed for the default directory. A custom directory must place quoted `--session-dir <dir>` before `--session`; the raw default `.jsonl` path must never print.
60
+ - [ ] Bare A1 quit (`/quit` and the second `Ctrl+C`): the configured quit effect plays over the last frame on the alternate screen, the terminal is restored once, and the parent shows only its earlier scrollback plus the dim `To resume this session:` hint. No frame rows, transcript rows, editor box, or footer may remain. With `/settings` → Quit → Effect set to `off`, the leave is immediate and the parent output is identical.
61
+ - [ ] Fullscreen exit `transcript` (`a1 pi` only): verify the parent is restored before styled user, assistant Markdown, thinking, tool, notice, warning, error, and spacing rows are printed. No overlay, draft, animation, scrollbar, or inline-image payload may appear.
62
+ - [ ] Fullscreen exit `resume-hint` (`a1 pi`) and bare A1: verify only dim `To resume this session:` plus `a1 --session <compact-id>` is printed for the default directory. A custom directory must place quoted `--session-dir <dir>` before `--session`; the raw default `.jsonl` path must never print.
62
63
 
63
64
  Record acceptance with:
64
65
 
@@ -12,10 +12,10 @@ Version-1 and version-2 deliveries and their existing comments, acceptance PRs,
12
12
  2. **Same-PR implementation:** continue after explicit approval in the same worktree, branch, history, draft PR, and phase-free body. Reconcile approved refinements in proposal, design, deltas, and tasks before corresponding code edits.
13
13
  3. **Complete evidence:** finish implementation, required tests/evidence, substantive tasks, and explicit known-gap disposition. CI success is objective evidence, not acceptance.
14
14
  4. **Plain acceptance list:** keep the body phase-free and add final `## Acceptance` with one to three concise implementation-specific behavior-and-result bullets. Do not use checkboxes, generic review/CI/approval/archive statements, URLs, mentions, or automated-test inventory.
15
- 5. **In-branch finalization:** reconcile current `origin/develop`, conservatively synchronize all deltas, move the active change into its dated archive, and stage the conditional acceptance manifest in the same branch.
16
- 6. **Ready and validate:** mark the finalized PR ready without changing its body lifecycle marker because none exists. One normal exact-head workflow validates the implementation, synchronized specs, archive, manifest, tasks/evidence, exact PR-body list, and every selected product/governance scope before emitting the stable protected aggregate. A new commit, acceptance-list change, or advanced target requires full renewed validation; no lifecycle body edit or second workflow run is required.
15
+ 5. **Ready and automated finalization:** mark the PR ready. The trusted `OpenSpec finalization` workflow reconciles current `develop`, conservatively synchronizes all deltas, moves the active change into its dated archive, stages the conditional acceptance manifest, commits that to the same branch with the archive App identity, and writes the emitted paths into the body's implementation fence. Running the [finalization command](#finalization-command) locally first is optional and yields the same bytes.
16
+ 6. **Validate:** one normal exact-head workflow validates the finalized head: implementation, synchronized specs, archive, manifest, tasks/evidence, exact PR-body list, and every selected product/governance scope before emitting the stable protected aggregate. A new commit or acceptance-list change re-finalizes automatically when needed and requires full renewed validation; no lifecycle body edit or second workflow run is required.
17
17
  7. **Manual merge accepts:** after the stable protected aggregate succeeds, an authorized human reviews and manually merges the exact validated head. That single action means the listed scenarios are accepted and explicitly authorizes integration. Auto-merge, merge queue, Apps, bots, and documentation reconciliation are forbidden.
18
- 8. **Verify and clean:** trusted post-merge policy derives `Archived` and reports `accepted-and-archived` from committed bytes and immutable GitHub provenance without editing the accepted PR body. It publishes no lifecycle branch or PR. Shared exact-head remote cleanup may delete the unchanged topic ref; local cleanup remains separately ownership-controlled.
18
+ 8. **Verify and clean:** trusted post-merge policy derives `Archived` and reports `accepted-and-archived` from committed bytes and immutable GitHub provenance without editing the accepted PR body. It publishes no lifecycle branch or PR. Shared exact-head remote cleanup may delete the unchanged topic ref; local cleanup remains separately ownership-controlled: the agent parks the worktree with `handoff` at step 7, and the next session's `sweep` removes it once the merge, archive, and remote-ref evidence verify (see [local cleanup](local-worktree-cleanup.md)).
19
19
 
20
20
  The implementation, synchronized canonical specs, conditional acceptance record, and archive therefore reach `develop` atomically. Closing the PR unmerged integrates none of them.
21
21
 
@@ -69,7 +69,7 @@ Do not add a quoted phase line or a routine `Validation` section listing command
69
69
  </details>
70
70
  ```
71
71
 
72
- Keep acceptance absent during proposal review so unfinished intent is not mistaken for final acceptance criteria. Keep the finalized phase-free body unchanged through exact-head validation, maintainer review, and authorized manual merge. The same workflow run validates its finalized delivery record and applicable product/governance scopes before the stable protected aggregate succeeds. Do not add a lifecycle body edit or start a second validation run. After manual merge, trusted verification derives `Archived`; do not rewrite the accepted body.
72
+ Keep acceptance absent during proposal review so unfinished intent is not mistaken for final acceptance criteria. The finalization workflow rewrites only the implementation fence; keep the rest of the phase-free body unchanged through exact-head validation, maintainer review, and authorized manual merge. The same workflow run validates its finalized delivery record and applicable product/governance scopes before the stable protected aggregate succeeds. Do not add a lifecycle body edit or start a second validation run. After manual merge, trusted verification derives `Archived`; do not rewrite the accepted body.
73
73
 
74
74
  ## Version-3 implementation metadata
75
75
 
@@ -129,9 +129,22 @@ The committed conditional manifest contains the same ordered text. Trusted polic
129
129
 
130
130
  If the body list changes, candidate validation reruns and compares it with the committed manifest. If the head changes, all prior exact-head CI is stale. If only the body changes to disagree with the manifest, integration remains blocked until the list and committed candidate agree again. No lifecycle body edit is needed after green CI.
131
131
 
132
+ ## Automated finalization
133
+
134
+ The `OpenSpec finalization` workflow (`.github/workflows/openspec-finalization.yml`) runs on `pull_request_target` for `synchronize`, `ready_for_review`, `reopened`, and `edited` events of every non-draft PR targeting `develop`, one event at a time per PR. It checks out default-branch policy, installs the pinned tooling without hooks, and runs `scripts/governance/publish-openspec-finalization.mjs --pr <n>`; the PR head enters that process only as the `openspec/` tree the pinned OpenSpec engine reads. Drafts, closed, legacy, and unassociated PRs are skipped. For a version-3 candidate it reconciles the head to its finalized form:
135
+
136
+ - **Active and current:** ordinary finalization with today's UTC date; one `docs(openspec): finalize <change>` commit.
137
+ - **Finalized, valid, and current:** nothing is pushed; the run reports `already-finalized`.
138
+ - **Finalized but drifted:** a later commit edited the archived tasks, evidence, design, or deltas, or the acceptance list changed. Finalization reruns from the archived form under the same archive date; one `docs(openspec): refinalize <change>` commit replaces the manifest and resynchronized specs.
139
+ - **Behind `develop`:** a restore commit returns the archive to its active form with the merge-base's spec bytes, a merge of `develop` follows, and finalization runs against the new tip. A merge conflict outside `openspec/` stops the run with `finalization-merge-conflict`; rebase or merge `develop` yourself and push.
140
+
141
+ The commit is pushed with a lease on the head the run read, so a developer push in between makes the run report `retry` and the next event finishes the work. Only after the push does the workflow `PATCH` the body fence, and only when the body is unchanged since it was read. The workflow's own push and body edit trigger further runs that report `already-finalized`. Every pushed commit is either a merge of the exact `develop` tip or confined to the change's active path, its archive path, and its declared canonical specs.
142
+
143
+ Because the branch gains commits from the archive App, pull before pushing. A rebase that drops them is harmless: the next push is reconciled from whatever the head contains. Do not revert a finalization commit to make a fix; push the fix and let the workflow re-finalize. When the workflow fails, its summary names the finalization code (`tasks-incomplete`, `acceptance-*`, `delivery-known-gaps`, `openspec-operation`, `finalization-merge-conflict`, ...), nothing is pushed, and `Finalized delivery validation` on the unfinalized head reports that automated finalization is pending.
144
+
132
145
  ## Finalization command
133
146
 
134
- Finalization has inspection mode by default and an explicit `--write` mode. It never commits, pushes, edits GitHub, marks a PR ready, or merges. Use a temporary body file so the operation can update exact version-3 paths without mutating remote PR state:
147
+ The local command remains available for inspection or when a developer prefers to finalize before marking the PR ready; the workflow then verifies the head and pushes nothing. It has inspection mode by default and an explicit `--write` mode. It never commits, pushes, edits GitHub, marks a PR ready, or merges. Use a temporary body file so the operation can update exact version-3 paths without mutating remote PR state:
135
148
 
136
149
  ```bash
137
150
  git fetch origin develop
@@ -164,7 +177,7 @@ Use repeated `--known-gap "exact disposition"` only for an actually reviewed exp
164
177
 
165
178
  The operation validates the active change strictly, requires complete substantive tasks, runs the pinned OpenSpec archive/synchronization engine in isolation, verifies the resulting canonical specs, retains every archive artifact, computes deterministic content digests, writes `acceptance.md`, and applies only the allowed OpenSpec diff. Repeating it against identical finalized inputs is verification-only and byte-stable.
166
179
 
167
- Before finalization, canonical specs must still equal the selected target. If `develop` advances, rebase/reconcile and regenerate. To refine a finalized but unmerged change, restore the active artifacts with ordinary branch history, update plan/code coherently, and rerun finalization; never hand-edit only the synchronized spec or archive copy.
180
+ Before a first finalization, canonical specs must still equal the selected target. On an already finalized head the command re-finalizes from the archived form: it resets the synchronized specs to the target's bytes, reapplies the deltas, and rewrites the manifest under the same archive path, reporting `refinalized` (or `would-refinalize` without `--write`). If `develop` advances, merge or rebase onto it and rerun; the archive engine, not a hand edit, must produce the synchronized spec and archive copy.
168
181
 
169
182
  ## Conditional acceptance and derived receipt
170
183
 
@@ -183,11 +196,11 @@ Unavailable, stale, automatic, unauthorized, conflicting, or contradictory prove
183
196
 
184
197
  ## CI and automation ownership
185
198
 
186
- Normal Development CI remains complete for the implementation. An archive-shaped final diff does not select documentation-only validation because the authoritative version-3 association remains implementation-bound. The trusted acceptance-policy job validates finalization from base-controlled policy while ordinary impact selection retains all applicable product and governance owners.
199
+ Normal Development CI remains complete for the implementation. An archive-shaped final diff does not select documentation-only validation because the authoritative version-3 association remains implementation-bound. The trusted acceptance-policy job validates finalization from base-controlled policy while ordinary impact selection retains all applicable product and governance owners. A ready head that still holds the active change fails that job with an `Awaiting automated finalization` notice pointing at the finalization workflow; the head the workflow pushes receives its own complete validation, and `Development validation` cancels the superseded run.
187
200
 
188
201
  `pull_request` body edits rerun required CI. The ordinary finalized phase-free run exposes the stable protected aggregate directly; it does not wait for a lifecycle body edit. Documentation auto-merge's trusted owner also reevaluates lifecycle association and disables any armed merge. Every publication entry point explicitly refuses version 3.
189
202
 
190
- The OpenSpec archive workflow remains default-branch trusted. For version 3 it uses read-only contents, PR, and Actions access to report the integrated result; App credentials are unnecessary and are not minted. For legacy candidates it retains its existing scoped App publication behavior.
203
+ The OpenSpec archive workflow remains default-branch trusted. For version 3 it uses read-only contents, PR, and Actions access to report the integrated result; App credentials are unnecessary and are not minted for post-merge verification. For legacy candidates it retains its existing scoped App publication behavior. The OpenSpec finalization workflow is the one version-3 user of the archive App: it mints a short-lived installation token with `contents: write` and `pull_requests: write`, pushes only to the candidate's own branch under a lease, edits only the body fence, and revokes the token when the run ends.
191
204
 
192
205
  ## Status and audit
193
206
 
@@ -230,7 +243,7 @@ This bootstrap policy itself finishes under deployed version-2 authority. The fi
230
243
 
231
244
  - draft planning remaining unmerged;
232
245
  - explicit plan approval and same-PR implementation;
233
- - current-target finalization and exact-head CI;
246
+ - automated current-target finalization and exact-head CI;
234
247
  - authorized human manual merge with plain acceptance scenarios;
235
248
  - integrated canonical specs/archive and no generated acceptance/archive PR;
236
249
  - read-only `accepted-and-archived` verification and exact-head branch cleanup;
@@ -8,7 +8,9 @@ npm run select:validation-impact -- --base <full-base-sha> --head <full-head-sha
8
8
 
9
9
  The versioned `config/validation-ownership.json` registry maps stable product and test path groups to a mandatory PR core, affected unit tests, resource-sensitive tests, and integration owners. The initial coarse owners are UI/rendering, launch/startup, release/package/update, Pi, native containment, image/history, governance, and shared product inputs. Each integration owner in `config/integration-owners.json` declares `pull-request` or `exhaustive` cadence. A changed pull-request test selects its owner; a changed exhaustive test selects focused deterministic contracts and records the exhaustive owner as cadence-deferred. Shared support selects every declared PR consumer and records affected exhaustive consumers; copies, renames, and deletions inspect both identities. Reasons and changed paths are recorded in `impact.json`.
10
10
 
11
- Unknown operational inputs, unavailable comparison history, workflow/selector/suite/aggregate changes, and manual Development dispatch select every pull-request owner. Missing, malformed, or unknown cadence blocks instead of guessing. Documentation-only and version-only changes retain explicit exemptions. Selection does not require whole-repository source parsing.
11
+ Unknown operational inputs, unavailable comparison history, workflow/selector/suite/aggregate changes, and manual Development dispatch select every pull-request owner. Missing, malformed, or unknown cadence blocks instead of guessing. Documentation-only and version-only changes retain explicit exemptions. An implementation-bound association (an `openspec-implementation` link in the PR body) only disables those two exemptions: the PR core still runs, and owners are still selected by impact rather than forced to conservative coverage. Selection does not require whole-repository source parsing.
12
+
13
+ The `changes` job then derives the modular job matrix from that same selection through `scripts/release/validation-matrix.mjs`, which declares every Development modular job and returns only the entries the selection activates; GitHub never schedules an inactive entry, so an ordinary PR does not spend runner minutes on jobs that would resolve to nothing. Each scheduled job still resolves its own owners from the uploaded impact artifact, and the aggregate still requires evidence for every selected owner, so an entry that should have run and did not fails the run.
12
14
 
13
15
  ## Commands and coverage levels
14
16
 
@@ -87,7 +89,7 @@ Download these artifacts from the exact workflow run:
87
89
  - `development-validation-aggregate-<head>-<run>-<attempt>`: selected/deferred owners, accepted/reused attempts, evidence count, runner critical path, total runner time, aggregate processing, setup/gate/scope time, cache state, and invocation count. It reports the eight-minute critical-path and five-minute individual-scope targets as met or unmet; hosted queue delay is not test execution and remains separate.
88
90
  - startup/resume phase JSONL and performance JSON: first-attempt launch evidence and retained failed setup/readiness records.
89
91
 
90
- A finalized version-3 PR keeps its phase-free body unchanged. Its ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and manual merge but does not claim human acceptance or merge automatically. No lifecycle body edit or second workflow run is required. Any implementation commit changes the head and reruns applicable validation; any body change reruns finalized-record validation and must continue to match the committed manifest. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
92
+ A finalized version-3 PR keeps its phase-free body unchanged. Its ordinary exact-head workflow runs selected product/governance lanes and `Finalized delivery validation` in parallel, then emits the stable protected `Development validation required` aggregate only when both authorities succeed. Green CI enables maintainer review and manual merge but does not claim human acceptance or merge automatically. No lifecycle body edit or second workflow run is required. Any implementation commit changes the head and reruns applicable validation; any body change reruns finalized-record validation and must continue to match the committed manifest. A ready head that still holds the active change fails `Finalized delivery validation` with an `Awaiting automated finalization` notice; the `OpenSpec finalization` workflow pushes the finalized head, which receives its own run. Legacy acceptance-record-only PRs retain their separate trusted `Acceptance record validation` route. Queue availability remains explicitly unavailable inside a runner and is calculated from the Actions API during final run analysis rather than guessed.
91
93
 
92
94
  ## Rollback
93
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.447",
3
+ "version": "0.1.8-dev.459",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",