@manny-est/node-red-flowpilot 0.4.0 → 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,39 @@
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
+
32
+ ## [0.4.1] - 2026-06-29
33
+
34
+ ### Fixed
35
+ - **Critical**: the editor hung at "Loading plugins" on any Node-RED instance with `adminAuth` enabled, due to a 401 on `/flowpilot/core.js`. 0.4.0's pop-out refactor moved FlowPilot's frontend into a separately-served script, but the route serving it (along with `core.css` and the pop-out's `view.html`) was gated behind `RED.auth.needsPermission(...)` — a check that requires an `Authorization` header, which a plain `<script src>`/`<link>`/`window.open` request can never carry. These three static client-asset routes are no longer gated (they contain no secrets); every data/action route is unaffected and still requires authentication exactly as before.
36
+ - **Critical**, found while verifying the fix above: every settings/chat/generate/document/modify/build request (anything going through the shared `ajaxJson` helper) also failed with "Unauthorized" on `adminAuth` instances, for the same root cause — these requests use an absolute URL (needed for the pop-out to resolve correctly), and Node-RED's editor only auto-attaches the admin auth token to relative URLs. The SSE-streaming `fetch()` calls already worked around this (`fetchHeaders()`); `ajaxJson` now attaches the same bearer token itself.
37
+
5
38
  ## [0.4.0] - 2026-06-27
6
39
 
7
40
  ### Added
@@ -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);
@@ -623,18 +625,35 @@ module.exports = function flowPilotRuntime(RED) {
623
625
  // pop-out's own minimal page — mirroring core Node-RED's debug-node
624
626
  // pattern (RED.httpAdmin.get("/debug/view/view.html", ...) serving a
625
627
  // static lib/debug/view.html that loads the SAME debug-utils.js the
626
- // sidebar uses). Gated the same as every other FlowPilot route, unlike
627
- // NR5's own debug view route, which has no permission check at all.
628
-
629
- RED.httpAdmin.get("/flowpilot/core.js", RED.auth.needsPermission("settings.read"), function (req, res) {
630
- res.sendFile(path.join(__dirname, "flowpilot-core.js"));
628
+ // sidebar uses).
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
+ //
635
+ // INTENTIONALLY UNGATED (fixed in 0.4.1 — was needsPermission("settings.
636
+ // read") in 0.4.0, which broke the editor on every adminAuth-enabled
637
+ // instance): these are static client assets, fetched via plain <script
638
+ // src>/<link>/window.open — none of which can carry the admin auth
639
+ // bearer token (that only gets attached to FlowPilot's own ajax/fetch
640
+ // calls, via Node-RED's editor-side request wrapper). needsPermission's
641
+ // bearer/tokens/anon strategies have no fallback for a request with no
642
+ // Authorization header, so the gate 401's unconditionally for this kind
643
+ // of request — confirmed against @node-red/editor-api's auth middleware.
644
+ // This is the same reason NR5's own debug-view route has no permission
645
+ // check either. No secrets live in these files; the real data/action
646
+ // routes (settings, chat, generate, modify, etc. below) stay gated.
647
+
648
+ RED.httpAdmin.get("/flowpilot/core.js", function (req, res) {
649
+ res.type("application/javascript").send(buildCoreScript());
631
650
  });
632
651
 
633
- RED.httpAdmin.get("/flowpilot/core.css", RED.auth.needsPermission("settings.read"), function (req, res) {
652
+ RED.httpAdmin.get("/flowpilot/core.css", function (req, res) {
634
653
  res.sendFile(path.join(__dirname, "flowpilot-core.css"));
635
654
  });
636
655
 
637
- RED.httpAdmin.get("/flowpilot/popout/view.html", RED.auth.needsPermission("settings.read"), function (req, res) {
656
+ RED.httpAdmin.get("/flowpilot/popout/view.html", function (req, res) {
638
657
  res.sendFile(path.join(__dirname, "lib", "popout", "view.html"));
639
658
  });
640
659
 
@@ -951,125 +970,6 @@ module.exports = function flowPilotRuntime(RED) {
951
970
  // types or wire integrity yet (that's the next chunk) — it returns the parsed
952
971
  // envelope so the frontend can display it for review.
953
972
 
954
- // Given s[startIdx] === "{", scans forward with brace-depth counting that
955
- // ignores braces inside string literals (so a value like "{{payload}}"
956
- // can't be mistaken for structure) to find the index of the MATCHING
957
- // closing "}". Returns -1 if the braces never balance before the string
958
- // ends (truncated/malformed input).
959
- function findMatchingBrace(s, startIdx) {
960
- let depth = 0;
961
- let inString = false;
962
- let escaped = false;
963
- for (let i = startIdx; i < s.length; i++) {
964
- const ch = s[i];
965
- if (inString) {
966
- if (escaped) { escaped = false; }
967
- else if (ch === "\\") { escaped = true; }
968
- else if (ch === "\"") { inString = false; }
969
- continue;
970
- }
971
- if (ch === "\"") { inString = true; }
972
- else if (ch === "{") { depth++; }
973
- else if (ch === "}") {
974
- depth--;
975
- if (depth === 0) { return i; }
976
- }
977
- }
978
- return -1;
979
- }
980
-
981
- function extractJsonObject(text) {
982
- if (!text) { throw new Error("Empty response from provider."); }
983
- let s = String(text).trim();
984
- // Strip markdown code fences if the model wrapped the JSON.
985
- s = s.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
986
-
987
- const firstBrace = s.indexOf("{");
988
- const firstBracket = s.indexOf("[");
989
-
990
- // The model occasionally returns a bare top-level array (e.g.
991
- // `[ {...node...} ]`) instead of the {explanation, flow} envelope. If we
992
- // fell through to the {...} extraction below, indexOf("{")/lastIndexOf("}")
993
- // would grab just the first node object — which has no "flow" key and
994
- // fails validation. Detect this case up front and wrap it as a minimal
995
- // envelope instead.
996
- if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
997
- const lastBracket = s.lastIndexOf("]");
998
- if (lastBracket !== -1 && lastBracket > firstBracket) {
999
- try {
1000
- const arr = JSON.parse(s.slice(firstBracket, lastBracket + 1));
1001
- if (Array.isArray(arr)) {
1002
- return { explanation: "", flow: arr };
1003
- }
1004
- } catch (e) {
1005
- // Not a parseable array — fall through to the {...} extraction.
1006
- }
1007
- }
1008
- }
1009
-
1010
- const firstObjIdx = s.indexOf("{");
1011
- if (firstObjIdx === -1) {
1012
- // No JSON object found at all — flagged separately from a found-
1013
- // but-unparseable ({...} present, JSON.parse failed) "garbled" error,
1014
- // so callers can distinguish "model just answered in prose" (tolerate)
1015
- // from "model's JSON envelope is broken" (still an error).
1016
- const err = new Error("Provider did not return a JSON object.");
1017
- err.noJsonFound = true;
1018
- throw err;
1019
- }
1020
-
1021
- // There may be more than one "{" before the real envelope — e.g. prose
1022
- // explaining a fix that mentions inline code like "{{payload}}" before
1023
- // the actual JSON (seen live: a review response started with "The
1024
- // template node is using `{{payload}}` with...", and slicing from THAT
1025
- // brace to the envelope's real closing "}" produced unparseable
1026
- // garbage). Try each candidate "{" in order with string-aware brace
1027
- // matching (findMatchingBrace, which ignores braces inside quoted
1028
- // strings) rather than just slicing from the first "{" to the last
1029
- // "}".
1030
- //
1031
- // A candidate must not just PARSE, it must also look like one of the
1032
- // known envelope shapes (have at least one recognized top-level key) —
1033
- // seen live: a pure-prose advice response that mentioned structured
1034
- // logging included the illustrative example
1035
- // `{"level":"info","event":"trivia_answer","user":"alex","correct":true}`,
1036
- // which IS valid standalone JSON, so the old "first candidate that
1037
- // parses wins" rule accepted it as "the envelope" and the caller threw
1038
- // "no recognizable modify fields" — when the right answer was to treat
1039
- // the whole reply as prose, since there was no real envelope at all.
1040
- const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
1041
- function looksLikeEnvelope(obj) {
1042
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
1043
- return ENVELOPE_KEYS.some(function (k) { return k in obj; });
1044
- }
1045
-
1046
- let lastError = null;
1047
- let searchFrom = firstObjIdx;
1048
- while (searchFrom !== -1 && searchFrom < s.length) {
1049
- const end = findMatchingBrace(s, searchFrom);
1050
- if (end !== -1) {
1051
- try {
1052
- const candidate = JSON.parse(s.slice(searchFrom, end + 1));
1053
- if (looksLikeEnvelope(candidate)) { return candidate; }
1054
- // Valid JSON, but not envelope-shaped (e.g. an illustrative
1055
- // example embedded in prose) — keep searching rather than
1056
- // accepting it.
1057
- } catch (e) {
1058
- lastError = e;
1059
- }
1060
- }
1061
- searchFrom = s.indexOf("{", searchFrom + 1);
1062
- }
1063
- // No candidate both parsed AND looked like a real envelope — equivalent
1064
- // to "the model just answered in prose," not "the envelope is broken."
1065
- // Let callers fall back to rendering this as a normal message instead
1066
- // of surfacing a parse error (same noJsonFound flag the "no { at all"
1067
- // branch above uses).
1068
- const err = lastError || new Error("Provider's JSON object could not be parsed.");
1069
- err.noJsonFound = true;
1070
- throw err;
1071
- }
1072
-
1073
973
  // ---------------------------------------------------------------------
1074
974
  // Shared helper: resolve the active provider and assemble the messages
1075
975
  // array for a generation-style request (generate/document/modify). Split
@@ -1526,29 +1426,42 @@ module.exports = function flowPilotRuntime(RED) {
1526
1426
  // Validate that changes contains no hallucinated ids, and that no id is
1527
1427
  // both patched and marked for removal. A group id from contextGroupIds
1528
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.
1529
1433
  const extraIds = changeIds.filter(function (id) { return !originalIds.has(String(id)) && !contextGroupIds.has(String(id)); });
1530
1434
  const wronglyRemovedIds = changeIds.filter(function (id) { return removeSet.has(String(id)); });
1531
1435
 
1532
- const idProblems = [];
1533
- if (extraIds.length) { idProblems.push("unexpected id(s) in changes: " + extraIds.join(", ")); }
1534
- if (wronglyRemovedIds.length) { idProblems.push("id(s) in both changes and removeNodes: " + wronglyRemovedIds.join(", ")); }
1535
-
1536
- if (idProblems.length > 0) {
1537
- storage.appendAudit({ action: "modify_id_mismatch", problems: idProblems });
1538
- return {
1539
- status: 422,
1540
- body: {
1541
- error: "The model returned inconsistent node ids (" + idProblems.join("; ") + "). Try again.",
1542
- raw: JSON.stringify(changes)
1543
- }
1544
- };
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
+ );
1545
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
+ });
1546
1459
 
1547
1460
  // Each patch's "set" is shallow-merged onto a copy of the original node.
1548
1461
  // "id", "x", "y", "z" can never move via a patch — strip them
1549
1462
  // defensively even though the prompt already forbids them.
1550
1463
  const patchById = {};
1551
- changes.forEach(function (c) {
1464
+ validChanges.forEach(function (c) {
1552
1465
  const set = (c.set && typeof c.set === "object") ? c.set : {};
1553
1466
  const clean = Object.assign({}, set);
1554
1467
  delete clean.id;
@@ -1575,7 +1488,8 @@ module.exports = function flowPilotRuntime(RED) {
1575
1488
  // is, and findLiveNode() already resolves a group id to the live
1576
1489
  // group object (Phase 8.5 C2 slice 1). This is how a group gets
1577
1490
  // renamed/restyled — pure property edit, no new apply-side code.
1578
- const groupPatchIds = changeIds.filter(function (id) {
1491
+ const validChangeIds = validChanges.map(function (c) { return c.id; });
1492
+ const groupPatchIds = validChangeIds.filter(function (id) {
1579
1493
  return contextGroupIds.has(String(id)) && !originalIds.has(String(id));
1580
1494
  });
1581
1495
  groupPatchIds.forEach(function (id) {
@@ -1686,6 +1600,7 @@ module.exports = function flowPilotRuntime(RED) {
1686
1600
  removeNodes: finalRemoveNodes,
1687
1601
  newGroups: newGroups
1688
1602
  };
1603
+ if (skippedDescriptions.length > 0) { body.skippedNote = skippedDescriptions.join(". ") + "."; }
1689
1604
  if (result.suggestedAction) { body.suggestedAction = result.suggestedAction; }
1690
1605
 
1691
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
  ---