@manny-est/node-red-flowpilot 0.4.1 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to FlowPilot are documented here.
4
4
 
5
+ ## [0.5.0] - 2026-07-06
6
+
7
+ ### Added
8
+ - **Slash autocomplete**: type `/` in the compose box and a panel shows all available commands with descriptions. Arrow keys, Tab, and Enter navigate; Escape dismisses.
9
+ - **Mode-suggestion chips**: FlowPilot detects when you describe a Generate, Modify, or Build task while in a different mode and offers a one-click chip to switch — no need to type the slash command manually. A chip also surfaces after a Generate response if the flow looks like it needs a follow-up Build loop.
10
+ - **Build loop — hold-at-next-step pacing**: a new Settings → Behavior toggle pauses the loop at each waypoint for review instead of auto-advancing. Useful for carefully inspecting each iteration.
11
+ - **Build loop — checkpoint questions**: the loop can now ask a focused clarifying question at any waypoint (e.g. "the debug output shows X — did you mean to test Y?") with prefilled quick-reply options, the same mechanism Modify uses. The loop only continues once you answer.
12
+ - **Build loop — context-aware start**: `/build` now reads your current selection as the starting context (same as `/modify`), letting you target an existing sub-flow to extend or fix rather than always building from scratch.
13
+ - **Build loop — done confirmation**: a "Done" confirmation step with an explicit success/fail verdict now closes the loop instead of silently stopping.
14
+ - **Pop-out — debug log**: the debug log panel is now fully mirrored in the pop-out window, including the "Clear debug" button and the "Attach N to context" indicator.
15
+ - **Pop-out — Recall and flight log**: the Recall panel and flight-log history are now accessible from the pop-out; opening a past conversation or recalling a message works the same as in the main window.
16
+ - **Pop-out — prompt resize handle**: the compose box in the pop-out can now be resized vertically, matching the main window.
17
+
18
+ ### Fixed
19
+ - **Partial id-validation** (`finalizeModifyResult`): a Modify response containing a mix of valid patches and one bad node id no longer rejects the whole response — valid patches are applied and the bad ones are silently dropped. Previously a single unrecognized id caused every change in the batch to be discarded.
20
+ - **Redaction round-trip poisoning** (issue #7): when a Modify request targets a field that FlowPilot redacted (e.g. an HTTP Authorization header), the model's response — echoing back the `[redacted: ...]` placeholder — previously produced a silent empty diff. Now the diff step recognises placeholder values, skips those fields, and if ALL proposed changes were redacted-field-only, shows an explicit warning explaining the limitation and instructing the user to edit that field directly in the node editor.
21
+ - **Token credential redaction**: `Authorization: Token <value>` headers (used by Django REST Framework and similar APIs) were not caught by the existing bearer-token pattern and could be sent to the model in plain text. The `Token <credential>` form is now recognised as a separate pattern.
22
+ - **Recursive sentinel check**: `isSanitizeSentinel` now walks into nested arrays and objects, so a placeholder buried inside a list property (e.g. `rules[0].v`) is correctly detected and skipped instead of producing a spurious empty diff.
23
+ - **Group data corruption**: a plain `changes` patch could reach the `nodes` membership array of a group container (since context exposes it as a plain field), overwriting the live group membership with a stringified copy. The `nodes` array is now in `DIFF_SKIP` so Modify patches never touch it directly.
24
+ - **Group create/extend on mixed-membership selections**: creating or extending a group when the selection contained nodes from more than one existing group (or some grouped and some ungrouped) could silently fail or corrupt membership. The membership reconciliation step now handles the mixed case correctly.
25
+ - **Invalid port wiring**: FlowPilot now guards against wiring to or from a port index that doesn't exist on a node (e.g. connecting output 3 of a node that only has 2 outputs) — the bad wire is skipped with a warning instead of being applied and then causing a Node-RED canvas error.
26
+ - **Pop-out — settings auth failure**: opening Settings from the pop-out could fail with a 404 or 401 in certain auth configurations because the settings request was issued from the pop-out's nested route rather than through the main window. All settings reads and writes are now relayed through the parent window.
27
+ - **Pop-out — recall and flight-log relay**: recall and flight-log API calls are now relayed through the parent window so they resolve correctly from the pop-out.
28
+
29
+ ### Internal
30
+ - Phase 9 refactor complete: `flowpilot-core.js` is now assembled at runtime from focused fragments under `lib/core/` (redaction, history, markdown, selection-context, apply-review, modes, main, init). The assembled script is functionally identical to the old single file; the split is purely for maintainability.
31
+
5
32
  ## [0.4.1] - 2026-06-29
6
33
 
7
34
  ### Fixed
@@ -115,13 +115,13 @@ including every step of the `/build` loop and every group mutation.
115
115
 
116
116
  - **`flowpilot.html`** — the editor-side plugin entry point: registers the
117
117
  sidebar panel and loads the shared client module below.
118
- - **`flowpilot-core.js`** — the shared client logic (chat/generate/modify/
119
- document/build, selection/context handling, settings UI, diff review and
120
- apply, group mutations) used by both the sidebar and the pop-out window,
121
- so they stay in sync by construction rather than by copy-pasted code.
122
- Served via a dedicated static route alongside the pop-out's own minimal
123
- page (`lib/popout/view.html`), which loads this same module and relays
124
- state to/from the main editor window over `postMessage`.
118
+ - **`lib/core/*.js`** — the shared client logic, split into focused fragments
119
+ (redaction, history, markdown, selection-context, apply-review, modes, main,
120
+ init) and assembled at request time by `lib/build-core-script.js` into one
121
+ complete IIFE served at `/flowpilot/core.js`. Used by both the sidebar and
122
+ the pop-out window so they stay in sync by construction. The pop-out's
123
+ minimal page (`lib/popout/view.html`) loads this same assembled module and
124
+ relays state to/from the main editor window over `postMessage`.
125
125
  - **`flowpilot.js`** — the Node-RED runtime plugin: HTTP routes
126
126
  (`/flowpilot/*`), provider calls, response parsing/validation, and audit
127
127
  logging.
package/README.md CHANGED
@@ -93,26 +93,32 @@ See the [User Guide](USER-GUIDE.md#privacy-and-safety) for the full details.
93
93
 
94
94
  Node-RED 4.x and 5.x, tested. Node.js 16+.
95
95
 
96
- ## Install in a local Node-RED user directory
96
+ ## Install
97
97
 
98
- From your Node-RED user directory:
98
+ In the Node-RED editor: **Menu → Manage palette → Install**, search for
99
+ `@manny-est/node-red-flowpilot`, and click **Install**.
100
+
101
+ Or, from your Node-RED user directory:
99
102
 
100
103
  ```bash
101
104
  cd ~/.node-red
102
- npm install /path/to/node-red-flowpilot
105
+ npm install @manny-est/node-red-flowpilot
103
106
  node-red
104
107
  ```
105
108
 
106
- For a Docker/container setup, place or install the package inside the
107
- mounted Node-RED user directory. If your user directory is
108
- `/data` (or `/workspaces/nodered`, etc.), this folder should exist:
109
+ Restart Node-RED (or the container) after installing or updating FlowPilot's
110
+ editor UI is cached server-side, so a browser refresh alone is not enough.
111
+
112
+ For a Docker/container setup, install into the mounted Node-RED user directory.
113
+ If your user directory is `/data` (or `/workspaces/nodered`, etc.), this folder
114
+ should exist:
109
115
 
110
116
  ```text
111
117
  <node-red-userDir>/node_modules/@manny-est/node-red-flowpilot
112
118
  ```
113
119
 
114
- Restart the Node-RED container/process after installing or updating
115
- plugin HTML is cached server-side, so a browser refresh alone is not enough.
120
+ Restart the Node-RED container/process after updating the package files
121
+ directly the plugin server caches assembled scripts in memory.
116
122
 
117
123
  FlowPilot stores its own settings and logs separately from the plugin code,
118
124
  under `<node-red-userDir>/flowpilot/`:
package/USER-GUIDE.md CHANGED
@@ -212,14 +212,23 @@ and apply like any other change.
212
212
  ### Build (the agentic loop)
213
213
 
214
214
  Describe a goal (e.g. "fetch the weather every hour and log it to a file")
215
- and arm **Build**, or type `/build <goal>` directly. FlowPilot plans the
216
- work, proposes a first flow, and once you apply and deploy it, walks an
217
- interactive loop with you: attach the resulting Debug output, FlowPilot
218
- reviews it against the goal, and either confirms it's working or proposes a
219
- fix repeating until it works or a configurable attempt limit is reached
220
- (**Settings Behavior Build loop iteration cap**). Every proposal and fix
221
- still goes through the same review-then-apply flow as Modify — FlowPilot
222
- never deploys for you.
215
+ and arm **Build**, or type `/build <goal>` directly. If you have nodes
216
+ selected, FlowPilot uses them as the starting context (like Modify) useful
217
+ for targeting an existing sub-flow to extend or fix.
218
+
219
+ FlowPilot plans the work, proposes a first flow, and once you apply and
220
+ deploy it, walks an interactive loop: attach the resulting Debug output,
221
+ FlowPilot reviews it against the goal, and either confirms it's working
222
+ (with a Done confirmation showing the debug output that proved success) or
223
+ proposes a fix — repeating until it works or a configurable attempt limit is
224
+ reached (**Settings → Behavior → Build loop iteration cap**). At any waypoint
225
+ FlowPilot may ask a focused clarifying question with quick-reply buttons
226
+ before continuing. Every proposal and fix still goes through the same
227
+ review-then-apply flow as Modify — FlowPilot never deploys for you.
228
+
229
+ **Hold at next step** (**Settings → Behavior**): when enabled, the loop pauses
230
+ at each waypoint for you to review before auto-advancing — useful for
231
+ careful, step-by-step inspection of each iteration.
223
232
 
224
233
  ### Document
225
234
 
@@ -263,13 +272,18 @@ them from there or by clicking **Clear chat**.
263
272
 
264
273
  ![A debug message attached, shown in the status strip](https://github.com/manny-est/flowpilot/releases/download/v0.2.1/debug-log-attached.png)
265
274
 
266
- ### Action chips
275
+ ### Action chips and mode suggestions
267
276
 
268
277
  When FlowPilot's reply describes a change you could make, it may offer an
269
278
  action chip — a one-click button that switches to the suggested mode
270
279
  (Generate/Modify/Document/Chat) with the request pre-filled. Nothing is sent
271
280
  until you review and hit Send yourself.
272
281
 
282
+ FlowPilot also watches what you type: if your prompt reads like a Generate,
283
+ Modify, or Build task but a different mode is armed, it surfaces a suggestion
284
+ chip below the reply so you can switch with one click instead of typing the
285
+ slash command manually.
286
+
273
287
  ![A reply with an action chip below it](https://github.com/manny-est/flowpilot/releases/download/v0.2.1/action-chip.png)
274
288
 
275
289
  ![The full chat thread leading up to an action chip](https://github.com/manny-est/flowpilot/releases/download/v0.2.1/action-chip-full.png)
@@ -303,7 +317,9 @@ Behavior → Stream chat replies**.
303
317
 
304
318
  ### Slash commands
305
319
 
306
- Type these directly into the prompt box:
320
+ Type `/` in the prompt box to see a floating panel listing all available
321
+ commands with descriptions. Arrow keys or Tab to highlight, Enter or Tab to
322
+ complete, Escape to dismiss.
307
323
 
308
324
  - `/help` — show the full command/feature briefing
309
325
  - `/generate`, `/document`, `/modify`, `/build` — arm that Execute mode
@@ -312,6 +328,7 @@ Type these directly into the prompt box:
312
328
  - `/history` — open the Flight log
313
329
  - `/settings` — open Settings
314
330
  - `/demo` — load a sample Generate request into the prompt box
331
+ - `/feedback` — links to the repo and issue tracker
315
332
  - `/compact` / `/expand` — hide/restore labels on the selected node(s),
316
333
  instant and deterministic, no AI round-trip
317
334
  - `/disable` / `/enable` — disable/re-enable the selected node(s) (skipped on
@@ -1056,6 +1056,50 @@
1056
1056
  background: rgba(127, 127, 127, 0.15);
1057
1057
  }
1058
1058
 
1059
+ /* Slash-command autocomplete panel */
1060
+ #fp-slash-suggest {
1061
+ position: absolute;
1062
+ bottom: 100%;
1063
+ left: 0;
1064
+ right: 0;
1065
+ margin-bottom: 4px;
1066
+ background: var(--red-ui-primary-background, #fff);
1067
+ border: 1px solid var(--red-ui-form-input-border-color, #ccc);
1068
+ border-radius: 6px;
1069
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
1070
+ overflow: hidden;
1071
+ z-index: 100;
1072
+ }
1073
+
1074
+ .fp-slash-row {
1075
+ display: flex;
1076
+ align-items: baseline;
1077
+ gap: 10px;
1078
+ padding: 6px 10px;
1079
+ cursor: pointer;
1080
+ user-select: none;
1081
+ }
1082
+
1083
+ .fp-slash-row:hover,
1084
+ .fp-slash-row.fp-slash-active {
1085
+ background: var(--red-ui-secondary-background, #f5f5f5);
1086
+ }
1087
+
1088
+ .fp-slash-cmd {
1089
+ font-weight: 600;
1090
+ font-size: 13px;
1091
+ min-width: 90px;
1092
+ color: var(--red-ui-primary-text-color, #333);
1093
+ }
1094
+
1095
+ .fp-slash-desc {
1096
+ font-size: 12px;
1097
+ color: var(--red-ui-secondary-text-color, #777);
1098
+ white-space: nowrap;
1099
+ overflow: hidden;
1100
+ text-overflow: ellipsis;
1101
+ }
1102
+
1059
1103
  #fp-provider-status {
1060
1104
  flex: 0 1 auto;
1061
1105
  min-width: 0;
package/flowpilot.js CHANGED
@@ -7,6 +7,8 @@ const documentSystemPrompt = require("./lib/document-system-prompt");
7
7
  const modifySystemPrompt = require("./lib/modify-system-prompt");
8
8
  const buildSystemPrompt = require("./lib/build-system-prompt");
9
9
  const personaPrompt = require("./lib/persona-prompt");
10
+ const { buildCoreScript } = require("./lib/build-core-script");
11
+ const { extractJsonObject } = require("./lib/envelope");
10
12
 
11
13
  module.exports = function flowPilotRuntime(RED) {
12
14
  const storage = createStorage(RED.settings.userDir);
@@ -625,6 +627,11 @@ module.exports = function flowPilotRuntime(RED) {
625
627
  // static lib/debug/view.html that loads the SAME debug-utils.js the
626
628
  // sidebar uses).
627
629
  //
630
+ // Phase 9 refactor: the SOURCE is now split into lib/core/*.js fragments
631
+ // (see lib/build-core-script.js for why and how), but this route's
632
+ // behavior is unchanged — it still serves one complete script at this
633
+ // same URL, just assembled instead of read off disk verbatim.
634
+ //
628
635
  // INTENTIONALLY UNGATED (fixed in 0.4.1 — was needsPermission("settings.
629
636
  // read") in 0.4.0, which broke the editor on every adminAuth-enabled
630
637
  // instance): these are static client assets, fetched via plain <script
@@ -639,7 +646,7 @@ module.exports = function flowPilotRuntime(RED) {
639
646
  // routes (settings, chat, generate, modify, etc. below) stay gated.
640
647
 
641
648
  RED.httpAdmin.get("/flowpilot/core.js", function (req, res) {
642
- res.sendFile(path.join(__dirname, "flowpilot-core.js"));
649
+ res.type("application/javascript").send(buildCoreScript());
643
650
  });
644
651
 
645
652
  RED.httpAdmin.get("/flowpilot/core.css", function (req, res) {
@@ -963,125 +970,6 @@ module.exports = function flowPilotRuntime(RED) {
963
970
  // types or wire integrity yet (that's the next chunk) — it returns the parsed
964
971
  // envelope so the frontend can display it for review.
965
972
 
966
- // Given s[startIdx] === "{", scans forward with brace-depth counting that
967
- // ignores braces inside string literals (so a value like "{{payload}}"
968
- // can't be mistaken for structure) to find the index of the MATCHING
969
- // closing "}". Returns -1 if the braces never balance before the string
970
- // ends (truncated/malformed input).
971
- function findMatchingBrace(s, startIdx) {
972
- let depth = 0;
973
- let inString = false;
974
- let escaped = false;
975
- for (let i = startIdx; i < s.length; i++) {
976
- const ch = s[i];
977
- if (inString) {
978
- if (escaped) { escaped = false; }
979
- else if (ch === "\\") { escaped = true; }
980
- else if (ch === "\"") { inString = false; }
981
- continue;
982
- }
983
- if (ch === "\"") { inString = true; }
984
- else if (ch === "{") { depth++; }
985
- else if (ch === "}") {
986
- depth--;
987
- if (depth === 0) { return i; }
988
- }
989
- }
990
- return -1;
991
- }
992
-
993
- function extractJsonObject(text) {
994
- if (!text) { throw new Error("Empty response from provider."); }
995
- let s = String(text).trim();
996
- // Strip markdown code fences if the model wrapped the JSON.
997
- s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
998
-
999
- const firstBrace = s.indexOf("{");
1000
- const firstBracket = s.indexOf("[");
1001
-
1002
- // The model occasionally returns a bare top-level array (e.g.
1003
- // `[ {...node...} ]`) instead of the {explanation, flow} envelope. If we
1004
- // fell through to the {...} extraction below, indexOf("{")/lastIndexOf("}")
1005
- // would grab just the first node object — which has no "flow" key and
1006
- // fails validation. Detect this case up front and wrap it as a minimal
1007
- // envelope instead.
1008
- if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
1009
- const lastBracket = s.lastIndexOf("]");
1010
- if (lastBracket !== -1 && lastBracket > firstBracket) {
1011
- try {
1012
- const arr = JSON.parse(s.slice(firstBracket, lastBracket + 1));
1013
- if (Array.isArray(arr)) {
1014
- return { explanation: "", flow: arr };
1015
- }
1016
- } catch (e) {
1017
- // Not a parseable array — fall through to the {...} extraction.
1018
- }
1019
- }
1020
- }
1021
-
1022
- const firstObjIdx = s.indexOf("{");
1023
- if (firstObjIdx === -1) {
1024
- // No JSON object found at all — flagged separately from a found-
1025
- // but-unparseable ({...} present, JSON.parse failed) "garbled" error,
1026
- // so callers can distinguish "model just answered in prose" (tolerate)
1027
- // from "model's JSON envelope is broken" (still an error).
1028
- const err = new Error("Provider did not return a JSON object.");
1029
- err.noJsonFound = true;
1030
- throw err;
1031
- }
1032
-
1033
- // There may be more than one "{" before the real envelope — e.g. prose
1034
- // explaining a fix that mentions inline code like "{{payload}}" before
1035
- // the actual JSON (seen live: a review response started with "The
1036
- // template node is using `{{payload}}` with...", and slicing from THAT
1037
- // brace to the envelope's real closing "}" produced unparseable
1038
- // garbage). Try each candidate "{" in order with string-aware brace
1039
- // matching (findMatchingBrace, which ignores braces inside quoted
1040
- // strings) rather than just slicing from the first "{" to the last
1041
- // "}".
1042
- //
1043
- // A candidate must not just PARSE, it must also look like one of the
1044
- // known envelope shapes (have at least one recognized top-level key) —
1045
- // seen live: a pure-prose advice response that mentioned structured
1046
- // logging included the illustrative example
1047
- // `{"level":"info","event":"trivia_answer","user":"alex","correct":true}`,
1048
- // which IS valid standalone JSON, so the old "first candidate that
1049
- // parses wins" rule accepted it as "the envelope" and the caller threw
1050
- // "no recognizable modify fields" — when the right answer was to treat
1051
- // the whole reply as prose, since there was no real envelope at all.
1052
- const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
1053
- function looksLikeEnvelope(obj) {
1054
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
1055
- return ENVELOPE_KEYS.some(function (k) { return k in obj; });
1056
- }
1057
-
1058
- let lastError = null;
1059
- let searchFrom = firstObjIdx;
1060
- while (searchFrom !== -1 && searchFrom < s.length) {
1061
- const end = findMatchingBrace(s, searchFrom);
1062
- if (end !== -1) {
1063
- try {
1064
- const candidate = JSON.parse(s.slice(searchFrom, end + 1));
1065
- if (looksLikeEnvelope(candidate)) { return candidate; }
1066
- // Valid JSON, but not envelope-shaped (e.g. an illustrative
1067
- // example embedded in prose) — keep searching rather than
1068
- // accepting it.
1069
- } catch (e) {
1070
- lastError = e;
1071
- }
1072
- }
1073
- searchFrom = s.indexOf("{", searchFrom + 1);
1074
- }
1075
- // No candidate both parsed AND looked like a real envelope — equivalent
1076
- // to "the model just answered in prose," not "the envelope is broken."
1077
- // Let callers fall back to rendering this as a normal message instead
1078
- // of surfacing a parse error (same noJsonFound flag the "no { at all"
1079
- // branch above uses).
1080
- const err = lastError || new Error("Provider's JSON object could not be parsed.");
1081
- err.noJsonFound = true;
1082
- throw err;
1083
- }
1084
-
1085
973
  // ---------------------------------------------------------------------
1086
974
  // Shared helper: resolve the active provider and assemble the messages
1087
975
  // array for a generation-style request (generate/document/modify). Split
@@ -1538,29 +1426,42 @@ module.exports = function flowPilotRuntime(RED) {
1538
1426
  // Validate that changes contains no hallucinated ids, and that no id is
1539
1427
  // both patched and marked for removal. A group id from contextGroupIds
1540
1428
  // is allowed here too (see above) even though it's not in originalIds.
1429
+ // Instead of rejecting the whole response when some ids are bad, drop
1430
+ // only the offending patches and apply the rest — same philosophy as the
1431
+ // applyInsertions partial-failure fix (Phase 8.5 #11). A skippedNote in
1432
+ // the response body tells the user what was dropped and why.
1541
1433
  const extraIds = changeIds.filter(function (id) { return !originalIds.has(String(id)) && !contextGroupIds.has(String(id)); });
1542
1434
  const wronglyRemovedIds = changeIds.filter(function (id) { return removeSet.has(String(id)); });
1543
1435
 
1544
- const idProblems = [];
1545
- if (extraIds.length) { idProblems.push("unexpected id(s) in changes: " + extraIds.join(", ")); }
1546
- if (wronglyRemovedIds.length) { idProblems.push("id(s) in both changes and removeNodes: " + wronglyRemovedIds.join(", ")); }
1547
-
1548
- if (idProblems.length > 0) {
1549
- storage.appendAudit({ action: "modify_id_mismatch", problems: idProblems });
1550
- return {
1551
- status: 422,
1552
- body: {
1553
- error: "The model returned inconsistent node ids (" + idProblems.join("; ") + "). Try again.",
1554
- raw: JSON.stringify(changes)
1555
- }
1556
- };
1436
+ const skippedDescriptions = [];
1437
+ if (extraIds.length) {
1438
+ skippedDescriptions.push(
1439
+ extraIds.length === 1
1440
+ ? "Skipped 1 change — that node wasn't in your current selection; reselect it to include it"
1441
+ : "Skipped " + extraIds.length + " changes — those nodes weren't in your current selection; reselect them to include them"
1442
+ );
1443
+ }
1444
+ if (wronglyRemovedIds.length) {
1445
+ skippedDescriptions.push(
1446
+ wronglyRemovedIds.length === 1
1447
+ ? "Skipped 1 change — that node was also marked for removal"
1448
+ : "Skipped " + wronglyRemovedIds.length + " changes — those nodes were also marked for removal"
1449
+ );
1557
1450
  }
1451
+ if (skippedDescriptions.length > 0) {
1452
+ storage.appendAudit({ action: "modify_id_mismatch_partial", skipped_extra: extraIds, skipped_conflict: wronglyRemovedIds });
1453
+ }
1454
+
1455
+ const badIdSet = new Set(extraIds.map(String).concat(wronglyRemovedIds.map(String)));
1456
+ const validChanges = changes.filter(function (c) {
1457
+ return c && c.id !== undefined && c.id !== null && !badIdSet.has(String(c.id));
1458
+ });
1558
1459
 
1559
1460
  // Each patch's "set" is shallow-merged onto a copy of the original node.
1560
1461
  // "id", "x", "y", "z" can never move via a patch — strip them
1561
1462
  // defensively even though the prompt already forbids them.
1562
1463
  const patchById = {};
1563
- changes.forEach(function (c) {
1464
+ validChanges.forEach(function (c) {
1564
1465
  const set = (c.set && typeof c.set === "object") ? c.set : {};
1565
1466
  const clean = Object.assign({}, set);
1566
1467
  delete clean.id;
@@ -1587,7 +1488,8 @@ module.exports = function flowPilotRuntime(RED) {
1587
1488
  // is, and findLiveNode() already resolves a group id to the live
1588
1489
  // group object (Phase 8.5 C2 slice 1). This is how a group gets
1589
1490
  // renamed/restyled — pure property edit, no new apply-side code.
1590
- const groupPatchIds = changeIds.filter(function (id) {
1491
+ const validChangeIds = validChanges.map(function (c) { return c.id; });
1492
+ const groupPatchIds = validChangeIds.filter(function (id) {
1591
1493
  return contextGroupIds.has(String(id)) && !originalIds.has(String(id));
1592
1494
  });
1593
1495
  groupPatchIds.forEach(function (id) {
@@ -1698,6 +1600,7 @@ module.exports = function flowPilotRuntime(RED) {
1698
1600
  removeNodes: finalRemoveNodes,
1699
1601
  newGroups: newGroups
1700
1602
  };
1603
+ if (skippedDescriptions.length > 0) { body.skippedNote = skippedDescriptions.join(". ") + "."; }
1701
1604
  if (result.suggestedAction) { body.suggestedAction = result.suggestedAction; }
1702
1605
 
1703
1606
  return { status: 200, body: body };
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // flowpilot-core.js is loaded directly in the BROWSER via a single
7
+ // <script src="flowpilot/core.js"> tag - one big IIFE, no module system,
8
+ // every function/var sharing one closure by lexical scope (not via
9
+ // require/import). Phase 9's refactor splits the SOURCE into focused files
10
+ // under lib/core/ for maintainability, but the served SCRIPT must stay
11
+ // exactly what it was: one concatenated text, so cross-fragment references
12
+ // keep working unmodified and script-loading/timing behavior never changes.
13
+ //
14
+ // Order matters only for top-level code that runs immediately at load time
15
+ // (e.g. a `var x = (function(){...})();` initializer) - plain function
16
+ // declarations are hoisted within the shared closure and safe in any
17
+ // order. Keep new fragments appended in the same relative order they held
18
+ // in the original single file unless a specific dependency says otherwise.
19
+ const FRAGMENT_ORDER = [
20
+ "redaction.js",
21
+ "history.js",
22
+ "markdown.js",
23
+ "selection-context.js",
24
+ "apply-review.js",
25
+ "modes.js",
26
+ "main.js",
27
+ "init.js"
28
+ ];
29
+
30
+ const HEADER = "(function () {\n \"use strict\";\n";
31
+ const FOOTER = "\n})();\n";
32
+
33
+ let cached = null;
34
+
35
+ function buildCoreScript() {
36
+ if (cached) { return cached; }
37
+ const body = FRAGMENT_ORDER.map(function (name) {
38
+ return fs.readFileSync(path.join(__dirname, "core", name), "utf8");
39
+ }).join("\n");
40
+ cached = HEADER + body + FOOTER;
41
+ return cached;
42
+ }
43
+
44
+ module.exports = { buildCoreScript: buildCoreScript };
@@ -25,6 +25,12 @@ Example "explanation" for a trivial goal:
25
25
 
26
26
  Deploy and trigger it; let me know what the debug output shows."
27
27
 
28
+ ALWAYS include at least one debug node in your proposed flow so the test-and-review cycle has something to observe. This is REQUIRED, not optional. Specific rules:
29
+ - HTTP endpoint flows (http in → function/change → http response): add a debug node tapping the function/change output BEFORE the http response node — wire the function/change output to BOTH the debug node AND the http response node.
30
+ - Inject-triggered flows: the debug node at the end is fine.
31
+ - Any other flow shape: add a debug node at the last meaningful output point.
32
+ Never generate a build flow without a debug node. The loop cannot review what it cannot see.
33
+
28
34
  Everything below describes the envelope/rules for THIS step specifically — they work exactly as written, including the parts that say "Generate mode": for the purposes of this prompt, treat that phrase as describing this build step, not a separate mode. The "explanation" field's content rules below still apply — your "Plan:" block comes first, then that content follows immediately after it in the same field.
29
35
 
30
36
  ---