@timurproko/a1 0.2.7-dev.1288 → 0.2.7-dev.1290

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 (34) hide show
  1. package/README.md +4 -0
  2. package/bin/cli.js +4 -0
  3. package/dist/cli/capabilities.d.ts +1 -0
  4. package/dist/cli/capabilities.js +1 -1
  5. package/dist/cli/dispatch.d.ts +4 -0
  6. package/dist/cli/dispatch.js +14 -0
  7. package/dist/cli/session-color.d.ts +6 -0
  8. package/dist/cli/session-color.js +44 -0
  9. package/dist/composition/owned-settings.d.ts +2 -2
  10. package/dist/composition/owned-settings.js +2 -2
  11. package/dist/composition/owned-ui.js +31 -11
  12. package/dist/composition/session-colors.d.ts +5 -0
  13. package/dist/composition/session-colors.js +33 -0
  14. package/dist/contracts/owned-ui/index.d.ts +2 -2
  15. package/dist/contracts/owned-ui/index.js +1 -0
  16. package/dist/contracts/presentation/progress-palette.generated.d.ts +31 -5
  17. package/dist/contracts/presentation/progress-palette.generated.js +1 -1
  18. package/dist/features/owned-ui/settings-app.js +1 -1
  19. package/dist/foundation/lifecycle/index.d.ts +2 -0
  20. package/dist/foundation/lifecycle/index.js +1 -0
  21. package/dist/foundation/lifecycle/session-control.d.ts +26 -0
  22. package/dist/foundation/lifecycle/session-control.js +222 -0
  23. package/dist/foundation/release/early-accent.d.ts +2 -1
  24. package/dist/integrations/pi/components/upstream/theme/theme.js +8 -6
  25. package/dist/ui/settings/declarations.d.ts +18 -2
  26. package/dist/ui/settings/declarations.js +14 -4
  27. package/dist/ui/settings/early-accent.d.ts +16 -0
  28. package/dist/ui/settings/early-accent.js +1 -3
  29. package/dist/ui/settings/manager.d.ts +4 -2
  30. package/dist/ui/settings/manager.js +36 -4
  31. package/dist/ui/settings/resolution.js +3 -2
  32. package/docs/features/session-colors.md +66 -0
  33. package/docs/openspec-archive-automation.md +2 -2
  34. package/package.json +3 -3
package/README.md CHANGED
@@ -99,6 +99,10 @@ a1 pi
99
99
 
100
100
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/assets/readme/separator-dark.svg"><img src="docs/assets/readme/separator.svg" alt="" width="280"></picture></p>
101
101
 
102
+ ## Session colors
103
+
104
+ Settings → Appearance separates a saved **Default color** (initially blue) from **Current color**, which affects only this session. Both offer blue, green, orange, pink, purple, cyan, and grey. External applications can read and change a targeted session through the [always-JSON local color API](docs/features/session-colors.md).
105
+
102
106
  ## Update
103
107
 
104
108
  Update on the channel you already use (release stays on release, develop stays on develop):
