akm-cli 0.9.0-rc.2 → 0.9.0-rc.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +40 -12
  2. package/README.md +7 -8
  3. package/SECURITY.md +5 -3
  4. package/dist/akm +44 -33
  5. package/dist/akm-migrate-storage +44 -33
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/improve-strategies/catchup.json +3 -1
  8. package/dist/assets/improve-strategies/consolidate.json +3 -1
  9. package/dist/assets/improve-strategies/frequent.json +3 -1
  10. package/dist/assets/improve-strategies/graph-refresh.json +3 -1
  11. package/dist/assets/improve-strategies/memory-focus.json +4 -1
  12. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -0
  13. package/dist/assets/improve-strategies/quick.json +3 -1
  14. package/dist/assets/improve-strategies/recombine-only.json +2 -0
  15. package/dist/assets/improve-strategies/reflect-distill.json +1 -0
  16. package/dist/assets/improve-strategies/synthesize.json +2 -0
  17. package/dist/assets/tasks/core/backup.yml +2 -2
  18. package/dist/cli/config-migrate.js +554 -26
  19. package/dist/cli.js +28 -11
  20. package/dist/commands/backup-cli.js +6 -4
  21. package/dist/commands/config-cli.js +10 -3
  22. package/dist/commands/feedback-cli.js +24 -7
  23. package/dist/commands/health/checks.js +94 -15
  24. package/dist/commands/health/html-report.js +23 -2
  25. package/dist/commands/health/improve-metrics.js +45 -6
  26. package/dist/commands/health/md-report.js +10 -1
  27. package/dist/commands/health/surfaces.js +2 -1
  28. package/dist/commands/health/windows.js +1 -1
  29. package/dist/commands/health.js +1 -1
  30. package/dist/commands/improve/anti-collapse.js +3 -3
  31. package/dist/commands/improve/collapse-detector.js +5 -5
  32. package/dist/commands/improve/consolidate.js +123 -66
  33. package/dist/commands/improve/distill/promote-memory.js +8 -6
  34. package/dist/commands/improve/distill/quality-gate.js +1 -1
  35. package/dist/commands/improve/distill-guards.js +1 -1
  36. package/dist/commands/improve/distill-promotion-policy.js +32 -26
  37. package/dist/commands/improve/distill.js +52 -36
  38. package/dist/commands/improve/eligibility.js +9 -8
  39. package/dist/commands/improve/extract-prompt.js +2 -2
  40. package/dist/commands/improve/extract.js +43 -11
  41. package/dist/commands/improve/improve-auto-accept.js +14 -28
  42. package/dist/commands/improve/improve-cli.js +4 -2
  43. package/dist/commands/improve/improve-strategies.js +2 -1
  44. package/dist/commands/improve/improve.js +49 -8
  45. package/dist/commands/improve/loop-stages.js +11 -6
  46. package/dist/commands/improve/preparation.js +51 -17
  47. package/dist/commands/improve/procedural.js +10 -6
  48. package/dist/commands/improve/recombine.js +31 -7
  49. package/dist/commands/improve/reflect.js +24 -14
  50. package/dist/commands/improve/salience.js +5 -1
  51. package/dist/commands/improve/source-identity.js +43 -0
  52. package/dist/commands/migrate-cli.js +36 -0
  53. package/dist/commands/mv-cli.js +647 -258
  54. package/dist/commands/proposal/drain.js +16 -4
  55. package/dist/commands/proposal/proposal-cli.js +3 -2
  56. package/dist/commands/proposal/proposal.js +16 -55
  57. package/dist/commands/proposal/repository.js +664 -58
  58. package/dist/commands/read/curate.js +4 -2
  59. package/dist/commands/read/knowledge.js +4 -1
  60. package/dist/commands/read/search.js +6 -2
  61. package/dist/commands/read/show.js +8 -7
  62. package/dist/commands/sources/self-update.js +156 -112
  63. package/dist/commands/sources/sources-cli.js +7 -2
  64. package/dist/commands/tasks/default-tasks.js +5 -5
  65. package/dist/commands/tasks/tasks-cli.js +7 -3
  66. package/dist/commands/tasks/tasks.js +261 -68
  67. package/dist/core/common.js +36 -3
  68. package/dist/core/config/config-io.js +2 -2
  69. package/dist/core/config/config-schema.js +15 -3
  70. package/dist/core/config/config.js +14 -13
  71. package/dist/core/file-lock.js +2 -2
  72. package/dist/core/improve-result.js +87 -7
  73. package/dist/core/migration-backup.js +816 -171
  74. package/dist/core/migration-operation.js +44 -0
  75. package/dist/core/state/migrations.js +4 -7
  76. package/dist/core/state-db.js +23 -7
  77. package/dist/core/write-source.js +86 -18
  78. package/dist/indexer/db/db.js +109 -53
  79. package/dist/indexer/index-writer-lock.js +38 -37
  80. package/dist/indexer/index-written-assets.js +73 -56
  81. package/dist/indexer/indexer.js +5 -1
  82. package/dist/indexer/search/search-source.js +2 -2
  83. package/dist/indexer/usage/usage-events.js +8 -2
  84. package/dist/integrations/agent/engine-resolution.js +14 -7
  85. package/dist/scripts/migrate-storage.js +887 -992
  86. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +494 -574
  87. package/dist/setup/detected-engines.js +7 -13
  88. package/dist/setup/engine-config.js +14 -4
  89. package/dist/setup/setup.js +1 -1
  90. package/dist/setup/steps/connection.js +1 -1
  91. package/dist/setup/steps/tasks.js +1 -1
  92. package/dist/sources/providers/git-stash.js +58 -21
  93. package/dist/sources/providers/git.js +1 -1
  94. package/dist/storage/engines/sqlite-migrations.js +138 -3
  95. package/dist/storage/repositories/events-repository.js +24 -0
  96. package/dist/storage/repositories/proposals-repository.js +14 -0
  97. package/dist/storage/repositories/task-history-repository.js +30 -3
  98. package/dist/tasks/backends/cron.js +71 -38
  99. package/dist/tasks/backends/exec-utils.js +55 -3
  100. package/dist/tasks/backends/launchd.js +241 -50
  101. package/dist/tasks/backends/schtasks.js +434 -59
  102. package/dist/tasks/command-executable.js +93 -0
  103. package/dist/tasks/embedded.js +2 -0
  104. package/dist/tasks/parser.js +142 -6
  105. package/dist/tasks/resolve-akm-bin.js +19 -4
  106. package/dist/tasks/runner.js +258 -93
  107. package/dist/tasks/schedule.js +108 -19
  108. package/dist/tasks/scheduler-invocation.js +86 -0
  109. package/dist/tasks/task-id.js +37 -0
  110. package/dist/workflows/db.js +24 -13
  111. package/dist/workflows/validator.js +24 -2
  112. package/docs/README.md +103 -0
  113. package/docs/migration/release-notes/0.9.0.md +28 -9
  114. package/docs/migration/v0.8-to-v0.9.md +186 -26
  115. package/package.json +2 -3
