bluekiwi 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -169,6 +169,80 @@ h2 {
169
169
  pointer-events: none;
170
170
  }
171
171
 
172
+ /* ─── Form Inputs ─── */
173
+
174
+ .bk-input,
175
+ .bk-textarea,
176
+ .bk-option-comment {
177
+ display: flex;
178
+ flex-direction: column;
179
+ gap: 0.5rem;
180
+ margin-top: 1rem;
181
+ }
182
+
183
+ .bk-field-label {
184
+ font-size: 0.85rem;
185
+ font-weight: 600;
186
+ color: var(--bk-text);
187
+ }
188
+
189
+ .bk-field-label .required {
190
+ color: var(--bk-danger);
191
+ margin-left: 0.25rem;
192
+ }
193
+
194
+ .bk-field-help {
195
+ font-size: 0.78rem;
196
+ color: var(--bk-text-muted);
197
+ }
198
+
199
+ .bk-input-control,
200
+ .bk-textarea-control {
201
+ width: 100%;
202
+ border: 1.5px solid var(--bk-border);
203
+ border-radius: var(--bk-radius);
204
+ background: var(--bk-surface);
205
+ color: var(--bk-text);
206
+ font: inherit;
207
+ padding: 0.8rem 0.9rem;
208
+ transition:
209
+ border-color 0.15s,
210
+ box-shadow 0.15s,
211
+ background 0.15s;
212
+ }
213
+
214
+ .bk-input-control::placeholder,
215
+ .bk-textarea-control::placeholder {
216
+ color: var(--bk-text-muted);
217
+ }
218
+
219
+ .bk-input-control:focus,
220
+ .bk-textarea-control:focus {
221
+ outline: none;
222
+ border-color: var(--bk-primary);
223
+ box-shadow: 0 0 0 3px rgba(65, 105, 225, 0.15);
224
+ }
225
+
226
+ .bk-textarea-control {
227
+ min-height: 7rem;
228
+ resize: vertical;
229
+ line-height: 1.55;
230
+ }
231
+
232
+ .bk-field-error {
233
+ font-size: 0.78rem;
234
+ color: var(--bk-danger);
235
+ }
236
+
237
+ .bk-option-comment[hidden] {
238
+ display: none;
239
+ }
240
+
241
+ .bk-option-comment {
242
+ border-top: 1px dashed var(--bk-border);
243
+ padding-top: 0.9rem;
244
+ }
245
+
172
246
  /* ─── 1. bk-options (A/B/C single select) ─── */
173
247
 
174
248
  .bk-options {
@@ -6,13 +6,30 @@
6
6
  (function () {
7
7
  "use strict";
8
8
 
9
+ const COMMENTABLE_SELECTOR =
10
+ ".bk-option, .bk-card, .bk-code-option, .bk-mockup-item, .bk-check-item";
11
+
9
12
  /* ── i18n ── */
10
13
  const UI_TEXT = {
11
- ko: { submit: "확인", submitted: "✓ 제출됨", selected: "{n}개 선택됨" },
14
+ ko: {
15
+ submit: "확인",
16
+ submitted: "✓ 제출됨",
17
+ selected: "{n}개 선택됨",
18
+ detailsLabel: "추가 요청사항",
19
+ detailsPlaceholder: "선택 이유나 보완 요청을 적어주세요",
20
+ required: "필수",
21
+ requiredComment: "선택한 항목에 대한 코멘트를 입력해 주세요",
22
+ requiredField: "필수 입력 항목을 채워 주세요",
23
+ },
12
24
  en: {
13
25
  submit: "Submit",
14
26
  submitted: "✓ Submitted",
15
27
  selected: "{n} selected",
28
+ detailsLabel: "Additional details",
29
+ detailsPlaceholder: "Add your reasoning or requested changes",
30
+ required: "Required",
31
+ requiredComment: "Add a comment for the selected option",
32
+ requiredField: "Fill in the required fields",
16
33
  },
17
34
  };
18
35
  const lang = document.documentElement.dataset.lang || "en";
@@ -32,6 +49,8 @@
32
49
  bindSliders();
33
50
  bindRanking();
34
51
  bindMatrix();
52
+ bindFields();
53
+ bindOptionComments();
35
54
 
36
55
  const btn = document.querySelector(".bk-vs-submit");
37
56
  if (btn) {
@@ -60,6 +79,7 @@
60
79
  });
61
80
  if (!wasSelected) item.classList.add("selected");
62
81
  hasInteracted = true;
82
+ syncConditionalFields();
63
83
  updateSubmitState();
64
84
  }
65
85
 
@@ -78,6 +98,7 @@
78
98
  });
