akm-cli 0.9.16-alpha.1 → 0.9.16

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 (147) hide show
  1. package/CHANGELOG.md +56 -132
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/core/index-refresh.yml +1 -1
  4. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  5. package/dist/cli/retired-commands.js +0 -4
  6. package/dist/cli/unknown-flags.js +3 -36
  7. package/dist/commands/env/env-binding.js +4 -4
  8. package/dist/commands/env/env-cli.js +3 -3
  9. package/dist/commands/improve/collapse-detector.js +2 -2
  10. package/dist/commands/improve/consolidate.js +4 -6
  11. package/dist/commands/improve/improve-cli.js +20 -15
  12. package/dist/commands/improve/reflect.js +23 -2
  13. package/dist/commands/lint/base-linter.js +9 -0
  14. package/dist/commands/lint/env-key-rules.js +2 -2
  15. package/dist/commands/proposal/propose.js +15 -1
  16. package/dist/commands/proposal/repository.js +3 -12
  17. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  18. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  19. package/dist/commands/read/curate.js +44 -34
  20. package/dist/commands/read/search.js +35 -54
  21. package/dist/commands/read/show.js +21 -2
  22. package/dist/commands/registry-cli.js +5 -5
  23. package/dist/commands/sources/add-cli.js +59 -16
  24. package/dist/commands/sources/bundle-cli.js +35 -11
  25. package/dist/commands/sources/bundle-config-ops.js +30 -0
  26. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  27. package/dist/commands/sources/info.js +8 -8
  28. package/dist/commands/sources/installed-stashes.js +55 -61
  29. package/dist/commands/sources/source-add.js +39 -38
  30. package/dist/commands/sources/source-manage.js +34 -12
  31. package/dist/commands/sources/stash-cli.js +111 -119
  32. package/dist/commands/sources/stash-skeleton.js +6 -3
  33. package/dist/commands/tasks/explain.js +4 -1
  34. package/dist/commands/tasks/tasks-cli.js +31 -9
  35. package/dist/commands/tasks/tasks.js +239 -194
  36. package/dist/commands/tasks/validate.js +20 -32
  37. package/dist/core/activation-policy.js +4 -4
  38. package/dist/core/adapter/adapters/akm-adapter.js +8 -35
  39. package/dist/core/adapter/adapters/akm-metadata.js +1 -11
  40. package/dist/core/adapter/execution-source.js +10 -29
  41. package/dist/core/asset/asset-placement.js +0 -35
  42. package/dist/core/config/config-schema.js +64 -8
  43. package/dist/core/config/config-sources.js +96 -2
  44. package/dist/core/config/config.js +190 -24
  45. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  46. package/dist/core/config/schema/embedding.js +30 -7
  47. package/dist/core/config/schema/execution.js +23 -0
  48. package/dist/core/config/schema/experimental.js +1 -1
  49. package/dist/core/config/schema/scheduler.js +20 -0
  50. package/dist/core/config/schema/search.js +10 -12
  51. package/dist/core/config/schema/sources-bundles.js +32 -1
  52. package/dist/core/content-safety.js +52 -0
  53. package/dist/core/errors.js +2 -5
  54. package/dist/core/maintenance-barrier.js +11 -13
  55. package/dist/core/paths.js +11 -0
  56. package/dist/core/run-lock.js +2 -5
  57. package/dist/core/state/migrations.js +1 -26
  58. package/dist/core/state-db.js +27 -63
  59. package/dist/core/type-presentation.js +1 -1
  60. package/dist/core/write-source.js +13 -8
  61. package/dist/indexer/bundle-identity-guard.js +45 -8
  62. package/dist/indexer/ensure-index.js +0 -5
  63. package/dist/indexer/index-db-contention.js +56 -0
  64. package/dist/indexer/index-rebuild-lock.js +73 -0
  65. package/dist/indexer/index-written-assets.js +171 -133
  66. package/dist/indexer/indexer.js +1621 -458
  67. package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
  68. package/dist/indexer/materialize-embeddings.js +785 -0
  69. package/dist/indexer/passes/dir-staleness.js +161 -0
  70. package/dist/indexer/passes/metadata.js +1 -18
  71. package/dist/indexer/scan/drain-dir.js +70 -27
  72. package/dist/indexer/search/db-search.js +89 -373
  73. package/dist/indexer/search/ranking-contributors.js +16 -21
  74. package/dist/indexer/search/ranking.js +57 -135
  75. package/dist/indexer/search/search-source.js +29 -11
  76. package/dist/integrations/agent/execution-lowering.js +3 -2
  77. package/dist/integrations/agent/execution-preparation.js +32 -1
  78. package/dist/integrations/agent/prompts.js +1 -1
  79. package/dist/integrations/agent/request-lowering.js +3 -2
  80. package/dist/llm/client.js +3 -11
  81. package/dist/llm/embedder.js +3 -10
  82. package/dist/llm/embedders/remote.js +104 -133
  83. package/dist/llm/feature-gate.js +2 -4
  84. package/dist/llm/rerank-client.js +3 -3
  85. package/dist/output/html-render.js +2 -1
  86. package/dist/output/shapes/passthrough.js +2 -1
  87. package/dist/output/stdout.js +24 -0
  88. package/dist/output/text/command-format.js +13 -19
  89. package/dist/output/text/helpers.js +1 -1
  90. package/dist/output/text/index.js +2 -5
  91. package/dist/output/text.js +4 -3
  92. package/dist/registry/resolve.js +37 -10
  93. package/dist/scripts/akm-migrate-node.js +15197 -11351
  94. package/dist/scripts/akm-migrate.js +15514 -11668
  95. package/dist/setup/semantic-assets.js +2 -2
  96. package/dist/setup/setup.js +3 -3
  97. package/dist/setup/steps/connection.js +2 -3
  98. package/dist/setup/steps/tasks.js +29 -36
  99. package/dist/sources/providers/git-install.js +17 -11
  100. package/dist/sources/providers/git-provider.js +12 -5
  101. package/dist/sources/providers/git-stash.js +38 -16
  102. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  103. package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
  104. package/dist/storage/repositories/index-connection.js +3 -1
  105. package/dist/storage/repositories/index-entries-repository.js +68 -77
  106. package/dist/storage/repositories/index-entry-schema.js +25 -16
  107. package/dist/storage/repositories/index-fts-repository.js +263 -29
  108. package/dist/storage/repositories/index-meta-repository.js +29 -0
  109. package/dist/storage/repositories/index-schema.js +122 -115
  110. package/dist/storage/repositories/index-utility-repository.js +1 -1
  111. package/dist/storage/repositories/index-vec-repository.js +435 -22
  112. package/dist/tasks/activation-config.js +90 -0
  113. package/dist/tasks/backends/cron.js +9 -0
  114. package/dist/tasks/backends/launchd.js +1 -0
  115. package/dist/tasks/backends/schtasks.js +2 -0
  116. package/dist/tasks/embedded.js +4 -5
  117. package/dist/tasks/scheduler-binding.js +2 -2
  118. package/dist/tasks/scheduler-sync-preview.js +8 -1
  119. package/dist/tasks/scheduler-sync.js +19 -10
  120. package/dist/tasks/source/parse-task-source.js +10 -113
  121. package/dist/tasks/source/project-v4.js +2 -2
  122. package/dist/tasks/source/task-source-v4.js +4 -12
  123. package/dist/tasks/source/task-to-v3.js +4 -12
  124. package/dist/tasks/source/task-to-v4.js +40 -7
  125. package/docs/migration/README.md +1 -0
  126. package/docs/migration/release-notes/0.9.15.md +36 -34
  127. package/docs/migration/release-notes/0.9.16.md +60 -98
  128. package/docs/migration/release-notes/README.md +0 -5
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  130. package/docs/reference/cli.md +124 -122
  131. package/docs/reference/configuration.md +137 -133
  132. package/docs/reference/data-and-telemetry.md +1 -2
  133. package/docs/reference/tasks.md +34 -29
  134. package/package.json +1 -1
  135. package/schemas/akm-config.json +170 -6
  136. package/schemas/akm-task.json +1 -2
  137. package/dist/commands/sources/index-status.js +0 -99
  138. package/dist/core/hash.js +0 -18
  139. package/dist/indexer/drain.js +0 -306
  140. package/dist/indexer/embedding-identity.js +0 -20
  141. package/dist/indexer/enrich.js +0 -260
  142. package/dist/indexer/reconcile.js +0 -890
  143. package/dist/indexer/scan/parse-file.js +0 -66
  144. package/dist/indexer/units/unit.js +0 -159
  145. package/dist/llm/embedders/provider-limits.js +0 -288
  146. package/dist/storage/repositories/files-repository.js +0 -181
  147. package/dist/storage/repositories/units-repository.js +0 -510
