@lhi/n8m 0.2.2 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { StateGraph, START, END, Send } from "@langchain/langgraph";
1
+ import { StateGraph, START, END } from "@langchain/langgraph";
2
2
  import { checkpointer } from "./checkpointer.js";
3
3
  import { architectNode } from "./nodes/architect.js";
4
4
  import { engineerNode } from "./nodes/engineer.js";
@@ -15,14 +15,8 @@ const workflow = new StateGraph(TeamState)
15
15
  .addNode("qa", qaNode)
16
16
  // Edges
17
17
  .addEdge(START, "architect")
18
- // Parallel Fan-Out: Architect -> Multiple Engineers (via Send)
19
- .addConditionalEdges("architect", (state) => {
20
- if (state.strategies && state.strategies.length > 0) {
21
- return state.strategies.map(s => new Send("engineer", { spec: s }));
22
- }
23
- // Fallback for linear path
24
- return "engineer";
25
- }, ["engineer"]) // We must declare valid destination nodes for visualization/compilation
18
+ // Architect -> Engineer (spec is chosen interactively in create.ts before resuming)
19
+ .addEdge("architect", "engineer")
26
20
  // Fan-In: Engineer -> Supervisor (Wait for all to finish) or route to Reviewer (if fixing)
27
21
  .addConditionalEdges("engineer", (state) => {
28
22
  // If we have errors, we are in "Repair Mode" -> Skip Supervisor (which only handles fresh candidates)
@@ -98,117 +98,150 @@ export default class Create extends Command {
98
98
  });
99
99
  }
100
100
  }
101
- else if (nodeName === 'engineer') {
102
- this.log(theme.agent(`⚙️ Engineer: Workflow code generated/updated.`));
103
- if (stateUpdate.workflowJson) {
104
- lastWorkflowJson = stateUpdate.workflowJson;
105
- }
106
- }
107
- else if (nodeName === 'qa') {
108
- const status = stateUpdate.validationStatus;
109
- if (status === 'passed') {
110
- this.log(theme.success(`🧪 QA: Validation Passed!`));
111
- }
112
- else {
113
- this.log(theme.fail(`🧪 QA: Validation Failed.`));
114
- if (stateUpdate.validationErrors && stateUpdate.validationErrors.length > 0) {
115
- stateUpdate.validationErrors.forEach((e) => this.log(theme.error(` - ${e}`)));
116
- }
117
- this.log(theme.warn(` Looping back to Engineer for repairs...`));
118
- }
119
- }
120
101
  }
121
- // Check for interrupt/pause
102
+ // Handle interrupt/pause loop
122
103
  let snapshot = await graph.getState({ configurable: { thread_id: threadId } });
