@skrr-ai/cli 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/commands/balance/overage.js +4 -3
  2. package/dist/commands/balance/plan.js +10 -6
  3. package/dist/commands/balance/usage/events.js +3 -2
  4. package/dist/commands/balance/usage.js +2 -1
  5. package/dist/commands/browser/install.js +3 -2
  6. package/dist/commands/browser/uninstall.js +3 -2
  7. package/dist/commands/code/install.js +6 -6
  8. package/dist/commands/followups/remind.js +4 -0
  9. package/dist/commands/harnesses/install.d.ts +29 -0
  10. package/dist/commands/harnesses/install.js +153 -25
  11. package/dist/commands/harnesses/installers.d.ts +10 -0
  12. package/dist/commands/harnesses/installers.js +31 -0
  13. package/dist/commands/harnesses/list.js +3 -2
  14. package/dist/commands/skills/import-as-actions.js +6 -15
  15. package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
  16. package/dist/commands/tasks/attachments/set-role.js +60 -0
  17. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  18. package/dist/commands/tasks/attachments/upload.js +18 -52
  19. package/dist/commands/tasks/comments/add.d.ts +1 -0
  20. package/dist/commands/tasks/comments/add.js +67 -2
  21. package/dist/commands/tasks/complete.d.ts +1 -0
  22. package/dist/commands/tasks/complete.js +69 -1
  23. package/dist/commands/tasks/result/submit.d.ts +3 -0
  24. package/dist/commands/tasks/result/submit.js +78 -5
  25. package/dist/commands/tasks/update.js +4 -2
  26. package/dist/commands/tasks/updates/add.d.ts +1 -0
  27. package/dist/commands/tasks/updates/add.js +63 -0
  28. package/dist/lib/api-fetch.js +7 -2
  29. package/dist/lib/cli-installers.d.ts +63 -4
  30. package/dist/lib/cli-installers.js +98 -8
  31. package/dist/lib/dedicated-machines.js +4 -2
  32. package/dist/lib/file-mime.js +1 -1
  33. package/dist/lib/first-party-harness-agent.js +6 -5
  34. package/dist/lib/first-party-harness-doctor.js +15 -32
  35. package/dist/lib/first-party-harness-managed.d.ts +9 -4
  36. package/dist/lib/first-party-harness-managed.js +13 -10
  37. package/dist/lib/first-party-harness.d.ts +25 -21
  38. package/dist/lib/first-party-harness.js +40 -27
  39. package/dist/lib/followups.d.ts +2 -0
  40. package/dist/lib/harness-provider-input.d.ts +18 -12
  41. package/dist/lib/harness-provider-input.js +18 -12
  42. package/dist/lib/harness-tiers.d.ts +4 -3
  43. package/dist/lib/harness-tiers.js +4 -3
  44. package/dist/lib/html-text.js +25 -0
  45. package/dist/lib/local-skills.d.ts +27 -0
  46. package/dist/lib/local-skills.js +38 -0
  47. package/dist/lib/session-task-endpoints.d.ts +1 -1
  48. package/dist/lib/session-task-endpoints.js +3 -0
  49. package/dist/lib/task-asset-upload.d.ts +84 -0
  50. package/dist/lib/task-asset-upload.js +374 -0
  51. package/dist/lib/task-closure.d.ts +28 -0
  52. package/dist/lib/task-closure.js +37 -0
  53. package/dist/lib/tasks.d.ts +8 -0
  54. package/dist/lib/tasks.js +16 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +113 -39
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +148 -71
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +113 -39
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +146 -70
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
  75. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  76. package/dist/node_modules/@skrr-ai/data-provider/index.js +4025 -3938
  77. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  78. package/oclif.manifest.json +12853 -12721
  79. package/package.json +2 -2
@@ -93,9 +93,14 @@ async function apiFetch(pathOrUrl, opts = {}) {
93
93
  ? assertTrustedAutonomousUrl(pathOrUrl, baseURL)
94
94
  : pathOrUrl
95
95
  : `${baseURL.replace(/\/+$/, '')}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`;
96
+ // A FormData body passes through untouched — fetch sets the multipart
97
+ // boundary itself, and stringifying would destroy it. Upload commands
98
+ // (tasks attachments) need this on the session-scoped path, which is not
99
+ // in dataService.
100
+ const isFormData = typeof FormData !== 'undefined' && opts.body instanceof FormData;
96
101
  let body;
97
102
  if (opts.body !== undefined && method !== 'GET') {
98
- body = JSON.stringify(opts.body);
103
+ body = isFormData ? opts.body : JSON.stringify(opts.body);
99
104
  }
100
105
  // Same rule as every `dataService` write (`@skrr-ai/data-provider`
101
106
  // idempotency), chosen once so the 401 retries below resend the same key.
@@ -145,7 +150,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
145
150
  Accept: 'application/json',
146
151
  'X-Oversky-Origin': (0, refresh_1.getCliOrigin)(),
147
152
  };
