@skrr-ai/cli 0.1.42 → 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 (74) 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 +3 -2
  8. package/dist/commands/harnesses/install.d.ts +11 -0
  9. package/dist/commands/harnesses/install.js +33 -14
  10. package/dist/commands/harnesses/installers.d.ts +10 -0
  11. package/dist/commands/harnesses/installers.js +31 -0
  12. package/dist/commands/harnesses/list.js +3 -2
  13. package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
  14. package/dist/commands/tasks/attachments/set-role.js +60 -0
  15. package/dist/commands/tasks/attachments/upload.d.ts +1 -0
  16. package/dist/commands/tasks/attachments/upload.js +18 -52
  17. package/dist/commands/tasks/comments/add.d.ts +1 -0
  18. package/dist/commands/tasks/comments/add.js +67 -2
  19. package/dist/commands/tasks/complete.d.ts +1 -0
  20. package/dist/commands/tasks/complete.js +56 -1
  21. package/dist/commands/tasks/result/submit.d.ts +3 -0
  22. package/dist/commands/tasks/result/submit.js +76 -8
  23. package/dist/commands/tasks/update.js +4 -2
  24. package/dist/commands/tasks/updates/add.d.ts +1 -0
  25. package/dist/commands/tasks/updates/add.js +63 -0
  26. package/dist/lib/api-fetch.js +7 -2
  27. package/dist/lib/cli-installers.d.ts +23 -4
  28. package/dist/lib/cli-installers.js +47 -7
  29. package/dist/lib/dedicated-machines.js +4 -2
  30. package/dist/lib/file-mime.js +1 -1
  31. package/dist/lib/first-party-harness-agent.js +6 -5
  32. package/dist/lib/first-party-harness-doctor.js +15 -32
  33. package/dist/lib/first-party-harness-managed.d.ts +9 -4
  34. package/dist/lib/first-party-harness-managed.js +13 -10
  35. package/dist/lib/first-party-harness.d.ts +25 -21
  36. package/dist/lib/first-party-harness.js +40 -27
  37. package/dist/lib/harness-provider-input.d.ts +18 -12
  38. package/dist/lib/harness-provider-input.js +18 -12
  39. package/dist/lib/harness-tiers.d.ts +4 -3
  40. package/dist/lib/harness-tiers.js +4 -3
  41. package/dist/lib/html-text.js +25 -0
  42. package/dist/lib/session-task-endpoints.d.ts +1 -1
  43. package/dist/lib/session-task-endpoints.js +3 -0
  44. package/dist/lib/task-asset-upload.d.ts +84 -0
  45. package/dist/lib/task-asset-upload.js +374 -0
  46. package/dist/lib/task-closure.d.ts +15 -0
  47. package/dist/lib/task-closure.js +20 -0
  48. package/dist/lib/tasks.d.ts +8 -0
  49. package/dist/lib/tasks.js +16 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +113 -39
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +148 -71
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +113 -39
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +146 -70
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
  70. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  71. package/dist/node_modules/@skrr-ai/data-provider/index.js +3855 -3779
  72. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  73. package/oclif.manifest.json +26935 -26810
  74. package/package.json +2 -2
@@ -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.
@@ -19,8 +19,10 @@
19
19
  * Every product-named value — the binary's file name, the engine home, the env
20
20
  * var names, the display name — is read from the identity module in