79
99
  if (!wasSelected) item.classList.add("selected");
80
100
  hasInteracted = true;
101
+ syncConditionalFields();
81
102
  updateSubmitState();
82
103
  });
83
104
  });
@@ -99,9 +120,201 @@
99
120
  function toggleCheck(item) {
100
121
  item.classList.toggle("checked");
101
122
  hasInteracted = true;
123
+ syncConditionalFields();
102
124
  updateSubmitState();
103
125
  }
104
126
 
127
+ function bindFields() {
128
+ document.querySelectorAll(".bk-textarea").forEach(function (container) {
129
+ if (container.querySelector("textarea")) return;
130
+ var textarea = document.createElement("textarea");
131
+ textarea.className = "bk-textarea-control";
132
+ textarea.name = container.dataset.name || "";
133
+ textarea.placeholder = container.dataset.placeholder || "";
134
+ textarea.rows = Number(container.dataset.rows || 4);
135
+ textarea.maxLength = container.dataset.maxlength
136
+ ? Number(container.dataset.maxlength)
137
+ : -1;
138
+ mountField(container, textarea);
139
+ });
140
+
141
+ document.querySelectorAll(".bk-input").forEach(function (container) {
142
+ if (container.querySelector("input")) return;
143
+ var input = document.createElement("input");
144
+ input.className = "bk-input-control";
145
+ input.name = container.dataset.name || "";
146
+ input.type = container.dataset.type || "text";
147
+ input.placeholder = container.dataset.placeholder || "";
148
+ input.maxLength = container.dataset.maxlength
149
+ ? Number(container.dataset.maxlength)
150
+ : -1;
151
+ mountField(container, input);
152
+ });
153
+ }
154
+
155
+ function mountField(container, control) {
156
+ container.classList.add("bk-field");
157
+
158
+ if (container.dataset.label) {
159
+ var label = document.createElement("label");
160
+ label.className = "bk-field-label";
161
+ label.textContent = container.dataset.label;
162
+ if (container.hasAttribute("data-required")) {
163
+ var required = document.createElement("span");
164
+ required.className = "required";
165
+ required.textContent = "*";
166
+ label.appendChild(required);
167
+ }
168
+ container.appendChild(label);
169
+ }
170
+
171
+ if (container.dataset.help) {
172
+ var help = document.createElement("div");
173
+ help.className = "bk-field-help";
174
+ help.textContent = container.dataset.help;
175
+ container.appendChild(help);
176
+ }
177
+
178
+ control.addEventListener("click", function (e) {
179
+ e.stopPropagation();
180
+ });
181
+ control.addEventListener("input", function () {
182
+ hasInteracted = true;
183
+ updateSubmitState();
184
+ });
185
+
186
+ container.appendChild(control);
187
+ }
188
+
189
+ function bindOptionComments() {
190
+ document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
191
+ var needsComment =
192
+ item.hasAttribute("data-requires-comment") || !!item.dataset.commentName;
193
+ if (!needsComment || item.querySelector(".bk-option-comment")) return;
194
+
195
+ var wrapper = document.createElement("div");
196
+ wrapper.className = "bk-option-comment";
197
+ wrapper.hidden = true;
198
+
199
+ var label = document.createElement("label");
200
+ label.className = "bk-field-label";
201
+ label.textContent = item.dataset.commentLabel || t.detailsLabel;
202
+ if (item.hasAttribute("data-requires-comment")) {
203
+ var required = document.createElement("span");
204
+ required.className = "required";
205
+ required.textContent = "*";
206
+ label.appendChild(required);
207
+ }
208
+ wrapper.appendChild(label);
209
+
210
+ var textarea = document.createElement("textarea");
211
+ textarea.className = "bk-textarea-control";
212
+ textarea.placeholder =
213
+ item.dataset.commentPlaceholder || t.detailsPlaceholder;
214
+ textarea.rows = Number(item.dataset.commentRows || 3);
215
+ textarea.addEventListener("click", function (e) {
216
+ e.stopPropagation();
217
+ });
218
+ textarea.addEventListener("input", function () {
219
+ hasInteracted = true;
220
+ updateSubmitState();
221
+ });
222
+ wrapper.appendChild(textarea);
223
+
224
+ item.appendChild(wrapper);
225
+ });
226
+
227
+ syncConditionalFields();
228
+ }
229
+
230
+ function syncConditionalFields() {
231
+ document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
232
+ var comment = item.querySelector(".bk-option-comment");
233
+ if (!comment) return;
234
+ var selected =
235
+ item.classList.contains("selected") || item.classList.contains("checked");
236
+ comment.hidden = !selected;
237
+ });
238
+ }
239
+
240
+ function readControlValue(container) {
241
+ var control = container.querySelector("input, textarea");
242
+ if (!control) return "";
243
+ return String(control.value || "").trim();
244
+ }
245
+
246
+ function collectStandaloneFields(state) {
247
+ var fields = {};
248
+ var hasFields = false;
249
+
250
+ document.querySelectorAll(".bk-textarea, .bk-input").forEach(function (el) {
251
+ var name = el.dataset.name;
252
+ if (!name) return;
253
+ var value = readControlValue(el);
254
+ if (!value) return;
255
+ if (el.dataset.responseKey === "comment" || name === "comment") {
256
+ state.comment = value;
257
+ return;
258
+ }
259
+ fields[name] = value;
260
+ hasFields = true;
261
+ });
262
+
263
+ if (hasFields) state.fields = fields;
264
+ }
265
+
266
+ function collectOptionComments(state) {
267
+ var optionComments = {};
268
+ var optionCommentCount = 0;
269
+ var fields = state.fields || {};
270
+ var hasFields = !!state.fields;
271
+
272
+ document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
273
+ var selected =
274
+ item.classList.contains("selected") || item.classList.contains("checked");
275
+ if (!selected) return;
276
+ var commentWrap = item.querySelector(".bk-option-comment");
277
+ if (!commentWrap) return;
278
+ var textarea = commentWrap.querySelector("textarea");
279
+ var value = textarea ? String(textarea.value || "").trim() : "";
280
+ if (!value) return;
281
+ if (item.dataset.value) {
282
+ optionComments[item.dataset.value] = value;
283
+ optionCommentCount += 1;
284
+ }
285
+ if (item.dataset.commentName) {
286
+ fields[item.dataset.commentName] = value;
287
+ hasFields = true;
288
+ }
289
+ });
290
+
291
+ if (optionCommentCount > 0) state.option_comments = optionComments;
292
+ if (hasFields) state.fields = fields;
293
+ }
294
+
295
+ function getValidationError() {
296
+ var requiredFieldMissing = false;
297
+ document.querySelectorAll(".bk-textarea, .bk-input").forEach(function (el) {
298
+ if (!el.hasAttribute("data-required")) return;
299
+ if (!readControlValue(el)) requiredFieldMissing = true;
300
+ });
301
+ if (requiredFieldMissing) return t.requiredField;
302
+
303
+ var requiredCommentMissing = false;
304
+ document.querySelectorAll(COMMENTABLE_SELECTOR).forEach(function (item) {
305
+ var selected =
306
+ item.classList.contains("selected") || item.classList.contains("checked");
307
+ if (!selected || !item.hasAttribute("data-requires-comment")) return;
308
+ var commentWrap = item.querySelector(".bk-option-comment");
309
+ if (!commentWrap || !readControlValue(commentWrap)) {
310
+ requiredCommentMissing = true;
311
+ }
312
+ });
313
+ if (requiredCommentMissing) return t.requiredComment;
314
+
315
+ return "";
316
+ }
317
+
105
318
  /* ── Sliders ── */