148
- if (opts.body !== undefined && method !== 'GET') {
153
+ if (opts.body !== undefined && method !== 'GET' && !isFormData) {
149
154
  headers['Content-Type'] = 'application/json';
150
155
  }
151
156
  if (credential.token) {
@@ -25,12 +25,16 @@ export interface ManagedCliProviderState {
25
25
  source?: string;
26
26
  installable?: boolean;
27
27
  installDisabledReason?: string | null;
28
+ /**
29
+ * Whether the daemon can update this installed CLI in place, and why not.
30
+ * ABSENT from a daemon older than managed updates (OSK-10096) — read that as
31
+ * "this daemon cannot say", never as `false`.
32
+ */
33
+ updatable?: boolean;
34
+ updateDisabledReason?: string | null;
28
35
  platformSupported?: boolean;
29
36
  authentication?: ManagedCliAuthentication | null;
30
- job?: {
31
- id?: string;
32
- status?: string;
33
- } | null;
37
+ job?: ManagedCliInstallJob | null;
34
38
  }
35
39
  export interface CliInstallersResponse {
36
40
  daemonId?: string;
@@ -143,18 +147,34 @@ export declare function readinessForRows(rows: readonly ReadinessRow[], credenti
143
147
  }>;
144
148
  export interface ManagedCliInstallJob {
145
149
  id: string;
150
+ provider?: string;
151
+ displayName?: string;
146
152
  status: 'queued' | 'running' | 'succeeded' | 'failed';
153
+ /** `update` for an in-place update; absent from an older daemon, meaning install. */
154
+ kind?: 'install' | 'update';
155
+ startedAt?: string;
156
+ previousVersion?: string | null;
157
+ installedVersion?: string | null;
147
158
  error?: string;
148
159
  /**
149
160
  * The daemon's job log: the LAST lines only (a sliding window, 200 lines in
150
161
  * `daemon/src/cli-installers.ts`). This is the field the daemon sends.
151
162
  */
152
163
  logTail?: string[];
164
+ /**
165
+ * Lines the job has EVER logged (daemons with OSK-10179). With it, the lines
166
+ * new since the last poll are exactly the last `count − printed` of the tail,
167
+ * even when they repeat.
168
+ */
169
+ logLineCount?: number;
153
170
  /** Never sent by any daemon; kept so an older caller's shape still types. */
154
171
  log?: string[];
155
172
  }
156
173
  export interface StartInstallResponse {
157
174
  started?: boolean;
175
+ /** The daemon declined because the CLI is already there. */
176
+ alreadyInstalled?: boolean;
177
+ alreadyRunning?: boolean;
158
178
  provider?: ManagedCliProviderState;
159
179
  job?: {
160
180
  id?: string;
@@ -162,6 +182,35 @@ export interface StartInstallResponse {
162
182
  };
163
183
  }
164
184
  export declare function startManagedCliInstall(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
185
+ /**
186
+ * How long after a start request times out a job for the same provider is taken
187
+ * to be the one that request began.
188
+ *
189
+ * The relay holds the request while the daemon answers, and a gateway can cut
190
+ * that off at its own deadline: an update returned HTTP 504 after 21.9s while
191
+ * the job it started ran to success, and the caller was told the outcome was
192
+ * unknown (OSK-10096 verifier). A job that started within this window of the
193
+ * request is that request's job.
194
+ */
195
+ export declare const START_TIMEOUT_JOB_WINDOW_MS = 120000;
196
+ /** Ask the daemon to update an installed CLI in place (OSK-10096). */
197
+ export declare function startManagedCliUpdate(daemonId: string, provider: string, credential?: unknown): Promise<StartInstallResponse>;
198
+ /**
199
+ * How an update job ended, from the versions the daemon recorded on it:
200
+ * "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
201
+ * its updater found nothing newer." Null when the job does not carry both.
202
+ */
203
+ /**
204
+ * The job a timed-out start request began, or null when none can be claimed.
205
+ *
206
+ * Read from the daemon's own inventory rather than assumed: it is the only
207
+ * party that knows whether the request reached it.
208
+ */
209
+ export declare function jobStartedDespiteTimeout(daemonId: string, provider: string, credential?: unknown, opts?: {
210
+ now?: number;
211
+ windowMs?: number;
212
+ }): Promise<ManagedCliInstallJob | null>;
213
+ export declare function updateOutcomeLine(job: ManagedCliInstallJob): string | null;
165
214
  export declare function readManagedCliInstallJob(daemonId: string, jobId: string, credential?: unknown): Promise<{
166
215
  job?: ManagedCliInstallJob;
167
216
  }>;
@@ -174,6 +223,16 @@ export declare function readManagedCliInstallJob(daemonId: string, jobId: string
174
223
  * the end of what was already printed, and print only what follows it.
175
224
  */
176
225
  export declare function unseenLogLines(printed: readonly string[], tail: readonly string[]): string[];
226
+ /**
227
+ * The new lines of a job's tail when the daemon reports its running line count:
228
+ * exactly the last `total − printedCount`, and how many of those already fell
229
+ * out of the window (the daemon keeps the last 200). Unlike overlap matching,
230
+ * this cannot mistake a repeated line for one already printed.
231
+ */
232
+ export declare function newLogLinesByCount(printedCount: number, tail: readonly string[], total: number): {
233
+ lines: string[];
234
+ skipped: number;
235
+ };
177
236
  /** A 100 MB download over a hotel connection is the case this has to survive. */
178
237
  export declare const INSTALL_MAX_WAIT_MS: number;
179
238
  export declare const INSTALL_POLL_INTERVAL_MS = 2000;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INSTALL_POLL_INTERVAL_MS = exports.INSTALL_MAX_WAIT_MS = exports.ROW_READINESS_TIMEOUT_MS = exports.MANAGED_CLI_PROVIDER_SINCE = void 0;
3
+ exports.INSTALL_POLL_INTERVAL_MS = exports.INSTALL_MAX_WAIT_MS = exports.START_TIMEOUT_JOB_WINDOW_MS = exports.ROW_READINESS_TIMEOUT_MS = exports.MANAGED_CLI_PROVIDER_SINCE = void 0;
4
4
  exports.describeProvidersNotOffered = describeProvidersNotOffered;
5
5
  exports.providersNotOffered = providersNotOffered;
6
6
  exports.listCliInstallers = listCliInstallers;
@@ -9,8 +9,12 @@ exports.readinessByProvider = readinessByProvider;
9
9
  exports.readinessKey = readinessKey;
10
10
  exports.readinessForRows = readinessForRows;
11
11
  exports.startManagedCliInstall = startManagedCliInstall;
12
+ exports.startManagedCliUpdate = startManagedCliUpdate;
13
+ exports.jobStartedDespiteTimeout = jobStartedDespiteTimeout;
14
+ exports.updateOutcomeLine = updateOutcomeLine;
12
15
  exports.readManagedCliInstallJob = readManagedCliInstallJob;
13
16
  exports.unseenLogLines = unseenLogLines;
17
+ exports.newLogLinesByCount = newLogLinesByCount;
14
18
  exports.followManagedCliInstallJob = followManagedCliInstallJob;
15
19
  /**
16
20
  * The daemon's managed-CLI installer inventory, as the CLI reads it.
@@ -85,12 +89,14 @@ function describeProvidersNotOffered(missing) {
85
89
  .join(', ');
86
90
  }
87
91
  function providersNotOffered(offered, known) {
88
- // Canonicalize BOTH sides. An older daemon answers under the spelling it was
89
- // built with — the 0.8.35 build reports the first-party harness under its
90
- // older spelling, so a raw comparison against the current one reported it missing on
91
- // a machine where it is installed and working. Caught by running this against
92
- // the very daemon the finding was about: the first version of this helper
93
- // said "Not offered: devin, <first-party harness>" when only `devin` was true.
92
+ // Canonicalize BOTH sides. A daemon answers under the spelling it was built
93
+ // with — during the rename window the 0.8.35 build reported the first-party
94
+ // harness under its older spelling, so a raw comparison against the current
95
+ // one reported it missing on a machine where it was installed and working (the
96
+ // first version of this helper said "Not offered: devin, <first-party
97
+ // harness>" when only `devin` was true). This is INGRESS, so it canonicalizes
98
+ // accepted spellings only: since contract a retired spelling is not the
99
+ // harness, and a daemon still answering under one predates the wire release.
94
100
  const seen = new Set((offered ?? []).map((provider) => canonical(provider.provider)).filter(Boolean));
95
101
  return known.filter((provider) => !seen.has(canonical(provider)));
96
102
  }
@@ -211,6 +217,62 @@ async function readinessForRows(rows, credential, opts = {}) {
211
217
  async function startManagedCliInstall(daemonId, provider, credential) {
212
218
  return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/install`, { method: 'POST', body: {}, credential: credential });
213
219
  }
220
+ /**
221
+ * How long after a start request times out a job for the same provider is taken
222
+ * to be the one that request began.
223
+ *
224
+ * The relay holds the request while the daemon answers, and a gateway can cut
225
+ * that off at its own deadline: an update returned HTTP 504 after 21.9s while
226
+ * the job it started ran to success, and the caller was told the outcome was
227
+ * unknown (OSK-10096 verifier). A job that started within this window of the
228
+ * request is that request's job.
229
+ */
230
+ exports.START_TIMEOUT_JOB_WINDOW_MS = 120_000;
231
+ /** Ask the daemon to update an installed CLI in place (OSK-10096). */
232
+ async function startManagedCliUpdate(daemonId, provider, credential) {
233
+ return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/${encodeURIComponent(provider)}/update`, { method: 'POST', body: {}, credential: credential });
234
+ }
235
+ /**
236
+ * How an update job ended, from the versions the daemon recorded on it:
237
+ * "Updated Claude Code 2.1.270 → 2.1.280." or "Claude Code is still 2.1.270:
238
+ * its updater found nothing newer." Null when the job does not carry both.
239
+ */
240
+ /**
241
+ * The job a timed-out start request began, or null when none can be claimed.
242
+ *
243
+ * Read from the daemon's own inventory rather than assumed: it is the only
244
+ * party that knows whether the request reached it.
245
+ */
246
+ async function jobStartedDespiteTimeout(daemonId, provider, credential, opts = {}) {
247
+ let response;
248
+ try {
249
+ response = await listCliInstallers(daemonId, credential);
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ const row = (response.providers ?? []).find((entry) => canonical(entry.provider) === canonical(provider));
255
+ const job = row?.job;
256
+ if (!job?.id)
257
+ return null;
258
+ if (job.status === 'queued' || job.status === 'running')
259
+ return job;
260
+ const startedAt = job.startedAt ? Date.parse(job.startedAt) : NaN;
261
+ if (!Number.isFinite(startedAt))
262
+ return null;
263
+ const now = opts.now ?? Date.now();
264
+ return now - startedAt <= (opts.windowMs ?? exports.START_TIMEOUT_JOB_WINDOW_MS) ? job : null;
265
+ }
266
+ function updateOutcomeLine(job) {
267
+ const before = job.previousVersion;
268
+ const after = job.installedVersion;
269
+ if (!before || !after)
270
+ return null;
271
+ const name = job.displayName || job.provider || 'The CLI';
272
+ return before === after
273
+ ? `${name} is still ${after}: its updater found nothing newer.`
274
+ : `Updated ${name} ${before} → ${after}.`;
275
+ }
214
276
  async function readManagedCliInstallJob(daemonId, jobId, credential) {
215
277
  return (0, api_fetch_1.apiFetch)(`/api/daemons/${encodeURIComponent(daemonId)}/cli-installers/jobs/${encodeURIComponent(jobId)}`, { credential: credential });
216
278
  }
@@ -237,6 +299,20 @@ function unseenLogLines(printed, tail) {
237
299
  }
238
300
  return [...tail];
239
301
  }
302
+ /**
303
+ * The new lines of a job's tail when the daemon reports its running line count:
304
+ * exactly the last `total − printedCount`, and how many of those already fell
305
+ * out of the window (the daemon keeps the last 200). Unlike overlap matching,
306
+ * this cannot mistake a repeated line for one already printed.
307
+ */
308
+ function newLogLinesByCount(printedCount, tail, total) {
309
+ const unseen = Math.max(0, total - printedCount);
310
+ if (unseen === 0)
311
+ return { lines: [], skipped: 0 };
312
+ if (unseen > tail.length)
313
+ return { lines: [...tail], skipped: unseen - tail.length };
314
+ return { lines: tail.slice(tail.length - unseen), skipped: 0 };
315
+ }
240
316
  /** A 100 MB download over a hotel connection is the case this has to survive. */
241
317
  exports.INSTALL_MAX_WAIT_MS = 10 * 60_000;
242
318
  exports.INSTALL_POLL_INTERVAL_MS = 2_000;
@@ -256,6 +332,7 @@ exports.INSTALL_POLL_INTERVAL_MS = 2_000;
256
332
  async function followManagedCliInstallJob(input) {
257
333
  const deadline = Date.now() + (input.maxWaitMs ?? exports.INSTALL_MAX_WAIT_MS);
258
334
  let printed = [];
335
+ let printedCount = 0;
259
336
  for (;;) {
260
337
  const res = await readManagedCliInstallJob(input.daemonId, input.jobId, input.credential);
261
338
  const job = res.job;
@@ -264,7 +341,20 @@ async function followManagedCliInstallJob(input) {
264
341
  if (input.onLogLine) {
265
342
  // `logTail`, the field the daemon sends — this read `log`, which no daemon
266
343
  // has ever sent, so every install printed nothing until it ended (OSK-10103).
267
- const fresh = unseenLogLines(printed, job.logTail ?? job.log ?? []);
344
+ const tail = job.logTail ?? job.log ?? [];
345
+ let fresh;
346
+ if (typeof job.logLineCount === 'number') {
347
+ // Exact: the daemon says how many lines exist in total (OSK-10179).
348
+ const { lines, skipped } = newLogLinesByCount(printedCount, tail, job.logLineCount);
349
+ if (skipped > 0) {
350
+ input.onLogLine(`(… ${skipped} log line${skipped === 1 ? '' : 's'} not shown)`);
351
+ }
352
+ fresh = lines;
353
+ printedCount = Math.max(printedCount, job.logLineCount);
354
+ }
355
+ else {
356
+ fresh = unseenLogLines(printed, tail);
357
+ }
268
358
  for (const line of fresh)
269
359
  input.onLogLine(line);
270
360
  printed = [...printed, ...fresh].slice(-MAX_TRACKED_LOG_LINES);
@@ -845,8 +845,10 @@ function selectDedicatedAttachHarness(lease, harnesses, engine) {
845
845
  }
846
846
  const providers = [...new Set(rows.map((row) => row.provider))];
847
847
  if (engine) {
848
- // Any accepted spelling of an engine names it: a user who typed the first-party
849
- // harness's older name means the row stored under its canonical one.
848
+ // Any ACCEPTED spelling of an engine names it — during a rename's alias window,
849
+ // a user who typed the first-party harness's older name means the row stored
850
+ // under its canonical one. A retired spelling is accepted nowhere, so it names
851
+ // no engine here either (`isSameHarnessProvider`).
850
852
  const match = rows.find((row) => (0, auth_core_1.isSameHarnessProvider)(row.provider, engine));
851
853
  if (!match) {
852
854
  return {
@@ -68,7 +68,7 @@ const MIME_TYPES = {
68
68
  '.md': 'text/markdown',
69
69
  '.mjs': 'text/javascript',
70
70
  '.mkv': 'video/mkv',
71
- '.mov': 'video/mov',
71
+ '.mov': 'video/quicktime',
72
72
  '.mp3': 'audio/mpeg',
73
73
  '.mp4': 'video/mp4',
74
74
  '.ogv': 'video/ogv',
@@ -220,7 +220,7 @@ async function resolveFirstPartyHarnessAgent(explicit, deps = {}, options = {})
220
220
  const created = options.createDefault === false
221
221
  ? null
222
222
  : await (deps.ensureDefaultAgent ?? ensureDefaultAgent)(
223
- // Canonical: what the server stores. It accepts every spelling.
223
+ // Canonical: what the server stores, and the one spelling every server accepts.
224
224
  auth_core_1.FIRST_PARTY_HARNESS.provider, options.signal);
225
225
  if (created?.id) {
226
226
  return {
@@ -330,11 +330,12 @@ function readRawConfigKey() {
330
330
  * first real session read. Returns whether the file now holds it under the
331
331
  * neutral key.
332
332
  *
333
- * Reading alone used to leave it where it was, which is safe only while that key
334
- * is still an accepted spelling. The contract phase empties the identity's
335
- * aliases, `legacyAgentConfigKeys()` then stops naming the old key, and a user
333
+ * Reading alone used to leave it where it was. `legacyAgentConfigKeys()` keeps
334
+ * naming an old key after its spelling is retired (it reads the identity's
335
+ * ARTIFACT spellings, since the file predates the rename), but a key that is only
336
+ * ever read is one the next pruning of `formerSpellings` would strand, and a user
336
337
  * who never ran `--set`, `--clear` or logout would silently lose their setting.
337
- * Moving it here retires the old key long before that date.
338
+ * Moving it on the first session read retires the old key long before that.
338
339
  *
339
340
  * Only the session path calls this — `skrr code doctor` reports and never
340
341
  * writes. Best-effort: an unwritable config is left alone and still read.
@@ -42,17 +42,6 @@ const first_party_harness_1 = require("./first-party-harness");
42
42
  const auth_core_1 = require("@skrr-ai/auth-core");
43
43
  const harness_tiers_1 = require("./harness-tiers");
44
44
  const first_party_harness_managed_1 = require("./first-party-harness-managed");
45
- /**
46
- * The variable one purpose was read from, and — when that was an older name — the
47
- * sentence that says so. `undefined` when the neutral name (or nothing) was used.
48
- */
49
- function legacyEnvNote(key, name) {
50
- const neutral = auth_core_1.FIRST_PARTY_HARNESS_ENV[key].name;
51
- if (!name || name === neutral)
52
- return undefined;
53
- return (`${name} is an older name for this setting and is still read; ` +
54
- `rename it to ${neutral}, which wins when both are set.`);
55
- }
56
45
  /**
57
46
  * Project-level state dir — the ENGINE's own project marker
58
47
  * (`FIRST_PARTY_HARNESS_ENGINE.projectDirectory`). Basename deliberately differs
@@ -78,10 +67,12 @@ function projectStateDir(cwd) {
78
67
  * the answer one copy-paste away and keeps the property — and the promise is
79
68
  * reworded to match what is delivered rather than the other way round.
80
69
  *
81
- * The URL is the CANONICAL feed candidate. A manifest may also be published under
82
- * each older spelling (`firstPartyHarnessFeedCandidates`), and an installer that
83
- * finds nothing at the canonical URL tries those next — so the remedy names them
84
- * too. Without that, "the canonical URL 404s" would read as "there is no release"
70
+ * The URL is the CANONICAL feed candidate. While a rename's alias window is open
71
+ * a manifest may also be published under each alias (`firstPartyHarnessFeedCandidates`
72
+ * for a channel read), and an installer that finds nothing at the canonical URL
73
+ * tries those next — so the remedy names them too. A retired spelling is not
74
+ * among them: its channel pointers stopped moving when publishing stopped writing
75
+ * them, and naming one would point at a stale release. Without that, "the canonical URL 404s" would read as "there is no release"
85
76
  * on a feed an older pipeline published under the other spelling. The doctor
86
77
  * cannot say which candidate actually holds a release, because it does not probe;
87
78
  * it says where to look, in the order an installer looks.
@@ -97,15 +88,13 @@ function releaseChannelCheck(env) {
97
88
  manifestFilename: candidate.manifestFilename,
98
89
  channel,
99
90
  }));
100
- const legacyNote = legacyEnvNote('channel', source);
101
91
  if (invalid) {
102
92
  return {
103
93
  name: 'release-channel',
104
94
  status: 'warn',
105
95
  detail: `${source}='${invalid}' is not a channel; following '${channel}' instead — ${canonical}`,
106
96
  remedy: `Set ${auth_core_1.FIRST_PARTY_HARNESS_ENV.channel.name} to one of ${auth_core_1.FIRST_PARTY_HARNESS_CHANNELS.join(', ')}, ` +
107
- 'or unset it to follow the default.' +
108
- (legacyNote ? ` ${legacyNote}` : ''),
97
+ 'or unset it to follow the default.',
109
98
  };
110
99
  }
111
100
  const alsoTried = alternates.filter((url) => url !== canonical);
@@ -114,10 +103,7 @@ function releaseChannelCheck(env) {
114
103
  status: 'ok',
115
104
  detail: `${channel} — ${canonical}`,
116
105
  remedy: 'Fetch that URL to see whether the channel has a published release, and which version.' +
117
- (alsoTried.length
118
- ? ` If it has none, an installer also tries ${alsoTried.join(', ')}.`
119
- : '') +
120
- (legacyNote ? ` ${legacyNote}` : ''),
106
+ (alsoTried.length ? ` If it has none, an installer also tries ${alsoTried.join(', ')}.` : ''),
121
107
  };
122
108
  }
123
109
  /**
@@ -159,7 +145,8 @@ function strandedLegacyEngine(env) {
159
145
  /**
160
146
  * Whether a managed engine resolved under an OLDER spelling of its file name.
161
147
  *
162
- * Not a fault: resolution tries every spelling on purpose, and the next
148
+ * Not a fault: resolution tries every artifact spelling on purpose (a former one
149
+ * included — the file predates the rename), and the next
163
150
  * `skrr code` run renames the file onto the canonical name (leaving a link at the
164
151
  * old one). Said anyway, because a path that does not match the name everything
165
152
  * else prints is exactly the surprise this module exists to explain.
@@ -196,14 +183,12 @@ function engineCheck(env) {
196
183
  };
197
184
  }
198
185
  if (bin && source === 'env') {
199
- // Name the variable, not just "env": two names are read for this override,
200
- // and the one to edit is the one that is set.
201
- const legacyNote = resolution.legacyEnv ? legacyEnvNote('path', resolution.envName) : undefined;
186
+ // Name the variable, not just "env": an override is the one resolution the
187
+ // user set by hand, and the variable is what they would edit.
202
188
  return {
203
189
  name: 'engine',
204
190
  status: 'ok',
205
191
  detail: `${bin} (resolved via env ${resolution.envName})`,
206
- ...(legacyNote ? { remedy: legacyNote } : {}),
207
192
  };
208
193
  }
209
194
  if (bin) {
@@ -232,7 +217,6 @@ function engineCheck(env) {
232
217
  };
233
218
  }
234
219
  const override = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'path');
235
- const legacyNote = legacyEnvNote('path', override.name);
236
220
  if (override.value && !node_path_1.default.isAbsolute(override.value)) {
237
221
  return {
238
222
  name: 'engine',
@@ -241,7 +225,7 @@ function engineCheck(env) {
241
225
  // agent-invoked command the cwd is attacker-influenced, so a repo
242
226
  // containing a file named like the engine could otherwise hijack it.
243
227
  detail: `${override.name} is set to a relative path ('${override.value}') and was ignored`,
244
- remedy: `Set ${pathVar} to an absolute path.${legacyNote ? ` ${legacyNote}` : ''}`,
228
+ remedy: `Set ${pathVar} to an absolute path.`,
245
229
  };
246
230
  }
247
231
  if (override.value) {
@@ -249,8 +233,7 @@ function engineCheck(env) {
249
233
  name: 'engine',
250
234
  status: 'fail',
251
235
  detail: `${override.name}='${override.value}' is not an executable file`,
252
- remedy: `Point it at a real ${auth_core_1.FIRST_PARTY_HARNESS.displayName} engine build, or unset it to fall back to PATH.` +
253
- (legacyNote ? ` ${legacyNote}` : ''),
236
+ remedy: `Point it at a real ${auth_core_1.FIRST_PARTY_HARNESS.displayName} engine build, or unset it to fall back to PATH.`,
254
237
  };
255
238
  }
256
239
  return {
@@ -451,7 +434,7 @@ function opencodeCheck(cwd) {
451
434
  * `daemon/src/harness-trust.ts` again.
452
435
  */
453
436
  function trustCheck() {
454
- // The canonical slug. The shared table carries every spelling at the same tier,
437
+ // The canonical slug. The shared table carries every accepted spelling at the same tier,
455
438
  // so which one is asked does not change the answer — but the report prints the
456
439
  // name the platform stores.
457
440
  const harness = auth_core_1.FIRST_PARTY_HARNESS.provider;
@@ -70,10 +70,15 @@ export declare const AGENT_FLAG_SPELLINGS: readonly ["--skrr-agent", "--oversky-
70
70
  export declare const AGENT_CONFIG_KEY: "firstPartyHarnessAgentId";
71
71
  /**
72
72
  * Keys earlier CLIs stored the agent setting under: `<camelCased spelling>AgentId`
73
- * for every spelling of the harness, canonical first.
74
- *
75
- * Derived rather than listed so no product name is written here — and so the key
76
- * is read for exactly as long as its spelling is accepted anywhere else.
73
+ * for every ARTIFACT spelling of the harness, canonical first.
74
+ *
75
+ * A config file on a user's disk is an artifact: a CLI that last ran before a
76
+ * rename wrote the key under the product name it had then, and that file does not
77
+ * change when the spelling stops being accepted anywhere. So the key is derived
78
+ * from `firstPartyHarnessArtifactSpellings` — former spellings included — and is
79
+ * read (and moved onto the neutral key) for as long as the identity remembers the
80
+ * spelling, not merely for as long as some ingress still accepts it. Derived
81
+ * rather than listed, so no product name is written here.
77
82
  */
78
83
  export declare function legacyAgentConfigKeys(): string[];
79
84
  export interface AgentSetting {
@@ -90,17 +90,22 @@ exports.AGENT_CONFIG_KEY = 'firstPartyHarnessAgentId';
90
90
  function camelCaseSlug(slug) {
91
91
  return slug.replace(/-([a-z0-9])/g, (_, next) => next.toUpperCase());
92
92
  }
93
- // TODO(identity): move to auth-core — the identity module records legacy ENV
94
- // names; it has no notion of a legacy CONFIG key yet.
93
+ // TODO(identity): move to auth-core — the identity module derives on-disk binary
94
+ // names from its artifact spellings; it has no notion of a legacy CONFIG key yet.
95
95
  /**
96
96
  * Keys earlier CLIs stored the agent setting under: `<camelCased spelling>AgentId`
97
- * for every spelling of the harness, canonical first.
97
+ * for every ARTIFACT spelling of the harness, canonical first.
98
98
  *
99
- * Derived rather than listed so no product name is written here — and so the key
100
- * is read for exactly as long as its spelling is accepted anywhere else.
99
+ * A config file on a user's disk is an artifact: a CLI that last ran before a
100
+ * rename wrote the key under the product name it had then, and that file does not
101
+ * change when the spelling stops being accepted anywhere. So the key is derived
102
+ * from `firstPartyHarnessArtifactSpellings` — former spellings included — and is
103
+ * read (and moved onto the neutral key) for as long as the identity remembers the
104
+ * spelling, not merely for as long as some ingress still accepts it. Derived
105
+ * rather than listed, so no product name is written here.
101
106
  */
102
107
  function legacyAgentConfigKeys() {
103
- return (0, auth_core_1.firstPartyHarnessSpellings)().map((spelling) => `${camelCaseSlug(spelling)}AgentId`);
108
+ return (0, auth_core_1.firstPartyHarnessArtifactSpellings)().map((spelling) => `${camelCaseSlug(spelling)}AgentId`);
104
109
  }
105
110
  /** Read the setting from a config record: the neutral key first, then each older key. */
106
111
  function readAgentSetting(config) {
@@ -250,8 +255,7 @@ function managedSkipReason(env = process.env) {
250
255
  announce: false,
251
256
  };
252
257
  }
253
- // The operator's opt-OUT (`FIRST_PARTY_HARNESS_ENV.managed`, neutral name first,
254
- // older name second). Present because "run with my own key" has to be
258
+ // The operator's opt-OUT (`FIRST_PARTY_HARNESS_ENV.managed`). Present because "run with my own key" has to be
255
259
  // expressible without logging out, and because a test that asserts argv
256
260
  // forwarding must be able to say "this run is not about acquisition" rather
257
261
  // than depend on whether the machine happens to be signed in.
@@ -259,8 +263,7 @@ function managedSkipReason(env = process.env) {
259
263
  if (envFlagIsOff(managedFlag.value)) {
260
264
  return {
261
265
  kind: 'disabled',
262
- // The variable that was actually read — two names are honoured, and the one
263
- // to change is the one that is set.
266
+ // The variable that was actually read, so the sentence names what to change.
264
267
  detail: `${managedFlag.name} is off`,
265
268
  announce: false,
266
269
  };
@@ -18,8 +18,10 @@
18
18
  * Every product-named value — the binary's file name, the engine home, the env
19
19
  * var names, the display name — is read from the identity module in
20
20
  * `@skrr-ai/auth-core` (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md`).
21
- * The engine's file name has more than one spelling while a rename is in
22
- * flight, so resolution tries every spelling, canonical first, in each location.
21
+ * An installed engine is an ARTIFACT: one installed before a rename is still on
22
+ * disk under its old file name after that name stops being accepted anywhere, so
23
+ * resolution tries every artifact spelling (`firstPartyHarnessArtifactSpellings`),
24
+ * canonical first, in each location. That recognises a file; it admits no input.
23
25
  */
24
26
  /**
25
27
  * The error `execEngine` throws when no engine resolves. A value the commands
@@ -44,11 +46,12 @@ export declare function engineHome(env?: NodeJS.ProcessEnv): string;
44
46
  /**
45
47
  * The user-level instruction root the engine owns.
46
48
  *
47
- * Read under the neutral variable first and its older name second
48
- * (`readFirstPartyHarnessEnv`). The older name is also the one the ENGINE reads,
49
- * which is why {@link engineOwnedEnv} hands the resolved value to the engine under
50
- * that name — otherwise the doctor would honour the neutral variable and the
51
- * engine would not.
49
+ * Read under the platform's neutral variable first, then under the variable the
50
+ * ENGINE itself reads (`FIRST_PARTY_HARNESS_ENGINE.env.home`). The second is not a
51
+ * legacy fallback: it is the engine's own name, the engine honours it whatever the
52
+ * platform does, and a CLI that ignored it would report one instruction root while
53
+ * the engine it launches used another. {@link engineOwnedEnv} closes the other
54
+ * direction, handing a neutral-variable value to the engine under its own name.
52
55
  */
53
56
  export declare function engineInstructionHome(env?: NodeJS.ProcessEnv): string;
54
57
  /**
@@ -58,8 +61,9 @@ export declare function engineInstructionHome(env?: NodeJS.ProcessEnv): string;
58
61
  */
59
62
  export declare function managedEnginePath(env?: NodeJS.ProcessEnv, provider?: string): string;
60
63
  /**
61
- * The same managed location under the PREVIOUS config root, for every spelling of
62
- * the binary and every engine home name, canonical first.
64
+ * The same managed location under the PREVIOUS config root, for every artifact
65
+ * spelling of the binary (former spellings included) and every engine home name,
66
+ * canonical first.
63
67
  *
64
68
  * The root moved `~/.oversky` → `~/.skrr` in the skrr rename, and an engine
65
69
  * installed before that is still on disk under the old one. `daemon/src/
@@ -69,7 +73,7 @@ export declare function managedEnginePath(env?: NodeJS.ProcessEnv, provider?: st
69
73
  *
70
74
  * The engine-home move (`migrateFirstPartyHarnessHome`) only ever works under the
71
75
  * CURRENT root, so nothing renames a binary here — which is why this list carries
72
- * every spelling rather than only the canonical one.
76
+ * every artifact spelling rather than only the canonical one.
73
77
  *
74
78
  * Empty when an explicit `OVERSKY_CONFIG_DIR` is set, matching the daemon: that
75
79
  * override names one root deliberately, and reaching past it to a hard-coded home
@@ -84,11 +88,9 @@ export interface EngineResolution {
84
88
  /**
85
89
  * The variable an explicit override was read from — set whenever one was set,
86
90
  * including an override that was refused. The doctor names it, because an
87
- * operator fixing a bad override needs to know WHICH of its names they set.
91
+ * operator fixing a bad override needs to be told which variable to change.
88
92
  */
89
93
  envName?: string;
90
- /** True when {@link envName} is an older name for the override. */
91
- legacyEnv?: boolean;
92
94
  }
93
95
  /**
94
96
  * Resolve the engine binary, reporting WHERE it came from.
@@ -100,12 +102,12 @@ export interface EngineResolution {
100
102
  * The env override is checked first and is absolute-only. A relative override
101
103
  * would resolve against the caller's cwd, which for an agent-invoked command is
102
104
  * attacker-influenced: a repo containing a file named like the engine could
103
- * hijack it. It is read under its neutral name first and its older name second.
105
+ * hijack it.
104
106
  *
105
107
  * **Order: env → managed → PATH → well-known (WL-6.2).** Within each location
106
- * every spelling of the binary is tried, canonical first — the daemon's order —
107
- * so an engine installed under an older name still resolves, and the canonical
108
- * one wins wherever both exist.
108
+ * every artifact spelling of the binary is tried, canonical first — the daemon's
109
+ * order — so an engine installed under an older or former name still resolves,
110
+ * and the canonical one wins wherever both exist.
109
111
  *
110
112
  * The managed location beating `PATH` is the load-bearing part, and it is a
111
113
  * deliberate reversal of the obvious ordering.
@@ -376,10 +378,12 @@ export declare function engineSpawnEnv(env: NodeJS.ProcessEnv, extraEnv: Record<
376
378
  * `skrr code doctor` honour it while the engine it launches ignored it — two
377
379
  * answers to "where do my instructions live" from one command.
378
380
  *
379
- * Only when the value came from a name the engine does NOT read: a value set under
380
- * the engine's own name is inherited already, and rewriting it would only replace
381
- * the user's spelling of a path with ours. The resolved path is passed rather than
382
- * the raw value, so the engine and the doctor agree on `~` expansion too.
381
+ * Only when the neutral variable supplied the value: a value set only under the
382
+ * engine's own name is inherited already, and rewriting it would only replace the
383
+ * user's spelling of a path with ours. When both are set the neutral one wins, as
384
+ * it does in {@link engineInstructionHome}, so the engine is handed that one. The
385
+ * resolved path is passed rather than the raw value, so the engine and the doctor
386
+ * agree on `~` expansion too.
383
387
  *
384
388
  * Not a credential and never one: it cannot reintroduce anything the sanitizer
385
389
  * strips, and `extraEnv` still layers over it.