agentweaver 0.1.18 → 0.1.20

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.
Files changed (50) hide show
  1. package/README.md +54 -6
  2. package/dist/artifacts.js +9 -0
  3. package/dist/executors/git-commit-executor.js +24 -6
  4. package/dist/flow-state.js +3 -8
  5. package/dist/git/git-diff-parser.js +223 -0
  6. package/dist/git/git-service.js +562 -0
  7. package/dist/git/git-stage-selection.js +24 -0
  8. package/dist/git/git-status-parser.js +171 -0
  9. package/dist/git/git-types.js +1 -0
  10. package/dist/index.js +454 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +489 -7
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +34 -0
  15. package/dist/interactive/web/index.js +237 -5
  16. package/dist/interactive/web/protocol.js +222 -1
  17. package/dist/interactive/web/server.js +497 -3
  18. package/dist/interactive/web/static/app.js +2462 -37
  19. package/dist/interactive/web/static/index.html +113 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +1383 -149
  22. package/dist/pipeline/auto-flow-blocks.js +307 -0
  23. package/dist/pipeline/auto-flow-config.js +273 -0
  24. package/dist/pipeline/auto-flow-identity.js +49 -0
  25. package/dist/pipeline/auto-flow-presets.js +52 -0
  26. package/dist/pipeline/auto-flow-resolver.js +830 -0
  27. package/dist/pipeline/auto-flow-types.js +17 -0
  28. package/dist/pipeline/context.js +1 -0
  29. package/dist/pipeline/declarative-flows.js +27 -1
  30. package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
  31. package/dist/pipeline/flow-specs/auto-golang.json +12 -1
  32. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
  35. package/dist/pipeline/flow-specs/review/review-project.json +19 -1
  36. package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
  37. package/dist/pipeline/node-registry.js +9 -0
  38. package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
  39. package/dist/pipeline/nodes/flow-run-node.js +5 -3
  40. package/dist/pipeline/nodes/git-status-node.js +2 -168
  41. package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
  42. package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
  43. package/dist/pipeline/nodes/plan-codex-node.js +8 -1
  44. package/dist/pipeline/spec-loader.js +14 -4
  45. package/dist/runtime/artifact-catalog.js +403 -0
  46. package/dist/runtime/settings.js +114 -0
  47. package/dist/scope.js +14 -4
  48. package/package.json +1 -1
  49. package/dist/pipeline/flow-specs/auto-common.json +0 -179
  50. package/dist/pipeline/flow-specs/auto-simple.json +0 -141
