@timurproko/a1 0.1.8-dev.436 → 0.1.8-dev.443
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/dist/foundation/startup/index.d.ts +1 -0
- package/dist/foundation/startup/index.js +1 -0
- package/dist/foundation/startup/startup-budget.d.ts +46 -0
- package/dist/foundation/startup/startup-budget.js +42 -0
- package/dist/foundation/startup/startup-runtime.d.ts +0 -19
- package/dist/foundation/startup/startup-runtime.js +0 -22
- 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/docs/local-worktree-cleanup.md +20 -3
- package/docs/openspec-archive-automation.md +1 -1
- package/docs/validation.md +17 -0
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StartupTraceEvent } from "./startup-runtime.js";
|
|
2
|
+
export interface StartupPerformanceEvidence {
|
|
3
|
+
readonly profileId: "a1" | "pi";
|
|
4
|
+
readonly launchKind: "post-update" | "no-live-supervisor" | "warm";
|
|
5
|
+
readonly events: readonly StartupTraceEvent[];
|
|
6
|
+
readonly moduleGraph?: {
|
|
7
|
+
readonly loadedFiles: number;
|
|
8
|
+
readonly evaluatedBytes: number;
|
|
9
|
+
readonly groups: readonly {
|
|
10
|
+
readonly group: string;
|
|
11
|
+
readonly files: number;
|
|
12
|
+
readonly evaluatedBytes: number;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface StartupBudgetViolation {
|
|
17
|
+
readonly profileId: "a1" | "pi";
|
|
18
|
+
readonly launchKind: "post-update" | "no-live-supervisor" | "warm";
|
|
19
|
+
readonly elapsedMs: number;
|
|
20
|
+
readonly budgetMs: number;
|
|
21
|
+
readonly dominantPhases: readonly {
|
|
22
|
+
readonly phase: string;
|
|
23
|
+
readonly durationMs: number;
|
|
24
|
+
}[];
|
|
25
|
+
readonly moduleGraph?: StartupPerformanceEvidence["moduleGraph"];
|
|
26
|
+
}
|
|
27
|
+
/** Resolve the declared budget for one launch kind so measurement and reporting never restate the numbers. */
|
|
28
|
+
export declare function resolveStartupBudgetMs(launchKind: StartupPerformanceEvidence["launchKind"], budgets?: {
|
|
29
|
+
readonly postUpdateMs: number;
|
|
30
|
+
readonly noSupervisorMs: number;
|
|
31
|
+
readonly warmMs: number;
|
|
32
|
+
}): number;
|
|
33
|
+
/** Compare measured startup evidence with its budget without deciding whether the channel enforces it. */
|
|
34
|
+
export declare function evaluateStartupPerformanceBudget(evidence: StartupPerformanceEvidence, budgets?: {
|
|
35
|
+
readonly postUpdateMs: number;
|
|
36
|
+
readonly noSupervisorMs: number;
|
|
37
|
+
readonly warmMs: number;
|
|
38
|
+
}): StartupBudgetViolation | null;
|
|
39
|
+
/** Render one budget violation as the single message used by failures, evidence, and run summaries. */
|
|
40
|
+
export declare function formatStartupBudgetViolation(violation: StartupBudgetViolation): string;
|
|
41
|
+
/** Enforce the startup budget in a channel that fails on an overrun. */
|
|
42
|
+
export declare function assertStartupPerformanceBudget(evidence: StartupPerformanceEvidence, budgets?: {
|
|
43
|
+
readonly postUpdateMs: number;
|
|
44
|
+
readonly noSupervisorMs: number;
|
|
45
|
+
readonly warmMs: number;
|
|
46
|
+
}): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const STARTUP_PERFORMANCE_BUDGETS = { postUpdateMs: 2_000, noSupervisorMs: 2_500, warmMs: 2_000 };
|
|
2
|
+
/** Resolve the declared budget for one launch kind so measurement and reporting never restate the numbers. */
|
|
3
|
+
export function resolveStartupBudgetMs(launchKind, budgets = STARTUP_PERFORMANCE_BUDGETS) {
|
|
4
|
+
return launchKind === "warm" ? budgets.warmMs : launchKind === "no-live-supervisor" ? budgets.noSupervisorMs : budgets.postUpdateMs;
|
|
5
|
+
}
|
|
6
|
+
/** Compare measured startup evidence with its budget without deciding whether the channel enforces it. */
|
|
7
|
+
export function evaluateStartupPerformanceBudget(evidence, budgets = STARTUP_PERFORMANCE_BUDGETS) {
|
|
8
|
+
const events = [...evidence.events].sort((left, right) => left.elapsedMs - right.elapsedMs);
|
|
9
|
+
const ready = events.findLast(event => event.phase === "first-input-ready-render");
|
|
10
|
+
// Invariant: a launch that never became input-ready is a functional failure rather than a
|
|
11
|
+
// timing observation, so it throws in every enforcement mode instead of being recorded.
|
|
12
|
+
if (!ready)
|
|
13
|
+
throw new Error(`startup budget failed for ${evidence.profileId}: first input-ready render was not recorded`);
|
|
14
|
+
const budgetMs = resolveStartupBudgetMs(evidence.launchKind, budgets);
|
|
15
|
+
if (ready.elapsedMs <= budgetMs)
|
|
16
|
+
return null;
|
|
17
|
+
const intervals = events.map((event, index) => ({
|
|
18
|
+
phase: event.phase,
|
|
19
|
+
durationMs: event.elapsedMs - (events[index - 1]?.elapsedMs ?? 0),
|
|
20
|
+
})).sort((left, right) => right.durationMs - left.durationMs);
|
|
21
|
+
return {
|
|
22
|
+
profileId: evidence.profileId,
|
|
23
|
+
launchKind: evidence.launchKind,
|
|
24
|
+
elapsedMs: ready.elapsedMs,
|
|
25
|
+
budgetMs,
|
|
26
|
+
dominantPhases: intervals.slice(0, 3),
|
|
27
|
+
...evidence.moduleGraph === undefined ? {} : { moduleGraph: evidence.moduleGraph },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Render one budget violation as the single message used by failures, evidence, and run summaries. */
|
|
31
|
+
export function formatStartupBudgetViolation(violation) {
|
|
32
|
+
const graph = violation.moduleGraph === undefined ? ""
|
|
33
|
+
: `; module graph: ${violation.moduleGraph.loadedFiles} files, ${violation.moduleGraph.evaluatedBytes} evaluated bytes (${violation.moduleGraph.groups.map(group => `${group.group} ${group.files}/${group.evaluatedBytes}`).join(", ")})`;
|
|
34
|
+
const phases = violation.dominantPhases.map(item => `${item.phase} ${Math.round(item.durationMs)}ms`).join(", ");
|
|
35
|
+
return `startup budget failed for ${violation.profileId} ${violation.launchKind}: ${Math.round(violation.elapsedMs)}ms exceeds ${violation.budgetMs}ms; dominant phases: ${phases}${graph}`;
|
|
36
|
+
}
|
|
37
|
+
/** Enforce the startup budget in a channel that fails on an overrun. */
|
|
38
|
+
export function assertStartupPerformanceBudget(evidence, budgets) {
|
|
39
|
+
const violation = evaluateStartupPerformanceBudget(evidence, budgets);
|
|
40
|
+
if (violation)
|
|
41
|
+
throw new Error(formatStartupBudgetViolation(violation));
|
|
42
|
+
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { PRODUCT_IDENTITY } from "../../product-identity.js";
|
|
2
2
|
export type StartupPhase = "command-invoked" | "bootstrap-start" | "bootstrap-selected" | "durable-validation-start" | "durable-validation-complete" | "replacement-supervisor-start" | "replacement-supervisor-ready" | "guardian-start" | "guardian-connected" | "ui-entry" | "ui-modules-loaded" | "pi-services" | "resource-discovery" | "session-created" | "settings-loaded" | "first-input-ready-render";
|
|
3
|
-
export interface StartupPerformanceEvidence {
|
|
4
|
-
readonly profileId: "a1" | "pi";
|
|
5
|
-
readonly launchKind: "post-update" | "no-live-supervisor" | "warm";
|
|
6
|
-
readonly events: readonly StartupTraceEvent[];
|
|
7
|
-
readonly moduleGraph?: {
|
|
8
|
-
readonly loadedFiles: number;
|
|
9
|
-
readonly evaluatedBytes: number;
|
|
10
|
-
readonly groups: readonly {
|
|
11
|
-
readonly group: string;
|
|
12
|
-
readonly files: number;
|
|
13
|
-
readonly evaluatedBytes: number;
|
|
14
|
-
}[];
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
3
|
export interface StartupTraceEvent {
|
|
18
4
|
readonly schema: typeof PRODUCT_IDENTITY.evidence.startupTraceSchema;
|
|
19
5
|
readonly traceId: string;
|
|
@@ -38,9 +24,4 @@ export declare function enableStartupCompileCache(dataDir: string, releaseId: st
|
|
|
38
24
|
export declare function startupCompileCachePath(dataDir: string, releaseId: string | null, dependencyLayerIds: readonly string[]): string;
|
|
39
25
|
/** Retain current compile namespaces plus a bounded number of recent fallbacks. */
|
|
40
26
|
export declare function collectCompileCaches(dataDir: string, protectedPaths: readonly string[], keepRecent?: number): Promise<void>;
|
|
41
|
-
export declare function assertStartupPerformanceBudget(evidence: StartupPerformanceEvidence, budgets?: {
|
|
42
|
-
readonly postUpdateMs: number;
|
|
43
|
-
readonly noSupervisorMs: number;
|
|
44
|
-
readonly warmMs: number;
|
|
45
|
-
}): void;
|
|
46
27
|
export declare function parseStartupTrace(source: string): readonly StartupTraceEvent[];
|
|
@@ -97,28 +97,6 @@ export async function collectCompileCaches(dataDir, protectedPaths, keepRecent =
|
|
|
97
97
|
await rm(candidate.path, { recursive: true, force: true });
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
export function assertStartupPerformanceBudget(evidence, budgets = {
|
|
101
|
-
postUpdateMs: 2_000,
|
|
102
|
-
noSupervisorMs: 2_500,
|
|
103
|
-
warmMs: 2_000,
|
|
104
|
-
}) {
|
|
105
|
-
const events = [...evidence.events].sort((left, right) => left.elapsedMs - right.elapsedMs);
|
|
106
|
-
const ready = events.findLast(event => event.phase === "first-input-ready-render");
|
|
107
|
-
if (!ready)
|
|
108
|
-
throw new Error(`startup budget failed for ${evidence.profileId}: first input-ready render was not recorded`);
|
|
109
|
-
const budget = evidence.launchKind === "warm"
|
|
110
|
-
? budgets.warmMs
|
|
111
|
-
: evidence.launchKind === "no-live-supervisor" ? budgets.noSupervisorMs : budgets.postUpdateMs;
|
|
112
|
-
if (ready.elapsedMs <= budget)
|
|
113
|
-
return;
|
|
114
|
-
const intervals = events.map((event, index) => ({
|
|
115
|
-
phase: event.phase,
|
|
116
|
-
durationMs: event.elapsedMs - (events[index - 1]?.elapsedMs ?? 0),
|
|
117
|
-
})).sort((left, right) => right.durationMs - left.durationMs);
|
|
118
|
-
const graph = evidence.moduleGraph === undefined ? ""
|
|
119
|
-
: `; module graph: ${evidence.moduleGraph.loadedFiles} files, ${evidence.moduleGraph.evaluatedBytes} evaluated bytes (${evidence.moduleGraph.groups.map(group => `${group.group} ${group.files}/${group.evaluatedBytes}`).join(", ")})`;
|
|
120
|
-
throw new Error(`startup budget failed for ${evidence.profileId} ${evidence.launchKind}: ${Math.round(ready.elapsedMs)}ms exceeds ${budget}ms; dominant phases: ${intervals.slice(0, 3).map(item => `${item.phase} ${Math.round(item.durationMs)}ms`).join(", ")}${graph}`);
|
|
121
|
-
}
|
|
122
100
|
export function parseStartupTrace(source) {
|
|
123
101
|
const events = source.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
|
|
124
102
|
for (const event of events) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-16T18:44:13.029Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-16T18:44:05.437Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-16T18:44:34.962Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "47b347f1a5782f17d232cf3e2619fdba4c91a96427d931b2a9b716b888c7470d",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Local worktree cleanup after verified delivery
|
|
2
2
|
|
|
3
|
-
Local cleanup completes the delivery order in [the archive runbook](openspec-archive-automation.md). For version 3 that is one authorized manual implementation/acceptance/archive merge, read-only verification, remote topic-ref deletion, then safe local cleanup. Legacy versions still require their applicable acceptance-record and automatic archive merges. GitHub Actions never reaches into a developer machine.
|
|
3
|
+
Local cleanup completes the delivery order in [the archive runbook](openspec-archive-automation.md). For version 3 that is one authorized manual implementation/acceptance/archive merge, read-only verification, remote topic-ref deletion, then safe local cleanup. Legacy versions still require their applicable acceptance-record and automatic archive merges. GitHub Actions never reaches into a developer machine. Local cleanup never publishes archives or merges PRs; `complete`, preview, and queue/watch do not delete remote refs, while explicitly confirmed closed-unmerged `discard` owns only its exact expected-SHA topic-ref deletion.
|
|
4
4
|
|
|
5
|
-
The implementation is repository tooling, not part of the installed A1 product. It requires Node, Git, and GitHub read access. No product build, dependency installation, interactive UI, or OS-service provisioning is needed. It supports this repository's `origin` on github.com, via HTTPS or SSH.
|
|
5
|
+
The implementation is repository tooling, not part of the installed A1 product. It requires Node, Git, and GitHub read access; the explicit closed-unmerged discard operation additionally requires authenticated permission to delete its exact remote topic ref. No product build, dependency installation, interactive UI, or OS-service provisioning is needed. It supports this repository's `origin` on github.com, via HTTPS or SSH.
|
|
6
6
|
|
|
7
7
|
## Standard completed-delivery command
|
|
8
8
|
|
|
@@ -18,10 +18,27 @@ node scripts/governance/local-worktree-cleanup.mjs complete \
|
|
|
18
18
|
|
|
19
19
|
`complete` is explicit cleanup authorization for that exact candidate. It creates and releases an exact registration when needed, applies the repository-owned generated-path policy, verifies live merge/archive/CI/ref evidence, evaluates only that candidate, uses journaled non-force Git removal, deletes only the unchanged local topic ref, and leaves persistent watcher authority unchanged. Repeating it reports the completed candidate as already absent. Existing conflicting ownership, identity drift, unavailable evidence, or unknown content remains blocking.
|
|
20
20
|
|
|
21
|
-
The central disposable policy is `node_modules`, `dist`, `.builds`, `.artifacts/openspec-archive`,
|
|
21
|
+
The central disposable policy is `node_modules`, `dist`, `.builds`, `.artifacts/openspec-archive`, `.artifacts/validation`, `native/process-guardian/target`, and `native/terminal-host/target`. The artifact roots contain repository-generated finalization and validation reports; the two native roots contain repository-generated Cargo output. Each encountered path must be ignored and stay inside the exact worktree with no link, special file, or nested repository boundary. Authority is component-exact: `.artifacts`, sibling directories such as `.artifacts/other`, near matches such as `.artifacts/validation-user`, arbitrary `target` directories, and sibling native projects remain blocking. Tracked/staged/unstaged/untracked content and every unknown ignored path still block. A tracked regular `.gitmodules` file alone is ordinary content; actual nested `.git` metadata, gitlinks, configured submodules, and submodule changes block. Ordinary content and these approved generated roots are traversed under separate finite entry allowances, so a normal dependency installation does not consume the ordinary source-tree allowance; both allowances retain the same deadline and content-boundary checks.
|
|
22
22
|
|
|
23
23
|
Agents do not manually remove generated content, call `git worktree remove`, or delete the local branch after delivery. The JSON result is authoritative: report success only for `removed` or verified `already-absent`; otherwise retain the worktree and report the exact blocker. Legacy roles can supply separate `--source-pr`, `--candidate-pr`, and `--role` values.
|
|
24
24
|
|
|
25
|
+
## Explicit closed-unmerged discard
|
|
26
|
+
|
|
27
|
+
Closing a PR does not itself authorize deletion. After the maintainer explicitly rejects one exact PR and separately confirms remote deletion, run the candidate-scoped command from the primary checkout:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node scripts/governance/local-worktree-cleanup.mjs discard \
|
|
31
|
+
--repo D:/Git/a1 \
|
|
32
|
+
--path D:/Git/a1/.worktrees/rejected-task \
|
|
33
|
+
--change rejected-change \
|
|
34
|
+
--pr 123 \
|
|
35
|
+
--confirm-closed-unmerged
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`discard` requires the named PR to remain closed without merge, target `develop`, belong to this repository, and identify the exact registered worktree HEAD and branch. It first applies the same clean-content and generated-root inspection used by `complete`. It then verifies the remote topic branch is unprotected, non-reserved, and still equals the PR head; deletes only that ref with an expected-SHA lease; verifies remote absence; rechecks the local candidate; removes the worktree non-forcibly; and atomically deletes only the unchanged local branch.
|
|
39
|
+
|
|
40
|
+
An open, merged, reopened, forked, protected, reserved, advanced, dirty, active, linked, nested, replaced, current, or unverifiable candidate remains blocking. Remote deletion followed by a Windows lock or later local blocker is reported as `partial`; the journal preserves the remaining worktree/local ref for the same exact confirmed command to inspect and resume. The command never scans by age or name, adopts another session's checkout, enables queue/watch, or turns PR close events into automatic discard authority.
|
|
41
|
+
|
|
25
42
|
## Preview first
|
|
26
43
|
|
|
27
44
|
From a checkout containing the reviewed tooling:
|
|
@@ -214,7 +214,7 @@ After version-3 merge, verify:
|
|
|
214
214
|
|
|
215
215
|
Only then shall the owning agent invoke the exact-candidate `complete` operation documented in [local cleanup](local-worktree-cleanup.md) from the primary checkout. The command owns registration/release, repository-generated disposables, one bounded evidence pass, non-force worktree removal, and unchanged local-ref cleanup; agents do not manually delete generated content, worktrees, or branches. Version 3 uses the implementation PR as both source and candidate and does not wait for nonexistent acceptance/archive PRs.
|
|
216
216
|
|
|
217
|
-
Closing an unmerged PR does not authorize local or remote deletion.
|
|
217
|
+
Closing an unmerged PR does not by itself authorize local or remote deletion. After explicit candidate-specific rejection and remote-deletion confirmation, the repository-owned local `discard` command may compare-and-delete only that closed-unmerged PR's exact unchanged same-repository unprotected topic ref, then apply the ordinary non-force local safeguards. Automatic remote cleanup remains merge-only and never touches local worktrees.
|
|
218
218
|
|
|
219
219
|
## Legacy delivery
|
|
220
220
|
|
package/docs/validation.md
CHANGED
|
@@ -44,6 +44,23 @@ A build receipt binds checkout head, complete build inputs, toolchain, emitted f
|
|
|
44
44
|
|
|
45
45
|
Npm download bytes may be reused with integrity checks and `--prefer-offline`, with normal network fallback. Every installation prefix remains fresh. Installed package trees, dependency certification, startup/profile state, mutable fixture repositories, passing outcomes, and publication evidence are never restored from caches.
|
|
46
46
|
|
|
47
|
+
## Startup budget enforcement
|
|
48
|
+
|
|
49
|
+
The exact-package startup gate always measures both profiles and all three launch kinds on the first attempt and never retries a measurement. `STARTUP_BUDGET_ENFORCEMENT` decides only what a timing overrun does:
|
|
50
|
+
|
|
51
|
+
| Value | Where | Effect |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `record` | Development publication (`release.yml` with `mode == 'develop'`) and the pull-request `startup` group in `ci.yml` | Keeps the measurement, appends the violation to the evidence, emits a `::warning::` annotation, and lets the run succeed. |
|
|
54
|
+
| `fail` | Nightly and stable publication, and Full regression | Throws the same message as before and blocks publication. |
|
|
55
|
+
|
|
56
|
+
Any absent, empty, or unrecognized value means `fail`, so a local run and a misspelled channel both keep enforcing. A launch that records no input-ready frame fails in either mode, because that is a functional failure rather than a timing observation.
|
|
57
|
+
|
|
58
|
+
`STARTUP_PERFORMANCE_RESULT` names the `a1-startup-performance-evidence-v1` file. It carries `enforcement`, `budgetViolations`, and one measurement per profile and launch kind with its `elapsedMs` and `budgetMs`. Publication lanes upload it as `release-validation-<version>-<platform>` next to the tier outcome and render it as a table in the run summary.
|
|
59
|
+
|
|
60
|
+
The budget numbers themselves live in one place, `src/foundation/startup/startup-budget.ts`, and are declared by the `a1-shell` capability. Do not restate them in a workflow.
|
|
61
|
+
|
|
62
|
+
To make development publication enforce budgets again, set `STARTUP_BUDGET_ENFORCEMENT: fail` in the `release.yml` validation step; rollback must not remove the measurement, the evidence fields, or the nightly and Full regression enforcement.
|
|
63
|
+
|
|
47
64
|
## Evidence inspection
|
|
48
65
|
|
|
49
66
|
Download these artifacts from the exact workflow run:
|
package/package.json
CHANGED