@@ -6,7 +6,8 @@ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
6
6
  import { isRemoteUrl } from "../../core/common.js";
7
7
  import { bundleEntryToSourceEntry, bundlesToSourceEntries, getSources, mutateConfig } from "../../core/config/config.js";
8
8
  import { ConfigError, UsageError } from "../../core/errors.js";
9
- import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey } from "./bundle-config-ops.js";
9
+ import { revokeSchedulerActivationsForBundle } from "../../tasks/activation-config.js";
10
+ import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey, placeBundle, } from "./bundle-config-ops.js";
10
11
  // ── Operations ──────────────────────────────────────────────────────────────
11
12
  /**
12
13
  * Add a stash source (filesystem path or remote provider URL) to config.
@@ -16,13 +17,16 @@ import { bundleKeyForPath, bundleKeyForUrl, nextBundleKey } from "./bundle-confi
16
17
  * (e.g. "website", "git").
17
18
  */
18
19
  export function addStash(opts) {
19
- const { target, name, providerType, options: providerOptions, writable } = opts;
20
+ const { target, name, providerType, options: providerOptions, writable, credential, before, after } = opts;
20
21
  if (providerType === "openviking") {
21
22
  throw new ConfigError("openviking is not supported in akm v1.", "INVALID_CONFIG_FILE");
22
23
  }
23
24
  if (writable === true && providerType && providerType !== "filesystem" && providerType !== "git") {
24
25
  throw new ConfigError("writable: true is only supported on filesystem and git sources", "INVALID_CONFIG_FILE");
25
26
  }
27
+ if (credential && providerType !== "git") {
28
+ throw new ConfigError("credential is only supported on git sources", "INVALID_CONFIG_FILE");
29
+ }
26
30
  let result;
27
31
  const targetIsUrl = isRemoteUrl(target);
28
32
  if (targetIsUrl && !providerType) {
@@ -53,7 +57,15 @@ export function addStash(opts) {
53
57
  return config;
54
58
  }
55
59
  key = nextBundleKey(bundles, name, target);
56
- bundles[key] = urlBundleDescriptor(providerType, target, providerOptions, writable === true);
60
+ const entry = urlBundleDescriptor(providerType, target, providerOptions, writable === true, credential);
61
+ const nextBundles = placeBundle(bundles, key, entry, { before, after });
62
+ const next = { ...config, bundles: nextBundles };
63
+ result = {
64
+ sources: bundlesToSourceEntries(next) ?? [],
65
+ added: true,
66
+ entry: bundleEntryToSourceEntry(key, entry),
67
+ };
68
+ return next;
57
69
  }
58
70
  else {
59
71
  const resolvedPath = path.resolve(target);
@@ -62,18 +74,22 @@ export function addStash(opts) {
62
74
  return config;
63
75
  }
64
76
  key = nextBundleKey(bundles, name, resolvedPath);
65
- bundles[key] = {
77
+ const entry = {
66
78
  path: resolvedPath,
67
79
  ...(writable === true ? { writable: true } : {}),
68
80
  components: {
69
81
  main: { root: ".", adapter: detectAdapterId(resolvedPath), writable: writable ?? true },
70
82
  },
71
83
  };
84
+ const nextBundles = placeBundle(bundles, key, entry, { before, after });
85
+ const next = { ...config, bundles: nextBundles };
86
+ result = {
87
+ sources: bundlesToSourceEntries(next) ?? [],
88
+ added: true,
89
+ entry: bundleEntryToSourceEntry(key, entry),
90
+ };
91
+ return next;
72
92
  }
73
- const next = { ...config, bundles };
74
- const entry = bundleEntryToSourceEntry(key, bundles[key]);
75
- result = { sources: bundlesToSourceEntries(next) ?? [], added: true, entry };
76
- return next;
77
93
  });
