@timurproko/a1 0.1.1-dev.9 → 0.1.5-dev.11
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 +76 -15
- package/bin/cli.js +14 -2
- package/bin/module-identity.js +132 -0
- package/bin/ui.js +10 -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/src/cli/capabilities.d.ts +15 -0
- package/dist/src/cli/capabilities.js +7 -0
- package/dist/src/cli/dispatch.d.ts +9 -3
- package/dist/src/cli/dispatch.js +104 -16
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/packages.d.ts +23 -0
- package/dist/src/cli/packages.js +84 -0
- package/dist/src/foundation/agent-engine-contracts/index.d.ts +1 -0
- package/dist/src/foundation/agent-engine-contracts/index.js +1 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.d.ts +54 -0
- package/dist/src/foundation/agent-engine-contracts/package-ports.js +31 -0
- package/dist/src/foundation/pi-component-adapter/shell-editor-autocomplete.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-extension-ui.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-footer-status.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.d.ts +15 -0
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +34 -1
- package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-shared-facade.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-shared-facade.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/adjacent/core/keybindings.d.ts +2 -2
- package/dist/src/foundation/pi-component-adapter/upstream/adjacent/core/keybindings.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/custom-entry.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/custom-entry.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/daxnuts.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/earendil-announcement.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/earendil-announcement.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/extension-editor.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/extension-editor.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/first-time-setup.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/first-time-setup.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/mermaid.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/owned-editor.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/owned-editor.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/scoped-models-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-footer.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-footer.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector-search.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/session-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/skill-invocation-message.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/status-indicator.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/status-indicator.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/tree-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/tree-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/trust-selector.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/components/trust-selector.js +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/upstream/theme/theme.js +1 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +8 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.js +28 -1
- package/dist/src/foundation/pi-engine-adapter/index.d.ts +1 -0
- package/dist/src/foundation/pi-engine-adapter/index.js +1 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.d.ts +13 -0
- package/dist/src/foundation/pi-engine-adapter/package-integration.js +112 -0
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.d.ts +18 -1
- package/dist/src/foundation/pi-engine-adapter/runtime-integration.js +37 -2
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +14 -2
- package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +1 -1
- package/dist/src/foundation/release/release.js +26 -2
- package/dist/src/foundation/release/update.d.ts +11 -1
- package/dist/src/foundation/release/update.js +106 -5
- package/docs/architecture/toolchain.md +3 -3
- package/docs/ci-release-runbook.md +80 -40
- package/docs/features/launch-profiles.md +2 -0
- package/package.json +8 -3
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
7
|
-
import { copyToClipboard, getAgentDir, ProjectTrustStore, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { copyToClipboard, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiSnapshot, } from "../owned-ui-contracts/index.js";
|
|
9
9
|
import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
|
|
10
10
|
import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
@@ -19,6 +19,7 @@ const DEFAULT_SURFACE = {
|
|
|
19
19
|
};
|
|
20
20
|
export class PiEngineAdapter {
|
|
21
21
|
#runtimeFactory;
|
|
22
|
+
#checkPackageUpdates;
|
|
22
23
|
#cwd;
|
|
23
24
|
#agentDir;
|
|
24
25
|
#sessionId;
|
|
@@ -73,6 +74,10 @@ export class PiEngineAdapter {
|
|
|
73
74
|
this.#agentDir = options.agentDir ?? getAgentDir();
|
|
74
75
|
this.#sessionId = options.sessionId ?? "owned-session-1";
|
|
75
76
|
this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
|
|
77
|
+
this.#checkPackageUpdates = options.checkPackageUpdates
|
|
78
|
+
?? (options.createRuntime
|
|
79
|
+
? async () => []
|
|
80
|
+
: settingsManager => checkDefaultPiPackageUpdates(this.#cwd, this.#agentDir, settingsManager));
|
|
76
81
|
this.#workflowHost = options.workflowHost ?? defaultWorkflowHost();
|
|
77
82
|
this.#availableThemes = options.availableThemes ?? null;
|
|
78
83
|
this.#workflowInteraction = { prompt: async () => null, notify() { } };
|
|
@@ -122,8 +127,25 @@ export class PiEngineAdapter {
|
|
|
122
127
|
this.#editor = { ...this.#editor, submitEnabled: true };
|
|
123
128
|
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
124
129
|
this.#emitView();
|
|
130
|
+
void this.#announcePackageUpdates(runtime.services.settingsManager);
|
|
125
131
|
return this.view();
|
|
126
132
|
}
|
|
133
|
+
async #announcePackageUpdates(settingsManager) {
|
|
134
|
+
if (process.env.PI_OFFLINE)
|
|
135
|
+
return;
|
|
136
|
+
let updates;
|
|
137
|
+
try {
|
|
138
|
+
updates = await this.#checkPackageUpdates(settingsManager);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (this.#disposed || updates.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
146
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
|
+
this.#emitView();
|
|
148
|
+
}
|
|
127
149
|
onEvent(listener) {
|
|
128
150
|
this.#listeners.add(listener);
|
|
129
151
|
listener(this.#event({ type: "session-view", view: this.view() }));
|
|
@@ -1876,6 +1898,11 @@ export async function createPiEngineAdapter(options = {}) {
|
|
|
1876
1898
|
async function createDefaultPiRuntime(input) {
|
|
1877
1899
|
return createPiRuntimeIntegration({ cwd: input.cwd, agentDir: input.agentDir });
|
|
1878
1900
|
}
|
|
1901
|
+
async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
|
|
1902
|
+
const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
|
|
1903
|
+
const updates = await packageManager.checkForAvailableUpdates();
|
|
1904
|
+
return updates.map(update => update.displayName);
|
|
1905
|
+
}
|
|
1879
1906
|
function pinnedSessionInfoPresentation(value, sessionName, entries, modelRuntime) {
|
|
1880
1907
|
const stats = isRecord(value) ? value : {};
|
|
1881
1908
|
const tokens = dynamicObject(stats, "tokens");
|
|
@@ -4,6 +4,7 @@ export * from "./runtime-integration.js";
|
|
|
4
4
|
export * from "./session-integration.js";
|
|
5
5
|
export * from "./model-auth-integration.js";
|
|
6
6
|
export * from "./settings-integration.js";
|
|
7
|
+
export * from "./package-integration.js";
|
|
7
8
|
export * from "./resource-extension-integration.js";
|
|
8
9
|
export * from "./workflow-controllers.js";
|
|
9
10
|
export * from "./workflows.js";
|
|
@@ -4,6 +4,7 @@ export * from "./runtime-integration.js";
|
|
|
4
4
|
export * from "./session-integration.js";
|
|
5
5
|
export * from "./model-auth-integration.js";
|
|
6
6
|
export * from "./settings-integration.js";
|
|
7
|
+
export * from "./package-integration.js";
|
|
7
8
|
export * from "./resource-extension-integration.js";
|
|
8
9
|
export * from "./workflow-controllers.js";
|
|
9
10
|
export * from "./workflows.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AgentPackagesPort, type AgentPackagesPortInput } from "../agent-engine-contracts/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pinned Pi's package manager already takes the profile root as an argument, so
|
|
4
|
+
* this binds it to the root A1 chose rather than to whatever the configuration-root
|
|
5
|
+
* environment variable happens to say. Pi's own package command handler is not used:
|
|
6
|
+
* it prints Pi's command names and ends the process, and both of those belong to
|
|
7
|
+
* whichever A1 command is running.
|
|
8
|
+
*
|
|
9
|
+
* The settings manager is created with project trust withheld, which is what keeps
|
|
10
|
+
* every operation to the user scope of the selected profile — a project-local
|
|
11
|
+
* `packages` list is never read, so it can never be written either.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createPiPackagesPort(input: AgentPackagesPortInput): AgentPackagesPort;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { DefaultPackageManager, ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { agentPackageOutcome, } from "../agent-engine-contracts/index.js";
|
|
4
|
+
const MODEL_REFRESH_TIMEOUT_MS = 15_000;
|
|
5
|
+
/**
|
|
6
|
+
* Pinned Pi's package manager already takes the profile root as an argument, so
|
|
7
|
+
* this binds it to the root A1 chose rather than to whatever the configuration-root
|
|
8
|
+
* environment variable happens to say. Pi's own package command handler is not used:
|
|
9
|
+
* it prints Pi's command names and ends the process, and both of those belong to
|
|
10
|
+
* whichever A1 command is running.
|
|
11
|
+
*
|
|
12
|
+
* The settings manager is created with project trust withheld, which is what keeps
|
|
13
|
+
* every operation to the user scope of the selected profile — a project-local
|
|
14
|
+
* `packages` list is never read, so it can never be written either.
|
|
15
|
+
*/
|
|
16
|
+
export function createPiPackagesPort(input) {
|
|
17
|
+
const { profileRoot, cwd } = input;
|
|
18
|
+
const settingsManager = SettingsManager.create(cwd, profileRoot, { projectTrusted: false });
|
|
19
|
+
const packageManager = new DefaultPackageManager({ cwd, agentDir: profileRoot, settingsManager });
|
|
20
|
+
packageManager.setProgressCallback(event => {
|
|
21
|
+
if (event.type !== "start" || !event.message)
|
|
22
|
+
return;
|
|
23
|
+
input.onProgress?.({ operation: operationFor(event.action), message: event.message });
|
|
24
|
+
});
|
|
25
|
+
const configured = () => packageManager.listConfiguredPackages()
|
|
26
|
+
.filter(entry => entry.scope === "user")
|
|
27
|
+
.map(entry => Object.freeze({
|
|
28
|
+
source: entry.source,
|
|
29
|
+
installedPath: entry.installedPath ?? null,
|
|
30
|
+
filtered: entry.filtered,
|
|
31
|
+
}));
|
|
32
|
+
const isConfigured = (source) => configured().some(entry => entry.source === source || entry.source.startsWith(`${source}@`));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
capabilities: Object.freeze({ install: true, remove: true, update: true, refreshModels: true }),
|
|
35
|
+
profileRoot,
|
|
36
|
+
async list() {
|
|
37
|
+
return await attempt("list", null, async () => agentPackageOutcome("list", "completed", null, null, configured()));
|
|
38
|
+
},
|
|
39
|
+
async install(source) {
|
|
40
|
+
return await attempt("install", source, async () => {
|
|
41
|
+
await packageManager.installAndPersist(source);
|
|
42
|
+
return agentPackageOutcome("install", "completed", null, source, configured());
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
async remove(source) {
|
|
46
|
+
return await attempt("remove", source, async () => {
|
|
47
|
+
const removed = await packageManager.removeAndPersist(source);
|
|
48
|
+
if (!removed)
|
|
49
|
+
return agentPackageOutcome("remove", "not-found", null, source);
|
|
50
|
+
return agentPackageOutcome("remove", "completed", null, source, configured());
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
async update(source) {
|
|
54
|
+
return await attempt("update", source ?? null, async () => {
|
|
55
|
+
// Asking first keeps "that package is not installed here" a structural
|
|
56
|
+
// answer rather than a string a caller would have to recognize.
|
|
57
|
+
if (source !== undefined && !isConfigured(source))
|
|
58
|
+
return agentPackageOutcome("update", "not-found", null, source);
|
|
59
|
+
await packageManager.update(source);
|
|
60
|
+
return agentPackageOutcome("update", "completed", null, source ?? null, configured());
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
async refreshModels() {
|
|
64
|
+
return await attempt("refresh-models", null, async () => {
|
|
65
|
+
await refreshModelCatalogs(profileRoot);
|
|
66
|
+
return agentPackageOutcome("refresh-models", "completed");
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function attempt(operation, source, run) {
|
|
72
|
+
try {
|
|
73
|
+
return await run();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return agentPackageOutcome(operation, "failed", describe(error), source);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function refreshModelCatalogs(profileRoot) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), MODEL_REFRESH_TIMEOUT_MS);
|
|
82
|
+
try {
|
|
83
|
+
const modelRuntime = await ModelRuntime.create({
|
|
84
|
+
authPath: join(profileRoot, "auth.json"),
|
|
85
|
+
modelsPath: join(profileRoot, "models.json"),
|
|
86
|
+
allowModelNetwork: false,
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
const result = await modelRuntime.refresh({ allowNetwork: true, force: true, signal: controller.signal });
|
|
90
|
+
if (result.aborted)
|
|
91
|
+
throw new Error(`model catalog refresh timed out after ${MODEL_REFRESH_TIMEOUT_MS}ms`);
|
|
92
|
+
if (result.errors.size > 0) {
|
|
93
|
+
throw new Error(Array.from(result.errors, ([provider, error]) => `${provider}: ${error.message}`).join("; "));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function operationFor(action) {
|
|
101
|
+
if (action === "install")
|
|
102
|
+
return "install";
|
|
103
|
+
if (action === "remove")
|
|
104
|
+
return "remove";
|
|
105
|
+
return "update";
|
|
106
|
+
}
|
|
107
|
+
function describe(error) {
|
|
108
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
|
|
109
|
+
if (message.length === 0)
|
|
110
|
+
return "unknown package failure";
|
|
111
|
+
return message.length > 600 ? `${message.slice(0, 597)}...` : message;
|
|
112
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentSession, type AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type AgentSession, type AgentSessionRuntime, type AgentSessionServices, type ScopedModel } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export interface PiRuntimeIntegrationOptions {
|
|
3
3
|
readonly cwd: string;
|
|
4
4
|
readonly agentDir: string;
|
|
@@ -10,9 +10,26 @@ export type PiSessionReplacement = {
|
|
|
10
10
|
readonly kind: "resume";
|
|
11
11
|
readonly sessionPath: string;
|
|
12
12
|
};
|
|
13
|
+
interface ConfiguredModelScope {
|
|
14
|
+
readonly scopedModels: readonly ScopedModel[];
|
|
15
|
+
readonly model: ScopedModel["model"] | undefined;
|
|
16
|
+
readonly thinkingLevel: ScopedModel["thinkingLevel"];
|
|
17
|
+
readonly diagnostics: readonly {
|
|
18
|
+
type: "info" | "warning" | "error";
|
|
19
|
+
message: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
|
|
24
|
+
* into a scoped model list, keep the resolver's warnings (e.g. "No models match
|
|
25
|
+
* pattern ..."), and pick the same initial model pinned Pi would pick — the
|
|
26
|
+
* saved default when it is in scope, otherwise the first scoped model.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveConfiguredModelScope(services: Pick<AgentSessionServices, "settingsManager" | "modelRuntime">): Promise<ConfiguredModelScope>;
|
|
13
29
|
export declare function createPiRuntimeIntegration(options: PiRuntimeIntegrationOptions): Promise<AgentSessionRuntime>;
|
|
14
30
|
export declare function bindPiRuntimeSession(runtime: AgentSessionRuntime, rebind: (session: AgentSession) => Promise<void>): () => void;
|
|
15
31
|
export declare function replacePiRuntimeSession(runtime: AgentSessionRuntime, replacement: PiSessionReplacement): Promise<{
|
|
16
32
|
readonly cancelled: boolean;
|
|
17
33
|
}>;
|
|
18
34
|
export declare function disposePiRuntimeIntegration(runtime: AgentSessionRuntime): Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -1,14 +1,49 @@
|
|
|
1
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, resolveModelScopeWithDiagnostics, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Mirrors pinned Pi's CLI startup: resolve the `models` patterns from settings
|
|
4
|
+
* into a scoped model list, keep the resolver's warnings (e.g. "No models match
|
|
5
|
+
* pattern ..."), and pick the same initial model pinned Pi would pick — the
|
|
6
|
+
* saved default when it is in scope, otherwise the first scoped model.
|
|
7
|
+
*/
|
|
8
|
+
export async function resolveConfiguredModelScope(services) {
|
|
9
|
+
const patterns = services.settingsManager.getEnabledModels();
|
|
10
|
+
if (!patterns || patterns.length === 0) {
|
|
11
|
+
return { scopedModels: [], model: undefined, thinkingLevel: undefined, diagnostics: [] };
|
|
12
|
+
}
|
|
13
|
+
const { scopedModels, diagnostics } = await resolveModelScopeWithDiagnostics([...patterns], services.modelRuntime, { signal: AbortSignal.timeout(15_000) });
|
|
14
|
+
let selected;
|
|
15
|
+
if (scopedModels.length > 0) {
|
|
16
|
+
const savedProvider = services.settingsManager.getDefaultProvider();
|
|
17
|
+
const savedModelId = services.settingsManager.getDefaultModel();
|
|
18
|
+
const savedModel = savedProvider && savedModelId
|
|
19
|
+
? services.modelRuntime.getModel(savedProvider, savedModelId)
|
|
20
|
+
: undefined;
|
|
21
|
+
selected = (savedModel
|
|
22
|
+
? scopedModels.find(scoped => scoped.model.provider === savedModel.provider && scoped.model.id === savedModel.id)
|
|
23
|
+
: undefined) ?? scopedModels[0];
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
scopedModels,
|
|
27
|
+
model: selected?.model,
|
|
28
|
+
thinkingLevel: selected?.thinkingLevel,
|
|
29
|
+
diagnostics: diagnostics.map(diagnostic => ({ type: diagnostic.type, message: diagnostic.message })),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
2
32
|
export async function createPiRuntimeIntegration(options) {
|
|
3
33
|
const sessionManager = SessionManager.create(options.cwd, options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR);
|
|
4
34
|
const createRuntime = async ({ cwd, sessionManager: targetSessionManager, sessionStartEvent, }) => {
|
|
5
35
|
const services = await createAgentSessionServices({ cwd, agentDir: options.agentDir });
|
|
36
|
+
const modelScope = await resolveConfiguredModelScope(services);
|
|
37
|
+
const hasExistingSession = targetSessionManager.buildSessionContext().messages.length > 0;
|
|
6
38
|
const created = await createAgentSessionFromServices({
|
|
7
39
|
services,
|
|
8
40
|
sessionManager: targetSessionManager,
|
|
9
41
|
...(sessionStartEvent ? { sessionStartEvent } : {}),
|
|
42
|
+
...(modelScope.model && !hasExistingSession ? { model: modelScope.model } : {}),
|
|
43
|
+
...(modelScope.thinkingLevel && !hasExistingSession ? { thinkingLevel: modelScope.thinkingLevel } : {}),
|
|
44
|
+
...(modelScope.scopedModels.length > 0 ? { scopedModels: [...modelScope.scopedModels] } : {}),
|
|
10
45
|
});
|
|
11
|
-
return { ...created, services, diagnostics:
|
|
46
|
+
return { ...created, services, diagnostics: [...modelScope.diagnostics] };
|
|
12
47
|
};
|
|
13
48
|
return createAgentSessionRuntime(createRuntime, {
|
|
14
49
|
cwd: options.cwd,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
|
|
2
2
|
import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
|
|
3
|
-
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
3
|
+
import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
|
|
4
4
|
import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
|
|
5
5
|
export class OwnedUiSessionShellRoot {
|
|
6
6
|
editor;
|
|
@@ -136,7 +136,17 @@ export class OwnedUiSessionShellRoot {
|
|
|
136
136
|
return ["", ...rows];
|
|
137
137
|
return rows;
|
|
138
138
|
});
|
|
139
|
-
const
|
|
139
|
+
const diagnostics = this.#view.diagnostics;
|
|
140
|
+
const startupRows = diagnostics
|
|
141
|
+
.filter(diagnostic => diagnostic.code === "engine-startup")
|
|
142
|
+
.flatMap(diagnostic => renderPiShellStartupDiagnostic(diagnostic, width));
|
|
143
|
+
const packageUpdateRows = diagnostics
|
|
144
|
+
.filter(diagnostic => diagnostic.code === "package-updates")
|
|
145
|
+
.flatMap(diagnostic => renderPiShellPackageUpdateNotice(diagnostic.message.split("\n").filter(line => line.startsWith("- ")).map(line => line.slice(2)), width));
|
|
146
|
+
const diagnosticRows = diagnostics
|
|
147
|
+
.filter(diagnostic => diagnostic.code !== "engine-startup" && diagnostic.code !== "package-updates")
|
|
148
|
+
.slice(-3)
|
|
149
|
+
.flatMap(diagnostic => renderPiShellTranscriptBlock({
|
|
140
150
|
id: `diagnostic-${diagnostic.sequence}`,
|
|
141
151
|
kind: diagnostic.severity === "error" ? "error" : "system",
|
|
142
152
|
status: "finalized",
|
|
@@ -149,9 +159,11 @@ export class OwnedUiSessionShellRoot {
|
|
|
149
159
|
if (resourceRows.at(-1) === "")
|
|
150
160
|
resourceRows.pop();
|
|
151
161
|
return [
|
|
162
|
+
...startupRows,
|
|
152
163
|
...(this.#extensionHeader ?? this.header).render(width),
|
|
153
164
|
...resourceRows,
|
|
154
165
|
...transcript,
|
|
166
|
+
...packageUpdateRows,
|
|
155
167
|
...diagnosticRows,
|
|
156
168
|
];
|
|
157
169
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "
|
|
1
|
+
import { HStack, ProcessTerminal, ScrollView, TuiAltScreen, TuiMainScreen, VStack, visibleWidth, } from "#pi-tui";
|
|
2
2
|
export class PiTuiRuntimeError extends Error {
|
|
3
3
|
stage;
|
|
4
4
|
constructor(stage, cause) {
|
|
@@ -139,11 +139,24 @@ async function findInstalledDependency(root, requesterRoot, name) {
|
|
|
139
139
|
let directory = requesterRoot;
|
|
140
140
|
while (true) {
|
|
141
141
|
const candidate = resolve(directory, "node_modules", name);
|
|
142
|
-
|
|
143
|
-
if (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot)) {
|
|
142
|
+
if (await isContained(root, candidate)) {
|
|
144
143
|
const metadata = await lstat(candidate).catch(() => null);
|
|
145
144
|
if (metadata?.isDirectory() && !metadata.isSymbolicLink())
|
|
146
145
|
return candidate;
|
|
146
|
+
// A linked dependency directory is how this installation unifies a module
|
|
147
|
+
// that would otherwise be present twice, and the link is made by A1 itself
|
|
148
|
+
// at install and at every launch. Follow it to the directory it names and
|
|
149
|
+
// collect from there, so the payload still reads only real files — but
|
|
150
|
+
// only when the target is inside this installation, so a link can never
|
|
151
|
+
// pull bytes in from somewhere else.
|
|
152
|
+
if (metadata?.isSymbolicLink()) {
|
|
153
|
+
const target = await realpath(candidate).catch(() => null);
|
|
154
|
+
if (target !== null && await isContained(root, target)) {
|
|
155
|
+
const targetMetadata = await lstat(target).catch(() => null);
|
|
156
|
+
if (targetMetadata?.isDirectory())
|
|
157
|
+
return target;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
147
160
|
}
|
|
148
161
|
if (directory === root)
|
|
149
162
|
return null;
|
|
@@ -153,6 +166,17 @@ async function findInstalledDependency(root, requesterRoot, name) {
|
|
|
153
166
|
directory = parent;
|
|
154
167
|
}
|
|
155
168
|
}
|
|
169
|
+
/** True when `path` sits inside `root`, comparing what each one actually resolves to. */
|
|
170
|
+
async function isContained(root, path) {
|
|
171
|
+
const within = (from, to) => {
|
|
172
|
+
const value = relative(from, to);
|
|
173
|
+
return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
|
174
|
+
};
|
|
175
|
+
if (within(root, path))
|
|
176
|
+
return true;
|
|
177
|
+
const canonicalRoot = await realpath(root).catch(() => null);
|
|
178
|
+
return canonicalRoot !== null && within(canonicalRoot, path);
|
|
179
|
+
}
|
|
156
180
|
function dependencyNames(value) {
|
|
157
181
|
if (value === undefined)
|
|
158
182
|
return [];
|
|
@@ -39,17 +39,27 @@ export interface SelfUpdateOptions {
|
|
|
39
39
|
runner?: UpdateProcessRunner;
|
|
40
40
|
lifecycle?: UpdateLifecycleCoordinator;
|
|
41
41
|
transactionStore?: UpdateTransactionJournal;
|
|
42
|
+
progress?: boolean;
|
|
42
43
|
onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
|
|
43
44
|
now?: () => number;
|
|
44
45
|
}
|
|
45
46
|
export type UpdateActivationPhase = Extract<UpdateTransactionPhase, "materialized" | "certified" | "active-reference-committed">;
|
|
47
|
+
/**
|
|
48
|
+
* Copying the release is the longest step with nothing to say for itself, so it
|
|
49
|
+
* reports the files it has written against the files it must write. A caller that
|
|
50
|
+
* shows progress can then move with the work instead of guessing at it.
|
|
51
|
+
*/
|
|
52
|
+
export interface UpdateMaterializationProgress {
|
|
53
|
+
readonly completed: number;
|
|
54
|
+
readonly total: number;
|
|
55
|
+
}
|
|
46
56
|
export interface UpdateLifecycleCoordinator {
|
|
47
57
|
targetIsActive(targetVersion: string): Promise<boolean>;
|
|
48
58
|
shutdownVerifiedOwners(targetVersion: string): Promise<{
|
|
49
59
|
priorActiveVersion: string | null;
|
|
50
60
|
}>;
|
|
51
61
|
verifyPackageUnlocked(packageRoot: string): Promise<void>;
|
|
52
|
-
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase: UpdateActivationPhase) => Promise<void
|
|
62
|
+
activateInstalled(packageRoot: string, targetVersion: string, phase: (phase: UpdateActivationPhase) => Promise<void>, onMaterializing?: (progress: UpdateMaterializationProgress) => void): Promise<void>;
|
|
53
63
|
}
|
|
54
64
|
export interface UpdateTransactionJournal {
|
|
55
65
|
readonly path: string;
|
|
@@ -105,8 +105,21 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
105
105
|
throw new Error(PRODUCT_TEXT.diagnostic(`package remains locked after verified shutdown: ${errorMessage(error)}`));
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
|
-
async activateInstalled(packageRoot, targetVersion, phase) {
|
|
109
|
-
|
|
108
|
+
async activateInstalled(packageRoot, targetVersion, phase, onMaterializing) {
|
|
109
|
+
let total = 0;
|
|
110
|
+
let completed = 0;
|
|
111
|
+
const candidate = await materializeRelease(packageRoot, paths.dataDir, {
|
|
112
|
+
onProgress: event => {
|
|
113
|
+
total = event.fileCount;
|
|
114
|
+
onMaterializing?.({ completed, total });
|
|
115
|
+
},
|
|
116
|
+
onOperation: event => {
|
|
117
|
+
if (event.operation !== "candidate-write")
|
|
118
|
+
return;
|
|
119
|
+
completed += 1;
|
|
120
|
+
onMaterializing?.({ completed, total });
|
|
121
|
+
},
|
|
122
|
+
});
|
|
110
123
|
if (candidate.packageVersion !== targetVersion)
|
|
111
124
|
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
|
|
112
125
|
await stateStore.recordCandidate(candidate);
|
|
@@ -121,6 +134,81 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
121
134
|
},
|
|
122
135
|
};
|
|
123
136
|
}
|
|
137
|
+
const PROGRESS_BAR_WIDTH = 39;
|
|
138
|
+
const PROGRESS_TICK_MS = 200;
|
|
139
|
+
/**
|
|
140
|
+
* Copying the release owns 78–92 and is reported file by file, so the bar crosses
|
|
141
|
+
* that span with the work rather than parking at its start. What follows is short,
|
|
142
|
+
* which is why the remaining milestones sit close together.
|
|
143
|
+
*/
|
|
144
|
+
const MATERIALIZE_PROGRESS = Object.freeze({ from: 78, to: 92 });
|
|
145
|
+
const ACTIVATION_PROGRESS = {
|
|
146
|
+
materialized: { at: 92, creepTo: 94 },
|
|
147
|
+
certified: { at: 94, creepTo: 96 },
|
|
148
|
+
"active-reference-committed": { at: 96, creepTo: 99 },
|
|
149
|
+
};
|
|
150
|
+
function renderProgressBar(percent) {
|
|
151
|
+
const bounded = Math.min(100, Math.max(0, Math.round(percent)));
|
|
152
|
+
const filled = Math.round((bounded / 100) * PROGRESS_BAR_WIDTH);
|
|
153
|
+
return `${"█".repeat(filled)}${"░".repeat(PROGRESS_BAR_WIDTH - filled)} ${bounded}%`;
|
|
154
|
+
}
|
|
155
|
+
function createUpdateProgress(output, enabled) {
|
|
156
|
+
let visible = false;
|
|
157
|
+
let current = 0;
|
|
158
|
+
let shown = -1;
|
|
159
|
+
let timer = null;
|
|
160
|
+
const draw = () => {
|
|
161
|
+
const rounded = Math.round(current);
|
|
162
|
+
if (rounded === shown)
|
|
163
|
+
return;
|
|
164
|
+
shown = rounded;
|
|
165
|
+
visible = true;
|
|
166
|
+
output.stdout(`\r${renderProgressBar(rounded)}`);
|
|
167
|
+
};
|
|
168
|
+
const stopCreep = () => {
|
|
169
|
+
if (timer !== null) {
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
timer = null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
set(percent, creepTo = percent) {
|
|
176
|
+
if (!enabled)
|
|
177
|
+
return;
|
|
178
|
+
stopCreep();
|
|
179
|
+
current = Math.max(current, percent);
|
|
180
|
+
shown = -1;
|
|
181
|
+
draw();
|
|
182
|
+
if (creepTo <= current)
|
|
183
|
+
return;
|
|
184
|
+
// Creep asymptotically toward (but never reach) the next milestone so
|
|
185
|
+
// long opaque phases such as npm install still show visible motion. The
|
|
186
|
+
// ceiling stays a whole point below the milestone: settling on `creepTo`
|
|
187
|
+
// itself would render as that milestone and make arriving at it invisible,
|
|
188
|
+
// which is what a stalled bar looks like.
|
|
189
|
+
timer = setInterval(() => {
|
|
190
|
+
current = Math.min(creepTo - 1, current + (creepTo - current) * 0.04);
|
|
191
|
+
draw();
|
|
192
|
+
}, PROGRESS_TICK_MS);
|
|
193
|
+
timer.unref?.();
|
|
194
|
+
},
|
|
195
|
+
finish() {
|
|
196
|
+
stopCreep();
|
|
197
|
+
if (!visible)
|
|
198
|
+
return;
|
|
199
|
+
output.stdout(`\r${renderProgressBar(100)}\n`);
|
|
200
|
+
visible = false;
|
|
201
|
+
},
|
|
202
|
+
clear() {
|
|
203
|
+
stopCreep();
|
|
204
|
+
if (!visible)
|
|
205
|
+
return;
|
|
206
|
+
output.stdout(`\r${" ".repeat(PROGRESS_BAR_WIDTH + 6)}\r`);
|
|
207
|
+
visible = false;
|
|
208
|
+
shown = -1;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
124
212
|
export async function runSelfUpdate(options) {
|
|
125
213
|
const fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
126
214
|
const output = options.output ?? defaultOutput;
|
|
@@ -157,7 +245,8 @@ export async function runSelfUpdate(options) {
|
|
|
157
245
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(targetLookup.result.stdout.trim())}.`)}\n`);
|
|
158
246
|
return 1;
|
|
159
247
|
}
|
|
160
|
-
output.stdout(`${PRODUCT_TEXT.
|
|
248
|
+
output.stdout(`${PRODUCT_TEXT.commandName} update (${channel}): ${runningVersion} → ${targetVersion}.\n`);
|
|
249
|
+
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
|
|
161
250
|
const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
|
|
162
251
|
if (rootLookup.result === null)
|
|
163
252
|
return rootLookup.exitCode;
|
|
@@ -190,9 +279,11 @@ export async function runSelfUpdate(options) {
|
|
|
190
279
|
await transactionStore.finish("completed");
|
|
191
280
|
}
|
|
192
281
|
await transactionStore.clearCompleted();
|
|
193
|
-
output.stdout(`${PRODUCT_TEXT.
|
|
282
|
+
output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
|
|
194
283
|
return 0;
|
|
195
284
|
}
|
|
285
|
+
// The bar first appears here so a no-change run never flashes it.
|
|
286
|
+
progress.set(3, 15);
|
|
196
287
|
const cohortState = await new CohortStateStore(paths.dataDir).read();
|
|
197
288
|
transaction = await transactionStore.begin({
|
|
198
289
|
channel,
|
|
@@ -207,6 +298,7 @@ export async function runSelfUpdate(options) {
|
|
|
207
298
|
});
|
|
208
299
|
transaction = await transactionStore.advance("ownership-released");
|
|
209
300
|
}
|
|
301
|
+
progress.set(15, 70);
|
|
210
302
|
if (phaseBefore(transaction.phase, "package-installed")) {
|
|
211
303
|
const installation = await measure("npm-install", async () => await runNpm(runner, ["install", "--global", "--loglevel=error", "--no-fund", "--no-audit", `${PRODUCT_PACKAGE}@${targetVersion}`], true, output, "start the global npm installation", false));
|
|
212
304
|
if (installation.result === null)
|
|
@@ -218,15 +310,22 @@ export async function runSelfUpdate(options) {
|
|
|
218
310
|
}
|
|
219
311
|
transaction = await transactionStore.advance("package-installed");
|
|
220
312
|
}
|
|
313
|
+
progress.set(70, 75);
|
|
221
314
|
// Ownership can be reacquired after an interrupted installation (for
|
|
222
315
|
// example, if bare A1 is launched before the update is resumed). Recheck
|
|
223
316
|
// immediately before activation so recovery cannot start a second cohort.
|
|
224
317
|
await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
|
|
318
|
+
progress.set(75, MATERIALIZE_PROGRESS.from);
|
|
225
319
|
let activationPhaseStartedAt = now();
|
|
226
320
|
await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
|
|
227
321
|
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
228
322
|
transaction = await transactionStore.advance(phase);
|
|
323
|
+
progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
|
|
229
324
|
activationPhaseStartedAt = now();
|
|
325
|
+
}, ({ completed, total }) => {
|
|
326
|
+
const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
|
|
327
|
+
const done = total > 0 ? Math.min(1, completed / total) : 0;
|
|
328
|
+
progress.set(MATERIALIZE_PROGRESS.from + span * done);
|
|
230
329
|
});
|
|
231
330
|
options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
232
331
|
const transactionStartedAt = now();
|
|
@@ -234,10 +333,12 @@ export async function runSelfUpdate(options) {
|
|
|
234
333
|
await transactionStore.finish("completed");
|
|
235
334
|
await transactionStore.clearCompleted();
|
|
236
335
|
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
237
|
-
|
|
336
|
+
progress.finish();
|
|
337
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion} (${channel}).\n`);
|
|
238
338
|
return 0;
|
|
239
339
|
}
|
|
240
340
|
catch (error) {
|
|
341
|
+
progress.clear();
|
|
241
342
|
const message = errorMessage(error);
|
|
242
343
|
const rollback = options.lifecycle
|
|
243
344
|
? "previous test lifecycle retained"
|
|
@@ -69,11 +69,11 @@ This optional, mutating presentation-maintenance workflow regenerates component
|
|
|
69
69
|
|
|
70
70
|
Presentation acceptance is the reader comparing `a1 pi` with pinned Pi. `node scripts/check-pinned-pi-source-ledger.mjs` validates accepted provenance; its `--engine-only` mode validates the ownership partition without comparing private upstream source.
|
|
71
71
|
|
|
72
|
-
##
|
|
72
|
+
## Publication
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
One workflow publishes both channels: `.github/workflows/release.yml`, triggered by every push to `develop`. What the pushed commit declares decides the channel: a prerelease version publishes a preview under npm `next` at a unique `-dev.N` version stamped at publish time, and a stable version publishes under npm `latest`. Both pack once, validate those exact bytes on Windows, Linux, and macOS, and publish with provenance from the `npm-publish` environment. The publisher never rebuilds, a preview never changes `latest`, and the release tag, GitHub Release, and `master` are written only after the registry has the package.
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
`docs/ci-release-runbook.md` is the operational reference.
|
|
77
77
|
|
|
78
78
|
## A1 state paths
|
|
79
79
|
|