diff --git a/claude-skills/backfill/SKILL.md b/claude-skills/backfill/SKILL.md
index 60b2f3f..d416c02 100644
--- a/claude-skills/backfill/SKILL.md
+++ b/claude-skills/backfill/SKILL.md
@@ -3,7 +3,7 @@ name: backfill
description: 'FluencyLoop safety net. Reconstruct store records for work that shipped without going through the loop — reads a merged diff, records the feature, session, decisions, knowledge, and architectural concepts, and defaults reconstructed decisions to unverified. Use post-merge, or when the user says "fluencyloop backfill", "document this PR after the fact", or "we skipped the loop on this one".'
---
-# fluencyloop-backfill — reconstruct, make fluent, then flag
+# /fluencyloop:backfill — reconstruct, make fluent, then flag
FluencyLoop never blocks a merge. The safety net for work that skipped the loop is
**post-merge backfill**: it gives ad-hoc work a home retroactively. Backfilled rationale
diff --git a/claude-skills/diagram-design/SKILL.md b/claude-skills/diagram-design/SKILL.md
index d7372e4..345ac39 100644
--- a/claude-skills/diagram-design/SKILL.md
+++ b/claude-skills/diagram-design/SKILL.md
@@ -19,36 +19,33 @@ Do not load the full guide, ask the user to choose a palette, tour templates, or
alternatives. FluencyLoop owns the surrounding reader design. Produce one restrained,
self-contained embedded HTML file with inline SVG and CSS.
-Work in one bounded pass:
-
-1. Choose the type without asking: use **architecture** for component ownership or system shape,
- **flowchart** for a directed interaction or decision path, and **sequence** only when message
- order between distinct actors is the point. For an architectural record, apply the same rule to
- its ADR mechanism. Load only that one type reference.
-2. Use 4–7 nodes and 3–8 connectors. Show only the relationship that earns the diagram; a product
- overview must not become a feature inventory. For an **architecture** diagram, default to
- unlabelled arrows: a small diagram should express routine direction through layout and node
- subtitles, not cramped connector text. This never overrides the selected type's grammar: label
- every decision exit in a flowchart and every message in a sequence diagram. Never abbreviate a
- label merely to make it fit.
-3. Write directly to the requested path. For `product-overview.html`, keep `product.md` as prose;
- do not add a Mermaid duplicate of the HTML diagram. Use only inline SVG and CSS with system
- font stacks: no Google Fonts ``, remote `src`/`href`, CSS `url(...)`, scripts, or iframes.
- Keep the existing light palette as the default. Use `--diagram-canvas`, `--diagram-surface`,
- `--diagram-ink`, `--diagram-muted`, `--diagram-rule`, and `--diagram-accent` throughout, then
- add a restrained `:root[data-fluencyloop-theme="dark"]` variable set. The reader supplies that
- attribute; do not use JavaScript or a system-preference media query.
-4. Draw connectors before cards. Use a straight connector only for aligned endpoints; otherwise use
- a rounded orthogonal route. Never overlap connector paths or reuse an attach point for separate
- connectors. A connector or its label must never run behind a non-endpoint card. If a label is
- necessary, place it only in a clear lane: give its opaque background mask at least 8px of visible
- space from both the connector and every card. If no lane exists, omit the label or change the
- layout; do not shrink, clip, or place text beneath a card.
-5. Confirm the file is nonempty, then run `fluencyloop site --ensure --open-once --json` when
- available so the reader can show it without opening a duplicate tab. Inspect the rendered result before handing off: every label
- must be readable, with no text behind a card, connector overlap, or viewBox clipping. Do not
- block the feature if Node is unavailable; say that the prose is available and the diagram will
- appear when the optional site can run.
+Use the native diagram renderer, not the general diagram workflow:
+
+1. Choose one supported layout without asking: `linear` for a 2–6 step path, `hub` for a shared
+ service/boundary with 2–7 participants, or `layered` for independent adjacent-layer mappings.
+ It supports 2–8 nodes and at most 10 edges. A fan-out or merge uses `hub`, never `layered`.
+2. Run exactly one `fluencyloop diagram` command. Give each node its short id, label, and detail as
+ separate fields, then give the directed edges. For example:
+
+ ```bash
+ fluencyloop diagram --output docs/fluencyloop/diagrams/product-overview.html --layout hub \
+ --title "Dog selection" --hub selection \
+ --node list --label "Dog list" --detail "Chooses a dog" \
+ --node selection --label "Selection service" --detail "Owns selected dog" \
+ --node detail --label "Dog detail" --detail "Reads selected dog" \
+ --edge list selection --edge selection detail
+ ```
+
+ Keep labels at 28 characters or fewer and details at 42 or fewer. The renderer owns canvas
+ height, card positions, routes, attachment points, arrows, dark theme, and no-scroll geometry.
+ Never edit its generated HTML.
+3. If the renderer rejects a graph, omit the diagram. Do not load a type reference, inspect CSS,
+ search the project for styling, browse templates, or escalate to custom SVG during a
+ FluencyLoop feature; prose remains the complete explanation.
+4. Do not run Playwright, browser automation, screenshots, theme checks, or iterative visual
+ inspection. The renderer validates its topology before writing. Confirm only that the file is
+ nonempty, then use `fluencyloop site --ensure --open-once --json` when available. Node remains
+ optional.
## General diagrams
diff --git a/claude-skills/feature/SKILL.md b/claude-skills/feature/SKILL.md
index c3dc48a..195bc0a 100644
--- a/claude-skills/feature/SKILL.md
+++ b/claude-skills/feature/SKILL.md
@@ -3,7 +3,7 @@ name: feature
description: 'FluencyLoop Stage 2–3. Declare a feature and build it while staying fluent: creates the feature branch, frames its concepts and relationships, then builds in slices, teaching the why of each real decision at the slice boundary and journaling it. Probes the concepts the work needs up front, adapts explanation depth to the developer''s knowledge, and builds/maintains a per-developer knowledge base in ~/.fluencyloop. Use when starting a new unit of work in a repo that has a .fluencyloop/ directory, or when the user says "fluencyloop feature", "start a feature", or describes something they want to build with FluencyLoop.'
---
-# fluencyloop-feature — declare a feature, build it fluent
+# /fluencyloop:feature — declare a feature, build it fluent
This is the contributor's entry point. A **feature is a branch** (`feature/`); it owns
its design reasoning and session journals. You will: (1) declare the feature, (2) frame its
@@ -63,6 +63,11 @@ in the current project directory when needed, then creates FluencyLoop's state.
continuing. Only stop if `init` itself fails. Do not hand-create `.fluencyloop`, `docs`, or
`.claude/skills`.
+**Report real entry points after a fresh init.** Say that a larger initiative starts with
+`fluencyloop plan ""` or `/fluencyloop:plan`; one buildable unit starts with
+`fluencyloop feature ""` or `/fluencyloop:feature`. Never recommend the retired prose
+names `fluencyloop-plan`, `fluencyloop-feature`, `fluencyloop-review`, or `fluencyloop-backfill`.
+
**Reattach a safe detached checkout.** If `branch` is `HEAD`, `state_matches_branch` is `true`,
and `state_branch` is nonempty, this is the exact recorded feature tip, not a split. Run `git
status --porcelain`. If it is empty, run `git checkout `, then rerun the bundled
@@ -353,9 +358,9 @@ Build the feature one **meaningful slice** at a time (a logical, commit-worthy c
```bash
fluencyloop knowledge \
- --component "||" \
- --component "|||follow-up" \
- --gotcha "|"
+ --component "" --role "" --conditions "" \
+ --component "" --role "" --conditions "" --status follow-up \
+ --gotcha "" --why ""
```
**Knowledge transfer** is still irreducible: make it **rich, not a token list**. Capture the
@@ -364,11 +369,10 @@ Build the feature one **meaningful slice** at a time (a logical, commit-worthy c
`documented`; use `follow-up` only when appropriate. Separate it from decisions: a role you
explained is knowledge transfer even if no fork was chosen. **About the work, never the
person** — no competence, prior knowledge, or "who learned what" (committed files, GDPR); the
- per-developer picture lives only in the calibration profile. Compose and validate every value
- before the one batched call: each `--component` has exactly three or four **nonempty**
- pipe-separated fields and each `--gotcha` has exactly two **nonempty** fields. Omit a field
- instead of emitting `||`. Escape a literal `|` as `\|` and a literal backslash as `\\`. Do not
- run incomplete, bare, or trial `fluencyloop knowledge` commands to discover this syntax.
+ per-developer picture lives only in the calibration profile. Use the explicit-field form above:
+ quote each value normally; it accepts pipes and Windows paths unchanged. The old compact
+ pipe-delimited form remains only for compatibility. Compose the complete batch before one call;
+ do not run incomplete, bare, or trial `fluencyloop knowledge` commands to discover syntax.
- **Decisions** *(the script formats them — you supply only the field values)* — for each, run
`fluencyloop decision` so the block is assembled deterministically; never hand-write the
bullet schema:
@@ -483,7 +487,10 @@ Write and commit at most these Markdown distillations under
concepts and behavior, never as a file list.
2. **Product overview** — refresh `product.md` only when this feature materially changes the
product's problem, shape, or major flow. A feature that changes nothing at that altitude gets
- **no overview rewrite**.
+ **no overview rewrite**. Keep it self-contained: do not add a `Related concepts` / `Related
+ records` section or Markdown links to record files. The reader's Records navigation already
+ provides that traversal; raw file references add no explanation and can appear as literal
+ Markdown in its deliberately small prose renderer.
3. **Concept explanation** — when this feature newly establishes a concept, create
`concepts/.md`; revise an existing explanation only when a feature decision contradicts
it. Do not create a concept explanation merely because the feature touched a concept.
@@ -523,22 +530,22 @@ Choose the visual yourself from the implemented product shape:
- Otherwise omit the diagram. A short hierarchy, list, or simple before/after statement remains
prose or a table; never manufacture a visual merely because `product.md` exists.
-When it qualifies, load the bundled `diagram-design` skill and invoke its **FluencyLoop embedded
-diagram fast path**. Give it the exact output path and the one relationship to clarify; choose the
-type and write the file in one bounded pass. Do not ask the user to choose the style, type, or
-whether to proceed. The local site embeds that file directly below the overview prose through a
-sandboxed route. Make it one restrained system overview, not a duplicate of every record diagram.
-Keep `product.md` prose-only: do not add a Mermaid copy of the companion HTML. Confirm the file is
-nonempty, then use `fluencyloop site --ensure --open-once --json` when Node is available. It makes
-the result available without opening a duplicate tab. The prose remains complete without the diagram
-and explains the same product shape in words.
+When it qualifies, load the bundled `diagram-design` skill and use its **FluencyLoop native
+renderer**. Give it the output path, a bounded graph (2–8 concise nodes and at most 10 edges), and
+the matching linear, hub, or layered layout. Do not search the codebase for styling, read
+templates, invoke Playwright, take screenshots, inspect themes, or iterate on the diagram. The
+renderer owns geometry, routes, attachment points, dark theme, and no-scroll document height;
+never edit its HTML. If it rejects the graph, omit the overview diagram rather than escalating to
+general diagram design. The local site embeds its output through a sandboxed route. Keep `product.md` prose-only.
+Confirm the file is nonempty, then use
+`fluencyloop site --ensure --open-once --json` when Node is available.
**Do not distill decisions.** Their why was taught and captured contemporaneously by
`fluencyloop decision`; re-synthesising it is both less trustworthy and unnecessary token spend.
Keep every distillation person-neutral: describe the product and its constraints, never a
developer's competence, knowledge, or authorship.
-When the feature is ready for a PR, tell the user they can run **fluencyloop-review** to
+When the feature is ready for a PR, tell the user they can run **`/fluencyloop:review`** to
assemble the reviewer-facing view from the sessions.
**Check what's actually possible here first** — run `gh auth status`. If `gh` isn't installed or
@@ -549,7 +556,7 @@ open the PR (and file plan issues) for them — using the delivery rule above (*
install from (pick the command that fits their OS — don't work from a
hardcoded package-manager list) then `gh auth login`. If `gh` stays unavailable (declined or
deferred), the hand-off is at most *commit + push*, and a PR can be opened later via
-`fluencyloop-review`. Only run the full **commit + push + open-PR** automation where `gh` works.
+`/fluencyloop:review`. Only run the full **commit + push + open-PR** automation where `gh` works.
**Create PR bodies through a file.** Write the assembled Markdown to a temporary, untracked file
and call `gh pr create --body-file ` (and `gh pr edit --body-file ` when correcting it).
@@ -563,8 +570,8 @@ The hand-off is a **behavioral pattern that recurs every feature** — so decide
once per feature. Check `~/.fluencyloop/preferences.md` (loaded in §0):
- **A preference is already recorded** — honor it silently, and **do not re-ask**. If it says
- automatic, go ahead and commit + push + open the PR yourself (run fluencyloop-review first) at
- completion; if manual, just point the user at fluencyloop-review and stop. Stage all
+ automatic, go ahead and commit + push + open the PR yourself (run `/fluencyloop:review` first) at
+ completion; if manual, just point the user at `/fluencyloop:review` and stop. Stage all
`docs/fluencyloop/store/` records created in this worktree with the handoff; never exclude the
completed legacy migration from the commit.
- **No preference yet (this is the first feature)** — ask **exactly once**, via a single
diff --git a/claude-skills/plan/SKILL.md b/claude-skills/plan/SKILL.md
index ebbd7d6..f4a39a1 100644
--- a/claude-skills/plan/SKILL.md
+++ b/claude-skills/plan/SKILL.md
@@ -1,15 +1,15 @@
---
name: plan
-description: 'FluencyLoop planning stage. Plan a large chunk of work before building it: design and document the overall architecture, break it into task items, sequence them into a roadmap with a critical path, and (optionally) open GitHub issues under a milestone. Produces a committed plan.md that the per-feature loop then builds from — one fluencyloop-feature per task item. Use when the work is too big for a single feature/branch, or when the user says "fluencyloop plan", "plan this", "design the architecture for", "break this down", or "make a roadmap".'
+description: 'FluencyLoop planning stage. Plan a large chunk of work before building it: design and document the overall architecture, break it into task items, sequence them into a roadmap with a critical path, and (optionally) open GitHub issues under a milestone. Produces a committed plan.md that the per-feature loop then builds from — one /fluencyloop:feature per task item. Use when the work is too big for a single feature/branch, or when the user says "fluencyloop plan", "plan this", "design the architecture for", "break this down", or "make a roadmap".'
---
-# fluencyloop-plan — map a big chunk before you build it
+# /fluencyloop:plan — map a big chunk before you build it
-Sits **upstream of `fluencyloop-feature`**. A *feature* is one branch; a **plan** is an
+Sits **upstream of `/fluencyloop:feature`**. A *feature* is one branch; a **plan** is an
*initiative* that will spawn several features. You will: (1) frame the chunk, (2) analyse
requirements and surface material gaps, (3) design and show the overall architecture, (4) break
it into task items, (5) sequence them into a roadmap with a critical path, (6) offer to open
-GitHub tickets under a milestone, (7) hand each task off to `fluencyloop-feature`. The plan is a
+GitHub tickets under a milestone, (7) hand each task off to `/fluencyloop:feature`. The plan is a
**map you build against, not a spec to ratify** —
do not over-invest. Keep the developer the architect.
@@ -90,10 +90,10 @@ stub**, this plan is where the constitution is born — see §5.
`dimension → level` map, level ∈ {`fluent`, `familiar`, `learning`, `new`}; per-developer, global,
never committed) — to set the depth you explain architectural choices at. Missing is fine.
Planning is also teaching: the same "teach the why, ask whether it is understood, don't lecture"
-posture from `fluencyloop-feature` applies to the architecture decisions here. Apply the
+posture from `/fluencyloop:feature` applies to the architecture decisions here. Apply the
self-report-only rule above to every architecture explanation.
-Is this actually a plan? If the work fits one branch, skip straight to `fluencyloop-feature` —
+Is this actually a plan? If the work fits one branch, skip straight to `/fluencyloop:feature` —
don't manufacture an initiative. Plans are for chunks that genuinely decompose into several
features.
@@ -195,7 +195,7 @@ may connect concepts to other concepts, components, or planned features.
## 3. Break it into task items
-Decompose the initiative into **task items — each a future `fluencyloop-feature`**. For each,
+Decompose the initiative into **task items — each a future `/fluencyloop:feature`**. For each,
capture in the `## Task breakdown` table: an `id` (T1, T2, …), a slug-able **intent**, a rough
**size** (S/M/L), and its **dependencies** (by id). Aim for items that are independently
build-and-mergeable. Keep them coarse; a task that's really two features is two rows.
@@ -275,10 +275,10 @@ The plan is complete either way — no friction.
## 7. Hand off to the build loop
-The plan is the map; each task item is built with **`fluencyloop-feature`** (one branch per
+The plan is the map; each task item is built with **`/fluencyloop:feature`** (one branch per
task, from the active development branch — `dev` for the 0.3 milestone), in roadmap order along
the critical path first. Tell the user that — and that
-`fluencyloop-review` assembles each feature's PR view when it's done. Do not open feature
+`/fluencyloop:review` assembles each feature's PR view when it's done. Do not open feature
branches yourself here; §7 hands off, it doesn't build.
## Rules
diff --git a/claude-skills/review/SKILL.md b/claude-skills/review/SKILL.md
index b917e1c..0304b79 100644
--- a/claude-skills/review/SKILL.md
+++ b/claude-skills/review/SKILL.md
@@ -3,7 +3,7 @@ name: review
description: 'FluencyLoop Stage 4. Assemble the reviewer-facing PR view from a feature''s sessions — a feature is a branch, so it assembles itself from git. Use when preparing a PR description, reviewing a FluencyLoop feature, or when the user says "fluencyloop review", "assemble the PR view", or "summarise this feature for review".'
---
-# fluencyloop-review — Stage 4, assemble the PR view
+# /fluencyloop:review — Stage 4, assemble the PR view
A **feature is a branch**, so the review view assembles itself: no manual linking. You turn
the feature's sessions into a summary a reviewer can read to get fluent fast.
@@ -112,4 +112,4 @@ explicitly asked.
- **Surface, don't gate.** Flag unverified trust and constitution conflicts; never block.
- **Truthful assembly.** Summarise what the sessions actually say; if a decision has no
journaled `why`, say it's undocumented rather than inventing one — or suggest
- **fluencyloop-backfill**.
+ **`/fluencyloop:backfill`**.
diff --git a/plugins/fluencyloop/fluencyloop b/plugins/fluencyloop/fluencyloop
index efb19f2..9c6ca9c 100755
--- a/plugins/fluencyloop/fluencyloop
+++ b/plugins/fluencyloop/fluencyloop
@@ -24,6 +24,8 @@
# fluencyloop check [--json] doctor: loop state + un-journaled drift
# fluencyloop site [--port ] [--ensure [--open|--open-once]|--status|--stop] [--json]
# serve or manage the local 0.3 site (requires Node.js 18+)
+# fluencyloop diagram --output --layout --node --label --detail [--edge ...]
+# render a bounded, self-contained site diagram (requires Node.js 18+)
# fluencyloop slice-context [--json] changed hunks + metadata for the current slice
# fluencyloop calibration your knowledge profile + its ledger
# fluencyloop index regenerate docs/fluencyloop/README.md
@@ -122,6 +124,11 @@ start_site() {
exec "$NODE_BIN" "$SELF/site/server.js" --root "$project_root" "$@"
}
+render_diagram() {
+ require_node_for_site false || return 1
+ exec "$NODE_BIN" "$SELF/site/diagram-renderer.js" "$@"
+}
+
cmd="${1:-help}"; shift || true
case "$cmd" in
init) exec bash "$BIN/init.sh" "$@" ;;
@@ -138,6 +145,7 @@ case "$cmd" in
review) exec bash "$BIN/assemble-pr-view.sh" "$@" ;;
check) exec bash "$BIN/check.sh" "$@" ;;
site) start_site "$@" ;;
+ diagram) render_diagram "$@" ;;
slice-context) exec bash "$BIN/slice-context.sh" "$@" ;;
calibration) exec bash "$BIN/calibration.sh" "$@" ;;
index) exec bash "$BIN/index.sh" "$@" ;;
diff --git a/plugins/fluencyloop/fluencyloop.ps1 b/plugins/fluencyloop/fluencyloop.ps1
index b8f9990..efa00a4 100644
--- a/plugins/fluencyloop/fluencyloop.ps1
+++ b/plugins/fluencyloop/fluencyloop.ps1
@@ -44,6 +44,8 @@ Usage:
fluencyloop check [--json] doctor: loop state + un-journaled drift
fluencyloop site [--port ] [--ensure [--open|--open-once]|--status|--stop] [--json]
serve or manage the local 0.3 site (requires Node.js 18+)
+ fluencyloop diagram --output --layout --node --label --detail [--edge ...]
+ render a bounded, self-contained site diagram (requires Node.js 18+)
fluencyloop slice-context [--json] changed hunks + metadata for the current slice
fluencyloop calibration your knowledge profile + its ledger
fluencyloop index regenerate docs/fluencyloop/README.md
@@ -115,6 +117,13 @@ function StartSite {
exit $LASTEXITCODE
}
+function RenderDiagram {
+ $nodeExe = RequireNodeForSite $false
+ if (-not $nodeExe) { exit 1 }
+ & $nodeExe (Join-Path $SELF 'site/diagram-renderer.js') @rest
+ exit $LASTEXITCODE
+}
+
switch -Regex ($cmd) {
'^init$' { Run 'init.ps1' }
'^plan$' { Run 'new-plan.ps1' }
@@ -130,6 +139,7 @@ switch -Regex ($cmd) {
'^review$' { Run 'assemble-pr-view.ps1' }
'^check$' { Run 'check.ps1' }
'^site$' { StartSite }
+ '^diagram$' { RenderDiagram }
'^slice-context$' { Run 'slice-context.ps1' }
'^calibration$' { Run 'calibration.ps1' }
'^index$' { Run 'index.ps1' }
diff --git a/plugins/fluencyloop/scripts/bash/add-knowledge.sh b/plugins/fluencyloop/scripts/bash/add-knowledge.sh
index 4ef36b7..96370da 100755
--- a/plugins/fluencyloop/scripts/bash/add-knowledge.sh
+++ b/plugins/fluencyloop/scripts/bash/add-knowledge.sh
@@ -1,11 +1,12 @@
#!/usr/bin/env bash
# add-knowledge.sh — append a session's component inventory and hard-won conditions in one batch.
-# Fields use | as their separator; write literal | as \| and literal \ as \\. The script unescapes
-# those two sequences before writing records, and validates the complete batch before it appends.
+# The primary form uses explicit fields, so prose and Windows paths need no escape grammar. The
+# older pipe form remains accepted for compatibility; \| still represents a literal pipe.
#
# Usage: add-knowledge.sh [--feature --session ]
-# [--component ...]
-# [--gotcha ...]
+# [--component --role --conditions [--status ] ...]
+# [--gotcha --why ...]
+# [--component ...] [--gotcha ...]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -14,12 +15,58 @@ source "$SCRIPT_DIR/common.sh"
require_fluency
declare -a COMPONENTS=() GOTCHAS=() PARSED_FIELDS=()
+declare -a STRUCTURED_COMPONENT_NAMES=() STRUCTURED_COMPONENT_ROLES=() STRUCTURED_COMPONENT_CONDITIONS=() STRUCTURED_COMPONENT_STATUSES=()
+declare -a STRUCTURED_GOTCHA_SUBJECTS=() STRUCTURED_GOTCHA_WHYS=()
FEATURE_OVERRIDE=""; SESSION_OVERRIDE=""; TARGET_OVERRIDE=false
HAS_INPUT=false
+ACTIVE_COMPONENT=-1
+ACTIVE_GOTCHA=-1
while [ "$#" -gt 0 ]; do
case "$1" in
- --component) shift; COMPONENTS+=("${1:-}"); HAS_INPUT=true ;;
- --gotcha) shift; GOTCHAS+=("${1:-}"); HAS_INPUT=true ;;
+ --component)
+ shift; value="${1:-}"; HAS_INPUT=true
+ if [[ "$value" == *"|"* && "${2:-}" != "--role" ]]; then
+ COMPONENTS+=("$value"); ACTIVE_COMPONENT=-1
+ else
+ STRUCTURED_COMPONENT_NAMES+=("$value")
+ STRUCTURED_COMPONENT_ROLES+=("")
+ STRUCTURED_COMPONENT_CONDITIONS+=("")
+ STRUCTURED_COMPONENT_STATUSES+=("documented")
+ ACTIVE_COMPONENT=$((${#STRUCTURED_COMPONENT_NAMES[@]} - 1))
+ fi
+ ACTIVE_GOTCHA=-1
+ ;;
+ --role)
+ shift
+ [ "$ACTIVE_COMPONENT" -ge 0 ] || { echo "Error: --role must follow an explicit --component." >&2; exit 1; }
+ STRUCTURED_COMPONENT_ROLES[ACTIVE_COMPONENT]="${1:-}"
+ ;;
+ --conditions)
+ shift
+ [ "$ACTIVE_COMPONENT" -ge 0 ] || { echo "Error: --conditions must follow an explicit --component." >&2; exit 1; }
+ STRUCTURED_COMPONENT_CONDITIONS[ACTIVE_COMPONENT]="${1:-}"
+ ;;
+ --status)
+ shift
+ [ "$ACTIVE_COMPONENT" -ge 0 ] || { echo "Error: --status must follow an explicit --component." >&2; exit 1; }
+ STRUCTURED_COMPONENT_STATUSES[ACTIVE_COMPONENT]="${1:-}"
+ ;;
+ --gotcha)
+ shift; value="${1:-}"; HAS_INPUT=true
+ if [[ "$value" == *"|"* && "${2:-}" != "--why" ]]; then
+ GOTCHAS+=("$value"); ACTIVE_GOTCHA=-1
+ else
+ STRUCTURED_GOTCHA_SUBJECTS+=("$value")
+ STRUCTURED_GOTCHA_WHYS+=("")
+ ACTIVE_GOTCHA=$((${#STRUCTURED_GOTCHA_SUBJECTS[@]} - 1))
+ fi
+ ACTIVE_COMPONENT=-1
+ ;;
+ --why)
+ shift
+ [ "$ACTIVE_GOTCHA" -ge 0 ] || { echo "Error: --why must follow an explicit --gotcha." >&2; exit 1; }
+ STRUCTURED_GOTCHA_WHYS[ACTIVE_GOTCHA]="${1:-}"
+ ;;
--feature) shift; FEATURE_OVERRIDE="${1:-}"; TARGET_OVERRIDE=true ;;
--session) shift; SESSION_OVERRIDE="${1:-}"; TARGET_OVERRIDE=true ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
@@ -27,9 +74,8 @@ while [ "$#" -gt 0 ]; do
shift
done
-# Split a pipe-delimited argument. Only \| and \\ are escapes: accepting other escapes would
-# silently change prose. The result is assigned to PARSED_FIELDS rather than printed, so callers
-# can preserve newlines and whitespace in a field.
+# Split a legacy pipe-delimited argument. \| represents a literal pipe; all other backslashes stay
+# literal so ordinary prose and Windows paths do not need a special escape vocabulary.
split_knowledge_fields() {
local value="$1" min_fields="$2" max_fields="$3" flag="$4"
local current="" char escaped=false i
@@ -37,10 +83,11 @@ split_knowledge_fields() {
for ((i = 0; i < ${#value}; i++)); do
char="${value:i:1}"
if $escaped; then
- case "$char" in
- '|'|\\) current+="$char" ;;
- *) printf 'Error: %s only permits \\| and \\\\ escapes.\n' "$flag" >&2; return 1 ;;
- esac
+ if [ "$char" = '|' ] || [ "$char" = "\\" ]; then
+ current+="$char"
+ else
+ current+="\\$char"
+ fi
escaped=false
else
case "$char" in
@@ -50,10 +97,7 @@ split_knowledge_fields() {
esac
fi
done
- if $escaped; then
- echo "Error: $flag cannot end with an escape." >&2
- return 1
- fi
+ $escaped && current+="\\"
PARSED_FIELDS+=("$current")
if [ "${#PARSED_FIELDS[@]}" -lt "$min_fields" ] || [ "${#PARSED_FIELDS[@]}" -gt "$max_fields" ]; then
echo "Error: $flag needs $min_fields-$max_fields pipe-separated fields; escape literal pipes as \\|." >&2
@@ -105,9 +149,25 @@ for component in ${COMPONENTS[@]+"${COMPONENTS[@]}"}; do
*) echo "Error: --component status must be documented or follow-up." >&2; exit 1 ;;
esac
done
+for ((i = 0; i < ${#STRUCTURED_COMPONENT_NAMES[@]}; i++)); do
+ if [ -z "${STRUCTURED_COMPONENT_NAMES[i]}" ] || [ -z "${STRUCTURED_COMPONENT_ROLES[i]}" ] || [ -z "${STRUCTURED_COMPONENT_CONDITIONS[i]}" ]; then
+ echo "Error: an explicit --component requires nonempty --role and --conditions fields." >&2
+ exit 1
+ fi
+ case "${STRUCTURED_COMPONENT_STATUSES[i]}" in
+ documented|follow-up) ;;
+ *) echo "Error: --status must be documented or follow-up." >&2; exit 1 ;;
+ esac
+done
for gotcha in ${GOTCHAS[@]+"${GOTCHAS[@]}"}; do
split_knowledge_fields "$gotcha" 2 2 --gotcha
done
+for ((i = 0; i < ${#STRUCTURED_GOTCHA_SUBJECTS[@]}; i++)); do
+ if [ -z "${STRUCTURED_GOTCHA_SUBJECTS[i]}" ] || [ -z "${STRUCTURED_GOTCHA_WHYS[i]}" ]; then
+ echo "Error: an explicit --gotcha requires a nonempty --why field." >&2
+ exit 1
+ fi
+done
STORE="$(feature_store_path "$FEATURE")"
WRITTEN=0
@@ -121,6 +181,14 @@ for component in ${COMPONENTS[@]+"${COMPONENTS[@]}"}; do
status "$status"
WRITTEN=$((WRITTEN + 1))
done
+for ((i = 0; i < ${#STRUCTURED_COMPONENT_NAMES[@]}; i++)); do
+ store_append_record "$STORE" component "$FEATURE" "$SESSION" \
+ name "${STRUCTURED_COMPONENT_NAMES[i]}" \
+ role "${STRUCTURED_COMPONENT_ROLES[i]}" \
+ conditions "${STRUCTURED_COMPONENT_CONDITIONS[i]}" \
+ status "${STRUCTURED_COMPONENT_STATUSES[i]}"
+ WRITTEN=$((WRITTEN + 1))
+done
for gotcha in ${GOTCHAS[@]+"${GOTCHAS[@]}"}; do
split_knowledge_fields "$gotcha" 2 2 --gotcha
store_append_record "$STORE" condition "$FEATURE" "$SESSION" \
@@ -128,5 +196,11 @@ for gotcha in ${GOTCHAS[@]+"${GOTCHAS[@]}"}; do
why "${PARSED_FIELDS[1]}"
WRITTEN=$((WRITTEN + 1))
done
+for ((i = 0; i < ${#STRUCTURED_GOTCHA_SUBJECTS[@]}; i++)); do
+ store_append_record "$STORE" condition "$FEATURE" "$SESSION" \
+ subject "${STRUCTURED_GOTCHA_SUBJECTS[i]}" \
+ why "${STRUCTURED_GOTCHA_WHYS[i]}"
+ WRITTEN=$((WRITTEN + 1))
+done
echo "Appended $WRITTEN knowledge record(s) to $STORE"
diff --git a/plugins/fluencyloop/scripts/powershell/add-knowledge.ps1 b/plugins/fluencyloop/scripts/powershell/add-knowledge.ps1
index 71de76f..cbf0d88 100644
--- a/plugins/fluencyloop/scripts/powershell/add-knowledge.ps1
+++ b/plugins/fluencyloop/scripts/powershell/add-knowledge.ps1
@@ -1,15 +1,50 @@
# add-knowledge.ps1 — PowerShell port of add-knowledge.sh. Appends one session's component
-# inventory and hard-won conditions as a validated batch.
+# inventory and hard-won conditions as a validated batch. Its primary explicit-field form needs no
+# pipe escaping; the historical compact pipe form remains supported for compatibility.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/common.ps1"
-$components = @(); $gotchas = @(); $hasInput = $false; $featureOverride = ''; $sessionOverride = ''; $targetOverride = $false
+$components = @(); $gotchas = @(); $structuredComponents = @(); $structuredGotchas = @()
+$hasInput = $false; $featureOverride = ''; $sessionOverride = ''; $targetOverride = $false
+$activeComponent = -1; $activeGotcha = -1
for ($i = 0; $i -lt $args.Count; $i++) {
switch ($args[$i]) {
- '--component' { $i++; $components += [string]$args[$i]; $hasInput = $true }
- '--gotcha' { $i++; $gotchas += [string]$args[$i]; $hasInput = $true }
+ '--component' {
+ $i++; $value = [string]$args[$i]; $hasInput = $true
+ if ($value.Contains('|') -and ($i + 1 -ge $args.Count -or $args[$i + 1] -ne '--role')) { $components += $value; $activeComponent = -1 }
+ else {
+ $structuredComponents += [pscustomobject]@{ name = $value; role = ''; conditions = ''; status = 'documented' }
+ $activeComponent = $structuredComponents.Count - 1
+ }
+ $activeGotcha = -1
+ }
+ '--role' {
+ $i++; if ($activeComponent -lt 0) { throw 'Error: --role must follow an explicit --component.' }
+ $structuredComponents[$activeComponent].role = [string]$args[$i]
+ }
+ '--conditions' {
+ $i++; if ($activeComponent -lt 0) { throw 'Error: --conditions must follow an explicit --component.' }
+ $structuredComponents[$activeComponent].conditions = [string]$args[$i]
+ }
+ '--status' {
+ $i++; if ($activeComponent -lt 0) { throw 'Error: --status must follow an explicit --component.' }
+ $structuredComponents[$activeComponent].status = [string]$args[$i]
+ }
+ '--gotcha' {
+ $i++; $value = [string]$args[$i]; $hasInput = $true
+ if ($value.Contains('|') -and ($i + 1 -ge $args.Count -or $args[$i + 1] -ne '--why')) { $gotchas += $value; $activeGotcha = -1 }
+ else {
+ $structuredGotchas += [pscustomobject]@{ subject = $value; why = '' }
+ $activeGotcha = $structuredGotchas.Count - 1
+ }
+ $activeComponent = -1
+ }
+ '--why' {
+ $i++; if ($activeGotcha -lt 0) { throw 'Error: --why must follow an explicit --gotcha.' }
+ $structuredGotchas[$activeGotcha].why = [string]$args[$i]
+ }
'--feature' { $i++; $featureOverride = [string]$args[$i]; $targetOverride = $true }
'--session' { $i++; $sessionOverride = [string]$args[$i]; $targetOverride = $true }
default { [Console]::Error.WriteLine("Unknown option: $($args[$i])"); exit 1 }
@@ -18,19 +53,16 @@ for ($i = 0; $i -lt $args.Count; $i++) {
FlRequireFluency
-# Split a pipe-delimited argument. A literal pipe is \| and a literal backslash is \\; accepting
-# other escapes would silently alter prose. Returning values instead of printing them preserves
-# whitespace and newlines inside a field.
+# Split a legacy pipe-delimited argument. A literal pipe is \|; other backslashes remain literal
+# so ordinary prose and Windows paths do not need a special escape vocabulary.
function Split-FlKnowledgeField([string]$value, [int]$minFields, [int]$maxFields, [string]$flag) {
$fields = [System.Collections.Generic.List[string]]::new()
$field = [System.Text.StringBuilder]::new()
$escaped = $false
foreach ($character in $value.ToCharArray()) {
if ($escaped) {
- if ($character -ne [char]124 -and $character -ne [char]92) {
- throw "Error: $flag only permits \| and \\ escapes."
- }
- [void]$field.Append($character)
+ if ($character -eq [char]124 -or $character -eq [char]92) { [void]$field.Append($character) }
+ else { [void]$field.Append([char]92); [void]$field.Append($character) }
$escaped = $false
continue
}
@@ -43,7 +75,7 @@ function Split-FlKnowledgeField([string]$value, [int]$minFields, [int]$maxFields
[void]$field.Append($character)
}
}
- if ($escaped) { throw "Error: $flag cannot end with an escape." }
+ if ($escaped) { [void]$field.Append([char]92) }
$fields.Add($field.ToString())
if ($fields.Count -lt $minFields -or $fields.Count -gt $maxFields) {
throw "Error: $flag needs $minFields-$maxFields pipe-separated fields; escape literal pipes as \|."
@@ -90,7 +122,20 @@ foreach ($component in $components) {
exit 1
}
}
+foreach ($component in $structuredComponents) {
+ if (-not $component.name -or -not $component.role -or -not $component.conditions) {
+ throw 'Error: an explicit --component requires nonempty --role and --conditions fields.'
+ }
+ if ($component.status -notin @('documented', 'follow-up')) {
+ throw 'Error: --status must be documented or follow-up.'
+ }
+}
foreach ($gotcha in $gotchas) { $null = @(Split-FlKnowledgeField $gotcha 2 2 '--gotcha') }
+foreach ($gotcha in $structuredGotchas) {
+ if (-not $gotcha.subject -or -not $gotcha.why) {
+ throw 'Error: an explicit --gotcha requires a nonempty --why field.'
+ }
+}
$store = FlFeatureStorePath $feature
$written = 0
@@ -101,10 +146,19 @@ foreach ($component in $components) {
'name', $parts[0], 'role', $parts[1], 'conditions', $parts[2], 'status', $status)
$written++
}
+foreach ($component in $structuredComponents) {
+ FlStoreAppendRecord $store 'component' $feature $session @(
+ 'name', $component.name, 'role', $component.role, 'conditions', $component.conditions, 'status', $component.status)
+ $written++
+}
foreach ($gotcha in $gotchas) {
$parts = @(Split-FlKnowledgeField $gotcha 2 2 '--gotcha')
FlStoreAppendRecord $store 'condition' $feature $session @('subject', $parts[0], 'why', $parts[1])
$written++
}
+foreach ($gotcha in $structuredGotchas) {
+ FlStoreAppendRecord $store 'condition' $feature $session @('subject', $gotcha.subject, 'why', $gotcha.why)
+ $written++
+}
FlOut "Appended $written knowledge record(s) to $store"
diff --git a/plugins/fluencyloop/site/diagram-renderer.js b/plugins/fluencyloop/site/diagram-renderer.js
new file mode 100644
index 0000000..4416f3c
--- /dev/null
+++ b/plugins/fluencyloop/site/diagram-renderer.js
@@ -0,0 +1,215 @@
+#!/usr/bin/env node
+'use strict';
+
+// diagram-renderer.js — FluencyLoop's deliberately small diagram compiler. Agents provide a
+// bounded graph; this file owns coordinates, attachment points, arrows, themes, and the fixed
+// document height used by the sandboxed site iframe.
+
+const fs = require('fs');
+const path = require('path');
+
+const usage = `Usage: fluencyloop diagram --output docs/fluencyloop/diagrams/.html
+ --layout --title
+ --node --label