106
319
  function bindSliders() {
107
320
  document.querySelectorAll(".bk-slider").forEach(function (container) {
@@ -345,6 +558,9 @@
345
558
  });
346
559
  if (hasMatrix) state.matrix = matrix;
347
560
 
561
+ collectStandaloneFields(state);
562
+ collectOptionComments(state);
563
+
348
564
  return state;
349
565
  }
350
566
 
@@ -365,11 +581,13 @@
365
581
  function updateSubmitState() {
366
582
  var btn = document.querySelector(".bk-vs-submit");
367
583
  if (!btn || btn.classList.contains("submitted")) return;
368
- /* Enable if user has interacted with any interactable component */
584
+ var status = document.querySelector(".bk-vs-status");
585
+ var validationError = getValidationError();
369
586
  var hasInteractable = document.querySelector(
370
- ".bk-options, .bk-cards, .bk-checklist, .bk-code-compare, .bk-slider, .bk-ranking, .bk-matrix, .bk-mockup-gallery, .bk-mockup-item",
587
+ ".bk-options, .bk-cards, .bk-checklist, .bk-code-compare, .bk-slider, .bk-ranking, .bk-matrix, .bk-mockup-gallery, .bk-mockup-item, .bk-textarea, .bk-input",
371
588
  );
372
- btn.disabled = !(hasInteractable && hasInteracted);
589
+ btn.disabled = !(hasInteractable && hasInteracted) || !!validationError;
590
+ if (status) status.textContent = validationError;
373
591
  }
