akm-cli 0.9.15 → 0.9.16-alpha.2

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 (86) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  4. package/dist/cli/retired-commands.js +0 -2
  5. package/dist/commands/env/env-binding.js +4 -4
  6. package/dist/commands/env/env-cli.js +3 -3
  7. package/dist/commands/improve/improve-cli.js +19 -14
  8. package/dist/commands/improve/reflect.js +23 -2
  9. package/dist/commands/lint/base-linter.js +9 -0
  10. package/dist/commands/lint/env-key-rules.js +2 -2
  11. package/dist/commands/proposal/propose.js +15 -1
  12. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  13. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  14. package/dist/commands/read/search.js +33 -4
  15. package/dist/commands/read/show.js +21 -2
  16. package/dist/commands/registry-cli.js +5 -5
  17. package/dist/commands/sources/add-cli.js +59 -16
  18. package/dist/commands/sources/bundle-cli.js +35 -11
  19. package/dist/commands/sources/bundle-config-ops.js +30 -0
  20. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  21. package/dist/commands/sources/installed-stashes.js +43 -28
  22. package/dist/commands/sources/source-add.js +33 -17
  23. package/dist/commands/sources/source-manage.js +34 -12
  24. package/dist/commands/sources/stash-skeleton.js +6 -3
  25. package/dist/commands/tasks/explain.js +4 -1
  26. package/dist/commands/tasks/tasks-cli.js +31 -9
  27. package/dist/commands/tasks/tasks.js +239 -194
  28. package/dist/commands/tasks/validate.js +20 -32
  29. package/dist/core/activation-policy.js +4 -4
  30. package/dist/core/adapter/adapters/akm-adapter.js +5 -0
  31. package/dist/core/adapter/execution-source.js +10 -29
  32. package/dist/core/config/config-schema.js +64 -8
  33. package/dist/core/config/config-sources.js +96 -2
  34. package/dist/core/config/config.js +190 -24
  35. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  36. package/dist/core/config/schema/execution.js +23 -0
  37. package/dist/core/config/schema/experimental.js +1 -1
  38. package/dist/core/config/schema/scheduler.js +20 -0
  39. package/dist/core/config/schema/search.js +1 -1
  40. package/dist/core/config/schema/sources-bundles.js +32 -1
  41. package/dist/core/content-safety.js +52 -0
  42. package/dist/core/maintenance-barrier.js +6 -6
  43. package/dist/core/type-presentation.js +1 -1
  44. package/dist/core/write-source.js +13 -8
  45. package/dist/indexer/bundle-identity-guard.js +45 -8
  46. package/dist/indexer/indexer.js +1 -1
  47. package/dist/indexer/materialize-embeddings.js +15 -1
  48. package/dist/indexer/search/search-source.js +29 -11
  49. package/dist/integrations/agent/execution-lowering.js +3 -2
  50. package/dist/integrations/agent/execution-preparation.js +32 -1
  51. package/dist/integrations/agent/prompts.js +1 -1
  52. package/dist/integrations/agent/request-lowering.js +3 -2
  53. package/dist/llm/client.js +2 -1
  54. package/dist/output/shapes/passthrough.js +2 -0
  55. package/dist/registry/resolve.js +37 -10
  56. package/dist/scripts/akm-migrate-node.js +13644 -9894
  57. package/dist/scripts/akm-migrate.js +12626 -8876
  58. package/dist/setup/setup.js +3 -3
  59. package/dist/setup/steps/tasks.js +29 -36
  60. package/dist/sources/providers/git-install.js +17 -11
  61. package/dist/sources/providers/git-provider.js +12 -5
  62. package/dist/sources/providers/git-stash.js +38 -16
  63. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  64. package/dist/storage/repositories/index-vec-repository.js +100 -0
  65. package/dist/tasks/activation-config.js +90 -0
  66. package/dist/tasks/backends/cron.js +9 -0
  67. package/dist/tasks/backends/launchd.js +1 -0
  68. package/dist/tasks/backends/schtasks.js +2 -0
  69. package/dist/tasks/embedded.js +4 -5
  70. package/dist/tasks/scheduler-binding.js +2 -2
  71. package/dist/tasks/scheduler-sync-preview.js +8 -1
  72. package/dist/tasks/scheduler-sync.js +19 -10
  73. package/dist/tasks/source/parse-task-source.js +10 -113
  74. package/dist/tasks/source/project-v4.js +2 -2
  75. package/dist/tasks/source/task-source-v4.js +4 -12
  76. package/dist/tasks/source/task-to-v3.js +4 -12
  77. package/dist/tasks/source/task-to-v4.js +40 -7
  78. package/docs/migration/README.md +1 -0
  79. package/docs/migration/release-notes/0.9.16.md +72 -0
  80. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  81. package/docs/reference/cli.md +37 -29
  82. package/docs/reference/configuration.md +48 -5
  83. package/docs/reference/tasks.md +34 -29
  84. package/package.json +1 -1
  85. package/schemas/akm-config.json +112 -4
  86. package/schemas/akm-task.json +1 -2