123
104
  while (snapshot.next.length > 0) {
124
105
  const nextNode = snapshot.next[0];
125
- this.log(theme.warn(`\n⏸️ Workflow Paused at step: ${nextNode}`));
126
106
  if (nextNode === 'engineer') {
127
- const { action } = await inquirer.prompt([{
128
- type: 'list',
129
- name: 'action',
130
- message: 'How would you like to proceed with the Blueprint?',
131
- choices: [
132
- { name: 'Approve and Generate Workflow', value: 'approve' },
133
- { name: 'Provide Feedback / Refine Strategy', value: 'feedback' },
134
- { name: 'Exit and Resume Later', value: 'exit' }
135
- ]
136
- }]);
137
- if (action === 'approve') {
138
- this.log(theme.agent("Approve! Proceeding to engineering..."));
139
- await graph.updateState({ configurable: { thread_id: threadId } }, { userFeedback: undefined }, nextNode);
140
- const stream = await graph.stream(null, { configurable: { thread_id: threadId } });
141
- for await (const event of stream) {
142
- const nodeName = Object.keys(event)[0];
143
- const stateUpdate = event[nodeName];
144
- if (nodeName === 'engineer') {
145
- this.log(theme.agent(`⚙️ Engineer: Workflow code generated/updated.`));
146
- if (stateUpdate.workflowJson)
147
- lastWorkflowJson = stateUpdate.workflowJson;
107
+ const isRepair = (snapshot.values.validationErrors || []).length > 0;
108
+ if (isRepair) {
109
+ // Repair iteration — auto-continue without asking the user
110
+ const repairStream = await graph.stream(null, { configurable: { thread_id: threadId } });
111
+ for await (const event of repairStream) {
112
+ const n = Object.keys(event)[0];
113
+ const u = event[n];
114
+ if (n === 'engineer') {
115
+ this.log(theme.agent(`⚙️ Engineer: Applying fixes...`));
116
+ if (u.workflowJson)
117
+ lastWorkflowJson = u.workflowJson;
148
118
  }
149
- else if (nodeName === 'qa') {
150
- const status = stateUpdate.validationStatus;
151
- if (status === 'passed')
152
- this.log(theme.success(`🧪 QA: Validation Passed!`));
153
- else
154
- this.log(theme.fail(`🧪 QA: Validation Failed.`));
119
+ else if (n === 'supervisor' && u.workflowJson) {
120
+ lastWorkflowJson = u.workflowJson;
155
121
  }
156
122
  }
157
123
  }
158
- else if (action === 'feedback') {
159
- const { feedback } = await inquirer.prompt([{
160
- type: 'input',
161
- name: 'feedback',
162
- message: 'Enter your feedback/instructions:',
124
+ else {
125
+ // Initial build let user choose which strategy to use
126
+ const strategies = snapshot.values.strategies || [];
127
+ const spec = snapshot.values.spec;
128
+ const choices = [];
129
+ if (strategies.length > 0) {
130
+ strategies.forEach((s, i) => {
131
+ const tag = i === 0 ? 'Primary' : 'Alternative';
132
+ const nodes = s.nodes
133
+ ?.map((n) => n.type?.split('.').pop())
134
+ .join(', ');
135
+ choices.push({
136
+ name: `[${tag}] ${s.suggestedName}${nodes ? ` · ${nodes}` : ''}`,
137
+ value: { type: 'build', strategy: s },
138
+ short: s.suggestedName,
139
+ });
140
+ });
141
+ }
142
+ else if (spec) {
143
+ choices.push({
144
+ name: spec.suggestedName,
145
+ value: { type: 'build', strategy: spec },
146
+ short: spec.suggestedName,
147
+ });
148
+ }
149
+ choices.push(new inquirer.Separator());
150
+ choices.push({
151
+ name: 'Add feedback before building',
152
+ value: { type: 'feedback' },
153
+ short: 'Add feedback',
154
+ });
155
+ choices.push({
156
+ name: 'Exit (save session to resume later)',
157
+ value: { type: 'exit' },
158
+ short: 'Exit',
159
+ });
160
+ const { choice } = await inquirer.prompt([{
161
+ type: 'list',
162
+ name: 'choice',
163
+ message: strategies.length > 1
164
+ ? 'The Architect designed two approaches — which should the Engineer build?'
165
+ : 'Blueprint ready. How would you like to proceed?',
166
+ choices,
163
167
  }]);
164
- this.log(theme.agent("Updating strategy with your feedback..."));
165
- // In a real implementation, we'd loop back to Architect or update the goal.
166
- // For now, let's update userFeedback and resume.
167
- // To actually RE-ARCHITECT, we might need to jump back.
168
- // LangGraph can handle this by updating state and using a conditional edge.
169
- await graph.updateState({ configurable: { thread_id: threadId } }, { userFeedback: feedback }, nextNode);
170
- // For now, just resume and let Engineer see the feedback.
171
- const stream = await graph.stream(null, { configurable: { thread_id: threadId } });
172
- for await (const event of stream) {
173
- const nodeName = Object.keys(event)[0];
174
- const stateUpdate = event[nodeName];
175
- if (nodeName === 'engineer') {
176
- this.log(theme.agent(`⚙️ Engineer: Workflow code generated/updated (Feedback incorporated).`));
177
- if (stateUpdate.workflowJson)
178
- lastWorkflowJson = stateUpdate.workflowJson;
168
+ if (choice.type === 'exit') {
169
+ this.log(theme.info(`\nSession saved. Resume later with: n8m resume ${threadId}`));
170
+ return;
171
+ }
172
+ let chosenSpec = choice.strategy ?? spec;
173
+ let stateUpdate = { spec: chosenSpec, userFeedback: undefined };
174
+ if (choice.type === 'feedback') {
175
+ const { feedback } = await inquirer.prompt([{
176
+ type: 'input',
177
+ name: 'feedback',
178
+ message: 'Describe your refinements (the Engineer will incorporate them):',
179
+ }]);
180
+ chosenSpec = strategies[0] ?? spec;
181
+ stateUpdate = { spec: chosenSpec, userFeedback: feedback };
182
+ this.log(theme.agent(`Feedback noted. Building "${chosenSpec?.suggestedName}" with your refinements...`));
183
+ }
184
+ else {
185
+ this.log(theme.agent(`Building "${chosenSpec?.suggestedName}"...`));
186
+ }
187
+ await graph.updateState({ configurable: { thread_id: threadId } }, stateUpdate, nextNode);
188
+ const buildStream = await graph.stream(null, { configurable: { thread_id: threadId } });
189
+ for await (const event of buildStream) {
190
+ const n = Object.keys(event)[0];
191
+ const u = event[n];
192
+ if (n === 'engineer') {
193
+ this.log(theme.agent(`⚙️ Engineer: Building workflow...`));
194
+ if (u.workflowJson)
195
+ lastWorkflowJson = u.workflowJson;
179
196
  }
180
- else if (nodeName === 'qa') {
181
- if (stateUpdate.validationStatus === 'passed')
182
- this.log(theme.success(`🧪 QA: Validation Passed!`));
197
+ else if (n === 'supervisor' && u.workflowJson) {
198
+ lastWorkflowJson = u.workflowJson;
199
+ }
200
+ else if (n === 'reviewer' && u.validationStatus === 'failed') {
201
+ this.log(theme.warn(` Reviewer flagged issues — Engineer will revise...`));
183
202
  }
184
203
  }
185
204
  }
186
- else {
187
- this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
188
- return;
189
- }
190
205
  }
191
- else {
192
- // Handle other interrupts (like QA)
193
- const { resume } = await inquirer.prompt([{
206
+ else if (nextNode === 'qa') {
207
+ const { proceed } = await inquirer.prompt([{
194
208
  type: 'confirm',
195
- name: 'resume',
196
- message: `Review completed for ${nextNode}. Resume workflow execution?`,
197
- default: true
209
+ name: 'proceed',
210
+ message: 'Workflow generated! Ready to run QA tests?',
211
+ default: true,
198
212
  }]);
199
- if (resume) {
200
- this.log(theme.agent("Resuming..."));
201
- const result = await resumeAgenticWorkflow(threadId);
202
- if (result.validationStatus === 'passed') {
203
- this.log(theme.success(`🧪 QA (Resumed): Validation Passed!`));
204
- if (result.workflowJson)
205
- lastWorkflowJson = result.workflowJson;
206
- }
207
- }
208
- else {
209
- this.log(theme.info(`Session persisted. Resume later with: n8m resume ${threadId}`));
213
+ if (!proceed) {
214
+ this.log(theme.info(`\nSession saved. Resume later with: n8m resume ${threadId}`));
210
215
  return;
211
216
  }
217
+ const qaStream = await graph.stream(null, { configurable: { thread_id: threadId } });
218
+ for await (const event of qaStream) {
219
+ const n = Object.keys(event)[0];
220
+ const u = event[n];
221
+ if (n === 'qa') {
222
+ if (u.validationStatus === 'passed') {
223
+ this.log(theme.success(`🧪 QA: Validation Passed!`));
224
+ if (u.workflowJson)
225
+ lastWorkflowJson = u.workflowJson;
226
+ }
227
+ else {
228
+ this.log(theme.fail(`🧪 QA: Validation Failed.`));
229
+ if (u.validationErrors?.length) {
230
+ u.validationErrors.forEach(e => this.log(theme.error(` - ${e}`)));
231
+ }
232
+ this.log(theme.warn(` Looping back to Engineer for repairs...`));
233
+ }
234
+ }
235
+ else if (n === 'supervisor' && u.workflowJson) {
236
+ lastWorkflowJson = u.workflowJson;
237
+ }
238
+ }
239
+ }
240
+ else {
241
+ // Unknown interrupt — auto-resume
242
+ const result = await resumeAgenticWorkflow(threadId);
243
+ if (result.workflowJson)
244
+ lastWorkflowJson = result.workflowJson;
212
245
  }
213
246
  snapshot = await graph.getState({ configurable: { thread_id: threadId } });
214
247
  }
@@ -439,5 +439,5 @@
439
439
  ]
440
440
  }
441
441
  },
442
- "version": "0.2.2"
442
+ "version": "0.2.3"
443
443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/n8m",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Agentic n8n CLI wrapper - A Skill Bridge for n8n workflow automation",
5
5
  "author": "Lem Canady",
6
6
  "license": "MIT",