374
592
 
375
593
  /* ── Boot ── */
@@ -21228,9 +21228,16 @@ SELECTION (collect choices):
21228
21228
 
21229
21229
  INPUT (collect values):
21230
21230
  - bk-slider: Numeric. data-name, data-min, data-max, data-value, data-unit on .bk-slider. Contains label.
21231
+ - bk-input: Short text input. data-name, data-label, data-placeholder, optional data-required, and optional data-response-key="comment".
21232
+ - bk-textarea: Long-form memo field. data-name, data-label, data-placeholder, optional data-required, and optional data-response-key="comment".
21231
21233
  - bk-ranking: Drag reorder. Wrap in .bk-ranking, each .bk-rank-item with data-value.
21232
21234
  - bk-matrix: 2x2 placement. .bk-matrix with data-x-label, data-y-label. Each .bk-matrix-item with data-value.
21233
21235
 
21236
+ OPTION-LEVEL FEEDBACK:
21237
+ - Any selectable item (.bk-option, .bk-card, .bk-code-option, .bk-mockup-item, .bk-check-item) may add data-requires-comment to force a memo before submit.
21238
+ - Optional data-comment-name stores that memo into response.fields[data-comment-name].
21239
+ - Optional data-comment-label and data-comment-placeholder customize the inline memo UI.
21240
+
21234
21241
  DISPLAY (no values):
21235
21242
  - bk-split: Side-by-side. Two .bk-split-panel inside .bk-split.
21236
21243
  - bk-pros-cons: .bk-pros + .bk-cons inside .bk-pros-cons.
@@ -21240,7 +21247,7 @@ DISPLAY (no values):
21240
21247
  Layout: h2 for title, .bk-subtitle, .bk-section for breaks, .bk-label for category.
21241
21248
 
21242
21249
  Response format (JSON via get_web_response):
