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
|
@@ -3,22 +3,101 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
6
|
+
import { canonicalizeWorkflowName } from "../../core/asset/asset-spec.js";
|
|
6
7
|
import { loadConfig } from "../../core/config/config.js";
|
|
7
8
|
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
8
9
|
import { appendEvent } from "../../core/events.js";
|
|
10
|
+
import { warn } from "../../core/warn.js";
|
|
9
11
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
10
12
|
import { getCurrentWorkflowScopeKey } from "../authoring/scope-key.js";
|
|
13
|
+
import { frozenSummaryJudge } from "../exec/frozen-judge.js";
|
|
14
|
+
import { detectSecretShapedParams } from "../exec/param-secrets.js";
|
|
15
|
+
import { collectProgramWarnings } from "../ir/compile.js";
|
|
16
|
+
import { compileResolveFreezeWorkflow } from "../ir/freeze.js";
|
|
17
|
+
import { validateWorkflowParams } from "../ir/params.js";
|
|
18
|
+
import { canonicalPlanJson, computePlanHash } from "../ir/plan-hash.js";
|
|
19
|
+
import { decodeWorkflowPlanV3, WORKFLOW_IR_VERSION } from "../ir/schema.js";
|
|
11
20
|
import { validateStepSummary } from "../validate-summary.js";
|
|
12
21
|
import { resolveAgentIdentity } from "./agent-identity.js";
|
|
13
22
|
import { evaluateCheckin } from "./checkin.js";
|
|
23
|
+
import { assertWorkflowSpineMatchesPlan, classifyWorkflowRunPlan, frozenStepRows, requireAbandonableWorkflowPlan, requireExecutableWorkflowPlan, } from "./plan-classifier.js";
|
|
24
|
+
import { evaluateStaleUnits } from "./unit-checkin.js";
|
|
14
25
|
import { loadWorkflowAsset, resolveWorkflowEntryId } from "./workflow-asset-loader.js";
|
|
26
|
+
/** Clip bound for a unit's `result_json` on the `--units` diagnostic surface. */
|
|
27
|
+
const UNIT_DIAGNOSTIC_CLIP = 2000;
|
|
28
|
+
function toUnitDiagnostic(row, stale, plannedEngine) {
|
|
29
|
+
let diagnostic = null;
|
|
30
|
+
if (row.result_json !== null) {
|
|
31
|
+
// `result_json` is a JSON-encoded value: a bare JSON string for a free-text
|
|
32
|
+
// unit, an object/array for a schema unit. Render the decoded string as-is
|
|
33
|
+
// (no surrounding quotes) and other shapes as compact JSON, then clip so a
|
|
34
|
+
// large artifact can't flood the diagnostic surface.
|
|
35
|
+
let text = row.result_json;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(row.result_json);
|
|
38
|
+
text = typeof parsed === "string" ? parsed : JSON.stringify(parsed);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* leave the raw journaled text */
|
|
42
|
+
}
|
|
43
|
+
diagnostic = text.length > UNIT_DIAGNOSTIC_CLIP ? `${text.slice(0, UNIT_DIAGNOSTIC_CLIP)}…` : text;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
unitId: row.unit_id,
|
|
47
|
+
nodeId: row.node_id,
|
|
48
|
+
stepId: row.step_id,
|
|
49
|
+
phase: row.phase,
|
|
50
|
+
status: row.status,
|
|
51
|
+
attempts: row.attempts,
|
|
52
|
+
tokens: row.tokens,
|
|
53
|
+
failureReason: row.failure_reason,
|
|
54
|
+
sessionId: row.session_id,
|
|
55
|
+
diagnostic,
|
|
56
|
+
startedAt: row.started_at,
|
|
57
|
+
finishedAt: row.finished_at,
|
|
58
|
+
stale: stale !== undefined,
|
|
59
|
+
staleIdleMs: stale ? (Number.isFinite(stale.idleMs) ? stale.idleMs : null) : null,
|
|
60
|
+
claimHolder: row.claim_holder,
|
|
61
|
+
claimExpiresAt: row.claim_expires_at,
|
|
62
|
+
engine: row.engine ?? null,
|
|
63
|
+
runtimeKind: row.engine && (row.runner === "llm" || row.runner === "agent" || row.runner === "sdk") ? row.runner : null,
|
|
64
|
+
platform: plannedEngine?.kind === "agent" ? plannedEngine.platform : null,
|
|
65
|
+
...(!row.engine && row.runner ? { legacyRunnerSelector: row.runner } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
15
68
|
export async function startWorkflowRun(ref, params = {}, options) {
|
|
16
69
|
const asset = await loadWorkflowAsset(ref);
|
|
70
|
+
// Frozen plan (redesign addendum, R1): compile the plan ONCE at start and
|
|
71
|
+
// persist it on the run row in the same transaction as the insert. Every
|
|
72
|
+
// later invocation executes this snapshot — the asset file is never re-read
|
|
73
|
+
// for an in-flight run; re-planning is an explicit new run.
|
|
74
|
+
const plan = decodeWorkflowPlanV3(compileResolveFreezeWorkflow(asset, loadConfig()).plan);
|
|
75
|
+
// Non-fatal WARNINGS (redesign addendum): a YAML program's untyped-step and
|
|
76
|
+
// undeclared-param advisories surface as `warn()` lines at start (stderr,
|
|
77
|
+
// consistent with the repo's other author-facing warnings) without blocking
|
|
78
|
+
// the run. Markdown workflows carry no `program` and warn about nothing.
|
|
79
|
+
if (asset.program) {
|
|
80
|
+
for (const w of collectProgramWarnings(asset.program)) {
|
|
81
|
+
warn(`workflow start: ${asset.path}:${w.line} — ${w.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Reviewer #12: validate supplied `--params` against the frozen param
|
|
85
|
+
// schemas BEFORE creating the run, so a type-mismatched param (e.g. a string
|
|
86
|
+
// for a `{ type: array }` param) is rejected with actionable errors instead
|
|
87
|
+
// of flowing silently into a unit prompt. Programs without declared param
|
|
88
|
+
// schemas (and every Markdown workflow) validate trivially.
|
|
89
|
+
const paramErrors = validateWorkflowParams(plan, params);
|
|
90
|
+
if (paramErrors.length > 0) {
|
|
91
|
+
throw new UsageError(`Cannot start ${asset.ref}: the supplied --params do not satisfy the workflow's declared parameter schemas:\n` +
|
|
92
|
+
paramErrors.map((e) => ` - ${e}`).join("\n"), "INVALID_JSON_ARGUMENT");
|
|
93
|
+
}
|
|
94
|
+
const planJson = canonicalPlanJson(plan);
|
|
95
|
+
const planHash = computePlanHash(plan);
|
|
17
96
|
return withWorkflowRunsRepo(async (repo) => {
|
|
18
97
|
const now = new Date().toISOString();
|
|
19
98
|
const runId = randomUUID();
|
|
20
99
|
const scopeKey = getCurrentWorkflowScopeKey();
|
|
21
|
-
const currentStepId =
|
|
100
|
+
const currentStepId = plan.steps[0]?.stepId ?? null;
|
|
22
101
|
const workflowEntryId = resolveWorkflowEntryId(asset.sourcePath, asset.ref);
|
|
23
102
|
// Capture the agent harness + session driving this run. Explicit options
|
|
24
103
|
// win; otherwise fall back to best-effort environment detection. This is
|
|
@@ -57,16 +136,27 @@ export async function startWorkflowRun(ref, params = {}, options) {
|
|
|
57
136
|
agentSessionId,
|
|
58
137
|
checkinArmedAt: now,
|
|
59
138
|
});
|
|
60
|
-
repo.insertSteps(
|
|
139
|
+
repo.insertSteps(frozenStepRows(plan).map((step) => ({
|
|
61
140
|
runId,
|
|
62
|
-
stepId: step.
|
|
63
|
-
stepTitle: step.
|
|
141
|
+
stepId: step.stepId,
|
|
142
|
+
stepTitle: step.stepTitle,
|
|
64
143
|
instructions: step.instructions,
|
|
65
|
-
completionJson: step.
|
|
66
|
-
sequenceIndex: step.sequenceIndex
|
|
144
|
+
completionJson: step.completionJson,
|
|
145
|
+
sequenceIndex: step.sequenceIndex,
|
|
67
146
|
})));
|
|
147
|
+
// Same transaction as the insert: a run row never exists without its
|
|
148
|
+
// frozen plan (rows with NULL plan_json are pre-006 legacy runs).
|
|
149
|
+
repo.setRunPlan(runId, planJson, planHash, WORKFLOW_IR_VERSION);
|
|
68
150
|
});
|
|
69
151
|
const result = await getWorkflowStatus(runId);
|
|
152
|
+
// #13: params are declared non-secret (they are copied verbatim into every
|
|
153
|
+
// unit prompt and hashed into the unit identity, so they cannot be redacted
|
|
154
|
+
// without breaking the driver protocol). Surface a loud, best-effort warning
|
|
155
|
+
// when a param LOOKS like a credential so the author moves it to an env
|
|
156
|
+
// binding. Advisory only — never blocks the start.
|
|
157
|
+
const secretWarnings = detectSecretShapedParams(params);
|
|
158
|
+
if (secretWarnings.length > 0)
|
|
159
|
+
result.warnings = [...(result.warnings ?? []), ...secretWarnings];
|
|
70
160
|
// 07 P1-B: emit only the run id + status — NOT the raw workflowTitle (which
|
|
71
161
|
// comes verbatim from the workflow asset's frontmatter and is therefore
|
|
72
162
|
// attacker-influenceable). Keeping raw titles out of the events stream
|
|
@@ -80,11 +170,25 @@ export async function startWorkflowRun(ref, params = {}, options) {
|
|
|
80
170
|
return result;
|
|
81
171
|
});
|
|
82
172
|
}
|
|
83
|
-
export async function getWorkflowStatus(runId) {
|
|
173
|
+
export async function getWorkflowStatus(runId, opts) {
|
|
84
174
|
return withWorkflowRunsRepo((repo) => {
|
|
85
175
|
const run = readWorkflowRun(repo, runId);
|
|
86
176
|
const steps = readWorkflowRunSteps(repo, run.id);
|
|
87
|
-
|
|
177
|
+
const detail = buildWorkflowRunDetail(run, steps);
|
|
178
|
+
if (opts?.includeUnits) {
|
|
179
|
+
// The honest diagnostic surface (#22): read the unit journal straight and
|
|
180
|
+
// project each row, INCLUDING failures whose diagnostic text the
|
|
181
|
+
// deterministic evidence graph drops. Read-only; never mutates the run.
|
|
182
|
+
const rows = repo.getUnitsForRun(run.id);
|
|
183
|
+
// Codex round-3 finding B: run the SAME pure stale-claim evaluator `brief`
|
|
184
|
+
// uses (`now` injected for deterministic tests) so a dead driver's claimed
|
|
185
|
+
// `running` unit surfaces as stale here too, not just as raw `running`.
|
|
186
|
+
const staleById = new Map(evaluateStaleUnits(rows, opts.now ?? Date.now()).map((u) => [u.unitId, u]));
|
|
187
|
+
const classified = classifyWorkflowRunPlan(run);
|
|
188
|
+
const engines = classified.support === "supported" ? classified.plan.execution?.engines : undefined;
|
|
189
|
+
detail.units = rows.map((row) => toUnitDiagnostic(row, staleById.get(row.unit_id), row.engine ? engines?.[row.engine] : undefined));
|
|
190
|
+
}
|
|
191
|
+
return detail;
|
|
88
192
|
});
|
|
89
193
|
}
|
|
90
194
|
export async function hasWorkflowRun(runId) {
|
|
@@ -99,7 +203,7 @@ export async function listWorkflowRuns(input) {
|
|
|
99
203
|
if (parsed.type !== "workflow") {
|
|
100
204
|
throw new UsageError(`Expected a workflow ref (workflow:<name>), got "${input.workflowRef}".`);
|
|
101
205
|
}
|
|
102
|
-
workflowRef = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
206
|
+
workflowRef = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${canonicalizeWorkflowName(parsed.name)}`;
|
|
103
207
|
}
|
|
104
208
|
const rows = repo.listRuns({
|
|
105
209
|
scopeKey,
|
|
@@ -113,39 +217,72 @@ export async function getNextWorkflowStep(specifier, params) {
|
|
|
113
217
|
return withWorkflowRunsRepo(async (repo) => {
|
|
114
218
|
const { run, autoStarted } = await resolveRunSpecifier(repo, specifier, params);
|
|
115
219
|
const steps = readWorkflowRunSteps(repo, run.id);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// the run looks stalled. Pure timestamp evaluation — no background thread.
|
|
120
|
-
const checkin = evaluateCheckin({
|
|
121
|
-
status: run.status,
|
|
122
|
-
updatedAt: run.updated_at,
|
|
123
|
-
checkinArmedAt: run.checkin_armed_at,
|
|
124
|
-
agentHarness: run.agent_harness,
|
|
125
|
-
agentSessionId: run.agent_session_id,
|
|
126
|
-
}) ?? undefined;
|
|
127
|
-
return {
|
|
128
|
-
run: toWorkflowRunSummary(run),
|
|
129
|
-
workflow: {
|
|
130
|
-
ref: run.workflow_ref,
|
|
131
|
-
title: run.workflow_title,
|
|
132
|
-
steps: steps.map(toWorkflowRunStepState),
|
|
133
|
-
},
|
|
134
|
-
step: currentStep ? toWorkflowRunStepState(currentStep) : null,
|
|
135
|
-
...(done ? { done } : {}),
|
|
136
|
-
...(autoStarted ? { autoStarted } : {}),
|
|
137
|
-
...(checkin ? { checkin } : {}),
|
|
138
|
-
};
|
|
220
|
+
const plan = requireExecutableWorkflowPlan(run);
|
|
221
|
+
assertWorkflowSpineMatchesPlan(plan, run, steps);
|
|
222
|
+
return { ...projectNextResult(run, steps), ...(autoStarted ? { autoStarted: true } : {}) };
|
|
139
223
|
});
|
|
140
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Project a run row + its step rows into a {@link WorkflowNextResult}. The pure
|
|
227
|
+
* read-shaping half of {@link getNextWorkflowStep}, extracted so the driver
|
|
228
|
+
* snapshot below reproduces the exact same projection without re-running the
|
|
229
|
+
* auto-start-capable {@link resolveRunSpecifier}.
|
|
230
|
+
*/
|
|
231
|
+
function projectNextResult(run, steps) {
|
|
232
|
+
const currentStep = resolveCurrentStep(run, steps);
|
|
233
|
+
const done = run.status === "completed" ? true : undefined;
|
|
234
|
+
// #506: surface a check-in directive through the normal command output when
|
|
235
|
+
// the run looks stalled. Pure timestamp evaluation — no background thread.
|
|
236
|
+
const checkin = evaluateCheckin({
|
|
237
|
+
status: run.status,
|
|
238
|
+
updatedAt: run.updated_at,
|
|
239
|
+
checkinArmedAt: run.checkin_armed_at,
|
|
240
|
+
agentHarness: run.agent_harness,
|
|
241
|
+
agentSessionId: run.agent_session_id,
|
|
242
|
+
}) ?? undefined;
|
|
243
|
+
return {
|
|
244
|
+
run: toWorkflowRunSummary(run),
|
|
245
|
+
workflow: {
|
|
246
|
+
ref: run.workflow_ref,
|
|
247
|
+
title: run.workflow_title,
|
|
248
|
+
steps: steps.map(toWorkflowRunStepState),
|
|
249
|
+
},
|
|
250
|
+
step: currentStep ? toWorkflowRunStepState(currentStep) : null,
|
|
251
|
+
...(done ? { done } : {}),
|
|
252
|
+
...(checkin ? { checkin } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A consistent point-in-time snapshot of a run for the harness-neutral driver
|
|
257
|
+
* protocol (PR #714 review round 2, #14). `brief`/`report` previously read the
|
|
258
|
+
* spine (`getNextWorkflowStep`) and then, in a SEPARATE connection, the run row
|
|
259
|
+
* + unit journal — so a concurrent `report`/`run`/manual completion could change
|
|
260
|
+
* the active step BETWEEN the two reads, leaving the described work-list
|
|
261
|
+
* inconsistent with the run row it was stamped against. This reads the run row,
|
|
262
|
+
* its steps, AND its unit rows inside ONE transaction (one connection, one
|
|
263
|
+
* snapshot) so all three agree. It never auto-starts — `runId` must already
|
|
264
|
+
* resolve to a concrete run — because the driver protocol never mutates on read.
|
|
265
|
+
*/
|
|
266
|
+
export async function snapshotRunForDriver(runId) {
|
|
267
|
+
return withWorkflowRunsRepo((repo) => repo.transaction(() => {
|
|
268
|
+
const run = readWorkflowRun(repo, runId);
|
|
269
|
+
const steps = readWorkflowRunSteps(repo, run.id);
|
|
270
|
+
const plan = requireExecutableWorkflowPlan(run);
|
|
271
|
+
assertWorkflowSpineMatchesPlan(plan, run, steps);
|
|
272
|
+
const units = repo.getUnitsForRun(run.id);
|
|
273
|
+
return { next: projectNextResult(run, steps), run, units };
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
141
276
|
export async function resumeWorkflowRun(runId) {
|
|
142
277
|
return withWorkflowRunsRepo((repo) => {
|
|
143
278
|
const run = readWorkflowRun(repo, runId);
|
|
279
|
+
const plan = requireExecutableWorkflowPlan(run);
|
|
280
|
+
const steps = readWorkflowRunSteps(repo, run.id);
|
|
281
|
+
assertWorkflowSpineMatchesPlan(plan, run, steps);
|
|
144
282
|
if (run.status === "completed") {
|
|
145
283
|
throw new UsageError(`Workflow run ${run.id} is already completed and cannot be resumed.`);
|
|
146
284
|
}
|
|
147
285
|
if (run.status === "active") {
|
|
148
|
-
const steps = readWorkflowRunSteps(repo, run.id);
|
|
149
286
|
return buildWorkflowRunDetail(run, steps);
|
|
150
287
|
}
|
|
151
288
|
// blocked or failed → flip back to active and re-open the current step so
|
|
@@ -158,8 +295,8 @@ export async function resumeWorkflowRun(runId) {
|
|
|
158
295
|
repo.markRunActive(run.id, now);
|
|
159
296
|
});
|
|
160
297
|
const updated = { ...run, status: "active", updated_at: now };
|
|
161
|
-
const
|
|
162
|
-
return buildWorkflowRunDetail(updated,
|
|
298
|
+
const refreshedSteps = readWorkflowRunSteps(repo, run.id);
|
|
299
|
+
return buildWorkflowRunDetail(updated, refreshedSteps);
|
|
163
300
|
});
|
|
164
301
|
}
|
|
165
302
|
/**
|
|
@@ -171,6 +308,11 @@ export async function resumeWorkflowRun(runId) {
|
|
|
171
308
|
export async function abandonWorkflowRun(runId) {
|
|
172
309
|
return withWorkflowRunsRepo((repo) => {
|
|
173
310
|
const run = readWorkflowRun(repo, runId);
|
|
311
|
+
requireAbandonableWorkflowPlan(run);
|
|
312
|
+
const classified = classifyWorkflowRunPlan(run);
|
|
313
|
+
const existingSteps = readWorkflowRunSteps(repo, run.id);
|
|
314
|
+
if (classified.support === "supported")
|
|
315
|
+
assertWorkflowSpineMatchesPlan(classified.plan, run, existingSteps);
|
|
174
316
|
if (run.status === "completed" || run.status === "failed") {
|
|
175
317
|
throw new UsageError(`Workflow run ${run.id} is already ${run.status}.`);
|
|
176
318
|
}
|
|
@@ -203,9 +345,13 @@ export async function completeWorkflowStep(input) {
|
|
|
203
345
|
// the write transaction — a slow/hung LLM must never hold a db write lock.
|
|
204
346
|
const preflight = await withWorkflowRunsRepo((repo) => {
|
|
205
347
|
const run = readWorkflowRun(repo, input.runId);
|
|
348
|
+
const plan = requireExecutableWorkflowPlan(run);
|
|
349
|
+
const steps = readWorkflowRunSteps(repo, run.id);
|
|
350
|
+
assertWorkflowSpineMatchesPlan(plan, run, steps);
|
|
206
351
|
if (run.status !== "active") {
|
|
207
352
|
throw new UsageError(`Workflow run ${run.id} is ${run.status} and cannot be updated.`);
|
|
208
353
|
}
|
|
354
|
+
assertLeaseAllowsSpineAdvance(run, input.leaseHolder);
|
|
209
355
|
const existing = repo.getStep(run.id, input.stepId);
|
|
210
356
|
if (!existing) {
|
|
211
357
|
throw new NotFoundError(`Step "${input.stepId}" was not found in workflow run ${run.id}.`);
|
|
@@ -216,7 +362,10 @@ export async function completeWorkflowStep(input) {
|
|
|
216
362
|
if (run.current_step_id !== existing.step_id) {
|
|
217
363
|
throw new UsageError(`Step "${input.stepId}" is not the current step for workflow run ${run.id}. Complete "${run.current_step_id}" first.`);
|
|
218
364
|
}
|
|
219
|
-
|
|
365
|
+
const stepPlan = plan.steps.find((step) => step.stepId === input.stepId);
|
|
366
|
+
if (!stepPlan)
|
|
367
|
+
throw new NotFoundError(`Step "${input.stepId}" was not found in workflow run ${run.id}.`);
|
|
368
|
+
return { existing, plan, stepPlan };
|
|
220
369
|
});
|
|
221
370
|
const summary = input.summary?.trim();
|
|
222
371
|
// #506: completing a step requires a summary of the work done.
|
|
@@ -227,9 +376,11 @@ export async function completeWorkflowStep(input) {
|
|
|
227
376
|
// completionCriteria via the configured LLM. Fail-open when no criteria or no
|
|
228
377
|
// judge. Only a well-formed `complete: false` blocks completion.
|
|
229
378
|
if (input.status === "completed" && summary) {
|
|
230
|
-
const criteria =
|
|
231
|
-
const judge = input.summaryJudge === undefined
|
|
232
|
-
|
|
379
|
+
const criteria = preflight.stepPlan.gate.criteria;
|
|
380
|
+
const judge = input.summaryJudge === undefined
|
|
381
|
+
? frozenSummaryJudge(preflight.plan, preflight.stepPlan.gate.judge)
|
|
382
|
+
: input.summaryJudge;
|
|
383
|
+
const verdict = await validateStepSummary({ stepTitle: preflight.stepPlan.title, completionCriteria: criteria, summary }, judge ?? undefined, { required: input.requireGate === true });
|
|
233
384
|
if (!verdict.complete) {
|
|
234
385
|
// Re-arm the check-in so a subsequent stall is still nudged, but leave the
|
|
235
386
|
// step pending and return corrective feedback instead of completing.
|
|
@@ -242,6 +393,9 @@ export async function completeWorkflowStep(input) {
|
|
|
242
393
|
stepId: input.stepId,
|
|
243
394
|
missing: verdict.missing,
|
|
244
395
|
feedback: verdict.feedback ?? "The summary does not satisfy the step's completion criteria.",
|
|
396
|
+
// A REQUIRED gate that could not be judged (finding A): the caller BLOCKS
|
|
397
|
+
// rather than treating this as a normal, retryable gate rejection.
|
|
398
|
+
...(verdict.errored ? { errored: true } : {}),
|
|
245
399
|
};
|
|
246
400
|
}
|
|
247
401
|
}
|
|
@@ -250,9 +404,16 @@ export async function completeWorkflowStep(input) {
|
|
|
250
404
|
let refreshedSteps = [];
|
|
251
405
|
repo.transaction(() => {
|
|
252
406
|
const run = readWorkflowRun(repo, input.runId);
|
|
407
|
+
const plan = requireExecutableWorkflowPlan(run);
|
|
408
|
+
const spine = readWorkflowRunSteps(repo, run.id);
|
|
409
|
+
assertWorkflowSpineMatchesPlan(plan, run, spine);
|
|
253
410
|
if (run.status !== "active") {
|
|
254
411
|
throw new UsageError(`Workflow run ${run.id} is ${run.status} and cannot be updated.`);
|
|
255
412
|
}
|
|
413
|
+
// Re-checked inside the write transaction (like every other preflight
|
|
414
|
+
// condition): an engine may have claimed the run while the summary gate
|
|
415
|
+
// above was awaiting its LLM judge.
|
|
416
|
+
assertLeaseAllowsSpineAdvance(run, input.leaseHolder);
|
|
256
417
|
const existing = repo.getStep(run.id, input.stepId);
|
|
257
418
|
if (!existing) {
|
|
258
419
|
throw new NotFoundError(`Step "${input.stepId}" was not found in workflow run ${run.id}.`);
|
|
@@ -295,10 +456,16 @@ export async function completeWorkflowStep(input) {
|
|
|
295
456
|
};
|
|
296
457
|
});
|
|
297
458
|
const detail = buildWorkflowRunDetail(updatedRun, refreshedSteps);
|
|
459
|
+
// #11: emit `workflow_step_completed` ONLY for a genuine `completed`
|
|
460
|
+
// transition; every other non-pending status (failed/skipped/blocked)
|
|
461
|
+
// carries the honest `workflow_step_updated` name. The status is ALWAYS
|
|
462
|
+
// in metadata so consumers never infer it from the event name. Raw `notes`
|
|
463
|
+
// are workflow/model-authored content — an event-stream prompt-injection
|
|
464
|
+
// surface — and never enter the events log; they live on the step row only.
|
|
298
465
|
appendEvent({
|
|
299
|
-
eventType: "workflow_step_completed",
|
|
466
|
+
eventType: input.status === "completed" ? "workflow_step_completed" : "workflow_step_updated",
|
|
300
467
|
ref: detail.run.workflowRef,
|
|
301
|
-
metadata: { runId: input.runId, stepId: input.stepId,
|
|
468
|
+
metadata: { runId: input.runId, stepId: input.stepId, status: input.status },
|
|
302
469
|
});
|
|
303
470
|
if (detail.run.status === "completed") {
|
|
304
471
|
appendEvent({ eventType: "workflow_finished", ref: detail.run.workflowRef, metadata: { runId: input.runId } });
|
|
@@ -321,7 +488,7 @@ async function resolveRunSpecifier(repo, specifier, params) {
|
|
|
321
488
|
if (parsed.type !== "workflow") {
|
|
322
489
|
throw new UsageError(`Expected a workflow ref or workflow run id, got "${specifier}".`);
|
|
323
490
|
}
|
|
324
|
-
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
491
|
+
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${canonicalizeWorkflowName(parsed.name)}`;
|
|
325
492
|
const scopeKey = getCurrentWorkflowScopeKey();
|
|
326
493
|
const active = repo.getActiveRunRowForScope(ref, scopeKey);
|
|
327
494
|
if (active) {
|
|
@@ -344,6 +511,16 @@ function readWorkflowRunSteps(repo, runId) {
|
|
|
344
511
|
return repo.getStepsForRun(runId);
|
|
345
512
|
}
|
|
346
513
|
function buildWorkflowRunDetail(run, steps) {
|
|
514
|
+
// Review M1: `workflow status` (and every other detail-shaped response) now
|
|
515
|
+
// evaluates the check-in, not just `workflow next`. Pure timestamp check —
|
|
516
|
+
// no background thread (see checkin.ts).
|
|
517
|
+
const checkin = evaluateCheckin({
|
|
518
|
+
status: run.status,
|
|
519
|
+
updatedAt: run.updated_at,
|
|
520
|
+
checkinArmedAt: run.checkin_armed_at,
|
|
521
|
+
agentHarness: run.agent_harness,
|
|
522
|
+
agentSessionId: run.agent_session_id,
|
|
523
|
+
});
|
|
347
524
|
return {
|
|
348
525
|
run: toWorkflowRunSummary(run),
|
|
349
526
|
workflow: {
|
|
@@ -351,9 +528,11 @@ function buildWorkflowRunDetail(run, steps) {
|
|
|
351
528
|
title: run.workflow_title,
|
|
352
529
|
steps: steps.map(toWorkflowRunStepState),
|
|
353
530
|
},
|
|
531
|
+
...(checkin ? { checkin } : {}),
|
|
354
532
|
};
|
|
355
533
|
}
|
|
356
534
|
function toWorkflowRunSummary(run) {
|
|
535
|
+
const plan = classifyWorkflowRunPlan(run);
|
|
357
536
|
return {
|
|
358
537
|
id: run.id,
|
|
359
538
|
workflowRef: run.workflow_ref,
|
|
@@ -368,8 +547,33 @@ function toWorkflowRunSummary(run) {
|
|
|
368
547
|
params: parseJsonObject(run.params_json),
|
|
369
548
|
agentHarness: run.agent_harness ?? null,
|
|
370
549
|
agentSessionId: run.agent_session_id ?? null,
|
|
550
|
+
planIrVersion: plan.irVersion,
|
|
551
|
+
executionSupport: plan.support,
|
|
552
|
+
// Surface the engine lease (holder id + expiry — never workflow-authored
|
|
553
|
+
// content) so `workflow next`/`status` show who is driving the run.
|
|
554
|
+
...(run.engine_lease_holder && run.engine_lease_until
|
|
555
|
+
? { engineLease: { holder: run.engine_lease_holder, until: run.engine_lease_until } }
|
|
556
|
+
: {}),
|
|
371
557
|
};
|
|
372
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Single-driver enforcement (R2 run lease): while a LIVE (unexpired) engine
|
|
561
|
+
* lease is held, only the holding engine may advance the gate spine. Manual
|
|
562
|
+
* `akm workflow complete` (no `leaseHolder`) — or a stale engine invocation
|
|
563
|
+
* whose lease was claimed by another — is refused with the holder + expiry.
|
|
564
|
+
* An EXPIRED lease never blocks: the engine that held it is presumed dead.
|
|
565
|
+
*/
|
|
566
|
+
function assertLeaseAllowsSpineAdvance(run, leaseHolder) {
|
|
567
|
+
if (!run.engine_lease_holder || !run.engine_lease_until)
|
|
568
|
+
return;
|
|
569
|
+
if (leaseHolder === run.engine_lease_holder)
|
|
570
|
+
return;
|
|
571
|
+
if (run.engine_lease_until < new Date().toISOString())
|
|
572
|
+
return; // expired ⇒ claimable, not live
|
|
573
|
+
throw new UsageError(`Workflow run ${run.id} is being driven by engine ${run.engine_lease_holder} ` +
|
|
574
|
+
`(run lease expires ${run.engine_lease_until}). The engine owns the step spine while it runs — ` +
|
|
575
|
+
`wait for it to finish or for the lease to expire before advancing steps manually.`);
|
|
576
|
+
}
|
|
373
577
|
function toWorkflowRunStepState(step) {
|
|
374
578
|
return {
|
|
375
579
|
id: step.step_id,
|
|
@@ -410,33 +614,6 @@ function deriveRunState(steps) {
|
|
|
410
614
|
.at(-1);
|
|
411
615
|
return { status: "completed", currentStepId: null, completedAt: completedAt ?? null };
|
|
412
616
|
}
|
|
413
|
-
/**
|
|
414
|
-
/**
|
|
415
|
-
* Build the default summary-validation judge from the configured LLM, or return
|
|
416
|
-
* `null` when no LLM is configured (gate is then skipped — fail-open). Lazily
|
|
417
|
-
* imports the client/config so the workflow engine has no hard LLM dependency.
|
|
418
|
-
*/
|
|
419
|
-
function buildDefaultSummaryJudge() {
|
|
420
|
-
let llm;
|
|
421
|
-
try {
|
|
422
|
-
const config = loadConfig();
|
|
423
|
-
const { getDefaultLlmConfig } = require("../../core/config/config");
|
|
424
|
-
llm = getDefaultLlmConfig(config);
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
if (!llm)
|
|
430
|
-
return null;
|
|
431
|
-
const resolved = llm;
|
|
432
|
-
return async ({ system, user }) => {
|
|
433
|
-
const { chatCompletion } = require("../../llm/client");
|
|
434
|
-
return chatCompletion(resolved, [
|
|
435
|
-
{ role: "system", content: system },
|
|
436
|
-
{ role: "user", content: user },
|
|
437
|
-
]);
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
617
|
function parseJsonObject(value) {
|
|
441
618
|
if (!value)
|
|
442
619
|
return undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { GATE_EVALUATION_PHASE } from "../exec/step-work.js";
|
|
5
|
+
/**
|
|
6
|
+
* Default staleness window. A unit claimed `running` whose last heartbeat (or,
|
|
7
|
+
* absent any heartbeat, its first claim) is older than this is surfaced as
|
|
8
|
+
* stale. Matches the run-level {@link ../runtime/checkin.CHECKIN_STALL_MS}.
|
|
9
|
+
*/
|
|
10
|
+
export const UNIT_STALE_MS = 90_000;
|
|
11
|
+
function parseIso(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return null;
|
|
14
|
+
const ms = Date.parse(value);
|
|
15
|
+
return Number.isNaN(ms) ? null : ms;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure stale-unit evaluator. Returns every `running` DISPATCH unit whose last
|
|
19
|
+
* heartbeat (`last_checkin_at`, else the first-claim `started_at`) is older than
|
|
20
|
+
* `staleMs`. Gate-evaluation rows (`phase = "gate"`) are excluded — they are
|
|
21
|
+
* synchronous engine judge calls, never driver-claimed work. A running row with
|
|
22
|
+
* no usable timestamp at all is treated as stale (a claim we can no longer age).
|
|
23
|
+
* Deterministic in `now` — free of timer flakiness.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateStaleUnits(rows, now = Date.now(), staleMs = UNIT_STALE_MS) {
|
|
26
|
+
const stale = [];
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
if (row.status !== "running")
|
|
29
|
+
continue;
|
|
30
|
+
if (row.phase === GATE_EVALUATION_PHASE)
|
|
31
|
+
continue;
|
|
32
|
+
const lastSeenAt = row.last_checkin_at ?? row.started_at;
|
|
33
|
+
const lastSeen = parseIso(lastSeenAt);
|
|
34
|
+
const idleMs = lastSeen === null ? Number.POSITIVE_INFINITY : now - lastSeen;
|
|
35
|
+
if (idleMs < staleMs)
|
|
36
|
+
continue;
|
|
37
|
+
stale.push({
|
|
38
|
+
unitId: row.unit_id,
|
|
39
|
+
stepId: row.step_id,
|
|
40
|
+
idleMs,
|
|
41
|
+
lastSeenAt: lastSeenAt ?? null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return stale;
|
|
45
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
6
|
+
import { canonicalizeWorkflowName } from "../../core/asset/asset-spec.js";
|
|
6
7
|
import { loadConfig } from "../../core/config/config.js";
|
|
7
8
|
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
8
9
|
import { getDbPath } from "../../core/paths.js";
|
|
@@ -12,6 +13,8 @@ import { resolveAssetPath } from "../../sources/resolve.js";
|
|
|
12
13
|
import { withIndexDb } from "../../storage/repositories/index-db.js";
|
|
13
14
|
import { formatWorkflowErrors } from "../authoring/authoring.js";
|
|
14
15
|
import { parseWorkflow } from "../parser.js";
|
|
16
|
+
import { parseWorkflowProgram } from "../program/parser.js";
|
|
17
|
+
import { isWorkflowProgramPath, projectProgramParameters, projectProgramStepDefinitions } from "../program/project.js";
|
|
15
18
|
/**
|
|
16
19
|
* Resolve a `workflow:<name>` ref to a fully-projected {@link WorkflowAsset}.
|
|
17
20
|
*
|
|
@@ -44,7 +47,19 @@ export async function loadWorkflowAsset(ref) {
|
|
|
44
47
|
throw new NotFoundError(`Workflow not found for ref: workflow:${parsed.name}`);
|
|
45
48
|
}
|
|
46
49
|
const resolvedSourcePath = sourcePath ?? config.stashDir ?? assetPath;
|
|
47
|
-
|
|
50
|
+
// Canonicalize the stored ref: `workflow:foo.yaml` and `workflow:foo`
|
|
51
|
+
// resolve to the same file, so they MUST share one run identity. The raw
|
|
52
|
+
// `parsed.name` (with any extension) is what drives file resolution above;
|
|
53
|
+
// only the persisted/queried ref is collapsed (matches the index entry key,
|
|
54
|
+
// which is keyed by the extension-stripped canonical name).
|
|
55
|
+
const canonicalName = canonicalizeWorkflowName(parsed.name);
|
|
56
|
+
const fullRef = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${canonicalName}`;
|
|
57
|
+
// Format detection by extension: `.yaml`/`.yml` is a YAML workflow program
|
|
58
|
+
// (redesign addendum, R1); everything else is the markdown document format.
|
|
59
|
+
if (isWorkflowProgramPath(assetPath)) {
|
|
60
|
+
const program = loadWorkflowProgramFromDisk(assetPath);
|
|
61
|
+
return projectProgramAsset(program, fullRef, assetPath, resolvedSourcePath);
|
|
62
|
+
}
|
|
48
63
|
const cached = readWorkflowDocumentFromIndex(resolvedSourcePath, fullRef);
|
|
49
64
|
const document = cached ?? loadWorkflowDocumentFromDisk(assetPath);
|
|
50
65
|
return projectAsset(document, fullRef, assetPath, resolvedSourcePath);
|
|
@@ -69,6 +84,14 @@ export function resolveWorkflowEntryId(sourcePath, ref) {
|
|
|
69
84
|
return row?.id ?? null;
|
|
70
85
|
});
|
|
71
86
|
}
|
|
87
|
+
function loadWorkflowProgramFromDisk(assetPath) {
|
|
88
|
+
const content = fs.readFileSync(assetPath, "utf8");
|
|
89
|
+
const result = parseWorkflowProgram(content, { path: assetPath });
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
throw new UsageError(formatWorkflowErrors(assetPath, result.errors));
|
|
92
|
+
}
|
|
93
|
+
return result.program;
|
|
94
|
+
}
|
|
72
95
|
function loadWorkflowDocumentFromDisk(assetPath) {
|
|
73
96
|
const content = fs.readFileSync(assetPath, "utf8");
|
|
74
97
|
const result = parseWorkflow(content, { path: assetPath });
|
|
@@ -121,5 +144,23 @@ function projectAsset(doc, ref, assetPath, sourcePath) {
|
|
|
121
144
|
...(s.completionCriteria ? { completionCriteria: s.completionCriteria.map((c) => c.text) } : {}),
|
|
122
145
|
sequenceIndex: s.sequenceIndex,
|
|
123
146
|
})),
|
|
147
|
+
document: doc,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Project a parsed YAML program into the run-repository asset shape. Step
|
|
152
|
+
* instructions carry the RAW `${{ … }}` templates — resolution happens in
|
|
153
|
+
* the engine against the frozen plan, never here.
|
|
154
|
+
*/
|
|
155
|
+
function projectProgramAsset(program, ref, assetPath, sourcePath) {
|
|
156
|
+
const parameters = projectProgramParameters(program);
|
|
157
|
+
return {
|
|
158
|
+
ref,
|
|
159
|
+
path: assetPath,
|
|
160
|
+
sourcePath,
|
|
161
|
+
title: program.name,
|
|
162
|
+
...(parameters ? { parameters } : {}),
|
|
163
|
+
steps: projectProgramStepDefinitions(program),
|
|
164
|
+
program,
|
|
124
165
|
};
|
|
125
166
|
}
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import validateSummaryJudgePrompt from "../assets/prompts/validate-summary-judge.md" with { type: "text" };
|
|
20
20
|
import { parseJsonResponse } from "../core/parse.js";
|
|
21
|
+
/** Feedback surfaced when a REQUIRED gate's judge could not be evaluated. */
|
|
22
|
+
const REQUIRED_GATE_JUDGE_UNAVAILABLE_FEEDBACK = "The required completion gate could not be judged — the LLM threw, was unreachable, or returned an " +
|
|
23
|
+
"unparseable verdict. A required gate must be judged; refusing to pass it. Restore the judge and re-evaluate.";
|
|
21
24
|
const JUDGE_SYSTEM = validateSummaryJudgePrompt;
|
|
22
25
|
function buildUserPrompt(input) {
|
|
23
26
|
const criteria = input.completionCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
@@ -36,14 +39,22 @@ function buildUserPrompt(input) {
|
|
|
36
39
|
/**
|
|
37
40
|
* Run the summary-validation gate.
|
|
38
41
|
*
|
|
39
|
-
* Fail-open contract:
|
|
42
|
+
* Fail-open contract (NON-required gates):
|
|
40
43
|
* - no criteria → `{ complete: true, skipped: true }`
|
|
41
44
|
* - no judge → `{ complete: true, skipped: true }`
|
|
42
45
|
* - judge throws / returns unparseable → `{ complete: true, skipped: true }`
|
|
43
46
|
*
|
|
47
|
+
* REQUIRED gates (`opts.required`) do NOT fail open on an un-evaluable judge
|
|
48
|
+
* (Codex round-3 finding A): a judge that throws / is unreachable, or returns an
|
|
49
|
+
* unparseable / malformed verdict, yields `{ complete: false, errored: true }`
|
|
50
|
+
* so the caller BLOCKS the step rather than silently passing an unjudged gate.
|
|
51
|
+
* (A required gate with NO judge at all is blocked upstream in
|
|
52
|
+
* `finalizeExecutedStep` before this gate runs, so that branch stays fail-open.)
|
|
53
|
+
*
|
|
44
54
|
* Only a well-formed `complete: false` verdict blocks completion.
|
|
45
55
|
*/
|
|
46
|
-
export async function validateStepSummary(input, judge) {
|
|
56
|
+
export async function validateStepSummary(input, judge, opts) {
|
|
57
|
+
const required = opts?.required === true;
|
|
47
58
|
const criteria = input.completionCriteria.filter((c) => c.trim().length > 0);
|
|
48
59
|
if (criteria.length === 0) {
|
|
49
60
|
return { complete: true, missing: [], skipped: true };
|
|
@@ -56,11 +67,21 @@ export async function validateStepSummary(input, judge) {
|
|
|
56
67
|
raw = await judge({ system: JUDGE_SYSTEM, user: buildUserPrompt({ ...input, completionCriteria: criteria }) });
|
|
57
68
|
}
|
|
58
69
|
catch {
|
|
59
|
-
// LLM unreachable / errored
|
|
70
|
+
// LLM unreachable / errored. A REQUIRED gate must be judged — block rather
|
|
71
|
+
// than pass an unjudged gate; a non-required gate fails open so offline use
|
|
72
|
+
// keeps working.
|
|
73
|
+
if (required) {
|
|
74
|
+
return { complete: false, missing: [], errored: true, feedback: REQUIRED_GATE_JUDGE_UNAVAILABLE_FEEDBACK };
|
|
75
|
+
}
|
|
60
76
|
return { complete: true, missing: [], skipped: true };
|
|
61
77
|
}
|
|
62
78
|
const parsed = parseJsonResponse(raw);
|
|
63
79
|
if (!parsed || typeof parsed.complete !== "boolean") {
|
|
80
|
+
// An unparseable / malformed verdict is a judge that did not actually judge:
|
|
81
|
+
// a required gate blocks; a non-required gate fails open.
|
|
82
|
+
if (required) {
|
|
83
|
+
return { complete: false, missing: [], errored: true, feedback: REQUIRED_GATE_JUDGE_UNAVAILABLE_FEEDBACK };
|
|
84
|
+
}
|
|
64
85
|
return { complete: true, missing: [], skipped: true };
|
|
65
86
|
}
|
|
66
87
|
if (parsed.complete) {
|