78
94
  return result;
79
95
  }
@@ -83,7 +99,7 @@ export function addStash(opts) {
83
99
  * package spec (e.g. "lodash", "@scope/pkg@^2") — never a URL (R-013,
84
100
  * rejected earlier in {@link addStash}).
85
101
  */
86
- function urlBundleDescriptor(providerType, locator, options, writable) {
102
+ function urlBundleDescriptor(providerType, locator, options, writable, credential) {
87
103
  if (providerType === "website") {
88
104
  // Website provider options ride on the (passthrough) website descriptor and
89
105
  // round-trip back to `entry.options` via bundleEntryToSourceEntry.
@@ -94,8 +110,9 @@ function urlBundleDescriptor(providerType, locator, options, writable) {
94
110
  }
95
111
  if (providerType === "npm")
96
112
  return { npm: locator };
97
- if (providerType === "git")
98
- return { git: locator, ...(writable ? { writable: true } : {}) };
113
+ if (providerType === "git") {
114
+ return { git: locator, ...(writable ? { writable: true } : {}), ...(credential ? { credential } : {}) };
115
+ }
99
116
  throw new ConfigError(`unsupported source type "${providerType}"; expected filesystem, git, website, or npm`, "INVALID_CONFIG_FILE");
100
117
  }
101
118
  /**
@@ -120,7 +137,12 @@ export function removeStash(target) {
120
137
  }
121
138
  const removed = bundleEntryToSourceEntry(key, bundles[key]);
122
139
  delete bundles[key];
123
- const next = { ...config, bundles: Object.keys(bundles).length > 0 ? bundles : undefined };
140
+ const next = revokeSchedulerActivationsForBundle({
141
+ ...config,
142
+ bundles: Object.keys(bundles).length > 0 ? bundles : undefined,
143
+ ...(config.defaultBundle === key ? { defaultBundle: undefined } : {}),
144
+ ...(config.defaultWriteTarget === key ? { defaultWriteTarget: undefined } : {}),
145
+ }, key);
124
146
  result = { sources: bundlesToSourceEntries(next) ?? [], removed: true, entry: removed };
125
147
  return next;
126
148
  });
@@ -28,9 +28,10 @@
28
28
  * SIGINT/SIGTERM handlers in a try/finally — left byte-for-byte untouched.
29
29
  */
30
30
  import path from "node:path";
31
+ import { defineCommand } from "citty";
31
32
  import * as p from "../../cli/clack.js";
32
33
  import { getParsedInvocation } from "../../cli/invocation.js";
33
- import { defineGroupCommand, defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, parseAllFlagValues, } from "../../cli/shared.js";
34
+ import { defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, parseAllFlagValues, runWithJsonErrors } from "../../cli/shared.js";
34
35
  import { assertFlatAssetName } from "../../core/asset/asset-create.js";
35
36
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
36
37
  import { isHttpUrl, resolveStashDir } from "../../core/common.js";
@@ -39,51 +40,17 @@ import { UsageError } from "../../core/errors.js";
39
40
  import { appendEvent } from "../../core/events.js";
40
41
  import { resolveBundleWriteTarget } from "../../core/mutation-target.js";
41
42
  import { getCacheDir } from "../../core/paths.js";
42
- import { clearLogFile, info, isVerbose, setLogFile, warn } from "../../core/warn.js";
43
+ import { clearLogFile, info, isVerbose, setLogFile } from "../../core/warn.js";
43
44
  import { resolveWriteTarget } from "../../core/write-source.js";
44
- import { DRAIN_BATCH_PROGRESS_PREFIX } from "../../indexer/drain.js";
45
+ import { releaseIndexRebuildLock, tryAcquireIndexRebuildLock } from "../../indexer/index-rebuild-lock.js";
45
46
  import { akmIndex } from "../../indexer/indexer.js";
46
- import { RECONCILE_ROOT_PROGRESS_PREFIX } from "../../indexer/reconcile.js";
47
47
  import { getHyphenatedBoolean, getOutputMode } from "../../output/context.js";
48
48
  import { inferAssetName, mergeXrefsIntoContent, readKnowledgeInput, resolveSupersedesForWrite, resolveSupersedesWriteTarget, resolveXrefsForWrite, writeMarkdownAsset, } from "../read/knowledge.js";
49
- import { assembleIndexStatus } from "./index-status.js";
50
49
  import { assembleInfo } from "./info.js";
51
- /**
52
- * The two high-frequency, one-line-per-unit-of-work progress lines (#954) —
53
- * drain's per-batch commit line and reconcile's per-root "done" line —
54
- * excluded from non-verbose, non-text (JSON/yaml/etc) stderr. Matched by the
55
- * exact prefix each producer exports, not a re-derived regex, so the two
56
- * never drift apart (index-redesign B5g): this used to be a regex tuned to
57
- * the deleted per-entry pipeline's `Embedded N/M entries.` line, which never
58
- * matched either replacement line, so every progress line reached stderr
59
- * regardless of `--verbose`.
60
- */
61
- function isDetailProgressLine(message) {
62
- return message.startsWith(DRAIN_BATCH_PROGRESS_PREFIX) || message.startsWith(RECONCILE_ROOT_PROGRESS_PREFIX);
63
- }
64
- export const indexStatusCommand = defineJsonCommand({
65
- meta: {
66
- name: "status",
67
- description: "Show index.db's current state: files, entries, unit coverage, and the last reconcile time.",
68
- },
69
- run() {
70
- output("index-status", assembleIndexStatus());
71
- },
72
- });
73
- /**
74
- * `akm index` = reconcile + drain (docs/plans/index-redesign.md). Still a raw
75
- * group command (not `defineJsonCommand`) because its default body owns a
76
- * spinner, an AbortController, and SIGINT/SIGTERM handlers in a try/finally;
77
- * `defineGroupCommand` gives it `status` as a real subcommand while keeping
78
- * that default body as plain `akm index`'s behavior (S-057 canonical
79
- * bare-group rule does not apply here — a bare `akm index` has always run the
80
- * indexer, and that stays).
81
- */
82
- export const indexCommand = defineGroupCommand({
83
- meta: {
84
- name: "index",
85
- description: "Reconcile the search index and drain the embedding queue (--full forces a full re-derivation)",
86
- },
50
+ /** Matches the high-frequency per-committed-batch progress line (#954), excluded from non-verbose JSON-mode stderr. */
51
+ const EMBEDDED_BATCH_PROGRESS_PATTERN = /^Embedded \d+\/\d+ entries\.$/;
52
+ export const indexCommand = defineCommand({
53
+ meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
87
54
  args: {
88
55
  // R-051: `index` is a raw `defineCommand` (not `defineJsonCommand`), so it
89
56
  // does not get `GLOBAL_OUTPUT_ARGS` for free. `--format`/`--detail`/
@@ -91,99 +58,124 @@ export const indexCommand = defineGroupCommand({
91
58
  // extra positional for a stray value to fall into), so this is purely a
92
59
  // `--help` visibility / consistency fix, not a behavior change.
93
60
  ...GLOBAL_OUTPUT_ARGS,
94
- full: {
61
+ full: { type: "boolean", description: "Force full reindex", default: false },
62
+ clean: {
63
+ type: "boolean",
64
+ description: "After indexing, remove any entries whose source file no longer exists on disk.",
65
+ default: false,
66
+ },
67
+ "dry-run": {
95
68
  type: "boolean",
96
- description: "Force every file to be re-derived (ignore the unchanged-file shortcut), reconciling in place — " +
97
- "existing rows keep their id/embeddings/utility scores; nothing is dropped first.",
69
+ description: "When combined with --clean, report stale entries without deleting them.",
98
70
  default: false,
99
71
  },
100
72
  reembed: {
101
73
  type: "boolean",
102
- description: "Drop the active embedding identity's vectors, then re-embed every unit from scratch.",
74
+ description: "Force re-embedding of every entry, bypassing the embedding-model-rename compatibility check.",
103
75
  default: false,
104
76
  },
105
77
  "skip-if-locked": {
106
78
  type: "boolean",
107
- description: "Deprecated, no effect. Index runs no longer take a rebuild lock (docs/plans/index-redesign.md) — " +
108
- "every write is a short, idempotent, content-addressed transaction, so two concurrent index runs " +
109
- "converge instead of contending. Kept only so existing scripts do not fail on an unknown flag.",
79
+ description: "If another `akm index` run already holds the rebuild lock, skip gracefully (exit 0) instead of contending with it. Use for scheduled/opportunistic index runs so they don't pile up against a longer run in progress.",
110
80
  default: false,
111
81
  },
112
82
  },
113
- subCommands: { status: indexStatusCommand },
114
- async defaultRun({ args }) {
115
- if (getHyphenatedBoolean(args, "enrich") || getParsedInvocation().getFlagValue("--enrich") !== undefined) {
116
- throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
117
- }
118
- if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
119
- throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
120
- }
121
- if (args["skip-if-locked"]) {
122
- warn("[index] --skip-if-locked is deprecated and has no effect — index runs no longer take a rebuild lock.");
123
- }
124
- const outputMode = getOutputMode();
125
- const controller = new AbortController();
126
- const abort = () => controller.abort(new Error("index interrupted"));
127
- process.once("SIGINT", abort);
128
- process.once("SIGTERM", abort);
129
- const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
130
- setLogFile(indexLogFile);
131
- const verbose = isVerbose();
132
- const spin = !verbose && outputMode.format === "text" ? p.spinner() : null;
133
- if (spin) {
134
- spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
135
- }
136
- let latestMessage = "";
137
- // Resolve the stash dir once at the `akm index` command boundary and
138
- // thread it into the indexer (WI-9.10 CLI-wide sweep) — the indexer leaf
139
- // no longer reads the ambient `resolveStashDir()`.
140
- const stashDir = resolveStashDir();
141
- try {
142
- const result = await akmIndex({
143
- stashDir,
144
- full: args.full,
145
- reembed: args.reembed,
146
- onProgress: ({ phase, message, processed, total }) => {
147
- latestMessage = message;
148
- const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
149
- if (verbose) {
150
- info(`[index:${phase}] ${progressPrefix}${message}`);
151
- }
152
- else if (spin) {
153
- spin.stop(`${progressPrefix}${message}`);
154
- spin.start(`${progressPrefix}${message}`);
155
- }
156
- else if (!isDetailProgressLine(message)) {
157
- // Non-verbose, non-text (JSON/yaml/etc) mode: silence used to be
158
- // total until the run finished (#954) — a stalled
159
- // run looked identical to "nothing written". Phase-start
160
- // messages, the credential diagnostic, and the reconcile/drain
161
- // totals now reach stderr here too; the high-frequency
162
- // per-root `Reconciled "…"` and per-batch `[drain] batch N: …`
163
- // lines are deliberately excluded — that would be spam, not a
164
- // heartbeat. `--verbose` (the `if` branch above) still gets
165
- // every one of them.
166
- info(`[index:${phase}] ${progressPrefix}${message}`);
167
- }
168
- },
169
- signal: controller.signal,
170
- });
171
- if (spin) {
172
- spin.stop(`Indexed ${result.totalEntries} assets.`);
83
+ async run({ args }) {
84
+ await runWithJsonErrors(async () => {
85
+ if (getHyphenatedBoolean(args, "enrich") || getParsedInvocation().getFlagValue("--enrich") !== undefined) {
86
+ throw new UsageError("`akm index --enrich` has been removed. Plain `akm index` now performs metadata enrichment by default.");
173
87
  }
174
- output("index", result);
175
- }
176
- catch (error) {
88
+ if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
89
+ throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
90
+ }
91
+ // #956: opt-in, non-blocking rebuild lock — never gates a human-typed
92
+ // `akm index` (it only warns and contends), but a scheduled/opportunistic
93
+ // caller can pass --skip-if-locked to step aside instead of piling up
94
+ // behind a run already in progress. Acquired before any other side
95
+ // effect (log file, spinner) so a skip does neither.
96
+ const lockAcquisition = tryAcquireIndexRebuildLock(args["skip-if-locked"]);
97
+ if (lockAcquisition.state === "skipped") {
98
+ output("index", {
99
+ ok: true,
100
+ skipped: {
101
+ reason: "lock-held",
102
+ pid: lockAcquisition.holder.pid,
103
+ // #956: the launcher pid (when known) alongside the pid that
104
+ // actually holds the lock — every process listing and task log
105
+ // shows the launcher pid, not the bun/node child's.
106
+ launcherPid: lockAcquisition.holder.launcherPid,
107
+ startedAt: lockAcquisition.holder.startedAt,
108
+ },
109
+ });
110
+ return;
111
+ }
112
+ const outputMode = getOutputMode();
113
+ const controller = new AbortController();
114
+ const abort = () => controller.abort(new Error("index interrupted"));
115
+ process.once("SIGINT", abort);
116
+ process.once("SIGTERM", abort);
117
+ const indexLogFile = path.join(getCacheDir(), "logs", "index", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
118
+ setLogFile(indexLogFile);
119
+ const verbose = isVerbose();
120
+ const spin = !verbose && outputMode.format === "text" ? p.spinner() : null;
177
121
  if (spin) {
178
- spin.stop(latestMessage ? `Indexing failed after: ${latestMessage}` : "Indexing failed.");
122
+ spin.start(`Building search index${args.full ? " (full rebuild)" : ""}...`);
179
123
  }
180
- throw error;
181
- }
182
- finally {
183
- clearLogFile();
184
- process.off("SIGINT", abort);
185
- process.off("SIGTERM", abort);
186
- }
124
+ let latestMessage = "";
125
+ // Resolve the stash dir once at the `akm index` command boundary and
126
+ // thread it into the indexer (WI-9.10 CLI-wide sweep) — the indexer leaf
127
+ // no longer reads the ambient `resolveStashDir()`.
128
+ const stashDir = resolveStashDir();
129
+ try {
130
+ const result = await akmIndex({
131
+ stashDir,
132
+ full: args.full,
133
+ clean: args.clean,
134
+ dryRun: args["dry-run"],
135
+ reembed: args.reembed,
136
+ onProgress: ({ phase, message, processed, total }) => {
137
+ latestMessage = message;
138
+ const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
139
+ if (verbose) {
140
+ info(`[index:${phase}] ${progressPrefix}${message}`);
141
+ }
142
+ else if (spin) {
143
+ spin.stop(`${progressPrefix}${message}`);
144
+ spin.start(`${progressPrefix}${message}`);
145
+ }
146
+ else if (!EMBEDDED_BATCH_PROGRESS_PATTERN.test(message)) {
147
+ // Non-verbose, non-text (JSON/yaml/etc) mode: silence used to be
148
+ // total until the run finished (#954) — a stalled
149
+ // run looked identical to "nothing written". Phase-start
150
+ // messages and the embedding heartbeat now reach stderr here
151
+ // too; the high-frequency per-batch `Embedded N/M entries.`
152
+ // line (emitted after every committed batch)
153
+ // is deliberately excluded — that would be spam, not a
154
+ // heartbeat.
155
+ info(`[index:${phase}] ${progressPrefix}${message}`);
156
+ }
157
+ },
158
+ signal: controller.signal,
159
+ });
160
+ if (spin) {
161
+ spin.stop(`Indexed ${result.totalEntries} assets.`);
162
+ }
163
+ output("index", result);
164
+ }
165
+ catch (error) {
166
+ if (spin) {
167
+ spin.stop(latestMessage ? `Indexing failed after: ${latestMessage}` : "Indexing failed.");
168
+ }
169
+ throw error;
170
+ }
171
+ finally {
172
+ clearLogFile();
173
+ process.off("SIGINT", abort);
174
+ process.off("SIGTERM", abort);
175
+ if (lockAcquisition.state === "acquired")
176
+ releaseIndexRebuildLock(lockAcquisition.ownership);
177
+ }
178
+ });
187
179
  },
188
180
  });
189
181
  export const infoCommand = defineJsonCommand({
@@ -73,6 +73,8 @@ export function scaffoldStashMeta(stashDir) {
73
73
  }
74
74
  /** Marks the akm-authored block in a stash `.gitignore` (idempotency anchor). */
75
75
  const STASH_GITIGNORE_MARKER = "# akm: keep secret material out of git by default";
76
+ /** Documented opt-out for private bundles that deliberately version env/ and secrets/. */
77
+ export const STASH_GITIGNORE_TRACK_SECRETS_MARKER = "# akm: intentionally track env and secrets";
76
78
  const STASH_GITIGNORE_BLOCK = [
77
79
  STASH_GITIGNORE_MARKER,
78
80
  "# env/ and secrets/ assets hold tokens and keys. They are ignored by default",
@@ -89,14 +91,15 @@ const STASH_GITIGNORE_BLOCK = [
89
91
  *
90
92
  * Idempotent + non-clobbering: creates the file when absent, appends the akm
91
93
  * block when the file exists but lacks it (preserving the user's own rules),
92
- * and no-ops once the marker is present. The user opts INTO versioning by
93
- * un-ignoring a path.
94
+ * and no-ops once the marker is present. A private bundle that deliberately
95
+ * versions all secret material can add `# akm: intentionally track env and
96
+ * secrets`; that explicit marker suppresses this helper entirely (#981).
94
97
  */
95
98
  export function ensureStashGitignore(stashDir) {
96
99
  try {
97
100
  const gitignorePath = path.join(stashDir, ".gitignore");
98
101
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
99
- if (existing.includes(STASH_GITIGNORE_MARKER))
102
+ if (existing.includes(STASH_GITIGNORE_MARKER) || existing.includes(STASH_GITIGNORE_TRACK_SECRETS_MARKER))
100
103
  return;
101
104
  const gap = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
102
105
  fs.writeFileSync(gitignorePath, `${existing}${gap}${STASH_GITIGNORE_BLOCK}`);
@@ -77,12 +77,14 @@
77
77
  */
78
78
  import fs from "node:fs";
79
79
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
80
+ import { makeBundleRef } from "../../core/asset/asset-ref.js";
80
81
  import { loadConfig } from "../../core/config/config.js";
81
82
  import { NotFoundError } from "../../core/errors.js";
82
83
  import { applyInputDefaults, materializeInputFlags, } from "../../execution/input-contract.js";
83
84
  import { createInlineResolvedCommand } from "../../execution/resolved-request.js";
84
85
  import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
85
86
  import { prepareResolvedExecution } from "../../integrations/agent/execution-preparation.js";
87
+ import { isSchedulerRefEnabled } from "../../tasks/activation-config.js";
86
88
  import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
87
89
  import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
88
90
  import { TASK_INPUT_DIAGNOSTICS } from "../../tasks/source/task-input-diagnostics.js";
@@ -270,10 +272,11 @@ export async function akmTaskExplain(ref, options = {}) {
270
272
  const materializedInputs = materializeInputFlags(inputContract, options.inputFlags ?? [], TASK_INPUT_DIAGNOSTICS);
271
273
  const defaultedInputs = applyInputDefaults(inputContract, materializedInputs);
272
274
  const document = projectTaskSourceV4(parsed.v4);
275
+ const enabled = isSchedulerRefEnabled(loadConfig(), "task", makeBundleRef(bundle.source.name, taskConceptId));
273
276
  const schedule = parsed.v4.schedule.map((entry) => ({
274
277
  ordinal: entry.ordinal,
275
278
  cron: entry.cron,
276
- enabled: entry.enabled,
279
+ enabled,
277
280
  source: entry.source,
278
281
  inputs: Object.fromEntries(Object.entries(entry.inputs).map(([name, value]) => {
279
282
  const redaction = redactIfSecretShaped(name, value);
@@ -12,13 +12,11 @@
12
12
  * that would skip pending cleanup).
13
13
  *
14
14
  * 0.9 CLI overhaul (S6): the group was renamed from the plural `tasks` to the
15
- * singular `task` — hard break, no alias. `init`/`enable`/`disable` are
16
- * dropped: the default improve-schedule task set now ships as embedded
15
+ * singular `task` — hard break, no alias. `init` was dropped: the default improve-schedule task set now ships as embedded
17
16
  * templates under src/assets/tasks/improve/ (see src/tasks/embedded.ts),
18
17
  * seeded through the interactive `akm setup` task-review step instead of a
19
- * separate CLI command, and toggling a task's enabled state is a file edit +
20
- * `task sync` (tasks-sync.test.ts already proves the flip path). Every
21
- * subcommand (`add`/`run`/`history`/`sync`) shares one `--bundle <bundle>`
18
+ * separate CLI command. `enable`/`disable` mutate the host-local scheduler
19
+ * allow-list, never the task source. Every subcommand shares one `--bundle <bundle>`
22
20
  * axis (S8.4 ratified `task add`'s `--target` → `--bundle`; a later Gate-1
23
21
  * fix reverted it to match `import`/`proposal accept`, splitting the
24
22
  * write-target axis three-vs-one against `remember`/`clone`/`improve` — the
@@ -35,7 +33,7 @@ import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-r
35
33
  import { akmSearch, parseSearchSource } from "../read/search.js";
36
34
  import { rejectRetiredSourceFlag } from "../read/search-cli.js";
37
35
  import { akmTaskExplain } from "./explain.js";
38
- import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
36
+ import { akmTasksAdd, akmTasksDisable, akmTasksDoctor, akmTasksEnable, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
39
37
  import { akmTaskValidate } from "./validate.js";
40
38
  /** Shared `--bundle <bundle>` arg wired onto every task subcommand. */
41
39
  const bundleArg = {
@@ -294,6 +292,28 @@ const tasksRunCommand = defineCommand({
294
292
  });
295
293
  },
296
294
  });
295
+ const tasksEnableCommand = defineJsonCommand({
296
+ meta: { name: "enable", description: "Enable a task in this host's scheduler configuration and sync it" },
297
+ args: {
298
+ ref: { type: "positional", description: "Task ref or id", required: true },
299
+ ...bundleArg,
300
+ },
301
+ async run({ args }) {
302
+ rejectRetiredTaskTargetFlag();
303
+ output("task-enable", await akmTasksEnable(args.ref, { target: args.bundle }));
304
+ },
305
+ });
306
+ const tasksDisableCommand = defineJsonCommand({
307
+ meta: { name: "disable", description: "Disable a task in this host's scheduler configuration and sync it" },
308
+ args: {
309
+ ref: { type: "positional", description: "Task ref or id", required: true },
310
+ ...bundleArg,
311
+ },
312
+ async run({ args }) {
313
+ rejectRetiredTaskTargetFlag();
314
+ output("task-disable", await akmTasksDisable(args.ref, { target: args.bundle }));
315
+ },
316
+ });
297
317
  /**
298
318
  * #911: `task run <id>` and `task explain <ref>` take the id positionally, so
299
319
  * `task history <id>` is the natural thing to write — and it used to be
@@ -415,8 +435,8 @@ const tasksDoctorCommand = defineJsonCommand({
415
435
  },
416
436
  });
417
437
  /**
418
- * #907: `akm task validate`'s exit-code contract — `valid`/`converts` are
419
- * successful outcomes (exit 0); `blocked`/`invalid`/`not-a-task` are
438
+ * #907: `akm task validate`'s exit-code contract — only current-schema
439
+ * `valid` is successful (exit 0); `blocked`/`invalid`/`not-a-task` are
420
440
  * diagnosed defects the caller must act on (exit 1, mirroring `task sync`'s
421
441
  * own `failures.length > 0 -> EXIT_CODES.GENERAL`). A missing path or an
422
442
  * unreadable file never reaches this function at all — `akmTaskValidate`
@@ -424,7 +444,7 @@ const tasksDoctorCommand = defineJsonCommand({
424
444
  * already maps to exit 2.
425
445
  */
426
446
  export function taskValidateExitCode(result) {
427
- return result.outcome === "valid" || result.outcome === "converts" ? undefined : EXIT_CODES.GENERAL;
447
+ return result.outcome === "valid" ? undefined : EXIT_CODES.GENERAL;
428
448
  }
429
449
  const tasksValidateCommand = defineJsonCommand({
430
450
  meta: {
@@ -532,6 +552,8 @@ export const taskCommand = defineGroupCommand({
532
552
  },
533
553
  subCommands: {
534
554
  add: tasksAddCommand,
555
+ enable: tasksEnableCommand,
556
+ disable: tasksDisableCommand,
535
557
  run: tasksRunCommand,
536
558
  explain: tasksExplainCommand,
537
559
  validate: tasksValidateCommand,