21243
- {selections: ["a"], values: {budget: 70}, ranking: ["security","ux"], matrix: {auth: {x:0.8,y:0.9}}}
21250
+ {selections: ["a"], values: {budget: 70}, ranking: ["security","ux"], matrix: {auth: {x:0.8,y:0.9}}, comment: "Tighten the pricing section", fields: {change_request: "Add rainy-day fallback"}, option_comments: {b: "Keep this option but improve lodging details"}}
21244
21251
  Only populated fields are included.`,
21245
21252
  {
21246
21253
  task_id: { type: "number" },
@@ -21859,10 +21866,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21859
21866
  args
21860
21867
  );
21861
21868
  const createdId = created?.data?.id;
21869
+ let nodeVerification;
21870
+ if (createdId !== void 0 && Array.isArray(args.nodes)) {
21871
+ const expectedCount = args.nodes.length;
21872
+ const createdCount = Array.isArray(
21873
+ created?.data?.nodes
21874
+ ) ? (created.data?.nodes ?? []).length : 0;
21875
+ const verified = await client.request(
21876
+ "GET",
21877
+ `/api/workflows/${createdId}`
21878
+ );
21879
+ const verifiedCount = Array.isArray(verified?.data?.nodes) ? verified.data.nodes.length : 0;
21880
+ nodeVerification = {
21881
+ expected_count: expectedCount,
21882
+ created_count: createdCount,
21883
+ verified_count: verifiedCount,
21884
+ mismatch: createdCount !== expectedCount || verifiedCount !== expectedCount
21885
+ };
21886
+ }
21862
21887
  return wrap({
21863
21888
  ...created,
21864
21889
  ...createdId !== void 0 && {
21865
21890
  webui_url: `${apiUrl.replace(/\/$/, "")}/workflows/${createdId}`
21891
+ },
21892
+ ...nodeVerification && {
21893
+ node_verification: nodeVerification
21866
21894
  }
21867
21895
  });
21868
21896
  }
@@ -21884,13 +21912,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21884
21912
  const workflowId = requireNumberArg(args, "workflow_id");
21885
21913
  const body = { ...args };
21886
21914
  delete body.workflow_id;
21887
- return wrap(
21888
- await client.request(
21889
- "POST",
21890
- `/api/workflows/${workflowId}/nodes`,
21891
- body
21892
- )
21915
+ logNodeMutationAudit("append_node.request", {
21916
+ workflow_id: workflowId,
21917
+ payload: sanitizeNodeMutationPayload(body)
21918
+ });
21919
+ const beforeNodes = await fetchWorkflowNodeSnapshot(workflowId);
21920
+ const appended = await client.request(
21921
+ "POST",
21922
+ `/api/workflows/${workflowId}/nodes`,
21923
+ body
21893
21924
  );
21925
+ const afterNodes = await fetchWorkflowNodeSnapshot(workflowId);
21926
+ const verification = buildNodeMutationVerification({
21927
+ mutation: "append_node",
21928
+ beforeNodes,
21929
+ afterNodes,
21930
+ expectedDelta: 1,
21931
+ expectedNodeId: appended?.data?.id ?? null
21932
+ });
21933
+ logNodeMutationAudit("append_node.verification", {
21934
+ workflow_id: workflowId,
21935
+ verification
21936
+ });
21937
+ if (verification.mismatch) {
21938
+ logNodeMutationAudit("append_node.mismatch", {
21939
+ workflow_id: workflowId,
21940
+ payload: sanitizeNodeMutationPayload(body),
21941
+ verification
21942
+ });
21943
+ throw new Error(
21944
+ `append_node verification failed for workflow ${workflowId}: ${JSON.stringify(verification)}`
21945
+ );
21946
+ }
21947
+ return wrap({
21948
+ ...appended,
21949
+ node_verification: verification
21950
+ });
21894
21951
  }
21895
21952
  case "insert_node": {
21896
21953
  const workflowId = requireNumberArg(args, "workflow_id");
@@ -21898,13 +21955,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21898
21955
  const body = { ...args };
21899
21956
  delete body.workflow_id;
21900
21957
  delete body.after_step;
21901
- return wrap(
21902
- await client.request(
21903
- "POST",
21904
- `/api/workflows/${workflowId}/nodes?after=${afterStep}`,
21905
- body
21906
- )
21958
+ logNodeMutationAudit("insert_node.request", {
21959
+ workflow_id: workflowId,
21960
+ after_step: afterStep,
21961
+ payload: sanitizeNodeMutationPayload(body)
21962
+ });
21963
+ const beforeNodes = await fetchWorkflowNodeSnapshot(workflowId);
21964
+ const inserted = await client.request(
21965
+ "POST",
21966
+ `/api/workflows/${workflowId}/nodes?after=${afterStep}`,
21967
+ body
21907
21968
  );
21969
+ const afterNodes = await fetchWorkflowNodeSnapshot(workflowId);
21970
+ const verification = buildNodeMutationVerification({
21971
+ mutation: "insert_node",
21972
+ beforeNodes,
21973
+ afterNodes,
21974
+ expectedDelta: 1,
21975
+ expectedNodeId: inserted?.data?.id ?? null
21976
+ });
21977
+ logNodeMutationAudit("insert_node.verification", {
21978
+ workflow_id: workflowId,
21979
+ after_step: afterStep,
21980
+ verification
21981
+ });
21982
+ if (verification.mismatch) {
21983
+ logNodeMutationAudit("insert_node.mismatch", {
21984
+ workflow_id: workflowId,
21985
+ after_step: afterStep,
21986
+ payload: sanitizeNodeMutationPayload(body),
21987
+ verification
21988
+ });
21989
+ throw new Error(
21990
+ `insert_node verification failed for workflow ${workflowId}: ${JSON.stringify(verification)}`
21991
+ );
21992
+ }
21993
+ return wrap({
21994
+ ...inserted,
21995
+ node_verification: verification
21996
+ });
21908
21997
  }
21909
21998
  case "update_node": {
21910
21999
  const workflowId = requireNumberArg(args, "workflow_id");
@@ -21915,7 +22004,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21915
22004
  return wrap(
21916
22005
  await client.request(
21917
22006
  "PATCH",
21918
- `/api/workflows/${workflowId}/node-items/${nodeId}`,
22007
+ `/api/workflows/${workflowId}/nodes/${nodeId}`,
21919
22008
  body
21920
22009
  )
21921
22010
  );
@@ -21926,7 +22015,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21926
22015
  return wrap(
21927
22016
  await client.request(
21928
22017
  "DELETE",
21929
- `/api/workflows/${workflowId}/node-items/${nodeId}`
22018
+ `/api/workflows/${workflowId}/nodes/${nodeId}`
21930
22019
  )
21931
22020
  );
21932
22021
  }
@@ -22184,6 +22273,72 @@ function wrapError(message) {
22184
22273
  isError: true
22185
22274
  };
22186
22275
  }
22276
+ function logNodeMutationAudit(event, data) {
22277
+ const record2 = {
22278
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
22279
+ scope: "node-mutation-audit",
22280
+ event,
22281
+ ...data
22282
+ };
22283
+ console.error(JSON.stringify(record2));
22284
+ }
22285
+ function sanitizeNodeMutationPayload(payload) {
22286
+ const out = {};
22287
+ const allowedKeys = [
22288
+ "title",
22289
+ "node_type",
22290
+ "instruction",
22291
+ "instruction_id",
22292
+ "credential_id",
22293
+ "hitl",
22294
+ "visual_selection",
22295
+ "loop_back_to"
22296
+ ];
22297
+ for (const key of allowedKeys) {
22298
+ if (key in payload) {
22299
+ out[key] = payload[key];
22300
+ }
22301
+ }
22302
+ if (typeof out["instruction"] === "string") {
22303
+ out["instruction_preview"] = String(out["instruction"]).slice(0, 160);
22304
+ delete out["instruction"];
22305
+ }
22306
+ return out;
22307
+ }
22308
+ async function fetchWorkflowNodeSnapshot(workflowId) {
22309
+ const workflow = await client.request("GET", `/api/workflows/${workflowId}`);
22310
+ const nodes = Array.isArray(workflow?.data?.nodes) ? workflow.data.nodes : [];
22311
+ return nodes.map((node) => ({
22312
+ id: Number(node["id"]),
22313
+ step_order: typeof node["step_order"] === "number" ? node["step_order"] : null,
22314
+ title: typeof node["title"] === "string" ? node["title"] : null,
22315
+ node_type: typeof node["node_type"] === "string" ? node["node_type"] : null
22316
+ }));
22317
+ }
22318
+ function buildNodeMutationVerification(input) {
22319
+ const beforeIds = new Set(input.beforeNodes.map((node) => node.id));
22320
+ const addedNodes = input.afterNodes.filter((node) => !beforeIds.has(node.id));
22321
+ const expectedCount = input.beforeNodes.length + input.expectedDelta;
22322
+ const countDelta = input.afterNodes.length - input.beforeNodes.length;
22323
+ const mismatch = input.afterNodes.length !== expectedCount || countDelta !== input.expectedDelta || addedNodes.length !== input.expectedDelta || input.expectedNodeId !== null && !addedNodes.some((node) => node.id === input.expectedNodeId);
22324
+ return {
22325
+ mutation: input.mutation,
22326
+ before_count: input.beforeNodes.length,
22327
+ after_count: input.afterNodes.length,
22328
+ expected_count: expectedCount,
22329
+ delta: countDelta,
22330
+ expected_delta: input.expectedDelta,
22331
+ expected_node_id: input.expectedNodeId,
22332
+ added_node_ids: addedNodes.map((node) => node.id),
22333
+ added_nodes: addedNodes.map((node) => ({
22334
+ id: node.id,
22335
+ step_order: node.step_order,
22336
+ title: node.title,
22337
+ node_type: node.node_type
22338
+ })),
22339
+ mismatch
22340
+ };
22341
+ }
22187
22342
  async function scanRepoLocal(args) {
22188
22343
  const scanPath = args["path"];
22189
22344
  if (typeof scanPath !== "string" || scanPath.trim().length === 0) {
@@ -108,6 +108,15 @@ Call `create_workflow`:
108
108
  }
109
109
  ```
