@cleocode/contracts 2026.10.3 → 2026.10.4

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.
@@ -19,9 +19,10 @@
19
19
  *
20
20
  * - `rewrite` — DEFAULT. Wrap it in place as
21
21
  * `cleo run --wait --passthrough --class <c> -- <cmd>`, when the harness
22
- * runs in `bypassPermissions` or `auto` mode and no deny or ask rule names
23
- * the command (otherwise the hook warns: a rewrite would change which
24
- * permission rules match).
22
+ * runs in `bypassPermissions` or `auto` mode, or (Claude Code `default`,
23
+ * `acceptEdits`, `dontAsk`) when the user's allow rules already approve the
24
+ * command, and no deny or ask rule names it. Otherwise the hook warns: a
25
+ * rewrite would change which permission rules match.
25
26
  * - `warn` — leave the command alone; add a context line naming the governed
26
27
  * form.
27
28
  * - `off` — do nothing (the installer also removes the hook entry).
@@ -124,9 +125,13 @@ export interface OpencodeHeavyCommandAnswer {
124
125
  export interface PreToolUseHookOutput {
125
126
  readonly hookSpecificOutput: {
126
127
  readonly hookEventName: 'PreToolUse';
127
- /** Omitted unless the provider requires it (Codex rewrites need `allow`). */
128
+ /**
129
+ * Omitted unless needed: Codex rewrites need `allow`, Kimi's "re-run it"
130
+ * is a `deny`, and a Claude Code rewrite carries `allow` only when the
131
+ * user's allow rules already approve the original command (T13124).
132
+ */
128
133
  readonly permissionDecision?: 'allow' | 'deny';
129
- /** Shown to the agent on `deny`. */
134
+ /** Shown to the agent on `deny`; written to Claude Code's debug log on `allow`. */
130
135
  readonly permissionDecisionReason?: string;
131
136
  /** Replaces the whole tool input. */
132
137
  readonly updatedInput?: ShellToolInput;
@@ -134,3 +139,168 @@ export interface PreToolUseHookOutput {
134
139
  readonly additionalContext?: string;
135
140
  };
136
141
  }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Project-level delivery (T13124)
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /**
148
+ * What delivering the hook to one provider did in one project.
149
+ *
150
+ * - `installed` / `updated` / `removed` / `unchanged` — the config file was
151
+ * written, refreshed, cleaned (mode `off`) or already current.
152
+ * - `skipped` — not applicable: the provider is not in use on this machine or
153
+ * project, or the project is the user's home directory (whose provider
154
+ * configs are user-global, which CLEO never writes).
155
+ * - `blocked` — a path the hook must live under exists but is not a
156
+ * directory (a stray `.codex` file, say); nothing was written.
157
+ * - `unsupported` — the provider cannot take a project-level hook (Kimi reads
158
+ * hooks only from its global config).
159
+ * - `needs-consent` — the config is the project's own (Codex's committable
160
+ * `.codex/hooks.json`, tracked or holding anything CLEO did not write), so
161
+ * CLEO does not write or hide it; `snippet` is the entry to add by hand.
162
+ * - `failed` — any other error, such as a config file that is not valid JSON
163
+ * (left untouched).
164
+ */
165
+ export type HeavyHookDeliveryStatus =
166
+ | 'installed'
167
+ | 'updated'
168
+ | 'removed'
169
+ | 'unchanged'
170
+ | 'skipped'
171
+ | 'blocked'
172
+ | 'unsupported'
173
+ | 'needs-consent'
174
+ | 'failed';
175
+
176
+ /** One provider's result from syncing the hook into a project. */
177
+ export interface HeavyHookDeliveryOutcome {
178
+ /** The harness. */
179
+ readonly provider: HeavyCommandHookProvider;
180
+ /** What the sync did. */
181
+ readonly status: HeavyHookDeliveryStatus;
182
+ /** The config file (or plugin file) the hook lives in, absolute. */
183
+ readonly target: string;
184
+ /** Why it was skipped, blocked, unsupported or failed. */
185
+ readonly reason?: string;
186
+ /** The exact step that fixes a `blocked`, `unsupported`, `needs-consent` or `failed` outcome. */
187
+ readonly remedy?: string;
188
+ /** `needs-consent`: the exact JSON entry to add to the config by hand. */
189
+ readonly snippet?: string;
190
+ }
191
+
192
+ /**
193
+ * The hook's state for one provider in one project, as `cleo doctor` reports it.
194
+ *
195
+ * - `installed` — present and identical to what this CLEO would write.
196
+ * - `outdated` — present but written by another CLEO build, only in a legacy
197
+ * location, or visible to git as an untracked file (gh#1805: it widens
198
+ * `cleo verify`'s evidence scope); `cleo upgrade` refreshes it.
199
+ * - `missing` — the provider is in use and the hook is absent.
200
+ * - `blocked` — see {@link HeavyHookDeliveryStatus}.
201
+ * - `unreadable` — the config file exists but cannot be read or parsed.
202
+ * - `unsupported` — the provider cannot take a project-level hook.
203
+ * - `needs-consent` — see {@link HeavyHookDeliveryStatus}; `snippet` holds the entry.
204
+ * - `disabled` — `resources.heavyCommandHook` is `off`, and no CLEO hook is left.
205
+ * - `not-detected` — the provider is not in use here; nothing is expected.
206
+ */
207
+ export type HeavyHookInstallState =
208
+ | 'installed'
209
+ | 'outdated'
210
+ | 'missing'
211
+ | 'blocked'
212
+ | 'unreadable'
213
+ | 'unsupported'
214
+ | 'needs-consent'
215
+ | 'disabled'
216
+ | 'not-detected';
217
+
218
+ /** One provider's hook state in one project. */
219
+ export interface HeavyHookInspection {
220
+ /** The harness. */
221
+ readonly provider: HeavyCommandHookProvider;
222
+ /** Whether the provider is in use on this machine or in this project. */
223
+ readonly detected: boolean;
224
+ /** The hook's state. */
225
+ readonly state: HeavyHookInstallState;
226
+ /** The config file (or plugin file) the hook lives in, absolute. */
227
+ readonly target: string;
228
+ /** One line saying what was found. */
229
+ readonly detail: string;
230
+ /** The exact step that fixes the state, when one is needed. */
231
+ readonly remedy?: string;
232
+ /** `needs-consent`: the exact JSON entry to add to the config by hand. */
233
+ readonly snippet?: string;
234
+ }
235
+
236
+ /**
237
+ * Whether the `cleo` a hook finds on PATH can answer it.
238
+ *
239
+ * - `current` — it answers `cleo hook heavy-command`.
240
+ * - `older` — it predates `cleo hook` (and `cleo run`): the hook lets every
241
+ * command run ungoverned. The hook's own marker (exit 127, "Unknown
242
+ * command") or a direct probe says so.
243
+ * - `missing` — no `cleo` on PATH: the hook stays silent.
244
+ * - `unknown` — the probe failed some other way (a broken version-manager
245
+ * shim, a timeout, a crash). The hook fails open, so it governs nothing
246
+ * while this lasts.
247
+ */
248
+ export type HeavyHookCliState = 'current' | 'older' | 'missing' | 'unknown';
249
+
250
+ /** The result of probing the `cleo` on PATH from a project directory. */
251
+ export interface HeavyHookCliProbe {
252
+ /** What the probe found. */
253
+ readonly state: HeavyHookCliState;
254
+ /** The `cleo` the hook resolves on PATH, or `null`. */
255
+ readonly path: string | null;
256
+ /** One line saying what was found. */
257
+ readonly detail: string;
258
+ /** The exact step that fixes (or diagnoses) a CLI that is not `current`. */
259
+ readonly remedy?: string;
260
+ }
261
+
262
+ /** Options for the project-level delivery functions (all injectable for tests). */
263
+ export interface HeavyHookDeliveryOptions {
264
+ /** Environment for provider detection (`PATH`, `HOME`, `CODEX_HOME`, …). Default `process.env`. */
265
+ readonly env?: Readonly<Record<string, string | undefined>>;
266
+ /** Providers to consider. Default: every provider CLEO knows. */
267
+ readonly providers?: readonly HeavyCommandHookProvider[];
268
+ /**
269
+ * Inspection only: run the git checks (a hook file git can see, a shared
270
+ * Codex `hooks.json`). Default `true`; the session briefing passes `false`
271
+ * to skip their git spawns (`cleo doctor` keeps them).
272
+ */
273
+ readonly gitChecks?: boolean;
274
+ }
275
+
276
+ /**
277
+ * The project-level delivery surface `@cleocode/adapters` exports at
278
+ * `@cleocode/adapters/heavy-command-hook`. `@cleocode/core` loads it at run
279
+ * time (adapters builds on core, so core cannot import it statically) for
280
+ * `cleo init`, `cleo upgrade`, `cleo doctor` and the session briefing.
281
+ */
282
+ export interface HeavyHookDeliveryApi {
283
+ /**
284
+ * Install, refresh or (mode `off`) remove CLEO's hook for every provider in
285
+ * use, each independently. Never throws: every provider gets an outcome.
286
+ */
287
+ readonly syncProjectHeavyCommandHooks: (
288
+ projectDir: string,
289
+ mode: HeavyCommandHookMode,
290
+ options?: HeavyHookDeliveryOptions,
291
+ ) => Promise<readonly HeavyHookDeliveryOutcome[]>;
292
+ /** Read-only: the hook's state for every provider. Never throws. */
293
+ readonly inspectProjectHeavyCommandHooks: (
294
+ projectDir: string,
295
+ mode: HeavyCommandHookMode,
296
+ options?: HeavyHookDeliveryOptions,
297
+ ) => readonly HeavyHookInspection[];
298
+ /**
299
+ * Whether the `cleo` on PATH (as the hook resolves it from `projectDir`)
300
+ * can answer the hook. Starts that `cleo` once. Never throws.
301
+ */
302
+ readonly probeHeavyHookCli: (
303
+ projectDir: string,
304
+ options?: HeavyHookDeliveryOptions,
305
+ ) => HeavyHookCliProbe;
306
+ }
package/src/index.ts CHANGED
@@ -780,6 +780,14 @@ export type {
780
780
  HeavyCommandHookProvider,
781
781
  HeavyCommandPlan,
782
782
  HeavyCommandSegment,
783
+ HeavyHookCliProbe,
784
+ HeavyHookCliState,
785
+ HeavyHookDeliveryApi,
786
+ HeavyHookDeliveryOptions,
787
+ HeavyHookDeliveryOutcome,
788
+ HeavyHookDeliveryStatus,
789
+ HeavyHookInspection,
790
+ HeavyHookInstallState,
783
791
  HookJsonValue,
784
792
  OpencodeHeavyCommandAnswer,
785
793
  PreToolUseHookInput,
@@ -2270,6 +2278,9 @@ export * from './render/index.js';
2270
2278
  export type {
2271
2279
  AdmissionResult,
2272
2280
  GovernorMode,
2281
+ HeavyHeapSource,
2282
+ HeavyLeverChange,
2283
+ HeavyToolResourcePlan,
2273
2284
  ResourceClass,
2274
2285
  ResourceDeferral,
2275
2286
  ResourceGrant,
@@ -3009,6 +3020,8 @@ export type {
3009
3020
  NexusAccountErrorCode,
3010
3021
  NexusAccountMe,
3011
3022
  NexusAccountOrganization,
3023
+ NexusAccountSetup,
3024
+ NexusAccountSetupStep,
3012
3025
  NexusAccountStatus,
3013
3026
  NexusAccountUser,
3014
3027
  NexusDeviceEndOutcome,
@@ -3092,6 +3105,18 @@ export {
3092
3105
  nexusCloudUserSchema,
3093
3106
  nexusCloudWhoamiSchema,
3094
3107
  } from './nexus-cloud.js';
3108
+ // === Guided first run of `cleo login nexus` (T13102) ===
3109
+ export {
3110
+ NEXUS_FIRST_RUN_STATES,
3111
+ type NexusFirstRunBackup,
3112
+ type NexusFirstRunChoice,
3113
+ type NexusFirstRunOffer,
3114
+ type NexusFirstRunRestore,
3115
+ type NexusFirstRunResult,
3116
+ type NexusFirstRunState,
3117
+ type NexusNamedProject,
3118
+ type NexusProjectNameSource,
3119
+ } from './nexus-first-run.js';
3095
3120
  export {
3096
3121
  type CloudActivityItem,
3097
3122
  type CloudActivityResult,
@@ -101,6 +101,11 @@ export const NEXUS_ACCOUNT_ERROR_CODES = [
101
101
  'E_NEXUS_VAULT_STORE_BUSY',
102
102
  /** A project restore target already holds a different project. */
103
103
  'E_NEXUS_VAULT_TARGET_OCCUPIED',
104
+ // Projects by name (`cleo cloud restore <name>`, T13102).
105
+ /** No project of the account has that name, label or id. */
106
+ 'E_NEXUS_PROJECT_NOT_FOUND',
107
+ /** Several of the account's projects have that name or label; the error lists them. */
108
+ 'E_NEXUS_PROJECT_AMBIGUOUS',
104
109
  ] as const;
105
110
 
106
111
  /** One of {@link NEXUS_ACCOUNT_ERROR_CODES}. */
@@ -181,6 +186,72 @@ export interface NexusLoginDevice {
181
186
  created: boolean | null;
182
187
  }
183
188
 
189
+ /**
190
+ * A step of the account setup `cleo login nexus` runs right after enrolment
191
+ * (T13100), named when it fails:
192
+ *
193
+ * - `connect`: open the vault connection with the new device credential;
194
+ * - `escrow-read`: read the escrowed account master key (`GET /v1/account/keys/escrow`);
195
+ * - `escrow-mint`: on the account's first device, create the master key and escrow it
196
+ * (`PUT /v1/account/keys/escrow`; a 409 means another device won, and its key is read);
197
+ * - `certify`: certify this device under the master key (`PUT /v1/devices/:id/key`);
198
+ * - `trust`: evaluate and record the signer trust state (`GET /v1/devices/trust`,
199
+ * `<cleoHome>/nexus-vault.json`).
200
+ */
201
+ export type NexusAccountSetupStep = 'connect' | 'escrow-read' | 'escrow-mint' | 'certify' | 'trust';
202
+
203
+ /**
204
+ * What `cleo login nexus` did to make the account ready for encrypted backups
205
+ * (T13100). Holds no key.
206
+ *
207
+ * - `ready`: this device holds the account master key and is certified under it;
208
+ * - `unsupported`: the server has no account key escrow; the login still succeeded;
209
+ * - `skipped`: a read-only device, which never mints or certifies;
210
+ * - `failed`: the login succeeded but the setup did not; `step` and `fix` say where and what to do.
211
+ */
212
+ export type NexusAccountSetup =
213
+ | {
214
+ status: 'ready';
215
+ /**
216
+ * `fetched`: the escrowed key was opened; `minted`: this device created and escrowed it;
217
+ * `adopted`: another device escrowed first (409) and its key was read, never re-minted.
218
+ */
219
+ escrow: 'fetched' | 'minted' | 'adopted';
220
+ /** `new`: this login certified the device; `existing`: it already was. */
221
+ certificate: 'new' | 'existing';
222
+ /** Version of the account master key. */
223
+ keyVersion: number;
224
+ /** One human line. */
225
+ summary: string;
226
+ }
227
+ | {
228
+ status: 'unsupported';
229
+ /** `E_NEXUS_VAULT_UNSUPPORTED`. */
230
+ code: NexusAccountErrorCode;
231
+ /** The remedy. */
232
+ fix: string;
233
+ /** One human line. */
234
+ summary: string;
235
+ }
236
+ | {
237
+ status: 'skipped';
238
+ /** One human line. */
239
+ summary: string;
240
+ }
241
+ | {
242
+ status: 'failed';
243
+ /** The step that failed. */
244
+ step: NexusAccountSetupStep;
245
+ /** Stable error code (a {@link NexusAccountErrorCode} when the flow raised one). */
246
+ code: string;
247
+ /** What went wrong. */
248
+ message: string;
249
+ /** The remedy. */
250
+ fix: string;
251
+ /** One human line naming the step and the remedy. */
252
+ summary: string;
253
+ };
254
+
184
255
  /** Result of `cleo login nexus` (never carries the token). */
185
256
  export interface NexusLoginResult {
186
257
  /** API origin the session belongs to. */
@@ -199,6 +270,8 @@ export interface NexusLoginResult {
199
270
  device?: NexusLoginDevice;
200
271
  /** Scopes of the stored device credential; present only with device credentials. */
201
272
  scopes?: string[];
273
+ /** The account key setup run right after enrolment (T13100); present only with device credentials. */
274
+ account?: NexusAccountSetup;
202
275
  }
203
276
 
204
277
  /** Result of `cleo logout nexus`. */
@@ -167,8 +167,15 @@ const projectFields = {
167
167
  export const nexusCloudProjectListItemSchema = z.object({
168
168
  ...projectFields,
169
169
  role: z.string(),
170
+ /**
171
+ * The project name sealed with the project data key (`Project.encryptedName`), base64;
172
+ * only a holder of the account key can open it (T13102).
173
+ */
174
+ encryptedName: z.string().nullable().optional(),
170
175
  streamId: z.string().optional(),
171
176
  headSeq: z.number().int().nonnegative().nullable().optional(),
177
+ /** The stream's head snapshot; null when nothing was ever pushed (T13102: restorable or not). */
178
+ headCheckpointId: z.string().nullable().optional(),
172
179
  openConflicts: z.number().int().nonnegative().optional(),
173
180
  replicas: z.array(nexusCloudReplicaSchema).default([]),
174
181
  devices: nexusCloudDeviceCountsSchema.optional(),
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Guided first run of `cleo login nexus` (T13102, owner decision 2026-10-02:
3
+ * onboarding = A + B + C' + D, so that `cleo login` is the only setup step in
4
+ * practice).
5
+ *
6
+ * After a successful sign-in, login looks at where it runs:
7
+ *
8
+ * - Inside a CLEO project this machine has not linked: it offers to link the
9
+ * project and take the first encrypted backup. `--yes` does it without asking,
10
+ * a terminal asks, and a non-interactive (agent) run never asks: it reports
11
+ * the exact next command. When Cleo Nexus already holds a backup of the
12
+ * project that this copy never synced (a fresh clone on a new machine), the
13
+ * offer is to restore that backup here instead; a backup would be refused.
14
+ * - Outside a CLEO project: it lists the account's projects, by name, with
15
+ * the exact `cleo cloud restore` command for each one that has a backup
16
+ * this machine does not hold yet.
17
+ *
18
+ * `cleo cloud restore <name>` resolves a project name or label (or an id) to
19
+ * the server's project id; an ambiguous name lists the candidates.
20
+ *
21
+ * This file is types + const data only (arch gate 10).
22
+ *
23
+ * @task T13102
24
+ * @epic T12322
25
+ */
26
+
27
+ import type { NexusProjectLink } from './nexus-account.js';
28
+ import type { CloudWarning } from './nexus-cloud.js';
29
+ import type { CloudVaultSnapshot } from './nexus-vault.js';
30
+
31
+ /**
32
+ * How the guided first run ended.
33
+ *
34
+ * - `backed-up`: the project was linked (or already linked) and its first backup pushed
35
+ * (or the cloud already held this exact state).
36
+ * - `link-failed`: linking failed; nothing was pushed.
37
+ * - `backup-failed`: the project is linked, but the backup failed.
38
+ * - `restored`: the cloud held a backup this copy never synced; it was restored here and linked.
39
+ * - `restore-failed`: restoring that backup failed (for example, local rows it would overwrite).
40
+ * - `offered`: a non-interactive run inside an unlinked project; nothing was done and
41
+ * `nextCommand` says what to run (link and back up, or restore; see `offer`).
42
+ * - `declined`: the user answered no at the prompt; `nextCommand` says what to run later.
43
+ * - `already-linked`: the project is linked and attached from this device; nothing to do.
44
+ * - `projects`: outside a CLEO project; `projects` lists the account's projects.
45
+ * - `skipped`: the first run does not apply (`reason` says why).
46
+ */
47
+ export const NEXUS_FIRST_RUN_STATES = [
48
+ 'backed-up',
49
+ 'link-failed',
50
+ 'backup-failed',
51
+ 'restored',
52
+ 'restore-failed',
53
+ 'offered',
54
+ 'declined',
55
+ 'already-linked',
56
+ 'projects',
57
+ 'skipped',
58
+ ] as const;
59
+
60
+ /** One of {@link NEXUS_FIRST_RUN_STATES}. */
61
+ export type NexusFirstRunState = (typeof NEXUS_FIRST_RUN_STATES)[number];
62
+
63
+ /** Where a project's display name came from. */
64
+ export type NexusProjectNameSource = 'encrypted-name' | 'label' | 'id';
65
+
66
+ /** One of the account's projects, as the first run and `cleo cloud restore <name>` see it. */
67
+ export interface NexusNamedProject {
68
+ /** The server's project id (the tracked local project id of whoever linked it). */
69
+ projectId: string;
70
+ /**
71
+ * Display name: the plaintext label, else the id. (`encryptedName` is shown only through a
72
+ * reader for its format, which cleo-nexus T098 will specify; none is wired in yet.)
73
+ */
74
+ name: string;
75
+ /** Where {@link NexusNamedProject.name} came from. */
76
+ nameSource: NexusProjectNameSource;
77
+ /** The plaintext label the server holds, if any. */
78
+ label: string | null;
79
+ /** The owning organization's name, when the server sent it. */
80
+ organizationName: string | null;
81
+ /** The newest sync over every replica, when the server sent it. */
82
+ lastSyncAt: string | null;
83
+ /**
84
+ * The project's stream holds a snapshot to restore. `null` when the server did not say
85
+ * (an older server without `headCheckpointId` on the project list).
86
+ */
87
+ hasBackup: boolean | null;
88
+ /** A replica of the project is attached from this device. */
89
+ onThisDevice: boolean;
90
+ /**
91
+ * The exact command that restores it into the current directory, by its id (an id never
92
+ * changes; for agents and scripts). `null` when there is nothing to restore here (no
93
+ * backup, or it is already on this device).
94
+ */
95
+ restoreCommand: string | null;
96
+ /**
97
+ * The same restore by name, for a person to type: set only when the name resolves to this
98
+ * project alone (and cannot be read as a flag or an id), else `null`.
99
+ */
100
+ restoreByNameCommand: string | null;
101
+ }
102
+
103
+ /** The first backup the guided run pushed. */
104
+ export interface NexusFirstRunBackup {
105
+ /** `pushed`: a new snapshot; `up-to-date`: the cloud already held this state. */
106
+ status: 'pushed' | 'up-to-date';
107
+ /** The snapshot, when the server named one. */
108
+ snapshot: CloudVaultSnapshot | null;
109
+ }
110
+
111
+ /** The backup the guided run restored into the project (`restored` only). */
112
+ export interface NexusFirstRunRestore {
113
+ /** `restored`: the snapshot was activated; `up-to-date`: this copy already held it. */
114
+ status: 'restored' | 'up-to-date';
115
+ /** The snapshot, when the server named one. */
116
+ snapshot: CloudVaultSnapshot | null;
117
+ /** Tables verified by count and hash. */
118
+ tables: number;
119
+ /** The local safety backup taken first, if any. */
120
+ safetyBackup: string | null;
121
+ }
122
+
123
+ /**
124
+ * What the run offered inside an unlinked project: `link-and-backup` (the
125
+ * project is new to the cloud, or this copy already synced with it), or
126
+ * `restore` (the cloud holds a backup this copy never synced).
127
+ */
128
+ export type NexusFirstRunOffer = 'link-and-backup' | 'restore';
129
+
130
+ /** One option of a choice the first run leaves to the user. */
131
+ export interface NexusFirstRunChoice {
132
+ /** The exact command. */
133
+ command: string;
134
+ /** What it does, in one line. */
135
+ effect: string;
136
+ }
137
+
138
+ /** The guided first run's outcome, carried as `data.firstRun` of the `cleo login nexus` envelope. */
139
+ export interface NexusFirstRunResult {
140
+ /** How it ended. */
141
+ state: NexusFirstRunState;
142
+ /** Why it was skipped (`skipped` only), e.g. a read-only device. */
143
+ reason: string | null;
144
+ /** The CLEO project login ran in, or `null` outside one. */
145
+ projectRoot: string | null;
146
+ /** The project's link after the run, when it is linked. */
147
+ link: NexusProjectLink | null;
148
+ /** What was offered inside an unlinked project, or `null` elsewhere. */
149
+ offer: NexusFirstRunOffer | null;
150
+ /** The backup pushed (`backed-up` only). */
151
+ backup: NexusFirstRunBackup | null;
152
+ /** The backup restored (`restored` only). */
153
+ restore: NexusFirstRunRestore | null;
154
+ /** The account's projects (`projects` only), newest first. */
155
+ projects: NexusNamedProject[];
156
+ /** The exact command to run next, when there is one (never asks: agents run it). */
157
+ nextCommand: string | null;
158
+ /**
159
+ * When the next step is a choice only the user can make (`nextCommand` is `null`), each
160
+ * option as `{ command, effect }`; empty otherwise.
161
+ */
162
+ choices: NexusFirstRunChoice[];
163
+ /** Non-fatal problems met on the way (secret-free). */
164
+ warnings: CloudWarning[];
165
+ }
@@ -126,10 +126,18 @@ export interface ProjectContext {
126
126
  command?: string;
127
127
  /**
128
128
  * Affected-scope test command template for `tool:test-affected` (D11150,
129
- * T12635): `{projects}`, `{filters}` and `{packages}` expand per affected
130
- * package, e.g. `pnpm exec vitest run {projects}`.
129
+ * T12635) and a scope-aware `tool:test` (T12959): `{projects}`,
130
+ * `{filters}`, `{workspaces}` and `{packages}` expand per affected
131
+ * package, e.g. `pnpm exec vitest run {projects}`. When absent, one is
132
+ * derived from a workspace-wide `command` where that is mechanical
133
+ * (`pnpm -r … test`, `npm … test --workspaces`, `turbo run test`; T13125).
131
134
  */
132
135
  affectedCommand?: string;
136
+ /**
137
+ * `false` makes `tool:test` always run the full suite instead of the
138
+ * affected scope (T12959).
139
+ */
140
+ preferAffected?: boolean;
133
141
  testFilePatterns?: string[];
134
142
  directories?: {
135
143
  unit?: string;
@@ -139,6 +139,66 @@ export function isResourceGrant(r: AdmissionResult): r is ResourceGrant {
139
139
  return r.deferred === false;
140
140
  }
141
141
 
142
+ /**
143
+ * Where the heap ceiling of a heavy tool run came from (T13122).
144
+ *
145
+ * - `default` — no heap in the caller's `NODE_OPTIONS`; CLEO's default applies.
146
+ * - `inherited` — the caller's `NODE_OPTIONS` heap fits the run's budget and is
147
+ * kept; the worker count shrinks so the run still fits.
148
+ * - `clamped` — the caller's `NODE_OPTIONS` heap exceeds the run's budget (a
149
+ * shell-profile export is not a per-project choice) and was lowered to it.
150
+ * - `override` — `CLEO_HEAVY_HEAP_MB` set it explicitly; never clamped.
151
+ */
152
+ export type HeavyHeapSource = 'default' | 'inherited' | 'clamped' | 'override';
153
+
154
+ /** One inherited environment value CLEO replaced because it exceeded the plan (T13122). */
155
+ export interface HeavyLeverChange {
156
+ /** Variable name, e.g. `VITEST_MAX_WORKERS` or `NODE_OPTIONS`. */
157
+ readonly name: string;
158
+ /** The inherited value, as the caller's environment carried it. */
159
+ readonly from: string;
160
+ /** The value the tool was spawned with. */
161
+ readonly to: string;
162
+ }
163
+
164
+ /**
165
+ * The resource plan a memory-bound tool (`test`, `build`, and since T13123
166
+ * `typecheck`, `lint`) was spawned with, and why (T13122). Reported by
167
+ * `cleo verify` (on the `tool` evidence atom and in a resource-kill message)
168
+ * and by `cleo run`, so an operator can see when an inherited value was
169
+ * clamped. A single-process tool's plan has one worker.
170
+ *
171
+ * The invariant it describes: `workspaceConcurrency × workers × heapMb` stays
172
+ * within `budgetMb`, unless an explicit `CLEO_HEAVY_*` override asked for more
173
+ * (`overBudget`).
174
+ */
175
+ export interface HeavyToolResourcePlan {
176
+ /** V8 old-space ceiling given to every Node process in the tool's tree, in MiB. */
177
+ readonly heapMb: number;
178
+ /** Where {@link heapMb} came from. */
179
+ readonly heapSource: HeavyHeapSource;
180
+ /** The heap the caller's `NODE_OPTIONS` asked for, in MiB; `null` when it set none. */
181
+ readonly inheritedHeapMb: number | null;
182
+ /** Worker count given to every runner that reads one (`VITEST_MAX_WORKERS`, `GOMAXPROCS`, …). */
183
+ readonly workers: number;
184
+ /** `plan` when derived from the heap and budget; `override` when `CLEO_HEAVY_WORKERS` set it. */
185
+ readonly workersSource: 'plan' | 'override';
186
+ /** Workspace packages allowed to run their script at once (`npm_config_workspace_concurrency`). */
187
+ readonly workspaceConcurrency: number;
188
+ /** Heap budget for the whole run, in MiB: the default worker count times the default heap. */
189
+ readonly budgetMb: number;
190
+ /** Total RAM the budget derives from, in MiB. */
191
+ readonly totalRamMb: number;
192
+ /** Inherited values replaced because they exceeded the plan. Empty when nothing was clamped. */
193
+ readonly clamped: readonly HeavyLeverChange[];
194
+ /** Inherited worker or concurrency values kept because they were within the plan, as `NAME=value`. */
195
+ readonly kept: readonly string[];
196
+ /** `true` when explicit `CLEO_HEAVY_*` overrides put the run over {@link budgetMb}. */
197
+ readonly overBudget: boolean;
198
+ /** One line naming the heap and worker count chosen and why. */
199
+ readonly summary: string;
200
+ }
201
+
142
202
  /** Explicit local user-manager connection for resource-controlled process launch. */
143
203
  export interface SystemdControlContext {
144
204
  /** Absolute existing runtime directory used only by the manager probe and launcher. */
package/src/task.ts CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  import type { AcceptanceGate, AcceptanceGateResult } from './acceptance-gate.js';
25
25
  import type { AcRow } from './data-accessor.js';
26
+ import type { HeavyToolResourcePlan } from './resource-governor.js';
26
27
  import type { TaskStatus } from './status-registry.js';
27
28
 
28
29
  export type { TaskStatus };
@@ -365,6 +366,13 @@ export type EvidenceAtom =
365
366
  treeHash?: string;
366
367
  /** `true` when the result was served from the tool cache (no spawn). */
367
368
  cacheHit?: boolean;
369
+ /**
370
+ * The heap, worker count and workspace concurrency a memory-bound tool
371
+ * (`test`, `build`, `typecheck`, `lint`) was spawned with, and why
372
+ * (T13122, T13123) — including any inherited `NODE_OPTIONS` heap or
373
+ * worker count that was clamped to the budget. Absent for other tools.
374
+ */
375
+ resources?: HeavyToolResourcePlan;
368
376
  }
369
377
  | { kind: 'url'; url: string }
370
378
  | { kind: 'note'; note: string }