From 3427427793af5a70d710e00822a636225bd22245 Mon Sep 17 00:00:00 2001 From: baokhang83 Date: Wed, 12 Aug 2026 19:07:52 +0200 Subject: [PATCH 1/2] fix: bound agent workflow overhead --- claude-skills/backfill/SKILL.md | 2 +- claude-skills/diagram-design/SKILL.md | 57 +++-- claude-skills/feature/SKILL.md | 48 ++-- claude-skills/plan/SKILL.md | 18 +- claude-skills/review/SKILL.md | 4 +- plugins/fluencyloop/fluencyloop | 8 + plugins/fluencyloop/fluencyloop.ps1 | 10 + .../fluencyloop/scripts/bash/add-knowledge.sh | 108 +++++++-- .../scripts/powershell/add-knowledge.ps1 | 78 ++++++- plugins/fluencyloop/site/diagram-renderer.js | 215 ++++++++++++++++++ plugins/fluencyloop/site/site.css | 4 +- .../skills/diagram-design/SKILL.md | 57 +++-- plugins/fluencyloop/skills/feature/SKILL.md | 38 ++-- tests/add-knowledge.bats | 19 ++ tests/diagram-renderer.bats | 38 ++++ tests/plugin.bats | 35 ++- tests/powershell/add-knowledge.Tests.ps1 | 12 + 17 files changed, 588 insertions(+), 163 deletions(-) create mode 100644 plugins/fluencyloop/site/diagram-renderer.js create mode 100644 tests/diagram-renderer.bats 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..d225712 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: @@ -523,22 +527,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 +553,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 +567,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 <id> --label <label> --detail <detail> [--node ...] + [--edge <from> <to> ...] [--hub <id>] + +Limits: 2–8 nodes, at most 10 edges, labels up to 20 characters, details up to 32 characters.`; + +function fail(message) { + process.stderr.write(`Error: ${message}\n${usage}\n`); + process.exit(1); +} + +function escapeHtml(value) { + return value.replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); +} + +const args = process.argv.slice(2); +const graph = { output: '', layout: '', title: '', nodes: [], edges: [], hub: '' }; +let active = null; +for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const next = () => { + index += 1; + if (index >= args.length || args[index].startsWith('--')) fail(`${arg} requires a value.`); + return args[index]; + }; + switch (arg) { + case '--output': graph.output = next(); break; + case '--layout': graph.layout = next(); break; + case '--title': graph.title = next(); break; + case '--node': { + const id = next(); + if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(id)) fail(`invalid node id: ${id}`); + if (graph.nodes.some((node) => node.id === id)) fail(`duplicate node id: ${id}`); + active = { id, label: '', detail: '' }; + graph.nodes.push(active); + break; + } + case '--label': if (!active) fail('--label must follow --node.'); active.label = next(); break; + case '--detail': if (!active) fail('--detail must follow --node.'); active.detail = next(); break; + case '--edge': { + const from = next(); + const to = next(); + graph.edges.push({ from, to }); + break; + } + case '--hub': graph.hub = next(); break; + case '--help': process.stdout.write(`${usage}\n`); process.exit(0); break; + default: fail(`unknown option: ${arg}`); + } +} + +if (!graph.output || !graph.layout || !graph.title) fail('--output, --layout, and --title are required.'); +if (!['linear', 'hub', 'layered'].includes(graph.layout)) fail(`unknown layout: ${graph.layout}`); +if (graph.nodes.length < 2 || graph.nodes.length > 8) fail('provide 2–8 nodes.'); +if (graph.edges.length > 10) fail('provide at most 10 edges.'); +if (graph.title.length > 56) fail('title is too long; use a concise site heading.'); +for (const node of graph.nodes) { + if (!node.label || !node.detail) fail(`node ${node.id} requires --label and --detail.`); + if (node.label.length > 20 || node.detail.length > 32) fail(`node ${node.id} text is too long; use concise site labels.`); +} +if (graph.layout === 'linear' && graph.nodes.length >= 5 + && graph.nodes.some((node) => node.label.length > 14 || node.detail.length > 18)) { + fail('a five-or-more-node linear layout needs short labels; choose layered or hub for longer text.'); +} +const nodeById = new Map(graph.nodes.map((node) => [node.id, node])); +for (const edge of graph.edges) { + if (!nodeById.has(edge.from) || !nodeById.has(edge.to) || edge.from === edge.to) fail(`invalid edge: ${edge.from} → ${edge.to}`); +} +if (!graph.edges.length) fail('provide at least one --edge.'); + +const output = path.resolve(process.cwd(), graph.output); +const diagramsRoot = path.resolve(process.cwd(), 'docs', 'fluencyloop', 'diagrams'); +if (!output.startsWith(`${diagramsRoot}${path.sep}`) || path.extname(output) !== '.html') { + fail('--output must be an .html file under docs/fluencyloop/diagrams/.'); +} + +const WIDTH = 960; +const CARD_W = 200; +const CARD_H = 104; +const HEIGHT = graph.layout === 'hub' ? 520 : 440; + +function card(node, box, shared = false) { + node.box = box; + const klass = shared ? 'card shared' : 'card'; + const nameY = box.y + box.h / 2 - 8; + return `<rect x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" rx="10" class="${klass}"/> + <text x="${box.x + 20}" y="${nameY}" class="name">${escapeHtml(node.label)}</text> + <text x="${box.x + 20}" y="${nameY + 28}" class="detail">${escapeHtml(node.detail)}</text>`; +} + +function linearLayout() { + const n = graph.nodes.length; + const width = Math.min(CARD_W, Math.floor((WIDTH - 96 - (n - 1) * 24) / n)); + if (width < 112) fail('linear layout cannot fit these nodes.'); + const gap = (WIDTH - 96 - n * width) / (n - 1); + graph.nodes.forEach((node, index) => { node.box = { x: Math.round(48 + index * (width + gap)), y: 190, w: width, h: CARD_H }; }); + const paths = graph.edges.map((edge) => { + const from = nodeById.get(edge.from).box; const to = nodeById.get(edge.to).box; + if (to.x <= from.x) fail('linear edges must point from left to right.'); + if (graph.nodes.some((node) => node.box.x > from.x && node.box.x < to.x)) fail('linear edges may connect adjacent nodes only.'); + if (to.y !== from.y) fail('linear layout requires aligned nodes.'); + return `<path d="M${from.x + from.w} ${from.y + CARD_H / 2} H${to.x}" class="flow"/>`; + }); + return { paths, cards: graph.nodes.map((node) => card(node, node.box)) }; +} + +function hubLayout() { + const hub = nodeById.get(graph.hub); + if (!hub) fail('hub layout requires --hub <node-id>.'); + if (graph.edges.some((edge) => edge.from !== hub.id && edge.to !== hub.id)) fail('hub edges must connect to --hub.'); + const leaves = graph.nodes.filter((node) => node !== hub); + const left = leaves.slice(0, Math.ceil(leaves.length / 2)); + const right = leaves.slice(left.length); + const degree = new Map(leaves.map((node) => [node.id, 0])); + graph.edges.forEach((edge) => { + const leafId = edge.from === hub.id ? edge.to : edge.from; + degree.set(leafId, degree.get(leafId) + 1); + }); + if ([...degree.values()].some((count) => count > 1)) fail('hub layout permits one edge per participant so every route stays distinct.'); + // Every leaf gets its own horizontal port. No elbows means no crossing, shared attachment + // point, or marker that can render inside a non-endpoint card. + const LEAF_H = 80; + hub.box = { x: 380, y: 96, w: CARD_W, h: 380 }; + const spaced = (items, x) => items.forEach((node, index) => { + const center = Math.round(136 + index * (300 / Math.max(1, items.length - 1))); + node.box = { x, y: center - LEAF_H / 2, w: CARD_W, h: LEAF_H }; + }); + spaced(left, 48); spaced(right, 712); + const paths = graph.edges.map((edge) => { + const fromNode = nodeById.get(edge.from); const toNode = nodeById.get(edge.to); + const leaf = fromNode === hub ? toNode : fromNode; + const leafOnLeft = leaf.box.x < hub.box.x; + const y = leaf.box.y + LEAF_H / 2; + const start = fromNode === hub + ? { x: leafOnLeft ? hub.box.x : hub.box.x + hub.box.w, y } + : { x: leafOnLeft ? leaf.box.x + leaf.box.w : leaf.box.x, y }; + const end = toNode === hub + ? { x: leafOnLeft ? hub.box.x : hub.box.x + hub.box.w, y } + : { x: leafOnLeft ? leaf.box.x + leaf.box.w : leaf.box.x, y }; + return `<path d="M${start.x} ${start.y} H${end.x}" class="flow"/>`; + }); + return { paths, cards: graph.nodes.map((node) => card(node, node.box, node === hub)) }; +} + +function layeredLayout() { + const parents = new Map(graph.nodes.map((node) => [node.id, []])); + graph.edges.forEach((edge) => parents.get(edge.to).push(edge.from)); + const ranks = new Map(graph.nodes.map((node) => [node.id, 0])); + for (let pass = 0; pass < graph.nodes.length; pass += 1) { + let changed = false; + for (const edge of graph.edges) { + const next = ranks.get(edge.from) + 1; + if (next > ranks.get(edge.to)) { ranks.set(edge.to, next); changed = true; } + } + if (!changed) break; + if (pass === graph.nodes.length - 1) fail('layered layout does not support cycles.'); + } + const maxRank = Math.max(...ranks.values()); + if (maxRank > 3) fail('layered layout supports at most four layers.'); + const groups = Array.from({ length: maxRank + 1 }, () => []); + graph.nodes.forEach((node) => groups[ranks.get(node.id)].push(node)); + groups.forEach((group, rank) => group.forEach((node, index) => { + const y = Math.round(126 + index * ((HEIGHT - 126 - CARD_H) / Math.max(1, group.length - 1))); + const x = Math.round(48 + rank * ((WIDTH - 96 - CARD_W) / Math.max(1, maxRank))); + node.box = { x, y, w: CARD_W, h: CARD_H }; + })); + for (const edge of graph.edges) { + if (ranks.get(edge.to) !== ranks.get(edge.from) + 1) fail('layered edges must connect adjacent layers.'); + } + const boundaryEdges = new Map(); + graph.edges.forEach((edge) => { + const key = `${ranks.get(edge.from)}:${ranks.get(edge.to)}`; + boundaryEdges.set(key, [...(boundaryEdges.get(key) || []), edge]); + }); + for (const edges of boundaryEdges.values()) { + const seenSources = new Set(); const seenTargets = new Set(); + for (const edge of edges) { + if (seenSources.has(edge.from) || seenTargets.has(edge.to)) fail('layered layout requires one edge per node boundary; choose hub for a fan-out or merge.'); + seenSources.add(edge.from); seenTargets.add(edge.to); + } + const ordered = [...edges].sort((a, b) => nodeById.get(a.from).box.y - nodeById.get(b.from).box.y); + const targetOrder = [...ordered].sort((a, b) => nodeById.get(a.to).box.y - nodeById.get(b.to).box.y); + if (ordered.some((edge, index) => edge !== targetOrder[index])) fail('layered edges would cross; choose hub or simplify the relationship.'); + } + const paths = graph.edges.map((edge) => { + const from = nodeById.get(edge.from).box; const to = nodeById.get(edge.to).box; + const lane = Math.round((from.x + from.w + to.x) / 2); + return `<path d="M${from.x + from.w} ${from.y + CARD_H / 2} H${lane} V${to.y + CARD_H / 2} H${to.x}" class="flow"/>`; + }); + return { paths, cards: graph.nodes.map((node) => card(node, node.box)) }; +} + +const rendered = graph.layout === 'linear' ? linearLayout() : graph.layout === 'hub' ? hubLayout() : layeredLayout(); +const html = `<!doctype html> +<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(graph.title)} + +${escapeHtml(graph.title)}ARCHITECTURE${escapeHtml(graph.title)}${rendered.paths.join('')}${rendered.cards.join('')}`; + +fs.mkdirSync(path.dirname(output), { recursive: true }); +fs.writeFileSync(output, html, 'utf8'); +process.stdout.write(`Rendered ${graph.nodes.length}-node ${graph.layout} diagram to ${output}\n`); diff --git a/plugins/fluencyloop/site/site.css b/plugins/fluencyloop/site/site.css index f5964c7..d69484b 100644 --- a/plugins/fluencyloop/site/site.css +++ b/plugins/fluencyloop/site/site.css @@ -588,9 +588,9 @@ code { font-family: var(--font-mono); font-size: 0.95em; } overflow: hidden; background: var(--surface-quiet); } -.record-diagram iframe { display: block; width: 100%; min-height: 30rem; border: 0; background: var(--surface); } +.record-diagram iframe { display: block; width: 100%; height: 33rem; border: 0; background: var(--surface); } .record-diagram figcaption { padding: var(--space-2) var(--space-3); color: var(--ink-muted); font-size: var(--text-xs); } -.overview-diagram iframe { min-height: 34rem; } +.overview-diagram iframe { height: 33rem; } .diagram-unavailable, [role="alert"] { padding: var(--space-3); diff --git a/plugins/fluencyloop/skills/diagram-design/SKILL.md b/plugins/fluencyloop/skills/diagram-design/SKILL.md index d7372e4..345ac39 100644 --- a/plugins/fluencyloop/skills/diagram-design/SKILL.md +++ b/plugins/fluencyloop/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/plugins/fluencyloop/skills/feature/SKILL.md b/plugins/fluencyloop/skills/feature/SKILL.md index 5d6563d..d2db127 100644 --- a/plugins/fluencyloop/skills/feature/SKILL.md +++ b/plugins/fluencyloop/skills/feature/SKILL.md @@ -61,6 +61,11 @@ Run `fluencyloop check --json`. If `git_repo` or `fluency` is false, run `fluenc without asking the developer. It initialises Git in the current project directory when needed, then creates FluencyLoop's state. Only stop if `init` itself fails. +**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`. + For that `fluencyloop init --json` command in Codex, request sandbox elevation before its first execution. It may create or update Codex-protected `.git` metadata; do not first attempt it in the standard sandbox. @@ -410,9 +415,9 @@ visible; the journal is its durable byproduct. ```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 @@ -421,11 +426,10 @@ visible; the journal is its durable byproduct. `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: @@ -578,15 +582,15 @@ 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. diff --git a/tests/add-knowledge.bats b/tests/add-knowledge.bats index 60af40a..4f2cfab 100644 --- a/tests/add-knowledge.bats +++ b/tests/add-knowledge.bats @@ -39,6 +39,25 @@ for record in records: PY } +@test "accepts explicit fields without a pipe escape grammar" { + before="$(wc -l < "$STORE")" + run knowledge \ + --component "C:\\work\\app.config.ts" --role "holds app providers | startup config" --conditions "loaded before bootstrap" \ + --component "Dog list" --role "renders choices" --conditions "uses the current selection" --status follow-up \ + --gotcha "selection | routing" --why "a normal Windows path C:\\work must stay literal" + [ "$status" -eq 0 ] + [ "$(wc -l < "$STORE")" -eq $((before + 3)) ] + python3 - "$STORE" <<'PY' +import json, sys +records = [json.loads(line) for line in open(sys.argv[1])][-3:] +assert records[0]['name'] == r'C:\work\app.config.ts' +assert records[0]['role'] == 'holds app providers | startup config' +assert records[1]['status'] == 'follow-up' +assert records[2]['subject'] == 'selection | routing' +assert records[2]['why'] == r'a normal Windows path C:\work must stay literal' +PY +} + @test "escaped pipes and backslashes round-trip through both record kinds" { run knowledge \ --component 'cache\|fallback|uses \\ local state|after a miss' \ diff --git a/tests/diagram-renderer.bats b/tests/diagram-renderer.bats new file mode 100644 index 0000000..a2602c3 --- /dev/null +++ b/tests/diagram-renderer.bats @@ -0,0 +1,38 @@ +#!/usr/bin/env bats +# diagram-renderer.js — agents supply graph facts; the renderer owns geometry and fixed iframe height. + +load test_helper + +@test "renders a six-node hub without custom SVG or vertical overflow" { + command -v node >/dev/null 2>&1 || skip "Node.js is required for the diagram renderer" + setup_initialized_repo + run bash "$DIST/fluencyloop" diagram \ + --output docs/fluencyloop/diagrams/product-overview.html --layout hub --title "Selection architecture" --hub service \ + --node list --label "Dog list" --detail "Chooses a dog" \ + --node filters --label "Filter controls" --detail "Refines the directory" \ + --node service --label "Selection service" --detail "Owns current selection" \ + --node detail --label "Dog detail" --detail "Reads selected dog" \ + --node metrics --label "Usage metrics" --detail "Observes selection" \ + --node data --label "Dog data" --detail "Provides fixed profiles" \ + --edge list service --edge filters service --edge data service --edge service detail --edge service metrics + [ "$status" -eq 0 ] + diagram="$TESTREPO/docs/fluencyloop/diagrams/product-overview.html" + [ -s "$diagram" ] + grep -q 'height:520px' "$diagram" + grep -q 'overflow:hidden' "$diagram" + [ "$(grep -o '/dev/null 2>&1 || skip "Node.js is required for the diagram renderer" + setup_initialized_repo + run bash "$DIST/fluencyloop" diagram \ + --output docs/fluencyloop/diagrams/product-overview.html --layout layered --title "Invalid fan" \ + --node source --label "Source" --detail "Starts the flow" \ + --node left --label "Left output" --detail "Consumes the result" \ + --node right --label "Right output" --detail "Consumes the result" \ + --edge source left --edge source right + [ "$status" -ne 0 ] + [[ "$output" == *"choose hub for a fan-out"* ]] +} diff --git a/tests/plugin.bats b/tests/plugin.bats index e098eb0..5ab8169 100644 --- a/tests/plugin.bats +++ b/tests/plugin.bats @@ -114,6 +114,8 @@ assert "Create PR bodies through a file." in feature_text assert "never exclude the\n completed legacy migration" in feature_text assert "If `git_repo` or `fluency` is" in feature_text assert "without asking the developer" in feature_text +assert "Report real entry points after a fresh init." in feature_text +assert "`fluencyloop plan \"\"` or `/fluencyloop:plan`" in feature_text assert "`store` must be a path under `docs/fluencyloop/`" in feature_text codex_feature_text = read_text(dist / "skills" / "feature" / "SKILL.md") assert "**Refuse split state.**" in codex_feature_text @@ -142,6 +144,8 @@ assert "without a teaching turn" in codex_feature_text assert "### Codex design teaching gate - before implementation" in codex_feature_text assert "conversation pause, not a build or merge gate" in codex_feature_text assert "request sandbox elevation before its first" in codex_feature_text +assert "Report real entry points after a fresh init." in codex_feature_text +assert "`fluencyloop plan \"\"` or `/fluencyloop:plan`" in codex_feature_text assert "never make explanation sound like a burden" in codex_feature_text assert "I am not comfortable" in codex_feature_text assert "Understanding checks are self-report, never quizzes" in codex_feature_text @@ -150,8 +154,9 @@ assert "topic-specific question" in codex_feature_text assert "standalone comprehension question" not in codex_feature_text assert "do not run another implementation" in codex_feature_text for feature_skill_text in [feature_text, codex_feature_text]: - assert "exactly three or four **nonempty**" in feature_skill_text - assert "Do not\n run incomplete, bare, or trial `fluencyloop knowledge`" in feature_skill_text + assert "Use the explicit-field form above" in feature_skill_text + assert "accepts pipes and Windows paths unchanged" in feature_skill_text + assert "do not run incomplete, bare, or trial `fluencyloop knowledge`" in feature_skill_text assert "Make the coverage decision explicit." in feature_skill_text assert "The first material feature is never an excuse" in feature_skill_text assert "Understanding checks are self-report, never quizzes" in feature_skill_text @@ -181,8 +186,8 @@ for feature_skill_text in [feature_text, codex_feature_text]: assert "### Product-overview diagram decision" in feature_skill_text assert "before drafting the overview" in feature_skill_text assert "Do not wait for the user to suggest a diagram" in feature_skill_text - assert "diagram fast path**. Give it" in feature_skill_text - assert "Do not ask the user to choose the style, type, or" in feature_skill_text + assert "**FluencyLoop native\nrenderer**" in feature_skill_text + assert "never edit its HTML" in feature_skill_text assert "Keep `product.md` prose-only" in feature_skill_text assert "site --ensure --open-once --json" in feature_skill_text assert "**Do not distill decisions.**" in feature_skill_text @@ -192,23 +197,11 @@ for diagram_skill_text in [ read_text(dist / "skills" / "diagram-design" / "SKILL.md"), ]: assert "## FluencyLoop embedded diagram fast path" in diagram_skill_text - assert "Do not load the full guide" in diagram_skill_text - assert "Load only that one type reference" in diagram_skill_text - assert "Use 4–7 nodes and 3–8 connectors" in diagram_skill_text - assert "do not add a Mermaid duplicate" in diagram_skill_text - assert "no Google Fonts ``, remote `src`/`href`" in diagram_skill_text - assert "Keep the existing light palette as the default" in diagram_skill_text - assert "--diagram-canvas" in diagram_skill_text - assert ':root[data-fluencyloop-theme="dark"]' in diagram_skill_text - assert "do not use JavaScript or a system-preference media query" in diagram_skill_text - assert "For an **architecture** diagram, default to\n unlabelled arrows" in diagram_skill_text - assert "label\n every decision exit in a flowchart and every message in a sequence diagram" in diagram_skill_text - assert "Never abbreviate a\n label merely to make it fit" in diagram_skill_text - assert "rounded orthogonal route" in diagram_skill_text - assert "Never overlap connector paths or reuse an attach point" in diagram_skill_text - assert "must never run behind a non-endpoint card" in diagram_skill_text - assert "8px of visible\n space from both the connector and every card" in diagram_skill_text - assert "no text behind a card, connector overlap, or viewBox clipping" in diagram_skill_text + assert "native diagram renderer" in diagram_skill_text + assert "2–8 nodes and at most 10 edges" in diagram_skill_text + assert "Run exactly one `fluencyloop diagram` command" in diagram_skill_text + assert "Do not run Playwright, browser automation, screenshots" in diagram_skill_text + assert "Never edit its generated HTML" in diagram_skill_text for full_guide_text in [ read_text(root / "claude-skills" / "diagram-design" / "references" / "full-guide.md"), read_text(dist / "skills" / "diagram-design" / "references" / "full-guide.md"), diff --git a/tests/powershell/add-knowledge.Tests.ps1 b/tests/powershell/add-knowledge.Tests.ps1 index 67fbf91..b74bee5 100644 --- a/tests/powershell/add-knowledge.Tests.ps1 +++ b/tests/powershell/add-knowledge.Tests.ps1 @@ -38,6 +38,18 @@ Describe 'add-knowledge.ps1' { } } + It 'accepts explicit fields without a pipe escape grammar' { + $before = @([System.IO.File]::ReadAllLines($script:store)).Count + (Invoke-FlExit 'add-knowledge.ps1' '--component' 'C:\work\app.config.ts' '--role' 'holds app providers | startup config' '--conditions' 'loaded before bootstrap' ` + '--component' 'Dog list' '--role' 'renders choices' '--conditions' 'uses the current selection' '--status' 'follow-up' ` + '--gotcha' 'selection | routing' '--why' 'a normal Windows path C:\work must stay literal') | Should -Be 0 + $records = @([System.IO.File]::ReadAllLines($script:store) | Select-Object -Last 3 | ForEach-Object { $_ | ConvertFrom-Json }) + $records[0].name | Should -Be 'C:\work\app.config.ts' + $records[0].role | Should -Be 'holds app providers | startup config' + $records[1].status | Should -Be 'follow-up' + $records[2].subject | Should -Be 'selection | routing' + } + It 'escaped pipes and backslashes round-trip through both record kinds' { (Invoke-FlExit 'add-knowledge.ps1' '--component' 'cache\|fallback|uses \\ local state|after a miss' ` '--gotcha' 'read\|write|keeps \\ ordering') | Should -Be 0 From 22cff80697e9e60c530c468f88df9f057835008a Mon Sep 17 00:00:00 2001 From: baokhang83 Date: Wed, 12 Aug 2026 19:12:45 +0200 Subject: [PATCH 2/2] fix: keep product overviews self-contained --- claude-skills/feature/SKILL.md | 5 ++++- plugins/fluencyloop/skills/feature/SKILL.md | 5 ++++- tests/plugin.bats | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/claude-skills/feature/SKILL.md b/claude-skills/feature/SKILL.md index d225712..195bc0a 100644 --- a/claude-skills/feature/SKILL.md +++ b/claude-skills/feature/SKILL.md @@ -487,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. diff --git a/plugins/fluencyloop/skills/feature/SKILL.md b/plugins/fluencyloop/skills/feature/SKILL.md index d2db127..23918ab 100644 --- a/plugins/fluencyloop/skills/feature/SKILL.md +++ b/plugins/fluencyloop/skills/feature/SKILL.md @@ -542,7 +542,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. diff --git a/tests/plugin.bats b/tests/plugin.bats index 5ab8169..2b776b8 100644 --- a/tests/plugin.bats +++ b/tests/plugin.bats @@ -175,6 +175,8 @@ for feature_skill_text in [feature_text, codex_feature_text]: assert "docs/fluencyloop/distillations/" in feature_skill_text assert "**Feature delta**" in feature_skill_text assert "**no overview rewrite**" in feature_skill_text + assert "do not add a `Related concepts` / `Related" in feature_skill_text + assert "raw file references add no explanation" in feature_skill_text assert "when this feature newly establishes a concept" in feature_skill_text assert "### Optional explanatory diagrams" in feature_skill_text assert "docs/fluencyloop/diagrams/product-overview.html" in feature_skill_text