akm-cli 0.9.0-beta.9 → 0.9.0-rc.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/CHANGELOG.md +715 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +228 -292
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +291 -13
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +79 -5
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +403 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/json-schema.js +142 -0
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +261 -770
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +48 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +41 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +79 -6
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +83 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +206 -81
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
- package/dist/integrations/harnesses/claude/index.js +27 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +28 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +17 -16
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -0
- package/dist/integrations/session-logs/index.js +40 -11
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +87 -15
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +6 -1
- package/dist/output/text/helpers.js +241 -2
- package/dist/output/text/workflow.js +4 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +2743 -8390
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +5 -0
- package/dist/workflows/db.js +138 -4
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -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 +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +248 -153
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
- package/dist/workflows/validate-summary.js +26 -10
- package/dist/workflows/validator.js +1 -1
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +7 -5
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -0,0 +1,596 @@
|
|
|
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
|
+
* Engine-driven workflow execution — `akm workflow run` (orchestration plan
|
|
6
|
+
* P1, owner decision 5). akm itself walks the plan and dispatches units; the
|
|
7
|
+
* existing `next`/`complete` loop remains for manual/agent-driven advancement
|
|
8
|
+
* of the same runs.
|
|
9
|
+
*
|
|
10
|
+
* Invariant (plan §*Never bypass the gate spine*): every step advances
|
|
11
|
+
* through `completeWorkflowStep`, never by writing step rows directly, so the
|
|
12
|
+
* summary-validation gate and run-state derivation stay authoritative. A gate
|
|
13
|
+
* rejection (SummaryValidationFailure) STOPS the engine and surfaces the
|
|
14
|
+
* corrective feedback — a gate is a gate, even for the engine.
|
|
15
|
+
*
|
|
16
|
+
* Artifact-judging gates (redesign addendum, R2): when a step declares
|
|
17
|
+
* completion criteria, the engine hands the gate a summary BUILT FROM the
|
|
18
|
+
* step's promoted artifact (canonical JSON, clipped, prefixed with a one-line
|
|
19
|
+
* unit count — `buildArtifactSummary`) instead of the machine-prose execution
|
|
20
|
+
* summary, so the judge evaluates real results. Each engine-driven judge call
|
|
21
|
+
* is journaled as a unit row (`node_id "<stepId>.gate"`, `unit_id
|
|
22
|
+
* "<stepId>.gate:l<loop>"`, runner "llm", result_json = the verdict) through
|
|
23
|
+
* the writer queue — it is an LLM call like any other. Human approvals are
|
|
24
|
+
* never cached: a blocked gate stays blocked.
|
|
25
|
+
*
|
|
26
|
+
* Bounded gate loops (`gate.max_loops`, addendum R2): a rejection on a step
|
|
27
|
+
* with maxLoops > 1 re-executes the step subgraph with the judge's feedback +
|
|
28
|
+
* missing[] threaded into every unit prompt (`gateFeedback` on
|
|
29
|
+
* StepExecutionContext) — the feedback changes each unit's input hash, so the
|
|
30
|
+
* loop re-dispatches naturally instead of reusing the rejected rows. After
|
|
31
|
+
* maxLoops rejections the engine stops with the gate feedback, exactly like
|
|
32
|
+
* the one-shot case. A typed-artifact schema mismatch feeds the same loop
|
|
33
|
+
* (the validation errors are the feedback; no judge ran, so no gate unit is
|
|
34
|
+
* journaled for that attempt) — only the FINAL loop's mismatch fails the run.
|
|
35
|
+
*
|
|
36
|
+
* Frozen plan (redesign addendum, R1): the plan graph is read from the run
|
|
37
|
+
* row (`plan_json`, persisted by `startWorkflowRun` under migration 006) with
|
|
38
|
+
* a `plan_hash` integrity check — the workflow asset file is NEVER re-read
|
|
39
|
+
* for an in-flight run, so a mid-run asset edit cannot change behavior.
|
|
40
|
+
* Legacy runs (created before migration 006, NULL plan_json) fall back to
|
|
41
|
+
* compile-from-asset with a warning. Durable-row resume: re-invoking a
|
|
42
|
+
* partially-executed run re-dispatches only work that never completed.
|
|
43
|
+
*
|
|
44
|
+
* Run lease (redesign addendum, R2): exactly one engine invocation drives a
|
|
45
|
+
* run at a time. The lease (random holder id + 90s expiry on the run row) is
|
|
46
|
+
* acquired before any dispatch, renewed between steps, and released in a
|
|
47
|
+
* `finally`; a second `workflow run` on a live-leased run refuses up front,
|
|
48
|
+
* and an expired lease is claimable (crash recovery). While the lease is
|
|
49
|
+
* live, manual `workflow complete` is refused too — the engine owns the
|
|
50
|
+
* spine while driving (enforced inside `completeWorkflowStep`).
|
|
51
|
+
*
|
|
52
|
+
* Process-lifecycle contract (owner finding 4 — no leaked handles): the SDK
|
|
53
|
+
* dispatch path caches `opencode serve` CHILD PROCESSES in a per-env registry
|
|
54
|
+
* for reuse across units. Each live child is an OS handle that keeps Bun's
|
|
55
|
+
* event loop open; the registry's own teardown is wired only to
|
|
56
|
+
* `process.once('exit')`, which never fires while a child holds the loop open.
|
|
57
|
+
* That deadlock hangs a one-shot CLI (`akm workflow run` has no `process.exit`
|
|
58
|
+
* on success — it relies on the loop draining). The engine therefore DRAINS
|
|
59
|
+
* the dispatch registry ({@link disposeDispatchResources}) in its run `finally`,
|
|
60
|
+
* on EVERY exit path, so the process exits cleanly the moment the run resolves.
|
|
61
|
+
* The drain is synchronous, idempotent, and a no-op when no SDK server started.
|
|
62
|
+
*/
|
|
63
|
+
import { randomUUID } from "node:crypto";
|
|
64
|
+
import { UsageError } from "../../core/errors.js";
|
|
65
|
+
import { warn } from "../../core/warn.js";
|
|
66
|
+
import { disposeDispatchResources } from "../../integrations/agent/runner-dispatch.js";
|
|
67
|
+
import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
|
|
68
|
+
import { assertRunParamsSatisfyPlan } from "../ir/params.js";
|
|
69
|
+
import { completeWorkflowStep, getNextWorkflowStep } from "../runtime/runs.js";
|
|
70
|
+
import { compileWorkflowAssetPlan, loadWorkflowAsset } from "../runtime/workflow-asset-loader.js";
|
|
71
|
+
import { executeStepPlan } from "./native-executor.js";
|
|
72
|
+
// Shared step semantics — route evaluation + cascaded-skip bookkeeping,
|
|
73
|
+
// gate-evaluation journaling, and the whole step-completion path
|
|
74
|
+
// (`finalizeExecutedStep`) live in step-work.ts so the engine loop and the R3
|
|
75
|
+
// brief/report driver protocol share ONE implementation (no drift).
|
|
76
|
+
import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE, parseFrozenPlan, recoverGateFeedback, seedJournaledRouteDecisions, } from "./step-work.js";
|
|
77
|
+
export async function runWorkflowSteps(options) {
|
|
78
|
+
const next = await getNextWorkflowStep(options.target, options.params);
|
|
79
|
+
// Refuse non-active runs BEFORE any dispatch — completeWorkflowStep would
|
|
80
|
+
// reject the completion anyway, but only after the units already ran (and
|
|
81
|
+
// cost money). Mirror its preflight up front.
|
|
82
|
+
if (!next.done && next.run.status !== "active") {
|
|
83
|
+
throw new UsageError(`Workflow run ${next.run.id} is ${next.run.status} and cannot be executed. ` +
|
|
84
|
+
`Use \`akm workflow resume ${next.run.id}\` to reopen it first.`);
|
|
85
|
+
}
|
|
86
|
+
// Run lease (R2 single-driver enforcement): claim the run BEFORE any
|
|
87
|
+
// dispatch — a second `akm workflow run` on a live-leased run refuses up
|
|
88
|
+
// front instead of racing the first engine's spine. An expired lease is
|
|
89
|
+
// claimable (crash recovery). Released in the finally below; renewed
|
|
90
|
+
// between steps inside the loop. A done run takes no lease: nothing will
|
|
91
|
+
// dispatch, and the status re-read below must stay a pure no-op.
|
|
92
|
+
const runId = next.run.id;
|
|
93
|
+
const leaseHolder = randomUUID();
|
|
94
|
+
const leased = !next.done;
|
|
95
|
+
if (leased) {
|
|
96
|
+
await acquireRunLease(runId, leaseHolder);
|
|
97
|
+
}
|
|
98
|
+
// Lease heartbeat (P1 fix): the lease TTL is renewed BETWEEN steps, but a
|
|
99
|
+
// single unit's dispatch can outlive the TTL (the default unit timeout is 10
|
|
100
|
+
// minutes, > the 90s lease). An unheartbeated lease would silently expire
|
|
101
|
+
// mid-dispatch, letting a second `akm workflow run` claim the run and
|
|
102
|
+
// re-dispatch the same units — the two engines clobber each other's journal
|
|
103
|
+
// rows and double-run side effects. A timer INSIDE this invocation renews the
|
|
104
|
+
// lease while dispatch is in flight; it is cleared in the `finally`, so it
|
|
105
|
+
// dies with the process — exactly when the lease SHOULD become claimable
|
|
106
|
+
// after TTL. A renewal that fails (the lease was genuinely stolen after an
|
|
107
|
+
// expiry, e.g. the process was suspended) aborts dispatch and fails the run
|
|
108
|
+
// loudly rather than keep double-driving.
|
|
109
|
+
const heartbeat = leased
|
|
110
|
+
? new LeaseHeartbeat(runId, leaseHolder, options.heartbeatScheduler, options.signal)
|
|
111
|
+
: undefined;
|
|
112
|
+
heartbeat?.start();
|
|
113
|
+
try {
|
|
114
|
+
return await driveRun(options, next, leaseHolder, heartbeat);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
heartbeat?.stop();
|
|
118
|
+
try {
|
|
119
|
+
if (leased) {
|
|
120
|
+
await withWorkflowRunsRepo((repo) => {
|
|
121
|
+
repo.releaseEngineLease(runId, leaseHolder);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
// Process-lifecycle drain (owner finding 4): release any cached SDK server
|
|
127
|
+
// child processes so a one-shot CLI invocation exits cleanly instead of
|
|
128
|
+
// hanging on the leaked handle. Runs even if lease release itself fails;
|
|
129
|
+
// a teardown-time repository error must not skip dispatch cleanup.
|
|
130
|
+
try {
|
|
131
|
+
(options.disposeDispatchResources ?? disposeDispatchResources)();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* disposal is best-effort; never let cleanup mask the run outcome */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Lease lifetime: long enough to survive slow steps between renewals, short
|
|
140
|
+
* enough that a crashed engine frees the run quickly. Renewed per step. */
|
|
141
|
+
const RUN_LEASE_TTL_MS = 90_000;
|
|
142
|
+
function leaseExpiry() {
|
|
143
|
+
return new Date(Date.now() + RUN_LEASE_TTL_MS).toISOString();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Atomically claim the run lease or refuse with a UsageError naming the
|
|
147
|
+
* current holder + expiry. The single-UPDATE claim in the repository is the
|
|
148
|
+
* arbiter — two racing invocations cannot both win.
|
|
149
|
+
*/
|
|
150
|
+
async function acquireRunLease(runId, holder) {
|
|
151
|
+
await withWorkflowRunsRepo((repo) => {
|
|
152
|
+
if (repo.acquireEngineLease(runId, holder, leaseExpiry(), new Date().toISOString()))
|
|
153
|
+
return;
|
|
154
|
+
const row = repo.getRunById(runId);
|
|
155
|
+
throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
|
|
156
|
+
`(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
|
|
157
|
+
`wait for that invocation to finish or for the lease to expire.`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Renew the lease between steps. Losing the lease mid-run (it expired during
|
|
162
|
+
* a long step and another engine claimed it) is a hard stop: the new owner
|
|
163
|
+
* drives the spine now, and continuing would race it.
|
|
164
|
+
*/
|
|
165
|
+
async function renewRunLease(runId, holder) {
|
|
166
|
+
await withWorkflowRunsRepo((repo) => {
|
|
167
|
+
if (repo.renewEngineLease(runId, holder, leaseExpiry()))
|
|
168
|
+
return;
|
|
169
|
+
const row = repo.getRunById(runId);
|
|
170
|
+
throw new UsageError(`Workflow run ${runId} lost its run lease (now held by ${row?.engine_lease_holder ?? "(nobody)"}). ` +
|
|
171
|
+
`Another engine invocation claimed the run after this one's lease expired — stopping to avoid racing it.`);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** Renew mid-dispatch this often. Well under the TTL so a slow/skipped tick
|
|
175
|
+
* still leaves ample margin before the lease would expire. */
|
|
176
|
+
const HEARTBEAT_INTERVAL_MS = RUN_LEASE_TTL_MS / 3;
|
|
177
|
+
/** Real timer: an unref'd interval so a live heartbeat never keeps the process alive. */
|
|
178
|
+
function defaultHeartbeatScheduler(tick) {
|
|
179
|
+
const id = setInterval(() => void tick(), HEARTBEAT_INTERVAL_MS);
|
|
180
|
+
id.unref?.();
|
|
181
|
+
return () => clearInterval(id);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Keeps the run lease alive while a step dispatches (P1 fix — the between-step
|
|
185
|
+
* renewal cannot cover a unit that runs longer than the TTL). A timer inside
|
|
186
|
+
* the engine invocation renews the lease through the holder-guarded
|
|
187
|
+
* {@link renewEngineLease}; the heartbeat owns an {@link AbortController}
|
|
188
|
+
* (chained onto the caller's signal) that becomes the effective DISPATCH
|
|
189
|
+
* signal, so a lost lease aborts in-flight dispatch PROMPTLY. After the abort,
|
|
190
|
+
* {@link assertAlive} throws a loud UsageError, so the engine stops instead of
|
|
191
|
+
* continuing to drive a run another engine now owns. No background daemon: the
|
|
192
|
+
* timer is cleared in the caller's `finally` and dies with the process.
|
|
193
|
+
*/
|
|
194
|
+
class LeaseHeartbeat {
|
|
195
|
+
runId;
|
|
196
|
+
holder;
|
|
197
|
+
controller = new AbortController();
|
|
198
|
+
detachUpstream;
|
|
199
|
+
schedule;
|
|
200
|
+
cancel;
|
|
201
|
+
renewing = false;
|
|
202
|
+
/** Set once a renewal failed — the lease was stolen after a genuine expiry. */
|
|
203
|
+
lost = false;
|
|
204
|
+
/** The holder that stole the lease, captured for the loud error. */
|
|
205
|
+
stolenBy = null;
|
|
206
|
+
constructor(runId, holder, scheduler, upstream) {
|
|
207
|
+
this.runId = runId;
|
|
208
|
+
this.holder = holder;
|
|
209
|
+
this.schedule = scheduler ?? defaultHeartbeatScheduler;
|
|
210
|
+
// A caller abort (Ctrl-C, budget) must abort dispatch too; chain it into
|
|
211
|
+
// the effective signal. Distinct from a lost lease: a caller abort does
|
|
212
|
+
// NOT set `lost`, so `assertAlive` stays quiet and the existing graceful
|
|
213
|
+
// break on `options.signal` handles it.
|
|
214
|
+
if (upstream) {
|
|
215
|
+
if (upstream.aborted) {
|
|
216
|
+
this.controller.abort();
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const onAbort = () => this.controller.abort();
|
|
220
|
+
upstream.addEventListener("abort", onAbort, { once: true });
|
|
221
|
+
this.detachUpstream = () => upstream.removeEventListener("abort", onAbort);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** The effective dispatch signal: aborts on a lost lease OR a caller abort. */
|
|
226
|
+
get signal() {
|
|
227
|
+
return this.controller.signal;
|
|
228
|
+
}
|
|
229
|
+
start() {
|
|
230
|
+
this.cancel ??= this.schedule(() => this.tick());
|
|
231
|
+
}
|
|
232
|
+
/** One renewal attempt. A failure marks the lease lost and aborts dispatch. */
|
|
233
|
+
async tick() {
|
|
234
|
+
if (this.lost || this.renewing || this.controller.signal.aborted)
|
|
235
|
+
return;
|
|
236
|
+
this.renewing = true;
|
|
237
|
+
try {
|
|
238
|
+
const renewed = await withWorkflowRunsRepo((repo) => repo.renewEngineLease(this.runId, this.holder, leaseExpiry()));
|
|
239
|
+
if (!renewed) {
|
|
240
|
+
this.stolenBy = await withWorkflowRunsRepo((repo) => repo.getRunById(this.runId)?.engine_lease_holder ?? null);
|
|
241
|
+
this.loseLease();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// A renewal that THREW (a DB error / connection failure, or the follow-up
|
|
246
|
+
// getRunById itself throwing) is treated exactly like a stolen lease: we
|
|
247
|
+
// can no longer PROVE we still hold it, so abort in-flight dispatch and let
|
|
248
|
+
// `assertAlive` stop the engine loudly. Swallowing the error here is what
|
|
249
|
+
// keeps the fire-and-forget `void tick()` in the default scheduler from
|
|
250
|
+
// leaking an unhandled promise rejection.
|
|
251
|
+
this.loseLease();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
this.renewing = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Mark the lease lost, stop the timer, and abort in-flight dispatch — the new
|
|
258
|
+
* owner drives the spine now (or, on a renewal error, we can no longer prove we
|
|
259
|
+
* do). Idempotent: repeated calls are harmless. */
|
|
260
|
+
loseLease() {
|
|
261
|
+
this.lost = true;
|
|
262
|
+
this.stop();
|
|
263
|
+
this.controller.abort();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Throw loudly if a heartbeat renewal failed. Called at dispatch boundaries:
|
|
267
|
+
* a lost lease means another engine claimed the run mid-step, so continuing
|
|
268
|
+
* (completing steps, dispatching more units) would double-drive it.
|
|
269
|
+
*/
|
|
270
|
+
assertAlive() {
|
|
271
|
+
if (!this.lost)
|
|
272
|
+
return;
|
|
273
|
+
throw new UsageError(`Workflow run ${this.runId} lost its run lease mid-dispatch (heartbeat renewal failed; lease now held by ` +
|
|
274
|
+
`${this.stolenBy ?? "(nobody)"}). Another engine invocation claimed the run after this one's lease expired — ` +
|
|
275
|
+
`aborting to avoid double-driving it.`);
|
|
276
|
+
}
|
|
277
|
+
stop() {
|
|
278
|
+
this.cancel?.();
|
|
279
|
+
this.cancel = undefined;
|
|
280
|
+
this.detachUpstream?.();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** The engine loop proper — runs under the lease held by `runWorkflowSteps`. */
|
|
284
|
+
async function driveRun(options, initial, leaseHolder, heartbeat) {
|
|
285
|
+
let next = initial;
|
|
286
|
+
// A terminal (completed) run is a PURE no-op. `runWorkflowSteps` already
|
|
287
|
+
// skipped lease acquisition for a done run (`leased = !next.done`), and this
|
|
288
|
+
// path must ALSO refuse to read the journal or load/integrity-check the
|
|
289
|
+
// frozen plan: a run that finished cleanly, then had its `plan_json` corrupted
|
|
290
|
+
// or tampered afterwards, must still report `done` here rather than throwing a
|
|
291
|
+
// frozen-plan integrity error (loadFrozenPlan would). Nothing will dispatch
|
|
292
|
+
// and the engine_lease_* columns stay exactly as they were, so return the
|
|
293
|
+
// fresh run state immediately.
|
|
294
|
+
if (initial.done) {
|
|
295
|
+
const doneState = await getNextWorkflowStep(initial.run.id);
|
|
296
|
+
return {
|
|
297
|
+
run: doneState.run,
|
|
298
|
+
executed: [],
|
|
299
|
+
...(doneState.run.status === "completed" ? { done: true } : {}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// The effective dispatch signal: the heartbeat's controller (a lost lease or
|
|
303
|
+
// a caller abort aborts it) while leased, else the raw caller signal.
|
|
304
|
+
const dispatchSignal = heartbeat?.signal ?? options.signal;
|
|
305
|
+
const executed = [];
|
|
306
|
+
let gateRejection;
|
|
307
|
+
const maxSteps = options.maxSteps ?? Number.POSITIVE_INFINITY;
|
|
308
|
+
// Seed the lifetime unit cap AND the budget ceilings from the journal so
|
|
309
|
+
// both are truly per-RUN: a resumed or re-invoked run must not restart the
|
|
310
|
+
// runaway backstop — or a declared `budget` — at zero. Journal rows = past
|
|
311
|
+
// dispatch ATTEMPTS (counted against `budget.max_units`); their summed
|
|
312
|
+
// `tokens` column is the run's spend so far (counted against
|
|
313
|
+
// `budget.max_tokens`). The executor consumes both only on new dispatches
|
|
314
|
+
// (durable-row reuses are free), so a large partially-completed fan-out
|
|
315
|
+
// stays resumable.
|
|
316
|
+
//
|
|
317
|
+
// Gate-evaluation rows (`phase = "gate"`, journaled by the completion-gate
|
|
318
|
+
// judge below) are EXCLUDED from the seed: the live path never consumes
|
|
319
|
+
// DispatchBudget for a judge call, so counting its journal row on resume
|
|
320
|
+
// would make an interrupted run hit `max_units` (and the lifetime cap)
|
|
321
|
+
// earlier than the identical uninterrupted run — a spurious hard failure
|
|
322
|
+
// that `on_error` cannot soften. The seed must reproduce exactly what live
|
|
323
|
+
// accounting would have accumulated.
|
|
324
|
+
//
|
|
325
|
+
// The seed sums each dispatch row's `attempts` (migration 008), NOT the row
|
|
326
|
+
// COUNT: a crash between a unit's dispatch and its finish leaves a `running`
|
|
327
|
+
// row that resume re-dispatches under the SAME content-derived unit_id, and
|
|
328
|
+
// `insertUnit` REPLACES that one row while bumping `attempts`. Counting rows
|
|
329
|
+
// would erase every prior crash-retried dispatch from budget/lifetime
|
|
330
|
+
// accounting, letting the run spend past its declared ceiling; summing
|
|
331
|
+
// `attempts` charges each dispatch exactly once.
|
|
332
|
+
const journaledUnits = await withWorkflowRunsRepo((repo) => repo.getUnitsForRun(next.run.id));
|
|
333
|
+
const journaledDispatches = journaledUnits.filter((row) => row.phase !== GATE_EVALUATION_PHASE);
|
|
334
|
+
let unitsDispatched = journaledDispatches.reduce((sum, row) => sum + row.attempts, 0);
|
|
335
|
+
let tokensUsed = journaledDispatches.reduce((sum, row) => sum + (row.tokens ?? 0), 0);
|
|
336
|
+
// One plan per invocation: the test seam receives the workflow ref; the
|
|
337
|
+
// default reads the run's frozen plan and never touches the asset file.
|
|
338
|
+
const plan = options.loadPlan
|
|
339
|
+
? await options.loadPlan(next.run.workflowRef)
|
|
340
|
+
: await loadFrozenPlan(next.run.id, next.run.workflowRef);
|
|
341
|
+
// Reviewer #12: the journaled params row must still satisfy the frozen param
|
|
342
|
+
// schemas before the engine resolves any unit prompt from it. Applied on ALL
|
|
343
|
+
// THREE driver surfaces (engine here, brief, report) so schema-violating
|
|
344
|
+
// params — post-start corruption — fail loudly and IDENTICALLY, preserving
|
|
345
|
+
// cross-surface parity (start already validated the params it stored).
|
|
346
|
+
assertRunParamsSatisfyPlan(next.run.id, plan, next.run.params ?? {});
|
|
347
|
+
// Route bookkeeping: targets a completed router did NOT select are skipped
|
|
348
|
+
// when the spine reaches them; a target ANY router selected is protected
|
|
349
|
+
// (two routers may share a target).
|
|
350
|
+
const routeSelected = new Set();
|
|
351
|
+
const routeUnselected = new Map();
|
|
352
|
+
// Resume contract: route decisions are journaled in the route step's
|
|
353
|
+
// evidence (`evidence.route.selected`) and must be REPLAYED into the
|
|
354
|
+
// bookkeeping before the spine advances — a re-invoked run (crash, Ctrl-C,
|
|
355
|
+
// maxSteps, gate rejection after the route completed) would otherwise reach
|
|
356
|
+
// the unselected targets with empty in-memory state and execute the wrong
|
|
357
|
+
// branch. Decisions stay pure functions of (frozen plan, params, journaled
|
|
358
|
+
// results) — the addendum determinism bar. A done run skips the seeding:
|
|
359
|
+
// nothing will dispatch, so an unrecoverable historical decision must not
|
|
360
|
+
// block the no-op status return below.
|
|
361
|
+
if (!next.done) {
|
|
362
|
+
seedJournaledRouteDecisions(plan, next, routeSelected, routeUnselected);
|
|
363
|
+
}
|
|
364
|
+
while (!next.done && next.step && next.run.status === "active" && executed.length < maxSteps) {
|
|
365
|
+
// A LOST lease (the heartbeat's renewal failed mid-step) is a loud stop —
|
|
366
|
+
// another engine owns the spine now. A caller abort (options.signal) is a
|
|
367
|
+
// graceful break, distinct from a lost lease.
|
|
368
|
+
heartbeat?.assertAlive();
|
|
369
|
+
if (options.signal?.aborted)
|
|
370
|
+
break;
|
|
371
|
+
// Renew the run lease between steps (a fresh 90s window per iteration).
|
|
372
|
+
// Losing it (expired mid-step + claimed by another engine) throws — the
|
|
373
|
+
// new owner drives the spine now.
|
|
374
|
+
await renewRunLease(next.run.id, leaseHolder);
|
|
375
|
+
const step = next.step;
|
|
376
|
+
const stepPlan = plan.steps.find((s) => s.stepId === step.id);
|
|
377
|
+
if (!stepPlan) {
|
|
378
|
+
throw new UsageError(`Step "${step.id}" of run ${next.run.id} is not present in the current workflow asset (${next.run.workflowRef}). ` +
|
|
379
|
+
`The source file changed since the run started — advance this step manually with \`akm workflow complete\`.`);
|
|
380
|
+
}
|
|
381
|
+
// A branch target no completed router selected → auto-skip, no dispatch.
|
|
382
|
+
const skipInfo = routeUnselected.get(step.id);
|
|
383
|
+
if (skipInfo && !routeSelected.has(step.id)) {
|
|
384
|
+
// Cascade (peer review R1): a skipped step that is ITSELF a router
|
|
385
|
+
// never evaluates its route, so none of its declared targets were
|
|
386
|
+
// selected — mark them all skip-on-reach too (a target another
|
|
387
|
+
// completed router selects stays protected via routeSelected). Without
|
|
388
|
+
// this, every branch of the skipped router would run unconditionally.
|
|
389
|
+
if (stepPlan.route) {
|
|
390
|
+
cascadeSkippedRouter(stepPlan.route, step.id, routeUnselected);
|
|
391
|
+
}
|
|
392
|
+
const notes = skipInfo.selected === null
|
|
393
|
+
? `Skipped by route: step "${skipInfo.router}" was itself skipped, so none of its branch targets run.`
|
|
394
|
+
: `Skipped by route: step "${skipInfo.router}" selected "${skipInfo.selected}".`;
|
|
395
|
+
executed.push({ stepId: step.id, ok: true, unitCount: 0, failedUnits: 0, summary: notes });
|
|
396
|
+
await completeWorkflowStep({ runId: next.run.id, stepId: step.id, status: "skipped", notes, leaseHolder });
|
|
397
|
+
next = await getNextWorkflowStep(next.run.id);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
// `dependsOn` edges (reserved in IR v2 — no frontend emits them today,
|
|
401
|
+
// but a frozen plan may carry them) are a declared ordering contract:
|
|
402
|
+
// every dependency must already be resolved before this step dispatches.
|
|
403
|
+
// Execution is sequential (spine order), so a violation means the plan
|
|
404
|
+
// ordered steps inconsistently with its declared edges — fail fast,
|
|
405
|
+
// before spending.
|
|
406
|
+
for (const dep of stepPlan.dependsOn ?? []) {
|
|
407
|
+
const depState = next.workflow.steps.find((s) => s.id === dep);
|
|
408
|
+
if (!depState || (depState.status !== "completed" && depState.status !== "skipped")) {
|
|
409
|
+
throw new UsageError(`Step "${step.id}" depends on step "${dep}", which is ${depState?.status ?? "missing"}. ` +
|
|
410
|
+
`Reorder the workflow so dependencies come first (execution is sequential in step order).`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const evidence = {};
|
|
414
|
+
for (const s of next.workflow.steps)
|
|
415
|
+
evidence[s.id] = s.evidence;
|
|
416
|
+
// Bounded gate loop (addendum R2, `gate.max_loops`): loop 1 is the normal
|
|
417
|
+
// execution; a gate rejection with attempts left re-executes the subgraph
|
|
418
|
+
// with the judge's feedback threaded into unit prompts. `advanced` = the
|
|
419
|
+
// step completed and the spine may move on; `stopEngine` = failure or
|
|
420
|
+
// final rejection — this invocation is done.
|
|
421
|
+
const maxLoops = Math.max(1, stepPlan.gate.maxLoops ?? 1);
|
|
422
|
+
// Crash-resume gate state (Codex P1): SEED the starting gate loop from the
|
|
423
|
+
// journal exactly as the brief/report surfaces do — the SAME shared helpers,
|
|
424
|
+
// no fork. A run interrupted after a rejected gate was journaled
|
|
425
|
+
// (`<step>.gate:l<n>`, complete:false) must resume at loop n+1 with the
|
|
426
|
+
// stored corrective feedback threaded into the unit prompts; without this
|
|
427
|
+
// the engine restarts at loop 1, reuses the rejected loop-1 rows, overwrites
|
|
428
|
+
// `<step>.gate:l1`, and re-judges the stale artifact — breaking journaled
|
|
429
|
+
// replay and diverging from what brief computes for the same run. The rows
|
|
430
|
+
// are re-read here (NOT the once-at-start `journaledUnits` budget seed) so a
|
|
431
|
+
// step reached later within THIS same invocation still starts fresh at loop 1.
|
|
432
|
+
const stepJournal = await withWorkflowRunsRepo((repo) => repo.getUnitsForRun(next.run.id));
|
|
433
|
+
const startLoop = activeGateLoop(stepJournal, step.id);
|
|
434
|
+
const seededFeedback = recoverGateFeedback(stepJournal, step.id, startLoop);
|
|
435
|
+
// Resume AFTER the FINAL rejection (`startLoop` past the loop bound): the
|
|
436
|
+
// gate was already exhausted before the crash, so there is NO fresh loop to
|
|
437
|
+
// run — reproduce the documented gateRejection outcome from the stored
|
|
438
|
+
// final-loop feedback instead of re-dispatching a spurious extra loop. The
|
|
439
|
+
// l1..l<maxLoops> rows stay untouched and the step stays active, exactly as
|
|
440
|
+
// when the engine first exhausted the gate.
|
|
441
|
+
if (startLoop > maxLoops) {
|
|
442
|
+
gateRejection = {
|
|
443
|
+
stepId: step.id,
|
|
444
|
+
missing: seededFeedback?.missing ?? [],
|
|
445
|
+
feedback: seededFeedback?.feedback ?? "",
|
|
446
|
+
};
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
let gateFeedback = seededFeedback;
|
|
450
|
+
let advanced = false;
|
|
451
|
+
let stopEngine = false;
|
|
452
|
+
for (let gateLoop = startLoop; gateLoop <= maxLoops; gateLoop++) {
|
|
453
|
+
// A loop re-execution dispatches a fresh round of units — renew the
|
|
454
|
+
// lease so a long evaluator-optimizer cycle cannot outlive the TTL.
|
|
455
|
+
if (gateLoop > 1)
|
|
456
|
+
await renewRunLease(next.run.id, leaseHolder);
|
|
457
|
+
// Route-only steps (YAML `route:` — no execution subgraph) dispatch no
|
|
458
|
+
// units; they only decide the spine's path below. Everything else
|
|
459
|
+
// executes its subgraph through the native executor.
|
|
460
|
+
const result = !stepPlan.root && stepPlan.route
|
|
461
|
+
? {
|
|
462
|
+
ok: true,
|
|
463
|
+
units: [],
|
|
464
|
+
evidence: {},
|
|
465
|
+
summary: `Step "${step.id}" is a route step — no units dispatched.`,
|
|
466
|
+
unitsDispatched,
|
|
467
|
+
}
|
|
468
|
+
: await executeStepPlan(stepPlan, {
|
|
469
|
+
runId: next.run.id,
|
|
470
|
+
workflowRef: next.run.workflowRef,
|
|
471
|
+
params: next.run.params ?? {},
|
|
472
|
+
evidence,
|
|
473
|
+
unitsDispatched,
|
|
474
|
+
tokensUsed,
|
|
475
|
+
// Budget ceilings ride the FROZEN plan (addendum R2): a mid-run
|
|
476
|
+
// asset edit can never loosen or tighten a run's budget.
|
|
477
|
+
...(plan.budget ? { budget: plan.budget } : {}),
|
|
478
|
+
gateLoop,
|
|
479
|
+
...(gateFeedback ? { gateFeedback } : {}),
|
|
480
|
+
// The heartbeat's signal is the effective dispatch signal: a lost
|
|
481
|
+
// lease (or a caller abort) aborts in-flight units promptly.
|
|
482
|
+
...(dispatchSignal ? { signal: dispatchSignal } : {}),
|
|
483
|
+
...(options.dispatcher ? { dispatcher: options.dispatcher } : {}),
|
|
484
|
+
...(options.maxConcurrency !== undefined ? { maxConcurrency: options.maxConcurrency } : {}),
|
|
485
|
+
});
|
|
486
|
+
// If the heartbeat lost the lease WHILE this step dispatched, another
|
|
487
|
+
// engine now owns the run — stop loudly BEFORE finalizing the step
|
|
488
|
+
// (completeWorkflowStep would race the new owner's spine).
|
|
489
|
+
heartbeat?.assertAlive();
|
|
490
|
+
unitsDispatched = result.unitsDispatched;
|
|
491
|
+
if (result.tokensUsed !== undefined)
|
|
492
|
+
tokensUsed = result.tokensUsed;
|
|
493
|
+
executed.push({
|
|
494
|
+
stepId: step.id,
|
|
495
|
+
ok: result.ok,
|
|
496
|
+
unitCount: result.units.length,
|
|
497
|
+
failedUnits: result.units.filter((u) => !u.ok).length,
|
|
498
|
+
summary: result.summary,
|
|
499
|
+
});
|
|
500
|
+
// Route evaluation + artifact-judged completion gate + gate-row
|
|
501
|
+
// journaling + the bounded-loop rejection contract are the SHARED
|
|
502
|
+
// completion path (`finalizeExecutedStep`): the R3 report surface drives
|
|
503
|
+
// the identical sequence, so an engine-driven and a report-driven run of
|
|
504
|
+
// the same frozen plan promote the same artifact and advance (or reject)
|
|
505
|
+
// the spine identically. The engine owns only the loop control the result
|
|
506
|
+
// maps onto (retry re-executes; advanced moves on; failure/exhaustion
|
|
507
|
+
// stops this invocation).
|
|
508
|
+
const finalize = await finalizeExecutedStep({
|
|
509
|
+
runId: next.run.id,
|
|
510
|
+
workflowRef: next.run.workflowRef,
|
|
511
|
+
stepId: step.id,
|
|
512
|
+
stepPlan,
|
|
513
|
+
completionCriteria: step.completionCriteria ?? [],
|
|
514
|
+
gateLoop,
|
|
515
|
+
loopsRemaining: gateLoop < maxLoops,
|
|
516
|
+
result,
|
|
517
|
+
priorEvidence: evidence,
|
|
518
|
+
params: next.run.params ?? {},
|
|
519
|
+
routeSelected,
|
|
520
|
+
routeUnselected,
|
|
521
|
+
summaryJudge: options.summaryJudge,
|
|
522
|
+
...(options.requireGates ? { requireGates: true } : {}),
|
|
523
|
+
leaseHolder,
|
|
524
|
+
});
|
|
525
|
+
if (finalize.kind === "retry") {
|
|
526
|
+
// Re-execute the subgraph with the judge/validation feedback threaded
|
|
527
|
+
// into unit prompts — the changed prompt changes each unit's input
|
|
528
|
+
// hash, so the re-run dispatches fresh work instead of reusing rows.
|
|
529
|
+
gateFeedback = finalize.gateFeedback;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (finalize.kind === "advanced") {
|
|
533
|
+
// A route-only step's summary IS its decision (finalize surfaces it).
|
|
534
|
+
if (finalize.summaryOverride !== undefined) {
|
|
535
|
+
executed[executed.length - 1] = { ...executed[executed.length - 1], summary: finalize.summaryOverride };
|
|
536
|
+
}
|
|
537
|
+
advanced = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
if (finalize.kind === "failed") {
|
|
541
|
+
// A route-failure was pushed as ok:true (the units succeeded); reflect
|
|
542
|
+
// the deterministic route failure in the executed report.
|
|
543
|
+
if (finalize.routeFailure) {
|
|
544
|
+
executed[executed.length - 1] = { ...executed[executed.length - 1], ok: false, summary: finalize.summary };
|
|
545
|
+
}
|
|
546
|
+
stopEngine = true;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
if (finalize.kind === "blocked") {
|
|
550
|
+
// Reviewer #18: a required gate with no judge available — the step is
|
|
551
|
+
// BLOCKED (not failed) for a human. The units succeeded, so overwrite the
|
|
552
|
+
// report's ok/summary to reflect the block, then stop this invocation.
|
|
553
|
+
executed[executed.length - 1] = { ...executed[executed.length - 1], ok: false, summary: finalize.summary };
|
|
554
|
+
stopEngine = true;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
// gate-exhausted: rejected with no loop budget left — stop with feedback.
|
|
558
|
+
gateRejection = finalize.gateRejection;
|
|
559
|
+
stopEngine = true;
|
|
560
|
+
}
|
|
561
|
+
if (stopEngine || !advanced)
|
|
562
|
+
break;
|
|
563
|
+
next = await getNextWorkflowStep(next.run.id);
|
|
564
|
+
}
|
|
565
|
+
// Re-read for the freshest run state (the loop may have exited on maxSteps).
|
|
566
|
+
const finalState = await getNextWorkflowStep(next.run.id);
|
|
567
|
+
return {
|
|
568
|
+
run: finalState.run,
|
|
569
|
+
executed,
|
|
570
|
+
...(finalState.run.status === "completed" ? { done: true } : {}),
|
|
571
|
+
...(gateRejection ? { gateRejection } : {}),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Load the plan a run executes (frozen-plan contract, migration 006):
|
|
576
|
+
*
|
|
577
|
+
* - `plan_json` present → parse it and verify `plan_hash` (sha256 of the
|
|
578
|
+
* canonical JSON). A mismatch means the journaled plan was tampered with
|
|
579
|
+
* or corrupted — fail loudly, never silently recompile. The workflow
|
|
580
|
+
* asset file is NEVER touched on this path.
|
|
581
|
+
* - `plan_json` NULL → the run predates frozen plans (created before
|
|
582
|
+
* migration 006). Warn and fall back to compiling from the live asset,
|
|
583
|
+
* preserving pre-006 behavior for in-flight legacy runs.
|
|
584
|
+
*/
|
|
585
|
+
async function loadFrozenPlan(runId, workflowRef) {
|
|
586
|
+
const row = await withWorkflowRunsRepo((repo) => {
|
|
587
|
+
const run = repo.getRunById(runId);
|
|
588
|
+
return run ? { planJson: run.plan_json, planHash: run.plan_hash } : undefined;
|
|
589
|
+
});
|
|
590
|
+
if (row?.planJson) {
|
|
591
|
+
return parseFrozenPlan(runId, row.planJson, row.planHash);
|
|
592
|
+
}
|
|
593
|
+
warn(`Workflow run ${runId} predates frozen plans (no plan_json on the run row); ` +
|
|
594
|
+
`compiling the plan from the live asset ${workflowRef}. New runs freeze their plan at start.`);
|
|
595
|
+
return compileWorkflowAssetPlan(await loadWorkflowAsset(workflowRef));
|
|
596
|
+
}
|