keystone-cli 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -32
- package/package.json +8 -4
- package/src/cli.ts +350 -416
- package/src/commands/doc.ts +31 -0
- package/src/commands/event.ts +29 -0
- package/src/commands/graph.ts +37 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/init.ts +185 -0
- package/src/commands/run.ts +124 -0
- package/src/commands/schema.ts +40 -0
- package/src/commands/utils.ts +78 -0
- package/src/commands/validate.ts +111 -0
- package/src/db/workflow-db.test.ts +314 -0
- package/src/db/workflow-db.ts +810 -210
- package/src/expression/evaluator-audit.test.ts +4 -2
- package/src/expression/evaluator.test.ts +14 -1
- package/src/expression/evaluator.ts +166 -19
- package/src/parser/config-schema.ts +18 -0
- package/src/parser/schema.ts +153 -22
- package/src/parser/test-schema.ts +6 -6
- package/src/parser/workflow-parser.test.ts +24 -0
- package/src/parser/workflow-parser.ts +65 -3
- package/src/runner/auto-heal.test.ts +5 -6
- package/src/runner/blueprint-executor.test.ts +2 -2
- package/src/runner/debug-repl.test.ts +5 -8
- package/src/runner/debug-repl.ts +59 -16
- package/src/runner/durable-timers.test.ts +11 -2
- package/src/runner/engine-executor.test.ts +1 -1
- package/src/runner/events.ts +57 -0
- package/src/runner/executors/artifact-executor.ts +166 -0
- package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
- package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
- package/src/runner/executors/file-executor.test.ts +48 -0
- package/src/runner/executors/file-executor.ts +324 -0
- package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
- package/src/runner/executors/human-executor.ts +144 -0
- package/src/runner/executors/join-executor.ts +75 -0
- package/src/runner/executors/llm-executor.ts +1266 -0
- package/src/runner/executors/memory-executor.ts +71 -0
- package/src/runner/executors/plan-executor.ts +104 -0
- package/src/runner/executors/request-executor.ts +265 -0
- package/src/runner/executors/script-executor.ts +43 -0
- package/src/runner/executors/shell-executor.ts +403 -0
- package/src/runner/executors/subworkflow-executor.ts +114 -0
- package/src/runner/executors/types.ts +69 -0
- package/src/runner/executors/wait-executor.ts +59 -0
- package/src/runner/join-scheduling.test.ts +197 -0
- package/src/runner/llm-adapter-runtime.test.ts +209 -0
- package/src/runner/llm-adapter.test.ts +419 -24
- package/src/runner/llm-adapter.ts +130 -26
- package/src/runner/llm-clarification.test.ts +2 -1
- package/src/runner/llm-executor.test.ts +532 -17
- package/src/runner/mcp-client-audit.test.ts +1 -2
- package/src/runner/mcp-client.ts +136 -46
- package/src/runner/mcp-manager.test.ts +4 -0
- package/src/runner/mcp-server.test.ts +58 -0
- package/src/runner/mcp-server.ts +26 -0
- package/src/runner/memoization.test.ts +190 -0
- package/src/runner/optimization-runner.ts +4 -9
- package/src/runner/quality-gate.test.ts +69 -0
- package/src/runner/reflexion.test.ts +6 -17
- package/src/runner/resource-pool.ts +102 -14
- package/src/runner/services/context-builder.ts +144 -0
- package/src/runner/services/secret-manager.ts +105 -0
- package/src/runner/services/workflow-validator.ts +131 -0
- package/src/runner/shell-executor.test.ts +28 -4
- package/src/runner/standard-tools-ast.test.ts +196 -0
- package/src/runner/standard-tools-execution.test.ts +27 -0
- package/src/runner/standard-tools-integration.test.ts +6 -10
- package/src/runner/standard-tools.ts +339 -102
- package/src/runner/step-executor.test.ts +216 -4
- package/src/runner/step-executor.ts +69 -941
- package/src/runner/stream-utils.ts +7 -3
- package/src/runner/test-harness.ts +20 -1
- package/src/runner/timeout.test.ts +10 -0
- package/src/runner/timeout.ts +11 -2
- package/src/runner/tool-integration.test.ts +1 -1
- package/src/runner/wait-step.test.ts +102 -0
- package/src/runner/workflow-runner.test.ts +208 -15
- package/src/runner/workflow-runner.ts +890 -818
- package/src/runner/workflow-scheduler.ts +75 -0
- package/src/runner/workflow-state.ts +269 -0
- package/src/runner/workflow-subflows.test.ts +13 -12
- package/src/scripts/generate-schemas.ts +16 -0
- package/src/templates/agents/explore.md +1 -0
- package/src/templates/agents/general.md +1 -0
- package/src/templates/agents/handoff-router.md +14 -0
- package/src/templates/agents/handoff-specialist.md +15 -0
- package/src/templates/agents/keystone-architect.md +13 -44
- package/src/templates/agents/my-agent.md +1 -0
- package/src/templates/agents/software-engineer.md +1 -0
- package/src/templates/agents/summarizer.md +1 -0
- package/src/templates/agents/test-agent.md +1 -0
- package/src/templates/agents/tester.md +1 -0
- package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
- package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +4 -1
- package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
- package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
- package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
- package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
- package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
- package/src/templates/control-flow/idempotency-example.yaml +30 -0
- package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
- package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
- package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
- package/src/templates/features/artifact-example.yaml +40 -0
- package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
- package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
- package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
- package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
- package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
- package/src/templates/features/script-example.yaml +28 -0
- package/src/templates/patterns/agent-handoff.yaml +53 -0
- package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
- package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
- package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +2 -1
- package/src/templates/patterns/composition-parent.yaml +18 -0
- package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
- package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
- package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
- package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
- package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
- package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
- package/src/templates/scaffolding/review-loop.yaml +97 -0
- package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
- package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
- package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
- package/src/templates/testing/invalid.yaml +6 -0
- package/src/ui/dashboard.tsx +191 -33
- package/src/utils/auth-manager.test.ts +337 -0
- package/src/utils/auth-manager.ts +157 -61
- package/src/utils/blueprint-utils.ts +4 -6
- package/src/utils/config-loader.test.ts +2 -0
- package/src/utils/config-loader.ts +12 -3
- package/src/utils/constants.ts +76 -0
- package/src/utils/container.ts +63 -0
- package/src/utils/context-injector.test.ts +200 -0
- package/src/utils/context-injector.ts +244 -0
- package/src/utils/doc-generator.ts +85 -0
- package/src/utils/env-filter.ts +45 -0
- package/src/utils/json-parser.test.ts +12 -0
- package/src/utils/json-parser.ts +30 -5
- package/src/utils/logger.ts +12 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.ts +52 -1
- package/src/utils/process-sandbox-worker.test.ts +46 -0
- package/src/utils/process-sandbox.ts +227 -14
- package/src/utils/redactor.test.ts +11 -6
- package/src/utils/redactor.ts +25 -9
- package/src/utils/sandbox.ts +3 -0
- package/src/runner/llm-executor.ts +0 -638
- package/src/runner/shell-executor.ts +0 -366
- package/src/templates/composition-parent.yaml +0 -14
- package/src/templates/invalid.yaml +0 -5
package/src/cli.ts
CHANGED
|
@@ -1,479 +1,305 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, watch, writeFileSync } from 'node:fs';
|
|
3
|
+
import type { FSWatcher } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { Command } from 'commander';
|
|
5
6
|
|
|
6
|
-
import exploreAgent from './templates/agents/explore.md' with { type: 'text' };
|
|
7
|
-
import generalAgent from './templates/agents/general.md' with { type: 'text' };
|
|
8
|
-
import architectAgent from './templates/agents/keystone-architect.md' with { type: 'text' };
|
|
9
|
-
import softwareEngineerAgent from './templates/agents/software-engineer.md' with { type: 'text' };
|
|
10
|
-
import summarizerAgent from './templates/agents/summarizer.md' with { type: 'text' };
|
|
11
|
-
import testerAgent from './templates/agents/tester.md' with { type: 'text' };
|
|
12
|
-
import decomposeImplementWorkflow from './templates/decompose-implement.yaml' with { type: 'text' };
|
|
13
|
-
import decomposeWorkflow from './templates/decompose-problem.yaml' with { type: 'text' };
|
|
14
|
-
import decomposeResearchWorkflow from './templates/decompose-research.yaml' with { type: 'text' };
|
|
15
|
-
import decomposeReviewWorkflow from './templates/decompose-review.yaml' with { type: 'text' };
|
|
16
|
-
import devWorkflow from './templates/dev.yaml' with { type: 'text' };
|
|
17
|
-
// Default templates
|
|
18
|
-
import scaffoldWorkflow from './templates/scaffold-feature.yaml' with { type: 'text' };
|
|
19
|
-
import scaffoldGenerateWorkflow from './templates/scaffold-generate.yaml' with { type: 'text' };
|
|
20
|
-
import scaffoldPlanWorkflow from './templates/scaffold-plan.yaml' with { type: 'text' };
|
|
21
|
-
|
|
22
7
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
8
|
+
import { MemoryDb } from './db/memory-db.ts';
|
|
23
9
|
import { WorkflowDb, type WorkflowRun } from './db/workflow-db.ts';
|
|
10
|
+
import { ExpressionEvaluator } from './expression/evaluator.ts';
|
|
11
|
+
import type { Workflow } from './parser/schema.ts';
|
|
24
12
|
import type { TestDefinition } from './parser/test-schema.ts';
|
|
25
13
|
import { WorkflowParser } from './parser/workflow-parser.ts';
|
|
26
14
|
import { WorkflowSuspendedError, WorkflowWaitingError } from './runner/step-executor.ts';
|
|
27
15
|
import { TestHarness } from './runner/test-harness.ts';
|
|
28
16
|
import { ConfigLoader } from './utils/config-loader.ts';
|
|
29
17
|
import { LIMITS } from './utils/constants.ts';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
18
|
+
import { container } from './utils/container.ts';
|
|
19
|
+
import { ConsoleLogger, SilentLogger } from './utils/logger.ts';
|
|
32
20
|
import { WorkflowRegistry } from './utils/workflow-registry.ts';
|
|
33
21
|
|
|
22
|
+
// Import modular commands
|
|
23
|
+
import {
|
|
24
|
+
parseInputs,
|
|
25
|
+
registerDocCommand,
|
|
26
|
+
registerEventCommand,
|
|
27
|
+
registerGraphCommand,
|
|
28
|
+
registerInitCommand,
|
|
29
|
+
registerRunCommand,
|
|
30
|
+
registerSchemaCommand,
|
|
31
|
+
registerValidateCommand,
|
|
32
|
+
} from './commands/index.ts';
|
|
33
|
+
|
|
34
34
|
import pkg from '../package.json' with { type: 'json' };
|
|
35
35
|
|
|
36
|
+
// Bootstrap DI container with default services
|
|
37
|
+
container.factory('logger', () => new ConsoleLogger());
|
|
38
|
+
container.factory('db', () => new WorkflowDb());
|
|
39
|
+
container.factory('memoryDb', () => new MemoryDb());
|
|
40
|
+
|
|
36
41
|
const program = new Command();
|
|
37
42
|
const defaultRetentionDays = ConfigLoader.load().storage?.retention_days ?? 30;
|
|
38
|
-
const MAX_INPUT_STRING_LENGTH = LIMITS.MAX_INPUT_STRING_LENGTH;
|
|
39
43
|
|
|
40
44
|
program
|
|
41
45
|
.name('keystone')
|
|
42
46
|
.description('A local-first, declarative, agentic workflow orchestrator')
|
|
43
47
|
.version(pkg.version);
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const key = pair.slice(0, index);
|
|
63
|
-
const value = pair.slice(index + 1);
|
|
49
|
+
// Register modular commands
|
|
50
|
+
registerInitCommand(program);
|
|
51
|
+
registerValidateCommand(program);
|
|
52
|
+
registerGraphCommand(program);
|
|
53
|
+
registerDocCommand(program);
|
|
54
|
+
registerSchemaCommand(program);
|
|
55
|
+
registerEventCommand(program);
|
|
56
|
+
registerRunCommand(program);
|
|
57
|
+
|
|
58
|
+
// Helper function used by remaining commands (rerun)
|
|
59
|
+
const collectDownstreamSteps = (workflow: Workflow, fromStepId: string): string[] => {
|
|
60
|
+
const stepIds = new Set(workflow.steps.map((step) => step.id));
|
|
61
|
+
if (!stepIds.has(fromStepId)) {
|
|
62
|
+
throw new Error(`Step not found in workflow: ${fromStepId}`);
|
|
63
|
+
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
continue;
|
|
65
|
+
const dependents = new Map<string, Set<string>>();
|
|
66
|
+
for (const step of workflow.steps) {
|
|
67
|
+
for (const dep of step.needs) {
|
|
68
|
+
if (!dependents.has(dep)) {
|
|
69
|
+
dependents.set(dep, new Set());
|
|
70
|
+
}
|
|
71
|
+
dependents.get(dep)?.add(step.id);
|
|
73
72
|
}
|
|
73
|
+
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (parsed.includes('\u0000')) {
|
|
86
|
-
console.warn(`⚠️ Input "${key}" contains invalid null characters`);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
75
|
+
const queue = [fromStepId];
|
|
76
|
+
const result = new Set<string>([fromStepId]);
|
|
77
|
+
while (queue.length > 0) {
|
|
78
|
+
const current = queue.shift();
|
|
79
|
+
if (!current) continue;
|
|
80
|
+
for (const next of dependents.get(current) || []) {
|
|
81
|
+
if (!result.has(next)) {
|
|
82
|
+
result.add(next);
|
|
83
|
+
queue.push(next);
|
|
89
84
|
}
|
|
90
|
-
inputs[key] = parsed;
|
|
91
|
-
} catch {
|
|
92
|
-
if (value.length > MAX_INPUT_STRING_LENGTH) {
|
|
93
|
-
console.warn(
|
|
94
|
-
`⚠️ Input "${key}" exceeds maximum length of ${MAX_INPUT_STRING_LENGTH} characters`
|
|
95
|
-
);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (value.includes('\u0000')) {
|
|
99
|
-
console.warn(`⚠️ Input "${key}" contains invalid null characters`);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
// Check if it looks like malformed JSON (starts with { or [)
|
|
103
|
-
if ((value.startsWith('{') || value.startsWith('[')) && value.length > 1) {
|
|
104
|
-
console.warn(
|
|
105
|
-
`⚠️ Input "${key}" looks like JSON but failed to parse. Check for syntax errors.`
|
|
106
|
-
);
|
|
107
|
-
console.warn(` Value: ${value.slice(0, 50)}${value.length > 50 ? '...' : ''}`);
|
|
108
|
-
}
|
|
109
|
-
// Fall back to string value
|
|
110
|
-
inputs[key] = value;
|
|
111
85
|
}
|
|
112
86
|
}
|
|
113
|
-
|
|
87
|
+
|
|
88
|
+
return Array.from(result);
|
|
114
89
|
};
|
|
115
90
|
|
|
116
|
-
// ===== keystone
|
|
91
|
+
// ===== keystone watch =====
|
|
117
92
|
program
|
|
118
|
-
.command('
|
|
119
|
-
.description('
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
93
|
+
.command('watch')
|
|
94
|
+
.description('Watch a workflow and re-run on changes')
|
|
95
|
+
.argument('<workflow>', 'Workflow name or path to workflow file')
|
|
96
|
+
.option('-i, --input <key=value...>', 'Input values')
|
|
97
|
+
.option('--debug', 'Enable interactive debug mode on failure')
|
|
98
|
+
.option('--events', 'Emit structured JSON events (NDJSON) to stdout')
|
|
99
|
+
.option('--debounce <ms>', 'Debounce delay in milliseconds', '200')
|
|
100
|
+
.action(async (workflowPathArg, options) => {
|
|
101
|
+
const inputs = parseInputs(options.input);
|
|
102
|
+
const eventsEnabled = !!options.events;
|
|
103
|
+
const logger = eventsEnabled ? new SilentLogger() : new ConsoleLogger();
|
|
104
|
+
const onEvent = eventsEnabled
|
|
105
|
+
? (event: unknown) => {
|
|
106
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
107
|
+
}
|
|
108
|
+
: undefined;
|
|
109
|
+
const debounceMs = Number.parseInt(options.debounce, 10);
|
|
133
110
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const defaultConfig = `# Keystone Configuration
|
|
138
|
-
default_provider: openai
|
|
139
|
-
|
|
140
|
-
providers:
|
|
141
|
-
openai:
|
|
142
|
-
type: openai
|
|
143
|
-
base_url: https://api.openai.com/v1
|
|
144
|
-
api_key_env: OPENAI_API_KEY
|
|
145
|
-
default_model: gpt-4o
|
|
146
|
-
anthropic:
|
|
147
|
-
type: anthropic
|
|
148
|
-
base_url: https://api.anthropic.com/v1
|
|
149
|
-
api_key_env: ANTHROPIC_API_KEY
|
|
150
|
-
default_model: claude-3-5-sonnet-20240620
|
|
151
|
-
groq:
|
|
152
|
-
type: openai
|
|
153
|
-
base_url: https://api.groq.com/openai/v1
|
|
154
|
-
api_key_env: GROQ_API_KEY
|
|
155
|
-
default_model: llama-3.3-70b-versatile
|
|
156
|
-
|
|
157
|
-
model_mappings:
|
|
158
|
-
"gpt-*": openai
|
|
159
|
-
"claude-*": anthropic
|
|
160
|
-
"o1-*": openai
|
|
161
|
-
"llama-*": groq
|
|
162
|
-
|
|
163
|
-
# mcp_servers:
|
|
164
|
-
# filesystem:
|
|
165
|
-
# command: npx
|
|
166
|
-
# args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
167
|
-
|
|
168
|
-
# engines:
|
|
169
|
-
# allowlist:
|
|
170
|
-
# codex:
|
|
171
|
-
# command: codex
|
|
172
|
-
# version: "1.2.3"
|
|
173
|
-
# versionArgs: ["--version"]
|
|
174
|
-
|
|
175
|
-
storage:
|
|
176
|
-
retention_days: 30
|
|
177
|
-
`;
|
|
178
|
-
writeFileSync(configPath, defaultConfig);
|
|
179
|
-
console.log(`✓ Created ${configPath}`);
|
|
180
|
-
} else {
|
|
181
|
-
console.log(`⊘ ${configPath} already exists`);
|
|
111
|
+
if (!Number.isFinite(debounceMs) || debounceMs < 0) {
|
|
112
|
+
console.error('✗ debounce must be a non-negative integer');
|
|
113
|
+
process.exit(1);
|
|
182
114
|
}
|
|
183
115
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else {
|
|
194
|
-
console.log(`⊘ ${envPath} already exists`);
|
|
116
|
+
let resolvedPath: string;
|
|
117
|
+
try {
|
|
118
|
+
resolvedPath = WorkflowRegistry.resolvePath(workflowPathArg);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(
|
|
121
|
+
'✗ Failed to resolve workflow:',
|
|
122
|
+
error instanceof Error ? error.message : error
|
|
123
|
+
);
|
|
124
|
+
process.exit(1);
|
|
195
125
|
}
|
|
196
126
|
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
path: '.keystone/workflows/scaffold-plan.yaml',
|
|
205
|
-
content: scaffoldPlanWorkflow,
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
path: '.keystone/workflows/scaffold-generate.yaml',
|
|
209
|
-
content: scaffoldGenerateWorkflow,
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
path: '.keystone/workflows/decompose-problem.yaml',
|
|
213
|
-
content: decomposeWorkflow,
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
path: '.keystone/workflows/decompose-research.yaml',
|
|
217
|
-
content: decomposeResearchWorkflow,
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
path: '.keystone/workflows/decompose-implement.yaml',
|
|
221
|
-
content: decomposeImplementWorkflow,
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
path: '.keystone/workflows/decompose-review.yaml',
|
|
225
|
-
content: decomposeReviewWorkflow,
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
path: '.keystone/workflows/agents/keystone-architect.md',
|
|
229
|
-
content: architectAgent,
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
path: '.keystone/workflows/agents/general.md',
|
|
233
|
-
content: generalAgent,
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
path: '.keystone/workflows/agents/explore.md',
|
|
237
|
-
content: exploreAgent,
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
path: '.keystone/workflows/agents/software-engineer.md',
|
|
241
|
-
content: softwareEngineerAgent,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
path: '.keystone/workflows/agents/summarizer.md',
|
|
245
|
-
content: summarizerAgent,
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
path: '.keystone/workflows/dev.yaml',
|
|
249
|
-
content: devWorkflow,
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
path: '.keystone/workflows/agents/tester.md',
|
|
253
|
-
content: testerAgent,
|
|
254
|
-
},
|
|
255
|
-
];
|
|
127
|
+
const watchers = new Map<string, FSWatcher>();
|
|
128
|
+
const warned = new Set<string>();
|
|
129
|
+
let running = false;
|
|
130
|
+
let rerunQueued = false;
|
|
131
|
+
let debounceTimer: NodeJS.Timeout | undefined;
|
|
256
132
|
|
|
257
|
-
|
|
258
|
-
if (!
|
|
259
|
-
|
|
260
|
-
console.log(`✓ Seeded ${seed.path}`);
|
|
261
|
-
} else {
|
|
262
|
-
console.log(`⊘ ${seed.path} already exists`);
|
|
133
|
+
const logInfo = (message: string) => {
|
|
134
|
+
if (!eventsEnabled) {
|
|
135
|
+
console.log(message);
|
|
263
136
|
}
|
|
264
|
-
}
|
|
137
|
+
};
|
|
265
138
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
139
|
+
const logWarn = (message: string) => {
|
|
140
|
+
if (!eventsEnabled) {
|
|
141
|
+
console.warn(message);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
272
144
|
|
|
273
|
-
|
|
274
|
-
program
|
|
275
|
-
.command('validate')
|
|
276
|
-
.description('Validate workflow files')
|
|
277
|
-
.argument('[path]', 'Workflow file or directory to validate (default: .keystone/workflows/)')
|
|
278
|
-
.option('--strict', 'Enable strict validation (schemas, enums)')
|
|
279
|
-
.option('--explain', 'Show detailed error context with suggestions')
|
|
280
|
-
.action(async (pathArg, options) => {
|
|
281
|
-
const path = pathArg || '.keystone/workflows/';
|
|
145
|
+
const normalizePath = (filePath: string) => resolve(filePath);
|
|
282
146
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
147
|
+
const scheduleRun = (reason?: string) => {
|
|
148
|
+
if (debounceTimer) {
|
|
149
|
+
clearTimeout(debounceTimer);
|
|
150
|
+
}
|
|
151
|
+
debounceTimer = setTimeout(() => {
|
|
152
|
+
debounceTimer = undefined;
|
|
153
|
+
if (reason && !eventsEnabled) {
|
|
154
|
+
console.log(`Change detected in ${reason}. Rerunning...`);
|
|
291
155
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
156
|
+
void runWorkflow();
|
|
157
|
+
}, debounceMs);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const ensureWatcher = (filePath: string) => {
|
|
161
|
+
if (watchers.has(filePath)) return;
|
|
162
|
+
if (!existsSync(filePath)) {
|
|
163
|
+
if (!warned.has(filePath)) {
|
|
164
|
+
warned.add(filePath);
|
|
165
|
+
logWarn(`⚠️ Watch skipped (path not found): ${filePath}`);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const watcher = watch(filePath, () => scheduleRun(filePath));
|
|
171
|
+
watchers.set(filePath, watcher);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (!warned.has(filePath)) {
|
|
174
|
+
warned.add(filePath);
|
|
175
|
+
logWarn(
|
|
176
|
+
`⚠️ Failed to watch ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
177
|
+
);
|
|
299
178
|
}
|
|
300
179
|
}
|
|
180
|
+
};
|
|
301
181
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
182
|
+
const updateWatchers = (paths: Set<string>) => {
|
|
183
|
+
for (const existing of Array.from(watchers.keys())) {
|
|
184
|
+
if (!paths.has(existing)) {
|
|
185
|
+
watchers.get(existing)?.close();
|
|
186
|
+
watchers.delete(existing);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const filePath of paths) {
|
|
191
|
+
ensureWatcher(filePath);
|
|
305
192
|
}
|
|
306
193
|
|
|
307
|
-
|
|
194
|
+
logInfo(`Watching ${paths.size} file(s).`);
|
|
195
|
+
};
|
|
308
196
|
|
|
309
|
-
|
|
310
|
-
|
|
197
|
+
const collectWatchPaths = (
|
|
198
|
+
workflowPath: string,
|
|
199
|
+
workflow: Workflow,
|
|
200
|
+
visited: Set<string> = new Set()
|
|
201
|
+
): Set<string> => {
|
|
202
|
+
const normalizedPath = normalizePath(workflowPath);
|
|
203
|
+
if (visited.has(normalizedPath)) return new Set();
|
|
204
|
+
visited.add(normalizedPath);
|
|
205
|
+
|
|
206
|
+
const watchPaths = new Set<string>([normalizedPath]);
|
|
207
|
+
const baseDir = dirname(workflowPath);
|
|
208
|
+
const allSteps = [...workflow.steps, ...(workflow.errors || []), ...(workflow.finally || [])];
|
|
209
|
+
|
|
210
|
+
for (const step of allSteps) {
|
|
211
|
+
if (step.type === 'file' && step.op === 'read') {
|
|
212
|
+
if (ExpressionEvaluator.hasExpression(step.path)) {
|
|
213
|
+
const warningKey = `${workflowPath}:${step.id}:file`;
|
|
214
|
+
if (!warned.has(warningKey)) {
|
|
215
|
+
warned.add(warningKey);
|
|
216
|
+
logWarn(`⚠️ Watch skipped for dynamic file path in step "${step.id}".`);
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
watchPaths.add(normalizePath(resolve(baseDir, step.path)));
|
|
221
|
+
}
|
|
311
222
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
223
|
+
if (step.type === 'workflow') {
|
|
224
|
+
if (ExpressionEvaluator.hasExpression(step.path)) {
|
|
225
|
+
const warningKey = `${workflowPath}:${step.id}:workflow`;
|
|
226
|
+
if (!warned.has(warningKey)) {
|
|
227
|
+
warned.add(warningKey);
|
|
228
|
+
logWarn(`⚠️ Watch skipped for dynamic workflow path in step "${step.id}".`);
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
318
231
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
renderError({
|
|
334
|
-
message: error instanceof Error ? error.message : String(error),
|
|
335
|
-
filePath: file,
|
|
336
|
-
})
|
|
232
|
+
try {
|
|
233
|
+
const childPath = WorkflowRegistry.resolvePath(step.path, baseDir);
|
|
234
|
+
const childWorkflow = WorkflowParser.loadWorkflow(childPath);
|
|
235
|
+
for (const child of collectWatchPaths(childPath, childWorkflow, visited)) {
|
|
236
|
+
watchPaths.add(child);
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const warningKey = `${workflowPath}:${step.id}:workflow-load`;
|
|
240
|
+
if (!warned.has(warningKey)) {
|
|
241
|
+
warned.add(warningKey);
|
|
242
|
+
logWarn(
|
|
243
|
+
`⚠️ Failed to load sub-workflow for step "${step.id}": ${
|
|
244
|
+
error instanceof Error ? error.message : String(error)
|
|
245
|
+
}`
|
|
337
246
|
);
|
|
338
247
|
}
|
|
339
|
-
} else {
|
|
340
|
-
console.error(
|
|
341
|
-
` ✗ ${file.padEnd(40)} ${error instanceof Error ? error.message : String(error)}`
|
|
342
|
-
);
|
|
343
248
|
}
|
|
344
|
-
failCount++;
|
|
345
249
|
}
|
|
346
250
|
}
|
|
347
251
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
process.exit(1);
|
|
351
|
-
}
|
|
352
|
-
} catch (error) {
|
|
353
|
-
console.error('✗ Validation failed:', error instanceof Error ? error.message : error);
|
|
354
|
-
process.exit(1);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
252
|
+
return watchPaths;
|
|
253
|
+
};
|
|
357
254
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
.argument('<workflow>', 'Workflow name or path to workflow file')
|
|
363
|
-
.action(async (workflowPath) => {
|
|
364
|
-
try {
|
|
365
|
-
const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
|
|
366
|
-
const workflow = WorkflowParser.loadWorkflow(resolvedPath);
|
|
367
|
-
const ascii = renderWorkflowAsAscii(workflow);
|
|
368
|
-
if (ascii) {
|
|
369
|
-
console.log(`\n${ascii}\n`);
|
|
370
|
-
} else {
|
|
371
|
-
const mermaid = generateMermaidGraph(workflow);
|
|
372
|
-
console.log('\n```mermaid');
|
|
373
|
-
console.log(mermaid);
|
|
374
|
-
console.log('```\n');
|
|
255
|
+
const runWorkflow = async () => {
|
|
256
|
+
if (running) {
|
|
257
|
+
rerunQueued = true;
|
|
258
|
+
return;
|
|
375
259
|
}
|
|
376
|
-
|
|
377
|
-
console.error('✗ Failed to generate graph:', error instanceof Error ? error.message : error);
|
|
378
|
-
process.exit(1);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// ===== keystone run =====
|
|
383
|
-
program
|
|
384
|
-
.command('run')
|
|
385
|
-
.description('Execute a workflow')
|
|
386
|
-
.argument('<workflow>', 'Workflow name or path to workflow file')
|
|
387
|
-
.option('-i, --input <key=value...>', 'Input values')
|
|
388
|
-
.option('--dry-run', 'Show what would be executed without actually running it')
|
|
389
|
-
.option('--debug', 'Enable interactive debug mode on failure')
|
|
390
|
-
.option('--no-dedup', 'Disable idempotency/deduplication')
|
|
391
|
-
.option('--resume', 'Resume the last run of this workflow if it failed or was paused')
|
|
392
|
-
.option('--explain', 'Show detailed error context with suggestions on failure')
|
|
393
|
-
.action(async (workflowPathArg, options) => {
|
|
394
|
-
const inputs = parseInputs(options.input);
|
|
395
|
-
let resolvedPath: string | undefined;
|
|
260
|
+
running = true;
|
|
396
261
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const db = new WorkflowDb();
|
|
411
|
-
const lastRun = await db.getLastRun(workflow.name);
|
|
412
|
-
db.close();
|
|
262
|
+
try {
|
|
263
|
+
const workflow = WorkflowParser.loadWorkflow(resolvedPath);
|
|
264
|
+
const watchPaths = collectWatchPaths(resolvedPath, workflow);
|
|
265
|
+
updateWatchers(watchPaths);
|
|
266
|
+
|
|
267
|
+
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
268
|
+
const runner = new WorkflowRunner(workflow, {
|
|
269
|
+
inputs,
|
|
270
|
+
workflowDir: dirname(resolvedPath),
|
|
271
|
+
debug: !!options.debug,
|
|
272
|
+
logger,
|
|
273
|
+
onEvent,
|
|
274
|
+
});
|
|
413
275
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
} else {
|
|
427
|
-
console.log(`Last run ${lastRun.id} completed successfully. Starting new run.`);
|
|
428
|
-
}
|
|
429
|
-
} else {
|
|
430
|
-
console.log('No previous run found. Starting new run.');
|
|
276
|
+
const outputs = await runner.run();
|
|
277
|
+
if (!eventsEnabled && Object.keys(outputs).length > 0) {
|
|
278
|
+
console.log('Outputs:');
|
|
279
|
+
console.log(JSON.stringify(runner.redact(outputs), null, 2));
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error('✗ Watch run failed:', error instanceof Error ? error.message : error);
|
|
283
|
+
} finally {
|
|
284
|
+
running = false;
|
|
285
|
+
if (rerunQueued) {
|
|
286
|
+
rerunQueued = false;
|
|
287
|
+
scheduleRun();
|
|
431
288
|
}
|
|
432
289
|
}
|
|
290
|
+
};
|
|
433
291
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
workflowDir: dirname(resolvedPath),
|
|
438
|
-
dryRun: !!options.dryRun,
|
|
439
|
-
debug: !!options.debug,
|
|
440
|
-
dedup: options.dedup,
|
|
441
|
-
resumeRunId,
|
|
442
|
-
logger,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
const outputs = await runner.run();
|
|
292
|
+
updateWatchers(new Set([normalizePath(resolvedPath)]));
|
|
293
|
+
logInfo(`Watching workflow: ${resolvedPath}`);
|
|
294
|
+
scheduleRun('initial');
|
|
446
295
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
296
|
+
process.on('SIGINT', () => {
|
|
297
|
+
for (const watcher of watchers.values()) {
|
|
298
|
+
watcher.close();
|
|
450
299
|
}
|
|
300
|
+
logInfo('\nStopping watch.');
|
|
451
301
|
process.exit(0);
|
|
452
|
-
}
|
|
453
|
-
if (options.explain) {
|
|
454
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
455
|
-
try {
|
|
456
|
-
const { readFileSync } = await import('node:fs');
|
|
457
|
-
const { renderError } = await import('./utils/error-renderer.ts');
|
|
458
|
-
const source = resolvedPath ? readFileSync(resolvedPath, 'utf-8') : undefined;
|
|
459
|
-
console.error(
|
|
460
|
-
renderError({
|
|
461
|
-
message,
|
|
462
|
-
source,
|
|
463
|
-
filePath: resolvedPath,
|
|
464
|
-
})
|
|
465
|
-
);
|
|
466
|
-
} catch {
|
|
467
|
-
console.error('✗ Failed to execute workflow:', message);
|
|
468
|
-
}
|
|
469
|
-
} else {
|
|
470
|
-
console.error(
|
|
471
|
-
'✗ Failed to execute workflow:',
|
|
472
|
-
error instanceof Error ? error.message : error
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
process.exit(1);
|
|
476
|
-
}
|
|
302
|
+
});
|
|
477
303
|
});
|
|
478
304
|
|
|
479
305
|
// ===== keystone test =====
|
|
@@ -516,7 +342,7 @@ program
|
|
|
516
342
|
const workflowPath = WorkflowRegistry.resolvePath(testDef.workflow);
|
|
517
343
|
const workflow = WorkflowParser.loadWorkflow(workflowPath);
|
|
518
344
|
|
|
519
|
-
const harness = new TestHarness(workflow, testDef.fixture);
|
|
345
|
+
const harness = new TestHarness(workflow, testDef.fixture, testDef.options);
|
|
520
346
|
const result = await harness.run();
|
|
521
347
|
|
|
522
348
|
if (!testDef.snapshot || options.update) {
|
|
@@ -622,9 +448,11 @@ program
|
|
|
622
448
|
.argument('<run_id>', 'Run ID to resume')
|
|
623
449
|
.option('-w, --workflow <path>', 'Path to workflow file (auto-detected if not specified)')
|
|
624
450
|
.option('-i, --input <key=value...>', 'Input values for resume')
|
|
451
|
+
.option('--events', 'Emit structured JSON events (NDJSON) to stdout')
|
|
625
452
|
.action(async (runId, options) => {
|
|
626
453
|
try {
|
|
627
454
|
const db = new WorkflowDb();
|
|
455
|
+
const eventsEnabled = !!options.events;
|
|
628
456
|
|
|
629
457
|
// Load run from database to get workflow name
|
|
630
458
|
const run = await db.getRun(runId);
|
|
@@ -635,7 +463,9 @@ program
|
|
|
635
463
|
process.exit(1);
|
|
636
464
|
}
|
|
637
465
|
|
|
638
|
-
|
|
466
|
+
if (!eventsEnabled) {
|
|
467
|
+
console.log(`Found run: ${run.workflow_name} (status: ${run.status})`);
|
|
468
|
+
}
|
|
639
469
|
|
|
640
470
|
// Determine workflow file path
|
|
641
471
|
let workflowPath = options.workflow;
|
|
@@ -652,7 +482,9 @@ program
|
|
|
652
482
|
}
|
|
653
483
|
}
|
|
654
484
|
|
|
655
|
-
|
|
485
|
+
if (!eventsEnabled) {
|
|
486
|
+
console.log(`Loading workflow from: ${workflowPath}\n`);
|
|
487
|
+
}
|
|
656
488
|
|
|
657
489
|
// Close DB before loading workflow (will be reopened by runner)
|
|
658
490
|
db.close();
|
|
@@ -662,18 +494,24 @@ program
|
|
|
662
494
|
|
|
663
495
|
// Import WorkflowRunner dynamically
|
|
664
496
|
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
665
|
-
const logger = new ConsoleLogger();
|
|
497
|
+
const logger = eventsEnabled ? new SilentLogger() : new ConsoleLogger();
|
|
498
|
+
const onEvent = eventsEnabled
|
|
499
|
+
? (event: unknown) => {
|
|
500
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
501
|
+
}
|
|
502
|
+
: undefined;
|
|
666
503
|
const inputs = parseInputs(options.input);
|
|
667
504
|
const runner = new WorkflowRunner(workflow, {
|
|
668
505
|
resumeRunId: runId,
|
|
669
506
|
resumeInputs: inputs,
|
|
670
507
|
workflowDir: dirname(workflowPath),
|
|
671
508
|
logger,
|
|
509
|
+
onEvent,
|
|
672
510
|
});
|
|
673
511
|
|
|
674
512
|
const outputs = await runner.run();
|
|
675
513
|
|
|
676
|
-
if (Object.keys(outputs).length > 0) {
|
|
514
|
+
if (!eventsEnabled && Object.keys(outputs).length > 0) {
|
|
677
515
|
console.log('Outputs:');
|
|
678
516
|
console.log(JSON.stringify(runner.redact(outputs), null, 2));
|
|
679
517
|
}
|
|
@@ -684,6 +522,94 @@ program
|
|
|
684
522
|
}
|
|
685
523
|
});
|
|
686
524
|
|
|
525
|
+
// ===== keystone rerun =====
|
|
526
|
+
program
|
|
527
|
+
.command('rerun')
|
|
528
|
+
.description('Rerun a workflow from a specific step (invalidates downstream steps)')
|
|
529
|
+
.argument('<workflow>', 'Workflow name or path to workflow file')
|
|
530
|
+
.requiredOption('--from <step_id>', 'Step ID to rerun (downstream steps will be invalidated)')
|
|
531
|
+
.option('-r, --run <run_id>', 'Run ID to rerun (defaults to last run of the workflow)')
|
|
532
|
+
.option('-i, --input <key=value...>', 'Input values for rerun')
|
|
533
|
+
.option('--events', 'Emit structured JSON events (NDJSON) to stdout')
|
|
534
|
+
.action(async (workflowPathArg, options) => {
|
|
535
|
+
let db: WorkflowDb | undefined;
|
|
536
|
+
try {
|
|
537
|
+
const resolvedPath = WorkflowRegistry.resolvePath(workflowPathArg);
|
|
538
|
+
const workflow = WorkflowParser.loadWorkflow(resolvedPath);
|
|
539
|
+
const inputs = parseInputs(options.input);
|
|
540
|
+
const eventsEnabled = !!options.events;
|
|
541
|
+
|
|
542
|
+
db = new WorkflowDb();
|
|
543
|
+
const runId =
|
|
544
|
+
options.run ||
|
|
545
|
+
(await db.getLastRun(workflow.name))?.id ||
|
|
546
|
+
((): never => {
|
|
547
|
+
throw new Error(`No runs found for workflow "${workflow.name}"`);
|
|
548
|
+
})();
|
|
549
|
+
|
|
550
|
+
const run = await db.getRun(runId);
|
|
551
|
+
if (!run) {
|
|
552
|
+
throw new Error(`Run not found: ${runId}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (run.workflow_name !== workflow.name) {
|
|
556
|
+
console.warn(
|
|
557
|
+
`⚠️ Run ${runId} is for workflow "${run.workflow_name}", but you provided "${workflow.name}".`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (run.status === 'running') {
|
|
562
|
+
console.warn('⚠️ Rerunning a run marked as running. Ensure no other instances are active.');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const stepIds = collectDownstreamSteps(workflow, options.from);
|
|
566
|
+
const clearedSteps = await db.clearStepExecutions(runId, stepIds);
|
|
567
|
+
const clearedIdempotency = await db.clearIdempotencyRecordsForSteps(runId, stepIds);
|
|
568
|
+
const clearedTimers = await db.clearTimersForSteps(runId, stepIds);
|
|
569
|
+
const clearedCompensations = await db.clearCompensationsForSteps(runId, stepIds);
|
|
570
|
+
|
|
571
|
+
await db.updateRunStatus(runId, 'paused');
|
|
572
|
+
db.close();
|
|
573
|
+
db = undefined;
|
|
574
|
+
|
|
575
|
+
if (!eventsEnabled) {
|
|
576
|
+
console.log(
|
|
577
|
+
`Cleared ${clearedSteps} step execution(s), ${clearedIdempotency} idempotency record(s), ${clearedTimers} timer(s), ${clearedCompensations} compensation(s).`
|
|
578
|
+
);
|
|
579
|
+
console.log(`Resuming run ${runId} from step ${options.from}...\n`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
583
|
+
const logger = eventsEnabled ? new SilentLogger() : new ConsoleLogger();
|
|
584
|
+
const onEvent = eventsEnabled
|
|
585
|
+
? (event: unknown) => {
|
|
586
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
587
|
+
}
|
|
588
|
+
: undefined;
|
|
589
|
+
const runner = new WorkflowRunner(workflow, {
|
|
590
|
+
resumeRunId: runId,
|
|
591
|
+
resumeInputs: inputs,
|
|
592
|
+
workflowDir: dirname(resolvedPath),
|
|
593
|
+
logger,
|
|
594
|
+
allowSuccessResume: true,
|
|
595
|
+
onEvent,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const outputs = await runner.run();
|
|
599
|
+
|
|
600
|
+
if (!eventsEnabled && Object.keys(outputs).length > 0) {
|
|
601
|
+
console.log('Outputs:');
|
|
602
|
+
console.log(JSON.stringify(runner.redact(outputs), null, 2));
|
|
603
|
+
}
|
|
604
|
+
process.exit(0);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error('✗ Failed to rerun workflow:', error instanceof Error ? error.message : error);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
} finally {
|
|
609
|
+
db?.close();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
687
613
|
// ===== keystone history =====
|
|
688
614
|
program
|
|
689
615
|
.command('history')
|
|
@@ -1881,9 +1807,11 @@ _keystone() {
|
|
|
1881
1807
|
commands=(
|
|
1882
1808
|
'init:Initialize a new Keystone project'
|
|
1883
1809
|
'validate:Validate workflow files'
|
|
1810
|
+
'lint:Lint workflow files'
|
|
1884
1811
|
'graph:Visualize a workflow as a Mermaid.js graph'
|
|
1885
1812
|
'run:Execute a workflow'
|
|
1886
1813
|
'resume:Resume a paused or failed workflow run'
|
|
1814
|
+
'rerun:Rerun a workflow from a specific step'
|
|
1887
1815
|
'workflows:List available workflows'
|
|
1888
1816
|
'history:List recent workflow runs'
|
|
1889
1817
|
'logs:Show logs for a workflow run'
|
|
@@ -1909,11 +1837,17 @@ _keystone() {
|
|
|
1909
1837
|
validate)
|
|
1910
1838
|
_arguments ':path:_files'
|
|
1911
1839
|
;;
|
|
1840
|
+
lint)
|
|
1841
|
+
_arguments ':path:_files'
|
|
1842
|
+
;;
|
|
1912
1843
|
resume)
|
|
1913
1844
|
_arguments \\
|
|
1914
1845
|
'(-i --input)'{-i,--input}'[Input values]:key=value' \\
|
|
1915
1846
|
':run_id:__keystone_runs'
|
|
1916
1847
|
;;
|
|
1848
|
+
rerun)
|
|
1849
|
+
_arguments ':workflow:__keystone_workflows'
|
|
1850
|
+
;;
|
|
1917
1851
|
logs)
|
|
1918
1852
|
_arguments ':run_id:__keystone_runs'
|
|
1919
1853
|
;;
|
|
@@ -1949,10 +1883,10 @@ __keystone_runs() {
|
|
|
1949
1883
|
COMPREPLY=()
|
|
1950
1884
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1951
1885
|
prev="\${COMP_WORDS[COMP_CWORD - 1]}"
|
|
1952
|
-
opts="init validate graph run resume workflows history logs prune ui mcp config auth completion"
|
|
1886
|
+
opts="init validate lint graph run watch resume rerun workflows history logs prune ui mcp config auth completion"
|
|
1953
1887
|
|
|
1954
1888
|
case "\${prev}" in
|
|
1955
|
-
run|graph)
|
|
1889
|
+
run|graph|rerun)
|
|
1956
1890
|
local workflows=$(keystone _list-workflows 2>/dev/null)
|
|
1957
1891
|
COMPREPLY=( $(compgen -W "\${workflows}" -- \${cur}) )
|
|
1958
1892
|
return 0
|