@@ -28,6 +28,7 @@
28
28
  * comparison over a non-empty index so the steady-state cost is a boolean check.
29
29
  */
30
30
  import fs from "node:fs";
31
+ import path from "node:path";
31
32
  import { loadConfig } from "../core/config/config.js";
32
33
  import { rethrowIfTestIsolationError } from "../core/errors.js";
33
34
  import { getDbPath } from "../core/paths.js";
@@ -39,8 +40,8 @@ let guardSettled = false;
39
40
  export function resetBundleIdentityGuardForTests() {
40
41
  guardSettled = false;
41
42
  }
42
- /** Distinct non-empty `bundle_id` prefixes persisted in the index, or `undefined` when unreadable. */
43
- function indexBundlePrefixes(dbPath) {
43
+ /** Bundle/path ownership persisted in the index, or `undefined` when unreadable. */
44
+ function indexedBundlePaths(dbPath) {
44
45
  if (!fs.existsSync(dbPath))
45
46
  return undefined;
46
47
  let db;
@@ -51,8 +52,9 @@ function indexBundlePrefixes(dbPath) {
51
52
  if (!isCanonicalIndexGeneration(db))
52
53
  return undefined;
53
54
  return db
54
- .prepare("SELECT DISTINCT bundle_id AS b FROM entries WHERE bundle_id IS NOT NULL AND bundle_id != ''")
55
- .all().map((row) => row.b);
55
+ .prepare("SELECT DISTINCT bundle_id AS bundleId, file_path AS filePath FROM entries " +
56
+ "WHERE bundle_id IS NOT NULL AND bundle_id != '' AND file_path IS NOT NULL AND file_path != ''")
57
+ .all();
56
58
  }
57
59
  catch (error) {
58
60
  rethrowIfTestIsolationError(error);
@@ -80,20 +82,55 @@ export function warnOnBundleRenameDrift(config = loadConfig()) {
80
82
  const configIds = new Set(Object.keys(bundles));
81
83
  if (configIds.size === 0)
82
84
  return;
83
- const indexIds = indexBundlePrefixes(getDbPath());
84
- if (indexIds === undefined || indexIds.length === 0)
85
+ const indexedPaths = indexedBundlePaths(getDbPath());
86
+ if (indexedPaths === undefined || indexedPaths.length === 0)
85
87
  return; // nothing indexed yet — re-check later
86
88
  // A real comparison happened over a populated index: settle so the steady
87
89
  // state is one boolean check.
88
90
  guardSettled = true;
91
+ const indexIds = [...new Set(indexedPaths.map((row) => row.bundleId))];
89
92
  const indexIdSet = new Set(indexIds);
90
93
  const configuredMissingFromIndex = [...configIds].filter((id) => !indexIdSet.has(id));
91
94
  const indexedNotConfigured = indexIds.filter((id) => !configIds.has(id));
92
95
  if (configuredMissingFromIndex.length === 0 || indexedNotConfigured.length === 0)
93
96
  return;
97
+ // A mismatched id set alone is also the ordinary signature of adding a new
98
+ // bundle while stale rows from a removed bundle still exist. Treat it as a
99
+ // hand rename only when the old rows physically live beneath the newly named
100
+ // filesystem bundle's root. This keeps the guard useful without warning on
101
+ // unrelated new bundles (#971).
102
+ const renamedConfiguredIds = configuredMissingFromIndex.filter((configuredId) => {
103
+ const configuredPath = config.bundles?.[configuredId]?.path;
104
+ if (typeof configuredPath !== "string")
105
+ return false;
106
+ const root = path.resolve(configuredPath);
107
+ return indexedPaths.some((row) => {
108
+ if (!indexedNotConfigured.includes(row.bundleId))
109
+ return false;
110
+ const relative = path.relative(root, path.resolve(row.filePath));
111
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
112
+ });
113
+ });
114
+ if (renamedConfiguredIds.length === 0)
115
+ return;
116
+ const renamedOldIds = [
117
+ ...new Set(indexedPaths
118
+ .filter((row) => {
119
+ if (!indexedNotConfigured.includes(row.bundleId))
120
+ return false;
121
+ return renamedConfiguredIds.some((configuredId) => {
122
+ const configuredPath = config.bundles?.[configuredId]?.path;
123
+ if (typeof configuredPath !== "string")
124
+ return false;
125
+ const relative = path.relative(path.resolve(configuredPath), path.resolve(row.filePath));
126
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
127
+ });
128
+ })
129
+ .map((row) => row.bundleId)),
130
+ ];
94
131
  warn("WARNING: bundle identity drift detected. " +
95
- `Configured bundle(s) with no indexed content: ${configuredMissingFromIndex.map((id) => `"${id}"`).join(", ")}; ` +
96
- `indexed content under unconfigured bundle id(s): ${indexedNotConfigured.map((id) => `"${id}"`).join(", ")}. ` +
132
+ `Configured bundle(s) with no indexed content: ${renamedConfiguredIds.map((id) => `"${id}"`).join(", ")}; ` +
133
+ `indexed content under unconfigured bundle id(s): ${renamedOldIds.map((id) => `"${id}"`).join(", ")}. ` +
97
134
  "This is the signature of a hand-renamed bundle id (spec §11.5). AKM will NOT silently re-mint fresh state " +
98
135
  "under the new id. There is no rekey command in 0.9.0: either restore the previous bundle id in config.json " +
99
136
  "to reattach the existing rows, or keep the new id and run `akm index --full` to re-mint under it, accepting " +
@@ -1641,7 +1641,7 @@ function verifyIndexState(db, config, embeddableEntries, embeddingResult) {
1641
1641
  message: `Semantic search ready (${embeddingCount}/${embeddableEntries} embeddings, ${vecActive
1642
1642
  ? "sqlite-vec active"
1643
1643
  : vecAvailable
1644
- ? "JS fallback active — vec fast path degraded, run 'akm index --full' to restore"
1644
+ ? "JS fallback active — vec fast path degraded; run 'akm index' to repair valid stored vectors or 'akm index --reembed' to regenerate incompatible ones"
1645
1645
  : "JS fallback active"}).`,
1646
1646
  semanticSearchEnabled: true,
1647
1647
  semanticSearchMode: config.semanticSearchMode,
@@ -11,7 +11,8 @@ import { cosineSimilarity } from "../llm/embedders/types.js";
11
11
  import { purgeEmbeddingSalvage, relabelEmbeddingSalvageFingerprint, reuseSalvagedEmbeddings, } from "../storage/repositories/embedding-salvage-repository.js";
12
12
  import { getEmbeddableEntryCount } from "../storage/repositories/index-entries-repository.js";
13
13
  import { deleteMeta, getMeta, setMeta } from "../storage/repositories/index-meta-repository.js";
14
- import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, sampleEmbeddedEntriesForCanary, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
14
+ import { EMBEDDING_DIM } from "../storage/repositories/index-schema.js";
15
+ import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, repairVecFastPath, sampleEmbeddedEntriesForCanary, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
15
16
  import { reclassifyIndexDbContention } from "./index-db-contention.js";
16
17
  /** Identifies the embedding provider+model+dimension a stored vector was generated with. */
17
18
  export function deriveSemanticProviderFingerprint(embedding) {
@@ -360,6 +361,19 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
360
361
  }
361
362
  try {
362
363
  throwIfAborted(signal);
364
+ if (entryIds === undefined && (!vecFastPathWasReady || !isVecFastPathComplete(db))) {
365
+ const storedDim = Number(getMeta(db, "embeddingDim"));
366
+ const expectedDim = Number.isInteger(storedDim) && storedDim > 0 ? storedDim : (config.embedding?.dimension ?? EMBEDDING_DIM);
367
+ const repair = repairVecFastPath(db, expectedDim);
368
+ if (repair.available &&
369
+ (repair.repaired > 0 || repair.removedOrphans > 0 || repair.rejected > 0 || repair.error !== undefined)) {
370
+ const detail = repair.error ? `; repair stopped: ${repair.error}` : "";
371
+ onProgress({
372
+ phase: "embeddings",
373
+ message: `[embed] Repaired ${repair.repaired} missing sqlite-vec row${repair.repaired === 1 ? "" : "s"}; removed ${repair.removedOrphans} orphan${repair.removedOrphans === 1 ? "" : "s"}; ${repair.rejected} rejected${detail}.`,
374
+ });
375
+ }
376
+ }
363
377
  const allEntries = getAllEntriesForEmbedding(db, targetEntryIds);
364
378
  let vecFailedCount = 0;
365
379
  let vecUnavailableCount = 0;
@@ -4,7 +4,8 @@
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { isWithin, resolveStashDir } from "../../core/common.js";
7
- import { bundleComponentConfig, bundlesToSourceEntries, getSources, loadConfig } from "../../core/config/config.js";
7
+ import { bundleComponentConfig, bundleKeyForContentRoot, bundlesToSourceEntries, getSources, isBundleEnabled, loadConfig, } from "../../core/config/config.js";
8
+ import { ConfigError } from "../../core/errors.js";
8
9
  import { getUnresolvedSourcesDir } from "../../core/paths.js";
9
10
  import { resolveGitContentRoot, resolveWritable } from "../../core/write-source.js";
10
11
  import { lockContentRootFor } from "../../integrations/lockfile.js";
@@ -23,7 +24,7 @@ import { warn } from "../../core/warn.js";
23
24
  * 2. The configured `defaultBundle`, after component-root validation.
24
25
  * 3. Remaining configured bundles in installation-priority order.
25
26
  *
26
- * Disabled entries (`enabled: false`) are filtered after deduplication.
27
+ * Disabled entries (`enabled: false`) are excluded before materialization.
27
28
  * Missing configured roots remain in the result so the
28
29
  * indexer can classify their scan as incomplete instead of mistaking them for
29
30
  * removed sources.
@@ -39,23 +40,39 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
39
40
  : config.defaultBundle
40
41
  ? undefined
41
42
  : resolveStashDir();
43
+ const implicitConfiguredBundle = implicitStashDir ? bundleKeyForContentRoot(config, implicitStashDir) : undefined;
44
+ if (implicitConfiguredBundle && !isBundleEnabled(config, implicitConfiguredBundle)) {
45
+ throw new ConfigError(`The requested working source is disabled as bundle ${JSON.stringify(implicitConfiguredBundle)}.`, "INVALID_CONFIG_FILE");
46
+ }
42
47
  // Explicit and environment overrides stay first. Without either override,
43
48
  // the configured default enters through the validated loop below.
44
- const sources = implicitStashDir ? [{ path: implicitStashDir, writable: true }] : [];
45
- const seen = new Set(implicitStashDir ? [implicitStashDir] : []);
46
- const addSource = (dir, registryId, writable, type, adapterId, unresolved = false) => {
49
+ const sources = implicitStashDir ? [{ path: implicitStashDir, writable: true, isDefault: true }] : [];
50
+ const sourceIdentity = (dir) => {
51
+ const resolved = path.resolve(dir);
52
+ try {
53
+ return fs.realpathSync.native(resolved);
54
+ }
55
+ catch {
56
+ return resolved;
57
+ }
58
+ };
59
+ const seen = new Set(implicitStashDir ? [sourceIdentity(implicitStashDir)] : []);
60
+ const addSource = (dir, registryId, writable, type, adapterId, unresolved = false, isDefault = false) => {
47
61
  const resolved = path.resolve(dir);
48
- if (seen.has(resolved)) {
62
+ const identity = sourceIdentity(resolved);
63
+ if (seen.has(identity)) {
49
64
  // Already in the source list — typically the primary stash injected at
50
65
  // sources[0] before this loop. Enrich that entry with whatever metadata
51
66
  // the matching config source carries so `--from <config-name>` can
52
67
  // find it via registryId. Without this, the primary stash entry stays
53
68
  // identity-less and a user-named primary source ("name": "my-stash")
54
69
  // would validate but match zero entries when filtering.
55
- const existing = sources.find((s) => s.path === resolved);
70
+ const existing = sources.find((source) => sourceIdentity(source.path) === identity);
56
71
  if (existing && existing.type === undefined) {
57
72
  if (registryId)
58
73
  existing.registryId = registryId;
74
+ if (isDefault)
75
+ existing.isDefault = true;
59
76
  existing.type = type;
60
77
  existing.writable = writable;
61
78
  existing.adapterId = adapterId;
@@ -64,13 +81,14 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
64
81
  existing.unresolved = true;
65
82
  return;
66
83
  }
67
- seen.add(resolved);
84
+ seen.add(identity);
68
85
  if (isSuspiciousStashRoot(dir)) {
69
86
  warn(`Warning: stash root "${dir}" appears to be a system directory. This may be unintentional.`);
70
87
  }
71
88
  sources.push({
72
89
  path: resolved,
73
90
  ...(registryId ? { registryId } : {}),
91
+ ...(isDefault ? { isDefault: true } : {}),
74
92
  writable,
75
93
  type,
76
94
  ...(adapterId ? { adapterId } : {}),
@@ -88,17 +106,17 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
88
106
  const contentRoot = resolveEntryContentDir(entry);
89
107
  if (contentRoot == null) {
90
108
  const unresolvedPath = path.join(getUnresolvedSourcesDir(implicitStashDir ?? process.cwd()), entry.name ?? entry.type);
91
- addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
109
+ addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true, entry.name === config.defaultBundle);
92
110
  continue;
93
111
  }
94
112
  const dir = path.resolve(contentRoot, component?.root ?? ".");
95
113
  if (!isWithin(dir, contentRoot)) {
96
114
  warn(`Warning: component root "${component?.root}" escapes bundle "${entry.name}"; skipping source.`);
97
115
  const unresolvedPath = path.join(getUnresolvedSourcesDir(contentRoot), entry.name ?? entry.type);
98
- addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true);
116
+ addSource(unresolvedPath, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, true, entry.name === config.defaultBundle);
99
117
  continue;
100
118
  }
101
- addSource(dir, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter);
119
+ addSource(dir, entry.name, component?.writable ?? resolveWritable(entry), entry.type, component?.adapter, false, entry.name === config.defaultBundle);
102
120
  }
103
121
  return sources;
104
122
  }
@@ -581,8 +581,9 @@ function lowerLlm(request, base) {
581
581
  (typeof tools === "object" && tools !== null && !Array.isArray(tools) && Object.keys(tools).length === 0);
582
582
  if (empty)
583
583
  translated.add("tools");
584
- else
585
- reject("tools");
584
+ else {
585
+ throw new ConfigError("The direct LLM transport cannot enforce the resolved tool policy.", "INVALID_CONFIG_FILE");
586
+ }
586
587
  }
587
588
  if (own(request.runtime, "workspace")) {
588
589
  if (request.runtime.workspace === null || request.runtime.workspace === "")
@@ -9,6 +9,37 @@ import { loadModelMap } from "./model-map.js";
9
9
  function own(value, key) {
10
10
  return Object.hasOwn(value, key);
11
11
  }
12
+ function requestedToolNames(tools) {
13
+ if (tools === null)
14
+ return Object.freeze([]);
15
+ if (typeof tools === "string") {
16
+ return Object.freeze(tools
17
+ .split(",")
18
+ .map((tool) => tool.trim())
19
+ .filter(Boolean));
20
+ }
21
+ if (Array.isArray(tools))
22
+ return Object.freeze(tools.map((tool) => tool.trim()).filter(Boolean));
23
+ const structured = tools;
24
+ // The portable structured spelling is a boolean allow/deny map. Opaque or
25
+ // nested provider policy cannot be reduced to tool names without guessing,
26
+ // so a host name allowlist must deny it (an explicit "*" may still allow it).
27
+ if (Object.values(structured).some((value) => typeof value !== "boolean"))
28
+ return undefined;
29
+ return Object.freeze(Object.keys(structured).filter((tool) => structured[tool] === true));
30
+ }
31
+ /** The production authorizer: executable assets may only narrow a host-owned ceiling. */
32
+ export function toolAuthorizerFromConfig(config) {
33
+ const allowed = new Set(config.execution?.allowedTools ?? []);
34
+ return (input) => {
35
+ const requested = requestedToolNames(input.tools);
36
+ const permitted = allowed.has("*") || requested?.every((tool) => allowed.has(tool)) === true;
37
+ return Object.freeze({
38
+ status: permitted ? "allowed" : "denied",
39
+ policy: "config-execution-allowed-tools",
40
+ });
41
+ };
42
+ }
12
43
  /** Pure common cascade composition once caller-specific sources are rendered. */
13
44
  export function planPreparedExecution(options) {
14
45
  return planExecutionCascade({
@@ -56,7 +87,7 @@ export function prepareResolvedExecution(options) {
56
87
  engines: executionEngineDefinitionsFromConfig(config),
57
88
  modelMap: options.modelMap ?? loadModelMap({ engines: config.engines }).map,
58
89
  invocationKind: options.invocationKind,
59
- ...(options.authorizeTools ? { authorizeTools: options.authorizeTools } : {}),
90
+ authorizeTools: options.authorizeTools ?? toolAuthorizerFromConfig(config),
60
91
  });
61
92
  return Object.freeze({
62
93
  plan,
@@ -324,7 +324,7 @@ export function buildReflectPrompt(input) {
324
324
  sections.push(lines.join("\n"));
325
325
  }
326
326
  if (input.avoidPatterns && input.avoidPatterns.length > 0) {
327
- sections.push(`## Avoid These Patterns\nPrevious assets in this run produced these errors — do not repeat them:\n${input.avoidPatterns.map((e) => `- ${e}`).join("\n")}`);
327
+ sections.push(`## Avoid These Patterns\nRun-only guidance: do not copy this heading, explanation, or any diagnostic below into the proposed asset.\nPrevious assets in this run produced these errors — do not repeat them:\n${input.avoidPatterns.map((e) => `- ${e}`).join("\n")}`);
328
328
  }
329
329
  // R-1 / #372: Self-Refine (arXiv:2303.17651) — inject prior draft as critique target.
330
330
  // On refinement iterations (iter > 0), the agent is shown its previous proposal
@@ -162,8 +162,9 @@ export function createAgentRequestLowerer(options) {
162
162
  const tools = request.tools;
163
163
  if (translatesTools(options.tools, tools))
164
164
  translated.add("tools");
165
- else
166
- reject("tools");
165
+ else {
166
+ throw new ConfigError(`The ${options.adapter} transport cannot enforce the resolved tool policy.`, "INVALID_CONFIG_FILE");
167
+ }
167
168
  dispatch.tools = tools;
168
169
  }
169
170
  if (own(request.runtime, "settings")) {
@@ -444,11 +444,12 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
444
444
  * attempts the schema request fresh every time and falls back once per call
445
445
  * on a 4xx — see the in-memory tracker below.
446
446
  */
447
- export async function probeLlmReachable(config) {
447
+ export async function probeLlmReachable(config, timeoutMs) {
448
448
  try {
449
449
  const raw = await chatCompletion(config, [{ role: "user", content: "Respond with just the word: ok" }], {
450
450
  maxTokens: 16,
451
451
  temperature: 0,
452
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
452
453
  });
453
454
  return raw.length > 0 ? { reachable: true } : { reachable: false, error: "empty response" };
454
455
  }
@@ -71,7 +71,9 @@ const PASSTHROUGH_COMMANDS = [
71
71
  "setup",
72
72
  "sync",
73
73
  "task-add",
74
+ "task-disable",
74
75
  "task-doctor",
76
+ "task-enable",
75
77
  "task-explain",
76
78
  "task-history",
77
79
  "task-prune",
@@ -11,6 +11,29 @@ import { NotFoundError, UsageError } from "../core/errors.js";
11
11
  import { asRecord, asString, GITHUB_API_BASE, githubHeaders } from "../integrations/github.js";
12
12
  import { cancelRegistryResponse, fetchRegistryResponse } from "./network.js";
13
13
  import { isExactSemver, isSemverRange, maxSatisfying } from "./semver.js";
14
+ /**
15
+ * Pass an HTTPS bearer credential to Git without placing it in the remote URL
16
+ * or argv. The value exists only in the child-process environment and is never
17
+ * persisted or included in diagnostics.
18
+ */
19
+ export function gitCredentialEnvironment(credential) {
20
+ if (!credential)
21
+ return { ...process.env };
22
+ if (/[\r\n\0]/.test(credential)) {
23
+ throw new UsageError("Git credential contains an invalid control character.");
24
+ }
25
+ const configuredCount = process.env.GIT_CONFIG_COUNT;
26
+ const nextIndex = configuredCount === undefined ? 0 : Number(configuredCount);
27
+ if (!Number.isSafeInteger(nextIndex) || nextIndex < 0) {
28
+ throw new UsageError("GIT_CONFIG_COUNT must be a non-negative integer before a Git credential can be added.");
29
+ }
30
+ return {
31
+ ...process.env,
32
+ GIT_CONFIG_COUNT: String(nextIndex + 1),
33
+ [`GIT_CONFIG_KEY_${nextIndex}`]: "http.extraHeader",
34
+ [`GIT_CONFIG_VALUE_${nextIndex}`]: `Authorization: Bearer ${credential}`,
35
+ };
36
+ }
14
37
  /**
15
38
  * Validate that a URL is safe to pass to git.
16
39
  * Allowlists https:, http:, ssh:, git: schemes and git@ SSH shorthand.
@@ -122,16 +145,16 @@ function detectRegistrySearchId(ref) {
122
145
  lines.push("Or search for the installable ref:", ` akm search "${segments.length > 2 ? segments[segments.length - 1] : rest}" --from registry`, "Then install using the installRef value from the result:", " akm bundle add github:owner/repo", " akm bundle add npm:package-name");
123
146
  return lines.join("\n");
124
147
  }
125
- export async function resolveRegistryArtifact(parsed) {
148
+ export async function resolveRegistryArtifact(parsed, options) {
126
149
  switch (parsed.source) {
127
150
  case "npm":
128
151
  return resolveNpmArtifact(parsed);
129
152
  case "local":
130
153
  return resolveLocalArtifact(parsed);
131
154
  case "git":
132
- return resolveGitArtifact(parsed);
155
+ return resolveGitArtifact(parsed, options?.gitCredential);
133
156
  case "github":
134
- return resolveGithubArtifact(parsed);
157
+ return resolveGithubArtifact(parsed, options?.gitCredential);
135
158
  }
136
159
  }
137
160
  function parseNpmRef(input, originalRef) {
@@ -427,12 +450,12 @@ async function resolveNpmArtifact(parsed) {
427
450
  allowPrivateRegistryOrigin: npmPolicy.allowPrivateRegistryOrigin,
428
451
  };
429
452
  }
430
- async function resolveGithubArtifact(parsed) {
453
+ async function resolveGithubArtifact(parsed, credential) {
431
454
  const gitUrl = `https://github.com/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}.git`;
432
455
  const repoBase = `${GITHUB_API_BASE}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`;
433
456
  // Prefer git-backed installs so private GitHub repos work with the user's
434
457
  // normal git credential helper rather than requiring API-specific auth.
435
- const gitResolvedRevision = resolveGitRevisionFromRemote(gitUrl, parsed.requestedRef);
458
+ const gitResolvedRevision = resolveGitRevisionFromRemote(gitUrl, parsed.requestedRef, credential);
436
459
  if (gitResolvedRevision) {
437
460
  return {
438
461
  id: parsed.id,
@@ -443,7 +466,7 @@ async function resolveGithubArtifact(parsed) {
443
466
  resolvedRevision: gitResolvedRevision,
444
467
  };
445
468
  }
446
- const headers = githubHeaders();
469
+ const headers = { ...githubHeaders(), ...(credential ? { Authorization: `Bearer ${credential}` } : {}) };
447
470
  if (parsed.requestedRef) {
448
471
  const commit = await tryFetchJson(`${repoBase}/commits/${encodeURIComponent(parsed.requestedRef)}`, headers, GITHUB_API_POLICY);
449
472
  const resolvedRevision = asString(commit?.sha) ?? parsed.requestedRef;
@@ -485,19 +508,23 @@ async function resolveGithubArtifact(parsed) {
485
508
  resolvedRevision: asString(commit?.sha) ?? defaultBranch,
486
509
  };
487
510
  }
488
- function resolveGitRevisionFromRemote(url, requestedRef) {
511
+ function resolveGitRevisionFromRemote(url, requestedRef, credential) {
489
512
  validateGitUrl(url);
490
513
  const ref = requestedRef ?? "HEAD";
491
514
  if (requestedRef)
492
515
  validateGitRef(requestedRef);
493
- const result = spawnSync("git", ["ls-remote", url, ref], { encoding: "utf8", timeout: 30_000 });
516
+ const result = spawnSync("git", ["ls-remote", url, ref], {
517
+ encoding: "utf8",
518
+ timeout: 30_000,
519
+ env: gitCredentialEnvironment(credential),
520
+ });
494
521
  if (result.status !== 0)
495
522
  return undefined;
496
523
  const firstLine = result.stdout.trim().split(/\r?\n/)[0];
497
524
  return firstLine?.split(/\s/)[0] || undefined;
498
525
  }
499
- async function resolveGitArtifact(parsed) {
500
- const resolvedRevision = resolveGitRevisionFromRemote(parsed.url, parsed.requestedRef);
526
+ async function resolveGitArtifact(parsed, credential) {
527
+ const resolvedRevision = resolveGitRevisionFromRemote(parsed.url, parsed.requestedRef, credential);
501
528
  if (!resolvedRevision) {
502
529
  // Unlike the GitHub path (which falls back to the REST API on a failed
503
530
  // `git ls-remote`), a plain git source has no fallback resolver — an