akm-cli 0.9.0-rc.0 → 0.9.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +339 -2
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +379 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +118 -2
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -50
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +16 -2
- package/dist/integrations/agent/profiles.js +36 -62
- package/dist/integrations/agent/runner-dispatch.js +91 -4
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +141 -20
- package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
- package/dist/integrations/harnesses/aider/index.js +57 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
- package/dist/integrations/harnesses/amazonq/index.js +58 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
- package/dist/integrations/harnesses/claude/index.js +25 -25
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
- package/dist/integrations/harnesses/codex/index.js +62 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
- package/dist/integrations/harnesses/copilot/index.js +59 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
- package/dist/integrations/harnesses/gemini/index.js +59 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +27 -28
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +15 -22
- package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
- package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
- package/dist/integrations/harnesses/openhands/index.js +57 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
- package/dist/integrations/harnesses/pi/index.js +57 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +8 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +6 -0
- package/dist/output/text/helpers.js +215 -2
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +16638 -0
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +372 -0
- package/dist/scripts/migrate-storage.js +10944 -8801
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +190 -1
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/text-import-hook.mjs +1 -1
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +200 -13
- package/dist/workflows/exec/brief.js +478 -0
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +1034 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1355 -0
- package/dist/workflows/exec/run-workflow.js +609 -0
- package/dist/workflows/exec/scheduler.js +71 -0
- package/dist/workflows/exec/step-work.js +1212 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +375 -0
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +68 -0
- package/dist/workflows/ir/schema.js +545 -0
- package/dist/workflows/parser.js +10 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +869 -0
- package/dist/workflows/program/project.js +104 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +246 -69
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +26 -1
- package/docs/data-and-telemetry.md +4 -3
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
* Renderer name for YAML workflow programs. Lives in this leaf module (not
|
|
6
|
+
* `../renderer.ts`) so the indexer matcher can name it without importing the
|
|
7
|
+
* renderer module and its registration side effects.
|
|
8
|
+
*/
|
|
9
|
+
export const WORKFLOW_PROGRAM_RENDERER_NAME = "workflow-program-yaml";
|
|
10
|
+
/** File extensions that mark a workflow asset as a YAML program. */
|
|
11
|
+
const WORKFLOW_PROGRAM_PATH_RE = /\.ya?ml$/i;
|
|
12
|
+
/** True when a workflow asset path holds a YAML program (vs. markdown). */
|
|
13
|
+
export function isWorkflowProgramPath(filePath) {
|
|
14
|
+
return WORKFLOW_PROGRAM_PATH_RE.test(filePath);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The instruction text a step contributes to the flat step projection.
|
|
18
|
+
* Unit and map steps carry their unit's RAW instruction template; route
|
|
19
|
+
* steps have no unit, so a deterministic description of the routing table
|
|
20
|
+
* stands in (the spine still needs a non-empty instructions string).
|
|
21
|
+
*/
|
|
22
|
+
export function programStepInstructions(step) {
|
|
23
|
+
if (step.unit)
|
|
24
|
+
return step.unit.instructions;
|
|
25
|
+
if (step.map)
|
|
26
|
+
return step.map.unit.instructions;
|
|
27
|
+
if (step.route) {
|
|
28
|
+
const branches = step.route.branches.map((b) => `"${b.match}" -> ${b.stepId}`);
|
|
29
|
+
if (step.route.defaultStepId !== undefined)
|
|
30
|
+
branches.push(`default -> ${step.route.defaultStepId}`);
|
|
31
|
+
return `Route on ${step.route.input}: ${branches.join(", ")}.`;
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Project the program's steps into flat `WorkflowStepDefinition`s.
|
|
37
|
+
*
|
|
38
|
+
* `gate.criteria` MUST project into `completionCriteria`: `startWorkflowRun`
|
|
39
|
+
* persists this projection as the step rows' `completion_json`, which is what
|
|
40
|
+
* `completeWorkflowStep` reads to run the summary-validation gate (fail-open
|
|
41
|
+
* when empty). Dropping the criteria here silently disarms every YAML gate.
|
|
42
|
+
*/
|
|
43
|
+
export function projectProgramStepDefinitions(program) {
|
|
44
|
+
return program.steps.map((step, index) => ({
|
|
45
|
+
id: step.id,
|
|
46
|
+
title: step.title ?? step.id,
|
|
47
|
+
instructions: programStepInstructions(step),
|
|
48
|
+
...(step.gate && step.gate.criteria.length > 0 ? { completionCriteria: [...step.gate.criteria] } : {}),
|
|
49
|
+
sequenceIndex: index,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
/** Project the `params` block into the flat `WorkflowParameter` list. */
|
|
53
|
+
export function projectProgramParameters(program) {
|
|
54
|
+
if (!program.params)
|
|
55
|
+
return undefined;
|
|
56
|
+
const parameters = Object.entries(program.params).map(([name, schema]) => {
|
|
57
|
+
const description = schema.description;
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
...(typeof description === "string" && description !== "" ? { description } : {}),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return parameters.length > 0 ? parameters : undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compact, show-facing orchestration summary for one program step, reusing
|
|
67
|
+
* the existing `WorkflowStepOrchestrationSummary` shape. Field mapping:
|
|
68
|
+
* `engine`/`model`/`timeoutMs` merge the run-level `defaults` exactly like
|
|
69
|
+
* the compiler does (per-unit wins), `fanOut.over` carries the raw `${{ … }}`
|
|
70
|
+
* expression, and `route` carries the explicit input + branch table.
|
|
71
|
+
* Returns undefined when the step declares nothing worth summarizing.
|
|
72
|
+
*/
|
|
73
|
+
export function summarizeProgramStepOrchestration(step, defaults) {
|
|
74
|
+
const unit = step.unit ?? step.map?.unit;
|
|
75
|
+
const engine = unit?.engine ?? defaults?.engine;
|
|
76
|
+
const model = unit?.model ?? defaults?.model;
|
|
77
|
+
const timeoutMs = unit?.timeoutMs !== undefined ? unit.timeoutMs : defaults?.timeoutMs;
|
|
78
|
+
const summary = {
|
|
79
|
+
...(engine !== undefined ? { engine } : {}),
|
|
80
|
+
...(model !== undefined ? { model } : {}),
|
|
81
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
82
|
+
...(step.map
|
|
83
|
+
? {
|
|
84
|
+
fanOut: {
|
|
85
|
+
over: step.map.over,
|
|
86
|
+
...(step.map.concurrency !== undefined ? { concurrency: step.map.concurrency } : {}),
|
|
87
|
+
reducer: step.map.reducer ?? "collect",
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
91
|
+
...(unit?.output !== undefined || step.output !== undefined ? { hasSchema: true } : {}),
|
|
92
|
+
...(unit?.env !== undefined ? { env: [...unit.env] } : {}),
|
|
93
|
+
...(step.route
|
|
94
|
+
? {
|
|
95
|
+
route: {
|
|
96
|
+
input: step.route.input,
|
|
97
|
+
branches: step.route.branches.map((b) => ({ match: b.match, stepId: b.stepId })),
|
|
98
|
+
...(step.route.defaultStepId !== undefined ? { defaultStepId: step.route.defaultStepId } : {}),
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
return Object.keys(summary).length > 0 ? summary : undefined;
|
|
104
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
export const WORKFLOW_PROGRAM_VERSION = 2;
|
|
5
|
+
/** @deprecated source v2 rejects runner; retained for TypeScript migration only. */
|
|
6
|
+
export const PROGRAM_RUNNER_KINDS = ["llm", "agent", "sdk", "inherit"];
|
|
7
|
+
/** How a map step folds its per-item unit results into the step artifact. */
|
|
8
|
+
export const PROGRAM_REDUCERS = ["collect", "vote"];
|
|
9
|
+
/** Failure policy: fail the step on first unit failure, or record and go on. */
|
|
10
|
+
export const PROGRAM_ON_ERROR = ["fail", "continue"];
|
|
11
|
+
/** Filesystem isolation for file-mutating units (enforcement is R2). */
|
|
12
|
+
export const PROGRAM_ISOLATION_KINDS = ["none", "worktree"];
|
|
13
|
+
/**
|
|
14
|
+
* `retry.on` vocabulary — exactly the persisted `AgentFailureReason` taxonomy
|
|
15
|
+
* from `src/integrations/agent/spawn.ts`. The `satisfies` clause fails the
|
|
16
|
+
* typecheck if spawn.ts adds/renames a reason without this list (and the JSON
|
|
17
|
+
* Schema, via the drift test) being updated.
|
|
18
|
+
*/
|
|
19
|
+
const RETRY_REASON_SET = {
|
|
20
|
+
timeout: true,
|
|
21
|
+
spawn_failed: true,
|
|
22
|
+
non_zero_exit: true,
|
|
23
|
+
parse_error: true,
|
|
24
|
+
cooldown: true,
|
|
25
|
+
llm_rate_limit: true,
|
|
26
|
+
llm_content_filter: true,
|
|
27
|
+
llm_invalid_json: true,
|
|
28
|
+
content_policy_reject: true,
|
|
29
|
+
unsupported_type: true,
|
|
30
|
+
no_change: true,
|
|
31
|
+
aborted: true,
|
|
32
|
+
};
|
|
33
|
+
export const PROGRAM_RETRY_REASONS = Object.keys(RETRY_REASON_SET);
|
|
34
|
+
/**
|
|
35
|
+
* Step ids: `[A-Za-z_][A-Za-z0-9_-]*` (also pinned in the JSON Schema).
|
|
36
|
+
*
|
|
37
|
+
* This is EXACTLY the `<ident>` grammar `readIdent` accepts in
|
|
38
|
+
* `program/expressions.ts` for `${{ steps.<id>.output }}` references: a
|
|
39
|
+
* letter/underscore first char, then letters/digits/underscores/dashes, and
|
|
40
|
+
* NO dots (the expression parser treats `.` as the `.output` path separator,
|
|
41
|
+
* so a dotted id could never be addressed). Keeping step ids inside the
|
|
42
|
+
* addressable grammar guarantees every step can be referenced from map
|
|
43
|
+
* inputs, routes, and unit templates without renaming.
|
|
44
|
+
*
|
|
45
|
+
* Forbidding dots additionally keeps the engine's internal gate-row node id
|
|
46
|
+
* `<stepId>.gate` collision-free: no user step id can contain a dot, so it can
|
|
47
|
+
* never equal another step's `<stepId>.gate` synthetic id.
|
|
48
|
+
*/
|
|
49
|
+
export const PROGRAM_STEP_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
50
|
+
/**
|
|
51
|
+
* Param names must be `${{ params.<ident> }}`-addressable, so they are plain
|
|
52
|
+
* identifiers (no dots/dashes).
|
|
53
|
+
*/
|
|
54
|
+
export const PROGRAM_PARAM_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* Show + indexing
|
|
5
|
+
* Show + indexing renderers for workflow assets.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Two formats, one asset type:
|
|
8
|
+
*
|
|
9
|
+
* - `workflow-md` — reads the classic linear markdown via `parseWorkflow`
|
|
10
|
+
* and projects the validated `WorkflowDocument` down to the public
|
|
11
|
+
* `ShowResponse` shape (which still uses the flat
|
|
12
|
+
* `WorkflowStepDefinition` type for backwards compatibility) and into
|
|
13
|
+
* search hints for the indexer.
|
|
14
|
+
* - `workflow-program-yaml` — reads a YAML workflow *program* (redesign
|
|
15
|
+
* addendum, R1) via `parseWorkflowProgram` and projects it through
|
|
16
|
+
* `program/project.ts`, including a compact orchestration summary per
|
|
17
|
+
* step (runner/model, `fanOut.over` expression, route table).
|
|
11
18
|
*/
|
|
12
19
|
import { makeAssetRef } from "../core/asset/asset-ref.js";
|
|
13
20
|
import { UsageError } from "../core/errors.js";
|
|
14
21
|
import { registerMetadataContributor } from "../indexer/passes/metadata-contributors.js";
|
|
15
22
|
import { parseWorkflow } from "./parser.js";
|
|
23
|
+
import { parseWorkflowProgram } from "./program/parser.js";
|
|
24
|
+
import { programStepInstructions, projectProgramParameters, summarizeProgramStepOrchestration, WORKFLOW_PROGRAM_RENDERER_NAME, } from "./program/project.js";
|
|
16
25
|
import { cacheWorkflowDocument } from "./runtime/document-cache.js";
|
|
26
|
+
export { WORKFLOW_PROGRAM_RENDERER_NAME };
|
|
17
27
|
function shellQuote(value) {
|
|
18
28
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
19
29
|
}
|
|
@@ -59,6 +69,43 @@ export const workflowMdRenderer = {
|
|
|
59
69
|
};
|
|
60
70
|
},
|
|
61
71
|
};
|
|
72
|
+
function loadProgram(ctx) {
|
|
73
|
+
const result = parseWorkflowProgram(ctx.content(), { path: ctx.relPath });
|
|
74
|
+
if (result.ok)
|
|
75
|
+
return result.program;
|
|
76
|
+
const summary = result.errors.map((e) => `${ctx.relPath}:${e.line} — ${e.message}`).join("\n");
|
|
77
|
+
throw new UsageError(`Workflow has errors:\n${summary}`);
|
|
78
|
+
}
|
|
79
|
+
/** Show renderer for YAML workflow programs — mirrors `workflowMdRenderer`. */
|
|
80
|
+
export const workflowProgramRenderer = {
|
|
81
|
+
name: WORKFLOW_PROGRAM_RENDERER_NAME,
|
|
82
|
+
buildShowResponse(ctx) {
|
|
83
|
+
const name = deriveName(ctx);
|
|
84
|
+
const program = loadProgram(ctx);
|
|
85
|
+
const ref = makeAssetRef("workflow", name, ctx.origin);
|
|
86
|
+
const parameters = projectProgramParameters(program);
|
|
87
|
+
return {
|
|
88
|
+
type: "workflow",
|
|
89
|
+
name,
|
|
90
|
+
path: ctx.absPath,
|
|
91
|
+
action: buildWorkflowAction(ref),
|
|
92
|
+
description: program.description,
|
|
93
|
+
workflowTitle: program.name,
|
|
94
|
+
...(parameters ? { parameters: parameters.map((p) => p.name), workflowParameters: parameters } : {}),
|
|
95
|
+
steps: program.steps.map((step, index) => {
|
|
96
|
+
const orchestration = summarizeProgramStepOrchestration(step, program.defaults);
|
|
97
|
+
return {
|
|
98
|
+
id: step.id,
|
|
99
|
+
title: step.title ?? step.id,
|
|
100
|
+
instructions: programStepInstructions(step),
|
|
101
|
+
...(step.gate ? { completionCriteria: [...step.gate.criteria] } : {}),
|
|
102
|
+
sequenceIndex: index,
|
|
103
|
+
...(orchestration ? { orchestration } : {}),
|
|
104
|
+
};
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
62
109
|
registerMetadataContributor({
|
|
63
110
|
name: "workflow-document-metadata",
|
|
64
111
|
appliesTo: ({ rendererName }) => rendererName === "workflow-md",
|
|
@@ -84,3 +131,33 @@ registerMetadataContributor({
|
|
|
84
131
|
cacheWorkflowDocument(entry, doc);
|
|
85
132
|
},
|
|
86
133
|
});
|
|
134
|
+
registerMetadataContributor({
|
|
135
|
+
name: "workflow-program-metadata",
|
|
136
|
+
appliesTo: ({ rendererName }) => rendererName === WORKFLOW_PROGRAM_RENDERER_NAME,
|
|
137
|
+
contribute(entry, { renderContext }) {
|
|
138
|
+
// Parse failures throw, which the metadata pass turns into a
|
|
139
|
+
// skip-with-warning — broken programs never land in the index, mirroring
|
|
140
|
+
// markdown workflows. No workflow_documents cache row is written: YAML
|
|
141
|
+
// programs are re-parsed from disk by the runtime loader.
|
|
142
|
+
const program = loadProgram(renderContext);
|
|
143
|
+
const hints = new Set(entry.searchHints ?? []);
|
|
144
|
+
hints.add(program.name);
|
|
145
|
+
for (const step of program.steps) {
|
|
146
|
+
hints.add(step.id);
|
|
147
|
+
if (step.title)
|
|
148
|
+
hints.add(step.title);
|
|
149
|
+
hints.add(programStepInstructions(step));
|
|
150
|
+
for (const criterion of step.gate?.criteria ?? []) {
|
|
151
|
+
hints.add(criterion);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
entry.searchHints = Array.from(hints).filter(Boolean);
|
|
155
|
+
if (!entry.description && program.description) {
|
|
156
|
+
entry.description = program.description;
|
|
157
|
+
}
|
|
158
|
+
const parameters = projectProgramParameters(program);
|
|
159
|
+
if (parameters?.length) {
|
|
160
|
+
entry.parameters = parameters;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export const WORKFLOW_MAX_PLAN_BYTES = 2 * 1024 * 1024;
|
|
5
|
+
export const WORKFLOW_MAX_SOURCE_BYTES = 1024 * 1024;
|
|
6
|
+
export const WORKFLOW_MAX_STEPS = 256;
|
|
7
|
+
export const WORKFLOW_MAX_ENGINES = 64;
|
|
8
|
+
export const WORKFLOW_MAX_PARAMS = 128;
|
|
9
|
+
export const WORKFLOW_MAX_ROUTE_BRANCHES = 256;
|
|
10
|
+
export const WORKFLOW_MAX_INSTRUCTION_BYTES = 256 * 1024;
|
|
11
|
+
export const WORKFLOW_MAX_SCHEMA_BYTES = 256 * 1024;
|
|
12
|
+
export const WORKFLOW_MAX_EXTRA_PARAMS_BYTES = 64 * 1024;
|
|
13
|
+
export const WORKFLOW_MAX_JSON_DEPTH = 64;
|
|
14
|
+
export const WORKFLOW_MAX_MAP_EXPANSION = 10_000;
|
|
15
|
+
export function utf8Bytes(value) {
|
|
16
|
+
return Buffer.byteLength(value, "utf8");
|
|
17
|
+
}
|
|
18
|
+
export function jsonBytes(value) {
|
|
19
|
+
return utf8Bytes(JSON.stringify(value));
|
|
20
|
+
}
|
|
@@ -11,14 +11,42 @@
|
|
|
11
11
|
* already exposes via the environment.
|
|
12
12
|
*
|
|
13
13
|
* Resolution is best-effort and environment-driven:
|
|
14
|
-
* - harness: AKM_AGENT_HARNESS, else inferred from a
|
|
14
|
+
* - harness: AKM_AGENT_HARNESS, else inferred from a harness session-id
|
|
15
|
+
* env var (`identityEnv`), else from a harness presence flag
|
|
16
|
+
* (`presenceEnv`).
|
|
15
17
|
* - sessionId: AKM_SESSION_ID, else the harness-native session env var.
|
|
18
|
+
* Presence flags NEVER contribute a session id — their values
|
|
19
|
+
* (`CODEX_SANDBOX=seatbelt`, `GEMINI_CLI=1`) are modes/flags,
|
|
20
|
+
* not sessions, and must not be persisted as agent_session_id.
|
|
16
21
|
*
|
|
17
22
|
* Explicit values passed to `startWorkflowRun` always win over the environment.
|
|
18
23
|
*
|
|
24
|
+
* The harness-native markers are DERIVED from `HARNESS_REGISTRY` (plan §"Kill
|
|
25
|
+
* registry drift", P2): each harness declares its session-id env vars via
|
|
26
|
+
* `identityEnv` and its presence-only flags via `presenceEnv`, so adding a
|
|
27
|
+
* harness never touches this module. Only the `AKM_*` explicit-override vars
|
|
28
|
+
* are non-registry (they are akm's own, not any harness's) and stay hardcoded
|
|
29
|
+
* here.
|
|
30
|
+
*
|
|
19
31
|
* @module workflows/agent-identity
|
|
20
32
|
*/
|
|
21
|
-
import { denormalizeRuntimeIdentity } from "../../integrations/harnesses/index.js";
|
|
33
|
+
import { denormalizeRuntimeIdentity, HARNESS_REGISTRY } from "../../integrations/harnesses/index.js";
|
|
34
|
+
/**
|
|
35
|
+
* Derive a marker table from one registry env-var field, ordered by canonical
|
|
36
|
+
* id. The sort keeps the pre-derivation precedence byte-identical ('claude'
|
|
37
|
+
* before 'opencode' — the old if/else chain's order) and independent of
|
|
38
|
+
* `HARNESS_REGISTRY` declaration order, which is pinned for JSON-schema enum
|
|
39
|
+
* stability, not detection precedence.
|
|
40
|
+
*/
|
|
41
|
+
function deriveMarkers(pick) {
|
|
42
|
+
return HARNESS_REGISTRY.filter((h) => (pick(h)?.length ?? 0) > 0)
|
|
43
|
+
.map((h) => ({ harnessId: h.id, envKeys: pick(h) ?? [] }))
|
|
44
|
+
.sort((a, b) => a.harnessId.localeCompare(b.harnessId));
|
|
45
|
+
}
|
|
46
|
+
/** Session-id-bearing markers — usable for BOTH harness inference and sessionId. */
|
|
47
|
+
const SESSION_MARKERS = deriveMarkers((h) => h.identityEnv);
|
|
48
|
+
/** Presence-only flags — harness inference ONLY; their values are never a session id. */
|
|
49
|
+
const PRESENCE_MARKERS = deriveMarkers((h) => h.presenceEnv);
|
|
22
50
|
function firstNonEmpty(env, keys) {
|
|
23
51
|
for (const key of keys) {
|
|
24
52
|
const value = env[key];
|
|
@@ -37,20 +65,37 @@ export function resolveAgentIdentity(env = process.env) {
|
|
|
37
65
|
// Explicit override always wins.
|
|
38
66
|
let harness = firstNonEmpty(env, ["AKM_AGENT_HARNESS"]);
|
|
39
67
|
if (!harness) {
|
|
40
|
-
// Infer the harness from a harness-specific *session* env var
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
// Infer the harness from a harness-specific *session* env var first
|
|
69
|
+
// (registry `identityEnv` markers). A concrete session id is the
|
|
70
|
+
// strongest evidence of the immediate driver, so it outranks any
|
|
71
|
+
// presence flag — e.g. opencode launched inside a codex sandbox
|
|
72
|
+
// (OPENCODE_SESSION_ID + CODEX_SANDBOX) attributes to opencode.
|
|
73
|
+
for (const marker of SESSION_MARKERS) {
|
|
74
|
+
if (firstNonEmpty(env, marker.envKeys)) {
|
|
75
|
+
// Report the harness's RUNTIME identity (e.g. canonical 'claude' →
|
|
76
|
+
// 'claude-code') via the registry's #562 bridge so the persisted
|
|
77
|
+
// runtime string can't drift.
|
|
78
|
+
harness = denormalizeRuntimeIdentity(marker.harnessId);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
49
81
|
}
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
}
|
|
83
|
+
if (!harness) {
|
|
84
|
+
// Fall back to presence-only flags (registry `presenceEnv`). These are
|
|
85
|
+
// stamped by the harness on its OWN child processes (CODEX_SANDBOX,
|
|
86
|
+
// GEMINI_CLI=1), so they cannot mis-attribute manual CLI invocations —
|
|
87
|
+
// but they carry no session id, so sessionId stays null below.
|
|
88
|
+
for (const marker of PRESENCE_MARKERS) {
|
|
89
|
+
if (firstNonEmpty(env, marker.envKeys)) {
|
|
90
|
+
harness = denormalizeRuntimeIdentity(marker.harnessId);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
52
93
|
}
|
|
53
94
|
}
|
|
54
|
-
|
|
95
|
+
// Session id: the explicit AKM override first, then the session-id-bearing
|
|
96
|
+
// registry markers in the same precedence order as harness inference (so
|
|
97
|
+
// harness and session id agree when multiple harness env vars are present).
|
|
98
|
+
// Presence flags are deliberately excluded — their values are not sessions.
|
|
99
|
+
const sessionId = firstNonEmpty(env, ["AKM_SESSION_ID", ...SESSION_MARKERS.flatMap((m) => [...m.envKeys])]);
|
|
55
100
|
return { harness, sessionId };
|
|
56
101
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { UsageError } from "../../core/errors.js";
|
|
5
|
+
import { decodeCanonicalPlan } from "../ir/plan-hash.js";
|
|
6
|
+
import { WORKFLOW_IR_VERSION } from "../ir/schema.js";
|
|
7
|
+
/** One policy authority for executable versus inspection-only historical runs. */
|
|
8
|
+
export function classifyWorkflowRunPlan(row) {
|
|
9
|
+
const runId = row.id ?? "(unknown)";
|
|
10
|
+
if (!row.plan_json) {
|
|
11
|
+
if (row.plan_ir_version === WORKFLOW_IR_VERSION) {
|
|
12
|
+
return {
|
|
13
|
+
support: "corrupt-plan",
|
|
14
|
+
irVersion: row.plan_ir_version,
|
|
15
|
+
error: `Workflow run ${runId} declares workflow IR version ${row.plan_ir_version} but has no frozen plan.`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
20
|
+
irVersion: row.plan_ir_version ?? null,
|
|
21
|
+
error: row.plan_ir_version === null || row.plan_ir_version === undefined
|
|
22
|
+
? `Workflow run ${runId} has no executable workflow IR plan.`
|
|
23
|
+
: `Workflow run ${runId} uses unsupported workflow IR version ${String(row.plan_ir_version)} and has no frozen plan.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(row.plan_json);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
32
|
+
return {
|
|
33
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
34
|
+
irVersion: row.plan_ir_version ?? null,
|
|
35
|
+
error: `Workflow run ${runId} has malformed historical frozen plan JSON that cannot be executed.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
support: "corrupt-plan",
|
|
40
|
+
irVersion: row.plan_ir_version ?? null,
|
|
41
|
+
error: `Workflow run ${runId} has corrupt frozen plan JSON.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const decodedVersion = typeof raw === "object" && raw !== null ? raw.irVersion : null;
|
|
45
|
+
if (!Number.isSafeInteger(decodedVersion) || decodedVersion < 1) {
|
|
46
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
47
|
+
return {
|
|
48
|
+
support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
|
|
49
|
+
irVersion: row.plan_ir_version ?? null,
|
|
50
|
+
error: `Workflow run ${runId} has historical frozen plan data with no supported IR version.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
support: "corrupt-plan",
|
|
55
|
+
irVersion: row.plan_ir_version ?? null,
|
|
56
|
+
error: `Workflow run ${runId} has a missing or invalid frozen plan IR version.`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (row.plan_ir_version !== null && row.plan_ir_version !== undefined && row.plan_ir_version !== decodedVersion) {
|
|
60
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION && decodedVersion !== WORKFLOW_IR_VERSION) {
|
|
61
|
+
return {
|
|
62
|
+
support: "unsupported-version",
|
|
63
|
+
irVersion: decodedVersion,
|
|
64
|
+
error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)} with mismatched historical metadata.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
support: "corrupt-plan",
|
|
69
|
+
irVersion: decodedVersion,
|
|
70
|
+
error: `Workflow run ${runId} has mismatched stored plan IR version (${String(row.plan_ir_version)} != ${String(decodedVersion)}).`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (decodedVersion !== WORKFLOW_IR_VERSION) {
|
|
74
|
+
return {
|
|
75
|
+
support: "unsupported-version",
|
|
76
|
+
irVersion: decodedVersion,
|
|
77
|
+
error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)}.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
|
|
81
|
+
return {
|
|
82
|
+
support: "corrupt-plan",
|
|
83
|
+
irVersion: row.plan_ir_version ?? null,
|
|
84
|
+
error: `Workflow run ${runId} has no stored plan IR version.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return {
|
|
89
|
+
support: "supported",
|
|
90
|
+
irVersion: WORKFLOW_IR_VERSION,
|
|
91
|
+
plan: decodeCanonicalPlan(runId, row.plan_json, row.plan_hash),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (cause) {
|
|
95
|
+
return { support: "corrupt-plan", irVersion: 3, error: cause instanceof Error ? cause.message : String(cause) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Reject an execution mutation while preserving inspection and abandon access. */
|
|
99
|
+
export function requireExecutableWorkflowPlan(row) {
|
|
100
|
+
const classified = classifyWorkflowRunPlan(row);
|
|
101
|
+
if (classified.support === "supported")
|
|
102
|
+
return classified.plan;
|
|
103
|
+
if (classified.support === "missing-plan" || classified.support === "unsupported-version") {
|
|
104
|
+
throw new UsageError(`${classified.error} This historical run is inspection-only; abandon it with \`akm workflow abandon ${row.id ?? "<run>"}\` and start a new run.`, "WORKFLOW_IR_VERSION_UNSUPPORTED");
|
|
105
|
+
}
|
|
106
|
+
throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
|
|
107
|
+
}
|
|
108
|
+
/** Abandon is the sole mutation allowed for a historical run; corrupt v3 data is never mutated. */
|
|
109
|
+
export function requireAbandonableWorkflowPlan(row) {
|
|
110
|
+
const classified = classifyWorkflowRunPlan(row);
|
|
111
|
+
if (classified.support === "corrupt-plan")
|
|
112
|
+
throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
|
|
113
|
+
}
|
|
114
|
+
/** Project persisted spine rows from the decoded plan, never from the mutable source asset. */
|
|
115
|
+
export function frozenStepRows(plan) {
|
|
116
|
+
return plan.steps.map((step) => ({
|
|
117
|
+
stepId: step.stepId,
|
|
118
|
+
stepTitle: step.title,
|
|
119
|
+
instructions: step.root
|
|
120
|
+
? step.root.kind === "map"
|
|
121
|
+
? step.root.template.instructions
|
|
122
|
+
: step.root.instructions
|
|
123
|
+
: routeInstructions(step.route),
|
|
124
|
+
completionJson: step.gate.criteria.length > 0 ? JSON.stringify(step.gate.criteria) : null,
|
|
125
|
+
sequenceIndex: step.sequenceIndex,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
/** Verify the durable spine still agrees with the decoded/hash-verified plan before any mutation. */
|
|
129
|
+
export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
|
|
130
|
+
const expected = frozenStepRows(plan);
|
|
131
|
+
if (rows.length !== expected.length)
|
|
132
|
+
corruptSpine(run.id, "step count differs from the frozen plan");
|
|
133
|
+
for (let index = 0; index < expected.length; index++) {
|
|
134
|
+
const actual = rows[index];
|
|
135
|
+
const planned = expected[index];
|
|
136
|
+
if (!actual ||
|
|
137
|
+
!planned ||
|
|
138
|
+
actual.step_id !== planned.stepId ||
|
|
139
|
+
actual.step_title !== planned.stepTitle ||
|
|
140
|
+
actual.instructions !== planned.instructions ||
|
|
141
|
+
actual.completion_json !== planned.completionJson ||
|
|
142
|
+
actual.sequence_index !== planned.sequenceIndex) {
|
|
143
|
+
const fields = [
|
|
144
|
+
actual.step_id !== planned.stepId ? "step_id" : "",
|
|
145
|
+
actual.step_title !== planned.stepTitle ? "step_title" : "",
|
|
146
|
+
actual.instructions !== planned.instructions ? "instructions" : "",
|
|
147
|
+
actual.completion_json !== planned.completionJson ? "completion_json" : "",
|
|
148
|
+
actual.sequence_index !== planned.sequenceIndex ? "sequence_index" : "",
|
|
149
|
+
].filter(Boolean);
|
|
150
|
+
corruptSpine(run.id, `step row ${index} differs from the frozen plan (${fields.join(", ") || "missing row"})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (run.current_step_id !== null && !expected.some((step) => step.stepId === run.current_step_id))
|
|
154
|
+
corruptSpine(run.id, `current step ${run.current_step_id} is not in the frozen plan`);
|
|
155
|
+
const current = run.current_step_id ? rows.find((row) => row.step_id === run.current_step_id) : undefined;
|
|
156
|
+
if (run.status === "active") {
|
|
157
|
+
const firstPending = rows.find((row) => row.status === "pending");
|
|
158
|
+
if (!current || current.status !== "pending" || firstPending?.step_id !== current.step_id)
|
|
159
|
+
corruptSpine(run.id, "active status/current step does not match the first pending plan step");
|
|
160
|
+
}
|
|
161
|
+
else if (run.status === "blocked") {
|
|
162
|
+
if (!current || current.status !== "blocked")
|
|
163
|
+
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
164
|
+
}
|
|
165
|
+
else if (run.status === "failed") {
|
|
166
|
+
// `workflow abandon` marks the run failed while intentionally leaving its
|
|
167
|
+
// current step pending so `resume` can reopen the same work.
|
|
168
|
+
if (!current || (current.status !== "failed" && current.status !== "pending"))
|
|
169
|
+
corruptSpine(run.id, `${run.status} status does not match the current plan step`);
|
|
170
|
+
}
|
|
171
|
+
else if (run.status === "completed") {
|
|
172
|
+
if (run.current_step_id !== null ||
|
|
173
|
+
rows.some((row) => row.status === "pending" || row.status === "blocked" || row.status === "failed"))
|
|
174
|
+
corruptSpine(run.id, "completed status disagrees with the plan spine");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function routeInstructions(route) {
|
|
178
|
+
const branches = Object.entries(route.when)
|
|
179
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
180
|
+
.map(([match, stepId]) => `"${match}" -> ${stepId}`);
|
|
181
|
+
if (route.defaultStepId !== undefined)
|
|
182
|
+
branches.push(`default -> ${route.defaultStepId}`);
|
|
183
|
+
return `Route on ${route.input}: ${branches.join(", ")}.`;
|
|
184
|
+
}
|
|
185
|
+
function corruptSpine(runId, detail) {
|
|
186
|
+
throw new UsageError(`Workflow run ${runId} has a corrupt durable step spine: ${detail}. Refusing to mutate state that disagrees with its frozen plan.`, "INVALID_JSON_ARGUMENT");
|
|
187
|
+
}
|