110
110
 
111
+ <HARD-RULE>
112
+ Immediately validate the `create_workflow` result before doing anything else:
113
+
114
+ - If the response includes `node_verification`, require `node_verification.mismatch === false`.
115
+ - If `nodes` were supplied to `create_workflow`, treat that call as the only allowed initial node creation step.
116
+ - Never append the same planned nodes after `create_workflow(nodes=[...])` because an older server response showed `data.nodes=[]`.
117
+ - If verification fails, STOP and investigate. Do not continue with `append_node` or `insert_node`.
118
+ </HARD-RULE>
119
+
111
120
  ### Step 6: Report Result + Open in Browser
112
121
 
113
122
  On success, open the workflow detail page in the browser:
@@ -336,11 +345,14 @@ Place two mockup cards side by side using display:grid;grid-template-columns:1fr
336
345
  ## Node Modification Strategy
337
346
 
338
347
  <HARD-RULE>
348
+ - Before any structural edit, refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
339
349
  - Update a single node → `update_node(workflow_id, node_id, ...only changed fields)`
340
350
  - Append a node (at the end) → `append_node(workflow_id, title, instruction, node_type, loop_back_to?, visual_selection?)`
341
351
  - Insert a node (in the middle) → `insert_node(workflow_id, after_step=N, title, instruction, node_type, loop_back_to?, visual_selection?)`
