agentweaver 0.1.2 → 0.1.4
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 +58 -23
- package/dist/artifacts.js +58 -2
- package/dist/executors/claude-executor.js +12 -2
- package/dist/executors/claude-summary-executor.js +1 -1
- package/dist/executors/codex-docker-executor.js +1 -1
- package/dist/executors/codex-local-executor.js +1 -1
- package/dist/executors/configs/claude-config.js +2 -1
- package/dist/executors/verify-build-executor.js +110 -9
- package/dist/index.js +466 -452
- package/dist/interactive-ui.js +538 -194
- package/dist/jira.js +3 -1
- package/dist/pipeline/auto-flow.js +9 -0
- package/dist/pipeline/checks.js +5 -0
- package/dist/pipeline/context.js +2 -0
- package/dist/pipeline/declarative-flow-runner.js +262 -0
- package/dist/pipeline/declarative-flows.js +24 -0
- package/dist/pipeline/flow-specs/auto.json +485 -0
- package/dist/pipeline/flow-specs/bug-analyze.json +140 -0
- package/dist/pipeline/flow-specs/bug-fix.json +44 -0
- package/dist/pipeline/flow-specs/implement.json +47 -0
- package/dist/pipeline/flow-specs/mr-description.json +61 -0
- package/dist/pipeline/flow-specs/plan.json +88 -0
- package/dist/pipeline/flow-specs/preflight.json +174 -0
- package/dist/pipeline/flow-specs/review-fix.json +76 -0
- package/dist/pipeline/flow-specs/review.json +233 -0
- package/dist/pipeline/flow-specs/run-linter-loop.json +149 -0
- package/dist/pipeline/flow-specs/run-tests-loop.json +149 -0
- package/dist/pipeline/flow-specs/task-describe.json +61 -0
- package/dist/pipeline/flow-specs/test-fix.json +24 -0
- package/dist/pipeline/flow-specs/test-linter-fix.json +24 -0
- package/dist/pipeline/flow-specs/test.json +19 -0
- package/dist/pipeline/flows/implement-flow.js +3 -4
- package/dist/pipeline/flows/preflight-flow.js +17 -57
- package/dist/pipeline/flows/review-fix-flow.js +3 -4
- package/dist/pipeline/flows/review-flow.js +8 -4
- package/dist/pipeline/flows/test-fix-flow.js +3 -4
- package/dist/pipeline/node-registry.js +74 -0
- package/dist/pipeline/node-runner.js +9 -3
- package/dist/pipeline/nodes/build-failure-summary-node.js +4 -4
- package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
- package/dist/pipeline/nodes/claude-summary-node.js +12 -6
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +1 -0
- package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
- package/dist/pipeline/nodes/file-check-node.js +15 -0
- package/dist/pipeline/nodes/flow-run-node.js +40 -0
- package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
- package/dist/pipeline/nodes/task-summary-node.js +12 -6
- package/dist/pipeline/nodes/verify-build-node.js +1 -0
- package/dist/pipeline/prompt-registry.js +27 -0
- package/dist/pipeline/prompt-runtime.js +18 -0
- package/dist/pipeline/registry.js +0 -2
- package/dist/pipeline/spec-compiler.js +213 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +1 -0
- package/dist/pipeline/spec-validator.js +302 -0
- package/dist/pipeline/value-resolver.js +217 -0
- package/dist/prompts.js +22 -3
- package/dist/runtime/process-runner.js +24 -23
- package/dist/structured-artifacts.js +178 -0
- package/dist/tui.js +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { TaskRunnerError } from "../errors.js";
|
|
2
|
+
import { isPromptTemplateRef } from "./prompt-registry.js";
|
|
3
|
+
function assert(condition, message) {
|
|
4
|
+
if (!condition) {
|
|
5
|
+
throw new TaskRunnerError(message);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function validateValueSpec(value, path) {
|
|
9
|
+
if ("const" in value || "ref" in value || "artifact" in value || "artifactList" in value) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if ("template" in value) {
|
|
13
|
+
if (value.vars) {
|
|
14
|
+
for (const [key, candidate] of Object.entries(value.vars)) {
|
|
15
|
+
validateValueSpec(candidate, `${path}.vars.${key}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if ("appendPrompt" in value) {
|
|
21
|
+
if (value.appendPrompt.base) {
|
|
22
|
+
validateValueSpec(value.appendPrompt.base, `${path}.appendPrompt.base`);
|
|
23
|
+
}
|
|
24
|
+
validateValueSpec(value.appendPrompt.suffix, `${path}.appendPrompt.suffix`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if ("concat" in value) {
|
|
28
|
+
value.concat.forEach((candidate, index) => validateValueSpec(candidate, `${path}.concat[${index}]`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if ("list" in value) {
|
|
32
|
+
value.list.forEach((candidate, index) => validateValueSpec(candidate, `${path}.list[${index}]`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
throw new TaskRunnerError(`Unsupported value spec at ${path}`);
|
|
36
|
+
}
|
|
37
|
+
function validateCondition(condition, path) {
|
|
38
|
+
if (!condition) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if ("ref" in condition) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if ("not" in condition) {
|
|
45
|
+
validateCondition(condition.not, `${path}.not`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if ("all" in condition) {
|
|
49
|
+
condition.all.forEach((candidate, index) => validateCondition(candidate, `${path}.all[${index}]`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if ("any" in condition) {
|
|
53
|
+
condition.any.forEach((candidate, index) => validateCondition(candidate, `${path}.any[${index}]`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if ("equals" in condition) {
|
|
57
|
+
validateValueSpec(condition.equals[0], `${path}.equals[0]`);
|
|
58
|
+
validateValueSpec(condition.equals[1], `${path}.equals[1]`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if ("exists" in condition) {
|
|
62
|
+
validateValueSpec(condition.exists, `${path}.exists`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw new TaskRunnerError(`Unsupported condition at ${path}`);
|
|
66
|
+
}
|
|
67
|
+
function validateStep(step, nodeRegistry, path) {
|
|
68
|
+
assert(nodeRegistry.has(step.node), `Unknown node kind '${step.node}' at ${path}.node`);
|
|
69
|
+
const nodeMeta = nodeRegistry.getMeta(step.node);
|
|
70
|
+
validateCondition(step.when, `${path}.when`);
|
|
71
|
+
if (step.prompt) {
|
|
72
|
+
assert(nodeMeta.prompt !== "forbidden", `Node '${step.node}' does not accept prompt binding at ${path}.prompt`);
|
|
73
|
+
assert(Boolean(step.prompt.templateRef || step.prompt.inlineTemplate), `Prompt binding at ${path}.prompt must define templateRef or inlineTemplate`);
|
|
74
|
+
}
|
|
75
|
+
assert(step.prompt !== undefined || nodeMeta.prompt !== "required", `Node '${step.node}' requires prompt binding at ${path}.prompt`);
|
|
76
|
+
for (const requiredParam of nodeMeta.requiredParams ?? []) {
|
|
77
|
+
assert(step.params?.[requiredParam] !== undefined, `Node '${step.node}' requires param '${requiredParam}' at ${path}.params.${requiredParam}`);
|
|
78
|
+
}
|
|
79
|
+
if (step.prompt?.templateRef) {
|
|
80
|
+
assert(isPromptTemplateRef(step.prompt.templateRef), `Unknown prompt template '${step.prompt.templateRef}' at ${path}.prompt.templateRef`);
|
|
81
|
+
}
|
|
82
|
+
if (step.prompt?.vars) {
|
|
83
|
+
for (const [key, value] of Object.entries(step.prompt.vars)) {
|
|
84
|
+
validateValueSpec(value, `${path}.prompt.vars.${key}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (step.prompt?.extraPrompt) {
|
|
88
|
+
validateValueSpec(step.prompt.extraPrompt, `${path}.prompt.extraPrompt`);
|
|
89
|
+
}
|
|
90
|
+
if (step.params) {
|
|
91
|
+
for (const [key, value] of Object.entries(step.params)) {
|
|
92
|
+
validateValueSpec(value, `${path}.params.${key}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (step.expect) {
|
|
96
|
+
step.expect.forEach((expectation, index) => validateExpectation(expectation, `${path}.expect[${index}]`));
|
|
97
|
+
}
|
|
98
|
+
if (step.stopFlowIf) {
|
|
99
|
+
validateCondition(step.stopFlowIf, `${path}.stopFlowIf`);
|
|
100
|
+
}
|
|
101
|
+
if (step.after) {
|
|
102
|
+
step.after.forEach((action, index) => validateAfterAction(action, `${path}.after[${index}]`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function validateExpectation(expectation, path) {
|
|
106
|
+
validateCondition(expectation.when, `${path}.when`);
|
|
107
|
+
if (expectation.kind === "require-artifacts") {
|
|
108
|
+
validateValueSpec(expectation.paths, `${path}.paths`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (expectation.kind === "require-structured-artifacts") {
|
|
112
|
+
expectation.items.forEach((item, index) => {
|
|
113
|
+
validateValueSpec(item.path, `${path}.items[${index}].path`);
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (expectation.kind === "require-file") {
|
|
118
|
+
validateValueSpec(expectation.path, `${path}.path`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (expectation.kind === "step-output") {
|
|
122
|
+
validateValueSpec(expectation.value, `${path}.value`);
|
|
123
|
+
if (expectation.equals) {
|
|
124
|
+
validateValueSpec(expectation.equals, `${path}.equals`);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
throw new TaskRunnerError(`Unsupported expectation at ${path}`);
|
|
129
|
+
}
|
|
130
|
+
function validateAfterAction(action, path) {
|
|
131
|
+
validateCondition(action.when, `${path}.when`);
|
|
132
|
+
if (action.kind === "set-summary-from-file") {
|
|
133
|
+
validateValueSpec(action.path, `${path}.path`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw new TaskRunnerError(`Unsupported after action at ${path}`);
|
|
137
|
+
}
|
|
138
|
+
function validatePhase(phase, nodeRegistry, path) {
|
|
139
|
+
assert(phase.id.trim().length > 0, `Phase id must be non-empty at ${path}.id`);
|
|
140
|
+
validateCondition(phase.when, `${path}.when`);
|
|
141
|
+
phase.steps.forEach((step, index) => validateStep(step, nodeRegistry, `${path}.steps[${index}]`));
|
|
142
|
+
}
|
|
143
|
+
function validateRefPath(ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
144
|
+
const [scope, ...rest] = ref.split(".");
|
|
145
|
+
const supportedScopes = new Set(["params", "flow", "context", "repeat", "steps"]);
|
|
146
|
+
assert(supportedScopes.has(scope ?? ""), `Unsupported ref scope '${scope ?? ""}' at ${path}`);
|
|
147
|
+
if (scope !== "steps") {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
assert(rest.length >= 3, `Invalid step ref '${ref}' at ${path}`);
|
|
151
|
+
const [phaseId, stepId, stepScope] = rest;
|
|
152
|
+
assert(stepScope === "outputs" || stepScope === "value" || stepScope === "status", `Unsupported step ref scope '${stepScope}' at ${path}`);
|
|
153
|
+
const phaseIndex = phases.findIndex((candidate) => candidate.id === phaseId);
|
|
154
|
+
assert(phaseIndex >= 0, `Unknown phase '${phaseId}' in ref '${ref}' at ${path}`);
|
|
155
|
+
const phase = phases[phaseIndex];
|
|
156
|
+
if (!phase) {
|
|
157
|
+
throw new TaskRunnerError(`Unknown phase '${phaseId}' in ref '${ref}' at ${path}`);
|
|
158
|
+
}
|
|
159
|
+
const stepIndex = phase.steps.findIndex((candidate) => candidate.id === stepId);
|
|
160
|
+
assert(stepIndex >= 0, `Unknown step '${stepId}' in ref '${ref}' at ${path}`);
|
|
161
|
+
const isCurrentOrFuturePhase = phaseIndex > currentPhaseIndex || (phaseIndex === currentPhaseIndex && stepIndex > currentStepIndex);
|
|
162
|
+
const isCurrentStep = phaseIndex === currentPhaseIndex && stepIndex === currentStepIndex;
|
|
163
|
+
assert(!isCurrentOrFuturePhase, `Step ref '${ref}' at ${path} must point to a previously completed step`);
|
|
164
|
+
assert(allowCurrentStepRef || !isCurrentStep, `Step ref '${ref}' at ${path} must not point to the current step`);
|
|
165
|
+
}
|
|
166
|
+
function validateExpandedValueSpec(value, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
167
|
+
if ("const" in value) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if ("ref" in value) {
|
|
171
|
+
validateRefPath(value.ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if ("artifact" in value) {
|
|
175
|
+
validateExpandedValueSpec(value.artifact.taskKey, phases, currentPhaseIndex, currentStepIndex, `${path}.artifact.taskKey`, allowCurrentStepRef);
|
|
176
|
+
if (value.artifact.iteration) {
|
|
177
|
+
validateExpandedValueSpec(value.artifact.iteration, phases, currentPhaseIndex, currentStepIndex, `${path}.artifact.iteration`, allowCurrentStepRef);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if ("artifactList" in value) {
|
|
182
|
+
validateExpandedValueSpec(value.artifactList.taskKey, phases, currentPhaseIndex, currentStepIndex, `${path}.artifactList.taskKey`, allowCurrentStepRef);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if ("template" in value) {
|
|
186
|
+
Object.entries(value.vars ?? {}).forEach(([key, candidate]) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.vars.${key}`, allowCurrentStepRef));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if ("appendPrompt" in value) {
|
|
190
|
+
if (value.appendPrompt.base) {
|
|
191
|
+
validateExpandedValueSpec(value.appendPrompt.base, phases, currentPhaseIndex, currentStepIndex, `${path}.appendPrompt.base`, allowCurrentStepRef);
|
|
192
|
+
}
|
|
193
|
+
validateExpandedValueSpec(value.appendPrompt.suffix, phases, currentPhaseIndex, currentStepIndex, `${path}.appendPrompt.suffix`, allowCurrentStepRef);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if ("concat" in value) {
|
|
197
|
+
value.concat.forEach((candidate, index) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.concat[${index}]`, allowCurrentStepRef));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if ("list" in value) {
|
|
201
|
+
value.list.forEach((candidate, index) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.list[${index}]`, allowCurrentStepRef));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function validateExpandedCondition(condition, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
205
|
+
if (!condition) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if ("ref" in condition) {
|
|
209
|
+
validateRefPath(condition.ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if ("not" in condition) {
|
|
213
|
+
validateExpandedCondition(condition.not, phases, currentPhaseIndex, currentStepIndex, `${path}.not`, allowCurrentStepRef);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if ("all" in condition) {
|
|
217
|
+
condition.all.forEach((candidate, index) => validateExpandedCondition(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.all[${index}]`, allowCurrentStepRef));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if ("any" in condition) {
|
|
221
|
+
condition.any.forEach((candidate, index) => validateExpandedCondition(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.any[${index}]`, allowCurrentStepRef));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if ("equals" in condition) {
|
|
225
|
+
validateExpandedValueSpec(condition.equals[0], phases, currentPhaseIndex, currentStepIndex, `${path}.equals[0]`, allowCurrentStepRef);
|
|
226
|
+
validateExpandedValueSpec(condition.equals[1], phases, currentPhaseIndex, currentStepIndex, `${path}.equals[1]`, allowCurrentStepRef);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if ("exists" in condition) {
|
|
230
|
+
validateExpandedValueSpec(condition.exists, phases, currentPhaseIndex, currentStepIndex, `${path}.exists`, allowCurrentStepRef);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export function validateFlowSpec(spec, nodeRegistry) {
|
|
234
|
+
assert(spec.kind.trim().length > 0, "Flow spec kind must be non-empty");
|
|
235
|
+
assert(Number.isInteger(spec.version) && spec.version > 0, "Flow spec version must be a positive integer");
|
|
236
|
+
spec.phases.forEach((item, index) => {
|
|
237
|
+
if ("repeat" in item) {
|
|
238
|
+
assert(item.repeat.var.trim().length > 0, `Repeat var must be non-empty at phases[${index}].repeat.var`);
|
|
239
|
+
assert(item.repeat.to >= item.repeat.from, `Repeat range is invalid at phases[${index}].repeat`);
|
|
240
|
+
item.phases.forEach((phase, phaseIndex) => validatePhase(phase, nodeRegistry, `phases[${index}].phases[${phaseIndex}]`));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
validatePhase(item, nodeRegistry, `phases[${index}]`);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export function validateExpandedPhases(phases) {
|
|
247
|
+
const ids = new Set();
|
|
248
|
+
for (const [phaseIndex, phase] of phases.entries()) {
|
|
249
|
+
if (ids.has(phase.id)) {
|
|
250
|
+
throw new TaskRunnerError(`Duplicate expanded phase id: ${phase.id}`);
|
|
251
|
+
}
|
|
252
|
+
ids.add(phase.id);
|
|
253
|
+
validateExpandedCondition(phase.when, phases, phaseIndex, 0, `phases.${phase.id}.when`);
|
|
254
|
+
const stepIds = new Set();
|
|
255
|
+
for (const [stepIndex, step] of phase.steps.entries()) {
|
|
256
|
+
if (stepIds.has(step.id)) {
|
|
257
|
+
throw new TaskRunnerError(`Duplicate step id '${step.id}' inside phase '${phase.id}'`);
|
|
258
|
+
}
|
|
259
|
+
stepIds.add(step.id);
|
|
260
|
+
validateExpandedCondition(step.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.when`);
|
|
261
|
+
if (step.prompt?.vars) {
|
|
262
|
+
Object.entries(step.prompt.vars).forEach(([key, value]) => validateExpandedValueSpec(value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.prompt.vars.${key}`));
|
|
263
|
+
}
|
|
264
|
+
if (step.prompt?.extraPrompt) {
|
|
265
|
+
validateExpandedValueSpec(step.prompt.extraPrompt, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.prompt.extraPrompt`);
|
|
266
|
+
}
|
|
267
|
+
if (step.params) {
|
|
268
|
+
Object.entries(step.params).forEach(([key, value]) => validateExpandedValueSpec(value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.params.${key}`));
|
|
269
|
+
}
|
|
270
|
+
if (step.expect) {
|
|
271
|
+
step.expect.forEach((expectation, index) => {
|
|
272
|
+
validateExpandedCondition(expectation.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].when`);
|
|
273
|
+
if (expectation.kind === "require-artifacts") {
|
|
274
|
+
validateExpandedValueSpec(expectation.paths, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].paths`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (expectation.kind === "require-structured-artifacts") {
|
|
278
|
+
expectation.items.forEach((item, itemIndex) => {
|
|
279
|
+
validateExpandedValueSpec(item.path, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].items[${itemIndex}].path`);
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (expectation.kind === "require-file") {
|
|
284
|
+
validateExpandedValueSpec(expectation.path, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].path`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
validateExpandedValueSpec(expectation.value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].value`);
|
|
288
|
+
if (expectation.equals) {
|
|
289
|
+
validateExpandedValueSpec(expectation.equals, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].equals`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
validateExpandedCondition(step.stopFlowIf, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.stopFlowIf`, true);
|
|
294
|
+
if (step.after) {
|
|
295
|
+
step.after.forEach((action, index) => {
|
|
296
|
+
validateExpandedCondition(action.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.after[${index}].when`);
|
|
297
|
+
validateExpandedValueSpec(action.path, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.after[${index}].path`);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { artifactFile, bugAnalyzeArtifacts, bugAnalyzeFile, bugAnalyzeJsonFile, bugFixDesignFile, bugFixDesignJsonFile, bugFixPlanFile, bugFixPlanJsonFile, designFile, jiraDescriptionFile, jiraTaskFile, mrDescriptionFile, planArtifacts, planFile, qaFile, readyToMergeFile, taskSummaryFile, } from "../artifacts.js";
|
|
3
|
+
import { TaskRunnerError } from "../errors.js";
|
|
4
|
+
import { formatTemplate } from "../prompts.js";
|
|
5
|
+
function readStepRef(segments, context, originalPath) {
|
|
6
|
+
const [phaseId, stepId, scope, ...rest] = segments;
|
|
7
|
+
if (!phaseId || !stepId || !scope) {
|
|
8
|
+
throw new TaskRunnerError(`Invalid step ref '${originalPath}'`);
|
|
9
|
+
}
|
|
10
|
+
const phase = context.executionState?.phases.find((candidate) => candidate.id === phaseId);
|
|
11
|
+
if (!phase) {
|
|
12
|
+
throw new TaskRunnerError(`Unable to resolve step ref '${originalPath}': unknown phase '${phaseId}'`);
|
|
13
|
+
}
|
|
14
|
+
const step = phase.steps.find((candidate) => candidate.id === stepId);
|
|
15
|
+
if (!step) {
|
|
16
|
+
throw new TaskRunnerError(`Unable to resolve step ref '${originalPath}': unknown step '${stepId}' in phase '${phaseId}'`);
|
|
17
|
+
}
|
|
18
|
+
let current;
|
|
19
|
+
if (scope === "outputs") {
|
|
20
|
+
current = step.outputs;
|
|
21
|
+
}
|
|
22
|
+
else if (scope === "value") {
|
|
23
|
+
current = step.value;
|
|
24
|
+
}
|
|
25
|
+
else if (scope === "status") {
|
|
26
|
+
current = step.status;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new TaskRunnerError(`Unsupported step ref scope in '${originalPath}'`);
|
|
30
|
+
}
|
|
31
|
+
for (const segment of rest) {
|
|
32
|
+
if (!segment) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!current || typeof current !== "object" || !(segment in current)) {
|
|
36
|
+
throw new TaskRunnerError(`Unable to resolve ref '${originalPath}'`);
|
|
37
|
+
}
|
|
38
|
+
current = current[segment];
|
|
39
|
+
}
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
function readRef(path, context) {
|
|
43
|
+
const [scope, ...rest] = path.split(".");
|
|
44
|
+
if (scope === "steps") {
|
|
45
|
+
return readStepRef(rest, context, path);
|
|
46
|
+
}
|
|
47
|
+
const root = scope === "params"
|
|
48
|
+
? context.flowParams
|
|
49
|
+
: scope === "flow"
|
|
50
|
+
? context.flowConstants
|
|
51
|
+
: scope === "context"
|
|
52
|
+
? context.pipelineContext
|
|
53
|
+
: scope === "repeat"
|
|
54
|
+
? context.repeatVars
|
|
55
|
+
: undefined;
|
|
56
|
+
if (root === undefined) {
|
|
57
|
+
throw new TaskRunnerError(`Unsupported ref scope in '${path}'`);
|
|
58
|
+
}
|
|
59
|
+
let current = root;
|
|
60
|
+
for (const segment of rest) {
|
|
61
|
+
if (!segment) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!current || typeof current !== "object" || !(segment in current)) {
|
|
65
|
+
throw new TaskRunnerError(`Unable to resolve ref '${path}'`);
|
|
66
|
+
}
|
|
67
|
+
current = current[segment];
|
|
68
|
+
}
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
function resolveArtifact(spec, context) {
|
|
72
|
+
const taskKey = String(resolveValue(spec.taskKey, context));
|
|
73
|
+
const iteration = spec.iteration === undefined ? undefined : Number(resolveValue(spec.iteration, context));
|
|
74
|
+
switch (spec.kind) {
|
|
75
|
+
case "bug-analyze-file":
|
|
76
|
+
return bugAnalyzeFile(taskKey);
|
|
77
|
+
case "bug-analyze-json-file":
|
|
78
|
+
return bugAnalyzeJsonFile(taskKey);
|
|
79
|
+
case "bug-fix-design-file":
|
|
80
|
+
return bugFixDesignFile(taskKey);
|
|
81
|
+
case "bug-fix-design-json-file":
|
|
82
|
+
return bugFixDesignJsonFile(taskKey);
|
|
83
|
+
case "bug-fix-plan-file":
|
|
84
|
+
return bugFixPlanFile(taskKey);
|
|
85
|
+
case "bug-fix-plan-json-file":
|
|
86
|
+
return bugFixPlanJsonFile(taskKey);
|
|
87
|
+
case "design-file":
|
|
88
|
+
return designFile(taskKey);
|
|
89
|
+
case "jira-description-file":
|
|
90
|
+
return jiraDescriptionFile(taskKey);
|
|
91
|
+
case "jira-task-file":
|
|
92
|
+
return jiraTaskFile(taskKey);
|
|
93
|
+
case "mr-description-file":
|
|
94
|
+
return mrDescriptionFile(taskKey);
|
|
95
|
+
case "plan-file":
|
|
96
|
+
return planFile(taskKey);
|
|
97
|
+
case "qa-file":
|
|
98
|
+
return qaFile(taskKey);
|
|
99
|
+
case "ready-to-merge-file":
|
|
100
|
+
return readyToMergeFile(taskKey);
|
|
101
|
+
case "review-file":
|
|
102
|
+
if (iteration === undefined) {
|
|
103
|
+
throw new TaskRunnerError("review-file requires iteration");
|
|
104
|
+
}
|
|
105
|
+
return artifactFile("review", taskKey, iteration);
|
|
106
|
+
case "review-fix-file":
|
|
107
|
+
if (iteration === undefined) {
|
|
108
|
+
throw new TaskRunnerError("review-fix-file requires iteration");
|
|
109
|
+
}
|
|
110
|
+
return artifactFile("review-fix", taskKey, iteration);
|
|
111
|
+
case "review-reply-file":
|
|
112
|
+
if (iteration === undefined) {
|
|
113
|
+
throw new TaskRunnerError("review-reply-file requires iteration");
|
|
114
|
+
}
|
|
115
|
+
return artifactFile("review-reply", taskKey, iteration);
|
|
116
|
+
case "review-reply-summary-file":
|
|
117
|
+
if (iteration === undefined) {
|
|
118
|
+
throw new TaskRunnerError("review-reply-summary-file requires iteration");
|
|
119
|
+
}
|
|
120
|
+
return artifactFile("review-reply-summary", taskKey, iteration);
|
|
121
|
+
case "review-summary-file":
|
|
122
|
+
if (iteration === undefined) {
|
|
123
|
+
throw new TaskRunnerError("review-summary-file requires iteration");
|
|
124
|
+
}
|
|
125
|
+
return artifactFile("review-summary", taskKey, iteration);
|
|
126
|
+
case "task-summary-file":
|
|
127
|
+
return taskSummaryFile(taskKey);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function resolveArtifactList(spec, context) {
|
|
131
|
+
const taskKey = String(resolveValue(spec.taskKey, context));
|
|
132
|
+
switch (spec.kind) {
|
|
133
|
+
case "bug-analyze-artifacts":
|
|
134
|
+
return bugAnalyzeArtifacts(taskKey);
|
|
135
|
+
case "plan-artifacts":
|
|
136
|
+
return planArtifacts(taskKey);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function resolveValue(value, context) {
|
|
140
|
+
if ("const" in value) {
|
|
141
|
+
return value.const;
|
|
142
|
+
}
|
|
143
|
+
if ("ref" in value) {
|
|
144
|
+
return readRef(value.ref, context);
|
|
145
|
+
}
|
|
146
|
+
if ("artifact" in value) {
|
|
147
|
+
return resolveArtifact(value.artifact, context);
|
|
148
|
+
}
|
|
149
|
+
if ("artifactList" in value) {
|
|
150
|
+
return resolveArtifactList(value.artifactList, context);
|
|
151
|
+
}
|
|
152
|
+
if ("template" in value) {
|
|
153
|
+
const vars = Object.fromEntries(Object.entries(value.vars ?? {}).map(([key, candidate]) => [key, String(resolveValue(candidate, context))]));
|
|
154
|
+
return formatTemplate(value.template, vars);
|
|
155
|
+
}
|
|
156
|
+
if ("appendPrompt" in value) {
|
|
157
|
+
const base = value.appendPrompt.base === undefined ? null : resolveValue(value.appendPrompt.base, context);
|
|
158
|
+
const suffix = resolveValue(value.appendPrompt.suffix, context);
|
|
159
|
+
const baseText = base === null || base === undefined ? "" : String(base).trim();
|
|
160
|
+
const suffixText = String(suffix).trim();
|
|
161
|
+
if (!baseText) {
|
|
162
|
+
return suffixText;
|
|
163
|
+
}
|
|
164
|
+
if (!suffixText) {
|
|
165
|
+
return baseText;
|
|
166
|
+
}
|
|
167
|
+
return `${baseText}\n${suffixText}`;
|
|
168
|
+
}
|
|
169
|
+
if ("concat" in value) {
|
|
170
|
+
return value.concat
|
|
171
|
+
.map((candidate) => resolveValue(candidate, context))
|
|
172
|
+
.filter((chunk) => chunk !== null && chunk !== undefined)
|
|
173
|
+
.map((chunk) => String(chunk))
|
|
174
|
+
.join("");
|
|
175
|
+
}
|
|
176
|
+
if ("list" in value) {
|
|
177
|
+
return value.list.map((candidate) => resolveValue(candidate, context));
|
|
178
|
+
}
|
|
179
|
+
throw new TaskRunnerError("Unsupported value spec");
|
|
180
|
+
}
|
|
181
|
+
export function resolveParams(params, context) {
|
|
182
|
+
if (!params) {
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, resolveValue(value, context)]));
|
|
186
|
+
}
|
|
187
|
+
function truthy(value) {
|
|
188
|
+
return Boolean(value);
|
|
189
|
+
}
|
|
190
|
+
export function evaluateCondition(condition, context) {
|
|
191
|
+
if (!condition) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if ("ref" in condition) {
|
|
195
|
+
return truthy(readRef(condition.ref, context));
|
|
196
|
+
}
|
|
197
|
+
if ("not" in condition) {
|
|
198
|
+
return !evaluateCondition(condition.not, context);
|
|
199
|
+
}
|
|
200
|
+
if ("all" in condition) {
|
|
201
|
+
return condition.all.every((candidate) => evaluateCondition(candidate, context));
|
|
202
|
+
}
|
|
203
|
+
if ("any" in condition) {
|
|
204
|
+
return condition.any.some((candidate) => evaluateCondition(candidate, context));
|
|
205
|
+
}
|
|
206
|
+
if ("equals" in condition) {
|
|
207
|
+
return resolveValue(condition.equals[0], context) === resolveValue(condition.equals[1], context);
|
|
208
|
+
}
|
|
209
|
+
if ("exists" in condition) {
|
|
210
|
+
const value = resolveValue(condition.exists, context);
|
|
211
|
+
if (typeof value !== "string") {
|
|
212
|
+
throw new TaskRunnerError("exists condition requires string path");
|
|
213
|
+
}
|
|
214
|
+
return existsSync(value);
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -4,9 +4,26 @@ export const PLAN_PROMPT_TEMPLATE = "Посмотри и проанализир
|
|
|
4
4
|
"Разработай системный дизайн решения, запиши в {design_file}. " +
|
|
5
5
|
"Разработай подробный план реализации и запиши его в {plan_file}. " +
|
|
6
6
|
"Разработай план тестирования для QA и запиши в {qa_file}. ";
|
|
7
|
-
export const
|
|
8
|
-
"
|
|
9
|
-
"
|
|
7
|
+
export const BUG_ANALYZE_PROMPT_TEMPLATE = "Посмотри и проанализируй баг в {jira_task_file}. " +
|
|
8
|
+
"Сначала создай структурированные JSON-артефакты, они являются source of truth для следующих flow. " +
|
|
9
|
+
"Человекочитаемые markdown-файлы сделай как краткое производное представление этих JSON-артефактов для пользователя. " +
|
|
10
|
+
"Запиши структурированный анализ бага в {bug_analyze_json_file}, затем краткую markdown-версию в {bug_analyze_file}. " +
|
|
11
|
+
"Запиши структурированный дизайн исправления в {bug_fix_design_json_file}, затем краткую markdown-версию в {bug_fix_design_file}. " +
|
|
12
|
+
"Запиши структурированный план реализации в {bug_fix_plan_json_file}, затем краткую markdown-версию в {bug_fix_plan_file}. " +
|
|
13
|
+
"JSON-файлы должны быть валидными и содержать только JSON без markdown-обёртки. " +
|
|
14
|
+
"Для {bug_analyze_json_file} используй объект: { summary: string, suspected_root_cause: { hypothesis: string, confidence: string }, reproduction_steps: string[], affected_components: string[], evidence: string[], risks: string[], open_questions: string[] }. " +
|
|
15
|
+
"Для {bug_fix_design_json_file} используй объект: { summary: string, goals: string[], non_goals: string[], target_components: string[], proposed_changes: [{ component: string, change: string, rationale: string }], alternatives_considered: [{ option: string, decision: string, rationale: string }], risks: string[], validation_strategy: string[] }. " +
|
|
16
|
+
"Для {bug_fix_plan_json_file} используй объект: { summary: string, prerequisites: string[], implementation_steps: [{ id: string, title: string, details: string }], tests: string[], rollout_notes: string[] }. ";
|
|
17
|
+
export const BUG_FIX_PROMPT_TEMPLATE = "Используй только структурированные артефакты как source of truth. " +
|
|
18
|
+
"Проанализируй баг по {bug_analyze_json_file}. " +
|
|
19
|
+
"Используй дизайн исправления из {bug_fix_design_json_file}. " +
|
|
20
|
+
"Используй план реализации из {bug_fix_plan_json_file}. " +
|
|
21
|
+
"Markdown-артефакты предназначены только для чтения человеком и не должны определять реализацию. " +
|
|
22
|
+
"После этого приступай к реализации исправления в коде. ";
|
|
23
|
+
export const MR_DESCRIPTION_PROMPT_TEMPLATE = "Посмотри задачу в {jira_task_file} и текущие изменения в репозитории. " +
|
|
24
|
+
"Подготовь очень краткое intent-описание для merge request без подробностей реализации, списков файлов и технических деталей. " +
|
|
25
|
+
"Сфокусируйся на том, что меняется и зачем. Запиши результат в {mr_description_file}. ";
|
|
26
|
+
export const IMPLEMENT_PROMPT_TEMPLATE = "Проанализируй системный дизайн {design_file}, план реализации {plan_file} и приступай к реализации по плану. ";
|
|
10
27
|
export const REVIEW_PROMPT_TEMPLATE = "Проведи код-ревью текущих изменений. " +
|
|
11
28
|
"Сверься с задачей в {jira_task_file}, дизайном {design_file} и планом {plan_file}. " +
|
|
12
29
|
"Замечания и комментарии запиши в {review_file}. " +
|
|
@@ -30,6 +47,8 @@ export const TASK_SUMMARY_PROMPT_TEMPLATE = "Посмотри в {jira_task_file
|
|
|
30
47
|
"запиши в {task_summary_file}.";
|
|
31
48
|
export const TEST_FIX_PROMPT_TEMPLATE = "Прогони тесты, исправь ошибки.";
|
|
32
49
|
export const TEST_LINTER_FIX_PROMPT_TEMPLATE = "Прогони линтер, исправь замечания.";
|
|
50
|
+
export const RUN_TESTS_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_tests.sh, проанализируй последнюю ошибку проверки, исправь код и подготовь изменения так, чтобы следующий прогон run_tests.sh прошёл успешно.";
|
|
51
|
+
export const RUN_LINTER_LOOP_FIX_PROMPT_TEMPLATE = "Запусти ./run_linter.sh, проанализируй последнюю ошибку линтера или генерации, исправь код и подготовь изменения так, чтобы следующий прогон run_linter.sh прошёл успешно.";
|
|
33
52
|
export const AUTO_REVIEW_FIX_EXTRA_PROMPT = "Исправлять только блокеры, критикалы и важные";
|
|
34
53
|
export function formatTemplate(template, values) {
|
|
35
54
|
let result = template;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { getExecutionState, getOutputAdapter, printFramedBlock, printInfo, setCurrentExecutor } from "../tui.js";
|
|
5
5
|
import { shellQuote } from "./command-resolution.js";
|
|
6
6
|
export function formatCommand(argv, env) {
|
|
7
7
|
const envParts = Object.entries(env ?? {})
|
|
@@ -16,6 +16,23 @@ export function formatDuration(ms) {
|
|
|
16
16
|
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
17
17
|
return `${minutes}:${seconds}`;
|
|
18
18
|
}
|
|
19
|
+
function formatLaunchDetails(statusLabel) {
|
|
20
|
+
const state = getExecutionState();
|
|
21
|
+
const lines = [];
|
|
22
|
+
if (state.node) {
|
|
23
|
+
lines.push(`Node: ${state.node}`);
|
|
24
|
+
}
|
|
25
|
+
const executorLabel = state.executor ?? statusLabel;
|
|
26
|
+
const separatorIndex = executorLabel.indexOf(":");
|
|
27
|
+
if (separatorIndex >= 0) {
|
|
28
|
+
lines.push(`Executor: ${executorLabel.slice(0, separatorIndex)}`);
|
|
29
|
+
lines.push(`Model: ${executorLabel.slice(separatorIndex + 1)}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
lines.push(`Executor: ${executorLabel}`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
19
36
|
export async function runCommand(argv, options = {}) {
|
|
20
37
|
const { env, dryRun = false, verbose = false, label, printFailureOutput = true } = options;
|
|
21
38
|
const outputAdapter = getOutputAdapter();
|
|
@@ -44,8 +61,6 @@ export async function runCommand(argv, options = {}) {
|
|
|
44
61
|
}
|
|
45
62
|
const startedAt = Date.now();
|
|
46
63
|
const statusLabel = label ?? path.basename(argv[0] ?? argv.join(" "));
|
|
47
|
-
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
48
|
-
let frameIndex = 0;
|
|
49
64
|
let output = "";
|
|
50
65
|
const child = spawn(argv[0] ?? "", argv.slice(1), {
|
|
51
66
|
env,
|
|
@@ -55,38 +70,27 @@ export async function runCommand(argv, options = {}) {
|
|
|
55
70
|
child.stdout?.on("data", (chunk) => {
|
|
56
71
|
const text = String(chunk);
|
|
57
72
|
output += text;
|
|
58
|
-
if (
|
|
73
|
+
if (verbose) {
|
|
59
74
|
outputAdapter.writeStdout(text);
|
|
60
75
|
}
|
|
61
76
|
});
|
|
62
77
|
child.stderr?.on("data", (chunk) => {
|
|
63
78
|
const text = String(chunk);
|
|
64
79
|
output += text;
|
|
65
|
-
if (
|
|
80
|
+
if (verbose) {
|
|
66
81
|
outputAdapter.writeStderr(text);
|
|
67
82
|
}
|
|
68
83
|
});
|
|
69
|
-
if (
|
|
70
|
-
|
|
84
|
+
if (outputAdapter.renderAuxiliaryOutput !== false) {
|
|
85
|
+
printFramedBlock("Запуск", formatLaunchDetails(statusLabel), "cyan");
|
|
71
86
|
}
|
|
72
|
-
const timer = outputAdapter.supportsTransientStatus
|
|
73
|
-
? setInterval(() => {
|
|
74
|
-
const elapsed = formatDuration(Date.now() - startedAt);
|
|
75
|
-
process.stdout.write(`\r${frames[frameIndex]} ${statusLabel} ${dim(elapsed)}`);
|
|
76
|
-
frameIndex = (frameIndex + 1) % frames.length;
|
|
77
|
-
}, 200)
|
|
78
|
-
: null;
|
|
79
87
|
try {
|
|
80
88
|
const exitCode = await new Promise((resolve, reject) => {
|
|
81
89
|
child.on("error", reject);
|
|
82
90
|
child.on("exit", (code) => resolve(code ?? 1));
|
|
83
91
|
});
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
process.stdout.write(`\r${" ".repeat(80)}\r${formatDone(formatDuration(Date.now() - startedAt))}\n`);
|
|
87
|
-
}
|
|
88
|
-
else if (outputAdapter.renderAuxiliaryOutput !== false) {
|
|
89
|
-
outputAdapter.writeStdout(`Done ${formatDuration(Date.now() - startedAt)}\n`);
|
|
92
|
+
if (outputAdapter.renderAuxiliaryOutput !== false) {
|
|
93
|
+
printInfo(`Закончили работу: ${statusLabel} (${formatDuration(Date.now() - startedAt)})`);
|
|
90
94
|
}
|
|
91
95
|
if (exitCode !== 0) {
|
|
92
96
|
if (output && printFailureOutput && outputAdapter.supportsTransientStatus) {
|
|
@@ -104,8 +108,5 @@ export async function runCommand(argv, options = {}) {
|
|
|
104
108
|
}
|
|
105
109
|
finally {
|
|
106
110
|
setCurrentExecutor(null);
|
|
107
|
-
if (timer) {
|
|
108
|
-
clearInterval(timer);
|
|
109
|
-
}
|
|
110
111
|
}
|
|
111
112
|
}
|