@@ -0,0 +1,307 @@
1
+ const SOURCE_SLOT = ["source"];
2
+ const NORMALIZE_SLOT = ["normalize"];
3
+ const PLANNING_SLOT = ["planning"];
4
+ const DESIGN_REVIEW_SLOT = ["designReview"];
5
+ const IMPLEMENTATION_SLOT = ["implementation"];
6
+ const REVIEW_SLOT = ["review"];
7
+ const ref = (value) => ({ ref: value });
8
+ const constant = (value) => ({ const: value });
9
+ function sourcePhase() {
10
+ return {
11
+ id: "source",
12
+ steps: [
13
+ {
14
+ id: "fetch_jira_source",
15
+ when: { ref: "params.jiraApiUrl" },
16
+ node: "flow-run",
17
+ params: {
18
+ fileName: constant("jira-fetch.json"),
19
+ labelText: constant("Fetching Jira task source"),
20
+ jiraApiUrl: ref("params.jiraApiUrl"),
21
+ taskKey: ref("params.taskKey"),
22
+ },
23
+ },
24
+ {
25
+ id: "collect_manual_source",
26
+ when: { not: { ref: "params.jiraApiUrl" } },
27
+ node: "flow-run",
28
+ params: {
29
+ fileName: constant("manual-jira-input.json"),
30
+ labelText: constant("Collecting manual Jira task source"),
31
+ taskKey: ref("params.taskKey"),
32
+ },
33
+ },
34
+ ],
35
+ };
36
+ }
37
+ function normalizePhase() {
38
+ return {
39
+ id: "normalize",
40
+ steps: [
41
+ {
42
+ id: "run_normalize_source",
43
+ node: "flow-run",
44
+ params: {
45
+ fileName: constant("normalize-task-source.json"),
46
+ labelText: constant("Normalizing task source"),
47
+ taskKey: ref("params.taskKey"),
48
+ iteration: ref("params.taskContextIteration"),
49
+ llmExecutor: ref("params.llmExecutor"),
50
+ llmModel: ref("params.llmModel"),
51
+ extraPrompt: ref("params.extraPrompt"),
52
+ },
53
+ },
54
+ ],
55
+ };
56
+ }
57
+ function planningPhase() {
58
+ return {
59
+ id: "plan",
60
+ steps: [
61
+ {
62
+ id: "run_plan_flow",
63
+ node: "flow-run",
64
+ params: {
65
+ fileName: constant("plan.json"),
66
+ labelText: constant("Running planning flow"),
67
+ taskKey: ref("params.taskKey"),
68
+ taskContextIteration: ref("params.taskContextIteration"),
69
+ designIteration: ref("params.designIteration"),
70
+ planIteration: ref("params.planIteration"),
71
+ qaIteration: ref("params.qaIteration"),
72
+ llmExecutor: ref("params.llmExecutor"),
73
+ llmModel: ref("params.llmModel"),
74
+ extraPrompt: ref("params.extraPrompt"),
75
+ mdLang: ref("params.mdLang"),
76
+ },
77
+ },
78
+ ],
79
+ };
80
+ }
81
+ function designReviewLoopPhase() {
82
+ return {
83
+ id: "design_review_loop",
84
+ steps: [
85
+ {
86
+ id: "run_design_review_loop",
87
+ node: "flow-run",
88
+ params: {
89
+ fileName: constant("design-review-loop.json"),
90
+ labelText: constant("Running design-review loop"),
91
+ taskKey: ref("params.taskKey"),
92
+ baseIteration: ref("params.designReviewBaseIteration"),
93
+ workspaceDir: ref("params.workspaceDir"),
94
+ extraPrompt: ref("params.extraPrompt"),
95
+ llmExecutor: ref("params.llmExecutor"),
96
+ llmModel: ref("params.llmModel"),
97
+ },
98
+ stopFlowIf: {
99
+ equals: [
100
+ ref("steps.design_review_loop.run_design_review_loop.value.executionState.terminationOutcome"),
101
+ constant("stopped"),
102
+ ],
103
+ },
104
+ stopFlowOutcome: "stopped",
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ function implementationPromptVars(context) {
110
+ const vars = {
111
+ design_file: ref("steps.implement.resolve_planning_bundle.value.designFile"),
112
+ design_json_file: ref("steps.implement.resolve_planning_bundle.value.designJsonFile"),
113
+ plan_file: ref("steps.implement.resolve_planning_bundle.value.planFile"),
114
+ plan_json_file: ref("steps.implement.resolve_planning_bundle.value.planJsonFile"),
115
+ qa_file: ref("steps.implement.resolve_planning_bundle.value.qaFile"),
116
+ qa_json_file: ref("steps.implement.resolve_planning_bundle.value.qaJsonFile"),
117
+ };
118
+ if (context.presetId === "standard") {
119
+ vars.project_guidance_file = constant("not provided");
120
+ vars.project_guidance_json_file = constant("not provided");
121
+ }
122
+ return vars;
123
+ }
124
+ function implementationPhase(context) {
125
+ const routingGroup = context.presetId === "standard" ? "implementation" : undefined;
126
+ const runImplementStep = {
127
+ id: "run_implement",
128
+ node: "llm-prompt",
129
+ ...(routingGroup ? { routingGroup } : {}),
130
+ prompt: {
131
+ templateRef: "implement",
132
+ vars: implementationPromptVars(context),
133
+ extraPrompt: ref("params.extraPrompt"),
134
+ format: "task-prompt",
135
+ },
136
+ params: {
137
+ labelText: constant("Running implementation mode locally"),
138
+ model: ref("params.llmModel"),
139
+ executor: ref("params.llmExecutor"),
140
+ },
141
+ };
142
+ return {
143
+ id: "implement",
144
+ steps: [
145
+ {
146
+ id: "resolve_planning_bundle",
147
+ node: "planning-bundle",
148
+ params: {
149
+ taskKey: ref("params.taskKey"),
150
+ },
151
+ },
152
+ runImplementStep,
153
+ {
154
+ id: "notify_implement_complete",
155
+ node: "telegram-notify",
156
+ params: {
157
+ message: {
158
+ template: "Implementation phase for {taskKey} complete.",
159
+ vars: {
160
+ taskKey: ref("params.taskKey"),
161
+ },
162
+ },
163
+ },
164
+ },
165
+ ],
166
+ };
167
+ }
168
+ function reviewLoopPhase(context) {
169
+ const runReviewLoopStep = {
170
+ id: "run_review_loop",
171
+ node: "flow-run",
172
+ ...(context.presetId === "standard"
173
+ ? {
174
+ stopFlowIf: {
175
+ not: {
176
+ equals: [
177
+ ref("steps.review-loop.run_review_loop.value.executionState.terminationOutcome"),
178
+ constant("success"),
179
+ ],
180
+ },
181
+ },
182
+ stopFlowOutcome: "stopped",
183
+ }
184
+ : {}),
185
+ params: {
186
+ fileName: constant("review-loop.json"),
187
+ labelText: constant("Running review-loop"),
188
+ taskKey: ref("params.taskKey"),
189
+ baseIteration: ref("params.baseIteration"),
190
+ workspaceDir: ref("params.workspaceDir"),
191
+ extraPrompt: ref("params.extraPrompt"),
192
+ reviewFixPoints: ref("params.reviewFixPoints"),
193
+ reviewBlockingSeverities: ref("params.reviewBlockingSeverities"),
194
+ llmExecutor: ref("params.llmExecutor"),
195
+ llmModel: ref("params.llmModel"),
196
+ },
197
+ };
198
+ return {
199
+ id: "review-loop",
200
+ steps: [
201
+ runReviewLoopStep,
202
+ {
203
+ id: "notify_task_complete",
204
+ node: "telegram-notify",
205
+ params: {
206
+ message: {
207
+ template: "Task {taskKey} complete.",
208
+ vars: {
209
+ taskKey: ref("params.taskKey"),
210
+ },
211
+ },
212
+ },
213
+ },
214
+ ],
215
+ };
216
+ }
217
+ export const BUILT_IN_AUTO_FLOW_BLOCKS = [
218
+ {
219
+ id: "source.jira",
220
+ title: "Task source",
221
+ category: "source",
222
+ allowedSlots: SOURCE_SLOT,
223
+ requires: [],
224
+ provides: ["task.source"],
225
+ locked: true,
226
+ defaultEnabled: true,
227
+ createPhase: sourcePhase,
228
+ },
229
+ {
230
+ id: "normalize.task-source",
231
+ title: "Task source normalization",
232
+ category: "normalize",
233
+ allowedSlots: NORMALIZE_SLOT,
234
+ requires: ["task.source"],
235
+ provides: ["task.context"],
236
+ locked: true,
237
+ defaultEnabled: true,
238
+ createPhase: normalizePhase,
239
+ },
240
+ {
241
+ id: "planning.plan",
242
+ title: "Planning",
243
+ category: "planning",
244
+ allowedSlots: PLANNING_SLOT,
245
+ requires: ["task.context"],
246
+ provides: ["planning.result", "planning.bundle"],
247
+ locked: true,
248
+ defaultEnabled: true,
249
+ createPhase: planningPhase,
250
+ },
251
+ {
252
+ id: "review.design-loop",
253
+ title: "Design review loop",
254
+ category: "review",
255
+ allowedSlots: DESIGN_REVIEW_SLOT,
256
+ requires: ["planning.result"],
257
+ provides: ["design-review.result"],
258
+ defaultEnabled: true,
259
+ params: {
260
+ maxIterations: {
261
+ type: "integer",
262
+ min: 1,
263
+ max: 5,
264
+ default: 3,
265
+ supportedExecutableValues: [3],
266
+ },
267
+ },
268
+ createPhase: designReviewLoopPhase,
269
+ },
270
+ {
271
+ id: "implementation.default",
272
+ title: "Default implementation",
273
+ category: "implementation",
274
+ allowedSlots: IMPLEMENTATION_SLOT,
275
+ requires: ["planning.bundle"],
276
+ provides: ["implementation.result"],
277
+ locked: true,
278
+ defaultEnabled: true,
279
+ createPhase: implementationPhase,
280
+ },
281
+ {
282
+ id: "review.loop",
283
+ title: "Review loop",
284
+ category: "review",
285
+ allowedSlots: REVIEW_SLOT,
286
+ requires: ["implementation.result"],
287
+ provides: ["review.result"],
288
+ defaultEnabled: true,
289
+ params: {
290
+ maxIterations: {
291
+ type: "integer",
292
+ min: 1,
293
+ max: 5,
294
+ default: 5,
295
+ supportedExecutableValues: [5],
296
+ },
297
+ },
298
+ createPhase: reviewLoopPhase,
299
+ },
300
+ ];
301
+ const builtInBlockById = new Map(BUILT_IN_AUTO_FLOW_BLOCKS.map((block) => [block.id, block]));
302
+ export function listBuiltInAutoFlowBlockDefinitions() {
303
+ return [...BUILT_IN_AUTO_FLOW_BLOCKS];
304
+ }
305
+ export function getBuiltInAutoFlowBlockDefinition(blockId) {
306
+ return builtInBlockById.get(blockId) ?? null;
307
+ }
@@ -0,0 +1,273 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import YAML from "yaml";
4
+ import { TaskRunnerError } from "../errors.js";
5
+ import { agentweaverConfigDir } from "../runtime/env-loader.js";
6
+ import { getBuiltInAutoFlowBlockDefinition } from "./auto-flow-blocks.js";
7
+ export const AUTO_FLOW_PRESETS = ["simple", "standard"];
8
+ export const AUTO_FLOW_SLOT_NAMES = [
9
+ "designReview",
10
+ "postImplementationChecks",
11
+ "review",
12
+ "final",
13
+ ];
14
+ export const AUTO_FLOW_BLOCK_IDS = [
15
+ "review.design-loop",
16
+ "checks.go.linter",
17
+ "checks.go.tests",
18
+ "review.loop",
19
+ ];
20
+ const SLOT_BLOCKS = {
21
+ designReview: ["review.design-loop"],
22
+ postImplementationChecks: ["checks.go.linter", "checks.go.tests"],
23
+ review: ["review.loop"],
24
+ final: ["checks.go.linter", "checks.go.tests"],
25
+ };
26
+ const ITERATIVE_BLOCKS = new Set([
27
+ "review.design-loop",
28
+ "checks.go.linter",
29
+ "checks.go.tests",
30
+ "review.loop",
31
+ ]);
32
+ function isRecord(value) {
33
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ function formatConfigError(configName, configPath, issue) {
36
+ const location = configPath ? ` at ${configPath}` : "";
37
+ return new TaskRunnerError(`Auto flow config '${configName}'${location}: ${issue}`);
38
+ }
39
+ function assertConfig(condition, configName, configPath, issue) {
40
+ if (!condition) {
41
+ throw formatConfigError(configName, configPath, issue);
42
+ }
43
+ }
44
+ function validateConfigNameForPath(name) {
45
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
46
+ throw new TaskRunnerError(`Auto flow config name '${name}' is invalid. Use only letters, numbers, dots, underscores, and dashes.`);
47
+ }
48
+ }
49
+ function assertOnlyKeys(value, allowed, configName, configPath, objectPath) {
50
+ const allowedSet = new Set(allowed);
51
+ for (const key of Object.keys(value)) {
52
+ assertConfig(allowedSet.has(key), configName, configPath, `unsupported key '${key}' at ${objectPath}`);
53
+ }
54
+ }
55
+ function isPreset(value) {
56
+ return typeof value === "string" && AUTO_FLOW_PRESETS.includes(value);
57
+ }
58
+ function isSlotName(value) {
59
+ return AUTO_FLOW_SLOT_NAMES.includes(value);
60
+ }
61
+ function isBlockId(value) {
62
+ return typeof value === "string" && AUTO_FLOW_BLOCK_IDS.includes(value);
63
+ }
64
+ function isEnabled(value) {
65
+ return value === true || value === false || value === "auto";
66
+ }
67
+ function parameterBoundsForBlock(blockId, paramName) {
68
+ const definition = getBuiltInAutoFlowBlockDefinition(blockId);
69
+ const paramDefinition = definition?.params?.[paramName];
70
+ if (paramDefinition?.type === "integer") {
71
+ return {
72
+ min: paramDefinition.min,
73
+ max: paramDefinition.max,
74
+ };
75
+ }
76
+ if (paramName === "maxIterations" && ITERATIVE_BLOCKS.has(blockId)) {
77
+ return {
78
+ min: 1,
79
+ max: 5,
80
+ };
81
+ }
82
+ return null;
83
+ }
84
+ function validateBlock(rawBlock, slotName, index, configName, configPath) {
85
+ const objectPath = `slots.${slotName}.blocks[${index}]`;
86
+ assertConfig(isRecord(rawBlock), configName, configPath, `${objectPath} must be an object`);
87
+ assertOnlyKeys(rawBlock, ["id", "enabled", "maxIterations"], configName, configPath, objectPath);
88
+ const blockId = rawBlock["id"];
89
+ assertConfig(isBlockId(blockId), configName, configPath, `${objectPath}.id must be a known block id`);
90
+ assertConfig(SLOT_BLOCKS[slotName].includes(blockId), configName, configPath, `${objectPath}.id '${blockId}' is not supported in slot '${slotName}'`);
91
+ const enabled = rawBlock["enabled"];
92
+ assertConfig(enabled === undefined || isEnabled(enabled), configName, configPath, `${objectPath}.enabled must be true, false, or auto`);
93
+ const maxIterations = rawBlock["maxIterations"];
94
+ assertConfig(maxIterations === undefined || (Number.isInteger(maxIterations) && Number(maxIterations) > 0), configName, configPath, `${objectPath}.maxIterations must be a positive integer`);
95
+ assertConfig(maxIterations === undefined || ITERATIVE_BLOCKS.has(blockId), configName, configPath, `${objectPath}.maxIterations is not supported by block '${blockId}'`);
96
+ const maxIterationBounds = parameterBoundsForBlock(blockId, "maxIterations");
97
+ assertConfig(maxIterations === undefined
98
+ || !maxIterationBounds
99
+ || (Number(maxIterations) >= maxIterationBounds.min && Number(maxIterations) <= maxIterationBounds.max), configName, configPath, `${objectPath}.maxIterations for block '${blockId}' must be between ${maxIterationBounds?.min ?? 1} and ${maxIterationBounds?.max ?? 5}; received ${Number(maxIterations)}`);
100
+ return {
101
+ id: blockId,
102
+ ...(enabled !== undefined ? { enabled } : {}),
103
+ ...(maxIterations !== undefined ? { maxIterations: Number(maxIterations) } : {}),
104
+ };
105
+ }
106
+ export function validateAutoFlowConfigValue(value, requestedName, configPath) {
107
+ const configNameForErrors = isRecord(value) && typeof value["name"] === "string" && value["name"].trim()
108
+ ? value["name"].trim()
109
+ : requestedName;
110
+ assertConfig(isRecord(value), configNameForErrors, configPath, "config root must be an object");
111
+ assertOnlyKeys(value, ["kind", "version", "name", "basePreset", "slots"], configNameForErrors, configPath, "root");
112
+ assertConfig(value["kind"] === "auto-flow-config", configNameForErrors, configPath, "kind must be 'auto-flow-config'");
113
+ assertConfig(value["version"] === 1, configNameForErrors, configPath, "version must be 1");
114
+ assertConfig(typeof value["name"] === "string" && value["name"].trim().length > 0, configNameForErrors, configPath, "name must be a non-empty string");
115
+ const name = String(value["name"]).trim();
116
+ assertConfig(name === requestedName, name, configPath, `name must match requested config name '${requestedName}'`);
117
+ assertConfig(isPreset(value["basePreset"]), name, configPath, "basePreset must be simple or standard");
118
+ let slots;
119
+ const rawSlots = value["slots"];
120
+ if (rawSlots !== undefined) {
121
+ assertConfig(isRecord(rawSlots), name, configPath, "slots must be an object");
122
+ slots = {};
123
+ for (const [slotName, rawSlot] of Object.entries(rawSlots)) {
124
+ assertConfig(isSlotName(slotName), name, configPath, `unknown slot '${slotName}'`);
125
+ assertConfig(isRecord(rawSlot), name, configPath, `slots.${slotName} must be an object`);
126
+ assertOnlyKeys(rawSlot, ["blocks"], name, configPath, `slots.${slotName}`);
127
+ const rawBlocks = rawSlot["blocks"];
128
+ assertConfig(Array.isArray(rawBlocks), name, configPath, `slots.${slotName}.blocks must be an array`);
129
+ slots[slotName] = {
130
+ blocks: rawBlocks.map((block, index) => validateBlock(block, slotName, index, name, configPath)),
131
+ };
132
+ }
133
+ }
134
+ return {
135
+ kind: "auto-flow-config",
136
+ version: 1,
137
+ name,
138
+ basePreset: value["basePreset"],
139
+ ...(slots !== undefined ? { slots } : {}),
140
+ };
141
+ }
142
+ export function normalizeAutoFlowConfigYaml(config) {
143
+ return YAML.stringify(config);
144
+ }
145
+ export function projectAutoFlowConfigPath(name, cwd = process.cwd()) {
146
+ validateConfigNameForPath(name);
147
+ return path.join(cwd, ".agentweaver", "flow-configs", `${name}.yaml`);
148
+ }
149
+ export function userAutoFlowConfigPath(name) {
150
+ validateConfigNameForPath(name);
151
+ return path.join(agentweaverConfigDir(), "flow-configs", `${name}.yaml`);
152
+ }
153
+ export function autoFlowConfigSearchPaths(name, cwd = process.cwd()) {
154
+ return {
155
+ projectPath: projectAutoFlowConfigPath(name, cwd),
156
+ userPath: userAutoFlowConfigPath(name),
157
+ };
158
+ }
159
+ function configNamesInDir(dirPath) {
160
+ if (!existsSync(dirPath)) {
161
+ return [];
162
+ }
163
+ return readdirSync(dirPath, { withFileTypes: true })
164
+ .filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
165
+ .map((entry) => entry.name.replace(/\.ya?ml$/i, ""))
166
+ .filter((name) => /^[A-Za-z0-9._-]+$/.test(name));
167
+ }
168
+ export function listAutoFlowConfigs(cwd = process.cwd()) {
169
+ const projectDir = path.join(cwd, ".agentweaver", "flow-configs");
170
+ const userDir = path.join(agentweaverConfigDir(), "flow-configs");
171
+ const names = new Set([...configNamesInDir(projectDir), ...configNamesInDir(userDir)]);
172
+ return [...names]
173
+ .sort((left, right) => left.localeCompare(right))
174
+ .map((name) => {
175
+ const projectPath = projectAutoFlowConfigPath(name, cwd);
176
+ const userPath = userAutoFlowConfigPath(name);
177
+ const hasProject = existsSync(projectPath);
178
+ const hasUser = existsSync(userPath);
179
+ return {
180
+ name,
181
+ ...(hasProject ? { projectPath } : {}),
182
+ ...(hasUser ? { userPath } : {}),
183
+ selectedSource: hasProject ? "project" : "user",
184
+ };
185
+ });
186
+ }
187
+ export function loadAutoFlowConfigByName(name, cwd = process.cwd()) {
188
+ const requestedName = name.trim();
189
+ validateConfigNameForPath(requestedName);
190
+ const { projectPath, userPath } = autoFlowConfigSearchPaths(requestedName, cwd);
191
+ const projectExists = existsSync(projectPath);
192
+ const userExists = existsSync(userPath);
193
+ const selectedPath = projectExists ? projectPath : userExists ? userPath : null;
194
+ if (!selectedPath) {
195
+ throw new TaskRunnerError(`Auto flow config '${requestedName}' was not found. Searched ${projectPath} and ${userPath}.`);
196
+ }
197
+ const rawYaml = readFileSync(selectedPath, "utf8");
198
+ let parsed;
199
+ try {
200
+ parsed = YAML.parse(rawYaml);
201
+ }
202
+ catch (error) {
203
+ throw formatConfigError(requestedName, selectedPath, `YAML parse failed: ${error.message}`);
204
+ }
205
+ const config = validateAutoFlowConfigValue(parsed, requestedName, selectedPath);
206
+ return {
207
+ config,
208
+ rawYaml,
209
+ normalizedYaml: normalizeAutoFlowConfigYaml(config),
210
+ source: {
211
+ type: projectExists ? "project" : "user",
212
+ path: selectedPath,
213
+ ...(projectExists && userExists ? { shadowedUserPath: userPath } : {}),
214
+ },
215
+ };
216
+ }
217
+ function writeTextAtomic(filePath, content) {
218
+ mkdirSync(path.dirname(filePath), { recursive: true });
219
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
220
+ writeFileSync(tempPath, content, "utf8");
221
+ renameSync(tempPath, filePath);
222
+ }
223
+ export function saveAutoFlowConfig(config, options = {}) {
224
+ const cwd = options.cwd ?? process.cwd();
225
+ const location = options.location ?? "project";
226
+ validateConfigNameForPath(config.name);
227
+ const configPath = location === "project"
228
+ ? projectAutoFlowConfigPath(config.name, cwd)
229
+ : userAutoFlowConfigPath(config.name);
230
+ const normalizedConfig = validateAutoFlowConfigValue(config, config.name, configPath);
231
+ const normalizedYaml = normalizeAutoFlowConfigYaml(normalizedConfig);
232
+ writeTextAtomic(configPath, normalizedYaml);
233
+ return {
234
+ config: normalizedConfig,
235
+ normalizedYaml,
236
+ source: {
237
+ type: location,
238
+ path: configPath,
239
+ },
240
+ };
241
+ }
242
+ export function allowedAutoFlowBlocksForSlot(slotName) {
243
+ return SLOT_BLOCKS[slotName];
244
+ }
245
+ export function validateAutoFlowBlockInsertion(slotName, blockId) {
246
+ if (!isSlotName(slotName)) {
247
+ return {
248
+ ok: false,
249
+ message: `Unknown auto-flow slot '${slotName}'.`,
250
+ };
251
+ }
252
+ if (!isBlockId(blockId)) {
253
+ return {
254
+ ok: false,
255
+ message: `Unknown auto-flow block '${blockId}'.`,
256
+ slotName,
257
+ };
258
+ }
259
+ if (!SLOT_BLOCKS[slotName].includes(blockId)) {
260
+ return {
261
+ ok: false,
262
+ message: `Auto-flow block '${blockId}' cannot be inserted into slot '${slotName}'. Allowed blocks: ${SLOT_BLOCKS[slotName].join(", ")}.`,
263
+ slotName,
264
+ blockId,
265
+ };
266
+ }
267
+ return {
268
+ ok: true,
269
+ message: `Auto-flow block '${blockId}' can be inserted into slot '${slotName}'.`,
270
+ slotName,
271
+ blockId,
272
+ };
273
+ }
@@ -0,0 +1,49 @@
1
+ export const AUTO_FLOW_STANDARD_FLOW_ID = "auto-common";
2
+ export const AUTO_FLOW_SIMPLE_FLOW_ID = "auto-simple";
3
+ export const AUTO_FLOW_CONFIG_FLOW_ID_PREFIX = "auto-config:";
4
+ const VALID_AUTO_FLOW_CONFIG_ID_RE = /^[A-Za-z0-9._-]+$/;
5
+ export function defaultAutoFlowSelection() {
6
+ return { kind: "preset", preset: "standard" };
7
+ }
8
+ export function autoFlowIdentityForSelection(selection, resolved) {
9
+ if (selection.kind === "config") {
10
+ return {
11
+ flowId: `${AUTO_FLOW_CONFIG_FLOW_ID_PREFIX}${resolved.config.name}`,
12
+ displayLabel: `config ${resolved.config.name}`,
13
+ selectedCommand: resolved.document.selectedCommand,
14
+ };
15
+ }
16
+ return {
17
+ flowId: selection.preset === "simple" ? AUTO_FLOW_SIMPLE_FLOW_ID : AUTO_FLOW_STANDARD_FLOW_ID,
18
+ displayLabel: `${selection.preset} preset`,
19
+ selectedCommand: selection.preset === "simple" ? AUTO_FLOW_SIMPLE_FLOW_ID : AUTO_FLOW_STANDARD_FLOW_ID,
20
+ };
21
+ }
22
+ export function isConfigurableAutoConfigFlowId(flowId) {
23
+ if (!flowId.startsWith(AUTO_FLOW_CONFIG_FLOW_ID_PREFIX)) {
24
+ return false;
25
+ }
26
+ const configName = flowId.slice(AUTO_FLOW_CONFIG_FLOW_ID_PREFIX.length);
27
+ return VALID_AUTO_FLOW_CONFIG_ID_RE.test(configName);
28
+ }
29
+ export function isConfigurableAutoPresetFlowId(flowId) {
30
+ return flowId === AUTO_FLOW_STANDARD_FLOW_ID || flowId === AUTO_FLOW_SIMPLE_FLOW_ID;
31
+ }
32
+ export function isConfigurableAutoFlowId(flowId) {
33
+ return isConfigurableAutoPresetFlowId(flowId) || isConfigurableAutoConfigFlowId(flowId);
34
+ }
35
+ export function isRestartArchivingFlowId(flowId) {
36
+ return (flowId === AUTO_FLOW_STANDARD_FLOW_ID ||
37
+ flowId === AUTO_FLOW_SIMPLE_FLOW_ID ||
38
+ flowId === "auto-common-guided" ||
39
+ flowId === "auto-golang" ||
40
+ flowId === "instant-task" ||
41
+ isConfigurableAutoConfigFlowId(flowId));
42
+ }
43
+ export function isContinuableParentFlowId(flowId) {
44
+ return (flowId === AUTO_FLOW_STANDARD_FLOW_ID ||
45
+ flowId === AUTO_FLOW_SIMPLE_FLOW_ID ||
46
+ flowId === "auto-golang" ||
47
+ flowId === "instant-task" ||
48
+ isConfigurableAutoConfigFlowId(flowId));
49
+ }
@@ -0,0 +1,52 @@
1
+ export const VIRTUAL_BUILT_IN_AUTO_FLOW_FILE_NAMES = ["auto-simple.json", "auto-common.json"];
2
+ export const BUILT_IN_AUTO_FLOW_PRESETS = [
3
+ {
4
+ id: "simple",
5
+ title: "Simple auto flow",
6
+ fileName: "auto-simple.json",
7
+ kind: "auto-flow",
8
+ version: 1,
9
+ description: "End-to-end resumable pipeline without language-specific checks. Runs: task source collection -> task source normalization -> plan -> implement -> review loop.",
10
+ blocks: [
11
+ { blockId: "source.jira", slot: "source", locked: true, defaultEnabled: true },
12
+ { blockId: "normalize.task-source", slot: "normalize", locked: true, defaultEnabled: true },
13
+ { blockId: "planning.plan", slot: "planning", locked: true, defaultEnabled: true },
14
+ { blockId: "implementation.default", slot: "implementation", locked: true, defaultEnabled: true },
15
+ { blockId: "review.loop", slot: "review", defaultEnabled: true },
16
+ ],
17
+ },
18
+ {
19
+ id: "standard",
20
+ title: "Standard auto flow",
21
+ fileName: "auto-common.json",
22
+ kind: "auto-flow",
23
+ version: 1,
24
+ description: "End-to-end resumable pipeline without language-specific checks. Runs: task source collection -> task source normalization -> plan -> design-review loop -> implement -> review loop.",
25
+ blocks: [
26
+ { blockId: "source.jira", slot: "source", locked: true, defaultEnabled: true },
27
+ { blockId: "normalize.task-source", slot: "normalize", locked: true, defaultEnabled: true },
28
+ { blockId: "planning.plan", slot: "planning", locked: true, defaultEnabled: true },
29
+ { blockId: "review.design-loop", slot: "designReview", defaultEnabled: true },
30
+ { blockId: "implementation.default", slot: "implementation", locked: true, defaultEnabled: true },
31
+ { blockId: "review.loop", slot: "review", defaultEnabled: true },
32
+ ],
33
+ },
34
+ ];
35
+ const presetById = new Map(BUILT_IN_AUTO_FLOW_PRESETS.map((preset) => [preset.id, preset]));
36
+ const presetByFileName = new Map(BUILT_IN_AUTO_FLOW_PRESETS.map((preset) => [preset.fileName, preset]));
37
+ export function listBuiltInAutoFlowPresets() {
38
+ return [...BUILT_IN_AUTO_FLOW_PRESETS];
39
+ }
40
+ export function getBuiltInAutoFlowPreset(presetId) {
41
+ const preset = presetById.get(presetId);
42
+ if (!preset) {
43
+ throw new Error(`Unknown built-in auto-flow preset '${presetId}'.`);
44
+ }
45
+ return preset;
46
+ }
47
+ export function getBuiltInAutoFlowPresetByFileName(fileName) {
48
+ return presetByFileName.get(fileName) ?? null;
49
+ }
50
+ export function isVirtualBuiltInAutoFlowFileName(fileName) {
51
+ return VIRTUAL_BUILT_IN_AUTO_FLOW_FILE_NAMES.includes(fileName);
52
+ }