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
package/dist/workflows/db.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { getWorkflowDbPath } from "../core/paths.js";
|
|
7
7
|
import { openDatabase } from "../storage/database.js";
|
|
8
8
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
9
|
+
import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
9
10
|
/**
|
|
10
11
|
* workflow.db — Durable SQLite database for workflow run state.
|
|
11
12
|
*
|
|
@@ -46,12 +47,10 @@ export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
|
|
|
46
47
|
fs.mkdirSync(dir, { recursive: true });
|
|
47
48
|
}
|
|
48
49
|
const db = openDatabase(dbPath);
|
|
49
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
50
50
|
// #589: 30 s busy timeout, matching index.db / state.db. Without it the
|
|
51
51
|
// default is 0 ms, so any concurrent writer fails immediately with
|
|
52
|
-
// SQLITE_BUSY.
|
|
53
|
-
db
|
|
54
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
52
|
+
// SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
|
|
53
|
+
applyStandardPragmas(db, { dataDir: dir });
|
|
55
54
|
ensureBaseSchema(db);
|
|
56
55
|
runMigrations(db);
|
|
57
56
|
return db;
|
|
@@ -166,6 +165,141 @@ const MIGRATIONS = [
|
|
|
166
165
|
ALTER TABLE workflow_run_steps ADD COLUMN summary TEXT;
|
|
167
166
|
`,
|
|
168
167
|
},
|
|
168
|
+
// ── Migration 004 — per-unit run state (orchestration plan P1) ──────────────
|
|
169
|
+
//
|
|
170
|
+
// A step's execution may now fan out into N concurrent units (native
|
|
171
|
+
// executor, docs/technical/akm-workflows-orchestration-plan.md). Units hang
|
|
172
|
+
// off the gated step spine: `workflow_run_steps` stays the durable top-level
|
|
173
|
+
// record; each dispatched unit gets its own row here so a crash-and-resume
|
|
174
|
+
// re-dispatches only incomplete units (durable-row resume) and budget/usage
|
|
175
|
+
// is attributable per unit. `input_hash` is reserved for the P5 deterministic
|
|
176
|
+
// replay mode; `failure_reason` carries runAgent's structured failure
|
|
177
|
+
// vocabulary so retry/continue-on-error policy has semantics to act on.
|
|
178
|
+
{
|
|
179
|
+
id: "004-workflow-run-units",
|
|
180
|
+
up: `
|
|
181
|
+
CREATE TABLE IF NOT EXISTS workflow_run_units (
|
|
182
|
+
run_id TEXT NOT NULL,
|
|
183
|
+
unit_id TEXT NOT NULL,
|
|
184
|
+
step_id TEXT,
|
|
185
|
+
node_id TEXT NOT NULL,
|
|
186
|
+
parent_unit_id TEXT,
|
|
187
|
+
phase TEXT,
|
|
188
|
+
runner TEXT,
|
|
189
|
+
model TEXT,
|
|
190
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed', 'skipped')),
|
|
191
|
+
input_hash TEXT,
|
|
192
|
+
result_json TEXT,
|
|
193
|
+
tokens INTEGER,
|
|
194
|
+
failure_reason TEXT,
|
|
195
|
+
worktree_path TEXT,
|
|
196
|
+
started_at TEXT,
|
|
197
|
+
finished_at TEXT,
|
|
198
|
+
PRIMARY KEY (run_id, unit_id),
|
|
199
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_units_run_step
|
|
203
|
+
ON workflow_run_units(run_id, step_id);
|
|
204
|
+
`,
|
|
205
|
+
},
|
|
206
|
+
// ── Migration 005 — harness-native unit session id (plan P2) ────────────────
|
|
207
|
+
//
|
|
208
|
+
// The P2 harness adapters' result extractors reveal the harness-native
|
|
209
|
+
// session id of a dispatched unit (codex `session_configured`, gemini/pi
|
|
210
|
+
// JSON envelopes, the opencode SDK session). It is stored opportunistically
|
|
211
|
+
// on the unit row so resume can replay the harness's own context cache
|
|
212
|
+
// (e.g. `codex exec resume <id>`, `gemini --resume <id>`); akm never
|
|
213
|
+
// *depends* on it — `workflow_run_units` remains the durable source of
|
|
214
|
+
// truth (plan §"Session, MCP, and identity across harnesses").
|
|
215
|
+
{
|
|
216
|
+
id: "005-unit-session-id",
|
|
217
|
+
up: `
|
|
218
|
+
ALTER TABLE workflow_run_units ADD COLUMN session_id TEXT;
|
|
219
|
+
`,
|
|
220
|
+
},
|
|
221
|
+
// ── Migration 006 — frozen plan + engine lease (redesign addendum, R1) ──────
|
|
222
|
+
//
|
|
223
|
+
// `workflow start` now compiles the workflow into its plan graph ONCE and
|
|
224
|
+
// freezes it on the run row: `plan_json` holds the canonical plan JSON
|
|
225
|
+
// (`ir/plan-hash.ts`) and `plan_hash` its sha256, so every subsequent
|
|
226
|
+
// invocation executes the frozen snapshot with an integrity check — the
|
|
227
|
+
// source file is never re-read for an in-flight run. Runs created before
|
|
228
|
+
// this migration have NULL plan_json (legacy) and fall back to
|
|
229
|
+
// compile-from-asset with a warning.
|
|
230
|
+
//
|
|
231
|
+
// `engine_lease_until` / `engine_lease_holder` reserve the run-lease columns
|
|
232
|
+
// (a second `workflow run` on a leased run refuses up front). TODO(R2):
|
|
233
|
+
// lease ENFORCEMENT is engine-rework scope — only the columns land now.
|
|
234
|
+
{
|
|
235
|
+
id: "006-frozen-plan-and-lease",
|
|
236
|
+
up: `
|
|
237
|
+
ALTER TABLE workflow_runs ADD COLUMN plan_json TEXT;
|
|
238
|
+
ALTER TABLE workflow_runs ADD COLUMN plan_hash TEXT;
|
|
239
|
+
ALTER TABLE workflow_runs ADD COLUMN engine_lease_until TEXT;
|
|
240
|
+
ALTER TABLE workflow_runs ADD COLUMN engine_lease_holder TEXT;
|
|
241
|
+
`,
|
|
242
|
+
},
|
|
243
|
+
// ── Migration 007 — unit-level check-in heartbeat (redesign addendum, R3) ────
|
|
244
|
+
//
|
|
245
|
+
// The harness-neutral driver protocol lets ANY agent session claim and
|
|
246
|
+
// heartbeat a unit it is executing via `akm workflow report --status running`.
|
|
247
|
+
// `last_checkin_at` records the most recent heartbeat (distinct from
|
|
248
|
+
// `started_at`, the first claim) so a pure timestamp evaluator
|
|
249
|
+
// (`runtime/unit-checkin.ts`) can surface a claimed-but-silent unit as stale
|
|
250
|
+
// in `workflow brief` without any background thread. Nullable and additive;
|
|
251
|
+
// engine-dispatched rows never set it (they finish before a heartbeat window
|
|
252
|
+
// could elapse), so their transient `running` state is judged from
|
|
253
|
+
// `started_at`.
|
|
254
|
+
{
|
|
255
|
+
id: "007-unit-last-checkin",
|
|
256
|
+
up: `
|
|
257
|
+
ALTER TABLE workflow_run_units ADD COLUMN last_checkin_at TEXT;
|
|
258
|
+
`,
|
|
259
|
+
},
|
|
260
|
+
// ── Migration 008 — per-unit dispatch-attempt counter (PR #714 review, P2) ───
|
|
261
|
+
//
|
|
262
|
+
// `workflow_run_units.unit_id` is CONTENT-derived and stable across
|
|
263
|
+
// crash/resume (retries/loops carry `~r<n>`/`~l<loop>` suffixes, so they are
|
|
264
|
+
// DISTINCT rows). A crash between a unit's dispatch (`insertUnit`, status
|
|
265
|
+
// `running`) and its finish leaves a stale `running` row; durable-row resume
|
|
266
|
+
// re-dispatches the SAME unit_id and `insertUnit` REPLACES that single row.
|
|
267
|
+
// Because the run's budget/lifetime seed was derived from the NUMBER of unit
|
|
268
|
+
// rows, each crash/resume of one unit erased the prior dispatch from
|
|
269
|
+
// `budget.max_units` / lifetime-cap accounting, letting a run spend past its
|
|
270
|
+
// declared ceiling. `attempts` counts how many times a row was (re)dispatched
|
|
271
|
+
// — incremented by `insertUnit` on every REPLACE of an existing row — so both
|
|
272
|
+
// budget seeds sum `attempts` instead of counting rows and crash-retried
|
|
273
|
+
// dispatches are charged. Existing rows back-fill to 1 (one dispatch each).
|
|
274
|
+
{
|
|
275
|
+
id: "008-unit-attempts",
|
|
276
|
+
up: `
|
|
277
|
+
ALTER TABLE workflow_run_units ADD COLUMN attempts INTEGER NOT NULL DEFAULT 1;
|
|
278
|
+
`,
|
|
279
|
+
},
|
|
280
|
+
// ── Migration 009 — per-unit claim ownership (PR #714 review round 2) ─────────
|
|
281
|
+
//
|
|
282
|
+
// `akm workflow report --status running` claims a unit before executing it.
|
|
283
|
+
// Round-2 review (#3): a running row had no claim owner, no compare-and-set,
|
|
284
|
+
// and no stale-hash guard, so a stale/tampered `running` row could be
|
|
285
|
+
// finalized by anyone with a fresh result while keeping an old input_hash.
|
|
286
|
+
// These columns record who holds the claim (`claim_holder` — the driver's
|
|
287
|
+
// `--session-id` or a token report mints and returns) and until when
|
|
288
|
+
// (`claim_expires_at`, ISO-8601 UTC). Heartbeating or finishing a live-claimed
|
|
289
|
+
// running row requires the matching holder; an EXPIRED claim is reclaimable by
|
|
290
|
+
// a new holder (crash recovery). The TTL equals the unit-checkin stale window
|
|
291
|
+
// (`runtime/unit-checkin.UNIT_STALE_MS`), so an expired claim is exactly a unit
|
|
292
|
+
// that `workflow brief` already surfaces as stale. Both columns are nullable
|
|
293
|
+
// and additive: engine-dispatched rows and simple claimless drivers leave them
|
|
294
|
+
// NULL, and finishing a never-claimed unit stays allowed (input_hash is still
|
|
295
|
+
// validated always).
|
|
296
|
+
{
|
|
297
|
+
id: "009-unit-claim",
|
|
298
|
+
up: `
|
|
299
|
+
ALTER TABLE workflow_run_units ADD COLUMN claim_holder TEXT;
|
|
300
|
+
ALTER TABLE workflow_run_units ADD COLUMN claim_expires_at TEXT;
|
|
301
|
+
`,
|
|
302
|
+
},
|
|
169
303
|
];
|
|
170
304
|
/**
|
|
171
305
|
* Stable id of the scope_key migration. Exported for bootstrap detection and
|
|
@@ -0,0 +1,484 @@
|
|
|
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
|
+
* `akm workflow brief <run>` — the read-only half of the harness-neutral driver
|
|
6
|
+
* protocol (redesign addendum R3). It tells ANY agent session (Claude Code,
|
|
7
|
+
* opencode, Codex, a human at a shell) exactly what units the native engine
|
|
8
|
+
* would dispatch for a run's active step, and how to report the results back
|
|
9
|
+
* through `akm workflow report` (the mutating half, R3 step 3).
|
|
10
|
+
*
|
|
11
|
+
* ## Read-only, no lease, no dispatch, no mutation
|
|
12
|
+
*
|
|
13
|
+
* `brief` computes; it never writes. It takes no engine lease, dispatches no
|
|
14
|
+
* units, and never advances the gate spine. The only database access is
|
|
15
|
+
* SELECTs (`getNextWorkflowStep` + the run row + the unit journal). A test
|
|
16
|
+
* proves the workflow.db file is byte-identical before and after a `brief`.
|
|
17
|
+
*
|
|
18
|
+
* ## No duplicated semantics (the cardinal rule)
|
|
19
|
+
*
|
|
20
|
+
* The expected work-list is computed by the SAME shared functions the engine
|
|
21
|
+
* uses (`step-work.ts`): {@link computeStepWorkList} for item resolution +
|
|
22
|
+
* content-derived unit ids + input hashes + prompt assembly,
|
|
23
|
+
* {@link activeGateLoop} / {@link recoverGateFeedback} to recover the gate-loop
|
|
24
|
+
* number and the judge feedback the engine threads into loop-N prompts (so a
|
|
25
|
+
* loop-2 brief's unit ids/hashes equal what the engine would compute),
|
|
26
|
+
* {@link stepOutputsFromEvidence} for the expression scope, and
|
|
27
|
+
* {@link evaluateRoute} for the deterministic route decision. Because both
|
|
28
|
+
* surfaces call one implementation, an engine-driven run and a brief/report
|
|
29
|
+
* driven run of the same frozen plan produce byte-identical unit graphs — the
|
|
30
|
+
* invariant R4 asserts.
|
|
31
|
+
*/
|
|
32
|
+
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
33
|
+
import { canonicalizeWorkflowName } from "../../core/asset/asset-spec.js";
|
|
34
|
+
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
35
|
+
import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
|
|
36
|
+
import { getCurrentWorkflowScopeKey } from "../authoring/scope-key.js";
|
|
37
|
+
import { assertRunParamsSatisfyPlan } from "../ir/params.js";
|
|
38
|
+
import { snapshotRunForDriver } from "../runtime/runs.js";
|
|
39
|
+
import { evaluateStaleUnits } from "../runtime/unit-checkin.js";
|
|
40
|
+
import { detectSecretShapedParams } from "./param-secrets.js";
|
|
41
|
+
import { activeGateLoop, assertJournaledRouteSelectionsValid, computeStepWorkList, evaluateRoute, GATE_EVALUATION_PHASE, isWorkListFullyTerminal, parseFrozenPlan, recoverGateFeedback, selectUnitAttemptRow, stepOutputsFromEvidence, } from "./step-work.js";
|
|
42
|
+
const EMPTY_WORK_LIST = { isFanOut: false, reducer: null, itemCount: 0, units: [] };
|
|
43
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Build the read-only brief for a run. `target` is a run id (preferred) or a
|
|
46
|
+
* workflow ref that ALREADY has an active run in the current scope — brief
|
|
47
|
+
* never auto-starts a run (that would mutate), so a ref with no active run is a
|
|
48
|
+
* NotFoundError, not a silent start.
|
|
49
|
+
*/
|
|
50
|
+
export async function buildWorkflowBrief(target) {
|
|
51
|
+
const runId = await resolveRunId(target);
|
|
52
|
+
// Read-only spine walk. #14: read the run row, its steps, AND its unit journal
|
|
53
|
+
// in ONE transaction so a concurrent report/run/manual completion cannot change
|
|
54
|
+
// the active step between the spine read and the unit-journal read. A bare run
|
|
55
|
+
// id never auto-starts (we resolved to a concrete id above).
|
|
56
|
+
const { next, run: runRow, units } = await snapshotRunForDriver(runId);
|
|
57
|
+
const planJson = runRow.plan_json;
|
|
58
|
+
const planHash = runRow.plan_hash;
|
|
59
|
+
const leaseHolder = runRow.engine_lease_holder;
|
|
60
|
+
const leaseUntil = runRow.engine_lease_until;
|
|
61
|
+
const run = {
|
|
62
|
+
id: next.run.id,
|
|
63
|
+
workflowRef: next.run.workflowRef,
|
|
64
|
+
workflowTitle: next.run.workflowTitle,
|
|
65
|
+
status: next.run.status,
|
|
66
|
+
currentStepId: next.run.currentStepId ?? null,
|
|
67
|
+
params: next.run.params ?? {},
|
|
68
|
+
};
|
|
69
|
+
const warnings = [];
|
|
70
|
+
// #13: params are declared NON-SECRET. They are interpolated into every unit
|
|
71
|
+
// prompt AND hashed into the unit identity, so `brief` cannot redact them
|
|
72
|
+
// without breaking the byte-identical-prompt contract a driver executes
|
|
73
|
+
// against. Surface the standing advisory (whenever the run carries params) plus
|
|
74
|
+
// any best-effort secret-shaped-value hits, so an author moves credentials to
|
|
75
|
+
// an env binding (which `brief` only ever names).
|
|
76
|
+
if (Object.keys(run.params).length > 0) {
|
|
77
|
+
warnings.push("Workflow params are copied verbatim into every unit prompt shown to any driver and are hashed into the unit " +
|
|
78
|
+
"identity — they are NOT secret. Never put credentials in params; put secrets in env bindings (`env:` refs), " +
|
|
79
|
+
"which `brief` surfaces by name only and never resolves.");
|
|
80
|
+
}
|
|
81
|
+
warnings.push(...detectSecretShapedParams(run.params));
|
|
82
|
+
const lease = buildLease(leaseHolder, leaseUntil);
|
|
83
|
+
if (lease?.live) {
|
|
84
|
+
warnings.push(`Engine ${lease.holder} holds a LIVE run lease (expires ${lease.until}). This run is being driven by the ` +
|
|
85
|
+
`native engine right now — \`akm workflow report\` is REFUSED while the lease is live. Do NOT execute these ` +
|
|
86
|
+
`units; wait for the engine to finish or for the lease to expire.`);
|
|
87
|
+
}
|
|
88
|
+
// Stale claimed units (pure timestamp evaluation): a driver claimed these via
|
|
89
|
+
// `report --status running` but has not heartbeated within the window — flag
|
|
90
|
+
// them so another driver can reclaim the abandoned work.
|
|
91
|
+
const staleUnits = evaluateStaleUnits(units);
|
|
92
|
+
if (staleUnits.length > 0) {
|
|
93
|
+
warnings.push(`${staleUnits.length} unit(s) were claimed with \`report --status running\` but have gone silent past the ` +
|
|
94
|
+
`check-in window (${staleUnits.map((u) => u.unitId).join(", ")}). Their driver may have died — you can ` +
|
|
95
|
+
`reclaim and re-execute them.`);
|
|
96
|
+
}
|
|
97
|
+
const reportGuidance = {
|
|
98
|
+
checkin: `akm workflow report ${run.id} --unit <unit_id> --status running --note "<short progress note>"`,
|
|
99
|
+
failure: `akm workflow report ${run.id} --unit <unit_id> --status failed --failure-reason <vocab>`,
|
|
100
|
+
note: "Run each unit, then report its result. A unit belongs to the active step's work-list; its unit_id is content-derived — copy it verbatim.",
|
|
101
|
+
};
|
|
102
|
+
// Spine watermark (#14): run-mutation counter shared by every return below.
|
|
103
|
+
// The active branch re-stamps it with the real gate loop + active step id.
|
|
104
|
+
const watermark = `${runRow.updated_at}:u${units.length}`;
|
|
105
|
+
const base = {
|
|
106
|
+
ok: true,
|
|
107
|
+
run,
|
|
108
|
+
spineToken: makeSpineToken(run.id, run.currentStepId, 1, watermark),
|
|
109
|
+
...(lease ? { engineLease: lease } : {}),
|
|
110
|
+
reportGuidance,
|
|
111
|
+
staleUnits,
|
|
112
|
+
warnings,
|
|
113
|
+
};
|
|
114
|
+
// Completed run: nothing to do.
|
|
115
|
+
if (next.done || run.status === "completed") {
|
|
116
|
+
return {
|
|
117
|
+
...base,
|
|
118
|
+
done: true,
|
|
119
|
+
active: false,
|
|
120
|
+
workList: EMPTY_WORK_LIST,
|
|
121
|
+
message: "Workflow run is completed — no work remains.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Blocked / failed: not active, so the engine dispatches nothing. Point the
|
|
125
|
+
// driver at `resume` rather than inventing a work-list for a dead run.
|
|
126
|
+
if (run.status !== "active") {
|
|
127
|
+
warnings.push(`Workflow run is ${run.status}, not active — no work-list. Reopen it first: \`akm workflow resume ${run.id}\`.`);
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
active: false,
|
|
131
|
+
workList: EMPTY_WORK_LIST,
|
|
132
|
+
message: `Workflow run is ${run.status} — resume it to continue.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const stepState = next.step;
|
|
136
|
+
if (!stepState) {
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
active: false,
|
|
140
|
+
workList: EMPTY_WORK_LIST,
|
|
141
|
+
message: "Workflow run is active but has no current step.",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Load the FROZEN plan the engine executes (migration 006). A legacy run
|
|
145
|
+
// (NULL plan_json) has no plan for brief to read — point at engine-driven
|
|
146
|
+
// mode, which still handles pre-006 runs by compiling from the asset.
|
|
147
|
+
const plan = loadFrozenPlanForBrief(run.id, planJson, planHash);
|
|
148
|
+
// Reviewer #12: the journaled params row must still satisfy the frozen param
|
|
149
|
+
// schemas — a violation is post-start corruption, loud on the brief surface
|
|
150
|
+
// too (mirrors the frozen-plan hash check and the tampered-params divergence).
|
|
151
|
+
assertRunParamsSatisfyPlan(run.id, plan, next.run.params ?? {});
|
|
152
|
+
// Reviewer #7: a completed route step whose journaled decision names a target
|
|
153
|
+
// the route never declared is tampered evidence — fail loudly on the read-only
|
|
154
|
+
// brief surface too, not just on the resume/report surfaces that replay it.
|
|
155
|
+
assertJournaledRouteSelectionsValid(plan, next);
|
|
156
|
+
const stepPlan = plan.steps.find((s) => s.stepId === stepState.id);
|
|
157
|
+
if (!stepPlan) {
|
|
158
|
+
throw new UsageError(`Step "${stepState.id}" of run ${run.id} is not present in the run's frozen plan. The plan and the step ` +
|
|
159
|
+
`journal disagree — this run cannot be described; drive it manually with \`akm workflow complete\`.`);
|
|
160
|
+
}
|
|
161
|
+
// Expression scope: prior steps' promoted artifacts + run params, projected
|
|
162
|
+
// exactly as the engine does (stepOutputsFromEvidence). The current (pending)
|
|
163
|
+
// step contributes no output yet.
|
|
164
|
+
const evidence = {};
|
|
165
|
+
for (const s of next.workflow.steps)
|
|
166
|
+
evidence[s.id] = s.evidence;
|
|
167
|
+
const stepOutputs = stepOutputsFromEvidence(evidence);
|
|
168
|
+
// Gate loop + recovered feedback — the journal-derived state that makes a
|
|
169
|
+
// loop-N brief predict the engine's loop-N dispatch (unit ids + hashes).
|
|
170
|
+
const gateLoop = activeGateLoop(units, stepState.id);
|
|
171
|
+
const gateFeedback = recoverGateFeedback(units, stepState.id, gateLoop);
|
|
172
|
+
const isRouteOnly = !!stepPlan.route && !stepPlan.root;
|
|
173
|
+
const kind = isRouteOnly ? "route" : stepPlan.route ? "execute-and-route" : "execute";
|
|
174
|
+
const criteria = stepState.completionCriteria ?? [];
|
|
175
|
+
const step = {
|
|
176
|
+
stepId: stepState.id,
|
|
177
|
+
title: stepState.title,
|
|
178
|
+
sequenceIndex: stepState.sequenceIndex ?? 0,
|
|
179
|
+
kind,
|
|
180
|
+
instructions: stepState.instructions,
|
|
181
|
+
gate: {
|
|
182
|
+
criteria,
|
|
183
|
+
maxLoops: Math.max(1, stepPlan.gate.maxLoops ?? 1),
|
|
184
|
+
currentLoop: gateLoop,
|
|
185
|
+
judgesArtifact: !isRouteOnly && criteria.length > 0,
|
|
186
|
+
required: stepPlan.gate.required === true,
|
|
187
|
+
},
|
|
188
|
+
...(stepPlan.outputSchema ? { outputSchema: stepPlan.outputSchema } : {}),
|
|
189
|
+
};
|
|
190
|
+
// Journaled dispatch rows for THIS step, keyed by unit id (exclude gate rows).
|
|
191
|
+
const journaledByUnit = new Map();
|
|
192
|
+
for (const row of units) {
|
|
193
|
+
if (row.step_id === stepState.id && row.phase !== GATE_EVALUATION_PHASE) {
|
|
194
|
+
journaledByUnit.set(row.unit_id, row);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// #15 action derivation inputs: the stale-claim set (by unit id) and whether a
|
|
198
|
+
// live engine lease means the whole work-list is `do_not_run` right now.
|
|
199
|
+
const staleIds = new Set(staleUnits.map((u) => u.unitId));
|
|
200
|
+
const leaseLive = lease?.live === true;
|
|
201
|
+
// The work-list — the SAME computation the engine runs (no drift).
|
|
202
|
+
let workList = EMPTY_WORK_LIST;
|
|
203
|
+
// True when every resolvable unit ran to a terminal state but the step never
|
|
204
|
+
// finalized (a required-gate block that was resumed, or a crash between the
|
|
205
|
+
// last unit write and completion) — the fully-terminal recovery state.
|
|
206
|
+
let fullyTerminal = false;
|
|
207
|
+
if (!isRouteOnly) {
|
|
208
|
+
const computed = computeStepWorkList(stepPlan, {
|
|
209
|
+
runId: run.id,
|
|
210
|
+
params: run.params,
|
|
211
|
+
stepOutputs,
|
|
212
|
+
gateLoop,
|
|
213
|
+
...(gateFeedback ? { gateFeedback } : {}),
|
|
214
|
+
});
|
|
215
|
+
if (!computed.ok) {
|
|
216
|
+
workList = { ...EMPTY_WORK_LIST, error: computed.error };
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const list = computed.list;
|
|
220
|
+
fullyTerminal = !leaseLive && isWorkListFullyTerminal(list, journaledByUnit);
|
|
221
|
+
// #21: a worktree-isolated unit runs in a throwaway git worktree that is
|
|
222
|
+
// auto-removed when clean. Files it writes to a `.gitignore`d path are
|
|
223
|
+
// treated as disposable and discarded — warn any driver so collectible
|
|
224
|
+
// artifacts go to a non-ignored path or come back as a reported result.
|
|
225
|
+
if (list.units.some((u) => u.isolation === "worktree")) {
|
|
226
|
+
warnings.push("This step runs unit(s) in an isolated git worktree (`isolation: worktree`). Outputs matched by the " +
|
|
227
|
+
"repository's `.gitignore` are treated as DISPOSABLE — a clean worktree is auto-removed, discarding them. " +
|
|
228
|
+
"Write any artifact that must survive to a NON-ignored path (a tracked or untracked-unignored file), or " +
|
|
229
|
+
"report it as the unit's result. Do not leave collectible work under `node_modules`/`dist`/build/cache paths.");
|
|
230
|
+
}
|
|
231
|
+
workList = {
|
|
232
|
+
isFanOut: list.isFanOut,
|
|
233
|
+
reducer: list.reducer,
|
|
234
|
+
...(list.concurrency !== undefined ? { concurrency: list.concurrency } : {}),
|
|
235
|
+
itemCount: list.items.length,
|
|
236
|
+
units: list.units.map((u) =>
|
|
237
|
+
// Resolve the unit's journaled state by its BEST terminal attempt
|
|
238
|
+
// (base + `~r<n>` retries), the SAME reuse the engine and report
|
|
239
|
+
// surfaces apply (shared selectUnitAttemptRow, finding C): a unit whose
|
|
240
|
+
// base attempt failed but whose retry completed surfaces as `done`, not
|
|
241
|
+
// `failed`, so brief never advertises re-running work a prior retry
|
|
242
|
+
// already finished — keeping the read-only surface consistent with what
|
|
243
|
+
// report/resume would reduce.
|
|
244
|
+
toBriefUnit(run.id, u, stepState.id, selectUnitAttemptRow(u, journaledByUnit), {
|
|
245
|
+
stale: staleIds.has(u.journalBaseId),
|
|
246
|
+
leaseLive,
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Route contract. A route-only step's decision depends solely on prior step
|
|
252
|
+
// outputs, so brief evaluates it deterministically NOW. An execute-and-route
|
|
253
|
+
// step's decision needs the current step's fresh output, which does not exist
|
|
254
|
+
// until the units run — so brief surfaces the contract without a decision.
|
|
255
|
+
let route;
|
|
256
|
+
if (stepPlan.route) {
|
|
257
|
+
route = {
|
|
258
|
+
input: stepPlan.route.input,
|
|
259
|
+
when: stepPlan.route.when,
|
|
260
|
+
...(stepPlan.route.defaultStepId ? { defaultStepId: stepPlan.route.defaultStepId } : {}),
|
|
261
|
+
evaluatedNow: isRouteOnly,
|
|
262
|
+
};
|
|
263
|
+
if (isRouteOnly) {
|
|
264
|
+
const scope = { params: run.params, stepOutputs };
|
|
265
|
+
const decision = evaluateRoute(stepPlan.route, scope);
|
|
266
|
+
if (decision.ok)
|
|
267
|
+
route.decision = { value: decision.value, selected: decision.selected };
|
|
268
|
+
else
|
|
269
|
+
route.decisionError = decision.error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// A step the driver cannot advance with a per-unit `report --unit` gets the
|
|
273
|
+
// `--settle` verb instead. TWO cases:
|
|
274
|
+
// - NON-DISPATCHING (finding D): a route-only step, an empty fan-out, an
|
|
275
|
+
// all-unresolvable work-list, or a whole-list failure — nothing was ever
|
|
276
|
+
// dispatchable.
|
|
277
|
+
// - FULLY TERMINAL (owner manual-validation finding 3): every resolvable
|
|
278
|
+
// unit already ran to a terminal state, but the step never finalized (a
|
|
279
|
+
// required-gate block that was resumed, or a crash before completion). The
|
|
280
|
+
// units show `done`/`failed` with no report command, so without this the
|
|
281
|
+
// driver is stranded — `--settle` runs the shared completion path.
|
|
282
|
+
// Never while a live engine lease owns the spine (a report/settle is refused
|
|
283
|
+
// then anyway). `--expect-step` guards a stale copy once the spine moves.
|
|
284
|
+
const hasReportableWork = !isRouteOnly && !workList.error && workList.units.some((u) => u.resolved.ok);
|
|
285
|
+
const nonDispatching = !hasReportableWork;
|
|
286
|
+
const settleable = !leaseLive && (nonDispatching || fullyTerminal);
|
|
287
|
+
const settleCommand = settleable ? `akm workflow report ${run.id} --settle --expect-step ${stepState.id}` : undefined;
|
|
288
|
+
const settleState = !settleable
|
|
289
|
+
? "none"
|
|
290
|
+
: fullyTerminal
|
|
291
|
+
? "finalize"
|
|
292
|
+
: "non-dispatching";
|
|
293
|
+
const message = buildMessage(step, workList, route, gateLoop, settleState);
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
// Re-stamp the spine token with the resolved active step + real gate loop.
|
|
297
|
+
spineToken: makeSpineToken(run.id, stepState.id, gateLoop, watermark),
|
|
298
|
+
active: true,
|
|
299
|
+
step,
|
|
300
|
+
...(gateFeedback ? { gateFeedback } : {}),
|
|
301
|
+
workList,
|
|
302
|
+
...(route ? { route } : {}),
|
|
303
|
+
...(settleCommand ? { settleCommand } : {}),
|
|
304
|
+
message,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The spine watermark stamped on every brief (#14): run id, active step id,
|
|
309
|
+
* gate loop, and a run-mutation watermark (`updated_at` + journal row count). A
|
|
310
|
+
* driver diffs it across polls to notice the spine moved; `report --expect-step`
|
|
311
|
+
* enforces the step half server-side.
|
|
312
|
+
*/
|
|
313
|
+
function makeSpineToken(runId, stepId, gateLoop, watermark) {
|
|
314
|
+
return `${runId}#${stepId ?? "-"}#l${gateLoop}#${watermark}`;
|
|
315
|
+
}
|
|
316
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
317
|
+
function toBriefUnit(runId, unit, stepId, journaled, ctx) {
|
|
318
|
+
const action = deriveUnitAction(unit, journaled, ctx);
|
|
319
|
+
const report = reportCommandForAction(runId, unit, stepId, action, journaled?.claim_holder ?? null);
|
|
320
|
+
return {
|
|
321
|
+
unitId: unit.unitId,
|
|
322
|
+
nodeId: unit.nodeId,
|
|
323
|
+
index: unit.index,
|
|
324
|
+
runner: unit.runner,
|
|
325
|
+
...(unit.profile ? { profile: unit.profile } : {}),
|
|
326
|
+
...(unit.model ? { model: unit.model } : {}),
|
|
327
|
+
timeoutMs: unit.timeoutMs,
|
|
328
|
+
...(unit.schema ? { outputSchema: unit.schema } : {}),
|
|
329
|
+
// Env asset REF names only — brief never resolves bindings, so no secret
|
|
330
|
+
// value can ever reach this output.
|
|
331
|
+
...(unit.env ? { env: unit.env } : {}),
|
|
332
|
+
...(unit.retry ? { retry: unit.retry } : {}),
|
|
333
|
+
onError: unit.onError,
|
|
334
|
+
...(unit.isFanOut ? { item: unit.item } : {}),
|
|
335
|
+
resolved: unit.resolved.ok
|
|
336
|
+
? { ok: true, instructions: unit.resolved.prompt, inputHash: unit.resolved.inputHash }
|
|
337
|
+
: { ok: false, error: unit.resolved.error },
|
|
338
|
+
...(journaled ? { journaled: toBriefJournaled(journaled) } : {}),
|
|
339
|
+
action,
|
|
340
|
+
...(report ? { report } : {}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Fold the journaled row + engine lease + resolvability into ONE driver-facing
|
|
345
|
+
* action (#15). A live engine lease or an unresolvable unit is `do_not_run`; a
|
|
346
|
+
* terminal row is `done`/`failed`; a live claim is `claimed` (or `stale` once
|
|
347
|
+
* silent); no row is `pending`.
|
|
348
|
+
*/
|
|
349
|
+
function deriveUnitAction(unit, journaled, ctx) {
|
|
350
|
+
if (!unit.resolved.ok)
|
|
351
|
+
return "do_not_run";
|
|
352
|
+
if (ctx.leaseLive)
|
|
353
|
+
return "do_not_run";
|
|
354
|
+
if (!journaled)
|
|
355
|
+
return "pending";
|
|
356
|
+
if (journaled.status === "completed")
|
|
357
|
+
return "done";
|
|
358
|
+
if (journaled.status === "failed")
|
|
359
|
+
return "failed";
|
|
360
|
+
if (journaled.status === "running")
|
|
361
|
+
return ctx.stale ? "stale" : "claimed";
|
|
362
|
+
return "pending";
|
|
363
|
+
}
|
|
364
|
+
function toBriefJournaled(row) {
|
|
365
|
+
// Claim state is meaningful only while the unit is still running; a terminal
|
|
366
|
+
// row keeps its claim columns but they are no longer actionable.
|
|
367
|
+
const claim = row.status === "running"
|
|
368
|
+
? {
|
|
369
|
+
...(row.claim_holder ? { claimedBy: row.claim_holder } : {}),
|
|
370
|
+
...(row.claim_expires_at ? { claimExpiresAt: row.claim_expires_at } : {}),
|
|
371
|
+
}
|
|
372
|
+
: {};
|
|
373
|
+
return {
|
|
374
|
+
unitId: row.unit_id,
|
|
375
|
+
status: row.status,
|
|
376
|
+
...(row.failure_reason ? { failureReason: row.failure_reason } : {}),
|
|
377
|
+
...(row.tokens !== null ? { tokens: row.tokens } : {}),
|
|
378
|
+
...(row.started_at ? { startedAt: row.started_at } : {}),
|
|
379
|
+
...(row.finished_at ? { finishedAt: row.finished_at } : {}),
|
|
380
|
+
...claim,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The `report` command line for a unit, tailored to its action (#15). Terminal
|
|
385
|
+
* `done` and `do_not_run` units get NO command; a `failed` unit gets the
|
|
386
|
+
* `--rerun` form (records a fresh attempt, per #25); `pending`/`stale`/`claimed`
|
|
387
|
+
* get the completed form. Every command carries `--expect-step` (#14) so a
|
|
388
|
+
* copy-pasted command from a stale brief is refused once the spine moves on.
|
|
389
|
+
*
|
|
390
|
+
* A `claimed` unit is held by a LIVE `--status running` claim (`claimHolder`):
|
|
391
|
+
* ONLY that holder can finish it — the report path's claim compare-and-set
|
|
392
|
+
* refuses any other `--session-id`. So its command carries `--session-id
|
|
393
|
+
* <holder>`, which (a) is the exact form the holding driver must use to finish
|
|
394
|
+
* its OWN claim, and (b) makes it unmistakable to a SECOND driver that the unit
|
|
395
|
+
* is spoken for rather than free, runnable work (owner manual-validation
|
|
396
|
+
* finding 2: two drivers must not both treat a live-claimed unit as free). A
|
|
397
|
+
* `stale` unit's claim has expired and is freely reclaimable/finishable by
|
|
398
|
+
* anyone, so it keeps the plain completed form (no `--session-id`).
|
|
399
|
+
*/
|
|
400
|
+
function reportCommandForAction(runId, unit, stepId, action, claimHolder) {
|
|
401
|
+
if (action === "done" || action === "do_not_run")
|
|
402
|
+
return undefined;
|
|
403
|
+
const resultHint = unit.schema
|
|
404
|
+
? "--result-file <result.json> # JSON matching the unit's outputSchema"
|
|
405
|
+
: "--result-file <result.txt> # or --result '<text>' / pipe via stdin";
|
|
406
|
+
const rerun = action === "failed" ? " --rerun" : "";
|
|
407
|
+
// A live claim requires its holder's --session-id to finish; surface it so the
|
|
408
|
+
// holder's command is correct and other drivers see the unit is claimed.
|
|
409
|
+
const session = action === "claimed" && claimHolder ? ` --session-id ${claimHolder}` : "";
|
|
410
|
+
return `akm workflow report ${runId} --unit ${unit.unitId} --expect-step ${stepId} --status completed${rerun}${session} ${resultHint}`;
|
|
411
|
+
}
|
|
412
|
+
export function buildLease(holder, until) {
|
|
413
|
+
if (!holder || !until)
|
|
414
|
+
return undefined;
|
|
415
|
+
return { holder, until, live: until >= new Date().toISOString() };
|
|
416
|
+
}
|
|
417
|
+
function buildMessage(step, workList, route, gateLoop, settleState) {
|
|
418
|
+
const loopNote = gateLoop > 1 ? ` (gate loop ${gateLoop}, addressing prior rejection feedback)` : "";
|
|
419
|
+
const settleNote = settleState !== "none" ? " Advance it with `akm workflow report --settle` (see settleCommand)." : "";
|
|
420
|
+
if (step.kind === "route") {
|
|
421
|
+
const decided = route?.decision ? ` → selects step "${route.decision.selected}"` : "";
|
|
422
|
+
return `Active step "${step.stepId}" is a route step — no units to execute${decided}.${settleNote || " Advances deterministically."}`;
|
|
423
|
+
}
|
|
424
|
+
if (workList.error) {
|
|
425
|
+
return `Active step "${step.stepId}" could not compute a work-list: ${workList.error}${settleNote}`;
|
|
426
|
+
}
|
|
427
|
+
const n = workList.units.length;
|
|
428
|
+
if (settleState === "finalize") {
|
|
429
|
+
// Fully-terminal work-list on a still-active step: everything ran, nothing
|
|
430
|
+
// remains to execute — the step only needs finalization (the run was
|
|
431
|
+
// gate-blocked then resumed, or a crash interrupted completion). A required
|
|
432
|
+
// gate with no judge available will re-block, which is correct behavior.
|
|
433
|
+
const gateNote = step.gate.required && step.gate.criteria.length > 0
|
|
434
|
+
? " If this step's REQUIRED gate has no judge available it will re-block, pending a configured judge or a manual `akm workflow complete`."
|
|
435
|
+
: "";
|
|
436
|
+
return (`Active step "${step.stepId}" has run all ${n} unit(s) to a terminal state${loopNote} — nothing remains to ` +
|
|
437
|
+
`execute or report. Finalize it with \`akm workflow report --settle\` (see settleCommand): the gate is judged ` +
|
|
438
|
+
`and the step advances.${gateNote}`);
|
|
439
|
+
}
|
|
440
|
+
if (settleState === "non-dispatching") {
|
|
441
|
+
return `Active step "${step.stepId}" dispatches no reportable units${loopNote}.${settleNote}`;
|
|
442
|
+
}
|
|
443
|
+
return `Active step "${step.stepId}" expects ${n} unit(s)${loopNote}. Execute them, then report each result.`;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* brief-specific frozen-plan loader: unlike the engine's loader, a NULL
|
|
447
|
+
* plan_json is a hard, actionable error rather than a warn-and-compile — brief
|
|
448
|
+
* describes the frozen plan the engine executes, and a legacy run has none.
|
|
449
|
+
*/
|
|
450
|
+
function loadFrozenPlanForBrief(runId, planJson, planHash) {
|
|
451
|
+
if (!planJson) {
|
|
452
|
+
throw new UsageError(`Workflow run ${runId} predates frozen plans (no plan_json on the run row) and cannot be described by ` +
|
|
453
|
+
`\`akm workflow brief\`. Drive it with engine-driven mode instead: \`akm workflow run ${runId}\` ` +
|
|
454
|
+
`(which compiles a legacy run's plan from the live asset).`);
|
|
455
|
+
}
|
|
456
|
+
return parseFrozenPlan(runId, planJson, planHash);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Resolve `target` to a concrete run id WITHOUT starting anything. A run id
|
|
460
|
+
* resolves directly; a workflow ref resolves to its active run in the current
|
|
461
|
+
* scope, and NO active run is a NotFoundError (brief never auto-starts — that
|
|
462
|
+
* would mutate).
|
|
463
|
+
*/
|
|
464
|
+
export async function resolveRunId(target) {
|
|
465
|
+
return withWorkflowRunsRepo((repo) => {
|
|
466
|
+
const byId = repo.getRunById(target);
|
|
467
|
+
if (byId)
|
|
468
|
+
return byId.id;
|
|
469
|
+
if (!target.includes(":")) {
|
|
470
|
+
throw new NotFoundError(`Workflow run "${target}" not found.`, "WORKFLOW_NOT_FOUND");
|
|
471
|
+
}
|
|
472
|
+
const parsed = parseAssetRef(target);
|
|
473
|
+
if (parsed.type !== "workflow") {
|
|
474
|
+
throw new UsageError(`Expected a workflow run id or workflow ref (workflow:<name>), got "${target}".`);
|
|
475
|
+
}
|
|
476
|
+
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${canonicalizeWorkflowName(parsed.name)}`;
|
|
477
|
+
const active = repo.getActiveRunRowForScope(ref, getCurrentWorkflowScopeKey());
|
|
478
|
+
if (!active) {
|
|
479
|
+
throw new NotFoundError(`No active workflow run for ${ref} in this scope. \`akm workflow brief\` describes an existing run and never ` +
|
|
480
|
+
`starts one — run \`akm workflow start ${ref}\` (or \`akm workflow run ${ref}\`) first.`, "WORKFLOW_NOT_FOUND");
|
|
481
|
+
}
|
|
482
|
+
return active.id;
|
|
483
|
+
});
|
|
484
|
+
}
|