342
352
  - Delete a node → `remove_node(workflow_id, node_id)`
343
353
  - Never use `update_workflow(nodes=[...])` for full replacement unless a complete redesign is intended
354
+ - After every `append_node` or `insert_node`, inspect the returned `node_verification`.
355
+ - If `node_verification.mismatch === true`, STOP immediately and do not continue issuing more structural edits from the current plan snapshot.
344
356
  </HARD-RULE>
345
357
 
346
358
  ## Inline vs Template Instructions
@@ -191,6 +191,15 @@ Call `create_workflow`:
191
191
  }
192
192
  ```
193
193
 
194
+ <HARD-RULE>
195
+ Immediately validate the `create_workflow` result:
196
+
197
+ - If the response includes `node_verification`, require `node_verification.mismatch === false`.
198
+ - If `nodes` were supplied to `create_workflow`, do not follow with `append_node` for the same imported step set.
199
+ - Never treat an older `data.nodes=[]` response shape as evidence that the initial nodes were not created.
200
+ - If verification fails, STOP and investigate instead of patching the workflow incrementally.
201
+ </HARD-RULE>
202
+
194
203
  ## Step 8: Report Result + Open in Browser
195
204
 
196
205
  On success:
@@ -119,6 +119,15 @@ remove_node(workflow_id=67, node_id=115)
119
119
  When editing in place, apply each change as a separate `update_node`, `insert_node`, `append_node`, or `remove_node` call. Never use `update_workflow(nodes=[...])` for in-place edits — it replaces ALL nodes and loses node IDs.
120
120
  </HARD-RULE>
121
121
 
122
+ <HARD-RULE>
123
+ Before any structural edit session:
124
+
125
+ - Refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
126
+ - Do not rely on a stale node snapshot captured before other edits or user actions.
127
+ - After every `append_node` or `insert_node`, inspect the returned `node_verification`.
128
+ - If `node_verification.mismatch === true`, STOP immediately and do not continue applying edits from the current plan.
129
+ </HARD-RULE>
130
+
122
131
  After all changes, report:
123
132
 
124
133
  ```
@@ -186,9 +195,12 @@ When improving a node with `visual_selection: true`:
186
195
  ## Node Modification Strategy
187
196
 
188
197
  <HARD-RULE>
198
+ - Before any structural edit, refresh the current workflow structure from the server and use the latest node ids / step orders as the source of truth.
189
199
  - Update a single node → `update_node(workflow_id, node_id, ...only changed fields)`
190
200
  - Append a node (at the end) → `append_node(workflow_id, title, instruction, node_type, loop_back_to?, visual_selection?)`
191
201
  - Insert a node (in the middle) → `insert_node(workflow_id, after_step=N, title, instruction, node_type, loop_back_to?, visual_selection?)`
192
202
  - Delete a node → `remove_node(workflow_id, node_id)`
193
203
  - Never use `update_workflow(nodes=[...])` for full replacement unless a complete redesign is intended
204
+ - After every `append_node` or `insert_node`, inspect the returned `node_verification`.
205
+ - If `node_verification.mismatch === true`, STOP immediately and do not continue issuing more structural edits from the current plan snapshot.
194
206
  </HARD-RULE>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluekiwi",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "BlueKiwi CLI — install MCP client and skills into your agent runtime",
5
5
  "license": "MIT",
6
6
  "repository": {