21
21
  * `@skrr-ai/auth-core` (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md`).
22
- * The engine's file name has more than one spelling while a rename is in
23
- * flight, so resolution tries every spelling, canonical first, in each location.
22
+ * An installed engine is an ARTIFACT: one installed before a rename is still on
23
+ * disk under its old file name after that name stops being accepted anywhere, so
24
+ * resolution tries every artifact spelling (`firstPartyHarnessArtifactSpellings`),
25
+ * canonical first, in each location. That recognises a file; it admits no input.
24
26
  */
25
27
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
28
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -81,14 +83,15 @@ function engineHome(env = process.env) {
81
83
  /**
82
84
  * The user-level instruction root the engine owns.
83
85
  *
84
- * Read under the neutral variable first and its older name second
85
- * (`readFirstPartyHarnessEnv`). The older name is also the one the ENGINE reads,
86
- * which is why {@link engineOwnedEnv} hands the resolved value to the engine under
87
- * that name — otherwise the doctor would honour the neutral variable and the
88
- * engine would not.
86
+ * Read under the platform's neutral variable first, then under the variable the
87
+ * ENGINE itself reads (`FIRST_PARTY_HARNESS_ENGINE.env.home`). The second is not a
88
+ * legacy fallback: it is the engine's own name, the engine honours it whatever the
89
+ * platform does, and a CLI that ignored it would report one instruction root while
90
+ * the engine it launches used another. {@link engineOwnedEnv} closes the other
91
+ * direction, handing a neutral-variable value to the engine under its own name.
89
92
  */
90
93
  function engineInstructionHome(env = process.env) {
91
- const configured = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value;
94
+ const configured = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value ?? engineHomeVariable(env);
92
95
  // Falls through to `engineHome()` rather than re-deriving `~/.skrr` — the two
93
96
  // describe the same root, and re-deriving it is what made them disagree under
94
97
  // `OVERSKY_CONFIG_DIR` (OSK-300).
@@ -100,6 +103,11 @@ function engineInstructionHome(env = process.env) {
100
103
  return node_path_1.default.join((0, node_os_1.homedir)(), configured.slice(2));
101
104
  return node_path_1.default.resolve(configured);
102
105
  }
106
+ /** The engine's own home variable, trimmed; undefined when unset or blank. */
107
+ function engineHomeVariable(env) {
108
+ const raw = env[auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.home];
109
+ return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : undefined;
110
+ }
103
111
  /**
104
112
  * The managed install location for one spelling of the binary (canonical by
105
113
  * default) — where the daemon, the Desktop app, and
@@ -109,8 +117,9 @@ function managedEnginePath(env = process.env, provider = auth_core_1.FIRST_PARTY
109
117
  return (0, auth_core_1.firstPartyHarnessInstalledBinaryPath)(env, provider, process.platform);
110
118
  }
111
119
  /**
112
- * The same managed location under the PREVIOUS config root, for every spelling of
113
- * the binary and every engine home name, canonical first.
120
+ * The same managed location under the PREVIOUS config root, for every artifact
121
+ * spelling of the binary (former spellings included) and every engine home name,
122
+ * canonical first.
114
123
  *
115
124
  * The root moved `~/.oversky` → `~/.skrr` in the skrr rename, and an engine
116
125
  * installed before that is still on disk under the old one. `daemon/src/
@@ -120,7 +129,7 @@ function managedEnginePath(env = process.env, provider = auth_core_1.FIRST_PARTY
120
129
  *
121
130
  * The engine-home move (`migrateFirstPartyHarnessHome`) only ever works under the
122
131
  * CURRENT root, so nothing renames a binary here — which is why this list carries
123
- * every spelling rather than only the canonical one.
132
+ * every artifact spelling rather than only the canonical one.
124
133
  *
125
134
  * Empty when an explicit `OVERSKY_CONFIG_DIR` is set, matching the daemon: that
126
135
  * override names one root deliberately, and reaching past it to a hard-coded home
@@ -149,11 +158,13 @@ function fallbackPaths(exeName) {
149
158
  ];
150
159
  }
151
160
  /**
152
- * The file names a PATH directory may hold the engine under, canonical first.
153
- * Windows also accepts a `.cmd` shim, as a package manager may install one.
161
+ * The file names a PATH directory may hold the engine under, canonical first —
162
+ * every artifact spelling, since a standalone install made before a rename keeps
163
+ * its old name. Windows also accepts a `.cmd` shim, as a package manager may
164
+ * install one.
154
165
  */
155
166
  function pathExecutableNames(platform) {
156
- return (0, auth_core_1.firstPartyHarnessSpellings)().flatMap((spelling) => platform === 'win32'
167
+ return (0, auth_core_1.firstPartyHarnessArtifactSpellings)().flatMap((spelling) => platform === 'win32'
157
168
  ? [(0, auth_core_1.firstPartyHarnessBinaryName)(spelling, platform), `${spelling}.cmd`]
158
169
  : [(0, auth_core_1.firstPartyHarnessBinaryName)(spelling, platform)]);
159
170
  }
@@ -176,12 +187,12 @@ function isExecutable(p) {
176
187
  * The env override is checked first and is absolute-only. A relative override
177
188
  * would resolve against the caller's cwd, which for an agent-invoked command is
178
189
  * attacker-influenced: a repo containing a file named like the engine could
179
- * hijack it. It is read under its neutral name first and its older name second.
190
+ * hijack it.
180
191
  *
181
192
  * **Order: env → managed → PATH → well-known (WL-6.2).** Within each location
182
- * every spelling of the binary is tried, canonical first — the daemon's order —
183
- * so an engine installed under an older name still resolves, and the canonical
184
- * one wins wherever both exist.
193
+ * every artifact spelling of the binary is tried, canonical first — the daemon's
194
+ * order — so an engine installed under an older or former name still resolves,
195
+ * and the canonical one wins wherever both exist.
185
196
  *
186
197
  * The managed location beating `PATH` is the load-bearing part, and it is a
187
198
  * deliberate reversal of the obvious ordering.
@@ -210,7 +221,7 @@ function resolveEngine(env = process.env) {
210
221
  const platform = process.platform;
211
222
  const override = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'path');
212
223
  if (override.value && override.name) {
213
- const origin = { envName: override.name, legacyEnv: override.legacy };
224
+ const origin = { envName: override.name };
214
225
  const candidate = override.value;
215
226
  if (!node_path_1.default.isAbsolute(candidate))
216
227
  return { path: null, source: 'unresolved', ...origin };
@@ -218,7 +229,7 @@ function resolveEngine(env = process.env) {
218
229
  ? { path: candidate, source: 'env', ...origin }
219
230
  : { path: null, source: 'unresolved', ...origin };
220
231
  }
221
- for (const spelling of (0, auth_core_1.firstPartyHarnessSpellings)()) {
232
+ for (const spelling of (0, auth_core_1.firstPartyHarnessArtifactSpellings)()) {
222
233
  const managed = managedEnginePath(env, spelling);
223
234
  if (isExecutable(managed))
224
235
  return { path: managed, source: 'managed' };
@@ -595,18 +606,19 @@ mode) {
595
606
  * `skrr code doctor` honour it while the engine it launches ignored it — two
596
607
  * answers to "where do my instructions live" from one command.
597
608
  *
598
- * Only when the value came from a name the engine does NOT read: a value set under
599
- * the engine's own name is inherited already, and rewriting it would only replace
600
- * the user's spelling of a path with ours. The resolved path is passed rather than
601
- * the raw value, so the engine and the doctor agree on `~` expansion too.
609
+ * Only when the neutral variable supplied the value: a value set only under the
610
+ * engine's own name is inherited already, and rewriting it would only replace the
611
+ * user's spelling of a path with ours. When both are set the neutral one wins, as
612
+ * it does in {@link engineInstructionHome}, so the engine is handed that one. The
613
+ * resolved path is passed rather than the raw value, so the engine and the doctor
614
+ * agree on `~` expansion too.
602
615
  *
603
616
  * Not a credential and never one: it cannot reintroduce anything the sanitizer
604
617
  * strips, and `extraEnv` still layers over it.
605
618
  */
606
619
  function engineOwnedEnv(env) {
607
620
  const out = {};
608
- const home = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home');
609
- if (home.value && home.name !== auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.home) {
621
+ if ((0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value) {
610
622
  out[auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.home] = engineInstructionHome(env);
611
623
  }
612
624
  return out;
@@ -615,7 +627,8 @@ async function execEngine(args, env = process.env, options = {}) {
615
627
  // Move an engine installed under an older binary name onto the canonical one,
616
628
  // BEFORE resolving. Idempotent, lossless, and it never throws: a move that
617
629
  // cannot be made leaves every file where it was, and `resolveEngine` still tries
618
- // every spelling — so the worst case is today's path, never a missing engine.
630
+ // every artifact spelling — so the worst case is today's path, never a missing
631
+ // engine.
619
632
  // Here and not in `resolveEngine`, which the doctor also calls and which must
620
633
  // not move anything.
621
634
  (0, auth_core_1.migrateFirstPartyHarnessHome)({ env });
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * Harness-valued INPUT, normalised at the edge.
3
3
  *
4
- * The first-party harness has more than one accepted spelling while a rename is
5
- * in flight (`FIRST_PARTY_HARNESS.aliases`). Every flag, setting and filter that
6
- * takes a harness name must accept all of them — a script written against either
7
- * spelling keeps working — and must hand the rest of the CLI ONE value, so no
8
- * comparison downstream has to know there were two
9
- * (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md` §2).
4
+ * INGRESS: every flag, setting and filter that takes a harness name accepts
5
+ * exactly the identity's ACCEPTED spellings (`firstPartyHarnessSpellings`) and
6
+ * hands the rest of the CLI ONE value, so no comparison downstream has to know
7
+ * whether there were several (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md`
8
+ * §2). During a rename's alias window that is the canonical spelling plus
9
+ * `FIRST_PARTY_HARNESS.aliases`, so a script written against either keeps
10
+ * working; outside one it is the canonical spelling alone. A FORMER spelling is
11
+ * accepted on no input — it is passed through unchanged, so a flag offering only
12
+ * the accepted spellings rejects it — even though the CLI still recognises it on
13
+ * an engine binary already on disk (`first-party-harness.ts`).
10
14
  *
11
15
  * Two destinations, two spellings, and the difference is the point:
12
16
  *
13
17
  * - to the SERVER (HTTP bodies, query params) and to the terminal: CANONICAL,
14
18
  * which is what the platform stores and returns;
15
- * - to the local DAEMON binary: the WIRE spelling, which every daemon version
16
- * already installed on a user's machine understands. A daemon that predates
17
- * the rename cannot be taught a new spelling by the CLI that talks to it.
19
+ * - to the local DAEMON binary: the WIRE spelling (`wireProvider`). It sits on
20
+ * the old spelling from a rename's flip until its wire phase, because a daemon
21
+ * that predates the rename cannot be taught a new spelling by the CLI that
22
+ * talks to it, and is canonical otherwise.
18
23
  *
19
24
  * Every other harness name passes through unchanged — this normalises one
20
25
  * harness's spellings, it is not a general lower-caser, and silently rewriting a
@@ -28,8 +33,9 @@ export declare function canonicalHarnessInputs(values: readonly string[]): strin
28
33
  export declare function daemonHarnessArgument(value: string): string;
29
34
  /**
30
35
  * The `options` list for an oclif flag that enumerates harness names: the given
31
- * names with every spelling of the first-party harness in place of its canonical
32
- * one, so oclif's own validation accepts an alias instead of rejecting it before
33
- * the command ever runs.
36
+ * names with every ACCEPTED spelling of the first-party harness in place of its
37
+ * canonical one, so oclif's own validation accepts an alias instead of rejecting
38
+ * it before the command ever runs — and rejects a former spelling, which no
39
+ * ingress accepts.
34
40
  */
35
41
  export declare function harnessFlagOptions(names: readonly string[]): string[];
@@ -2,20 +2,25 @@
2
2
  /**
3
3
  * Harness-valued INPUT, normalised at the edge.
4
4
  *
5
- * The first-party harness has more than one accepted spelling while a rename is
6
- * in flight (`FIRST_PARTY_HARNESS.aliases`). Every flag, setting and filter that
7
- * takes a harness name must accept all of them — a script written against either
8
- * spelling keeps working — and must hand the rest of the CLI ONE value, so no
9
- * comparison downstream has to know there were two
10
- * (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md` §2).
5
+ * INGRESS: every flag, setting and filter that takes a harness name accepts
6
+ * exactly the identity's ACCEPTED spellings (`firstPartyHarnessSpellings`) and
7
+ * hands the rest of the CLI ONE value, so no comparison downstream has to know
8
+ * whether there were several (`docs/architecture/skrr-code-identifier-rename-2026-09-13.md`
9
+ * §2). During a rename's alias window that is the canonical spelling plus
10
+ * `FIRST_PARTY_HARNESS.aliases`, so a script written against either keeps
11
+ * working; outside one it is the canonical spelling alone. A FORMER spelling is
12
+ * accepted on no input — it is passed through unchanged, so a flag offering only
13
+ * the accepted spellings rejects it — even though the CLI still recognises it on
14
+ * an engine binary already on disk (`first-party-harness.ts`).
11
15
  *
12
16
  * Two destinations, two spellings, and the difference is the point:
13
17
  *
14
18
  * - to the SERVER (HTTP bodies, query params) and to the terminal: CANONICAL,
15
19
  * which is what the platform stores and returns;
16
- * - to the local DAEMON binary: the WIRE spelling, which every daemon version
17
- * already installed on a user's machine understands. A daemon that predates
18
- * the rename cannot be taught a new spelling by the CLI that talks to it.
20
+ * - to the local DAEMON binary: the WIRE spelling (`wireProvider`). It sits on
21
+ * the old spelling from a rename's flip until its wire phase, because a daemon
22
+ * that predates the rename cannot be taught a new spelling by the CLI that
23
+ * talks to it, and is canonical otherwise.
19
24
  *
20
25
  * Every other harness name passes through unchanged — this normalises one
21
26
  * harness's spellings, it is not a general lower-caser, and silently rewriting a
@@ -48,9 +53,10 @@ function daemonHarnessArgument(value) {
48
53
  }
49
54
  /**
50
55
  * The `options` list for an oclif flag that enumerates harness names: the given
51
- * names with every spelling of the first-party harness in place of its canonical
52
- * one, so oclif's own validation accepts an alias instead of rejecting it before
53
- * the command ever runs.
56
+ * names with every ACCEPTED spelling of the first-party harness in place of its
57
+ * canonical one, so oclif's own validation accepts an alias instead of rejecting
58
+ * it before the command ever runs — and rejects a former spelling, which no
59
+ * ingress accepts.
54
60
  */
55
61
  function harnessFlagOptions(names) {
56
62
  return canonicalHarnessInputs(names).flatMap((name) => (0, auth_core_1.isFirstPartyHarnessProvider)(name) ? [...(0, auth_core_1.firstPartyHarnessSpellings)()] : [name]);
@@ -47,9 +47,10 @@ export declare const HARNESS_TIER_AUTHORITY = "daemon/src/harness-trust.ts";
47
47
  /**
48
48
  * The tier the shared table records for a harness, or `null` for an unknown name.
49
49
  *
50
- * Canonicalised first, so no spelling of the first-party harness can reach the
51
- * lookup as an unknown name — the shared table also carries every spelling, and
52
- * this keeps the answer right even for one written in a different case.
50
+ * Canonicalised first, so no accepted spelling of the first-party harness can reach
51
+ * the lookup as an unknown name — the shared table also carries every accepted
52
+ * spelling, and this keeps the answer right even for one written in a different
53
+ * case. A retired spelling is an unknown name here, as everywhere trust is decided.
53
54
  */
54
55
  export declare function mirroredTier(harness: string): HarnessTrustTier | null;
55
56
  export interface TierDescription {
@@ -52,9 +52,10 @@ exports.HARNESS_TIER_AUTHORITY = 'daemon/src/harness-trust.ts';
52
52
  /**
53
53
  * The tier the shared table records for a harness, or `null` for an unknown name.
54
54
  *
55
- * Canonicalised first, so no spelling of the first-party harness can reach the
56
- * lookup as an unknown name — the shared table also carries every spelling, and
57
- * this keeps the answer right even for one written in a different case.
55
+ * Canonicalised first, so no accepted spelling of the first-party harness can reach
56
+ * the lookup as an unknown name — the shared table also carries every accepted
57
+ * spelling, and this keeps the answer right even for one written in a different
58
+ * case. A retired spelling is an unknown name here, as everywhere trust is decided.
58
59
  */
59
60
  function mirroredTier(harness) {
60
61
  const key = String((0, auth_core_1.canonicalHarnessProvider)(harness));
@@ -19,10 +19,35 @@ exports.htmlToDisplayText = htmlToDisplayText;
19
19
  function looksLikeHtml(value) {
20
20
  return /<\/?[a-z][^>]*>/i.test(value);
21
21
  }
22
+ /** Pull one attribute out of a tag body — `src="…"`, `src='…'`, `src=…`. */
23
+ function tagAttr(tag, name) {
24
+ const m = new RegExp(`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(tag);
25
+ return m ? (m[1] ?? m[2] ?? m[3] ?? '') : '';
26
+ }
22
27
  function htmlToDisplayText(value) {
23
28
  if (!looksLikeHtml(value))
24
29
  return value;
25
30
  return (value
31
+ // A <video> is the same kind of meaning as an <img>, and the tag strip
32
+ // would erase it the same way, leaving a terminal reader no sign the
33
+ // description holds a recording at all. Markdown has no video syntax, so
34
+ // it becomes a labelled link to the stable reference. The element's
35
+ // children — `<source>` and fallback text — go with it; a `<source>` src
36
+ // is used when the element itself carries none (OSK-10129).
37
+ .replace(/<video\b([^>]*)>([\s\S]*?)<\/video\s*>|<video\b([^>]*)\/?>/gi, (_m, open, inner, bare) => {
38
+ const tag = String(open ?? bare ?? '');
39
+ const src = tagAttr(tag, 'src') || tagAttr(/<source\b[^>]*>/i.exec(inner ?? '')?.[0] ?? '', 'src');
40
+ const title = tagAttr(tag, 'title');
41
+ return src ? `[video: ${title || 'recording'}](${src})\n` : '';
42
+ })
43
+ // An <img> carries meaning — the asset reference or URL — that the
44
+ // generic tag strip below would erase entirely. Keep it as a markdown
45
+ // image so `task-asset:<id>` refs stay legible in a terminal (OSK-9856).
46
+ .replace(/<img\b[^>]*>/gi, (tag) => {
47
+ const src = tagAttr(tag, 'src');
48
+ const alt = tagAttr(tag, 'alt');
49
+ return src ? `![${alt || 'image'}](${src})` : '';
50
+ })
26
51
  .replace(/<br\s*\/?>/gi, '\n')
27
52
  .replace(/<\/(p|div|li|h[1-6])>/gi, '\n')
28
53
  .replace(/<li[^>]*>/gi, ' - ')