akm-cli 0.9.0-rc.0 → 0.9.0-rc.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 +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import workflowTemplate from "../../assets/workflows/workflow-template.md" with { type: "text" };
|
|
7
|
-
import {
|
|
7
|
+
import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../../core/asset/asset-spec.js";
|
|
8
8
|
import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
9
9
|
import { UsageError } from "../../core/errors.js";
|
|
10
10
|
import { warn } from "../../core/warn.js";
|
|
11
|
+
import { compileWorkflowProgram } from "../ir/compile.js";
|
|
11
12
|
import { parseWorkflow } from "../parser.js";
|
|
13
|
+
import { parseWorkflowProgram } from "../program/parser.js";
|
|
14
|
+
import workflowProgramTemplate from "./workflow-program-template.yaml" with { type: "text" };
|
|
12
15
|
const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
|
|
13
16
|
title: "Example Workflow",
|
|
14
17
|
firstStepTitle: "First Step",
|
|
@@ -17,6 +20,15 @@ const DEFAULT_WORKFLOW_TEMPLATE = renderWorkflowTemplate({
|
|
|
17
20
|
export function getWorkflowTemplate() {
|
|
18
21
|
return DEFAULT_WORKFLOW_TEMPLATE;
|
|
19
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Minimal valid YAML workflow *program* (redesign addendum, R1), printed by
|
|
25
|
+
* `akm workflow template --yaml`. Kept as an external asset file per the repo
|
|
26
|
+
* convention (see `workflow-program-template.yaml` next to this module);
|
|
27
|
+
* `tests/workflows/program-assets.test.ts` pins that it parses AND compiles.
|
|
28
|
+
*/
|
|
29
|
+
export function getWorkflowProgramTemplate() {
|
|
30
|
+
return workflowProgramTemplate;
|
|
31
|
+
}
|
|
20
32
|
export function buildWorkflowTemplate(name) {
|
|
21
33
|
if (!name)
|
|
22
34
|
return DEFAULT_WORKFLOW_TEMPLATE;
|
|
@@ -33,25 +45,97 @@ export function buildWorkflowTemplate(name) {
|
|
|
33
45
|
}
|
|
34
46
|
return customized;
|
|
35
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Customize the shipped YAML program template ({@link getWorkflowProgramTemplate})
|
|
50
|
+
* for a named workflow: swap the placeholder `name:` for the workflow's slug so
|
|
51
|
+
* a freshly created `.yaml`/`.yml` asset round-trips through the program parser
|
|
52
|
+
* (its `title` becomes the slug). Parses AND compiles the result — mirroring
|
|
53
|
+
* {@link buildWorkflowTemplate} — so a create never writes an asset that
|
|
54
|
+
* `show`/`start`/`validate` would then reject.
|
|
55
|
+
*/
|
|
56
|
+
export function buildWorkflowProgramTemplate(name) {
|
|
57
|
+
if (!name)
|
|
58
|
+
return workflowProgramTemplate;
|
|
59
|
+
const programName = slugifyWorkflowStepId(name);
|
|
60
|
+
const customized = workflowProgramTemplate.replace(/^name:.*$/m, `name: ${programName}`);
|
|
61
|
+
const parsed = parseWorkflowProgram(customized, { path: `<template:${name}>` });
|
|
62
|
+
if (!parsed.ok) {
|
|
63
|
+
throw new UsageError(formatWorkflowErrors(`<template:${name}>`, parsed.errors));
|
|
64
|
+
}
|
|
65
|
+
const compiled = compileWorkflowProgram(parsed.program);
|
|
66
|
+
if (!compiled.ok) {
|
|
67
|
+
throw new UsageError(formatWorkflowErrors(`<template:${name}>`, compiled.errors));
|
|
68
|
+
}
|
|
69
|
+
return customized;
|
|
70
|
+
}
|
|
71
|
+
/** Recognized YAML workflow-program suffixes (see {@link WORKFLOW_EXTENSIONS}). */
|
|
72
|
+
const WORKFLOW_PROGRAM_SUFFIX_RE = /\.ya?ml$/i;
|
|
36
73
|
export function createWorkflowAsset(input) {
|
|
37
74
|
const stashDir = resolveStashDir();
|
|
38
75
|
const typeRoot = path.join(stashDir, "workflows");
|
|
39
76
|
fs.mkdirSync(typeRoot, { recursive: true });
|
|
77
|
+
// A `.yaml`/`.yml` name selects the YAML *program* format (redesign
|
|
78
|
+
// addendum, R1); capture the exact suffix the user typed so the written file
|
|
79
|
+
// keeps it, then strip every workflow extension to get the canonical name.
|
|
80
|
+
const suffixMatch = input.name.trim().replace(/\\/g, "/").match(WORKFLOW_PROGRAM_SUFFIX_RE);
|
|
81
|
+
const programSuffix = suffixMatch ? suffixMatch[0].toLowerCase() : undefined;
|
|
82
|
+
const isProgram = programSuffix !== undefined;
|
|
40
83
|
const normalizedName = normalizeWorkflowName(input.name);
|
|
41
|
-
|
|
84
|
+
// The write target is DEFINITIVE — the canonical name plus the chosen format's
|
|
85
|
+
// extension (`.yaml`/`.yml` for a program, `.md` for markdown). We deliberately
|
|
86
|
+
// do NOT go through `resolveAssetPathFromName`, which PROBES existing files and
|
|
87
|
+
// would redirect a markdown create onto an existing `foo.yaml` (writing
|
|
88
|
+
// markdown into a `.yaml`). Computing the target directly makes a markdown
|
|
89
|
+
// create always write `.md`, so the finding-C cross-extension check below sees
|
|
90
|
+
// the real collision instead of a self-match.
|
|
91
|
+
const targetSuffix = isProgram ? programSuffix : ".md";
|
|
92
|
+
const assetPath = path.join(typeRoot, `${normalizedName}${targetSuffix}`);
|
|
42
93
|
if (!isWithin(assetPath, typeRoot)) {
|
|
43
94
|
throw new UsageError(`Resolved workflow path escapes the stash: "${normalizedName}"`, "PATH_ESCAPE_VIOLATION");
|
|
44
95
|
}
|
|
96
|
+
// Codex round-3 finding C: a `workflow:<name>` ref is canonical across every
|
|
97
|
+
// recognized extension (`.md`/`.yaml`/`.yml`) and resolves `.md` BEFORE
|
|
98
|
+
// `.yaml`. So creating `foo.yaml` while `foo.md` exists would return the ref
|
|
99
|
+
// `workflow:foo` that still starts the OLD markdown workflow — a silently
|
|
100
|
+
// shadowed asset. Reject creation when ANY recognized extension already holds
|
|
101
|
+
// the same canonical name, naming the existing file. A same-extension collision
|
|
102
|
+
// (the target path itself exists) keeps the classic `--force` overwrite escape;
|
|
103
|
+
// a DIFFERENT-extension collision cannot be force-overwritten (writing a new
|
|
104
|
+
// file would leave the old one shadowing it) — remove the existing file first.
|
|
105
|
+
const existingPaths = WORKFLOW_EXTENSIONS.map((ext) => path.join(typeRoot, `${normalizedName}${ext}`)).filter((candidate) => fs.existsSync(candidate));
|
|
106
|
+
const conflicting = existingPaths.find((p) => p !== assetPath);
|
|
107
|
+
if (conflicting !== undefined) {
|
|
108
|
+
throw new UsageError(`Workflow "${normalizedName}" already exists as ${path.relative(stashDir, conflicting)} — the ` +
|
|
109
|
+
`\`workflow:${normalizedName}\` ref resolves to that file, so creating this one would shadow it. ` +
|
|
110
|
+
`Remove or rename the existing file first, or create the workflow under a different name.`, "RESOURCE_ALREADY_EXISTS");
|
|
111
|
+
}
|
|
45
112
|
if (fs.existsSync(assetPath) && !input.force) {
|
|
46
113
|
throw new UsageError(`Workflow "${normalizedName}" already exists. Re-run with --force to overwrite it.`, "RESOURCE_ALREADY_EXISTS");
|
|
47
114
|
}
|
|
48
115
|
const content = input.from
|
|
49
116
|
? readWorkflowSource(input.from, stashDir)
|
|
50
|
-
: (input.content ??
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
117
|
+
: (input.content ??
|
|
118
|
+
(isProgram ? buildWorkflowProgramTemplate(normalizedName) : buildWorkflowTemplate(normalizedName)));
|
|
119
|
+
const sourcePath = input.from ?? `workflows/${normalizedName}${isProgram ? programSuffix : ".md"}`;
|
|
120
|
+
// Validate against the format the destination extension selects — a YAML
|
|
121
|
+
// program parses+compiles as a program, markdown as a document — so the
|
|
122
|
+
// created asset is guaranteed usable by show/start/validate, which pick
|
|
123
|
+
// their parser by the same extension.
|
|
124
|
+
if (isProgram) {
|
|
125
|
+
const parsed = parseWorkflowProgram(content, { path: sourcePath });
|
|
126
|
+
if (!parsed.ok) {
|
|
127
|
+
throw new UsageError(formatWorkflowErrors(sourcePath, parsed.errors));
|
|
128
|
+
}
|
|
129
|
+
const compiled = compileWorkflowProgram(parsed.program);
|
|
130
|
+
if (!compiled.ok) {
|
|
131
|
+
throw new UsageError(formatWorkflowErrors(sourcePath, compiled.errors));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const result = parseWorkflow(content, { path: sourcePath });
|
|
136
|
+
if (!result.ok) {
|
|
137
|
+
throw new UsageError(formatWorkflowErrors(sourcePath, result.errors));
|
|
138
|
+
}
|
|
55
139
|
}
|
|
56
140
|
fs.mkdirSync(path.dirname(assetPath), { recursive: true });
|
|
57
141
|
fs.writeFileSync(assetPath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
|
|
@@ -83,11 +167,14 @@ function readWorkflowSource(source, stashDir) {
|
|
|
83
167
|
return fs.readFileSync(resolved, "utf8");
|
|
84
168
|
}
|
|
85
169
|
function normalizeWorkflowName(name) {
|
|
86
|
-
|
|
170
|
+
// Strip any recognized workflow extension (.md/.yaml/.yml) so the canonical
|
|
171
|
+
// name — and thus the `workflow:<name>` ref — is extension-free regardless of
|
|
172
|
+
// how the user spelled it. The chosen format is recovered from the raw suffix
|
|
173
|
+
// by the caller (createWorkflowAsset).
|
|
174
|
+
const normalized = canonicalizeWorkflowName(name
|
|
87
175
|
.trim()
|
|
88
176
|
.replace(/\\/g, "/")
|
|
89
|
-
.replace(/^\/+|\/+$/g, "")
|
|
90
|
-
.replace(/\.md$/i, "");
|
|
177
|
+
.replace(/^\/+|\/+$/g, ""));
|
|
91
178
|
if (!normalized) {
|
|
92
179
|
throw new UsageError("Workflow name cannot be empty.");
|
|
93
180
|
}
|
|
@@ -137,6 +224,32 @@ export function validateWorkflowSource(target) {
|
|
|
137
224
|
const content = fs.readFileSync(resolved, "utf8");
|
|
138
225
|
return { path: target, parse: parseWorkflow(content, { path: target }) };
|
|
139
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Validate a YAML workflow *program* by filesystem path: parse via
|
|
229
|
+
* `parseWorkflowProgram`, then — only when the parse is clean — compile via
|
|
230
|
+
* `compileWorkflowProgram` so expression/reference errors surface too. Both
|
|
231
|
+
* error lists carry line numbers and are returned in the same
|
|
232
|
+
* `WorkflowError[]` shape, ready for `formatWorkflowErrors`. Throws
|
|
233
|
+
* `UsageError` only when the target cannot be located on disk.
|
|
234
|
+
*/
|
|
235
|
+
export function validateWorkflowProgramSource(target) {
|
|
236
|
+
const resolved = path.resolve(target);
|
|
237
|
+
if (!fs.existsSync(resolved)) {
|
|
238
|
+
throw new UsageError(`Workflow file not found: "${target}".`);
|
|
239
|
+
}
|
|
240
|
+
const content = fs.readFileSync(resolved, "utf8");
|
|
241
|
+
const parse = parseWorkflowProgram(content, { path: target });
|
|
242
|
+
if (!parse.ok) {
|
|
243
|
+
return { path: target, result: { ok: false, errors: parse.errors } };
|
|
244
|
+
}
|
|
245
|
+
const compiled = compileWorkflowProgram(parse.program);
|
|
246
|
+
if (!compiled.ok) {
|
|
247
|
+
return { path: target, result: { ok: false, errors: compiled.errors } };
|
|
248
|
+
}
|
|
249
|
+
// Non-fatal advisories ride along on a successful validation (additive) so
|
|
250
|
+
// `workflow validate` can surface them without changing the ok verdict.
|
|
251
|
+
return { path: target, result: { ok: true, program: parse.program, warnings: compiled.warnings } };
|
|
252
|
+
}
|
|
140
253
|
function renderWorkflowTemplate(input) {
|
|
141
254
|
return workflowTemplate
|
|
142
255
|
.replace("{{TITLE}}", input.title)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# akm YAML workflow program (binding YAML v2).
|
|
2
|
+
# Save under workflows/<name>.yaml and lint with:
|
|
3
|
+
# akm workflow validate workflows/<name>.yaml
|
|
4
|
+
version: 2
|
|
5
|
+
name: example-workflow
|
|
6
|
+
description: Describe what this workflow accomplishes.
|
|
7
|
+
|
|
8
|
+
params:
|
|
9
|
+
example_param: { type: string, description: Explain this parameter }
|
|
10
|
+
|
|
11
|
+
defaults: # run-level defaults, overridable per unit
|
|
12
|
+
# engine: my-engine # optional; otherwise defaults.engine from config
|
|
13
|
+
timeout: 10m # "<n>ms" | "<n>s" | "<n>m" | "none"
|
|
14
|
+
on_error: fail # fail | continue
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- id: first-step
|
|
18
|
+
title: First Step
|
|
19
|
+
unit:
|
|
20
|
+
instructions: |
|
|
21
|
+
Describe what to do in this step.
|
|
22
|
+
Reference run parameters explicitly: ${{ params.example_param }}.
|
|
23
|
+
gate:
|
|
24
|
+
criteria:
|
|
25
|
+
- Confirm the first step is complete
|
|
26
|
+
|
|
27
|
+
- id: second-step
|
|
28
|
+
title: Second Step
|
|
29
|
+
unit:
|
|
30
|
+
instructions: |
|
|
31
|
+
Describe what happens next.
|
package/dist/workflows/cli.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
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 os from "node:os";
|
|
5
|
+
export const WORKFLOW_MAX_CONCURRENCY_CEILING = 64;
|
|
6
|
+
export function cpuDerivedUnitConcurrency(cpuCount = os.cpus()?.length ?? 4) {
|
|
7
|
+
return Math.min(16, Math.max(1, cpuCount - 2));
|
|
8
|
+
}
|
|
9
|
+
export function clampMaxConcurrency(value) {
|
|
10
|
+
return Math.min(WORKFLOW_MAX_CONCURRENCY_CEILING, Math.max(1, Math.floor(value)));
|
|
11
|
+
}
|
|
12
|
+
/** Resolve and freeze the engine-wide cap once when a workflow run starts. */
|
|
13
|
+
export function workflowMaxConcurrency(configured, cpuCount = os.cpus()?.length ?? 4) {
|
|
14
|
+
return configured === undefined ? cpuDerivedUnitConcurrency(cpuCount) : clampMaxConcurrency(configured);
|
|
15
|
+
}
|
package/dist/workflows/db.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { acquireMaintenanceActivitySync } from "../core/maintenance-barrier.js";
|
|
7
|
+
import { ensureMigrationBackup, getMigrationBackupDir } from "../core/migration-backup.js";
|
|
6
8
|
import { getWorkflowDbPath } from "../core/paths.js";
|
|
7
9
|
import { openDatabase } from "../storage/database.js";
|
|
8
10
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
@@ -42,18 +44,55 @@ import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
|
42
44
|
*/
|
|
43
45
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
44
46
|
export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const isCanonical = path.resolve(dbPath) === path.resolve(getWorkflowDbPath());
|
|
48
|
+
const releaseActivity = isCanonical ? acquireMaintenanceActivitySync("workflow-db") : undefined;
|
|
49
|
+
let db;
|
|
50
|
+
try {
|
|
51
|
+
// Preserve an originally absent workflow.db in the recovery manifest. SQLite
|
|
52
|
+
// creates the file at open time, so the cutover bundle must exist first.
|
|
53
|
+
if (isCanonical && !fs.existsSync(getMigrationBackupDir()))
|
|
54
|
+
ensureMigrationBackup();
|
|
55
|
+
const dir = path.dirname(dbPath);
|
|
56
|
+
if (!fs.existsSync(dir)) {
|
|
57
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
db = openDatabase(dbPath);
|
|
60
|
+
// #589: 30 s busy timeout, matching index.db / state.db. Without it the
|
|
61
|
+
// default is 0 ms, so any concurrent writer fails immediately with
|
|
62
|
+
// SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
|
|
63
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
64
|
+
ensureBaseSchema(db);
|
|
65
|
+
runMigrations(db, { ensureCutoverBackup: isCanonical });
|
|
66
|
+
if (!releaseActivity)
|
|
67
|
+
return db;
|
|
68
|
+
const openedDb = db;
|
|
69
|
+
let closed = false;
|
|
70
|
+
return {
|
|
71
|
+
prepare: openedDb.prepare.bind(openedDb),
|
|
72
|
+
exec: openedDb.exec.bind(openedDb),
|
|
73
|
+
run: openedDb.run.bind(openedDb),
|
|
74
|
+
transaction: openedDb.transaction.bind(openedDb),
|
|
75
|
+
get inTransaction() {
|
|
76
|
+
return openedDb.inTransaction;
|
|
77
|
+
},
|
|
78
|
+
close() {
|
|
79
|
+
if (closed)
|
|
80
|
+
return;
|
|
81
|
+
closed = true;
|
|
82
|
+
try {
|
|
83
|
+
openedDb.close();
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
releaseActivity();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
db?.close();
|
|
93
|
+
releaseActivity?.();
|
|
94
|
+
throw error;
|
|
48
95
|
}
|
|
49
|
-
const db = openDatabase(dbPath);
|
|
50
|
-
// #589: 30 s busy timeout, matching index.db / state.db. Without it the
|
|
51
|
-
// default is 0 ms, so any concurrent writer fails immediately with
|
|
52
|
-
// SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
|
|
53
|
-
applyStandardPragmas(db, { dataDir: dir });
|
|
54
|
-
ensureBaseSchema(db);
|
|
55
|
-
runMigrations(db);
|
|
56
|
-
return db;
|
|
57
96
|
}
|
|
58
97
|
export function closeWorkflowDatabase(db) {
|
|
59
98
|
db.close();
|
|
@@ -165,6 +204,148 @@ const MIGRATIONS = [
|
|
|
165
204
|
ALTER TABLE workflow_run_steps ADD COLUMN summary TEXT;
|
|
166
205
|
`,
|
|
167
206
|
},
|
|
207
|
+
// ── Migration 004 — per-unit run state (orchestration plan P1) ──────────────
|
|
208
|
+
//
|
|
209
|
+
// A step's execution may now fan out into N concurrent units (native
|
|
210
|
+
// executor, docs/technical/akm-workflows-orchestration-plan.md). Units hang
|
|
211
|
+
// off the gated step spine: `workflow_run_steps` stays the durable top-level
|
|
212
|
+
// record; each dispatched unit gets its own row here so a crash-and-resume
|
|
213
|
+
// re-dispatches only incomplete units (durable-row resume) and budget/usage
|
|
214
|
+
// is attributable per unit. `input_hash` is reserved for the P5 deterministic
|
|
215
|
+
// replay mode; `failure_reason` carries runAgent's structured failure
|
|
216
|
+
// vocabulary so retry/continue-on-error policy has semantics to act on.
|
|
217
|
+
{
|
|
218
|
+
id: "004-workflow-run-units",
|
|
219
|
+
up: `
|
|
220
|
+
CREATE TABLE IF NOT EXISTS workflow_run_units (
|
|
221
|
+
run_id TEXT NOT NULL,
|
|
222
|
+
unit_id TEXT NOT NULL,
|
|
223
|
+
step_id TEXT,
|
|
224
|
+
node_id TEXT NOT NULL,
|
|
225
|
+
parent_unit_id TEXT,
|
|
226
|
+
phase TEXT,
|
|
227
|
+
runner TEXT,
|
|
228
|
+
model TEXT,
|
|
229
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
|
|
230
|
+
input_hash TEXT,
|
|
231
|
+
result_json TEXT,
|
|
232
|
+
tokens INTEGER,
|
|
233
|
+
failure_reason TEXT,
|
|
234
|
+
worktree_path TEXT,
|
|
235
|
+
started_at TEXT,
|
|
236
|
+
finished_at TEXT,
|
|
237
|
+
PRIMARY KEY (run_id, unit_id),
|
|
238
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_units_run_step
|
|
242
|
+
ON workflow_run_units(run_id, step_id);
|
|
243
|
+
`,
|
|
244
|
+
},
|
|
245
|
+
// ── Migration 005 — harness-native unit session id (plan P2) ────────────────
|
|
246
|
+
//
|
|
247
|
+
// The P2 harness adapters' result extractors reveal the harness-native
|
|
248
|
+
// session id of a dispatched unit (codex `session_configured`, gemini/pi
|
|
249
|
+
// JSON envelopes, the opencode SDK session). It is stored opportunistically
|
|
250
|
+
// on the unit row so resume can replay the harness's own context cache
|
|
251
|
+
// (e.g. `codex exec resume <id>`, `gemini --resume <id>`); akm never
|
|
252
|
+
// *depends* on it — `workflow_run_units` remains the durable source of
|
|
253
|
+
// truth (plan §"Session, MCP, and identity across harnesses").
|
|
254
|
+
{
|
|
255
|
+
id: "005-unit-session-id",
|
|
256
|
+
up: `
|
|
257
|
+
ALTER TABLE workflow_run_units ADD COLUMN session_id TEXT;
|
|
258
|
+
`,
|
|
259
|
+
},
|
|
260
|
+
// ── Migration 006 — frozen plan + engine lease (redesign addendum, R1) ──────
|
|
261
|
+
//
|
|
262
|
+
// `workflow start` now compiles the workflow into its plan graph ONCE and
|
|
263
|
+
// freezes it on the run row: `plan_json` holds the canonical plan JSON
|
|
264
|
+
// (`ir/plan-hash.ts`) and `plan_hash` its sha256, so every subsequent
|
|
265
|
+
// invocation executes the frozen snapshot with an integrity check — the
|
|
266
|
+
// source file is never re-read for an in-flight run. Runs created before
|
|
267
|
+
// this migration have NULL plan_json (legacy) and fall back to
|
|
268
|
+
// compile-from-asset with a warning.
|
|
269
|
+
//
|
|
270
|
+
// `engine_lease_until` / `engine_lease_holder` reserve the run-lease columns
|
|
271
|
+
// (a second `workflow run` on a leased run refuses up front). TODO(R2):
|
|
272
|
+
// lease ENFORCEMENT is engine-rework scope — only the columns land now.
|
|
273
|
+
{
|
|
274
|
+
id: "006-frozen-plan-and-lease",
|
|
275
|
+
up: `
|
|
276
|
+
ALTER TABLE workflow_runs ADD COLUMN plan_json TEXT;
|
|
277
|
+
ALTER TABLE workflow_runs ADD COLUMN plan_hash TEXT;
|
|
278
|
+
ALTER TABLE workflow_runs ADD COLUMN engine_lease_until TEXT;
|
|
279
|
+
ALTER TABLE workflow_runs ADD COLUMN engine_lease_holder TEXT;
|
|
280
|
+
`,
|
|
281
|
+
},
|
|
282
|
+
// ── Migration 007 — unit-level check-in heartbeat (redesign addendum, R3) ────
|
|
283
|
+
//
|
|
284
|
+
// The harness-neutral driver protocol lets ANY agent session claim and
|
|
285
|
+
// heartbeat a unit it is executing via `akm workflow report --status running`.
|
|
286
|
+
// `last_checkin_at` records the most recent heartbeat (distinct from
|
|
287
|
+
// `started_at`, the first claim) so a pure timestamp evaluator
|
|
288
|
+
// (`runtime/unit-checkin.ts`) can surface a claimed-but-silent unit as stale
|
|
289
|
+
// in `workflow brief` without any background thread. Nullable and additive;
|
|
290
|
+
// engine-dispatched rows never set it (they finish before a heartbeat window
|
|
291
|
+
// could elapse), so their transient `running` state is judged from
|
|
292
|
+
// `started_at`.
|
|
293
|
+
{
|
|
294
|
+
id: "007-unit-last-checkin",
|
|
295
|
+
up: `
|
|
296
|
+
ALTER TABLE workflow_run_units ADD COLUMN last_checkin_at TEXT;
|
|
297
|
+
`,
|
|
298
|
+
},
|
|
299
|
+
// ── Migration 008 — per-unit dispatch-attempt counter (PR #714 review, P2) ───
|
|
300
|
+
//
|
|
301
|
+
// `workflow_run_units.unit_id` is CONTENT-derived and stable across
|
|
302
|
+
// crash/resume (retries/loops carry `~r<n>`/`~l<loop>` suffixes, so they are
|
|
303
|
+
// DISTINCT rows). A crash between a unit's dispatch (`insertUnit`, status
|
|
304
|
+
// `running`) and its finish leaves a stale `running` row; durable-row resume
|
|
305
|
+
// re-dispatches the SAME unit_id and `insertUnit` REPLACES that single row.
|
|
306
|
+
// Because the run's budget/lifetime seed was derived from the NUMBER of unit
|
|
307
|
+
// rows, each crash/resume of one unit erased the prior dispatch from
|
|
308
|
+
// `budget.max_units` / lifetime-cap accounting, letting a run spend past its
|
|
309
|
+
// declared ceiling. `attempts` counts how many times a row was (re)dispatched
|
|
310
|
+
// — incremented by `insertUnit` on every REPLACE of an existing row — so both
|
|
311
|
+
// budget seeds sum `attempts` instead of counting rows and crash-retried
|
|
312
|
+
// dispatches are charged. Existing rows back-fill to 1 (one dispatch each).
|
|
313
|
+
{
|
|
314
|
+
id: "008-unit-attempts",
|
|
315
|
+
up: `
|
|
316
|
+
ALTER TABLE workflow_run_units ADD COLUMN attempts INTEGER NOT NULL DEFAULT 1;
|
|
317
|
+
`,
|
|
318
|
+
},
|
|
319
|
+
// ── Migration 009 — per-unit claim ownership (PR #714 review round 2) ─────────
|
|
320
|
+
//
|
|
321
|
+
// `akm workflow report --status running` claims a unit before executing it.
|
|
322
|
+
// Round-2 review (#3): a running row had no claim owner, no compare-and-set,
|
|
323
|
+
// and no stale-hash guard, so a stale/tampered `running` row could be
|
|
324
|
+
// finalized by anyone with a fresh result while keeping an old input_hash.
|
|
325
|
+
// These columns record who holds the claim (`claim_holder` — the driver's
|
|
326
|
+
// `--session-id` or a token report mints and returns) and until when
|
|
327
|
+
// (`claim_expires_at`, ISO-8601 UTC). Heartbeating or finishing a live-claimed
|
|
328
|
+
// running row requires the matching holder; an EXPIRED claim is reclaimable by
|
|
329
|
+
// a new holder (crash recovery). The TTL equals the unit-checkin stale window
|
|
330
|
+
// (`runtime/unit-checkin.UNIT_STALE_MS`), so an expired claim is exactly a unit
|
|
331
|
+
// that `workflow brief` already surfaces as stale. Both columns are nullable
|
|
332
|
+
// and additive: engine-dispatched rows and simple claimless drivers leave them
|
|
333
|
+
// NULL, and finishing a never-claimed unit stays allowed (input_hash is still
|
|
334
|
+
// validated always).
|
|
335
|
+
{
|
|
336
|
+
id: "009-unit-claim",
|
|
337
|
+
up: `
|
|
338
|
+
ALTER TABLE workflow_run_units ADD COLUMN claim_holder TEXT;
|
|
339
|
+
ALTER TABLE workflow_run_units ADD COLUMN claim_expires_at TEXT;
|
|
340
|
+
`,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "010-ir-v3-engine",
|
|
344
|
+
up: `
|
|
345
|
+
ALTER TABLE workflow_runs ADD COLUMN plan_ir_version INTEGER;
|
|
346
|
+
ALTER TABLE workflow_run_units ADD COLUMN engine TEXT;
|
|
347
|
+
`,
|
|
348
|
+
},
|
|
168
349
|
];
|
|
169
350
|
/**
|
|
170
351
|
* Stable id of the scope_key migration. Exported for bootstrap detection and
|
|
@@ -210,6 +391,12 @@ function bootstrapPreVersioningDb(db) {
|
|
|
210
391
|
*
|
|
211
392
|
* Called automatically by {@link openWorkflowDatabase}.
|
|
212
393
|
*/
|
|
213
|
-
export function runMigrations(db) {
|
|
214
|
-
runSqliteMigrations(db, MIGRATIONS, {
|
|
394
|
+
export function runMigrations(db, options) {
|
|
395
|
+
runSqliteMigrations(db, MIGRATIONS, {
|
|
396
|
+
bootstrap: bootstrapPreVersioningDb,
|
|
397
|
+
beforeMigration(migration) {
|
|
398
|
+
if (options?.ensureCutoverBackup && migration.id === "010-ir-v3-engine")
|
|
399
|
+
ensureMigrationBackup();
|
|
400
|
+
},
|
|
401
|
+
});
|
|
215
402
|
}
|