akm-cli 0.9.1 → 0.9.2-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.
- package/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
* Side-effect-free SQLite snapshots for planning and preview paths.
|
|
6
|
+
*
|
|
7
|
+
* A normal SQLite read-only connection can still write lock bytes into an
|
|
8
|
+
* existing `-shm` file when the source database is in WAL mode. Dry planning
|
|
9
|
+
* must not do that. This helper copies a stable main/WAL pair and opens that
|
|
10
|
+
* private copy, so SQLite never attaches to the operator's original
|
|
11
|
+
* main/WAL/SHM files.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { openDatabaseFinalizing } from "./database.js";
|
|
17
|
+
export class SqliteReadSnapshotUnavailableError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "SqliteReadSnapshotUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function pathExists(filePath) {
|
|
24
|
+
try {
|
|
25
|
+
fs.lstatSync(filePath);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === "ENOENT")
|
|
30
|
+
return false;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function fileFingerprint(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs.statSync(filePath, { bigint: true });
|
|
37
|
+
return { size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code === "ENOENT")
|
|
41
|
+
return undefined;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function databaseFingerprint(dbPath) {
|
|
46
|
+
const main = fileFingerprint(dbPath);
|
|
47
|
+
if (!main) {
|
|
48
|
+
throw new SqliteReadSnapshotUnavailableError("SQLite snapshot source disappeared while it was being inspected");
|
|
49
|
+
}
|
|
50
|
+
const wal = fileFingerprint(`${dbPath}-wal`);
|
|
51
|
+
return { main, ...(wal ? { wal } : {}) };
|
|
52
|
+
}
|
|
53
|
+
function fingerprintsEqual(left, right) {
|
|
54
|
+
const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs;
|
|
55
|
+
return sameFile(left.main, right.main) && sameFile(left.wal, right.wal);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Open an isolated copy of an existing SQLite database.
|
|
59
|
+
*
|
|
60
|
+
* Returns `undefined` only when the source is absent. Committed WAL frames are
|
|
61
|
+
* copied beside the private main file; the source SHM is intentionally not
|
|
62
|
+
* copied because SQLite can safely create one inside the disposable directory.
|
|
63
|
+
* A live rollback-journal database fails closed because a file-level copy
|
|
64
|
+
* cannot distinguish its committed and uncommitted pages without attaching to
|
|
65
|
+
* the source.
|
|
66
|
+
*/
|
|
67
|
+
export function openSqliteReadSnapshot(dbPath) {
|
|
68
|
+
if (!pathExists(dbPath))
|
|
69
|
+
return undefined;
|
|
70
|
+
if (pathExists(`${dbPath}-journal`)) {
|
|
71
|
+
throw new SqliteReadSnapshotUnavailableError("an active SQLite rollback journal is present; a non-mutating point-in-time snapshot is unavailable");
|
|
72
|
+
}
|
|
73
|
+
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-sqlite-read-"));
|
|
74
|
+
const snapshotPath = path.join(snapshotDir, "snapshot.db");
|
|
75
|
+
let db;
|
|
76
|
+
try {
|
|
77
|
+
let copied = false;
|
|
78
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
if (pathExists(`${dbPath}-journal`)) {
|
|
81
|
+
throw new SqliteReadSnapshotUnavailableError("an active SQLite rollback journal appeared while taking the non-mutating snapshot");
|
|
82
|
+
}
|
|
83
|
+
const before = databaseFingerprint(dbPath);
|
|
84
|
+
fs.copyFileSync(dbPath, snapshotPath);
|
|
85
|
+
if (before.wal)
|
|
86
|
+
fs.copyFileSync(`${dbPath}-wal`, `${snapshotPath}-wal`);
|
|
87
|
+
else
|
|
88
|
+
fs.rmSync(`${snapshotPath}-wal`, { force: true });
|
|
89
|
+
const after = databaseFingerprint(dbPath);
|
|
90
|
+
if (fingerprintsEqual(before, after)) {
|
|
91
|
+
copied = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error.code === "ENOENT")
|
|
97
|
+
continue;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!copied) {
|
|
102
|
+
throw new SqliteReadSnapshotUnavailableError("SQLite main/WAL files kept changing while taking the non-mutating snapshot");
|
|
103
|
+
}
|
|
104
|
+
db = openDatabaseFinalizing(snapshotPath, { readonly: true, create: false });
|
|
105
|
+
const closeSnapshot = db.close.bind(db);
|
|
106
|
+
let closed = false;
|
|
107
|
+
const cleanup = (bestEffort) => {
|
|
108
|
+
if (closed)
|
|
109
|
+
return;
|
|
110
|
+
closed = true;
|
|
111
|
+
process.removeListener("exit", cleanupOnExit);
|
|
112
|
+
if (bestEffort) {
|
|
113
|
+
try {
|
|
114
|
+
closeSnapshot();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Process teardown is already committed; keep removing the copy.
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
fs.rmSync(snapshotDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Best-effort process-exit backstop. Normal close still surfaces IO errors.
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
closeSnapshot();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
fs.rmSync(snapshotDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const cleanupOnExit = () => cleanup(true);
|
|
135
|
+
process.once("exit", cleanupOnExit);
|
|
136
|
+
db.close = () => cleanup(false);
|
|
137
|
+
return db;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
try {
|
|
141
|
+
db?.close();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
fs.rmSync(snapshotDir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -30,7 +30,8 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
30
30
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
31
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
32
32
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
33
|
-
import {
|
|
33
|
+
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
34
|
+
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
34
35
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
35
36
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
36
37
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
@@ -38,6 +39,7 @@ const DISABLED_PREFIX = "# akm:disabled ";
|
|
|
38
39
|
const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
|
|
39
40
|
const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
|
|
40
41
|
export const PORTABLE_CRON_LINE_LIMIT = 1000;
|
|
42
|
+
const CRON_SNAPSHOT = Symbol("akm-cron-binding-snapshot");
|
|
41
43
|
export function CRON_BACKEND(options = {}) {
|
|
42
44
|
const exec = options.exec ?? defaultCronExec();
|
|
43
45
|
const fsLike = options.fs ?? nodeFs();
|
|
@@ -48,7 +50,9 @@ export function CRON_BACKEND(options = {}) {
|
|
|
48
50
|
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, envPath ?? ""));
|
|
49
51
|
return {
|
|
50
52
|
name: "cron",
|
|
51
|
-
install(task, opts) {
|
|
53
|
+
install(task, opts, expected) {
|
|
54
|
+
if (expected)
|
|
55
|
+
assertSchedulerExpectationIdentity(expected, task);
|
|
52
56
|
// Create the log directory before writing the crontab line — cron
|
|
53
57
|
// appends with `>>` and the surrounding shell will fail the entire
|
|
54
58
|
// entry if the parent directory doesn't exist.
|
|
@@ -56,77 +60,201 @@ export function CRON_BACKEND(options = {}) {
|
|
|
56
60
|
assertPortableCronLine(cronLine);
|
|
57
61
|
fsLike.ensureDir(logDir);
|
|
58
62
|
const existing = readCrontab(exec);
|
|
59
|
-
const
|
|
60
|
-
const
|
|
63
|
+
const nativeId = schedulerBindingNativeId(task);
|
|
64
|
+
const blocks = listBlocks(existing);
|
|
65
|
+
const matching = blocks.filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
|
|
66
|
+
const artifacts = matching.map((block) => cronArtifact(block.id, block.body));
|
|
67
|
+
const priorArtifact = expected
|
|
68
|
+
? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, expected.state === "absent" ? 0 : 1)
|
|
69
|
+
: matching.length > 1
|
|
70
|
+
? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, 1)
|
|
71
|
+
: artifacts[0];
|
|
72
|
+
const prior = priorArtifact ? matching.find((block) => block.id === priorArtifact.nativeId) : undefined;
|
|
73
|
+
if (expected) {
|
|
74
|
+
assertSchedulerMutationArtifact(priorArtifact, expected);
|
|
75
|
+
}
|
|
76
|
+
else if (prior) {
|
|
77
|
+
assertSchedulerNativeArtifactOwner(prior.id, task, extractCronInvocation(prior.body)?.invocation);
|
|
78
|
+
}
|
|
79
|
+
const block = renderBlock(nativeId, cronLine, task.enabled, task.executionEvidenceDigest);
|
|
80
|
+
const next = upsertBlock(existing, nativeId, block);
|
|
61
81
|
replaceCrontab(exec, existing, next);
|
|
62
82
|
},
|
|
63
|
-
uninstall(
|
|
83
|
+
uninstall(nativeId, expected) {
|
|
84
|
+
if (expected)
|
|
85
|
+
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
64
86
|
const existing = readCrontab(exec);
|
|
65
|
-
const
|
|
87
|
+
const matching = listBlocks(existing).filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
|
|
88
|
+
const priorArtifact = expected
|
|
89
|
+
? assertSchedulerNativeArtifactCardinality(matching.map((block) => cronArtifact(block.id, block.body)), nativeId, 1)
|
|
90
|
+
: undefined;
|
|
91
|
+
const prior = expected
|
|
92
|
+
? matching.find((block) => block.id === priorArtifact?.nativeId)
|
|
93
|
+
: matching.find(({ id }) => id === nativeId);
|
|
94
|
+
if (!prior)
|
|
95
|
+
return;
|
|
96
|
+
if (expected) {
|
|
97
|
+
assertSchedulerRemovalArtifact(nativeId, expected, extractCronInvocation(prior.body)?.invocation, normalizeSignature(prior.body));
|
|
98
|
+
}
|
|
99
|
+
const next = removeBlock(existing, nativeId);
|
|
66
100
|
replaceCrontab(exec, existing, next);
|
|
67
101
|
},
|
|
68
|
-
setEnabled(
|
|
102
|
+
setEnabled(nativeId, enabled) {
|
|
69
103
|
const existing = readCrontab(exec);
|
|
70
|
-
const next = toggleBlock(existing,
|
|
104
|
+
const next = toggleBlock(existing, nativeId, enabled);
|
|
71
105
|
replaceCrontab(exec, existing, next);
|
|
72
106
|
},
|
|
73
107
|
list() {
|
|
74
|
-
|
|
75
|
-
const refs = [];
|
|
76
|
-
for (const { id, body } of listBlocks(existing)) {
|
|
77
|
-
const installed = extractCronInvocation(body);
|
|
78
|
-
// An invocation that no longer parses (e.g. a pre-0.9 `tasks run`
|
|
79
|
-
// entry surviving the scheduler-ABI respelling, or any other
|
|
80
|
-
// foreign/malformed line between our markers) is an orphan of its
|
|
81
|
-
// marker id, not a hard failure: omit it here so `akmTasksSync`
|
|
82
|
-
// treats the id as "not present" and reinstalls it from the current
|
|
83
|
-
// task file — going through the normal eligibility/`--rebind` gate
|
|
84
|
-
// exactly like a fresh install.
|
|
85
|
-
if (!installed)
|
|
86
|
-
continue;
|
|
87
|
-
refs.push({
|
|
88
|
-
id,
|
|
89
|
-
signature: normalizeSignature(body),
|
|
90
|
-
...(installed.target !== undefined ? { target: installed.target } : {}),
|
|
91
|
-
binding: installed.binding,
|
|
92
|
-
contextPath: installed.contextPath,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return refs;
|
|
108
|
+
return [...inspectCronState(readCrontab(exec)).installed];
|
|
96
109
|
},
|
|
97
110
|
listForRebind() {
|
|
98
111
|
const existing = readCrontab(exec);
|
|
99
112
|
return listBlocks(existing).map(({ id, body }) => {
|
|
100
113
|
const installed = extractCronInvocation(body);
|
|
101
|
-
|
|
102
|
-
id,
|
|
114
|
+
const ref = {
|
|
115
|
+
id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
|
|
103
116
|
signature: normalizeSignature(body),
|
|
104
117
|
...(installed?.target !== undefined ? { target: installed.target } : {}),
|
|
105
118
|
};
|
|
119
|
+
Object.defineProperty(ref, "nativeId", { value: id });
|
|
120
|
+
if (installed)
|
|
121
|
+
Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
|
|
122
|
+
return ref;
|
|
106
123
|
});
|
|
107
124
|
},
|
|
125
|
+
listNativeArtifacts() {
|
|
126
|
+
return [...inspectCronState(readCrontab(exec)).artifacts];
|
|
127
|
+
},
|
|
128
|
+
inspectBindings() {
|
|
129
|
+
return inspectCronState(readCrontab(exec));
|
|
130
|
+
},
|
|
131
|
+
snapshotBindings(ids) {
|
|
132
|
+
const crontab = readCrontab(exec);
|
|
133
|
+
const inspection = inspectCronState(crontab);
|
|
134
|
+
const keys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
kind: CRON_SNAPSHOT,
|
|
137
|
+
nativeIds: Object.freeze([...ids]),
|
|
138
|
+
artifacts: Object.freeze(inspection.artifacts.filter((artifact) => keys.has(schedulerNativeArtifactKey(artifact.nativeId)))),
|
|
139
|
+
crontab,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
restoreBindings(snapshot, expectedCurrent) {
|
|
143
|
+
if (!isCronBindingSnapshot(snapshot)) {
|
|
144
|
+
throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
145
|
+
}
|
|
146
|
+
const existing = readCrontab(exec);
|
|
147
|
+
const current = inspectCronState(existing);
|
|
148
|
+
const safeNativeIds = [];
|
|
149
|
+
const errors = [];
|
|
150
|
+
if (expectedCurrent) {
|
|
151
|
+
for (const nativeId of snapshot.nativeIds) {
|
|
152
|
+
try {
|
|
153
|
+
const expected = expectedCurrent.find((candidate) => candidate.nativeId === nativeId);
|
|
154
|
+
if (!expected) {
|
|
155
|
+
throw new ConfigError(`Missing cron rollback expectation for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
|
|
156
|
+
}
|
|
157
|
+
assertSchedulerRollbackArtifactCardinality(current.artifacts, expected);
|
|
158
|
+
safeNativeIds.push(nativeId);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
errors.push(error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
safeNativeIds.push(...snapshot.nativeIds);
|
|
167
|
+
}
|
|
168
|
+
const priorBlocks = new Map(listBlocks(snapshot.crontab).map((block) => [block.id, block]));
|
|
169
|
+
let restored = existing;
|
|
170
|
+
for (const nativeId of safeNativeIds) {
|
|
171
|
+
const prior = priorBlocks.get(nativeId);
|
|
172
|
+
restored = prior
|
|
173
|
+
? upsertBlock(restored, nativeId, [BEGIN(nativeId), prior.body, END(nativeId)].join("\n"))
|
|
174
|
+
: removeBlock(restored, nativeId);
|
|
175
|
+
}
|
|
176
|
+
if (restored !== existing) {
|
|
177
|
+
try {
|
|
178
|
+
replaceCrontab(exec, existing, restored);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
errors.push(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (errors.length > 0) {
|
|
185
|
+
throw new AggregateError(errors, "Cron rollback CAS rejected one or more changed native artifacts.");
|
|
186
|
+
}
|
|
187
|
+
},
|
|
108
188
|
expectedSignature(task, opts) {
|
|
109
189
|
const cronLine = buildCronLine(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
|
|
110
190
|
assertPortableCronLine(cronLine);
|
|
111
|
-
return normalizeSignature(cronBlockBody(cronLine, task.enabled));
|
|
191
|
+
return normalizeSignature(cronBlockBody(cronLine, task.enabled, task.executionEvidenceDigest));
|
|
112
192
|
},
|
|
113
193
|
};
|
|
114
194
|
}
|
|
195
|
+
function inspectCronState(crontab) {
|
|
196
|
+
const installed = [];
|
|
197
|
+
const artifacts = [];
|
|
198
|
+
for (const { id, body } of listBlocks(crontab)) {
|
|
199
|
+
const parsed = extractCronInvocation(body);
|
|
200
|
+
const fingerprint = normalizeSignature(body);
|
|
201
|
+
const artifact = cronArtifact(id, body);
|
|
202
|
+
artifacts.push(artifact);
|
|
203
|
+
if (!parsed)
|
|
204
|
+
continue;
|
|
205
|
+
const ref = {
|
|
206
|
+
id: schedulerLogicalBindingId(id, parsed.invocation),
|
|
207
|
+
signature: fingerprint,
|
|
208
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
209
|
+
binding: parsed.binding,
|
|
210
|
+
contextPath: parsed.contextPath,
|
|
211
|
+
};
|
|
212
|
+
Object.defineProperty(ref, "nativeId", { value: id });
|
|
213
|
+
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
214
|
+
installed.push(ref);
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
|
|
217
|
+
}
|
|
218
|
+
function cronArtifact(nativeId, body) {
|
|
219
|
+
const parsed = extractCronInvocation(body);
|
|
220
|
+
const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
|
|
221
|
+
const artifact = parsed
|
|
222
|
+
? {
|
|
223
|
+
nativeId,
|
|
224
|
+
...(owner !== undefined ? { bindingId: owner } : {}),
|
|
225
|
+
invocation: Object.freeze([...parsed.invocation]),
|
|
226
|
+
}
|
|
227
|
+
: { nativeId };
|
|
228
|
+
Object.defineProperty(artifact, "fingerprint", { value: normalizeSignature(body) });
|
|
229
|
+
return artifact;
|
|
230
|
+
}
|
|
231
|
+
function isCronBindingSnapshot(value) {
|
|
232
|
+
return (typeof value === "object" &&
|
|
233
|
+
value !== null &&
|
|
234
|
+
value.kind === CRON_SNAPSHOT &&
|
|
235
|
+
typeof value.crontab === "string");
|
|
236
|
+
}
|
|
115
237
|
// ── helpers (exported for tests) ────────────────────────────────────────────
|
|
116
|
-
export function buildCronLine(task, akmArgv, logDir, contextPath,
|
|
117
|
-
const spec = parseSchedule(task.
|
|
238
|
+
export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
|
|
239
|
+
const spec = parseSchedule(task.cron, "cron");
|
|
118
240
|
const cronExpr = translateToCron(spec);
|
|
119
|
-
const logPath = path.join(logDir, `${task
|
|
120
|
-
const invocation =
|
|
241
|
+
const logPath = path.join(logDir, `${schedulerBindingNativeId(task)}.log`);
|
|
242
|
+
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
121
243
|
const cmd = invocation.argv.map((part) => quoteForCron(part)).join(" ");
|
|
122
244
|
return `${cronExpr} ${cmd} >> ${quoteForCron(logPath)} 2>&1`;
|
|
123
245
|
}
|
|
124
246
|
/** The crontab line as it appears inside a block — commented when disabled. */
|
|
125
|
-
export function cronBlockBody(cronLine, enabled) {
|
|
126
|
-
|
|
247
|
+
export function cronBlockBody(cronLine, enabled, executionEvidenceDigest) {
|
|
248
|
+
const lines = [
|
|
249
|
+
cronLine,
|
|
250
|
+
...(executionEvidenceDigest === undefined
|
|
251
|
+
? []
|
|
252
|
+
: [`# akm:workflow-evidence ${assertSchedulerExecutionEvidenceDigest(executionEvidenceDigest)}`]),
|
|
253
|
+
];
|
|
254
|
+
return (enabled ? lines : lines.map((line) => `${DISABLED_PREFIX}${line}`)).join("\n");
|
|
127
255
|
}
|
|
128
|
-
export function renderBlock(id, cronLine, enabled) {
|
|
129
|
-
return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
|
|
256
|
+
export function renderBlock(id, cronLine, enabled, executionEvidenceDigest) {
|
|
257
|
+
return [BEGIN(id), cronBlockBody(cronLine, enabled, executionEvidenceDigest), END(id)].join("\n");
|
|
130
258
|
}
|
|
131
259
|
/**
|
|
132
260
|
* Parse the akm-owned blocks out of a crontab, returning each task id with the
|
|
@@ -195,7 +323,7 @@ export function extractCronInvocation(body) {
|
|
|
195
323
|
const redirectIndex = fields.indexOf(">>", commandStart);
|
|
196
324
|
if (redirectIndex === -1)
|
|
197
325
|
return undefined;
|
|
198
|
-
return
|
|
326
|
+
return parseScheduledBindingArgv(fields.slice(commandStart, redirectIndex));
|
|
199
327
|
}
|
|
200
328
|
/** Reverse {@link quoteForCron} for a single whitespace-free token. */
|
|
201
329
|
function splitCronShellWords(value) {
|
|
@@ -5,7 +5,7 @@ import { CRON_BACKEND } from "./cron.js";
|
|
|
5
5
|
import { LAUNCHD_BACKEND } from "./launchd.js";
|
|
6
6
|
import { SCHTASKS_BACKEND } from "./schtasks.js";
|
|
7
7
|
// WI-9.10e: the former `_setBackendsForTests` module-mutation seam was retired.
|
|
8
|
-
// Tests inject a fake `
|
|
8
|
+
// Tests inject a fake `SchedulerBackend` directly via the `deps.backend` parameter
|
|
9
9
|
// the `akm task` mutation entries already accept (the backend carries its own
|
|
10
10
|
// `name`), so no module-level override binding is needed. `selectBackend`'s
|
|
11
11
|
// `options.platform` covers the platform-steering the seam's second override
|