akm-cli 0.8.0-rc2 → 0.8.1
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/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/assets/help/help-accept.md +12 -0
- package/dist/assets/help/help-improve.md +81 -0
- package/dist/{commands → assets}/help/help-proposals.md +7 -4
- package/dist/assets/help/help-reject.md +11 -0
- package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
- package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
- package/dist/assets/profiles/default.json +15 -0
- package/dist/assets/profiles/graph-refresh.json +13 -0
- package/dist/assets/profiles/memory-focus.json +12 -0
- package/dist/assets/profiles/quick.json +15 -0
- package/dist/assets/profiles/thorough.json +15 -0
- package/dist/assets/prompts/extract-session.md +80 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1557 -147
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +217 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +1042 -55
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +138 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1736 -346
- package/dist/commands/info.js +26 -28
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +86 -16
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +402 -31
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/output/cli-hints.js +7 -4
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +4 -1
- package/dist/tasks/backends/schtasks.js +4 -1
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +6 -3
- package/dist/wiki/wiki.js +4 -1
- package/dist/workflows/authoring.js +4 -1
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/help/help-accept.md +0 -9
- package/dist/commands/help/help-improve.md +0 -53
- package/dist/commands/help/help-reject.md +0 -8
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
- package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
- /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
- /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
- /package/dist/{commands → assets}/help/help-propose.md +0 -0
- /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
- /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* schtasks.exe backend for `akm tasks` (Windows default).
|
|
3
6
|
*
|
|
@@ -29,12 +32,12 @@
|
|
|
29
32
|
import fs from "node:fs";
|
|
30
33
|
import os from "node:os";
|
|
31
34
|
import path from "node:path";
|
|
35
|
+
import schtasksTemplate from "../../assets/backends/schtasks-template.xml" with { type: "text" };
|
|
32
36
|
import { ConfigError } from "../../core/errors";
|
|
33
37
|
import { getTaskLogDir } from "../../core/paths";
|
|
34
38
|
import { resolveAkmInvocation } from "../resolveAkmBin";
|
|
35
39
|
import { parseSchedule, translateToSchtasks } from "../schedule";
|
|
36
40
|
import { escapeXml, spawnCommand } from "./exec-utils";
|
|
37
|
-
import schtasksTemplate from "./schtasks-template.xml" with { type: "text" };
|
|
38
41
|
export const DEFAULT_FOLDER_PREFIX = "\\akm\\";
|
|
39
42
|
export function SCHTASKS_BACKEND(options = {}) {
|
|
40
43
|
const exec = options.exec ?? defaultSchtasksExec();
|
package/dist/tasks/parser.js
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
|
-
* Parse a task
|
|
5
|
+
* Parse a task YAML file into a {@link TaskDocument}.
|
|
3
6
|
*
|
|
4
|
-
* The on-disk shape is
|
|
7
|
+
* The on-disk shape is a pure YAML file at `<stash>/tasks/<id>.yml`:
|
|
5
8
|
*
|
|
6
|
-
* ```
|
|
7
|
-
* ---
|
|
9
|
+
* ```yaml
|
|
8
10
|
* schedule: "0 9 * * *"
|
|
9
11
|
* # one of:
|
|
10
12
|
* workflow: workflow:daily-backup
|
|
11
13
|
* params:
|
|
12
14
|
* region: us-east-1
|
|
13
15
|
* # ...or:
|
|
14
|
-
* prompt: inline # body is the prompt
|
|
15
|
-
* profile: opencode # optional
|
|
16
|
-
* # ...or:
|
|
17
16
|
* prompt: agent:my-agent # asset ref
|
|
18
17
|
* # ...or:
|
|
19
18
|
* prompt: ./prompts/my-prompt.md # relative file path
|
|
19
|
+
* # ...or:
|
|
20
|
+
* prompt: | # inline multi-line prompt (block scalar)
|
|
21
|
+
* Do the thing.
|
|
22
|
+
* And the other thing.
|
|
23
|
+
* # ...or:
|
|
24
|
+
* command: akm improve --auto-accept=90 --limit 25
|
|
20
25
|
* enabled: true # default true
|
|
26
|
+
* name: Daily backup
|
|
21
27
|
* description: …
|
|
28
|
+
* when_to_use: …
|
|
22
29
|
* tags: [scheduled, backup]
|
|
23
|
-
* ---
|
|
24
|
-
*
|
|
25
|
-
* # Task: Daily backup (optional notes; for prompt:inline this is the prompt)
|
|
26
30
|
* ```
|
|
27
31
|
*
|
|
28
32
|
* Validation lives in {@link validateTaskDocument}. The parser only enforces
|
|
@@ -30,29 +34,42 @@
|
|
|
30
34
|
* checked separately so callers can choose how strictly to surface errors.
|
|
31
35
|
*/
|
|
32
36
|
import path from "node:path";
|
|
37
|
+
import { parse as parseYaml } from "yaml";
|
|
33
38
|
import { UsageError } from "../core/errors";
|
|
34
|
-
import { parseFrontmatter } from "../core/frontmatter";
|
|
35
39
|
import { TASK_SCHEMA_VERSION } from "./schema";
|
|
36
40
|
export function parseTaskDocument(input) {
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const { yaml, filePath, id } = input;
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
const parsed = parseYaml(yaml);
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new UsageError(`Task "${id}" YAML must be a mapping (key: value pairs). File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
47
|
+
}
|
|
48
|
+
data = parsed;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof UsageError)
|
|
52
|
+
throw err;
|
|
53
|
+
throw new UsageError(`Task "${id}" has invalid YAML: ${err instanceof Error ? err.message : String(err)}. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
54
|
+
}
|
|
40
55
|
const schedule = readString(data.schedule, "schedule", filePath);
|
|
41
56
|
if (!schedule) {
|
|
42
|
-
throw new UsageError(`Task "${id}" is missing a schedule (
|
|
57
|
+
throw new UsageError(`Task "${id}" is missing a schedule (YAML key "schedule"). File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
43
58
|
}
|
|
44
59
|
const enabled = data.enabled === undefined ? true : data.enabled === true;
|
|
60
|
+
const name = readString(data.name, "name", filePath);
|
|
45
61
|
const description = readString(data.description, "description", filePath);
|
|
62
|
+
const when_to_use = readString(data.when_to_use, "when_to_use", filePath);
|
|
46
63
|
const tags = readStringArray(data.tags);
|
|
47
|
-
const hasWorkflow = "workflow" in data && data.workflow !== "";
|
|
48
|
-
const hasPrompt = "prompt" in data && data.prompt !== "";
|
|
49
|
-
const hasCommand = "command" in data && data.command !== "" && data.command
|
|
64
|
+
const hasWorkflow = "workflow" in data && data.workflow !== "" && data.workflow != null;
|
|
65
|
+
const hasPrompt = "prompt" in data && data.prompt !== "" && data.prompt != null;
|
|
66
|
+
const hasCommand = "command" in data && data.command !== "" && data.command != null;
|
|
50
67
|
const targetCount = [hasWorkflow, hasPrompt, hasCommand].filter(Boolean).length;
|
|
51
68
|
if (targetCount > 1) {
|
|
52
69
|
throw new UsageError(`Task "${id}" sets more than one of \`workflow\`, \`prompt\`, \`command\`; pick exactly one. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
53
70
|
}
|
|
54
71
|
if (targetCount === 0) {
|
|
55
|
-
throw new UsageError(`Task "${id}" must set one of \`workflow\`, \`prompt\`, or \`command
|
|
72
|
+
throw new UsageError(`Task "${id}" must set one of \`workflow\`, \`prompt\`, or \`command\`. File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
56
73
|
}
|
|
57
74
|
let target;
|
|
58
75
|
if (hasWorkflow) {
|
|
@@ -78,7 +95,7 @@ export function parseTaskDocument(input) {
|
|
|
78
95
|
const profile = readString(data.profile, "profile", filePath);
|
|
79
96
|
target = {
|
|
80
97
|
kind: "prompt",
|
|
81
|
-
source: resolvePromptSource(promptRaw,
|
|
98
|
+
source: resolvePromptSource(promptRaw, filePath, id),
|
|
82
99
|
profile: profile && profile.length > 0 ? profile : undefined,
|
|
83
100
|
};
|
|
84
101
|
}
|
|
@@ -101,30 +118,24 @@ export function parseTaskDocument(input) {
|
|
|
101
118
|
schedule,
|
|
102
119
|
enabled,
|
|
103
120
|
target,
|
|
121
|
+
name: name && name.length > 0 ? name : undefined,
|
|
104
122
|
description: description && description.length > 0 ? description : undefined,
|
|
123
|
+
when_to_use: when_to_use && when_to_use.length > 0 ? when_to_use : undefined,
|
|
105
124
|
tags: tags && tags.length > 0 ? tags : undefined,
|
|
106
125
|
source: { path: filePath },
|
|
107
126
|
timeoutMs,
|
|
108
127
|
};
|
|
109
128
|
}
|
|
110
129
|
/**
|
|
111
|
-
*
|
|
130
|
+
* Resolve a `prompt:` value into a {@link TaskPromptSource} variant.
|
|
112
131
|
*
|
|
113
|
-
* • "
|
|
114
|
-
* • "
|
|
115
|
-
* • "
|
|
116
|
-
* • anything else
|
|
117
|
-
* (the value itself is the prompt)
|
|
132
|
+
* • "<type>:<name>" (asset ref) → asset
|
|
133
|
+
* • "./foo.md", "../foo.md", "/abs" → file
|
|
134
|
+
* • "C:\\abs" (Windows absolute) → file
|
|
135
|
+
* • anything else (incl. block scalars) → inline text
|
|
118
136
|
*/
|
|
119
|
-
function resolvePromptSource(raw,
|
|
137
|
+
function resolvePromptSource(raw, filePath, id) {
|
|
120
138
|
const trimmed = raw.trim();
|
|
121
|
-
if (trimmed === "inline") {
|
|
122
|
-
const text = body.trim();
|
|
123
|
-
if (!text) {
|
|
124
|
-
throw new UsageError(`Task "${id}" sets \`prompt: inline\` but the markdown body is empty. File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
125
|
-
}
|
|
126
|
-
return { kind: "inline", text };
|
|
127
|
-
}
|
|
128
139
|
if (trimmed.startsWith("./") || trimmed.startsWith("../") || path.isAbsolute(trimmed)) {
|
|
129
140
|
return { kind: "file", path: trimmed };
|
|
130
141
|
}
|
|
@@ -134,7 +145,9 @@ function resolvePromptSource(raw, body, filePath, id) {
|
|
|
134
145
|
if (/^[a-z][a-z0-9_-]*:[^\s]/i.test(trimmed)) {
|
|
135
146
|
return { kind: "asset", ref: trimmed };
|
|
136
147
|
}
|
|
137
|
-
|
|
148
|
+
if (!trimmed) {
|
|
149
|
+
throw new UsageError(`Task "${id}" has empty \`prompt\`. File: ${filePath}`, "MISSING_REQUIRED_ARGUMENT");
|
|
150
|
+
}
|
|
138
151
|
return { kind: "inline", text: trimmed };
|
|
139
152
|
}
|
|
140
153
|
function readString(value, key, filePath) {
|
|
@@ -144,7 +157,7 @@ function readString(value, key, filePath) {
|
|
|
144
157
|
return value.trim();
|
|
145
158
|
if (typeof value === "number" || typeof value === "boolean")
|
|
146
159
|
return String(value);
|
|
147
|
-
throw new UsageError(`
|
|
160
|
+
throw new UsageError(`Key "${key}" must be a string. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
148
161
|
}
|
|
149
162
|
function readStringArray(value) {
|
|
150
163
|
if (value === undefined || value === null)
|
|
@@ -175,7 +188,7 @@ function readCommand(value, filePath, id) {
|
|
|
175
188
|
}
|
|
176
189
|
return parts;
|
|
177
190
|
}
|
|
178
|
-
throw new UsageError(`
|
|
191
|
+
throw new UsageError(`Key "command" must be a string or array of strings. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
179
192
|
}
|
|
180
193
|
function readParams(value, filePath) {
|
|
181
194
|
if (value === undefined || value === null)
|
|
@@ -194,5 +207,5 @@ function readParams(value, filePath) {
|
|
|
194
207
|
// fall through
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
|
-
throw new UsageError(`
|
|
210
|
+
throw new UsageError(`Key "params" must be a mapping or a JSON object. File: ${filePath}`, "INVALID_FLAG_VALUE");
|
|
198
211
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Resolve the absolute invocation that the OS scheduler should run.
|
|
3
6
|
*
|
package/dist/tasks/runner.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* `akm tasks run <id>` — what cron / launchd / schtasks invoke at the
|
|
3
6
|
* scheduled moment.
|
|
@@ -24,12 +27,13 @@ import path from "node:path";
|
|
|
24
27
|
import { parseAssetRef } from "../core/asset-ref";
|
|
25
28
|
import { resolveStashDir } from "../core/common";
|
|
26
29
|
import { loadConfig } from "../core/config";
|
|
27
|
-
import { NotFoundError } from "../core/errors";
|
|
30
|
+
import { NotFoundError, rethrowIfTestIsolationError } from "../core/errors";
|
|
28
31
|
import { getTaskLogDir } from "../core/paths";
|
|
29
32
|
import { getTaskHistory, openStateDatabase, queryTaskHistory, upsertTaskHistory } from "../core/state-db";
|
|
30
33
|
import { error } from "../core/warn";
|
|
31
34
|
import { requireAgentProfile, runAgent } from "../integrations/agent";
|
|
32
35
|
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
36
|
+
import { resolveRunner } from "../integrations/agent/runner";
|
|
33
37
|
import { resolveAssetPath } from "../sources/resolve";
|
|
34
38
|
import { startWorkflowRun } from "../workflows/runs";
|
|
35
39
|
import { parseTaskDocument } from "./parser";
|
|
@@ -40,8 +44,8 @@ export async function runTask(id, options = {}) {
|
|
|
40
44
|
const now = options.now ?? (() => new Date());
|
|
41
45
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
42
46
|
const filePath = await resolveAssetPath(stashDir, "task", id);
|
|
43
|
-
const
|
|
44
|
-
const task = parseTaskDocument({
|
|
47
|
+
const yaml = fs.readFileSync(filePath, "utf8");
|
|
48
|
+
const task = parseTaskDocument({ yaml, filePath, id });
|
|
45
49
|
const startedAt = now();
|
|
46
50
|
const startedIso = startedAt.toISOString();
|
|
47
51
|
const tsSlug = startedIso.replace(/[:.]/g, "-");
|
|
@@ -91,8 +95,8 @@ export async function runTask(id, options = {}) {
|
|
|
91
95
|
now,
|
|
92
96
|
runAgentImpl,
|
|
93
97
|
agentOptions: options.agentOptions,
|
|
94
|
-
agentConfig: config
|
|
95
|
-
agentTimeoutMs:
|
|
98
|
+
agentConfig: config,
|
|
99
|
+
agentTimeoutMs: undefined,
|
|
96
100
|
});
|
|
97
101
|
}
|
|
98
102
|
// ── command target ──────────────────────────────────────────────────────────
|
|
@@ -111,6 +115,7 @@ async function runCommandTask(input) {
|
|
|
111
115
|
stdin: "ignore",
|
|
112
116
|
stdout: "pipe",
|
|
113
117
|
stderr: "pipe",
|
|
118
|
+
cwd: process.env.HOME ?? "/tmp",
|
|
114
119
|
});
|
|
115
120
|
let timer;
|
|
116
121
|
let timedOut = false;
|
|
@@ -249,15 +254,35 @@ async function runPromptTask(input) {
|
|
|
249
254
|
// Use pre-resolved agent config when available to avoid redundant loadConfig()
|
|
250
255
|
// calls in batch task runs (Fix C6). Fall back to loadConfig() for callers
|
|
251
256
|
// that invoke runPromptTask directly without threading config.
|
|
252
|
-
const
|
|
257
|
+
const fullConfig = loadConfig();
|
|
258
|
+
const agentCfg = input.agentConfig !== undefined ? input.agentConfig : fullConfig;
|
|
253
259
|
// Resolve the profile for this task. When the task doc specifies a profile,
|
|
254
260
|
// use it directly. Otherwise fall back to the per-process config for "task"
|
|
255
261
|
// (agent.processes["task"]), which itself falls back to agent.default.
|
|
256
262
|
let profile;
|
|
257
263
|
let processTimeoutMs;
|
|
258
264
|
if (task.target.profile) {
|
|
259
|
-
//
|
|
260
|
-
|
|
265
|
+
// v2: if profiles.agent is configured, resolve through new runner
|
|
266
|
+
if (fullConfig.profiles?.agent) {
|
|
267
|
+
const mode = task.target.mode ?? "agent";
|
|
268
|
+
if (mode !== "llm") {
|
|
269
|
+
const runnerSpec = resolveRunner(mode, task.target.profile, fullConfig);
|
|
270
|
+
if (runnerSpec.kind === "agent" || runnerSpec.kind === "sdk") {
|
|
271
|
+
profile = runnerSpec.profile;
|
|
272
|
+
processTimeoutMs = runnerSpec.timeoutMs;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
profile = requireAgentProfile(agentCfg, task.target.profile);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
profile = requireAgentProfile(agentCfg, task.target.profile);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// v1: Task doc explicitly names a profile — honour it directly.
|
|
284
|
+
profile = requireAgentProfile(agentCfg, task.target.profile);
|
|
285
|
+
}
|
|
261
286
|
}
|
|
262
287
|
else {
|
|
263
288
|
// No per-task profile: use process config for "task" as a fallback.
|
|
@@ -273,7 +298,7 @@ async function runPromptTask(input) {
|
|
|
273
298
|
? processTimeoutMs
|
|
274
299
|
: input.agentTimeoutMs !== undefined
|
|
275
300
|
? input.agentTimeoutMs
|
|
276
|
-
:
|
|
301
|
+
: undefined;
|
|
277
302
|
const promptText = await resolvePromptText(task, stashDir);
|
|
278
303
|
const result = await runAgentImpl(profile, promptText, {
|
|
279
304
|
stdio: "captured",
|
|
@@ -362,6 +387,7 @@ function appendHistory(result) {
|
|
|
362
387
|
}
|
|
363
388
|
}
|
|
364
389
|
catch (err) {
|
|
390
|
+
rethrowIfTestIsolationError(err);
|
|
365
391
|
error(`[akm] task history DB write failed: ${String(err)}`);
|
|
366
392
|
}
|
|
367
393
|
}
|
package/dist/tasks/schedule.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Cross-platform schedule parsing and translation.
|
|
3
6
|
*
|
|
@@ -38,7 +41,7 @@ const FIELD_LIMITS = {
|
|
|
38
41
|
month: { min: 1, max: 12 },
|
|
39
42
|
dow: { min: 0, max: 6 },
|
|
40
43
|
};
|
|
41
|
-
const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`),
|
|
44
|
+
const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), step-on-star (`*/N`), and comma lists (`7,37`). " +
|
|
42
45
|
"Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`. " +
|
|
43
46
|
"Lists, ranges, and named days/months are not supported.";
|
|
44
47
|
export function parseSchedule(input, backend) {
|
|
@@ -103,6 +106,19 @@ function parseField(raw, name, limit, original) {
|
|
|
103
106
|
}
|
|
104
107
|
return { kind: "value", value };
|
|
105
108
|
}
|
|
109
|
+
// Comma-separated list: `7,37` or `0,15,30,45`. Each element must be an
|
|
110
|
+
// integer within [limit.min, limit.max]. Duplicates and unsorted input
|
|
111
|
+
// are accepted but the parsed form is deduped + ascending so downstream
|
|
112
|
+
// consumers can rely on a canonical shape.
|
|
113
|
+
if (/^\d+(,\d+)+$/.test(raw)) {
|
|
114
|
+
const values = [...new Set(raw.split(",").map((s) => Number(s)))].sort((a, b) => a - b);
|
|
115
|
+
for (const v of values) {
|
|
116
|
+
if (!Number.isInteger(v) || v < limit.min || v > limit.max) {
|
|
117
|
+
throw new UsageError(`Invalid ${name} list value "${v}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { kind: "list", values };
|
|
121
|
+
}
|
|
106
122
|
throw new UsageError(`Unsupported ${name} expression "${raw}" in schedule "${original}". ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE");
|
|
107
123
|
}
|
|
108
124
|
// ── Backend translators ─────────────────────────────────────────────────────
|
|
@@ -161,6 +177,9 @@ function rejectStepInsideCalendar(field, name, spec) {
|
|
|
161
177
|
if (field.kind === "step") {
|
|
162
178
|
throw new UsageError(`Schedule "${spec.raw}" uses step (${name} = */N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either restrict the step to the minute or hour field only, or rewrite the schedule with concrete values.");
|
|
163
179
|
}
|
|
180
|
+
if (field.kind === "list") {
|
|
181
|
+
throw new UsageError(`Schedule "${spec.raw}" uses comma list (${name} = a,b,...) which macOS launchd cannot express as a single trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either install one task per list element, or rewrite the schedule with a step (`*/N`) or single value.");
|
|
182
|
+
}
|
|
164
183
|
}
|
|
165
184
|
export function translateToSchtasks(spec) {
|
|
166
185
|
const f = spec.fields;
|
package/dist/tasks/schema.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Task asset schema. A task pairs a cron-style schedule with exactly one of:
|
|
3
6
|
*
|
|
@@ -6,8 +9,7 @@
|
|
|
6
9
|
* agent harness (e.g. `opencode run`)
|
|
7
10
|
* • a command target — invoked directly via `Bun.spawn()`, no AI agent
|
|
8
11
|
*
|
|
9
|
-
* Tasks are stored as
|
|
10
|
-
*
|
|
11
|
-
* markdown body is the prompt text.
|
|
12
|
+
* Tasks are stored as pure YAML files at `<stash>/tasks/<id>.yml`. Multi-line
|
|
13
|
+
* inline prompts use a YAML block scalar (`prompt: |`).
|
|
12
14
|
*/
|
|
13
15
|
export const TASK_SCHEMA_VERSION = 1;
|
package/dist/tasks/validator.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Validate a parsed {@link TaskDocument} for runnability.
|
|
3
6
|
*
|
|
@@ -36,12 +39,12 @@ export async function validateTaskDocument(task, options) {
|
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
41
|
// Prompt target. Resolve the profile unconditionally — when no profile is
|
|
39
|
-
// set on the task, requireAgentProfile falls back to
|
|
40
|
-
//
|
|
42
|
+
// set on the task, requireAgentProfile falls back to defaults.agent and
|
|
43
|
+
// throws a clear error if neither is configured. Catching this at
|
|
41
44
|
// `tasks add` / `tasks sync` time is much more useful than failing only
|
|
42
45
|
// when the OS scheduler fires.
|
|
43
46
|
const config = loadConfig();
|
|
44
|
-
requireAgentProfile(config
|
|
47
|
+
requireAgentProfile(config, task.target.profile);
|
|
45
48
|
const src = task.target.source;
|
|
46
49
|
if (src.kind === "asset") {
|
|
47
50
|
const stashDir = options.stashDir ?? resolveStashDir();
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
3
6
|
// Version: prefer compile-time define, then package.json, then fallback
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
import indexTemplate from "../assets/wiki/index-template.md" with { type: "text" };
|
|
5
|
+
import logTemplate from "../assets/wiki/log-template.md" with { type: "text" };
|
|
6
|
+
import schemaTemplate from "../assets/wiki/schema-template.md" with { type: "text" };
|
|
4
7
|
export function buildSchemaMd(wikiName) {
|
|
5
8
|
return schemaTemplate.replaceAll("{{WIKI_NAME}}", wikiName);
|
|
6
9
|
}
|
package/dist/wiki/wiki.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Multi-wiki support for akm (issue #119).
|
|
3
6
|
*
|
|
@@ -41,13 +44,13 @@
|
|
|
41
44
|
import fs from "node:fs";
|
|
42
45
|
import path from "node:path";
|
|
43
46
|
import { parse as yamlParse } from "yaml";
|
|
47
|
+
import ingestWorkflowTemplate from "../assets/wiki/ingest-workflow-template.md" with { type: "text" };
|
|
44
48
|
import { akmSearch } from "../commands/search";
|
|
45
49
|
import { isWithin, todayIso } from "../core/common";
|
|
46
50
|
import { getSources, loadUserConfig, saveConfig } from "../core/config";
|
|
47
51
|
import { NotFoundError, UsageError } from "../core/errors";
|
|
48
52
|
import { parseFrontmatter, parseFrontmatterBlock } from "../core/frontmatter";
|
|
49
53
|
import { resolveSourceEntries } from "../indexer/search-source";
|
|
50
|
-
import ingestWorkflowTemplate from "./ingest-workflow-template.md" with { type: "text" };
|
|
51
54
|
import { buildIndexMd, buildLogMd, buildSchemaMd } from "./wiki-templates";
|
|
52
55
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
53
56
|
export const WIKIS_SUBDIR = "wikis";
|
|
@@ -1,11 +1,14 @@
|
|
|
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/.
|
|
1
4
|
import fs from "node:fs";
|
|
2
5
|
import path from "node:path";
|
|
6
|
+
import workflowTemplate from "../assets/workflows/workflow-template.md" with { type: "text" };
|
|
3
7
|
import { resolveAssetPathFromName } from "../core/asset-spec";
|
|
4
8
|
import { isWithin, resolveStashDir } from "../core/common";
|
|
5
9
|
import { UsageError } from "../core/errors";
|
|
6
10
|
import { warn } from "../core/warn";
|
|
7
11
|
import { parseWorkflow } from "./parser";
|
|
8
|
-
import workflowTemplate from "./workflow-template.md" with { type: "text" };
|
|
9
12
|
const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
|
|
10
13
|
title: "Example Workflow",
|
|
11
14
|
firstStepTitle: "First Step",
|
package/dist/workflows/cli.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
import { UsageError } from "../core/errors";
|
|
2
5
|
export const WORKFLOW_STEP_STATES = [
|
|
3
6
|
"completed",
|
package/dist/workflows/db.js
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
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/.
|
|
1
4
|
import { Database } from "bun:sqlite";
|
|
2
5
|
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { getWorkflowDbPath } from "../core/paths";
|
|
8
|
+
/**
|
|
9
|
+
* workflow.db — Durable SQLite database for workflow run state.
|
|
10
|
+
*
|
|
11
|
+
* Owns the `workflow_runs` and `workflow_run_steps` tables that track active /
|
|
12
|
+
* completed workflow executions. Like `state.db` (and unlike `index.db`), the
|
|
13
|
+
* rows here are NON-REGENERABLE — losing them is data loss. Schema must evolve
|
|
14
|
+
* via incremental, additive migrations recorded in `schema_migrations`.
|
|
15
|
+
*
|
|
16
|
+
* ## Migration-safety contract
|
|
17
|
+
*
|
|
18
|
+
* The `schema_migrations` table records every applied migration by a stable
|
|
19
|
+
* string ID. `runMigrations(db)` is idempotent: new installs run every
|
|
20
|
+
* migration in order; upgrades run only the ones not yet applied. The
|
|
21
|
+
* migration framework here intentionally mirrors `src/core/state-db.ts` so
|
|
22
|
+
* future schema evolution follows a single proven pattern.
|
|
23
|
+
*
|
|
24
|
+
* Permitted schema evolution operations (always migration-safe in SQLite):
|
|
25
|
+
* - ALTER TABLE … ADD COLUMN <name> <type> DEFAULT <value>
|
|
26
|
+
* - CREATE INDEX IF NOT EXISTS …
|
|
27
|
+
* - CREATE TABLE IF NOT EXISTS … (additive new tables)
|
|
28
|
+
*
|
|
29
|
+
* ## Bootstrapping pre-versioning databases
|
|
30
|
+
*
|
|
31
|
+
* Workflow databases created before this file gained `schema_migrations`
|
|
32
|
+
* already have the `workflow_runs.scope_key` column applied by the previous
|
|
33
|
+
* ad-hoc `PRAGMA table_info` + `ALTER TABLE` code. To avoid re-running the
|
|
34
|
+
* migration (which would no-op but still wastes work and clutters logs), the
|
|
35
|
+
* runner detects this state and back-fills the `schema_migrations` row for
|
|
36
|
+
* the scope-key migration before evaluating the migration list. See
|
|
37
|
+
* `bootstrapPreVersioningDb()`.
|
|
38
|
+
*
|
|
39
|
+
* @module workflows/db
|
|
40
|
+
*/
|
|
41
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
5
42
|
export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
6
43
|
const dir = path.dirname(dbPath);
|
|
7
44
|
if (!fs.existsSync(dir)) {
|
|
@@ -10,18 +47,29 @@ export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
|
10
47
|
const db = new Database(dbPath);
|
|
11
48
|
db.exec("PRAGMA journal_mode = WAL");
|
|
12
49
|
db.exec("PRAGMA foreign_keys = ON");
|
|
13
|
-
|
|
50
|
+
ensureBaseSchema(db);
|
|
51
|
+
runMigrations(db);
|
|
14
52
|
return db;
|
|
15
53
|
}
|
|
16
54
|
export function closeWorkflowDatabase(db) {
|
|
17
55
|
db.close();
|
|
18
56
|
}
|
|
19
|
-
|
|
57
|
+
// ── Base schema ──────────────────────────────────────────────────────────────
|
|
58
|
+
/**
|
|
59
|
+
* Create the baseline `workflow_runs` and `workflow_run_steps` tables if they
|
|
60
|
+
* do not exist. These statements are idempotent: existing databases keep their
|
|
61
|
+
* current schema, and migrations evolve them further.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: the `scope_key` column on `workflow_runs` is intentionally NOT declared
|
|
64
|
+
* here. It is added by migration `001-add-scope-key`. Fresh databases will run
|
|
65
|
+
* the migration immediately on first open; pre-versioning databases that
|
|
66
|
+
* already have the column are bootstrapped — see {@link runMigrations}.
|
|
67
|
+
*/
|
|
68
|
+
function ensureBaseSchema(db) {
|
|
20
69
|
db.exec(`
|
|
21
70
|
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
22
71
|
id TEXT PRIMARY KEY,
|
|
23
72
|
workflow_ref TEXT NOT NULL,
|
|
24
|
-
scope_key TEXT,
|
|
25
73
|
workflow_entry_id INTEGER,
|
|
26
74
|
workflow_title TEXT NOT NULL,
|
|
27
75
|
status TEXT NOT NULL CHECK (status IN ('active', 'completed', 'blocked', 'failed')),
|
|
@@ -53,12 +101,94 @@ function ensureWorkflowSchema(db) {
|
|
|
53
101
|
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_sequence
|
|
54
102
|
ON workflow_run_steps(run_id, sequence_index);
|
|
55
103
|
`);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* All workflow.db migrations in application order. New migrations are
|
|
107
|
+
* APPENDED — never inserted in the middle or reordered.
|
|
108
|
+
*/
|
|
109
|
+
const MIGRATIONS = [
|
|
110
|
+
// ── Migration 001 — add scope_key column ────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// Adds the `scope_key` column to `workflow_runs` so runs can be partitioned
|
|
113
|
+
// per stash/scope. Pre-versioning databases that already have this column
|
|
114
|
+
// are bootstrapped before this migration runs — see runMigrations().
|
|
115
|
+
{
|
|
116
|
+
id: "001-add-scope-key",
|
|
117
|
+
up: `
|
|
118
|
+
ALTER TABLE workflow_runs ADD COLUMN scope_key TEXT;
|
|
119
|
+
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_scope_ref_status
|
|
121
|
+
ON workflow_runs(scope_key, workflow_ref, status);
|
|
122
|
+
`,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* Stable id of the scope_key migration. Exported for bootstrap detection and
|
|
127
|
+
* tests.
|
|
128
|
+
*/
|
|
129
|
+
const SCOPE_KEY_MIGRATION_ID = "001-add-scope-key";
|
|
130
|
+
/**
|
|
131
|
+
* Create the migrations table if it does not exist. Called unconditionally on
|
|
132
|
+
* every open so a fresh database bootstraps correctly.
|
|
133
|
+
*/
|
|
134
|
+
function ensureMigrationsTable(db) {
|
|
135
|
+
db.exec(`
|
|
136
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
137
|
+
id TEXT PRIMARY KEY,
|
|
138
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
139
|
+
);
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detect whether a column exists on a given table.
|
|
144
|
+
*/
|
|
145
|
+
function hasColumn(db, table, column) {
|
|
146
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all();
|
|
147
|
+
return rows.some((r) => r.name === column);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Back-fill `schema_migrations` rows for any schema state that existed before
|
|
151
|
+
* this file gained migration tracking.
|
|
152
|
+
*
|
|
153
|
+
* The pre-versioning code added the `scope_key` column on `workflow_runs` via
|
|
154
|
+
* an ad-hoc PRAGMA / ALTER TABLE pair. Those databases must not re-run the
|
|
155
|
+
* scope_key migration (the ALTER would fail with "duplicate column name"
|
|
156
|
+
* since the migration body does not use IF NOT EXISTS — SQLite does not
|
|
157
|
+
* support that clause on ALTER TABLE ADD COLUMN). Instead, we mark the
|
|
158
|
+
* migration as already applied.
|
|
159
|
+
*
|
|
160
|
+
* This function is a no-op on fresh databases: the `scope_key` column does
|
|
161
|
+
* not exist, so the migration runs normally and records itself.
|
|
162
|
+
*/
|
|
163
|
+
function bootstrapPreVersioningDb(db) {
|
|
164
|
+
const alreadyRecorded = db.prepare("SELECT 1 FROM schema_migrations WHERE id = ?").get(SCOPE_KEY_MIGRATION_ID);
|
|
165
|
+
if (alreadyRecorded)
|
|
166
|
+
return;
|
|
167
|
+
if (hasColumn(db, "workflow_runs", "scope_key")) {
|
|
168
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(SCOPE_KEY_MIGRATION_ID);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Apply every pending migration in a single transaction per migration.
|
|
173
|
+
*
|
|
174
|
+
* Each migration is applied in its own transaction so a failure in migration N
|
|
175
|
+
* does not roll back already-applied migrations 1..N-1. The migration row is
|
|
176
|
+
* inserted AFTER the DDL succeeds — a crash mid-migration leaves no row and
|
|
177
|
+
* the migration is retried on next open.
|
|
178
|
+
*
|
|
179
|
+
* Called automatically by {@link openWorkflowDatabase}.
|
|
180
|
+
*/
|
|
181
|
+
export function runMigrations(db) {
|
|
182
|
+
ensureMigrationsTable(db);
|
|
183
|
+
bootstrapPreVersioningDb(db);
|
|
184
|
+
const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
|
|
185
|
+
const applied = new Set(appliedRows.map((r) => r.id));
|
|
186
|
+
for (const migration of MIGRATIONS) {
|
|
187
|
+
if (applied.has(migration.id))
|
|
188
|
+
continue;
|
|
189
|
+
db.transaction(() => {
|
|
190
|
+
db.exec(migration.up);
|
|
191
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
|
|
192
|
+
})();
|
|
62
193
|
}
|
|
63
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_runs_scope_ref_status ON workflow_runs(scope_key, workflow_ref, status)");
|
|
64
194
|
}
|