@@ -1,6 +1,7 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { AsyncLocalStorage } from "node:async_hooks";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../core/file-lock.js";
@@ -9,7 +10,7 @@ import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
9
10
  const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
10
11
  const INDEX_WRITER_WAIT_MS = 100;
11
12
  const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
12
- const heldLocks = new Map();
13
+ const leaseContext = new AsyncLocalStorage();
13
14
  function buildPayload(purpose) {
14
15
  return createLockPayload({
15
16
  purpose,
@@ -25,29 +26,20 @@ function throwIfAborted(signal) {
25
26
  return;
26
27
  throw signal.reason instanceof Error ? signal.reason : new Error("index writer wait aborted");
27
28
  }
28
- function releaseHeldLock(lockPath) {
29
- const held = heldLocks.get(lockPath);
30
- if (!held)
31
- return;
32
- held.depth -= 1;
33
- if (held.depth > 0)
34
- return;
35
- heldLocks.delete(lockPath);
36
- process.off("exit", held.exitHandler);
37
- releaseLock(held.ownership);
38
- }
39
- function retainHeldLock(lockPath, ownership) {
40
- const existing = heldLocks.get(lockPath);
41
- if (existing) {
42
- existing.depth += 1;
43
- return { lockPath, release: () => releaseHeldLock(lockPath) };
44
- }
45
- if (!ownership)
46
- throw new Error(`Missing ownership for index writer lock at ${lockPath}.`);
29
+ function createLease(lockPath, ownership) {
47
30
  const exitHandler = () => releaseLock(ownership);
48
31
  process.on("exit", exitHandler);
49
- heldLocks.set(lockPath, { depth: 1, exitHandler, ownership });
50
- return { lockPath, release: () => releaseHeldLock(lockPath) };
32
+ let released = false;
33
+ return {
34
+ lockPath,
35
+ release: () => {
36
+ if (released)
37
+ return;
38
+ released = true;
39
+ process.off("exit", exitHandler);
40
+ releaseLock(ownership);
41
+ },
42
+ };
51
43
  }
52
44
  export async function acquireIndexWriterLease(options) {
53
45
  const mode = options.mode ?? "wait";
@@ -61,14 +53,10 @@ export async function acquireIndexWriterLease(options) {
61
53
  const releaseBarrier = tryAcquireMaintenanceBarrier();
62
54
  if (releaseBarrier) {
63
55
  try {
64
- if (heldLocks.has(lockPath)) {
65
- options.onAcquired?.({ waitedMs: Date.now() - startedAt });
66
- return retainHeldLock(lockPath);
67
- }
68
56
  const ownership = tryAcquireLockSync(lockPath, buildPayload(options.purpose));
69
57
  if (ownership) {
70
58
  options.onAcquired?.({ waitedMs: Date.now() - startedAt });
71
- return retainHeldLock(lockPath, ownership);
59
+ return createLease(lockPath, ownership);
72
60
  }
73
61
  const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
74
62
  if (probe.state === "stale") {
@@ -97,16 +85,29 @@ export async function acquireIndexWriterLease(options) {
97
85
  }
98
86
  }
99
87
  export async function withIndexWriterLease(options, run) {
100
- const lease = await acquireIndexWriterLease(options);
101
- if (!lease) {
102
- throw new Error(`index writer lease unavailable for ${options.purpose}`);
103
- }
104
- try {
105
- return await run();
106
- }
107
- finally {
108
- lease.release();
109
- }
88
+ const lockPath = getIndexWriterLockPath();
89
+ const inherited = leaseContext.getStore();
90
+ if (inherited?.has(lockPath))
91
+ return run();
92
+ const context = inherited ?? new Set();
93
+ const execute = async () => {
94
+ const lease = await acquireIndexWriterLease(options);
95
+ if (!lease)
96
+ throw new Error(`index writer lease unavailable for ${options.purpose}`);
97
+ context.add(lockPath);
98
+ try {
99
+ return await run();
100
+ }
101
+ finally {
102
+ context.delete(lockPath);
103
+ lease.release();
104
+ }
105
+ };
106
+ return inherited ? execute() : leaseContext.run(context, execute);
107
+ }
108
+ /** Asset writes and index rebuilds share one lease so scans cannot publish stale snapshots. */
109
+ export function withAssetMutationLease(purpose, run) {
110
+ return withIndexWriterLease({ purpose }, run);
110
111
  }
111
112
  export function probeIndexWriterLease() {
112
113
  return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
@@ -24,7 +24,8 @@ import path from "node:path";
24
24
  import { getDbPath } from "../core/paths.js";
25
25
  import { warnVerbose } from "../core/warn.js";
26
26
  import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
27
- import { closeDatabase, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry, upsertWorkflowDocument, } from "./db/db.js";
27
+ import { closeDatabase, deleteEntriesByIds, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry, upsertWorkflowDocument, } from "./db/db.js";
28
+ import { withIndexWriterLease } from "./index-writer-lock.js";
28
29
  import { generateMetadataFlat } from "./passes/metadata.js";
29
30
  import { buildSearchText } from "./search/search-fields.js";
30
31
  /**
@@ -47,68 +48,84 @@ export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
47
48
  * first read (`ensureIndex`) or an explicit `akm index`, which also cover
48
49
  * embeddings and the other passes this fast path skips.
49
50
  */
50
- export async function indexWrittenAssets(stashDir, filePaths) {
51
+ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
51
52
  try {
52
- const dbPath = getDbPath();
53
- if (!fs.existsSync(dbPath))
54
- return;
55
- // The full walk never descends into dot-directories (they hold state like
56
- // `.meta/`, `.stash.json`), and `shouldIndexStashFile` relies on the walker
57
- // for that — mirror it here so this fast path indexes exactly what a full
58
- // run would.
59
- const files = filePaths.filter((f) => {
60
- if (!fs.existsSync(f))
61
- return false;
62
- const rel = path.relative(stashDir, f);
63
- return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
64
- });
65
- if (files.length === 0)
66
- return;
67
- // Generate metadata BEFORE opening the DB so the write window stays
68
- // short. One call per file keeps the entry↔path pairing exact.
69
- const pairs = [];
70
- for (const file of files) {
71
- const generated = await generateMetadataFlat(stashDir, [file]);
72
- const entry = generated.entries[0];
73
- // Workflows also carry a workflow_documents side-table upsert — handled
74
- // below, mirroring the full walk — since `akm mv` rewrites citer files
75
- // that can be workflows.
76
- if (entry)
77
- pairs.push({ file, entry });
78
- }
79
- if (pairs.length === 0)
80
- return;
81
- const db = openExistingDatabase(dbPath);
82
- try {
83
- db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
84
- if (getEntryCount(db) === 0)
85
- return;
86
- for (const { file, entry } of pairs) {
87
- const entryKey = `${stashDir}:${entry.type}:${entry.name}`;
88
- let entryWithSize = entry;
89
- try {
90
- entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
53
+ return await withIndexWriterLease({ purpose: "index-written-assets" }, async () => {
54
+ if (options.recoverMoves !== false) {
55
+ const { recoverInterruptedMoveTransactions } = await import("../commands/mv-cli.js");
56
+ await recoverInterruptedMoveTransactions(stashDir);
57
+ }
58
+ const dbPath = getDbPath();
59
+ if (!fs.existsSync(dbPath))
60
+ return true;
61
+ // The full walk never descends into dot-directories (they hold state like
62
+ // `.meta/`, `.stash.json`), and `shouldIndexStashFile` relies on the walker
63
+ // for that — mirror it here so this fast path indexes exactly what a full
64
+ // run would.
65
+ const files = filePaths.filter((f) => {
66
+ const rel = path.relative(stashDir, f);
67
+ return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
68
+ });
69
+ if (files.length === 0)
70
+ return true;
71
+ // Generate metadata BEFORE opening the DB so the write window stays
72
+ // short. One call per file keeps the entry↔path pairing exact.
73
+ const pairs = [];
74
+ const unindexable = new Set();
75
+ for (const file of files) {
76
+ if (!fs.existsSync(file)) {
77
+ unindexable.add(file);
78
+ continue;
91
79
  }
92
- catch {
93
- // stat raced a delete — index without the size, like the full walk does.
80
+ const generated = await generateMetadataFlat(stashDir, [file]);
81
+ const entry = generated.entries[0];
82
+ // Workflows also carry a workflow_documents side-table upsert — handled
83
+ // below, mirroring the full walk — since `akm mv` rewrites citer files
84
+ // that can be workflows.
85
+ if (entry)
86
+ pairs.push({ file, entry });
87
+ else
88
+ unindexable.add(file);
89
+ }
90
+ const db = openExistingDatabase(dbPath);
91
+ try {
92
+ db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
93
+ if (getEntryCount(db) === 0)
94
+ return true;
95
+ for (const file of unindexable) {
96
+ const rows = db.prepare("SELECT id FROM entries WHERE file_path = ?").all(file);
97
+ deleteEntriesByIds(db, rows.map((row) => row.id));
94
98
  }
95
- const entryId = upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
96
- if (entry.type === "workflow") {
97
- // Same contract as the full walk (indexer.ts): the renderer cached
98
- // the parsed document during metadata generation; persist it so the
99
- // workflow runtime never sees an entry without its document.
100
- const doc = takeWorkflowDocument(entry);
101
- if (doc)
102
- upsertWorkflowDocument(db, entryId, doc, fs.readFileSync(file));
99
+ for (const { file, entry } of pairs) {
100
+ const entryKey = `${stashDir}:${entry.type}:${entry.name}`;
101
+ let entryWithSize = entry;
102
+ try {
103
+ entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
104
+ }
105
+ catch {
106
+ // stat raced a delete — index without the size, like the full walk does.
107
+ }
108
+ const entryId = upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
109
+ if (entry.type === "workflow") {
110
+ // Same contract as the full walk (indexer.ts): the renderer cached
111
+ // the parsed document during metadata generation; persist it so the
112
+ // workflow runtime never sees an entry without its document.
113
+ const doc = takeWorkflowDocument(entry);
114
+ if (doc)
115
+ upsertWorkflowDocument(db, entryId, doc, fs.readFileSync(file));
116
+ }
103
117
  }
118
+ if (pairs.length > 0 || unindexable.size > 0)
119
+ rebuildFts(db, { incremental: true });
104
120
  }
105
- rebuildFts(db, { incremental: true });
106
- }
107
- finally {
108
- closeDatabase(db);
109
- }
121
+ finally {
122
+ closeDatabase(db);
123
+ }
124
+ return true;
125
+ });
110
126
  }
111
127
  catch (error) {
112
128
  warnVerbose("Write-path index update skipped (asset appears after the next full index):", error instanceof Error ? error.message : String(error));
129
+ return false;
113
130
  }
114
131
  }
@@ -155,7 +155,7 @@ async function runFinalizePhase(ctx) {
155
155
  ctx.timing.tFtsEnd = Date.now();
156
156
  // Re-link detached usage_events and recompute utility scores.
157
157
  onProgress({ phase: "finalize", message: "Relinking usage events." });
158
- relinkUsageEvents(db);
158
+ relinkUsageEvents(db, { sources, defaultStashDir: stashDir });
159
159
  onProgress({ phase: "finalize", message: "Recomputing utility scores." });
160
160
  recomputeUtilityScores(db);
161
161
  // Purge LLM cache entries for assets that no longer exist in the index.
@@ -326,6 +326,10 @@ async function akmIndexReal(options) {
326
326
  const sourceCacheEnd = Date.now();
327
327
  const allSourceEntries = resolveSourceEntries(stashDir, config);
328
328
  const allSourceDirs = allSourceEntries.map((s) => s.path);
329
+ const { recoverInterruptedMoveTransactions } = await import("../commands/mv-cli.js");
330
+ for (const sourceDir of new Set([stashDir, ...allSourceDirs])) {
331
+ await recoverInterruptedMoveTransactions(sourceDir);
332
+ }
329
333
  onProgress({
330
334
  phase: "preflight",
331
335
  message: `Resolved ${allSourceDirs.length} stash source${allSourceDirs.length === 1 ? "" : "s"}.`,
@@ -5,6 +5,7 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { resolveStashDir } from "../../core/common.js";
7
7
  import { getSources, loadConfig } from "../../core/config/config.js";
8
+ import { resolveGitContentRoot } from "../../core/write-source.js";
8
9
  import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
9
10
  // Eager side-effect imports so all built-in source providers self-register
10
11
  // before resolveEntryContentDir() runs.
@@ -141,8 +142,7 @@ function resolveEntryContentDir(entry) {
141
142
  // that subdirectory. This is a content-layout convention, not a provider
142
143
  // capability — keep it here.
143
144
  if (GIT_STASH_TYPES.has(entry.type)) {
144
- const contentDir = path.join(dir, "content");
145
- return isValidDirectory(contentDir) ? contentDir : dir;
145
+ return resolveGitContentRoot(dir);
146
146
  }
147
147
  return dir;
148
148
  }
@@ -47,8 +47,14 @@ export function getUsageEvents(db, filters) {
47
47
  params.push(filters.event_type);
48
48
  }
49
49
  if (filters?.entry_ref) {
50
- conditions.push("entry_ref = ?");
51
- params.push(filters.entry_ref);
50
+ if (filters.entry_ref.includes("//")) {
51
+ conditions.push("entry_ref = ?");
52
+ params.push(filters.entry_ref);
53
+ }
54
+ else {
55
+ conditions.push("(entry_ref = ? OR substr(entry_ref, -length(?) - 2) = '//' || ?)");
56
+ params.push(filters.entry_ref, filters.entry_ref, filters.entry_ref);
57
+ }
52
58
  }
53
59
  if (filters?.source) {
54
60
  conditions.push("source = ?");
@@ -164,17 +164,24 @@ function lowerAgentEngine(name, engine, config) {
164
164
  timeoutMs: hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : DEFAULT_AGENT_TIMEOUT_MS,
165
165
  };
166
166
  }
167
- const fallback = resolveLlmEngineUse(config, [{ engine: engine.llmEngine ?? config.defaults?.llmEngine }]);
168
- if (!fallback)
169
- throw new ConfigError(`SDK engine "${name}" has no fallback LLM engine.`, "LLM_NOT_CONFIGURED");
167
+ const fallbackName = engine.llmEngine ?? config.defaults?.llmEngine;
168
+ const fallback = fallbackName
169
+ ? resolveLlmEngineUse(config, [{ engine: fallbackName }], { optional: true })
170
+ : undefined;
170
171
  return {
171
172
  kind: "sdk",
172
173
  engine: name,
173
174
  profile,
174
- fallbackConnection: fallback.connection,
175
- ...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
176
- fallbackTimeoutMs: fallback.timeoutMs,
177
- timeoutMs: hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : fallback.timeoutMs,
175
+ ...(fallback
176
+ ? {
177
+ fallbackConnection: fallback.connection,
178
+ ...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
179
+ fallbackTimeoutMs: fallback.timeoutMs,
180
+ }
181
+ : {}),
182
+ timeoutMs: hasOwn(engine, "timeoutMs")
183
+ ? (engine.timeoutMs ?? null)
184
+ : (fallback?.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS),
178
185
  };
179
186
  }
180
187
  /** Lower a named engine through the canonical harness platform. */