akm-cli 0.9.15-beta.2 → 0.9.15-beta.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.
- package/CHANGELOG.md +116 -3
- package/dist/cli.js +3 -3
- package/dist/commands/health/checks.js +13 -9
- package/dist/commands/health/scheduler-binary.js +120 -0
- package/dist/commands/health.js +9 -0
- package/dist/commands/improve/improve-cli.js +107 -6
- package/dist/commands/improve/improve.js +47 -3
- package/dist/commands/improve/reflect.js +159 -64
- package/dist/core/config/schema/embedding.js +2 -3
- package/dist/core/errors.js +2 -0
- package/dist/core/maintenance-barrier.js +35 -6
- package/dist/indexer/indexer.js +44 -3
- package/dist/indexer/materialize-embeddings.js +51 -9
- package/dist/llm/client.js +18 -0
- package/dist/llm/embedders/remote.js +106 -8
- package/dist/llm/usage-persist.js +7 -1
- package/dist/scripts/akm-migrate-node.js +77 -17
- package/dist/scripts/akm-migrate.js +77 -17
- package/docs/migration/release-notes/0.9.15.md +83 -0
- package/docs/reference/cli.md +60 -6
- package/docs/reference/configuration.md +55 -5
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [0.9.15-beta.
|
|
7
|
+
## [0.9.15-beta.4] - 2026-09-10
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -250,8 +250,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
250
250
|
`embedding.contextLength` is Ollama's `num_ctx` only now — it used to also
|
|
251
251
|
silently set the per-request token budget (`embedding.maxTokens`), so
|
|
252
252
|
setting it for the server's context window changed request batching too.
|
|
253
|
-
The request budget is `embedding.maxTokens` (default
|
|
254
|
-
carries about
|
|
253
|
+
The request budget is `embedding.maxTokens` (default 6000, see #954
|
|
254
|
+
below), so a request carries about 11 documents alongside the new
|
|
255
|
+
per-document cap by default.
|
|
255
256
|
- **`akm index` reports where its embedding credential came from, before the
|
|
256
257
|
first provider request (#953).** A field report suspected a gateway was
|
|
257
258
|
receiving unauthenticated embedding requests despite `embedding.apiKey`
|
|
@@ -268,6 +269,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
268
269
|
apiKey`, or `none configured` — never the credential's value, so a field
|
|
269
270
|
run can compare it directly against what the gateway actually logged.
|
|
270
271
|
`--verbose` also names the config file the run loaded.
|
|
272
|
+
- **`akm health` gains a `scheduler-binary` advisory for scheduler binary
|
|
273
|
+
drift (#953).** A field report found `akm task sync`'s recorded absolute
|
|
274
|
+
akm path can go stale after upgrading through a different installer (npm
|
|
275
|
+
global to a standalone download, or vice versa), leaving a scheduled run
|
|
276
|
+
invoking the old binary indefinitely with nothing surfacing it. The new
|
|
277
|
+
`--probe`-gated advisory reads the scheduler's recorded akm invocation —
|
|
278
|
+
the same binding `task sync`/`task doctor` already read, no crontab text
|
|
279
|
+
parsing — runs it with `--version`, and `warn`s naming both versions when
|
|
280
|
+
it differs from the running CLI, pointing at `akm task sync` as the
|
|
281
|
+
remedy. `unknown` when not probed, no task is installed, or the recorded
|
|
282
|
+
binary cannot be executed.
|
|
283
|
+
- **`akm improve <ref> --show-prompt` prints the composed reflect prompt for
|
|
284
|
+
one asset and exits (#952).** A beta.3 field round confirmed the #952 prompt
|
|
285
|
+
fix by reading source, but no live `akm improve` completed across three
|
|
286
|
+
attempts, leaving no cheap way to see the prompt in practice. `--show-prompt`
|
|
287
|
+
reuses every read-only step `akm improve`'s live reflect step already
|
|
288
|
+
performs (source resolution, runner selection, feedback/schema-hint/
|
|
289
|
+
related-lesson/rejected-proposal gathering) and stops before the dispatch
|
|
290
|
+
lease reflect would otherwise acquire — no lock, index write, or engine call,
|
|
291
|
+
same as `--dry-run`. Requires a fully-qualified asset ref as the scope; JSON/
|
|
292
|
+
yaml output carries the prompt as a `prompt` field, text output prints it
|
|
293
|
+
directly so the #952 framing (feedback shown as an unverified report, and
|
|
294
|
+
the instruction never to emit the truncation marker or out-of-asset content)
|
|
295
|
+
can be checked by eye.
|
|
271
296
|
|
|
272
297
|
### Changed
|
|
273
298
|
|
|
@@ -322,6 +347,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
322
347
|
single document that still fails this way is skipped, as
|
|
323
348
|
`context-window-exceeded`; every other failure (network error, 5xx, malformed
|
|
324
349
|
response) keeps the prior skip-the-whole-batch behavior.
|
|
350
|
+
- **The default per-request token budget is lower, and adapts mid-run after a
|
|
351
|
+
context-size rejection (#954, field report on beta.1).**
|
|
352
|
+
`embedding.maxTokens`'s default dropped from 8000 to 6000: the 4-chars-per-
|
|
353
|
+
token estimator undercounts dense technical text by 7-55%, so 8000 regularly
|
|
354
|
+
overshot a real 8192-token endpoint. On an `akm index` run's first
|
|
355
|
+
context-size rejection, the effective request budget additionally shrinks to
|
|
356
|
+
three quarters of its current value (floored at twice
|
|
357
|
+
`embedding.maxInputTokens`) for every request not yet sent, and one
|
|
358
|
+
default-level line reports the new value; it never shrinks a second time in
|
|
359
|
+
the same run. Users who set `embedding.maxTokens` explicitly keep it as the
|
|
360
|
+
starting point but still benefit from this same-run recovery.
|
|
325
361
|
- **Embedding requests are dispatched through a small in-flight window instead of
|
|
326
362
|
strictly sequentially (#954).** The window defaults to 1 request at a time for
|
|
327
363
|
a loopback endpoint and 2 for a remote one; the actual throughput knob is
|
|
@@ -432,6 +468,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
432
468
|
Source-cache hydration (which runs before `index.db` is even opened) now
|
|
433
469
|
reports its own progress the same way: `Hydrating source i/n: <name>` per
|
|
434
470
|
source, plus a 15s heartbeat while a sync is in flight.
|
|
471
|
+
- **`embedding.chunkSize` is retired (#954).** Nothing under `src/` ever read
|
|
472
|
+
it; it was declared in the config schema but had no effect. It is removed
|
|
473
|
+
from `EmbeddingConnectionConfigSchema` and `schemas/akm-config.json`. The
|
|
474
|
+
`embedding` object stays `.passthrough()`, so a config that still sets
|
|
475
|
+
`embedding.chunkSize` keeps loading exactly as before — the key is simply
|
|
476
|
+
ignored, not rejected or warned about.
|
|
435
477
|
|
|
436
478
|
### Fixed
|
|
437
479
|
|
|
@@ -533,6 +575,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
533
575
|
physical batch size", which the existing context-size pattern
|
|
534
576
|
(`exceed_context_size_error`, "context size", …) did not match, so the
|
|
535
577
|
whole batch was dropped instead of being split and retried like a 413.
|
|
578
|
+
- **The end-of-run throughput line now sums the capped text actually sent to
|
|
579
|
+
the embedding provider (#954).** `storedTokens` accumulated
|
|
580
|
+
`estimateTokenCount(entry.searchText)` — the entry's pre-cap search text —
|
|
581
|
+
while the request `embedBatch` received held the text `capEmbeddingText`
|
|
582
|
+
had already truncated to `embedding.maxInputTokens`, so the reported
|
|
583
|
+
`tokens/s` figure overstated throughput for every entry over the cap. The
|
|
584
|
+
final line now sums the estimate of the capped text the batching loop
|
|
585
|
+
already built, matching what the provider was actually asked to embed.
|
|
536
586
|
- **A `kill <launcher-pid>` no longer orphans the running `akm` process
|
|
537
587
|
(#956).** The published launcher (`scripts/node-runtime/akm`/
|
|
538
588
|
`akm-migrate`) now forwards SIGTERM/SIGINT/SIGHUP to its bun/node child
|
|
@@ -562,6 +612,69 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
562
612
|
instead of reusing them. A plain `akm index` resume after an interruption
|
|
563
613
|
now embeds only the entries still missing a vector, with no purge and no
|
|
564
614
|
canary.
|
|
615
|
+
- **A concurrent `akm index` without `--skip-if-locked` now fails with a
|
|
616
|
+
retryable-shortly exit code instead of a raw driver error (#956).**
|
|
617
|
+
Contention with another writer touching index.db (a second `akm index`, a
|
|
618
|
+
source add/update's embedding pass, the per-command background reindex)
|
|
619
|
+
used to exhaust the SQLite driver's retry window and surface as
|
|
620
|
+
`{"ok":false,"error":"database is locked"}` at exit 70
|
|
621
|
+
(internal/unclassified). It is now reclassified into a `TransientError`
|
|
622
|
+
with a dedicated `INDEX_DB_CONTENDED` code (exit 75), naming the rebuild
|
|
623
|
+
lock's live holder pid when known, mirroring `STATE_DB_CONTENDED`'s
|
|
624
|
+
precedent (#948) for state.db; the original driver text survives as
|
|
625
|
+
`cause`. `--skip-if-locked` is unaffected — it already skips gracefully
|
|
626
|
+
before ever attempting the write.
|
|
627
|
+
- **A concurrent plain `akm index` (no `--skip-if-locked`) could still exit
|
|
628
|
+
78 instead of 75, a 2026-09-10 field re-test found (#956).** Two `akm
|
|
629
|
+
index` runs colliding on the short internal barrier that registers the
|
|
630
|
+
opt-in rebuild lock (shared with every other akm lock/lease) threw
|
|
631
|
+
`ConfigError("INVALID_CONFIG_FILE")` — a config-error exit that tells a
|
|
632
|
+
supervisor to stop retrying, when this is ordinary contention between two
|
|
633
|
+
legitimate runs. The barrier is meant to be held only milliseconds, so it
|
|
634
|
+
now retries briefly (a bounded, jittered backoff) before giving up, letting
|
|
635
|
+
an ordinary collision succeed instead of erroring at all; if it is still
|
|
636
|
+
busy after that, it raises `TransientError` with a dedicated
|
|
637
|
+
`MAINTENANCE_BARRIER_BUSY` code (exit 75) instead of the config error. The
|
|
638
|
+
rebuild lock itself is unaffected and still never blocks (#872).
|
|
639
|
+
- **The fingerprint-rename canary embeds the exact text the stored vector was
|
|
640
|
+
generated from (#955).** `sampleEmbeddedEntriesForCanary` handed the canary
|
|
641
|
+
the entry's raw `search_text`, while the main embedding pass caps it to
|
|
642
|
+
`embedding.maxInputTokens` before ever calling the provider — so for any
|
|
643
|
+
entry whose search text exceeded the cap, the canary's freshly re-embedded
|
|
644
|
+
vector came from a different input than the one that produced the stored
|
|
645
|
+
vector, and the median cosine similarity could fall below the compatibility
|
|
646
|
+
threshold for reasons unrelated to the model, triggering a needless full
|
|
647
|
+
purge and rebuild on a same-model rename. The canary now caps each sampled
|
|
648
|
+
entry's search text the same way, through the same `capEmbeddingText`
|
|
649
|
+
helper, before requesting its vector.
|
|
650
|
+
- **`akm improve --require-engines` now probes reachability instead of only
|
|
651
|
+
checking config/credentials, and a dead engine can no longer hang a run
|
|
652
|
+
past its timeout or a signal (#957).** Field, beta.3: engines pointed at a
|
|
653
|
+
dead endpoint, then `akm improve --require-engines` ran ~4 minutes with
|
|
654
|
+
zero output, ignoring an external `timeout 30` (SIGTERM) and akm's own
|
|
655
|
+
`--timeout-ms` — the documented exit-78 "required engines unavailable"
|
|
656
|
+
path could never be observed, because `--require-engines` only checked
|
|
657
|
+
that an engine was configured and credentialed, never whether it actually
|
|
658
|
+
answered. It now also runs the same bounded reachability probe `akm
|
|
659
|
+
health`'s `default-llm-engine`/`configured-engines` checks already use
|
|
660
|
+
(one `/models` request per distinct endpoint), before any lock, log, or
|
|
661
|
+
index side effect, and aborts at exit 78 naming the unreachable engine and
|
|
662
|
+
endpoint. Separately, a live run now prints one default-level line if it
|
|
663
|
+
has waited more than a few seconds on its first engine response, so a
|
|
664
|
+
scheduled run's log is never silently empty while an engine is slow or
|
|
665
|
+
dead — `--timeout-ms` and an engine's own configured timeout already
|
|
666
|
+
aborted the in-flight request correctly (confirmed by this investigation,
|
|
667
|
+
not changed), and SIGTERM/SIGINT already ended the process within its
|
|
668
|
+
documented grace period.
|
|
669
|
+
- **`akm improve --show-prompt` now includes `avoidPatterns` when a live
|
|
670
|
+
improve loop has set them (#952).** The preview built its own second copy
|
|
671
|
+
of reflect's prompt-source gathering and `ReflectPromptInput` assembly,
|
|
672
|
+
which had already drifted from the real dispatch path: it never read
|
|
673
|
+
`avoidPatterns` (recent-error context from earlier assets in the same
|
|
674
|
+
run), so the preview was not always the prompt a live iteration would
|
|
675
|
+
actually send. Both the preview and the real dispatch path (`akmReflect`,
|
|
676
|
+
`runReflectRefineIterations`) now gather sources and assemble the prompt
|
|
677
|
+
input through the same two shared helpers, so this cannot drift again.
|
|
565
678
|
|
|
566
679
|
## [0.9.14] - 2026-09-04
|
|
567
680
|
|
package/dist/cli.js
CHANGED
|
@@ -295,8 +295,8 @@ const healthCommand = defineCommand({
|
|
|
295
295
|
probe: {
|
|
296
296
|
type: "boolean",
|
|
297
297
|
default: true,
|
|
298
|
-
description: "Probe default-llm-engine / configured-engines reachability
|
|
299
|
-
negativeDescription: "Skip the reachability probes and the
|
|
298
|
+
description: "Probe default-llm-engine / configured-engines reachability, check for a newer akm release, and check the scheduler's recorded akm binary version (on by default).",
|
|
299
|
+
negativeDescription: "Skip the reachability probes, the update check, and the scheduler-binary version check (for an offline or air-gapped host).",
|
|
300
300
|
},
|
|
301
301
|
},
|
|
302
302
|
async run({ args }) {
|
|
@@ -556,7 +556,7 @@ export const main = defineCommand({
|
|
|
556
556
|
" 2 usage error\n" +
|
|
557
557
|
" 4 health warn (akm health only)\n" +
|
|
558
558
|
" 70 internal / unclassified error\n" +
|
|
559
|
-
" 75 transient (retry shortly — another akm process holds a lock or is writing state.db)\n" +
|
|
559
|
+
" 75 transient (retry shortly — another akm process holds a lock or is writing state.db or index.db)\n" +
|
|
560
560
|
" 78 config error",
|
|
561
561
|
},
|
|
562
562
|
args: {
|
|
@@ -12,6 +12,7 @@ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js"
|
|
|
12
12
|
import { isLlmCredentialAvailable, resolveEngine, } from "../../integrations/agent/engine-resolution.js";
|
|
13
13
|
import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution-definitions.js";
|
|
14
14
|
import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
|
|
15
|
+
import { probeEndpointOnce } from "../../llm/client.js";
|
|
15
16
|
import { listKeys } from "../env/env.js";
|
|
16
17
|
import { resolveImprovePlan } from "../improve/improve-strategies.js";
|
|
17
18
|
import { ENGINE_LAST_USED_LOOKBACK_DAYS } from "./engine-usage.js";
|
|
@@ -20,13 +21,7 @@ import { ACTIVE_RUN_WARN_MS, TASK_FAIL_RATE_WARN, } from "./types.js";
|
|
|
20
21
|
function probeConnectionReachable(connection, deps, cache) {
|
|
21
22
|
if (!deps.probeReachable)
|
|
22
23
|
return Promise.resolve(undefined);
|
|
23
|
-
|
|
24
|
-
let pending = cache.get(key);
|
|
25
|
-
if (!pending) {
|
|
26
|
-
pending = deps.probeReachable(connection);
|
|
27
|
-
cache.set(key, pending);
|
|
28
|
-
}
|
|
29
|
-
return pending;
|
|
24
|
+
return probeEndpointOnce(connection, cache, deps.probeReachable);
|
|
30
25
|
}
|
|
31
26
|
function reachabilityEvidence(reach) {
|
|
32
27
|
return reach
|
|
@@ -1164,11 +1159,20 @@ export const HEALTH_CHECKS = [
|
|
|
1164
1159
|
run: (ctx) => ctx.versionDrift,
|
|
1165
1160
|
},
|
|
1166
1161
|
{
|
|
1167
|
-
// #950:
|
|
1168
|
-
// doc comment above). Advisory channel, `kind: "deterministic"` — same
|
|
1162
|
+
// #950: advisory channel, `kind: "deterministic"` — same
|
|
1169
1163
|
// exit-code-gating rationale as thinking-control above.
|
|
1170
1164
|
name: "engine-last-used",
|
|
1171
1165
|
channel: "advisory",
|
|
1172
1166
|
run: (ctx) => projectEngineLastUsedCheck(ctx.activeImproveStrategyEngines, ctx.engineLastUsed, ctx.improveRunsInLookbackWindow, ENGINE_LAST_USED_LOOKBACK_DAYS),
|
|
1173
1167
|
},
|
|
1168
|
+
{
|
|
1169
|
+
// #953: registered last — order is load-bearing (see the HEALTH_CHECKS
|
|
1170
|
+
// doc comment above). Best-effort scheduler-binary-drift advisory, gated
|
|
1171
|
+
// behind the same --probe/--no-probe flag as engine reachability and
|
|
1172
|
+
// cli-version. Computed once in health.ts (process-spawn IO), projected
|
|
1173
|
+
// here like versionDrift/engineProbes.
|
|
1174
|
+
name: "scheduler-binary",
|
|
1175
|
+
channel: "advisory",
|
|
1176
|
+
run: (ctx) => ctx.schedulerBinaryDrift,
|
|
1177
|
+
},
|
|
1174
1178
|
];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `scheduler-binary` advisory for `akm health` (#953).
|
|
6
|
+
*
|
|
7
|
+
* `akm task sync` records an absolute akm invocation path in the OS
|
|
8
|
+
* scheduler (`src/tasks/resolve-akm-bin.ts`) because cron/launchd/schtasks
|
|
9
|
+
* all run jobs with a minimal PATH. A field report showed that path going
|
|
10
|
+
* stale after upgrading akm through a different installer than the one
|
|
11
|
+
* active at the last `task sync` (e.g. npm-global to a standalone binary):
|
|
12
|
+
* the schedule kept invoking a version behind, with nothing in `akm health`
|
|
13
|
+
* surfacing it, until an unrelated failure (a rejected `secret://` engine
|
|
14
|
+
* key on the stale binary) surfaced the drift.
|
|
15
|
+
*
|
|
16
|
+
* Modelled 1:1 on `version-drift.ts`'s shape: an injectable seam,
|
|
17
|
+
* best-effort, `--probe`-gated so an air-gapped host's `--no-probe` habit
|
|
18
|
+
* suppresses this too, and `unknown` on any uncertainty rather than a false
|
|
19
|
+
* `pass`/`warn`. Reads the scheduler's recorded binding via
|
|
20
|
+
* `SchedulerBackend.list()` — the same reader `akm task doctor` uses — never
|
|
21
|
+
* a second crontab/launchd/schtasks text parser.
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { selectBackend } from "../../tasks/backends/index.js";
|
|
25
|
+
import { pkgVersion } from "../../version.js";
|
|
26
|
+
/**
|
|
27
|
+
* Bound on the scheduler-recorded binary's `--version` probe. Matches the
|
|
28
|
+
* `--version` timeout the engine-reachability checks already use
|
|
29
|
+
* (checks.ts's `runConfiguredEngineProbe`), so a wedged or missing binary
|
|
30
|
+
* degrades this advisory to `unknown` in seconds rather than blocking
|
|
31
|
+
* `akm health --probe`.
|
|
32
|
+
*/
|
|
33
|
+
const SCHEDULER_BINARY_VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
34
|
+
/**
|
|
35
|
+
* Build the `scheduler-binary` advisory. `probe` mirrors the
|
|
36
|
+
* engine-reachability and `cli-version` checks' `--probe`/`--no-probe`
|
|
37
|
+
* gating: only inspects the scheduler and spawns a process when `true`;
|
|
38
|
+
* otherwise `unknown` with "not probed", never touching the OS scheduler.
|
|
39
|
+
*/
|
|
40
|
+
export async function collectSchedulerBinaryAdvisory(probe, deps = {}) {
|
|
41
|
+
const cliVersion = deps.cliVersion ?? pkgVersion;
|
|
42
|
+
if (!probe) {
|
|
43
|
+
return {
|
|
44
|
+
name: "scheduler-binary",
|
|
45
|
+
kind: "deterministic",
|
|
46
|
+
status: "unknown",
|
|
47
|
+
confidence: "high",
|
|
48
|
+
message: "Scheduler binary version drift was not probed.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let installed;
|
|
52
|
+
try {
|
|
53
|
+
installed = await (deps.backend ?? selectBackend()).list();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
name: "scheduler-binary",
|
|
58
|
+
kind: "deterministic",
|
|
59
|
+
status: "unknown",
|
|
60
|
+
confidence: "high",
|
|
61
|
+
message: `Installed scheduled tasks could not be inspected: ${error instanceof Error ? error.message : String(error)}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// task sync rewrites every installed binding to the same current
|
|
65
|
+
// invocation atomically, so the first entry's binding represents the
|
|
66
|
+
// whole schedule under normal operation.
|
|
67
|
+
const [firstInstalled] = installed;
|
|
68
|
+
const [binaryPath, ...leadingArgs] = firstInstalled?.binding ?? [];
|
|
69
|
+
if (!firstInstalled || !binaryPath) {
|
|
70
|
+
return {
|
|
71
|
+
name: "scheduler-binary",
|
|
72
|
+
kind: "deterministic",
|
|
73
|
+
status: "unknown",
|
|
74
|
+
confidence: "high",
|
|
75
|
+
message: "No scheduled task is installed.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const binding = firstInstalled.binding;
|
|
79
|
+
const run = deps.spawnSync ?? spawnSync;
|
|
80
|
+
let scheduledVersion;
|
|
81
|
+
try {
|
|
82
|
+
const result = run(binaryPath, [...leadingArgs, "--version"], {
|
|
83
|
+
encoding: "utf8",
|
|
84
|
+
timeout: SCHEDULER_BINARY_VERSION_PROBE_TIMEOUT_MS,
|
|
85
|
+
});
|
|
86
|
+
if ((result.status ?? 1) === 0)
|
|
87
|
+
scheduledVersion = result.stdout?.trim() || undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
scheduledVersion = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (!scheduledVersion) {
|
|
93
|
+
return {
|
|
94
|
+
name: "scheduler-binary",
|
|
95
|
+
kind: "deterministic",
|
|
96
|
+
status: "unknown",
|
|
97
|
+
confidence: "high",
|
|
98
|
+
message: "The scheduler's recorded akm binary could not be executed.",
|
|
99
|
+
evidence: { binding },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (scheduledVersion === cliVersion) {
|
|
103
|
+
return {
|
|
104
|
+
name: "scheduler-binary",
|
|
105
|
+
kind: "deterministic",
|
|
106
|
+
status: "pass",
|
|
107
|
+
confidence: "high",
|
|
108
|
+
message: `Scheduled tasks are bound to akm v${scheduledVersion}, matching the running CLI.`,
|
|
109
|
+
evidence: { binding, scheduledVersion, cliVersion },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
name: "scheduler-binary",
|
|
114
|
+
kind: "deterministic",
|
|
115
|
+
status: "warn",
|
|
116
|
+
confidence: "high",
|
|
117
|
+
message: `Scheduled tasks are bound to akm v${scheduledVersion}, but the running CLI is v${cliVersion} — run \`akm task sync\` to rebind the schedule.`,
|
|
118
|
+
evidence: { binding, scheduledVersion, cliVersion },
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/commands/health.js
CHANGED
|
@@ -27,6 +27,7 @@ import { buildImproveSkipSummary, computeWallTimeStats, countAgentFailureReasons
|
|
|
27
27
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
28
28
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
|
|
29
29
|
import { collectPluginStalenessAdvisories } from "./health/plugin-staleness.js";
|
|
30
|
+
import { collectSchedulerBinaryAdvisory } from "./health/scheduler-binary.js";
|
|
30
31
|
import { collectStashExposureAdvisory } from "./health/stash-exposure.js";
|
|
31
32
|
import { collectSurfacesAdvisories } from "./health/surfaces.js";
|
|
32
33
|
import { buildPerRunSummaries } from "./health/task-runs.js";
|
|
@@ -542,6 +543,12 @@ export async function akmHealth(options = {}) {
|
|
|
542
543
|
// above — started here, alongside it, and awaited later.
|
|
543
544
|
const versionDriftPromise = collectVersionDriftAdvisory(Boolean(options.probe), { cliVersion: pkgVersion });
|
|
544
545
|
versionDriftPromise.catch(() => undefined);
|
|
546
|
+
// #953: same --probe-gated, best-effort discipline as versionDriftPromise
|
|
547
|
+
// above.
|
|
548
|
+
const schedulerBinaryDriftPromise = collectSchedulerBinaryAdvisory(Boolean(options.probe), {
|
|
549
|
+
cliVersion: pkgVersion,
|
|
550
|
+
});
|
|
551
|
+
schedulerBinaryDriftPromise.catch(() => undefined);
|
|
545
552
|
const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
|
|
546
553
|
const { tableNames, missingTables, probe } = taskHistory;
|
|
547
554
|
const { egressConfigView, thinkingOffEngines } = gatherEgressConfigPhase();
|
|
@@ -559,6 +566,7 @@ export async function akmHealth(options = {}) {
|
|
|
559
566
|
const improveRunsInLookbackWindow = countImproveRunsSince(db, engineLastUsedSinceIso);
|
|
560
567
|
const engineProbes = await engineProbesPromise;
|
|
561
568
|
const versionDrift = await versionDriftPromise;
|
|
569
|
+
const schedulerBinaryDrift = await schedulerBinaryDriftPromise;
|
|
562
570
|
// Read once, shared by the `thinking-control` check (#949) and the
|
|
563
571
|
// `metrics.llmUsage` report field below — same window, same aggregate.
|
|
564
572
|
const llmUsage = readLlmUsageAggregate(stateDbPath, since);
|
|
@@ -592,6 +600,7 @@ export async function akmHealth(options = {}) {
|
|
|
592
600
|
activeImproveStrategyEngines,
|
|
593
601
|
engineLastUsed,
|
|
594
602
|
improveRunsInLookbackWindow,
|
|
603
|
+
schedulerBinaryDrift,
|
|
595
604
|
};
|
|
596
605
|
for (const check of HEALTH_CHECKS) {
|
|
597
606
|
const result = check.run(checkContext);
|
|
@@ -15,12 +15,16 @@ import { redactSensitiveText } from "../../core/redaction.js";
|
|
|
15
15
|
import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
|
|
18
|
-
import {
|
|
18
|
+
import { probeEndpointOnce, probeLlmEndpoint } from "../../llm/client.js";
|
|
19
|
+
import { getOutputMode } from "../../output/context.js";
|
|
20
|
+
import { deliverRendered } from "../../output/html-render.js";
|
|
21
|
+
import { akmImprove, resolveImproveReadSource } from "./improve.js";
|
|
19
22
|
import { runImproveReportQuery } from "./improve-report.js";
|
|
20
23
|
import { buildImproveRunId, recordImproveRunResult, recordTerminatedImproveRun, } from "./improve-result-file.js";
|
|
21
24
|
import { runImproveSession } from "./improve-session.js";
|
|
22
|
-
import { resolveImprovePlan } from "./improve-strategies.js";
|
|
25
|
+
import { resolveImprovePlan, } from "./improve-strategies.js";
|
|
23
26
|
import { formatUsageReportTable } from "./improve-usage-report.js";
|
|
27
|
+
import { renderReflectPromptPreview } from "./reflect.js";
|
|
24
28
|
let akmImproveForRun = akmImprove;
|
|
25
29
|
/** Swap the CLI's improve work implementation in deterministic subprocess tests. */
|
|
26
30
|
export function _setAkmImproveForTests(fake) {
|
|
@@ -93,6 +97,84 @@ function assertRequiredEnginesAvailable(plan) {
|
|
|
93
97
|
const lines = plan.engineUnavailable.map((item) => ` - ${item.process} (${item.configKey}): ${item.reason}`);
|
|
94
98
|
throw new ConfigError(`--require-engines: ${plan.engineUnavailable.length} improve process${plan.engineUnavailable.length === 1 ? "" : "es"} cannot run because ${plan.engineUnavailable.length === 1 ? "its" : "their"} engine is unavailable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Every distinct `kind: "llm"` connection the active strategy's plan would
|
|
102
|
+
* actually dispatch against — the main per-process runners plus triage's own
|
|
103
|
+
* judgment engine, which is resolved separately (#957).
|
|
104
|
+
*/
|
|
105
|
+
function collectRequiredEngineTargets(plan) {
|
|
106
|
+
const targets = [];
|
|
107
|
+
for (const [processName, process] of Object.entries(plan.processes)) {
|
|
108
|
+
if (process.runner) {
|
|
109
|
+
targets.push({ process: processName, engine: process.runner.engine, connection: process.runner.connection });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (plan.triageJudgment?.kind === "llm") {
|
|
113
|
+
targets.push({
|
|
114
|
+
process: "triage.judgment",
|
|
115
|
+
engine: plan.triageJudgment.engine,
|
|
116
|
+
connection: plan.triageJudgment.connection,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return targets;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `--require-engines` field re-test (#957): the static check above only
|
|
123
|
+
* proves an engine is configured and credentialed — it cannot see a dead
|
|
124
|
+
* endpoint. A field run against an unreachable engine sat silent for
|
|
125
|
+
* minutes instead of hitting the documented exit-78 path. Reuse the SAME
|
|
126
|
+
* bounded reachability probe `akm health`'s `default-llm-engine` /
|
|
127
|
+
* `configured-engines` checks already run (`probeLlmEndpoint`, a single
|
|
128
|
+
* `/models` GET bounded by its own default timeout) once per distinct
|
|
129
|
+
* endpoint (via the shared `probeEndpointOnce` memoization health/checks.ts
|
|
130
|
+
* also uses), so a dead engine is caught here instead of during dispatch.
|
|
131
|
+
*/
|
|
132
|
+
async function assertRequiredEnginesReachable(plan, probeReachable = probeLlmEndpoint) {
|
|
133
|
+
const targets = collectRequiredEngineTargets(plan);
|
|
134
|
+
if (targets.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const probesByEndpoint = new Map();
|
|
137
|
+
const probed = await Promise.all(targets.map(async (target) => ({
|
|
138
|
+
...target,
|
|
139
|
+
reach: await probeEndpointOnce(target.connection, probesByEndpoint, probeReachable),
|
|
140
|
+
})));
|
|
141
|
+
const unreachable = probed.filter((item) => !item.reach.reachable);
|
|
142
|
+
if (unreachable.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const lines = unreachable.map((item) => ` - ${item.process} (engine "${item.engine}", ${item.connection.endpoint}): ${item.reach.error ?? "did not respond"}`);
|
|
145
|
+
throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine endpoint is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `--show-prompt` (#952): render the composed reflect prompt for one asset ref
|
|
149
|
+
* and exit, before any lock, log, index write, or engine dispatch — the field
|
|
150
|
+
* had no cheap way to confirm the #952 prompt fix (unverified-feedback framing,
|
|
151
|
+
* no-truncation-marker instruction) without running a full improve cycle.
|
|
152
|
+
* Reuses `renderReflectPromptPreview` (reflect.ts), which stops before the
|
|
153
|
+
* dispatch lease reflect would otherwise acquire, so this never calls an engine.
|
|
154
|
+
*/
|
|
155
|
+
async function runShowPromptCli(refArg, parsedRef, taskArg, targetArg, resolvedPlan) {
|
|
156
|
+
const readSource = resolveImproveReadSource(resolvedPlan.config, parsedRef, targetArg);
|
|
157
|
+
const preview = await renderReflectPromptPreview({
|
|
158
|
+
ref: refArg,
|
|
159
|
+
...(taskArg ? { task: taskArg } : {}),
|
|
160
|
+
improveProfile: resolvedPlan.strategy.config,
|
|
161
|
+
config: resolvedPlan.config,
|
|
162
|
+
stashDir: readSource.source.path,
|
|
163
|
+
});
|
|
164
|
+
const outputMode = getOutputMode();
|
|
165
|
+
if (outputMode.format === "text") {
|
|
166
|
+
deliverRendered(preview.prompt, outputMode.outputPath);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
output("improve", {
|
|
170
|
+
schemaVersion: 2,
|
|
171
|
+
ok: true,
|
|
172
|
+
ref: preview.ref,
|
|
173
|
+
engine: preview.engine,
|
|
174
|
+
engineKind: preview.engineKind,
|
|
175
|
+
prompt: preview.prompt,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
96
178
|
/**
|
|
97
179
|
* `akm improve report` (#944): a scope value that dispatches to the per-run
|
|
98
180
|
* LLM usage/routing report instead of a real improve run — "report" is not,
|
|
@@ -172,7 +254,12 @@ export const improveCommand = defineCommand({
|
|
|
172
254
|
},
|
|
173
255
|
"require-engines": {
|
|
174
256
|
type: "boolean",
|
|
175
|
-
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment. Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
257
|
+
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe (the same probe akm health runs). Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
258
|
+
default: false,
|
|
259
|
+
},
|
|
260
|
+
"show-prompt": {
|
|
261
|
+
type: "boolean",
|
|
262
|
+
description: "Print the composed reflect prompt for one asset ref and exit — no lock, index write, or engine dispatch (#952). Requires a fully-qualified asset ref as the scope positional (e.g. `akm improve lessons/my-lesson --show-prompt`). JSON/yaml format carries the prompt as a `prompt` field; text format prints it directly.",
|
|
176
263
|
default: false,
|
|
177
264
|
},
|
|
178
265
|
run: {
|
|
@@ -216,8 +303,10 @@ export const improveCommand = defineCommand({
|
|
|
216
303
|
const targetArg = getStringArg(args, "bundle");
|
|
217
304
|
const taskArg = getStringArg(args, "task");
|
|
218
305
|
// #947 — `--plan` is a zero-logic discoverability alias for `--dry-run`;
|
|
219
|
-
// it must never fork the computation, only set the same flag.
|
|
220
|
-
|
|
306
|
+
// it must never fork the computation, only set the same flag. #952 —
|
|
307
|
+
// `--show-prompt` implies the same read-only posture (it never reaches
|
|
308
|
+
// akmImprove at all, but keeps writeTarget/resolvedPlan unset the same way).
|
|
309
|
+
const dryRun = args["dry-run"] || args.plan || args["show-prompt"];
|
|
221
310
|
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
222
311
|
const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
|
|
223
312
|
const requireFeedbackSignal = args["require-feedback-signal"];
|
|
@@ -239,8 +328,20 @@ export const improveCommand = defineCommand({
|
|
|
239
328
|
// is disabled purely by an unreachable credential; a live run keeps
|
|
240
329
|
// throwing (allowAllDisabled unset).
|
|
241
330
|
const resolvedPlan = resolveImprovePlan(strategyArg, effectiveConfig, { allowAllDisabled: Boolean(dryRun) });
|
|
242
|
-
|
|
331
|
+
// #952 — same interception point as the `report` scope above: before any
|
|
332
|
+
// lock, log, or index side effect. Requires a single fully-qualified
|
|
333
|
+
// asset ref (not a type or whole-bundle scope).
|
|
334
|
+
if (args["show-prompt"]) {
|
|
335
|
+
if (!scopeArg || !scopeRef) {
|
|
336
|
+
throw new UsageError("`--show-prompt` requires a fully-qualified asset ref as the scope (e.g. `akm improve lessons/my-lesson --show-prompt`).", "INVALID_FLAG_VALUE");
|
|
337
|
+
}
|
|
338
|
+
await runShowPromptCli(scopeArg, scopeRef, taskArg, targetArg, resolvedPlan);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (args["require-engines"]) {
|
|
243
342
|
assertRequiredEnginesAvailable(resolvedPlan);
|
|
343
|
+
await assertRequiredEnginesReachable(resolvedPlan);
|
|
344
|
+
}
|
|
244
345
|
const selectedStrategyName = resolvedPlan.strategy.name;
|
|
245
346
|
const sensitiveValues = collectEngineCredentialValues(effectiveConfig);
|
|
246
347
|
// Only set the keys the user actually passed (citty leaves the flag
|
|
@@ -65,6 +65,19 @@ export function renderSyncCommitMessage(template, result, nowMs) {
|
|
|
65
65
|
};
|
|
66
66
|
return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* How long the improve loop waits for its FIRST engine response (success or
|
|
70
|
+
* error — any terminal record proves the run is not silent) before printing
|
|
71
|
+
* one default-level line. The timer is armed once the triage/index prepass
|
|
72
|
+
* finishes and the loop is about to start dispatching engine requests — not
|
|
73
|
+
* at run start — so it measures engine latency, not prepass time. Field
|
|
74
|
+
* re-test (#957): an engine pointed at a dead endpoint produced zero output
|
|
75
|
+
* for minutes, so a genuine hang looked identical to a normal-but-slow run.
|
|
76
|
+
* A few seconds is short enough that an operator watching a scheduled run's
|
|
77
|
+
* live log sees something promptly, long enough that an ordinary fast
|
|
78
|
+
* response never prints it.
|
|
79
|
+
*/
|
|
80
|
+
export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
|
|
68
81
|
export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
69
82
|
const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
|
|
70
83
|
const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
|
|
@@ -119,6 +132,12 @@ export async function akmImprove(options = {}) {
|
|
|
119
132
|
options = setup.options;
|
|
120
133
|
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
|
|
121
134
|
let clearBudgetTimer = () => { };
|
|
135
|
+
let clearFirstResponseHeartbeat = () => { };
|
|
136
|
+
// #957: set by the usage sink's onRecord callback the moment any engine
|
|
137
|
+
// call terminates (success or error), including one issued by the prepass
|
|
138
|
+
// itself — makes arming the heartbeat below a no-op when the run is
|
|
139
|
+
// already known not to be silent.
|
|
140
|
+
let firstEngineResponseSeen = false;
|
|
122
141
|
let initialGitPaths = new Set();
|
|
123
142
|
const runJournal = createRunWriteJournal();
|
|
124
143
|
const preEnsureCleanupWarnings = [];
|
|
@@ -175,7 +194,10 @@ export async function akmImprove(options = {}) {
|
|
|
175
194
|
return buildLockSkippedResult(selectedStrategy.name, scope, options.runId);
|
|
176
195
|
}
|
|
177
196
|
improveLockOwnership = acquisition.ownership;
|
|
178
|
-
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx)
|
|
197
|
+
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx, () => {
|
|
198
|
+
firstEngineResponseSeen = true;
|
|
199
|
+
clearFirstResponseHeartbeat();
|
|
200
|
+
});
|
|
179
201
|
exitBackstop = releaseRunLock;
|
|
180
202
|
process.on("exit", exitBackstop);
|
|
181
203
|
initialGitPaths =
|
|
@@ -215,6 +237,7 @@ export async function akmImprove(options = {}) {
|
|
|
215
237
|
// If the live prepass fails, emit its summary and clear the owning sink
|
|
216
238
|
// before any run teardown. The disposer is idempotent with the main finalizer.
|
|
217
239
|
disposeLlmUsageSink();
|
|
240
|
+
clearFirstResponseHeartbeat();
|
|
218
241
|
clearBudgetTimer();
|
|
219
242
|
if (exitBackstop) {
|
|
220
243
|
process.removeListener("exit", exitBackstop);
|
|
@@ -233,6 +256,20 @@ export async function akmImprove(options = {}) {
|
|
|
233
256
|
// are all in hand. See buildImproveRunContext for exactly which
|
|
234
257
|
// already-resolved values back each field.
|
|
235
258
|
const ctx = buildImproveRunContext(setup, eventsCtx);
|
|
259
|
+
// #957: arm the heartbeat here, immediately before the improve loop
|
|
260
|
+
// starts dispatching engine requests — not at run start, where its timer
|
|
261
|
+
// would measure the triage/index prepass instead of engine latency. A
|
|
262
|
+
// no-op when the prepass already produced a terminal LLM record (the
|
|
263
|
+
// onRecord callback above already saw it). Cleared the moment any call
|
|
264
|
+
// terminates (success or error) — never rearmed, so this prints at most
|
|
265
|
+
// once per run.
|
|
266
|
+
if (!firstEngineResponseSeen) {
|
|
267
|
+
const firstResponseTimer = setTimeout(() => {
|
|
268
|
+
warn("[improve] Still waiting for the first engine response...");
|
|
269
|
+
}, FIRST_ENGINE_RESPONSE_HEARTBEAT_MS);
|
|
270
|
+
firstResponseTimer.unref?.();
|
|
271
|
+
clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
|
|
272
|
+
}
|
|
236
273
|
const seq = await runImproveStageSequence({
|
|
237
274
|
run: setup,
|
|
238
275
|
strategyFilteredRefs,
|
|
@@ -292,6 +329,8 @@ export async function akmImprove(options = {}) {
|
|
|
292
329
|
// #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
|
|
293
330
|
// no late sink invocation can write through a closed handle.
|
|
294
331
|
disposeLlmUsageSink();
|
|
332
|
+
// #957: never leave the first-response heartbeat timer pending past the run.
|
|
333
|
+
clearFirstResponseHeartbeat();
|
|
295
334
|
// O-1 (#364): Clear the budget abort timer so it does not keep the event
|
|
296
335
|
// loop alive after the run completes.
|
|
297
336
|
clearBudgetTimer();
|
|
@@ -345,8 +384,13 @@ function describeRunWrittenPaths(setup, writtenPaths) {
|
|
|
345
384
|
}
|
|
346
385
|
return [...described].sort();
|
|
347
386
|
}
|
|
348
|
-
/**
|
|
349
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Resolve a dry-run inspection source without adapting it into a write target.
|
|
389
|
+
* Exported so `improve-cli.ts`'s `--show-prompt` (#952) can resolve the same
|
|
390
|
+
* read-only bundle a plain `--dry-run` would, without duplicating this
|
|
391
|
+
* selector/target/fallback precedence.
|
|
392
|
+
*/
|
|
393
|
+
export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
|
|
350
394
|
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
|
|
351
395
|
throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
|
|
352
396
|
}
|