package/bin/cli.js CHANGED
@@ -54,6 +54,10 @@ process.exitCode = await dispatchCli(process.argv.slice(2), {
54
54
  const { createPiPackagesPort } = await import("../dist/integrations/pi/engine/package-integration.js");
55
55
  return await runPackageCommand(request, { createPort: createPiPackagesPort });
56
56
  },
57
+ sessionColor: async arguments_ => {
58
+ const { runSessionColorCommand } = await import("../dist/cli/session-color.js");
59
+ return await runSessionColorCommand(arguments_);
60
+ },
57
61
  sessionContext: async request => {
58
62
  const { runSessionContextCommand } = await import("../dist/cli/session-context.js");
59
63
  return await runSessionContextCommand(request);
@@ -1,6 +1,7 @@
1
1
  /** The Pi comparison launch is a prerelease development instrument, not product. */
2
2
  export interface CliCapabilities {
3
3
  readonly developmentComparison: boolean;
4
+ readonly sessionColors?: boolean;
4
5
  /** Bare `update` stays on this build's channel; absent means stable. */
5
6
  readonly runningChannel?: "stable" | "next";
6
7
  }
@@ -1,7 +1,7 @@
1
1
  import { prerelease } from "semver";
2
2
  export function cliCapabilities(version) {
3
3
  const development = isPrereleaseVersion(version);
4
- return Object.freeze({ developmentComparison: development, runningChannel: development ? "next" : "stable" });
4
+ return Object.freeze({ developmentComparison: development, sessionColors: true, runningChannel: development ? "next" : "stable" });
5
5
  }
6
6
  export function isPrereleaseVersion(version) {
7
7
  return (prerelease(version)?.length ?? 0) > 0;
@@ -10,6 +10,7 @@ export interface CliHandlers {
10
10
  readonly update: (channel: UpdateChannel, target?: string) => Promise<number>;
11
11
  readonly packages: (request: PackageCommandRequest) => Promise<number>;
12
12
  readonly sessionContext: (request: SessionContextRequest) => Promise<number>;
13
+ readonly sessionColor?: (arguments_: readonly string[]) => Promise<number>;
13
14
  /** Removes A1 from the machine; absent in hosts that cannot uninstall. */
14
15
  readonly uninstall?: (request: {
15
16
  readonly yes: boolean;
@@ -51,6 +52,9 @@ export type CliCommand = {
51
52
  } | {
52
53
  readonly kind: "session-context";
53
54
  readonly request: SessionContextRequest;
55
+ } | {
56
+ readonly kind: "session-color";
57
+ readonly arguments: readonly string[];
54
58
  } | {
55
59
  readonly kind: "uninstall";
56
60
  readonly request: {
@@ -25,6 +25,7 @@ export function cliUsage(capabilities) {
25
25
  "worktree detach",
26
26
  "worktree complete [--change <id>] [--pr <number>]",
27
27
  "worktree list",
28
+ "session color list | get [--session <id>] | set <color> [--session <id>]",
28
29
  "session worktrees",
29
30
  "session link-worktree <path>",
30
31
  "session unlink-worktree",
@@ -63,6 +64,11 @@ export function cliHelp(capabilities) {
63
64
  ` ${command} worktree complete [--change <id>] [--pr <number>]`,
64
65
  ` ${command} worktree list`,
65
66
  "",
67
+ "Session colors (always JSON; explicit session id required outside the session shell):",
68
+ ` ${command} session color list`,
69
+ ` ${command} session color get --session <id>`,
70
+ ` ${command} session color set <color> --session <id>`,
71
+ "",
66
72
  "Session context (compatibility aliases):",
67
73
  ` ${command} session worktrees`,
68
74
  ` ${command} session link-worktree <path>`,
@@ -99,6 +105,12 @@ export async function dispatchCli(arguments_, handlers, output, capabilities) {
99
105
  return await handlers.packages(command.request);
100
106
  if (command.kind === "session-context")
101
107
  return await handlers.sessionContext(command.request);
108
+ if (command.kind === "session-color") {
109
+ if (handlers.sessionColor)
110
+ return await handlers.sessionColor(command.arguments);
111
+ output.stdout(`${JSON.stringify({ error: { code: "control-unavailable", message: "Session color API is unavailable in this host." } })}\n`);
112
+ return 1;
113
+ }
102
114
  if (command.kind === "uninstall")
103
115
  return handlers.uninstall ? await handlers.uninstall(command.request) : 2;
104
116
  return await handlers.update(command.channel, command.target);
@@ -137,6 +149,8 @@ export function parseCliCommand(arguments_, capabilities) {
137
149
  return parseUpdate(rest, capabilities.runningChannel ?? "stable");
138
150
  if (command === "worktree")
139
151
  return parseWorktree(rest);
152
+ if (command === "session" && rest[0] === "color")
153
+ return { kind: "session-color", arguments: rest.slice(1) };
140
154
  if (command === "session")
141
155
  return parseSessionContext(rest);
142
156
  if (command?.startsWith("update:"))
@@ -0,0 +1,6 @@
1
+ interface ColorCommandOptions {
2
+ readonly environment?: NodeJS.ProcessEnv;
3
+ readonly stdout?: (message: string) => void;
4
+ }
5
+ export declare function runSessionColorCommand(arguments_: readonly string[], options?: ColorCommandOptions): Promise<number>;
6
+ export {};
@@ -0,0 +1,44 @@
1
+ import { UI_ACCENT_COLORS } from "../contracts/owned-ui/index.js";
2
+ import { requestSessionControl, resolveProductPaths } from "../foundation/lifecycle/index.js";
3
+ import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../product-identity.js";
4
+ const failure = (code, message) => Object.assign(new Error(message), { code });
5
+ export async function runSessionColorCommand(arguments_, options = {}) {
6
+ const stdout = options.stdout ?? (message => process.stdout.write(message));
7
+ const environment = options.environment ?? process.env;
8
+ try {
9
+ const [action, ...rest] = arguments_;
10
+ if (action === "help" || action === "--help" || action === "-h") {
11
+ stdout(`${PRODUCT_TEXT.commandName} session color list | get [--session <id>] | set <color> [--session <id>]\nAlways JSON; get/set target Current color only. Outside an A1 shell, --session is required.\n`);
12
+ return 0;
13
+ }
14
+ if (action === "list" && rest.length === 0) {
15
+ stdout(`${JSON.stringify({ colors: UI_ACCENT_COLORS })}\n`);
16
+ return 0;
17
+ }
18
+ if (action !== "get" && action !== "set")
19
+ throw failure("invalid-request", "Use session color list, get, or set <color>.");
20
+ const color = action === "set" ? rest.shift() : undefined;
21
+ if (action === "set" && !UI_ACCENT_COLORS.includes(color))
22
+ throw failure("invalid-color", "Choose blue, green, orange, pink, purple, cyan, or grey.");
23
+ if (rest.length !== 0 && (rest.length !== 2 || rest[0] !== "--session" || !rest[1]))
24
+ throw failure("invalid-request", "Expected only --session followed by a complete session id.");
25
+ const explicit = rest.length === 2;
26
+ const runtimeId = environment[PRODUCT_IDENTITY.environment.sessionRuntimeId];
27
+ const sessionId = explicit ? rest[1] : runtimeId && environment.PI_SESSION_FILE ? environment.PI_SESSION_ID : undefined;
28
+ if (!sessionId)
29
+ throw failure("session-required", "Supply --session <id> outside an exact A1 shell runtime.");
30
+ const result = await requestSessionControl({
31
+ runtimeDir: resolveProductPaths(environment).runtimeDir, sessionId,
32
+ ...(!explicit && runtimeId ? { runtimeId } : {}),
33
+ payload: { action, ...(color === undefined ? {} : { color }) },
34
+ });
35
+ stdout(`${JSON.stringify(result)}\n`);
36
+ return 0;
37
+ }
38
+ catch (error) {
39
+ const detail = error;
40
+ const code = detail.code ?? "control-failed";
41
+ stdout(`${JSON.stringify({ error: { code, message: detail.message } })}\n`);
42
+ return ["invalid-request", "invalid-color", "invalid-session", "session-required"].includes(code) ? 2 : 1;
43
+ }
44
+ }
@@ -1,12 +1,12 @@
1
1
  import type { AgentSettingsPort } from "../contracts/agent-engine/index.js";
2
- import { OwnedSettingsManager } from "../ui/settings/manager.js";
2
+ import type { OwnedSettingsManager } from "../ui/settings/manager.js";
3
3
  interface OwnedSettingsManagerFactoryOptions {
4
4
  readonly profileId: string;
5
5
  readonly agent?: AgentSettingsPort | null;
6
6
  readonly agentProvider?: () => AgentSettingsPort | null;
7
7
  }
8
8
  /** Builds the profile-global settings manager shared by the Agent and slim utility compositions. */
9
- export declare function createOwnedSettingsManager(options: OwnedSettingsManagerFactoryOptions): OwnedSettingsManager;
9
+ export declare function createOwnedSettingsManager(options: OwnedSettingsManagerFactoryOptions): Promise<OwnedSettingsManager>;
10
10
  /** Rereads the shared settings document whenever its stat changes; returns the stop. */
11
11
  export declare function watchSharedSettings(settings: Pick<OwnedSettingsManager, "file" | "refresh">): () => void;
12
12
  export {};
@@ -1,9 +1,9 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { resolveLaunchProfilePaths } from "../features/launch/profile-paths.js";
3
3
  import { PRODUCT_IDENTITY } from "../product-identity.js";
4
- import { OwnedSettingsManager } from "../ui/settings/manager.js";
5
4
  /** Builds the profile-global settings manager shared by the Agent and slim utility compositions. */
6
- export function createOwnedSettingsManager(options) {
5
+ export async function createOwnedSettingsManager(options) {
6
+ const { OwnedSettingsManager } = await import("../ui/settings/manager.js");
7
7
  const profilePaths = resolveLaunchProfilePaths();
8
8
  return new OwnedSettingsManager({
9
9
  profileDir: profilePaths.agentProfile,
@@ -39,7 +39,7 @@ export async function composeOwnedUi(options = {}) {
39
39
  let backend;
40
40
  const settings = options.profileId === undefined
41
41
  ? null
42
- : createOwnedSettingsManager({
42
+ : await createOwnedSettingsManager({
43
43
  profileId: options.profileId,
44
44
  agentProvider: () => backend.settingsPort(),
45
45
  });
@@ -72,18 +72,12 @@ export async function composeOwnedUi(options = {}) {
72
72
  ...(options.packageVersion === undefined ? {} : { packageVersion: options.packageVersion }),
73
73
  ...(options.profileId === undefined ? {} : { profileId: options.profileId }),
74
74
  });
75
- const unsubscribeAccentChange = bareSettings === null
76
- ? () => { }
77
- : bareSettings.onChange(current => {
78
- const accent = current.value("accentColor");
79
- host.setAccentColor(accent);
80
- });
81
- // Rationale: separate A1 processes share one settings document; a live scope change saved in
82
- // one reaches the others only by rereading that document.
75
+ const sessionColor = bindSessionColor(backend, bareSettings, color => host.setAccentColor(color), productPaths.runtimeDir);
76
+ // Rationale: peer refresh shares persisted preferences, never session-local Current color.
83
77
  const unwatchSharedSettings = bareSettings === null
84
78
  ? () => { }
85
79
  : watchSharedSettings(bareSettings);
86
- const unsubscribeAccent = () => { unwatchSharedSettings(); unsubscribeAccentChange(); };
80
+ const unsubscribeAccent = async () => { unwatchSharedSettings(); await sessionColor.dispose(); };
87
81
  // Invariant: the Settings search and agent prompt borrow the same resolved Pi keybindings.
88
82
  const keybindingsHost = createPiKeybindingsHost({ profile: bare ? "a1" : "pi", agentDir: backend.identity.agentDir });
89
83
  // Rationale: Routes open only after shell construction.
@@ -118,7 +112,7 @@ export async function composeOwnedUi(options = {}) {
118
112
  const releaseShared = async () => {
119
113
  naming.dispose();
120
114
  clipboardServices?.dispose();
121
- unsubscribeAccent();
115
+ await unsubscribeAccent();
122
116
  await sessionRuntimes.dispose();
123
117
  await releaseNote.releaseClaim();
124
118
  suggestionDiagnostics?.dispose();
@@ -184,6 +178,7 @@ export async function composeOwnedUi(options = {}) {
184
178
  exitNotice = import("../app/session-shell/exit-notice.js")
185
179
  .then(module => module.armExitNotice(backend, exitNoticePath), () => null);
186
180
  presenter.start();
181
+ sessionColor.start();
187
182
  releaseUpdates.start();
188
183
  },
189
184
  flush: () => backend.session.flushEvents(),
@@ -204,6 +199,31 @@ export async function composeOwnedUi(options = {}) {
204
199
  };
205
200
  return { application, settings };
206
201
  }
202
+ function bindSessionColor(backend, settings, apply, runtimeDir) {
203
+ if (settings === null)
204
+ return { start() { }, async dispose() { } };
205
+ let control = Promise.resolve(null);
206
+ let started = false, stopped = false;
207
+ const unsubscribe = settings.onChange(current => apply(current.value("accentColor")));
208
+ let binding = backend.identity.sessionBindingGeneration;
209
+ const unbind = backend.session.onEvent(() => {
210
+ if (binding === backend.identity.sessionBindingGeneration)
211
+ return;
212
+ binding = backend.identity.sessionBindingGeneration;
213
+ settings.startSession();
214
+ });
215
+ return {
216
+ start() {
217
+ if (started || stopped)
218
+ return;
219
+ started = true;
220
+ control = import("./session-colors.js").then(module => stopped ? null
221
+ : module.startSessionColorControl(backend, settings, runtimeDir, process.env[PRODUCT_IDENTITY.environment.sessionRuntimeId]))
222
+ .catch(() => null);
223
+ },
224
+ async dispose() { stopped = true; unbind(); unsubscribe(); await (await control)?.dispose(); },
225
+ };
226
+ }
207
227
  function spinnerPresenterOptions(settings) {
208
228
  return settings === null ? {} : { spinner: {
209
229
  snapshot: () => settings.value("spinnerStyle"),
@@ -0,0 +1,5 @@
1
+ import { type OwnedUiSessionBackend } from "../contracts/owned-ui/index.js";
2
+ import type { OwnedSettingsManager } from "../ui/settings/manager.js";
3
+ export declare function startSessionColorControl(backend: OwnedUiSessionBackend, settings: OwnedSettingsManager, runtimeDir: string, runtimeId?: string): Promise<{
4
+ dispose: () => Promise<void>;
5
+ }>;
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { UI_ACCENT_COLORS } from "../contracts/owned-ui/index.js";
3
+ import { serveSessionControl } from "../foundation/lifecycle/session-control.js";
4
+ export async function startSessionColorControl(backend, settings, runtimeDir, runtimeId = randomUUID()) {
5
+ const identity = () => ({ sessionId: backend.identity.currentHistorySessionId() ?? backend.identity.sessionId, binding: backend.identity.sessionBindingGeneration });
6
+ const control = await serveSessionControl({ runtimeDir, runtimeId, identity, handle: async (payload) => {
7
+ if (backend.identity.disposed)
8
+ throw Object.assign(new Error("The session is stopped."), { code: "session-unavailable" });
9
+ if (!payload || typeof payload !== "object")
10
+ throw Object.assign(new Error("Invalid color request."), { code: "invalid-request" });
11
+ const request = payload;
12
+ const sessionId = identity().sessionId;
13
+ if (request.action === "set") {
14
+ if (!UI_ACCENT_COLORS.includes(request.color))
15
+ throw Object.assign(new Error("Unknown color."), { code: "invalid-color" });
16
+ const result = await settings.change("a1", "accentColor", request.color);
17
+ if (!result.applied)
18
+ throw Object.assign(new Error(result.failure ?? "Color could not be applied."), { code: "control-failed" });
19
+ return { sessionId, color: result.effectiveValue };
20
+ }
21
+ if (request.action !== "get")
22
+ throw Object.assign(new Error("Unknown color action."), { code: "invalid-request" });
23
+ return { sessionId, color: settings.value("accentColor") };
24
+ } });
25
+ let stopped = false, binding = backend.identity.sessionBindingGeneration, publication = Promise.resolve();
26
+ const unsubscribe = backend.session.onEvent(() => {
27
+ if (stopped || binding === backend.identity.sessionBindingGeneration)
28
+ return;
29
+ binding = backend.identity.sessionBindingGeneration;
30
+ publication = publication.then(() => control.publish()).catch(() => { });
31
+ });
32
+ return { dispose: async () => { stopped = true; unsubscribe(); await publication; await control.dispose(); } };
33
+ }
@@ -1,6 +1,6 @@
1
1
  export type { OwnedUiSessionNameState, OwnedUiSessionNameCompletion, OwnedUiSessionNamingPort } from "./session-naming.js";
2
- /** Stable stored choices for bare A1's semantic UI accent. */
3
- export type UiAccentColor = "purple" | "blue" | "cyan" | "green" | "orange" | "pink";
2
+ export declare const UI_ACCENT_COLORS: readonly ["blue", "green", "orange", "pink", "purple", "cyan", "grey"];
3
+ export type UiAccentColor = typeof UI_ACCENT_COLORS[number];
4
4
  export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
5
5
  export type { OwnedUiExtensionAutocompleteItem, OwnedUiExtensionAutocompleteProvider, OwnedUiExtensionAutocompleteProviderFactory, OwnedUiExtensionAutocompleteSuggestions, OwnedUiExtensionComponent, OwnedUiExtensionComponentFactory, OwnedUiExtensionComponentFactoryContext, OwnedUiExtensionCustomFactory, OwnedUiExtensionCustomOptions, OwnedUiExtensionDialogOptions, OwnedUiExtensionEditorComponent, OwnedUiExtensionEditorFactory, OwnedUiExtensionEditorThemePort, OwnedUiExtensionEntryRenderInput, OwnedUiExtensionFooterDataPort, OwnedUiExtensionFooterFactory, OwnedUiExtensionInputListener, OwnedUiExtensionKeybindingsPort, OwnedUiExtensionMessageRenderInput, OwnedUiExtensionNotificationSeverity, OwnedUiExtensionOverlayAnchor, OwnedUiExtensionOverlayHandle, OwnedUiExtensionOverlayMargin, OwnedUiExtensionOverlayOptions, OwnedUiExtensionRenderCallback, OwnedUiExtensionRendererPort, OwnedUiExtensionRuntimePort, OwnedUiExtensionSelectListThemePort, OwnedUiExtensionSizeValue, OwnedUiExtensionTerminalPort, OwnedUiExtensionThemeBackground, OwnedUiExtensionThemeColor, OwnedUiExtensionThemeInfo, OwnedUiExtensionThemePort, OwnedUiExtensionToolRenderContext, OwnedUiExtensionUiCallback, OwnedUiExtensionUiPort, OwnedUiExtensionUiProperty, OwnedUiExtensionWidgetOptions, OwnedUiExtensionWidgetPlacement, OwnedUiExtensionWorkingIndicatorOptions, } from "./extension-ui.js";
6
6
  export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
@@ -1,3 +1,4 @@
1
+ export const UI_ACCENT_COLORS = Object.freeze(["blue", "green", "orange", "pink", "purple", "cyan", "grey"]);
1
2
  export { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiExtensionUiPort, } from "./extension-ui.js";
2
3
  export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
3
4
  export { OWNED_UI_HIDDEN_COMMAND_NAMES, OWNED_UI_MODELS_COMMAND_NAME, OWNED_UI_WORKFLOW_COMMAND_NAMES, } from "./session-workflows.js";
@@ -29,7 +29,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
29
29
  };
30
30
  };
31
31
  };
32
- purple: {
32
+ green: {
33
33
  dark: {
34
34
  truecolor: {
35
35
  accent: string;
@@ -55,7 +55,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
55
55
  };
56
56
  };
57
57
  };
58
- cyan: {
58
+ orange: {
59
59
  dark: {
60
60
  truecolor: {
61
61
  accent: string;
@@ -81,7 +81,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
81
81
  };
82
82
  };
83
83
  };
84
- green: {
84
+ pink: {
85
85
  dark: {
86
86
  truecolor: {
87
87
  accent: string;
@@ -107,7 +107,7 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
107
107
  };
108
108
  };
109
109
  };
110
- orange: {
110
+ purple: {
111
111
  dark: {
112
112
  truecolor: {
113
113
  accent: string;
@@ -133,7 +133,33 @@ export declare const SHARED_PROGRESS_PALETTE: Readonly<{
133
133
  };
134
134
  };
135
135
  };
136
- pink: {
136
+ cyan: {
137
+ dark: {
138
+ truecolor: {
139
+ accent: string;
140
+ border: string;
141
+ selectedBg: string;
142
+ };
143
+ "256color": {
144
+ accent: string;
145
+ border: string;
146
+ selectedBg: string;
147
+ };
148
+ };
149
+ light: {
150
+ truecolor: {
151
+ accent: string;
152
+ border: string;
153
+ selectedBg: string;
154
+ };
155
+ "256color": {
156
+ accent: string;
157
+ border: string;
158
+ selectedBg: string;
159
+ };
160
+ };
161
+ };
162
+ grey: {
137
163
  dark: {
138
164
  truecolor: {
139
165
  accent: string;
@@ -2,5 +2,5 @@
2
2
  * Generated by scripts/pi/sync-progress-palette.ts from A1's named accent projection
3
3
  * over pinned Pi 1.5.2 (MIT), commit b4afdddf44e059c67ccf5097e5317d913521b470. Do not edit directly.
4
4
  */
5
- export const SHARED_PROGRESS_PALETTE = Object.freeze({ "blue": { "dark": { "truecolor": { "accent": "\u001b[38;2;130;160;241m", "border": "\u001b[38;2;116;102;197m", "selectedBg": "\u001b[48;2;52;57;69m" }, "256color": { "accent": "\u001b[38;5;111m", "border": "\u001b[38;5;98m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;63;96;226m", "border": "\u001b[38;2;83;67;154m", "selectedBg": "\u001b[48;2;243;244;246m" }, "256color": { "accent": "\u001b[38;5;62m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "purple": { "dark": { "truecolor": { "accent": "\u001b[38;2;167;152;215m", "border": "\u001b[38;2;105;115;167m", "selectedBg": "\u001b[48;2;58;56;66m" }, "256color": { "accent": "\u001b[38;5;140m", "border": "\u001b[38;5;61m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;116;89;180m", "border": "\u001b[38;2;68;77;130m", "selectedBg": "\u001b[48;2;243;243;245m" }, "256color": { "accent": "\u001b[38;5;97m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "cyan": { "dark": { "truecolor": { "accent": "\u001b[38;2;93;179;186m", "border": "\u001b[38;2;74;128;146m", "selectedBg": "\u001b[48;2;48;60;61m" }, "256color": { "accent": "\u001b[38;5;73m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;40;122;129m", "border": "\u001b[38;2;40;89;106m", "selectedBg": "\u001b[48;2;241;244;244m" }, "256color": { "accent": "\u001b[38;5;30m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "green": { "dark": { "truecolor": { "accent": "\u001b[38;2;104;183;141m", "border": "\u001b[38;2;75;132;120m", "selectedBg": "\u001b[48;2;50;60;54m" }, "256color": { "accent": "\u001b[38;5;72m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;51;126;88m", "border": "\u001b[38;2;41;93;82m", "selectedBg": "\u001b[48;2;242;244;243m" }, "256color": { "accent": "\u001b[38;5;65m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "orange": { "dark": { "truecolor": { "accent": "\u001b[38;2;231;135;83m", "border": "\u001b[38;2;178;89;79m", "selectedBg": "\u001b[48;2;67;54;48m" }, "256color": { "accent": "\u001b[38;5;173m", "border": "\u001b[38;5;131m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;174;80;16m", "border": "\u001b[38;2;133;57;49m", "selectedBg": "\u001b[48;2;245;243;242m" }, "256color": { "accent": "\u001b[38;5;130m", "border": "\u001b[38;5;95m", "selectedBg": "\u001b[48;5;255m" } } }, "pink": { "dark": { "truecolor": { "accent": "\u001b[38;2;223;122;199m", "border": "\u001b[38;2;150;91;171m", "selectedBg": "\u001b[48;2;65;53;62m" }, "256color": { "accent": "\u001b[38;5;176m", "border": "\u001b[38;5;97m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;178;56;153m", "border": "\u001b[38;2;114;61;131m", "selectedBg": "\u001b[48;2;245;243;245m" }, "256color": { "accent": "\u001b[38;5;132m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } } });
5
+ export const SHARED_PROGRESS_PALETTE = Object.freeze({ "blue": { "dark": { "truecolor": { "accent": "\u001b[38;2;130;160;241m", "border": "\u001b[38;2;116;102;197m", "selectedBg": "\u001b[48;2;52;57;69m" }, "256color": { "accent": "\u001b[38;5;111m", "border": "\u001b[38;5;98m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;63;96;226m", "border": "\u001b[38;2;83;67;154m", "selectedBg": "\u001b[48;2;243;244;246m" }, "256color": { "accent": "\u001b[38;5;62m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "green": { "dark": { "truecolor": { "accent": "\u001b[38;2;104;183;141m", "border": "\u001b[38;2;75;132;120m", "selectedBg": "\u001b[48;2;50;60;54m" }, "256color": { "accent": "\u001b[38;5;72m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;51;126;88m", "border": "\u001b[38;2;41;93;82m", "selectedBg": "\u001b[48;2;242;244;243m" }, "256color": { "accent": "\u001b[38;5;65m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "orange": { "dark": { "truecolor": { "accent": "\u001b[38;2;231;135;83m", "border": "\u001b[38;2;178;89;79m", "selectedBg": "\u001b[48;2;67;54;48m" }, "256color": { "accent": "\u001b[38;5;173m", "border": "\u001b[38;5;131m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;174;80;16m", "border": "\u001b[38;2;133;57;49m", "selectedBg": "\u001b[48;2;245;243;242m" }, "256color": { "accent": "\u001b[38;5;130m", "border": "\u001b[38;5;95m", "selectedBg": "\u001b[48;5;255m" } } }, "pink": { "dark": { "truecolor": { "accent": "\u001b[38;2;223;122;199m", "border": "\u001b[38;2;150;91;171m", "selectedBg": "\u001b[48;2;65;53;62m" }, "256color": { "accent": "\u001b[38;5;176m", "border": "\u001b[38;5;97m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;178;56;153m", "border": "\u001b[38;2;114;61;131m", "selectedBg": "\u001b[48;2;245;243;245m" }, "256color": { "accent": "\u001b[38;5;132m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "purple": { "dark": { "truecolor": { "accent": "\u001b[38;2;167;152;215m", "border": "\u001b[38;2;105;115;167m", "selectedBg": "\u001b[48;2;58;56;66m" }, "256color": { "accent": "\u001b[38;5;140m", "border": "\u001b[38;5;61m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;116;89;180m", "border": "\u001b[38;2;68;77;130m", "selectedBg": "\u001b[48;2;243;243;245m" }, "256color": { "accent": "\u001b[38;5;97m", "border": "\u001b[38;5;60m", "selectedBg": "\u001b[48;5;255m" } } }, "cyan": { "dark": { "truecolor": { "accent": "\u001b[38;2;93;179;186m", "border": "\u001b[38;2;74;128;146m", "selectedBg": "\u001b[48;2;48;60;61m" }, "256color": { "accent": "\u001b[38;5;73m", "border": "\u001b[38;5;66m", "selectedBg": "\u001b[48;5;59m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;40;122;129m", "border": "\u001b[38;2;40;89;106m", "selectedBg": "\u001b[48;2;241;244;244m" }, "256color": { "accent": "\u001b[38;5;30m", "border": "\u001b[38;5;23m", "selectedBg": "\u001b[48;5;255m" } } }, "grey": { "dark": { "truecolor": { "accent": "\u001b[38;2;163;163;163m", "border": "\u001b[38;2;119;119;119m", "selectedBg": "\u001b[48;2;57;57;57m" }, "256color": { "accent": "\u001b[38;5;247m", "border": "\u001b[38;5;243m", "selectedBg": "\u001b[48;5;237m" } }, "light": { "truecolor": { "accent": "\u001b[38;2;111;111;111m", "border": "\u001b[38;2;84;84;84m", "selectedBg": "\u001b[48;2;244;244;244m" }, "256color": { "accent": "\u001b[38;5;242m", "border": "\u001b[38;5;240m", "selectedBg": "\u001b[48;5;255m" } } } });
6
6
  export const SHARED_PROGRESS_ACCENT_ANSI = SHARED_PROGRESS_PALETTE.blue.dark.truecolor.accent;
@@ -732,7 +732,7 @@ export class SettingsApp {
732
732
  const state = {
733
733
  choices: menu.choices.map(choice => displaySettingValue(menu.entry, choice)),
734
734
  ...(menu.preview !== null ? { previews: menu.preview.frames(menu.choices, menu.index, theme) }
735
- : menu.entry.id === "accentColor" ? {
735
+ : ["accentColor", "defaultAccentColor"].includes(menu.entry.id) ? {
736
736
  previews: menu.choices.map(choice => theme.accentPreview?.(String(choice), "■") ?? ""),
737
737
  } : {}),
738
738
  current: this.#edits.shown(menu.entry) === null ? null : displaySettingValue(menu.entry, this.#edits.shown(menu.entry)),
@@ -2,6 +2,8 @@ export { assertLaunchProfileId } from "./model.js";
2
2
  export type { LaunchProfileId, ProcessIdentity } from "./model.js";
3
3
  export { PID_LIVENESS_IDENTITY, inspectLiveProcess } from "./process-liveness.js";
4
4
  export { resolveProductPaths } from "./paths.js";
5
+ export { serveSessionControl, requestSessionControl } from "./session-control.js";
6
+ export type { SessionControlHostOptions, SessionControlIdentity, SessionControlRequestOptions } from "./session-control.js";
5
7
  export type { ProductPaths } from "./paths.js";
6
8
  export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
7
9
  export type { SessionSelection } from "./session-selection.js";
@@ -1,6 +1,7 @@
1
1
  export { assertLaunchProfileId } from "./model.js";
2
2
  export { PID_LIVENESS_IDENTITY, inspectLiveProcess } from "./process-liveness.js";
3
3
  export { resolveProductPaths } from "./paths.js";
4
+ export { serveSessionControl, requestSessionControl } from "./session-control.js";
4
5
  export { parseSessionSelection, sessionSelectionArguments } from "./session-selection.js";
5
6
  export { PRIMARY_SESSION_AGENT_ID, activateSessionRepositoryContext, clearSessionRepositoryContext, listSessionRepositoryWorktrees, readActiveSessionRepositoryContext, readSessionStartupRoot, readSessionRepositoryContext, registerSessionRepositoryRuntime, releaseInvalidSessionRepositoryContext, releaseSessionRepositoryRuntime, setSessionRepositoryContext, withSessionRepositoryObservation, } from "./session-repository-context.js";
6
7
  export { retrySessionCoordination } from "./session-context-retry.js";
@@ -0,0 +1,26 @@
1
+ import type { ProcessIdentity } from "./model.js";
2
+ export interface SessionControlIdentity {
3
+ readonly sessionId: string;
4
+ readonly binding: number;
5
+ }
6
+ interface ControlOptions {
7
+ readonly runtimeDir: string;
8
+ readonly inspectProcess?: (pid: number) => Promise<ProcessIdentity | null>;
9
+ }
10
+ export interface SessionControlHostOptions extends ControlOptions {
11
+ readonly runtimeId: string;
12
+ readonly identity: () => SessionControlIdentity;
13
+ readonly handle: (payload: unknown) => Promise<unknown>;
14
+ }
15
+ export interface SessionControlRequestOptions extends ControlOptions {
16
+ readonly sessionId: string;
17
+ readonly runtimeId?: string;
18
+ readonly payload: unknown;
19
+ readonly timeoutMs?: number;
20
+ }
21
+ export declare function serveSessionControl(options: SessionControlHostOptions): Promise<{
22
+ publish(): Promise<void>;
23
+ dispose(): Promise<void>;
24
+ }>;
25
+ export declare function requestSessionControl(options: SessionControlRequestOptions): Promise<unknown>;
26
+ export {};
@@ -0,0 +1,222 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readdir, open, realpath, rename, unlink, writeFile, chmod } from "node:fs/promises";
3
+ import { createConnection, createServer } from "node:net";
4
+ import { join, resolve } from "node:path";
5
+ import { inspectLiveProcess } from "./process-liveness.js";
6
+ const LIMIT = 8192;
7
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/iu;
8
+ const SLUG = /^[a-z0-9-]{1,128}$/iu;
9
+ const fail = (code, message) => Object.assign(new Error(message), { code });
10
+ const hash = (value) => createHash("sha256").update(value).digest("hex").slice(0, 32);
11
+ const address = (directory, id) => process.platform === "win32"
12
+ ? `\\\\.\\pipe\\session-control-${hash(directory)}-${id}` : join(directory, `${id}.sock`);
13
+ async function privateDirectory(runtimeDir, create) {
14
+ const selectedRoot = resolve(runtimeDir);
15
+ if (create)
16
+ await mkdir(selectedRoot, { recursive: true, mode: 0o700 });
17
+ const root = await realpath(selectedRoot);
18
+ const directory = join(root, "session-controls");
19
+ if (create)
20
+ await mkdir(directory, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST")
21
+ throw error; });
22
+ const stat = await lstat(directory);
23
+ if (!stat.isDirectory() || stat.isSymbolicLink() || resolve(await realpath(directory)) !== directory) {
24
+ throw fail("control-boundary", "Session control directory is not a private regular directory.");
25
+ }
26
+ if (process.platform !== "win32" && (stat.mode & 0o077) !== 0)
27
+ throw fail("control-boundary", "Session control directory permissions are not private.");
28
+ return directory;
29
+ }
30
+ async function readRegistration(file) {
31
+ const stat = await lstat(file);
32
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > LIMIT || (process.platform !== "win32" && (stat.mode & 0o077) !== 0)) {
33
+ throw fail("control-boundary", "Invalid session control record.");
34
+ }
35
+ const handle = await open(file, "r");
36
+ let raw;
37
+ try {
38
+ const opened = await handle.stat();
39
+ if (opened.dev !== stat.dev || opened.ino !== stat.ino || !opened.isFile())
40
+ throw fail("control-boundary", "Session control record changed during lookup.");
41
+ const buffer = Buffer.alloc(LIMIT + 1);
42
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
43
+ if (bytesRead > LIMIT)
44
+ throw fail("control-boundary", "Session control record exceeds the size limit.");
45
+ raw = buffer.subarray(0, bytesRead).toString("utf8");
46
+ }
47
+ finally {
48
+ await handle.close();
49
+ }
50
+ let value;
51
+ try {
52
+ value = JSON.parse(raw);
53
+ }
54
+ catch {
55
+ throw fail("control-boundary", "Malformed session control record.");
56
+ }
57
+ if (!value || typeof value !== "object")
58
+ throw fail("control-boundary", "Malformed session control record.");
59
+ if (value.version !== 1 || !UUID.test(value.id) || !UUID.test(value.sessionId) || typeof value.runtimeId !== "string" || !SLUG.test(value.runtimeId)
60
+ || !UUID.test(value.nonce) || !Number.isSafeInteger(value.binding) || value.binding < 0
61
+ || !Number.isSafeInteger(value.owner?.pid) || value.owner.pid <= 0 || typeof value.owner.startIdentity !== "string") {
62
+ throw fail("control-boundary", "Malformed session control record.");
63
+ }
64
+ return value;
65
+ }
66
+ function receive(socket, consume) {
67
+ let data = "", complete = false;
68
+ socket.setEncoding("utf8");
69
+ socket.on("data", chunk => {
70
+ if (complete) {
71
+ socket.destroy();
72
+ return;
73
+ }
74
+ data += chunk;
75
+ if (Buffer.byteLength(data) > LIMIT) {
76
+ socket.destroy();
77
+ return;
78
+ }
79
+ const newline = data.indexOf("\n");
80
+ if (newline < 0)
81
+ return;
82
+ complete = true;
83
+ if (data.slice(newline + 1).trim() !== "") {
84
+ socket.destroy();
85
+ return;
86
+ }
87
+ try {
88
+ consume(JSON.parse(data.slice(0, newline)));
89
+ }
90
+ catch {
91
+ socket.destroy();
92
+ }
93
+ });
94
+ }
95
+ export async function serveSessionControl(options) {
96
+ if (!SLUG.test(options.runtimeId))
97
+ throw fail("session-unavailable", "Runtime identity is invalid.");
98
+ const directory = await privateDirectory(options.runtimeDir, true);
99
+ const inspect = options.inspectProcess ?? inspectLiveProcess;
100
+ const owner = await inspect(process.pid);
101
+ if (!owner || owner.pid !== process.pid || !owner.startIdentity)
102
+ throw fail("session-unavailable", "Runtime identity cannot be verified.");
103
+ const id = randomUUID(), nonce = randomUUID(), file = join(directory, `${id}.json`);
104
+ let disposed = false, queue = Promise.resolve(), publication = Promise.resolve();
105
+ const sockets = new Set();
106
+ const server = createServer(socket => {
107
+ sockets.add(socket);
108
+ socket.setTimeout(5000, () => socket.destroy());
109
+ socket.on("error", () => { });
110
+ socket.on("close", () => sockets.delete(socket));
111
+ receive(socket, input => {
112
+ queue = queue.then(async () => {
113
+ try {
114
+ const request = input;
115
+ const current = options.identity();
116
+ if (disposed || socket.destroyed || request.nonce !== nonce || request.runtimeId !== options.runtimeId
117
+ || request.sessionId !== current.sessionId || request.binding !== current.binding) {
118
+ throw fail("session-unavailable", "The selected session is no longer active.");
119
+ }
120
+ if (!UUID.test(request.requestId) || !Number.isFinite(request.deadline) || request.deadline <= Date.now() || request.deadline > Date.now() + 5000) {
121
+ throw fail("request-expired", "Session request expired before application.");
122
+ }
123
+ // Concurrency: no await separates the identity/deadline check from calling the live handler.
124
+ const result = await options.handle(request.payload);
125
+ if (!socket.destroyed)
126
+ socket.end(`${JSON.stringify({ requestId: request.requestId, result })}\n`);
127
+ }
128
+ catch (error) {
129
+ const failure = error;
130
+ if (!socket.destroyed)
131
+ socket.end(`${JSON.stringify({ error: { code: failure.code ?? "control-failed", message: failure.message } })}\n`);
132
+ }
133
+ }).catch(() => { socket.destroy(); });
134
+ });
135
+ });
136
+ await new Promise((done, reject) => { server.once("error", reject); server.listen(address(directory, id), () => { server.off("error", reject); done(); }); });
137
+ server.on("error", () => { });
138
+ server.maxConnections = 64;
139
+ const publish = () => {
140
+ publication = publication.catch(() => { }).then(async () => {
141
+ if (disposed)
142
+ return;
143
+ const identity = options.identity();
144
+ if (!UUID.test(identity.sessionId) || !Number.isSafeInteger(identity.binding) || identity.binding < 0)
145
+ throw fail("session-unavailable", "Session identity is not available.");
146
+ const temporary = join(directory, `${id}.${randomUUID()}.tmp`);
147
+ try {
148
+ await writeFile(temporary, JSON.stringify({ version: 1, id, nonce, runtimeId: options.runtimeId, owner, ...identity }), { mode: 0o600, flag: "wx" });
149
+ await rename(temporary, file);
150
+ }
151
+ finally {
152
+ await unlink(temporary).catch(() => { });
153
+ }
154
+ });
155
+ return publication;
156
+ };
157
+ const dispose = async () => {
158
+ if (disposed)
159
+ return;
160
+ disposed = true;
161
+ for (const socket of sockets)
162
+ socket.destroy();
163
+ await new Promise(done => server.close(() => done()));
164
+ await queue;
165
+ await publication.catch(() => { });
166
+ await unlink(file).catch(() => { });
167
+ };
168
+ try {
169
+ if (process.platform !== "win32")
170
+ await chmod(address(directory, id), 0o600);
171
+ await publish();
172
+ }
173
+ catch (error) {
174
+ await dispose();
175
+ throw error;
176
+ }
177
+ return { publish, dispose };
178
+ }
179
+ export async function requestSessionControl(options) {
180
+ if (!UUID.test(options.sessionId))
181
+ throw fail("invalid-session", "Supply a complete session UUID.");
182
+ const budget = Math.min(5000, Math.max(1, options.timeoutMs ?? 3000));
183
+ const deadline = Date.now() + budget;
184
+ const directory = await privateDirectory(options.runtimeDir, false).catch(() => { throw fail("session-unavailable", "No live session control is available."); });
185
+ const names = (await readdir(directory)).filter(name => UUID.test(name.slice(0, -5)) && name.endsWith(".json"));
186
+ if (names.length > 256)
187
+ throw fail("control-limit", "Too many session records to resolve safely.");
188
+ const matches = [];
189
+ for (const name of names) {
190
+ const record = await readRegistration(join(directory, name)).catch(error => { if (error.code === "ENOENT")
191
+ return null; throw error; });
192
+ if (record === null || `${record.id}.json` !== name || record.sessionId !== options.sessionId || (options.runtimeId !== undefined && record.runtimeId !== options.runtimeId))
193
+ continue;
194
+ const owner = await (options.inspectProcess ?? inspectLiveProcess)(record.owner.pid);
195
+ if (owner?.startIdentity === record.owner.startIdentity)
196
+ matches.push(record);
197
+ if (Date.now() >= deadline)
198
+ throw fail("request-expired", "Session lookup exceeded its deadline.");
199
+ }
200
+ if (matches.length !== 1)
201
+ throw fail(matches.length > 1 ? "ambiguous-session" : "session-unavailable", "Select one running bare session by its complete session id.");
202
+ const record = matches[0];
203
+ return await new Promise((done, reject) => {
204
+ const socket = createConnection(address(directory, record.id));
205
+ const requestId = randomUUID();
206
+ const timer = setTimeout(() => { socket.destroy(); reject(fail("request-expired", "Session request timed out; application was not acknowledged.")); }, Math.max(1, deadline - Date.now()));
207
+ const finish = () => { clearTimeout(timer); socket.destroy(); };
208
+ socket.on("error", () => { finish(); reject(fail("session-unavailable", "The selected session could not be reached.")); });
209
+ socket.on("close", () => { clearTimeout(timer); reject(fail("session-unavailable", "The selected session closed before acknowledgement.")); });
210
+ socket.on("connect", () => socket.write(`${JSON.stringify({ ...record, requestId, deadline, payload: options.payload })}\n`));
211
+ receive(socket, input => {
212
+ const response = input;
213
+ finish();
214
+ if (response.error)
215
+ reject(fail(response.error.code, response.error.message));
216
+ else if (response.requestId !== requestId)
217
+ reject(fail("control-failed", "Session acknowledgement did not match the request."));
218
+ else
219
+ done(response.result);
220
+ });
221
+ });
222
+ }
@@ -1,4 +1,5 @@
1
- type UiAccentColor = "blue" | "purple" | "cyan" | "green" | "orange" | "pink";
1
+ import { SHARED_PROGRESS_PALETTE } from "../../contracts/presentation/index.js";
2
+ type UiAccentColor = keyof typeof SHARED_PROGRESS_PALETTE;
2
3
  type EarlyAccentAppearance = "dark" | "light";
3
4
  type EarlyAccentMode = "truecolor" | "256color";
4
5
  /** Read only global profile data, never project settings or the interactive resource pipeline. */
@@ -45,6 +45,7 @@ const ACCENT_PALETTE = Object.freeze({
45
45
  green: Object.freeze({ dark: okhslColor(159, 0.59, 0.67), light: okhslColor(159, 0.75, 0.46) }),
46
46
  orange: Object.freeze({ dark: okhslColor(48, 0.75, 0.67), light: okhslColor(48, 0.90, 0.47) }),
47
47
  pink: Object.freeze({ dark: okhslColor(337, 0.72, 0.67), light: okhslColor(337, 0.75, 0.48) }),
48
+ grey: Object.freeze({ dark: okhslColor(0, 0, 0.67), light: okhslColor(0, 0, 0.47) }),
48
49
  });
49
50
  /** Canonical named color used by both the interactive theme and pre-load palette generation. */
50
51
  export function namedPiAccent(color, appearance) {
@@ -59,6 +60,7 @@ function wrapHue(hue) {
59
60
  /** Derive the complete semantic family from any primary accent, including future custom colors. */
60
61
  export function derivePiAccentProjection(accent, appearance) {
61
62
  const { h, s, l } = colorToOkhsl(accent);
63
+ const neutral = s < 0.001;
62
64
  const warm = h < 90 || h >= 270;
63
65
  const magenta = h >= 270 && h < 325;
64
66
  const blue = h >= 210 && h < 270;
@@ -69,10 +71,10 @@ export function derivePiAccentProjection(accent, appearance) {
69
71
  const selectedLightness = dark ? l * 0.37 : 1 - (1 - l) * 0.075;
70
72
  return Object.freeze({
71
73
  accent,
72
- border: okhslColor(borderHue, clamp(s * 0.78, 0.30, 0.64), clamp(l - (dark ? 0.17 : 0.11), 0, 1)),
73
- secondaryHeading: okhslColor(secondaryHue, clamp(s + 0.20, 0.62, 0.82), clamp(l + (dark ? 0.09 : -0.09), 0, 1)),
74
- selectedBg: okhslColor(h, selectedSaturation, selectedLightness),
75
- userMessageBg: okhslColor(h, selectedSaturation * 2 / 3, dark ? l * 0.34 : 1 - (1 - l) * 0.055),
74
+ border: okhslColor(borderHue, neutral ? 0 : clamp(s * 0.78, 0.30, 0.64), clamp(l - (dark ? 0.17 : 0.11), 0, 1)),
75
+ secondaryHeading: okhslColor(secondaryHue, neutral ? 0 : clamp(s + 0.20, 0.62, 0.82), clamp(l + (dark ? 0.09 : -0.09), 0, 1)),
76
+ selectedBg: okhslColor(h, neutral ? 0 : selectedSaturation, selectedLightness),
77
+ userMessageBg: okhslColor(h, neutral ? 0 : selectedSaturation * 2 / 3, dark ? l * 0.34 : 1 - (1 - l) * 0.055),
76
78
  });
77
79
  }
78
80
  let activeBaseTheme;
@@ -118,8 +120,8 @@ export function derivePiCanvasBackground(style, accent) {
118
120
  return null;
119
121
  if (style === "dark")
120
122
  return okhslColor(229, 0.03, 0.12);
121
- const { h } = colorToOkhsl(accent);
122
- return okhslColor(h, 0.12, 0.12);
123
+ const { h, s } = colorToOkhsl(accent);
124
+ return okhslColor(h, s < 0.001 ? 0 : 0.12, 0.12);
123
125
  }
124
126
  /** Resolves the selected canvas through the active accent and terminal color mode. */
125
127
  export function piCanvasBackgroundAnsi(style) {
@@ -9,6 +9,8 @@ export interface OwnedUiSettingDeclaration {
9
9
  };
10
10
  readonly description: string;
11
11
  readonly application: OwnedUiSettingApplication;
12
+ readonly storage?: "session";
13
+ readonly initialValueFrom?: string;
12
14
  readonly defaultValue: OwnedUiSettingValue;
13
15
  readonly allowedValues: readonly OwnedUiSettingValue[];
14
16
  /**
@@ -34,9 +36,23 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
34
36
  defaultValue: "auto";
35
37
  allowedValues: readonly ["auto", "manual"];
36
38
  }>;
39
+ readonly defaultAccentColor: Readonly<{
40
+ id: "defaultAccentColor";
41
+ label: "Default color";
42
+ section: Readonly<{
43
+ id: "appearance";
44
+ title: "Appearance";
45
+ }>;
46
+ description: "Startup color for future sessions. Changing this does not recolor running sessions.";
47
+ application: "restart";
48
+ defaultValue: "blue";
49
+ allowedValues: readonly ["blue", "green", "orange", "pink", "purple", "cyan", "grey"];
50
+ }>;
37
51
  readonly accentColor: Readonly<{
38
52
  id: "accentColor";
39
- label: "Accent color";
53
+ label: "Current color";
54
+ storage: "session";
55
+ initialValueFrom: "defaultAccentColor";
40
56
  section: Readonly<{
41
57
  id: "appearance";
42
58
  title: "Appearance";
@@ -44,7 +60,7 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
44
60
  description: "Color used by highlighted interface controls, list markers, and tonal interface surfaces.";
45
61
  application: "live";
46
62
  defaultValue: "blue";
47
- allowedValues: readonly ["blue", "purple", "cyan", "green", "orange", "pink"];
63
+ allowedValues: readonly ["blue", "green", "orange", "pink", "purple", "cyan", "grey"];
48
64
  }>;
49
65
  readonly backgroundStyle: Readonly<{
50
66
  id: "backgroundStyle";
@@ -1,6 +1,5 @@
1
- const UI_ACCENT_COLOR_CHOICES = Object.freeze([
2
- "blue", "purple", "cyan", "green", "orange", "pink",
3
- ]);
1
+ import { UI_ACCENT_COLORS } from "../../contracts/owned-ui/index.js";
2
+ const UI_ACCENT_COLOR_CHOICES = UI_ACCENT_COLORS;
4
3
  const MAX_ID_LENGTH = 64;
5
4
  const ID_PATTERN = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
6
5
  const GENERIC_SECTION = Object.freeze({ id: "generic", title: "Generic" });
@@ -17,9 +16,20 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
17
16
  defaultValue: "auto",
18
17
  allowedValues: Object.freeze(["auto", "manual"]),
19
18
  }),
19
+ defaultAccentColor: Object.freeze({
20
+ id: "defaultAccentColor",
21
+ label: "Default color",
22
+ section: APPEARANCE_SECTION,
23
+ description: "Startup color for future sessions. Changing this does not recolor running sessions.",
24
+ application: "restart",
25
+ defaultValue: "blue",
26
+ allowedValues: UI_ACCENT_COLOR_CHOICES,
27
+ }),
20
28
  accentColor: Object.freeze({
21
29
  id: "accentColor",
22
- label: "Accent color",
30
+ label: "Current color",
31
+ storage: "session",
32
+ initialValueFrom: "defaultAccentColor",
23
33
  section: APPEARANCE_SECTION,
24
34
  description: "Color used by highlighted interface controls, list markers, and tonal interface surfaces.",
25
35
  application: "live",
@@ -99,6 +99,22 @@ export declare function preTrustPalette(color: UiAccentColor, appearance: Appear
99
99
  accent: string;
100
100
  border: string;
101
101
  selectedBg: string;
102
+ } | {
103
+ accent: string;
104
+ border: string;
105
+ selectedBg: string;
106
+ } | {
107
+ accent: string;
108
+ border: string;
109
+ selectedBg: string;
110
+ } | {
111
+ accent: string;
112
+ border: string;
113
+ selectedBg: string;
114
+ } | {
115
+ accent: string;
116
+ border: string;
117
+ selectedBg: string;
102
118
  };
103
119
  export declare function preTrustTerminalAppearance(environment?: NodeJS.ProcessEnv): Appearance;
104
120
  export declare function preTrustTerminalColorMode(environment?: NodeJS.ProcessEnv): ColorMode;
@@ -21,9 +21,7 @@ export function readPreTrustAccent(environment = process.env) {
21
21
  const namespace = root[PRODUCT_IDENTITY.commandName];
22
22
  if (namespace === null || typeof namespace !== "object" || Array.isArray(namespace))
23
23
  return "blue";
24
- const value = namespace.accentColor;
25
- if (value === "default")
26
- return "purple";
24
+ const value = namespace.defaultAccentColor;
27
25
  return typeof value === "string" && Object.hasOwn(SHARED_PROGRESS_PALETTE, value) ? value : "blue";
28
26
  }
29
27
  catch {
@@ -30,8 +30,9 @@ export interface OwnedUiSettingsChangeOutcome {
30
30
  }
31
31
  export type OwnedUiSettingsListener = (manager: OwnedSettingsManager) => void;
32
32
  /**
33
- * One profile's A1 settings: resolves the declaration table against the stored document,
34
- * persists each accepted change atomically without discarding unknown keys, offers the
33
+ * Owns a profile's durable settings and this session's ephemeral overrides. Durable changes
34
+ * persist atomically without discarding unknown or legacy session keys; session changes stay local.
35
+ * Offers the
35
36
  * grouped sections a surface presents, and routes every accepted change only to its
36
37
  * owning backend, with agent settings reached through the engine settings port.
37
38
  */
@@ -42,6 +43,7 @@ export declare class OwnedSettingsManager {
42
43
  get resolution(): OwnedUiSettingsResolution;
43
44
  /** Reads and resolves the product namespace. Never throws: every failure becomes a notice. */
44
45
  read(): OwnedUiSettingsResolution;
46
+ startSession(): void;
45
47
  load(): Promise<void>;
46
48
  /**
47
49
  * Adopts values another process stored in the shared document. Notifies only when a
@@ -7,8 +7,9 @@ import { readProfileSettingsNamespace, repairProfileSettingsDocument, updateProf
7
7
  const PROFILE_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
8
8
  const MAX_DOCUMENT_BYTES = 256 * 1024;
9
9
  /**
10
- * One profile's A1 settings: resolves the declaration table against the stored document,
11
- * persists each accepted change atomically without discarding unknown keys, offers the
10
+ * Owns a profile's durable settings and this session's ephemeral overrides. Durable changes
11
+ * persist atomically without discarding unknown or legacy session keys; session changes stay local.
12
+ * Offers the
12
13
  * grouped sections a surface presents, and routes every accepted change only to its
13
14
  * owning backend, with agent settings reached through the engine settings port.
14
15
  */
@@ -26,6 +27,7 @@ export class OwnedSettingsManager {
26
27
  #agentSnapshot = null;
27
28
  #pending = new Map();
28
29
  #restartEffective = new Map();
30
+ #sessionValues = new Map();
29
31
  constructor(options) {
30
32
  if (!PROFILE_ID_PATTERN.test(options.profileId)) {
31
33
  throw new Error(`owned UI settings profile id is not a bounded slug: ${options.profileId}`);
@@ -42,6 +44,7 @@ export class OwnedSettingsManager {
42
44
  this.#hiddenAgentSettingIds = new Set(options.hiddenAgentSettingIds ?? []);
43
45
  this.#agentSettingLabelOverrides = { ...(options.agentSettingLabelOverrides ?? {}) };
44
46
  this.#resolution = this.read();
47
+ this.startSession();
45
48
  for (const setting of this.#resolution.settings) {
46
49
  if (setting.declaration.application === "restart")
47
50
  this.#restartEffective.set(setting.declaration.id, setting.value);
@@ -71,11 +74,31 @@ export class OwnedSettingsManager {
71
74
  return this.#resolveNamespace(null, shared.status === "invalid" ? shared.detail : undefined);
72
75
  }
73
76
  #resolveNamespace(values, unreadableDetail) {
74
- return resolveOwnedUiSettings({
77
+ const resolution = resolveOwnedUiSettings({
75
78
  declarations: this.#declarations,
76
79
  values,
77
80
  ...(unreadableDetail === undefined ? {} : { unreadableDetail }),
78
81
  });
82
+ return { ...resolution, settings: resolution.settings.map(setting => {
83
+ const value = this.#sessionValues.get(setting.declaration.id);
84
+ return setting.declaration.storage !== "session" || value === undefined ? setting : { ...setting, value };
85
+ }) };
86
+ }
87
+ startSession() {
88
+ this.#resolution = this.read();
89
+ for (const declaration of this.#declarations) {
90
+ if (declaration.storage !== "session")
91
+ continue;
92
+ const source = this.#resolution.settings.find(setting => setting.declaration.id === declaration.initialValueFrom);
93
+ const value = source !== undefined && declaration.allowedValues.includes(source.value) ? source.value : declaration.defaultValue;
94
+ this.#sessionValues.set(declaration.id, value);
95
+ if (source !== undefined && source.declaration.application === "restart") {
96
+ this.#restartEffective.set(source.declaration.id, source.value);
97
+ this.#pending.delete(source.declaration.id);
98
+ }
99
+ }
100
+ this.#resolution = this.read();
101
+ this.#notify();
79
102
  }
80
103
  async load() {
81
104
  this.#resolution = this.read();
@@ -159,6 +182,15 @@ export class OwnedSettingsManager {
159
182
  return backend === "a1" ? this.#changeOwned(id, value) : await this.#changeAgentValue(id, value);
160
183
  }
161
184
  #changeOwned(id, value) {
185
+ const declaration = this.#declarations.find(candidate => candidate.id === id);
186
+ if (!declaration?.allowedValues.includes(value))
187
+ return failed(`value ${JSON.stringify(value)} is not allowed for ${id}`);
188
+ if (declaration.storage === "session") {
189
+ this.#sessionValues.set(id, value);
190
+ this.#resolution = this.read();
191
+ this.#notify();
192
+ return changed("applied", "live", value, value);
193
+ }
162
194
  const outcome = this.#write(id, value);
163
195
  if (!outcome.stored)
164
196
  return failed(outcome.failure ?? `${id} could not be stored`);
@@ -285,7 +317,7 @@ function fromAgentOutcome(result) {
285
317
  function compactValues(resolution) {
286
318
  const values = { ...resolution.preserved };
287
319
  for (const setting of resolution.settings) {
288
- if (setting.value !== setting.declaration.defaultValue)
320
+ if (setting.declaration.storage !== "session" && setting.value !== setting.declaration.defaultValue)
289
321
  values[setting.declaration.id] = setting.value;
290
322
  }
291
323
  return values;
@@ -11,7 +11,7 @@ export function resolveOwnedUiSettings(input) {
11
11
  const values = input.values ?? {};
12
12
  const settings = [];
13
13
  for (const declaration of input.declarations) {
14
- if (!Object.hasOwn(values, declaration.id)) {
14
+ if (declaration.storage === "session" || !Object.hasOwn(values, declaration.id)) {
15
15
  settings.push({ declaration, value: declaration.defaultValue, source: "default" });
16
16
  continue;
17
17
  }
@@ -29,7 +29,8 @@ export function resolveOwnedUiSettings(input) {
29
29
  }
30
30
  const preserved = {};
31
31
  for (const [key, value] of Object.entries(values)) {
32
- if (findOwnedUiSettingDeclaration(input.declarations, key) === null)
32
+ const declaration = findOwnedUiSettingDeclaration(input.declarations, key);
33
+ if (declaration === null || declaration.storage === "session")
33
34
  preserved[key] = value;
34
35
  }
35
36
  return {
@@ -0,0 +1,66 @@
1
+ # Session colors and local API
2
+
3
+ Bare A1 offers two Settings → Appearance controls:
4
+
5
+ - **Default color** is saved in the active profile as `defaultAccentColor`. It initially resolves to blue. It controls the next session start, not the running interface.
6
+ - **Current color** applies immediately to this active session only. It is not saved to profile settings and does not propagate to other sessions on the same machine, profile, or project.
7
+
8
+ Both menus offer **blue → green → orange → pink → purple → cyan → grey**, with color previews. There is no automatic/dashed option. Grey keeps the derived semantic family and accent canvas neutral; unrelated success/warning/error colors are unchanged.
9
+
10
+ A fresh launch, resumed transcript, or successful new/resume/fork/import replacement initializes Current color from the latest saved Default color. Missing or invalid defaults resolve to blue. A failed replacement retains the old session and color. Changing the saved default does not recolor any running session. Cancel and undo respect each setting's storage scope.
11
+
12
+ The legacy profile `accentColor` key is retained but ignored by interactive color resolution. Current colors never overwrite it. The comparison `a1 pi` profile is unchanged. Standalone installation/update progress keeps its existing saved-profile preference policy.
13
+
14
+ ## Integration commands
15
+
16
+ External applications such as Herdr can invoke the installed CLI as a subprocess. These commands always return JSON without a `--json` switch:
17
+
18
+ | Arguments after `a1` | Result |
19
+ | --- | --- |
20
+ | `session color list` | Available color identifiers in display order; no running session needed |
21
+ | `session color get --session <id>` | One live session's Current color |
22
+ | `session color set pink --session <id>` | Set only that session's Current color and acknowledge the applied value |
23
+
24
+ The selector is a **complete session UUID**, shown by A1's Session Info workflow. It is not a PID, terminal index, abbreviated prefix, transcript path, or machine-wide current session. Get/set require one explicitly targeted live bare-A1 session outside an A1 shell. Within a shell launched by that exact session, omission uses the existing `PI_SESSION_ID`, `PI_SESSION_FILE`, and A1 runtime identity together; it never selects a different session implicitly. Keep the runtime-directory environment consistent when controlling a custom runtime root.
25
+
26
+ Get/set do not change Default color. Configure the startup preference in Settings. An API write immediately updates the Current color value visible there, along with semantic colors and accent-derived backgrounds.
27
+
28
+ ### Responses
29
+
30
+ Inventory returns only the color list, not a hard-coded default that could be confused with the configured startup preference:
31
+
32
+ ```json
33
+ {"colors":["blue","green","orange","pink","purple","cyan","grey"]}
34
+ ```
35
+
36
+ Get and successful set use the same shape:
37
+
38
+ ```json
39
+ {"sessionId":"57e38e80-f348-4b28-b791-7cc080886a70","color":"pink"}
40
+ ```
41
+
42
+ Failures return:
43
+
44
+ ```json
45
+ {"error":{"code":"session-unavailable","message":"Select one running bare session by its complete session id."}}
46
+ ```
47
+
48
+ Exactly one newline-terminated JSON object is written to stdout, without progress or human-readable prefixes. Error messages are explanatory text and can evolve; applications should branch on `error.code`. Exit statuses:
49
+
50
+ - **0:** successful list/get/applied set.
51
+ - **2:** invalid arguments, color, session format, or missing required selector.
52
+ - **1:** unavailable/ambiguous target, timeout, or control failure.
53
+
54
+ Error codes include `invalid-request`, `invalid-color`, `invalid-session`, `session-required`, `session-unavailable`, `ambiguous-session`, `request-expired`, `control-boundary`, `control-limit`, and `control-failed`. Hosts without a color command adapter return `control-unavailable`. Ordinary `session color help` and global help remain human-readable. CLI capability metadata advertises `sessionColors: true`.
55
+
56
+ ### Herdr integration
57
+
58
+ Invoke `a1` with an argument array rather than interpolating a shell string. For example, pass `["session", "color", "set", "pink", "--session", sessionId]`, capture stdout, await process completion, and parse its one JSON object. On exit 0, update Herdr's local UI from the returned `sessionId` and `color`. On nonzero exit, display or handle the structured error; do not treat a queued write or process creation as successful application. No subscription/event feed or host-app icon recoloring is provided.
59
+
60
+ ## Lifetime and safety
61
+
62
+ Control starts alongside the interactive shell and stops with it. It uses an in-process local IPC endpoint: a Windows named pipe or Unix-domain socket, with bounded private discovery records under the runtime directory. It does not introduce a separate resident service, TCP port, or HTTP server. Comparison-profile sessions do not publish endpoints.
63
+
64
+ Requests have a three-second end-to-end budget, bounded payloads, process-liveness checks, runtime identity, a private endpoint nonce, session binding checks, and correlated acknowledgements. Concurrent requests serialize in the target runtime. Set reports success only after its live settings change path applies the color. Requests still queued after expiry, disconnect, stop, or session replacement cannot recolor a later session. A timeout means application was not acknowledged: a change already applied just before a lost response is not rolled back; get the current value before deciding whether to retry. No delayed request is persisted for future replay.
65
+
66
+ Private runtime records are not a public file-editing API. Do not write settings files or edit endpoint records to control Current color. Unverifiable, ambiguous, replaced, stopped, malformed, and comparison-profile targets fail closed without choosing another session. An unavailable control directory or failed endpoint startup leaves the interactive session usable, but external control reports unavailable. Worktree attachment authority is unaffected.
@@ -42,8 +42,8 @@ Follow the [development worktree protocol](architecture/project-structure.md#dev
42
42
 
43
43
  ## Version-3 lifecycle
44
44
 
45
- 1. **Draft plan:** create one normally named branch/PR such as `feature/...`, `fix/...`, or `chore/...`. Start the body with `## Proposal`, use it for one or two sentences of intent, and follow it with one or more unchecked, outcome-sized task items under `## Implementation`, with no fixed maximum. Each item names a delivered code outcome. Omit generic process tasks; run, verify, validate, evidence, or manual-test items; quoted phase lines; and routine validation command lists, and keep machine linkage under final collapsed `Automation`. Keep it draft and include only planning artifacts until the maintainer approves the plan and explicitly requests implementation.
46
- 2. **Same-PR implementation:** continue after explicit approval in the same worktree, branch, history, draft PR, and phase-free body. Check an Implementation item as soon as its associated code and tests are written and its detailed task bookkeeping is complete; an ordinary progress edit changes only `[ ]` to `[x]` and is not deferred until handoff. Adding, removing, reordering, or rewriting an item is a scope refinement requiring explicit approval and coherent planning updates, with affected scope left unchecked. Reconcile approved refinements before corresponding code edits.
45
+ 1. **Draft plan:** create one normally named branch/PR such as `feature/...`, `fix/...`, or `chore/...`. Start the body with `## Proposal`, use it for one or two sentences of intent, and follow it with one or more unchecked, outcome-sized task items under `## Implementation`, with no fixed maximum. Each item names a delivered code outcome, and `tasks.md` holds one task group per item in the same order. Omit generic process tasks; run, verify, validate, evidence, or manual-test items; quoted phase lines; and routine validation command lists, and keep machine linkage under final collapsed `Automation`. Keep it draft and include only planning artifacts until the maintainer approves the plan and explicitly requests implementation.
46
+ 2. **Same-PR implementation:** continue after explicit approval in the same worktree, branch, history, draft PR, and phase-free body. Follow the `Implementation progress` rule in `AGENTS.md`: deliver one item at a time, and once its code and tests are written and its task group is ticked, commit, push, and check that item before starting the next. An ordinary progress edit changes only `[ ]` to `[x]` and is never saved for handoff. Adding, removing, reordering, or rewriting an item is a scope refinement requiring explicit approval and coherent planning updates, with affected scope left unchecked. Reconcile approved refinements before corresponding code edits.
47
47
  3. **Complete implementation:** finish the code and its tests, substantive tasks, and explicit known-gap disposition. CI runs those tests after readiness; its success is objective evidence, not acceptance, and GitHub check counts are not Implementation task progress. Manual validation is the maintainer's optional choice before integrating and never blocks a checkbox, finalization, or merge.
48
48
  4. **Plain acceptance list:** before checking the final item, keep the body phase-free and add final `## Acceptance` with one to ten concise implementation-specific behavior-and-result bullets. Do not use checkboxes, generic review/CI/approval/archive statements, angle-bracket placeholders or HTML, URLs, mentions, or automated-test inventory.
49
49
  5. **Automatic readiness and finalization:** reconcile and push current `origin/develop`, run the [delivery body check](#delivery-body-check) on the intended final body, then check the final item. That checkbox update completes the agent-owned implementation job; agents never mark implementation PRs ready themselves. Trusted automation revalidates the exact fully complete version-3 task-list snapshot and all pre-finalization inputs, marks the PR ready, and then 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. It never checks tasks or infers completion. Running the [finalization command](#finalization-command) locally first is optional and yields the same bytes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.2.7-dev.1288",
3
+ "version": "0.2.7-dev.1290",
4
4
  "description": "Standalone single-agent terminal UI on pinned Pi",
5
5
  "type": "module",
6
6
  "privateLaunchContract": "neutral-launch-v1",
@@ -113,7 +113,7 @@
113
113
  "@mariozechner/clipboard": "^0.3.9"
114
114
  },
115
115
  "publication": {
116
- "source": "bfbed9a4849fe7ba1b6a879cef7a800a132e18e9",
117
- "pullRequest": 915
116
+ "source": "c39a95552f7be42f1f7c83f7b3fdf8d92203d965",
117
+ "pullRequest": 917
118
118
  }
119
119
  }