agentweaver 0.1.2 → 0.1.3
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 +11 -10
- package/dist/artifacts.js +24 -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/index.js +388 -451
- package/dist/interactive-ui.js +451 -194
- package/dist/jira.js +3 -1
- package/dist/pipeline/auto-flow.js +9 -0
- package/dist/pipeline/context.js +2 -0
- package/dist/pipeline/declarative-flow-runner.js +246 -0
- package/dist/pipeline/declarative-flows.js +24 -0
- package/dist/pipeline/flow-specs/auto.json +471 -0
- package/dist/pipeline/flow-specs/implement.json +47 -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/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 +71 -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/summary-file-load-node.js +16 -0
- package/dist/pipeline/nodes/task-summary-node.js +12 -6
- package/dist/pipeline/prompt-registry.js +22 -0
- package/dist/pipeline/prompt-runtime.js +18 -0
- package/dist/pipeline/registry.js +0 -2
- package/dist/pipeline/spec-compiler.js +200 -0
- package/dist/pipeline/spec-loader.js +14 -0
- package/dist/pipeline/spec-types.js +1 -0
- package/dist/pipeline/spec-validator.js +290 -0
- package/dist/pipeline/value-resolver.js +199 -0
- package/dist/prompts.js +1 -3
- package/dist/runtime/process-runner.js +24 -23
- package/dist/tui.js +39 -0
- package/package.json +2 -2
|
@@ -0,0 +1,290 @@
|
|
|
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-file") {
|
|
112
|
+
validateValueSpec(expectation.path, `${path}.path`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (expectation.kind === "step-output") {
|
|
116
|
+
validateValueSpec(expectation.value, `${path}.value`);
|
|
117
|
+
if (expectation.equals) {
|
|
118
|
+
validateValueSpec(expectation.equals, `${path}.equals`);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw new TaskRunnerError(`Unsupported expectation at ${path}`);
|
|
123
|
+
}
|
|
124
|
+
function validateAfterAction(action, path) {
|
|
125
|
+
validateCondition(action.when, `${path}.when`);
|
|
126
|
+
if (action.kind === "set-summary-from-file") {
|
|
127
|
+
validateValueSpec(action.path, `${path}.path`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw new TaskRunnerError(`Unsupported after action at ${path}`);
|
|
131
|
+
}
|
|
132
|
+
function validatePhase(phase, nodeRegistry, path) {
|
|
133
|
+
assert(phase.id.trim().length > 0, `Phase id must be non-empty at ${path}.id`);
|
|
134
|
+
validateCondition(phase.when, `${path}.when`);
|
|
135
|
+
phase.steps.forEach((step, index) => validateStep(step, nodeRegistry, `${path}.steps[${index}]`));
|
|
136
|
+
}
|
|
137
|
+
function validateRefPath(ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
138
|
+
const [scope, ...rest] = ref.split(".");
|
|
139
|
+
const supportedScopes = new Set(["params", "flow", "context", "repeat", "steps"]);
|
|
140
|
+
assert(supportedScopes.has(scope ?? ""), `Unsupported ref scope '${scope ?? ""}' at ${path}`);
|
|
141
|
+
if (scope !== "steps") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
assert(rest.length >= 3, `Invalid step ref '${ref}' at ${path}`);
|
|
145
|
+
const [phaseId, stepId, stepScope] = rest;
|
|
146
|
+
assert(stepScope === "outputs" || stepScope === "value" || stepScope === "status", `Unsupported step ref scope '${stepScope}' at ${path}`);
|
|
147
|
+
const phaseIndex = phases.findIndex((candidate) => candidate.id === phaseId);
|
|
148
|
+
assert(phaseIndex >= 0, `Unknown phase '${phaseId}' in ref '${ref}' at ${path}`);
|
|
149
|
+
const phase = phases[phaseIndex];
|
|
150
|
+
if (!phase) {
|
|
151
|
+
throw new TaskRunnerError(`Unknown phase '${phaseId}' in ref '${ref}' at ${path}`);
|
|
152
|
+
}
|
|
153
|
+
const stepIndex = phase.steps.findIndex((candidate) => candidate.id === stepId);
|
|
154
|
+
assert(stepIndex >= 0, `Unknown step '${stepId}' in ref '${ref}' at ${path}`);
|
|
155
|
+
const isCurrentOrFuturePhase = phaseIndex > currentPhaseIndex || (phaseIndex === currentPhaseIndex && stepIndex > currentStepIndex);
|
|
156
|
+
const isCurrentStep = phaseIndex === currentPhaseIndex && stepIndex === currentStepIndex;
|
|
157
|
+
assert(!isCurrentOrFuturePhase, `Step ref '${ref}' at ${path} must point to a previously completed step`);
|
|
158
|
+
assert(allowCurrentStepRef || !isCurrentStep, `Step ref '${ref}' at ${path} must not point to the current step`);
|
|
159
|
+
}
|
|
160
|
+
function validateExpandedValueSpec(value, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
161
|
+
if ("const" in value) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if ("ref" in value) {
|
|
165
|
+
validateRefPath(value.ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if ("artifact" in value) {
|
|
169
|
+
validateExpandedValueSpec(value.artifact.taskKey, phases, currentPhaseIndex, currentStepIndex, `${path}.artifact.taskKey`, allowCurrentStepRef);
|
|
170
|
+
if (value.artifact.iteration) {
|
|
171
|
+
validateExpandedValueSpec(value.artifact.iteration, phases, currentPhaseIndex, currentStepIndex, `${path}.artifact.iteration`, allowCurrentStepRef);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if ("artifactList" in value) {
|
|
176
|
+
validateExpandedValueSpec(value.artifactList.taskKey, phases, currentPhaseIndex, currentStepIndex, `${path}.artifactList.taskKey`, allowCurrentStepRef);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if ("template" in value) {
|
|
180
|
+
Object.entries(value.vars ?? {}).forEach(([key, candidate]) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.vars.${key}`, allowCurrentStepRef));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if ("appendPrompt" in value) {
|
|
184
|
+
if (value.appendPrompt.base) {
|
|
185
|
+
validateExpandedValueSpec(value.appendPrompt.base, phases, currentPhaseIndex, currentStepIndex, `${path}.appendPrompt.base`, allowCurrentStepRef);
|
|
186
|
+
}
|
|
187
|
+
validateExpandedValueSpec(value.appendPrompt.suffix, phases, currentPhaseIndex, currentStepIndex, `${path}.appendPrompt.suffix`, allowCurrentStepRef);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if ("concat" in value) {
|
|
191
|
+
value.concat.forEach((candidate, index) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.concat[${index}]`, allowCurrentStepRef));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if ("list" in value) {
|
|
195
|
+
value.list.forEach((candidate, index) => validateExpandedValueSpec(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.list[${index}]`, allowCurrentStepRef));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function validateExpandedCondition(condition, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef = false) {
|
|
199
|
+
if (!condition) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if ("ref" in condition) {
|
|
203
|
+
validateRefPath(condition.ref, phases, currentPhaseIndex, currentStepIndex, path, allowCurrentStepRef);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if ("not" in condition) {
|
|
207
|
+
validateExpandedCondition(condition.not, phases, currentPhaseIndex, currentStepIndex, `${path}.not`, allowCurrentStepRef);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if ("all" in condition) {
|
|
211
|
+
condition.all.forEach((candidate, index) => validateExpandedCondition(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.all[${index}]`, allowCurrentStepRef));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if ("any" in condition) {
|
|
215
|
+
condition.any.forEach((candidate, index) => validateExpandedCondition(candidate, phases, currentPhaseIndex, currentStepIndex, `${path}.any[${index}]`, allowCurrentStepRef));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if ("equals" in condition) {
|
|
219
|
+
validateExpandedValueSpec(condition.equals[0], phases, currentPhaseIndex, currentStepIndex, `${path}.equals[0]`, allowCurrentStepRef);
|
|
220
|
+
validateExpandedValueSpec(condition.equals[1], phases, currentPhaseIndex, currentStepIndex, `${path}.equals[1]`, allowCurrentStepRef);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if ("exists" in condition) {
|
|
224
|
+
validateExpandedValueSpec(condition.exists, phases, currentPhaseIndex, currentStepIndex, `${path}.exists`, allowCurrentStepRef);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function validateFlowSpec(spec, nodeRegistry) {
|
|
228
|
+
assert(spec.kind.trim().length > 0, "Flow spec kind must be non-empty");
|
|
229
|
+
assert(Number.isInteger(spec.version) && spec.version > 0, "Flow spec version must be a positive integer");
|
|
230
|
+
spec.phases.forEach((item, index) => {
|
|
231
|
+
if ("repeat" in item) {
|
|
232
|
+
assert(item.repeat.var.trim().length > 0, `Repeat var must be non-empty at phases[${index}].repeat.var`);
|
|
233
|
+
assert(item.repeat.to >= item.repeat.from, `Repeat range is invalid at phases[${index}].repeat`);
|
|
234
|
+
item.phases.forEach((phase, phaseIndex) => validatePhase(phase, nodeRegistry, `phases[${index}].phases[${phaseIndex}]`));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
validatePhase(item, nodeRegistry, `phases[${index}]`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
export function validateExpandedPhases(phases) {
|
|
241
|
+
const ids = new Set();
|
|
242
|
+
for (const [phaseIndex, phase] of phases.entries()) {
|
|
243
|
+
if (ids.has(phase.id)) {
|
|
244
|
+
throw new TaskRunnerError(`Duplicate expanded phase id: ${phase.id}`);
|
|
245
|
+
}
|
|
246
|
+
ids.add(phase.id);
|
|
247
|
+
validateExpandedCondition(phase.when, phases, phaseIndex, 0, `phases.${phase.id}.when`);
|
|
248
|
+
const stepIds = new Set();
|
|
249
|
+
for (const [stepIndex, step] of phase.steps.entries()) {
|
|
250
|
+
if (stepIds.has(step.id)) {
|
|
251
|
+
throw new TaskRunnerError(`Duplicate step id '${step.id}' inside phase '${phase.id}'`);
|
|
252
|
+
}
|
|
253
|
+
stepIds.add(step.id);
|
|
254
|
+
validateExpandedCondition(step.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.when`);
|
|
255
|
+
if (step.prompt?.vars) {
|
|
256
|
+
Object.entries(step.prompt.vars).forEach(([key, value]) => validateExpandedValueSpec(value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.prompt.vars.${key}`));
|
|
257
|
+
}
|
|
258
|
+
if (step.prompt?.extraPrompt) {
|
|
259
|
+
validateExpandedValueSpec(step.prompt.extraPrompt, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.prompt.extraPrompt`);
|
|
260
|
+
}
|
|
261
|
+
if (step.params) {
|
|
262
|
+
Object.entries(step.params).forEach(([key, value]) => validateExpandedValueSpec(value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.params.${key}`));
|
|
263
|
+
}
|
|
264
|
+
if (step.expect) {
|
|
265
|
+
step.expect.forEach((expectation, index) => {
|
|
266
|
+
validateExpandedCondition(expectation.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].when`);
|
|
267
|
+
if (expectation.kind === "require-artifacts") {
|
|
268
|
+
validateExpandedValueSpec(expectation.paths, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].paths`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (expectation.kind === "require-file") {
|
|
272
|
+
validateExpandedValueSpec(expectation.path, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].path`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
validateExpandedValueSpec(expectation.value, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].value`);
|
|
276
|
+
if (expectation.equals) {
|
|
277
|
+
validateExpandedValueSpec(expectation.equals, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.expect[${index}].equals`);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
validateExpandedCondition(step.stopFlowIf, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.stopFlowIf`, true);
|
|
282
|
+
if (step.after) {
|
|
283
|
+
step.after.forEach((action, index) => {
|
|
284
|
+
validateExpandedCondition(action.when, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.after[${index}].when`);
|
|
285
|
+
validateExpandedValueSpec(action.path, phases, phaseIndex, stepIndex, `phases.${phase.id}.steps.${step.id}.after[${index}].path`);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { artifactFile, designFile, jiraTaskFile, 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 "design-file":
|
|
76
|
+
return designFile(taskKey);
|
|
77
|
+
case "jira-task-file":
|
|
78
|
+
return jiraTaskFile(taskKey);
|
|
79
|
+
case "plan-file":
|
|
80
|
+
return planFile(taskKey);
|
|
81
|
+
case "qa-file":
|
|
82
|
+
return qaFile(taskKey);
|
|
83
|
+
case "ready-to-merge-file":
|
|
84
|
+
return readyToMergeFile(taskKey);
|
|
85
|
+
case "review-file":
|
|
86
|
+
if (iteration === undefined) {
|
|
87
|
+
throw new TaskRunnerError("review-file requires iteration");
|
|
88
|
+
}
|
|
89
|
+
return artifactFile("review", taskKey, iteration);
|
|
90
|
+
case "review-fix-file":
|
|
91
|
+
if (iteration === undefined) {
|
|
92
|
+
throw new TaskRunnerError("review-fix-file requires iteration");
|
|
93
|
+
}
|
|
94
|
+
return artifactFile("review-fix", taskKey, iteration);
|
|
95
|
+
case "review-reply-file":
|
|
96
|
+
if (iteration === undefined) {
|
|
97
|
+
throw new TaskRunnerError("review-reply-file requires iteration");
|
|
98
|
+
}
|
|
99
|
+
return artifactFile("review-reply", taskKey, iteration);
|
|
100
|
+
case "review-reply-summary-file":
|
|
101
|
+
if (iteration === undefined) {
|
|
102
|
+
throw new TaskRunnerError("review-reply-summary-file requires iteration");
|
|
103
|
+
}
|
|
104
|
+
return artifactFile("review-reply-summary", taskKey, iteration);
|
|
105
|
+
case "review-summary-file":
|
|
106
|
+
if (iteration === undefined) {
|
|
107
|
+
throw new TaskRunnerError("review-summary-file requires iteration");
|
|
108
|
+
}
|
|
109
|
+
return artifactFile("review-summary", taskKey, iteration);
|
|
110
|
+
case "task-summary-file":
|
|
111
|
+
return taskSummaryFile(taskKey);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function resolveArtifactList(spec, context) {
|
|
115
|
+
const taskKey = String(resolveValue(spec.taskKey, context));
|
|
116
|
+
switch (spec.kind) {
|
|
117
|
+
case "plan-artifacts":
|
|
118
|
+
return planArtifacts(taskKey);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function resolveValue(value, context) {
|
|
122
|
+
if ("const" in value) {
|
|
123
|
+
return value.const;
|
|
124
|
+
}
|
|
125
|
+
if ("ref" in value) {
|
|
126
|
+
return readRef(value.ref, context);
|
|
127
|
+
}
|
|
128
|
+
if ("artifact" in value) {
|
|
129
|
+
return resolveArtifact(value.artifact, context);
|
|
130
|
+
}
|
|
131
|
+
if ("artifactList" in value) {
|
|
132
|
+
return resolveArtifactList(value.artifactList, context);
|
|
133
|
+
}
|
|
134
|
+
if ("template" in value) {
|
|
135
|
+
const vars = Object.fromEntries(Object.entries(value.vars ?? {}).map(([key, candidate]) => [key, String(resolveValue(candidate, context))]));
|
|
136
|
+
return formatTemplate(value.template, vars);
|
|
137
|
+
}
|
|
138
|
+
if ("appendPrompt" in value) {
|
|
139
|
+
const base = value.appendPrompt.base === undefined ? null : resolveValue(value.appendPrompt.base, context);
|
|
140
|
+
const suffix = resolveValue(value.appendPrompt.suffix, context);
|
|
141
|
+
const baseText = base === null || base === undefined ? "" : String(base).trim();
|
|
142
|
+
const suffixText = String(suffix).trim();
|
|
143
|
+
if (!baseText) {
|
|
144
|
+
return suffixText;
|
|
145
|
+
}
|
|
146
|
+
if (!suffixText) {
|
|
147
|
+
return baseText;
|
|
148
|
+
}
|
|
149
|
+
return `${baseText}\n${suffixText}`;
|
|
150
|
+
}
|
|
151
|
+
if ("concat" in value) {
|
|
152
|
+
return value.concat
|
|
153
|
+
.map((candidate) => resolveValue(candidate, context))
|
|
154
|
+
.filter((chunk) => chunk !== null && chunk !== undefined)
|
|
155
|
+
.map((chunk) => String(chunk))
|
|
156
|
+
.join("");
|
|
157
|
+
}
|
|
158
|
+
if ("list" in value) {
|
|
159
|
+
return value.list.map((candidate) => resolveValue(candidate, context));
|
|
160
|
+
}
|
|
161
|
+
throw new TaskRunnerError("Unsupported value spec");
|
|
162
|
+
}
|
|
163
|
+
export function resolveParams(params, context) {
|
|
164
|
+
if (!params) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, resolveValue(value, context)]));
|
|
168
|
+
}
|
|
169
|
+
function truthy(value) {
|
|
170
|
+
return Boolean(value);
|
|
171
|
+
}
|
|
172
|
+
export function evaluateCondition(condition, context) {
|
|
173
|
+
if (!condition) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if ("ref" in condition) {
|
|
177
|
+
return truthy(readRef(condition.ref, context));
|
|
178
|
+
}
|
|
179
|
+
if ("not" in condition) {
|
|
180
|
+
return !evaluateCondition(condition.not, context);
|
|
181
|
+
}
|
|
182
|
+
if ("all" in condition) {
|
|
183
|
+
return condition.all.every((candidate) => evaluateCondition(candidate, context));
|
|
184
|
+
}
|
|
185
|
+
if ("any" in condition) {
|
|
186
|
+
return condition.any.some((candidate) => evaluateCondition(candidate, context));
|
|
187
|
+
}
|
|
188
|
+
if ("equals" in condition) {
|
|
189
|
+
return resolveValue(condition.equals[0], context) === resolveValue(condition.equals[1], context);
|
|
190
|
+
}
|
|
191
|
+
if ("exists" in condition) {
|
|
192
|
+
const value = resolveValue(condition.exists, context);
|
|
193
|
+
if (typeof value !== "string") {
|
|
194
|
+
throw new TaskRunnerError("exists condition requires string path");
|
|
195
|
+
}
|
|
196
|
+
return existsSync(value);
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -4,9 +4,7 @@ export const PLAN_PROMPT_TEMPLATE = "Посмотри и проанализир
|
|
|
4
4
|
"Разработай системный дизайн решения, запиши в {design_file}. " +
|
|
5
5
|
"Разработай подробный план реализации и запиши его в {plan_file}. " +
|
|
6
6
|
"Разработай план тестирования для QA и запиши в {qa_file}. ";
|
|
7
|
-
export const IMPLEMENT_PROMPT_TEMPLATE = "Проанализируй системный дизайн {design_file}, план реализации {plan_file} и приступай к реализации по плану. "
|
|
8
|
-
"По окончании обязательно прогони вне песочницы линтер, все тесты, сгенерируй make swagger. " +
|
|
9
|
-
"Исправь ошибки линтера и тестов, если будут.";
|
|
7
|
+
export const IMPLEMENT_PROMPT_TEMPLATE = "Проанализируй системный дизайн {design_file}, план реализации {plan_file} и приступай к реализации по плану. ";
|
|
10
8
|
export const REVIEW_PROMPT_TEMPLATE = "Проведи код-ревью текущих изменений. " +
|
|
11
9
|
"Сверься с задачей в {jira_task_file}, дизайном {design_file} и планом {plan_file}. " +
|
|
12
10
|
"Замечания и комментарии запиши в {review_file}. " +
|
|
@@ -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
|
}
|