@timurproko/a1 0.1.8-dev.447 → 0.1.8-dev.457
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.
- package/README.md +9 -0
- package/dist/composition/owned-ui.js +11 -0
- package/dist/contracts/owned-ui/model.d.ts +10 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +17 -0
- package/dist/integrations/pi/engine/adapter.d.ts +2 -0
- package/dist/integrations/pi/engine/adapter.js +25 -6
- package/dist/integrations/pi/engine/settings-effects.d.ts +1 -1
- package/dist/integrations/pi/engine/settings-effects.js +1 -1
- package/dist/integrations/pi/session-ui/quit-outro-effects.d.ts +29 -0
- package/dist/integrations/pi/session-ui/quit-outro-effects.js +242 -0
- package/dist/integrations/pi/session-ui/quit-outro.d.ts +38 -0
- package/dist/integrations/pi/session-ui/quit-outro.js +98 -0
- package/dist/integrations/pi/session-ui/session-shell-root.d.ts +12 -1
- package/dist/integrations/pi/session-ui/session-shell.js +57 -5
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +10 -2
- package/dist/integrations/pi/tui-runtime/adapter.js +53 -4
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +7 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +12 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/settings/declarations.d.ts +3 -1
- package/dist/ui/settings/declarations.js +22 -1
- package/dist/ui/settings/migrations.js +7 -0
- package/docs/ci-release-runbook.md +5 -5
- package/docs/local-worktree-cleanup.md +8 -4
- package/docs/manual-owned-ui-checkpoint.md +3 -2
- package/docs/openspec-archive-automation.md +21 -8
- package/docs/validation.md +4 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OwnedUiPromptSuggestionGeneratorPort, OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, SuggestionDecision, SuggestionDiagnosticObserver } from "../../../contracts/owned-ui/index.js";
|
|
1
|
+
import type { OwnedUiPromptSuggestionGeneratorPort, OwnedUiSessionViewModel, OwnedUiViewportSettings, OwnedUiViewportSettingsPort, OwnedUiQuitOutroSettingsPort, SuggestionDecision, SuggestionDiagnosticObserver } from "../../../contracts/owned-ui/index.js";
|
|
2
2
|
import type { PiTuiPointerSurface } from "../tui-runtime/contracts.js";
|
|
3
3
|
import type { UiRouteHost } from "../../../ui/apps/contracts.js";
|
|
4
4
|
import type { TranscriptViewportFrame, TranscriptViewportFrameDescriptor } from "../../../ui/components/transcript-viewport.js";
|
|
@@ -47,6 +47,17 @@ export interface OwnedUiSessionShellOptions {
|
|
|
47
47
|
readonly sessionLayout?: "pinned" | "custom-viewport";
|
|
48
48
|
/** Live profile-local settings, supplied only to the bare-A1 composition. */
|
|
49
49
|
readonly viewportSettings?: OwnedUiViewportSettingsPort;
|
|
50
|
+
/**
|
|
51
|
+
* Quit outro settings and seams, supplied only to the bare-A1 composition.
|
|
52
|
+
* `interactive` states whether the terminal can show the animation; production
|
|
53
|
+
* passes stdout's TTY state, tests opt in explicitly.
|
|
54
|
+
*/
|
|
55
|
+
readonly quitOutro?: OwnedUiQuitOutroSettingsPort & {
|
|
56
|
+
readonly interactive: boolean;
|
|
57
|
+
readonly now?: () => number;
|
|
58
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
59
|
+
readonly seed?: number;
|
|
60
|
+
};
|
|
50
61
|
/** Optional platform seam; production uses A1's system clipboard adapter. */
|
|
51
62
|
readonly clipboard?: OwnedUiClipboardPort;
|
|
52
63
|
/** Owned response-copy transport and payload-free diagnostic seams. Comparison profiles ignore them. */
|
|
@@ -55,6 +55,7 @@ export class OwnedUiSessionShell {
|
|
|
55
55
|
#responseCopy;
|
|
56
56
|
#unbindClipboardWriter;
|
|
57
57
|
#damageTerminal;
|
|
58
|
+
#quitOutro;
|
|
58
59
|
#streamPresentation;
|
|
59
60
|
#removeViewportPreInput;
|
|
60
61
|
#unsubscribeSettings;
|
|
@@ -250,6 +251,7 @@ export class OwnedUiSessionShell {
|
|
|
250
251
|
runtime = new PiTuiRuntimeAdapter(runtimeOptions);
|
|
251
252
|
this.runtime = runtime;
|
|
252
253
|
this.#damageTerminal = damageTerminal ?? null;
|
|
254
|
+
this.#quitOutro = options.quitOutro;
|
|
253
255
|
const presentationInterval = options.streamPresentation?.intervalMs ?? STREAM_PRESENTATION_INTERVAL_MS;
|
|
254
256
|
streamPresentation = options.streamPresentation?.scheduler === undefined
|
|
255
257
|
? new StreamPresentationCoalescer(() => this.runtime.requestRender(), presentationInterval)
|
|
@@ -282,7 +284,9 @@ export class OwnedUiSessionShell {
|
|
|
282
284
|
this.runtime.setClearOnShrink(initialPiSettings.clearOnShrink);
|
|
283
285
|
this.#terminalProgressEnabled = initialPiSettings.showTerminalProgress;
|
|
284
286
|
this.#fullscreenExitOutput = initialPiSettings.fullscreenExitOutput;
|
|
285
|
-
|
|
287
|
+
// Invariant: bare A1 prints only the resume hint at exit, so the pinned exit-output
|
|
288
|
+
// choice is hidden there and cannot be bound; the comparison profile binds and honors it.
|
|
289
|
+
this.#unbindShutdownSettings = this.backend.settingsProductMode === "bare" ? () => { } : this.backend.bindSettingsOwner("shutdown", {
|
|
286
290
|
fullscreenExitOutput: { apply() { } },
|
|
287
291
|
});
|
|
288
292
|
this.#unbindTerminalSettings = this.backend.bindSettingsOwner("terminal", {
|
|
@@ -1350,6 +1354,8 @@ export class OwnedUiSessionShell {
|
|
|
1350
1354
|
}
|
|
1351
1355
|
async #dispose() {
|
|
1352
1356
|
this.#disposed = true;
|
|
1357
|
+
// Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
|
|
1358
|
+
const outroFrame = this.#captureQuitOutroFrame();
|
|
1353
1359
|
this.#responseCopy?.dispose();
|
|
1354
1360
|
this.#cancelWaitingImages();
|
|
1355
1361
|
const failures = [];
|
|
@@ -1373,11 +1379,13 @@ export class OwnedUiSessionShell {
|
|
|
1373
1379
|
let fullscreenExitText = "";
|
|
1374
1380
|
attempt(() => {
|
|
1375
1381
|
const exitMode = this.backend.disposed ? this.#fullscreenExitOutput : this.backend.pinnedSettingsSnapshot().fullscreenExitOutput;
|
|
1376
|
-
const exitTranscript = this.root.exitTranscript(this.runtime.viewport().columns);
|
|
1377
1382
|
const resume = this.backend.currentSessionResumeMetadata();
|
|
1378
1383
|
const resumeHint = resume === null ? "" : `${dim("To resume this session:")} ${formatSessionResumeCommand(resume)}`;
|
|
1384
|
+
// Invariant: bare A1 leaves only the hint behind; the pinned comparison profile still
|
|
1385
|
+
// honors fullscreenExitOutput, including the styled transcript.
|
|
1379
1386
|
fullscreenExitText = this.runtime.mode !== "fullscreen" ? ""
|
|
1380
|
-
: exitMode === "resume-hint" ? resumeHint
|
|
1387
|
+
: this.#customViewport || exitMode === "resume-hint" ? resumeHint
|
|
1388
|
+
: [this.root.exitTranscript(this.runtime.viewport().columns), resumeHint].filter(Boolean).join("\n\n");
|
|
1381
1389
|
});
|
|
1382
1390
|
attempt(() => this.#unbindClipboardWriter());
|
|
1383
1391
|
attempt(() => this.#unbindPiSettings());
|
|
@@ -1386,8 +1394,11 @@ export class OwnedUiSessionShell {
|
|
|
1386
1394
|
attempt(() => this.#unsubscribe());
|
|
1387
1395
|
attempt(() => this.#dialogHandle?.hide());
|
|
1388
1396
|
attempt(() => this.#extensionBridge.dispose());
|
|
1389
|
-
|
|
1390
|
-
|
|
1397
|
+
await this.#playQuitOutro(outroFrame);
|
|
1398
|
+
// Invariant: terminal restoration precedes any potentially stalled backend teardown. The
|
|
1399
|
+
// fullscreen leave preserves the screen: the pinned runtime never dumps its final document
|
|
1400
|
+
// into the parent terminal, so only the configured exit text follows the leave.
|
|
1401
|
+
await this.runtime.dispose({ preserveScreen: this.runtime.mode === "fullscreen" }).catch(error => failures.push(error));
|
|
1391
1402
|
await historyCleanup.catch(() => false); // Security: background durability outcomes never enter terminal output.
|
|
1392
1403
|
await boundedCleanup(() => pasteCleanup).catch(error => failures.push(error));
|
|
1393
1404
|
await boundedCleanup(() => this.backend.unbindExtensionUi()).catch(error => failures.push(error));
|
|
@@ -1396,6 +1407,47 @@ export class OwnedUiSessionShell {
|
|
|
1396
1407
|
if (fullscreenExitText.length > 0)
|
|
1397
1408
|
this.runtime.writeAfterStop(`${fullscreenExitText}\n`);
|
|
1398
1409
|
}
|
|
1410
|
+
// Invariant: the snapshot is synchronous; the outro module itself loads only at quit.
|
|
1411
|
+
#captureQuitOutroFrame() {
|
|
1412
|
+
const outro = this.#quitOutro;
|
|
1413
|
+
if (outro === undefined || !outro.interactive || !this.#customViewport || this.#damageTerminal === null)
|
|
1414
|
+
return null;
|
|
1415
|
+
if (!this.runtime.active || this.runtime.mode !== "fullscreen")
|
|
1416
|
+
return null;
|
|
1417
|
+
try {
|
|
1418
|
+
const { effect, durationMs } = outro.snapshot();
|
|
1419
|
+
if (effect === "off")
|
|
1420
|
+
return null;
|
|
1421
|
+
const viewport = this.runtime.viewport();
|
|
1422
|
+
return { rows: this.#damageTerminal.presentedRows(), columns: viewport.columns, height: viewport.rows, settings: { effect, durationMs } };
|
|
1423
|
+
}
|
|
1424
|
+
catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
// Rationale: any failure here only skips the effect; restoration always follows.
|
|
1429
|
+
async #playQuitOutro(capture) {
|
|
1430
|
+
const outro = this.#quitOutro;
|
|
1431
|
+
if (capture === null || outro === undefined || !this.runtime.active)
|
|
1432
|
+
return;
|
|
1433
|
+
try {
|
|
1434
|
+
// Rationale: the effects stay off the startup graph; quit is the only time they load.
|
|
1435
|
+
const { captureQuitOutroFrame, playQuitOutro } = await import("./quit-outro.js");
|
|
1436
|
+
const frame = captureQuitOutroFrame(capture.rows, capture.columns, capture.height);
|
|
1437
|
+
if (frame === null || !this.runtime.active)
|
|
1438
|
+
return;
|
|
1439
|
+
this.runtime.freezePresentation();
|
|
1440
|
+
await playQuitOutro(frame, capture.settings.effect, capture.settings.durationMs, {
|
|
1441
|
+
write: data => this.runtime.writeControl(data),
|
|
1442
|
+
...(outro.now === undefined ? {} : { now: outro.now }),
|
|
1443
|
+
...(outro.sleep === undefined ? {} : { sleep: outro.sleep }),
|
|
1444
|
+
...(outro.seed === undefined ? {} : { seed: outro.seed }),
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
catch {
|
|
1448
|
+
// Rationale: a failed or interrupted effect must never hold the terminal; restoration follows.
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1399
1451
|
#settleStoppedLifecycle() {
|
|
1400
1452
|
void this.dispose().then(() => this.#resolveStoppedLifecycle(), () => this.#resolveStoppedLifecycle());
|
|
1401
1453
|
}
|
|
@@ -26,10 +26,18 @@ export declare class PiTuiRuntimeAdapter {
|
|
|
26
26
|
hasFocusedOverlay(): boolean;
|
|
27
27
|
/**
|
|
28
28
|
* Writes a terminal control sequence. Used to enable and disable mouse
|
|
29
|
-
* reporting while an A1-owned application is presented
|
|
30
|
-
*
|
|
29
|
+
* reporting while an A1-owned application is presented and to paint the quit
|
|
30
|
+
* outro while presentation is frozen, and for nothing else: the transparent
|
|
31
|
+
* and pinned paths never call it.
|
|
31
32
|
*/
|
|
32
33
|
writeControl(data: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Drops every pinned frame write until the runtime stops. The quit outro owns
|
|
36
|
+
* the alternate screen between the last presented frame and the leave; the
|
|
37
|
+
* renderer keeps scheduling but nothing it paints reaches the terminal.
|
|
38
|
+
*/
|
|
39
|
+
freezePresentation(): void;
|
|
40
|
+
get presentationFrozen(): boolean;
|
|
33
41
|
addPreInputListener(listener: PiTuiPreInputListener): () => void;
|
|
34
42
|
addInputListener(listener: PiTuiInputListener): () => void;
|
|
35
43
|
switchMode(mode: "regular" | "fullscreen"): boolean;
|
|
@@ -120,15 +120,20 @@ export class PiTuiRuntimeAdapter {
|
|
|
120
120
|
#stopPromise;
|
|
121
121
|
#rootDisposed = false;
|
|
122
122
|
#terminalProgress = false;
|
|
123
|
+
#presentationFrozen = false;
|
|
123
124
|
constructor(options) {
|
|
124
125
|
this.#root = options.root;
|
|
125
126
|
this.#overlayGeometry = options.onOverlayGeometry === undefined ? undefined : new OverlayGeometryTracker(options.onOverlayGeometry);
|
|
126
127
|
this.#terminal = options.terminal ?? new ProcessTerminal();
|
|
127
128
|
this.#inputDiagnostics = options.inputDiagnostics;
|
|
128
129
|
this.#diagnosticNow = options.inputDiagnostics?.now ?? (() => performance.now());
|
|
130
|
+
// Invariant: the pinned renderer's frames pass through this gate, while writeControl and
|
|
131
|
+
// the stop sequence reach the terminal directly. Freezing drops frames without touching
|
|
132
|
+
// the renderer, so a scheduled repaint cannot land on top of the quit outro.
|
|
133
|
+
const gatedTerminal = frozenGateTerminal(this.#terminal, () => this.#presentationFrozen);
|
|
129
134
|
const tracedTerminal = options.inputDiagnostics === undefined
|
|
130
|
-
?
|
|
131
|
-
: diagnosticTerminal(
|
|
135
|
+
? gatedTerminal
|
|
136
|
+
: diagnosticTerminal(gatedTerminal, phase => this.#traceRuntimePhase(phase));
|
|
132
137
|
const decoratedTerminal = options.decorateTerminal?.(tracedTerminal) ?? tracedTerminal;
|
|
133
138
|
const coordination = options.inputCoordination ?? (options.inputDiagnostics === undefined
|
|
134
139
|
? undefined
|
|
@@ -297,13 +302,26 @@ export class PiTuiRuntimeAdapter {
|
|
|
297
302
|
}
|
|
298
303
|
/**
|
|
299
304
|
* Writes a terminal control sequence. Used to enable and disable mouse
|
|
300
|
-
* reporting while an A1-owned application is presented
|
|
301
|
-
*
|
|
305
|
+
* reporting while an A1-owned application is presented and to paint the quit
|
|
306
|
+
* outro while presentation is frozen, and for nothing else: the transparent
|
|
307
|
+
* and pinned paths never call it.
|
|
302
308
|
*/
|
|
303
309
|
writeControl(data) {
|
|
304
310
|
this.#assertRunning("control sequence");
|
|
305
311
|
this.#terminal.write(data);
|
|
306
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Drops every pinned frame write until the runtime stops. The quit outro owns
|
|
315
|
+
* the alternate screen between the last presented frame and the leave; the
|
|
316
|
+
* renderer keeps scheduling but nothing it paints reaches the terminal.
|
|
317
|
+
*/
|
|
318
|
+
freezePresentation() {
|
|
319
|
+
this.#assertRunning("presentation freeze");
|
|
320
|
+
this.#presentationFrozen = true;
|
|
321
|
+
}
|
|
322
|
+
get presentationFrozen() {
|
|
323
|
+
return this.#presentationFrozen;
|
|
324
|
+
}
|
|
307
325
|
addPreInputListener(listener) {
|
|
308
326
|
if (this.#preInputListeners.has(listener))
|
|
309
327
|
throw new TypeError("Pi TUI pre-input listener is already registered");
|
|
@@ -433,6 +451,9 @@ export class PiTuiRuntimeAdapter {
|
|
|
433
451
|
}
|
|
434
452
|
try {
|
|
435
453
|
const stopOptions = options.preserveScreen === undefined ? undefined : { preserveScreen: options.preserveScreen };
|
|
454
|
+
// Invariant: the gate opens only for the synchronous stop sequence, so no render
|
|
455
|
+
// scheduled during the outro can slip in before the alternate screen is left.
|
|
456
|
+
this.#presentationFrozen = false;
|
|
436
457
|
this.#tui.stop(stopOptions);
|
|
437
458
|
}
|
|
438
459
|
catch (error) {
|
|
@@ -566,6 +587,7 @@ export class PiTuiRuntimeAdapter {
|
|
|
566
587
|
throw new Error(`Pi TUI ${operation} requires a running runtime`);
|
|
567
588
|
}
|
|
568
589
|
#restoreAfterFailedStart() {
|
|
590
|
+
this.#presentationFrozen = false;
|
|
569
591
|
try {
|
|
570
592
|
this.#tui.stop();
|
|
571
593
|
}
|
|
@@ -581,6 +603,7 @@ export class PiTuiRuntimeAdapter {
|
|
|
581
603
|
this.#terminalProgress = false;
|
|
582
604
|
}
|
|
583
605
|
#bestEffortTerminalRestore() {
|
|
606
|
+
this.#presentationFrozen = false;
|
|
584
607
|
this.#clearTerminalProgress();
|
|
585
608
|
try {
|
|
586
609
|
if (this.mode === "fullscreen")
|
|
@@ -676,6 +699,32 @@ function diagnosticTerminal(terminal, trace) {
|
|
|
676
699
|
setProgress: active => terminal.setProgress(active),
|
|
677
700
|
};
|
|
678
701
|
}
|
|
702
|
+
function frozenGateTerminal(terminal, frozen) {
|
|
703
|
+
return {
|
|
704
|
+
get columns() { return terminal.columns; },
|
|
705
|
+
get rows() { return terminal.rows; },
|
|
706
|
+
get kittyProtocolActive() { return terminal.kittyProtocolActive; },
|
|
707
|
+
start: (onInput, onResize) => terminal.start(onInput, onResize),
|
|
708
|
+
stop: () => terminal.stop(),
|
|
709
|
+
drainInput: (maxMs, idleMs) => terminal.drainInput(maxMs, idleMs),
|
|
710
|
+
write: data => { if (!frozen())
|
|
711
|
+
terminal.write(data); },
|
|
712
|
+
moveBy: lines => { if (!frozen())
|
|
713
|
+
terminal.moveBy(lines); },
|
|
714
|
+
hideCursor: () => { if (!frozen())
|
|
715
|
+
terminal.hideCursor(); },
|
|
716
|
+
showCursor: () => { if (!frozen())
|
|
717
|
+
terminal.showCursor(); },
|
|
718
|
+
clearLine: () => { if (!frozen())
|
|
719
|
+
terminal.clearLine(); },
|
|
720
|
+
clearFromCursor: () => { if (!frozen())
|
|
721
|
+
terminal.clearFromCursor(); },
|
|
722
|
+
clearScreen: () => { if (!frozen())
|
|
723
|
+
terminal.clearScreen(); },
|
|
724
|
+
setTitle: title => terminal.setTitle(title),
|
|
725
|
+
setProgress: active => terminal.setProgress(active),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
679
728
|
function preInputTerminal(terminal, route, frameMouse) {
|
|
680
729
|
let mouse;
|
|
681
730
|
return {
|
|
@@ -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-
|
|
8
|
+
"builtAt": "2026-09-17T10:19:32.993Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-09-17T10:19:33.794Z",
|
|
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-
|
|
8
|
+
"builtAt": "2026-09-17T10:20:20.745Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "049f0659bfe5d506a900d07e219c6a96480e8495dede8375c7ea5f9e5dc1990d",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export declare const OWNED_UI_SETTINGS_VERSION = 5;
|
|
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,7 +1,10 @@
|
|
|
1
|
-
export const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export const OWNED_UI_SETTINGS_VERSION = 5;
|
|
2
2
|
const MAX_ID_LENGTH = 64;
|
|
3
3
|
const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
4
4
|
const SCROLL_SECTION = Object.freeze({ id: "scroll", title: "Scroll" });
|
|
5
|
+
const QUIT_SECTION = Object.freeze({ id: "quit", title: "Quit" });
|
|
6
|
+
/** Playback lengths the quit outro offers, in milliseconds. */
|
|
7
|
+
export const QUIT_EFFECT_DURATIONS_MS = Object.freeze(Array.from({ length: 18 }, (_, index) => 300 + index * 100));
|
|
5
8
|
export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
|
|
6
9
|
Object.freeze({
|
|
7
10
|
id: "scrollbarAppearance",
|
|
@@ -48,6 +51,24 @@ export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze([
|
|
|
48
51
|
defaultValue: 100,
|
|
49
52
|
allowedValues: Object.freeze([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]),
|
|
50
53
|
}),
|
|
54
|
+
Object.freeze({
|
|
55
|
+
id: "quitEffect",
|
|
56
|
+
label: "Effect",
|
|
57
|
+
section: QUIT_SECTION,
|
|
58
|
+
description: "Animation played over the last screen when the session quits.",
|
|
59
|
+
application: "live",
|
|
60
|
+
defaultValue: "fall",
|
|
61
|
+
allowedValues: Object.freeze(["fall", "dissolve", "starburst", "waves", "off"]),
|
|
62
|
+
}),
|
|
63
|
+
Object.freeze({
|
|
64
|
+
id: "quitEffectDurationMs",
|
|
65
|
+
label: "Duration",
|
|
66
|
+
section: QUIT_SECTION,
|
|
67
|
+
description: "Milliseconds the quit animation plays before the terminal is restored.",
|
|
68
|
+
application: "live",
|
|
69
|
+
defaultValue: 800,
|
|
70
|
+
allowedValues: QUIT_EFFECT_DURATIONS_MS,
|
|
71
|
+
}),
|
|
51
72
|
Object.freeze({
|
|
52
73
|
id: "promptSuggestions",
|
|
53
74
|
label: "Prompt suggestions",
|
|
@@ -29,6 +29,13 @@ 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
|
+
}),
|
|
32
39
|
]);
|
|
33
40
|
export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
|
|
34
41
|
const firstProduced = currentVersion - migrations.length + 1;
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -18,7 +18,7 @@ node scripts/governance/local-worktree-cleanup.mjs complete \
|
|
|
18
18
|
|
|
19
19
|
`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
20
|
|
|
21
|
-
The central disposable policy is `node_modules`, `dist`, `.builds`, `.artifacts
|
|
21
|
+
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
22
|
|
|
23
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.
|
|
24
24
|
|
|
@@ -135,12 +135,16 @@ State, journals, stop controls, and execution reports live in `<git-common-dir>/
|
|
|
135
135
|
- `unmanaged`: no local registration; no automatic adoption.
|
|
136
136
|
- `removed`: worktree and eligible local-ref operations were verified.
|
|
137
137
|
- `already-absent`: a completed journal's path/ref are still absent.
|
|
138
|
-
- `partial`: a destructive step began but all cleanup could not be verified.
|
|
138
|
+
- `partial`: a destructive step began but all cleanup could not be verified; the same command resumes it.
|
|
139
139
|
- `deferred`: a bounded pass or concurrent mutation owner prevented evaluation.
|
|
140
140
|
|
|
141
|
-
Non-force Git removal is the only
|
|
141
|
+
Non-force Git removal is the only operation that deletes tracked content from an intact worktree. 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.
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
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
148
|
|
|
145
149
|
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
150
|
|
|
@@ -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
|
-
- [ ]
|
|
61
|
-
- [ ] Fullscreen exit `
|
|
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,8 +12,8 @@ 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. **
|
|
16
|
-
6. **
|
|
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
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.
|
|
19
19
|
|
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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;
|