diff --git a/.floop/corrections.jsonl b/.floop/corrections.jsonl index 02d983b..e6317f3 100644 --- a/.floop/corrections.jsonl +++ b/.floop/corrections.jsonl @@ -8,3 +8,4 @@ {"id":"c-1771390886273390213","timestamp":"2026-02-17T21:01:26.273390213-08:00","context":{"timestamp":"2026-02-17T21:01:26.269161096-08:00","repo":"git@github.com:nvandessel/feedback-loop.git","repo_root":".","branch":"main","project_type":"go","task":"development","user":"nvandessel","environment":"development"},"agent_action":"Committed and pushed Go code without running `gofmt` or `golangci-lint` locally first, causing CI lint failure on import ordering","human_response":"","corrected_action":"Before committing Go code, always run `gofmt -l` on changed files (or `gofmt -w` to auto-fix) to catch formatting issues. For broader checks, run `golangci-lint run` locally before pushing. Import groups must be alphabetically sorted within each group.","conversation_id":"","turn_number":0,"corrector":"mcp-client","processed":true,"processed_at":"2026-02-17T21:01:26.439987987-08:00"} {"id":"c-1771396948890433001","timestamp":"2026-02-17T22:42:28.890433001-08:00","context":{"timestamp":"2026-02-17T22:42:28.884624714-08:00","repo":"git@github.com:nvandessel/feedback-loop.git","repo_root":".","branch":"main","project_type":"go","task":"development","user":"nvandessel","environment":"development"},"agent_action":"Created a stacked PR (targeting a feature branch instead of main). When the base PR was squash-merged, GitHub auto-retargeted the stacked PR to main, but the branch still contained the original pre-squash commits from the base branch. This made the PR diff show extra files (JSON data files, beads) that weren't part of the actual change.","human_response":"","corrected_action":"After a base PR is squash-merged and GitHub retargets the stacked PR to main, rebase the stacked branch using `git rebase --onto origin/main \u003clast-base-commit\u003e ` to replay only the new commits onto main. This drops the pre-squash commits that are now redundant. Always review the PR diff (`gh pr diff --name-only`) after retargeting to catch stale files before requesting review.","conversation_id":"","turn_number":0,"corrector":"mcp-client","processed":true,"processed_at":"2026-02-17T22:42:29.081028409-08:00"} {"id":"c-1771396957423537500","timestamp":"2026-02-17T22:42:37.4235375-08:00","context":{"timestamp":"2026-02-17T22:42:37.416084797-08:00","repo":"git@github.com:nvandessel/feedback-loop.git","repo_root":".","branch":"main","project_type":"go","file_path":"internal/store/schema.go","file_language":"go","file_ext":".go","task":"development","user":"nvandessel","environment":"development"},"agent_action":"Assumed dirty tracking triggers on the main `behaviors` table would cover all changes that affect JSONL export. Stats changes (times_activated, times_confirmed, times_overridden) update `behavior_stats` directly, bypassing the behaviors table triggers entirely. Stats were silently lost on DB recreation/reimport.","human_response":"","corrected_action":"When a database schema separates frequently-updated data into satellite tables (like behavior_stats), each satellite table that contributes to the export needs its own dirty tracking trigger. The export path (getNodeUnlocked) and import path (addBehavior) may already handle the data correctly — the gap is specifically in the change detection layer. Audit all UPDATE paths to verify they fire dirty tracking.","conversation_id":"","turn_number":0,"corrector":"mcp-client","processed":true,"processed_at":"2026-02-17T22:42:37.592380325-08:00"} +{"id":"c-1771519733701650875","timestamp":"2026-02-19T08:48:53.701650875-08:00","context":{"timestamp":"2026-02-19T08:48:53.696325814-08:00","repo":"git@github.com:nvandessel/feedback-loop.git","repo_root":".","branch":"chore/capture-corrections-feb18","project_type":"go","user":"nvandessel","environment":"development"},"agent_action":"Only using weighted Jaccard score (when + content + tags blended) to determine similar-to edges. Two behaviors sharing 2+ tags but with different canonical text score ~0.3 and get no edge, breaking spreading activation.","human_response":"","corrected_action":"Add a tag-overlap rule for edge derivation: if two behaviors share \u003e= 2 tags, create a similar-to edge regardless of overall similarity score. Tag co-occurrence is a strong signal for conceptual relatedness — the whole premise of spreading activation is associative recall (git → branch, worktree, etc).","conversation_id":"","turn_number":0,"corrector":"mcp-client","processed":true,"processed_at":"2026-02-19T08:48:53.882350316-08:00"} diff --git a/.floop/edges.jsonl b/.floop/edges.jsonl index 37211d5..55a1f57 100644 --- a/.floop/edges.jsonl +++ b/.floop/edges.jsonl @@ -7,3 +7,65 @@ {"source":"behavior-dbef97df332b","target":"behavior-ae49727578ff","kind":"overrides","weight":0.8,"created_at":"2026-02-17T22:07:30-08:00"} {"source":"behavior-dbef97df332b","target":"behavior-beads-merged","kind":"overrides","weight":0.8,"created_at":"2026-02-17T22:07:30-08:00"} {"source":"behavior-dfdc5e16b9e0","target":"behavior-floop-scope-merged","kind":"similar-to","weight":0.8,"created_at":"2026-02-17T22:07:30-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-714d55f38be5","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-ae49727578ff","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-beads-merged","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-440356e972ee","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-23dd4a209d65","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-2c96557905fe","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-3889d615a50a","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-96a14bcf82a8","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-0ec1712a883b","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-e8c2f147f305","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-7d973646149f","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-889e959ff49f","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-4afe78b90fc4","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-6b92109dd498","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-45661080e612","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-8531ca06d020","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-43d1fb940f14","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-eec0fc443780","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-1de742da1707","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-5d991c44bfa0","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-4441585a0478","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-8845135841be","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-605806a1bb47","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-5c366b1ced0e","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-d92923c5a1e7","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-9f0962d71fa5","target":"behavior-8264fce58fd8","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:29-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-714d55f38be5","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-ae49727578ff","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-beads-merged","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-dfdc5e16b9e0","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-9f0962d71fa5","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-440356e972ee","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-23dd4a209d65","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-2c96557905fe","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-3889d615a50a","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-96a14bcf82a8","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-0ec1712a883b","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-e8c2f147f305","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-7d973646149f","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-889e959ff49f","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-4afe78b90fc4","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-6b92109dd498","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-45661080e612","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-8531ca06d020","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-43d1fb940f14","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-eec0fc443780","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-1de742da1707","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-5d991c44bfa0","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-4441585a0478","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-8845135841be","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-605806a1bb47","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-5c366b1ced0e","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-d92923c5a1e7","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-0cec8456c0e0","target":"behavior-8264fce58fd8","kind":"overrides","weight":1,"created_at":"2026-02-17T22:42:37-08:00"} +{"source":"behavior-dbef97df332b","target":"behavior-c83aad31d913","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-01cb13038379","target":"behavior-ae49727578ff","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-714d55f38be5","target":"behavior-5ca906dd04fc","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-ae49727578ff","target":"behavior-9c9d9ff7e96e","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-5ca906dd04fc","target":"behavior-150ae3a40011","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-5ca906dd04fc","target":"behavior-9c9d9ff7e96e","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-floop-scope-merged","target":"behavior-dfdc5e16b9e0","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} +{"source":"behavior-150ae3a40011","target":"behavior-9c9d9ff7e96e","kind":"similar-to","weight":0.8,"created_at":"2026-02-19T08:50:44-08:00"} diff --git a/.floop/nodes.jsonl b/.floop/nodes.jsonl index cff4b81..10811a8 100644 --- a/.floop/nodes.jsonl +++ b/.floop/nodes.jsonl @@ -1,24 +1,24 @@ -{"id":"behavior-150ae3a40011","kind":"behavior","content":{"content":{"canonical":"When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context.","expanded":"When working on this type of task, avoid: When auditing the spreading activation system, found features were working correctly but didn't capture the audit findings via floop_learn. A comprehensive system audit that validates end-to-end functionality is a valuable learning opportunity that should persist across sessions.\n\nInstead: When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context.","structured":{"avoid":"When auditing the spreading activation system, found features were working correctly but didn't capture the audit findings via floop_learn. A comprehensive system audit that validates end-to-end functionality is a valuable learning opportunity that should persist across sessions.","prefer":"When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context."},"tags":["floop","spreading-activation"]},"kind":"directive","name":"learned/when-completing-a-deep-system-audit-like-spreadin","provenance":{"correction_id":"c-1770867616134151808","source_type":"learned"}},"metadata":{"confidence":0.6,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-95d6b9f414e4","kind":"forgotten-behavior","content":{"content":{"canonical":"Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value.","expanded":"When working on this type of task, avoid: Assumed one storage location would be enough\n\nInstead: Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value.","structured":{"avoid":"Assumed one storage location would be enough","prefer":"Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value."}},"kind":"preference","name":"learned/global-scope-is-for-agents-personal-preferences-s","provenance":{"correction_id":"c-1769577671639472329","source_type":"learned"},"when":{}},"metadata":{"confidence":0.62,"forget_reason":"Superseded by behavior-floop-scope-merged","forgotten_at":"2026-02-07T19:08:32-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} +{"id":"behavior-b86932976d88","kind":"behavior","content":{"content":{"canonical":"Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits.","expanded":"When working on this type of task, avoid: Committed beads sync/housekeeping changes directly to main and pushed, following the plan without questioning it. Three commits went straight to main without a PR.\n\nInstead: Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits.","structured":{"avoid":"Committed beads sync/housekeeping changes directly to main and pushed, following the plan without questioning it. Three commits went straight to main without a PR.","prefer":"Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits."},"tags":["beads","pr"]},"kind":"constraint","name":"learned/even-for-beads-state-sync-and-chore-commits-creat","provenance":{"correction_id":"c-1770570585979824699","source_type":"learned"}},"metadata":{"confidence":0.6,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} +{"id":"behavior-91819ae1aa27","kind":"behavior","content":{"content":{"canonical":"Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention.","expanded":"When working on this type of task, avoid: Considered adding a 'floop refine' or 'floop edit' command to surgically fix bad learnings, which fights the human memory metaphor the system is built on.\n\nInstead: Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention.","structured":{"avoid":"Considered adding a 'floop refine' or 'floop edit' command to surgically fix bad learnings, which fights the human memory metaphor the system is built on.","prefer":"Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention."},"tags":["go","spreading-activation"]},"kind":"directive","name":"learned/bad-learnings-are-corrected-by-learning-again-bet","provenance":{"correction_id":"c-1770704353415650816","source_type":"learned"}},"metadata":{"confidence":0.7200000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-81e201973717","kind":"behavior","content":{"content":{"canonical":"Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive","expanded":"When working on this type of task, avoid: Created inline usage instructions in AGENTS.md\n\nInstead: Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive","structured":{"avoid":"Created inline usage instructions in AGENTS.md","prefer":"Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive"}},"kind":"directive","name":"learned/create-a-dedicated-usage-guide-docs-floop_usage-m","provenance":{"correction_id":"c-1769578257801753560","created_at":"2026-01-27T21:30:57.802170973-08:00","source_type":"learned"},"when":{"language":"markdown"}},"metadata":{"confidence":0.66,"priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} +{"id":"behavior-dbef97df332b","kind":"behavior","content":{"content":{"canonical":"Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template.","expanded":"When working on this type of task, avoid: Used template.HTML for injecting JSON into a script block in html/template. html/template applies JS-encoding on template.HTML values inside script contexts, turning JSON objects into quoted strings.\n\nInstead: Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template.","structured":{"avoid":"Used template.HTML for injecting JSON into a script block in html/template. html/template applies JS-encoding on template.HTML values inside script contexts, turning JSON objects into quoted strings.","prefer":"Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template."},"tags":["javascript","json","security"]},"kind":"preference","name":"learned/use-template-js-for-values-injected-into-script-bl","provenance":{"correction_id":"c-1770862232174665584","source_type":"learned"},"when":{"file_path":"visualization/*","language":"go"}},"metadata":{"confidence":0.625,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:38:40-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} {"id":"behavior-01cb13038379","kind":"behavior","content":{"content":{"canonical":"When documenting all CLI commands, also account for Cobra's implicit built-in commands: completion (shell autocompletion) and help. Run `floop --help` to get the full command list, not just what's in main.go","expanded":"When working on this type of task, avoid: When documenting CLI commands, only looked at explicitly registered commands in main.go and missed Cobra's auto-generated built-in commands (completion, help)\n\nInstead: When documenting all CLI commands, also account for Cobra's implicit built-in commands: completion (shell autocompletion) and help. Run `floop --help` to get the full command list, not just what's in main.go","structured":{"avoid":"When documenting CLI commands, only looked at explicitly registered commands in main.go and missed Cobra's auto-generated built-in commands (completion, help)","prefer":"When documenting all CLI commands, also account for Cobra's implicit built-in commands: completion (shell autocompletion) and help. Run `floop --help` to get the full command list, not just what's in main.go"},"tags":["bash","cli","floop","go"]},"kind":"directive","name":"learned/when-documenting-all-cli-commands-also-account-fo","provenance":{"correction_id":"c-1770713639385927936","source_type":"learned"}},"metadata":{"confidence":0.7000000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-9d180407fda6","kind":"behavior","content":{"content":{"canonical":"Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error.","expanded":"When working on this type of task, avoid: SQLite store uses INSERT OR REPLACE with a UNIQUE content_hash constraint. When two behaviors have identical canonical content, the second silently replaces the first instead of erroring or deduplicating properly.\n\nInstead: Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error.","structured":{"avoid":"SQLite store uses INSERT OR REPLACE with a UNIQUE content_hash constraint. When two behaviors have identical canonical content, the second silently replaces the first instead of erroring or deduplicating properly.","prefer":"Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error."},"tags":["behavior"]},"kind":"preference","name":"learned/content-hash-collision-during-insert-should-either","provenance":{"correction_id":"c-1770270981283723438","source_type":"learned"},"when":{"file_path":"store/*","language":"go"}},"metadata":{"confidence":0.66,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} -{"id":"behavior-9c9d9ff7e96e","kind":"behavior","content":{"content":{"canonical":"When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity >= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget.","expanded":"When working on this type of task, avoid: When writing tests for token budget enforcement, created behaviors with identical canonical content across all 40 items, triggering duplicate content detection. Also initially assumed behaviors with `when: {}` would exceed the budget at ~25 tokens each, not accounting for the sigmoid squashing (0.3 → 0.5 post-sigmoid) putting them at Summary tier where long content gets truncated to 60 chars (~15 tokens).\n\nInstead: When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity >= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget.","structured":{"avoid":"When writing tests for token budget enforcement, created behaviors with identical canonical content across all 40 items, triggering duplicate content detection. Also initially assumed behaviors with `when: {}` would exceed the budget at ~25 tokens each, not accounting for the sigmoid squashing (0.3 → 0.5 post-sigmoid) putting them at Summary tier where long content gets truncated to 60 chars (~15 tokens).","prefer":"When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity >= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget."},"tags":["behavior","ci","floop","spreading-activation","testing"]},"kind":"preference","name":"learned/when-testing-token-budget-enforcement-in-floop_act","provenance":{"correction_id":"c-1770712806536601605","source_type":"learned"},"when":{"task":"testing"}},"metadata":{"confidence":0.7000000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} {"id":"behavior-967fd13bd390","kind":"forgotten-behavior","content":{"content":{"canonical":"Use scope=both to save important learnings to both local and global","expanded":"When working on this type of task, avoid: Only saved to one store\n\nInstead: Use scope=both to save important learnings to both local and global","structured":{"avoid":"Only saved to one store","prefer":"Use scope=both to save important learnings to both local and global"}},"kind":"preference","name":"learned/use-scope=both-to-save-important-learnings-to-both","provenance":{"correction_id":"c-1769616836888545203","source_type":"learned"},"when":{}},"metadata":{"confidence":0.62,"forget_reason":"Superseded by behavior-floop-scope-merged","forgotten_at":"2026-02-07T19:08:32-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"both","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} {"id":"behavior-7a098cb05216","kind":"forgotten-behavior","content":{"content":{"canonical":"Always create detailed beads (epic + tasks) with dependency graphs after planning - enables easy handoff between agents and sessions","expanded":"When working on this type of task, avoid: Jumped directly into implementation after planning without creating beads structure\n\nInstead: Always create detailed beads (epic + tasks) with dependency graphs after planning - enables easy handoff between agents and sessions","structured":{"avoid":"Jumped directly into implementation after planning without creating beads structure","prefer":"Always create detailed beads (epic + tasks) with dependency graphs after planning - enables easy handoff between agents and sessions"}},"kind":"directive","name":"learned/always-create-detailed-beads-epic-+-tasks-with-d","provenance":{"correction_id":"c-1769580766063884670","source_type":"learned"}},"metadata":{"confidence":0.64,"forget_reason":"Superseded by behavior-beads-merged","forgotten_at":"2026-02-07T19:08:27-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} +{"id":"behavior-0070f24ee3e8","kind":"forgotten-behavior","content":{"content":{"canonical":"When working on beads: (1) claim with `bd update \u003cid\u003e --status in_progress` when starting, (2) close with `bd close \u003cid\u003e --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused.","expanded":"When working on this type of task, avoid: Made commits that closed beads without updating bead status - didn't claim beads when starting work, didn't close them with the associated commit, waited for user to remind me\n\nInstead: When working on beads: (1) claim with `bd update \u003cid\u003e --status in_progress` when starting, (2) close with `bd close \u003cid\u003e --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused.","structured":{"avoid":"Made commits that closed beads without updating bead status - didn't claim beads when starting work, didn't close them with the associated commit, waited for user to remind me","prefer":"When working on beads: (1) claim with `bd update \u003cid\u003e --status in_progress` when starting, (2) close with `bd close \u003cid\u003e --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused."}},"kind":"preference","name":"learned/when-working-on-beads-1-claim-with-`bd-update-\u003c","provenance":{"correction_id":"correction-1770095930","source_type":"learned"},"when":{}},"metadata":{"confidence":0.655,"forget_reason":"Superseded by behavior-beads-merged","forgotten_at":"2026-02-07T19:08:27-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"both","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} +{"id":"behavior-714d55f38be5","kind":"behavior","content":{"content":{"canonical":"When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results.","expanded":"When working on this type of task, avoid: During a deep docs audit, discovered significant documentation gaps (no centralized CLI reference, missing integration guides for 6+ tools, no architecture docs, undocumented token optimization features) but failed to capture the findings as a floop learning. The exploration yielded clear insights about the project's documentation state that should persist across sessions.\n\nInstead: When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results.","structured":{"avoid":"During a deep docs audit, discovered significant documentation gaps (no centralized CLI reference, missing integration guides for 6+ tools, no architecture docs, undocumented token optimization features) but failed to capture the findings as a floop learning. The exploration yielded clear insights about the project's documentation state that should persist across sessions.","prefer":"When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results."},"tags":["correction","floop"]},"kind":"preference","name":"learned/when-performing-audits-exploration-or-research-t","provenance":{"correction_id":"c-1770702998768527690","source_type":"learned"},"when":{}},"metadata":{"confidence":0.7400000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-ae49727578ff","kind":"behavior","content":{"content":{"canonical":"This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement.","expanded":"When working on this type of task, avoid: floop_learn returns requires_review: true for constraint behaviors, but there is no CLI command or MCP tool to actually approve or reject pending behaviors. The ApprovePending() and RejectPending() methods exist in internal/learning/loop.go but are not exposed to users.\n\nInstead: This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement.","structured":{"avoid":"floop_learn returns requires_review: true for constraint behaviors, but there is no CLI command or MCP tool to actually approve or reject pending behaviors. The ApprovePending() and RejectPending() methods exist in internal/learning/loop.go but are not exposed to users.","prefer":"This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement."},"tags":["behavior","cli","floop","mcp"]},"kind":"directive","name":"learned/this-is-a-known-feature-gap-the-review-mechanism","provenance":{"correction_id":"c-1770827519205934507","source_type":"learned"},"when":{}},"metadata":{"confidence":0.68,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-5ca906dd04fc","kind":"behavior","content":{"content":{"canonical":"Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out.","expanded":"When working on this type of task, avoid: Only called floop_learn when explicitly corrected or when making obvious mistakes. Missed learning opportunities during exploration, research, audits, and general work where insights were discovered but not captured.\n\nInstead: Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out.","structured":{"avoid":"Only called floop_learn when explicitly corrected or when making obvious mistakes. Missed learning opportunities during exploration, research, audits, and general work where insights were discovered but not captured.","prefer":"Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out."},"tags":["correction","floop","spreading-activation"]},"kind":"constraint","name":"learned/call-floop_learn-whenever-insights-emerge-during-a","provenance":{"correction_id":"c-1770704347456486267","source_type":"learned"}},"metadata":{"confidence":0.7200000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-floop-scope-merged","kind":"behavior","content":{"content":{"canonical":"Floop has two scopes: global (~/.floop/) for agent personal preferences across ALL projects, and local (./.floop/) for project-specific conventions. Use scope=both to save important learnings to both stores.","tags":["floop"]},"kind":"preference","name":"learned/floop-scope-strategy","provenance":{"source_type":"merged"},"when":{}},"metadata":{"confidence":0.72,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} +{"id":"behavior-150ae3a40011","kind":"behavior","content":{"content":{"canonical":"When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context.","expanded":"When working on this type of task, avoid: When auditing the spreading activation system, found features were working correctly but didn't capture the audit findings via floop_learn. A comprehensive system audit that validates end-to-end functionality is a valuable learning opportunity that should persist across sessions.\n\nInstead: When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context.","structured":{"avoid":"When auditing the spreading activation system, found features were working correctly but didn't capture the audit findings via floop_learn. A comprehensive system audit that validates end-to-end functionality is a valuable learning opportunity that should persist across sessions.","prefer":"When completing a deep system audit (like spreading activation + tag affinity validation), immediately capture the key findings via floop_learn: what was tested, what works, what gaps were found, what dictionary keywords are missing, connectivity metrics, token budget health. These audit results are high-value persistent context."},"tags":["floop","spreading-activation"]},"kind":"directive","name":"learned/when-completing-a-deep-system-audit-like-spreadin","provenance":{"correction_id":"c-1770867616134151808","source_type":"learned"}},"metadata":{"confidence":0.6,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-9d180407fda6","kind":"behavior","content":{"content":{"canonical":"Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error.","expanded":"When working on this type of task, avoid: SQLite store uses INSERT OR REPLACE with a UNIQUE content_hash constraint. When two behaviors have identical canonical content, the second silently replaces the first instead of erroring or deduplicating properly.\n\nInstead: Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error.","structured":{"avoid":"SQLite store uses INSERT OR REPLACE with a UNIQUE content_hash constraint. When two behaviors have identical canonical content, the second silently replaces the first instead of erroring or deduplicating properly.","prefer":"Content hash collision during INSERT should either error explicitly or trigger proper deduplication flow. The current silent replace behavior can cause data loss. Consider: (1) remove UNIQUE constraint and handle dedup separately, or (2) check for existing content_hash before insert and return meaningful error."},"tags":["behavior"]},"kind":"preference","name":"learned/content-hash-collision-during-insert-should-either","provenance":{"correction_id":"c-1770270981283723438","source_type":"learned"},"when":{"file_path":"store/*","language":"go"}},"metadata":{"confidence":0.66,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} +{"id":"behavior-9c9d9ff7e96e","kind":"behavior","content":{"content":{"canonical":"When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity \u003e= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget.","expanded":"When working on this type of task, avoid: When writing tests for token budget enforcement, created behaviors with identical canonical content across all 40 items, triggering duplicate content detection. Also initially assumed behaviors with `when: {}` would exceed the budget at ~25 tokens each, not accounting for the sigmoid squashing (0.3 → 0.5 post-sigmoid) putting them at Summary tier where long content gets truncated to 60 chars (~15 tokens).\n\nInstead: When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity \u003e= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget.","structured":{"avoid":"When writing tests for token budget enforcement, created behaviors with identical canonical content across all 40 items, triggering duplicate content detection. Also initially assumed behaviors with `when: {}` would exceed the budget at ~25 tokens each, not accounting for the sigmoid squashing (0.3 → 0.5 post-sigmoid) putting them at Summary tier where long content gets truncated to 60 chars (~15 tokens).","prefer":"When testing token budget enforcement in floop_active: (1) each behavior MUST have unique canonical content due to duplicate content detection, (2) understand the full activation pipeline: specificity → SpecificityToActivation → spreading engine sigmoid(x) → tier thresholds. Specificity 0 → activation 0.3 → sigmoid 0.5 → Summary tier. Specificity 1 → 0.4 → sigmoid ~0.731 → Full tier. To trigger budget demotion, use language-matched behaviors (specificity \u003e= 1, Full tier) with large canonical content so the total exceeds the 2000-token budget."},"tags":["behavior","ci","floop","spreading-activation","testing"]},"kind":"preference","name":"learned/when-testing-token-budget-enforcement-in-floop_act","provenance":{"correction_id":"c-1770712806536601605","source_type":"learned"},"when":{"task":"testing"}},"metadata":{"confidence":0.7000000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} {"id":"behavior-9c3d12c74c61","kind":"behavior","content":{"content":{"canonical":"Expand tag extraction dictionary with: d.add(\"documentation\", \"documentation\", \"docs\", \"guide\", \"readme\"), d.add(\"subagent\", \"subagent\", \"subagents\", \"agent\", \"agents\", \"orchestrator\"), d.add(\"permissions\", \"permissions\", \"permission\", \"allow\", \"deny\"). Map \"parallel\" to existing \"concurrency\" tag, \"scope\" to \"configuration\", \"hooks\" to \"workflow\". Then run `floop tags backfill` to achieve 100% tag coverage.","expanded":"When working on this type of task, avoid: Tag extraction dictionary in internal/tagging/dictionary.go is missing keywords that appear in behavior content: \"documentation\"/\"docs\"/\"guide\" (3 untagged behaviors mention this), \"subagent\"/\"subagents\"/\"agents\" (9 mentions), \"permissions\"/\"permission\" (6 mentions), \"parallel\"/\"parallelization\" (4 mentions), \"scope\" (3 mentions), \"hooks\" (3 mentions). This leaves 5% of behaviors untagged.\n\nInstead: Expand tag extraction dictionary with: d.add(\"documentation\", \"documentation\", \"docs\", \"guide\", \"readme\"), d.add(\"subagent\", \"subagent\", \"subagents\", \"agent\", \"agents\", \"orchestrator\"), d.add(\"permissions\", \"permissions\", \"permission\", \"allow\", \"deny\"). Map \"parallel\" to existing \"concurrency\" tag, \"scope\" to \"configuration\", \"hooks\" to \"workflow\". Then run `floop tags backfill` to achieve 100% tag coverage.","structured":{"avoid":"Tag extraction dictionary in internal/tagging/dictionary.go is missing keywords that appear in behavior content: \"documentation\"/\"docs\"/\"guide\" (3 untagged behaviors mention this), \"subagent\"/\"subagents\"/\"agents\" (9 mentions), \"permissions\"/\"permission\" (6 mentions), \"parallel\"/\"parallelization\" (4 mentions), \"scope\" (3 mentions), \"hooks\" (3 mentions). This leaves 5% of behaviors untagged.","prefer":"Expand tag extraction dictionary with: d.add(\"documentation\", \"documentation\", \"docs\", \"guide\", \"readme\"), d.add(\"subagent\", \"subagent\", \"subagents\", \"agent\", \"agents\", \"orchestrator\"), d.add(\"permissions\", \"permissions\", \"permission\", \"allow\", \"deny\"). Map \"parallel\" to existing \"concurrency\" tag, \"scope\" to \"configuration\", \"hooks\" to \"workflow\". Then run `floop tags backfill` to achieve 100% tag coverage."},"tags":["concurrency","configuration","floop","workflow"]},"kind":"procedure","name":"learned/expand-tag-extraction-dictionary-with-d-adddocu","provenance":{"correction_id":"c-1770867776463535061","source_type":"learned"}},"metadata":{"confidence":0.6,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-0070f24ee3e8","kind":"forgotten-behavior","content":{"content":{"canonical":"When working on beads: (1) claim with `bd update --status in_progress` when starting, (2) close with `bd close --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused.","expanded":"When working on this type of task, avoid: Made commits that closed beads without updating bead status - didn't claim beads when starting work, didn't close them with the associated commit, waited for user to remind me\n\nInstead: When working on beads: (1) claim with `bd update --status in_progress` when starting, (2) close with `bd close --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused.","structured":{"avoid":"Made commits that closed beads without updating bead status - didn't claim beads when starting work, didn't close them with the associated commit, waited for user to remind me","prefer":"When working on beads: (1) claim with `bd update --status in_progress` when starting, (2) close with `bd close --reason \"...\"` at the same time as the commit that completes the work. Keep bead state synchronized with actual work progress so other agents aren't confused."}},"kind":"preference","name":"learned/when-working-on-beads-1-claim-with-`bd-update-<","provenance":{"correction_id":"correction-1770095930","source_type":"learned"},"when":{}},"metadata":{"confidence":0.655,"forget_reason":"Superseded by behavior-beads-merged","forgotten_at":"2026-02-07T19:08:27-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"both","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} -{"id":"behavior-b86932976d88","kind":"behavior","content":{"content":{"canonical":"Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits.","expanded":"When working on this type of task, avoid: Committed beads sync/housekeeping changes directly to main and pushed, following the plan without questioning it. Three commits went straight to main without a PR.\n\nInstead: Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits.","structured":{"avoid":"Committed beads sync/housekeeping changes directly to main and pushed, following the plan without questioning it. Three commits went straight to main without a PR.","prefer":"Even for beads state sync and chore commits, create a feature branch and PR. The rule 'never merge directly to main' applies to ALL commits, not just code changes. Push back on plans that specify direct-to-main commits."},"tags":["beads","pr"]},"kind":"constraint","name":"learned/even-for-beads-state-sync-and-chore-commits-creat","provenance":{"correction_id":"c-1770570585979824699","source_type":"learned"}},"metadata":{"confidence":0.6,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} {"id":"behavior-8341f0d52ce6","kind":"behavior","content":{"content":{"canonical":"MCP go-sdk expects jsonschema:\"Description text\" format without key=value syntax. The tag value is directly the description.","expanded":"When working on this type of task, avoid: Subagent used jsonschema:\"description=...\" tag format which caused MCP SDK panic\n\nInstead: MCP go-sdk expects jsonschema:\"Description text\" format without key=value syntax. The tag value is directly the description.","structured":{"avoid":"Subagent used jsonschema:\"description=...\" tag format which caused MCP SDK panic","prefer":"MCP go-sdk expects jsonschema:\"Description text\" format without key=value syntax. The tag value is directly the description."},"tags":["mcp"]},"kind":"directive","name":"learned/mcp-go-sdk-expects-jsonschemadescription-text-f","provenance":{"correction_id":"correction-1769817031","source_type":"learned"},"when":{"file_path":"mcp/*","language":"go"}},"metadata":{"confidence":0.66,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} -{"id":"behavior-91819ae1aa27","kind":"behavior","content":{"content":{"canonical":"Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention.","expanded":"When working on this type of task, avoid: Considered adding a 'floop refine' or 'floop edit' command to surgically fix bad learnings, which fights the human memory metaphor the system is built on.\n\nInstead: Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention.","structured":{"avoid":"Considered adding a 'floop refine' or 'floop edit' command to surgically fix bad learnings, which fights the human memory metaphor the system is built on.","prefer":"Bad learnings are corrected by learning again, better — not by editing. The spreading activation system mirrors human memory: you can't edit a memory, but you can form a stronger one that outcompetes it. Broader learnings activate in more contexts, win token budget competition, and the narrow/wrong ones naturally go dormant. Trust the system to self-correct through more learning, not surgical intervention."},"tags":["go","spreading-activation"]},"kind":"directive","name":"learned/bad-learnings-are-corrected-by-learning-again-bet","provenance":{"correction_id":"c-1770704353415650816","source_type":"learned"}},"metadata":{"confidence":0.7200000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} {"id":"behavior-a9a4b09dda1f","kind":"behavior","content":{"content":{"canonical":"Create explicit hooks in AGENTS.md that mandate tool usage - use WARNING/CRITICAL markers and 'READ FIRST' to ensure visibility","expanded":"When working on this type of task, avoid: Assumed agents would know when to use tools based on general context\n\nInstead: Create explicit hooks in AGENTS.md that mandate tool usage - use WARNING/CRITICAL markers and 'READ FIRST' to ensure visibility","structured":{"avoid":"Assumed agents would know when to use tools based on general context","prefer":"Create explicit hooks in AGENTS.md that mandate tool usage - use WARNING/CRITICAL markers and 'READ FIRST' to ensure visibility"}},"kind":"procedure","name":"learned/create-explicit-hooks-in-agents-md-that-mandate-to","provenance":{"correction_id":"c-1769578263708845277","created_at":"2026-01-27T21:31:03.709247071-08:00","source_type":"learned"},"when":{}},"metadata":{"confidence":0.66,"priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} -{"id":"behavior-714d55f38be5","kind":"behavior","content":{"content":{"canonical":"When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results.","expanded":"When working on this type of task, avoid: During a deep docs audit, discovered significant documentation gaps (no centralized CLI reference, missing integration guides for 6+ tools, no architecture docs, undocumented token optimization features) but failed to capture the findings as a floop learning. The exploration yielded clear insights about the project's documentation state that should persist across sessions.\n\nInstead: When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results.","structured":{"avoid":"During a deep docs audit, discovered significant documentation gaps (no centralized CLI reference, missing integration guides for 6+ tools, no architecture docs, undocumented token optimization features) but failed to capture the findings as a floop learning. The exploration yielded clear insights about the project's documentation state that should persist across sessions.","prefer":"When performing audits, exploration, or research that reveals project-level insights (gaps, patterns, architectural understanding), immediately capture findings via floop_learn. Any discovery that would be useful context for future sessions is a learning opportunity — not just corrections or mistakes. Proactive learning includes: audit findings, documentation gaps identified, architectural patterns discovered, feature inventory results."},"tags":["correction","floop"]},"kind":"preference","name":"learned/when-performing-audits-exploration-or-research-t","provenance":{"correction_id":"c-1770702998768527690","source_type":"learned"},"when":{}},"metadata":{"confidence":0.7400000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-95d6b9f414e4","kind":"forgotten-behavior","content":{"content":{"canonical":"Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value.","expanded":"When working on this type of task, avoid: Assumed one storage location would be enough\n\nInstead: Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value.","structured":{"avoid":"Assumed one storage location would be enough","prefer":"Global scope is for agent's personal preferences/style across ALL work. Local scope is for team/project-specific conventions. Both have distinct value."}},"kind":"preference","name":"learned/global-scope-is-for-agents-personal-preferences-s","provenance":{"correction_id":"c-1769577671639472329","source_type":"learned"},"when":{}},"metadata":{"confidence":0.62,"forget_reason":"Superseded by behavior-floop-scope-merged","forgotten_at":"2026-02-07T19:08:32-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} {"id":"behavior-0b09b561c160","kind":"forgotten-behavior","content":{"content":{"canonical":"Always close beads when completing associated work - beads should close with their work, not be left dangling","expanded":"When working on this type of task, avoid: Completed work without closing the associated beads\n\nInstead: Always close beads when completing associated work - beads should close with their work, not be left dangling","structured":{"avoid":"Completed work without closing the associated beads","prefer":"Always close beads when completing associated work - beads should close with their work, not be left dangling"}},"kind":"directive","name":"learned/always-close-beads-when-completing-associated-work","provenance":{"correction_id":"correction-1770083726","source_type":"learned"},"when":{}},"metadata":{"confidence":0.62,"forget_reason":"Superseded by behavior-beads-merged","forgotten_at":"2026-02-07T19:08:27-08:00","forgotten_by":"nvandessel","original_kind":"behavior","priority":0,"scope":"both","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} -{"id":"behavior-c83aad31d913","kind":"behavior","content":{"content":{"canonical":"Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts < > & to unicode escapes, preventing breakout)","expanded":"When working on this type of task, avoid: Used template.HTML for JSON data inside an html/template block, which caused the template engine to JS-escape the value (wrapping in quotes and escaping inner quotes), turning the JSON object into a string literal\n\nInstead: Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts < > & to unicode escapes, preventing breakout)","structured":{"avoid":"Used template.HTML for JSON data inside an html/template block, which caused the template engine to JS-escape the value (wrapping in quotes and escaping inner quotes), turning the JSON object into a string literal","prefer":"Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts < > & to unicode escapes, preventing breakout)"},"tags":["javascript","json","security"]},"kind":"preference","name":"learned/use-template-js-not-template-html-for-data-injec","provenance":{"correction_id":"c-1770862037019587237","source_type":"learned"},"when":{"file_path":"visualization/*","language":"go"}},"metadata":{"confidence":0.625,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:38:40-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-ae49727578ff","kind":"behavior","content":{"content":{"canonical":"This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement.","expanded":"When working on this type of task, avoid: floop_learn returns requires_review: true for constraint behaviors, but there is no CLI command or MCP tool to actually approve or reject pending behaviors. The ApprovePending() and RejectPending() methods exist in internal/learning/loop.go but are not exposed to users.\n\nInstead: This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement.","structured":{"avoid":"floop_learn returns requires_review: true for constraint behaviors, but there is no CLI command or MCP tool to actually approve or reject pending behaviors. The ApprovePending() and RejectPending() methods exist in internal/learning/loop.go but are not exposed to users.","prefer":"This is a known feature gap. The review mechanism (ApprovePending/RejectPending) exists internally but needs to be exposed as MCP tools (floop_approve/floop_reject) and/or CLI commands. Pending behaviors still activate via floop_active — they just lack the approval stamp and confidence boost. Track this as a feature to implement."},"tags":["behavior","cli","floop","mcp"]},"kind":"directive","name":"learned/this-is-a-known-feature-gap-the-review-mechanism","provenance":{"correction_id":"c-1770827519205934507","source_type":"learned"},"when":{}},"metadata":{"confidence":0.68,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-beads-merged","kind":"behavior","content":{"content":{"canonical":"When working with beads: (1) create detailed epics + tasks with dependency graphs after planning, (2) claim work with 'bd update --status in_progress' when starting, (3) close with 'bd close --reason \"...\"' when committing the completed work. Keep bead state synchronized with actual work progress.","tags":["beads"]},"kind":"directive","name":"learned/beads-workflow","provenance":{"source_type":"merged"},"when":{}},"metadata":{"confidence":0.72,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} -{"id":"behavior-81e201973717","kind":"behavior","content":{"content":{"canonical":"Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive","expanded":"When working on this type of task, avoid: Created inline usage instructions in AGENTS.md\n\nInstead: Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive","structured":{"avoid":"Created inline usage instructions in AGENTS.md","prefer":"Create a dedicated usage guide (docs/FLOOP_USAGE.md) and reference it prominently in AGENTS.md - separates 'what' from 'how' and makes instructions comprehensive"}},"kind":"directive","name":"learned/create-a-dedicated-usage-guide-docs-floop_usage-m","provenance":{"correction_id":"c-1769578257801753560","created_at":"2026-01-27T21:30:57.802170973-08:00","source_type":"learned"},"when":{"language":"markdown"}},"metadata":{"confidence":0.66,"priority":0,"scope":"local","stats":{"created_at":"2026-02-16T20:35:07-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-16T20:35:07-08:00"}}} -{"id":"behavior-5ca906dd04fc","kind":"behavior","content":{"content":{"canonical":"Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out.","expanded":"When working on this type of task, avoid: Only called floop_learn when explicitly corrected or when making obvious mistakes. Missed learning opportunities during exploration, research, audits, and general work where insights were discovered but not captured.\n\nInstead: Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out.","structured":{"avoid":"Only called floop_learn when explicitly corrected or when making obvious mistakes. Missed learning opportunities during exploration, research, audits, and general work where insights were discovered but not captured.","prefer":"Call floop_learn whenever insights emerge during ANY work — not just corrections. Discovery of gaps, patterns, architectural understanding, feature inventories, design decisions, and any finding that would benefit future sessions is a learning opportunity. The system handles bloat through spreading activation, token budgets, and tiering — so learn freely and trust the system to curate. If a previous learning was too narrow, don't try to edit it — just learn the better version and let weights sort it out."},"tags":["correction","floop","spreading-activation"]},"kind":"constraint","name":"learned/call-floop_learn-whenever-insights-emerge-during-a","provenance":{"correction_id":"c-1770704347456486267","source_type":"learned"}},"metadata":{"confidence":0.7200000000000001,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:19:55-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-c83aad31d913","kind":"behavior","content":{"content":{"canonical":"Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts \u003c \u003e \u0026 to unicode escapes, preventing breakout)","expanded":"When working on this type of task, avoid: Used template.HTML for JSON data inside an html/template block, which caused the template engine to JS-escape the value (wrapping in quotes and escaping inner quotes), turning the JSON object into a string literal\n\nInstead: Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts \u003c \u003e \u0026 to unicode escapes, preventing breakout)","structured":{"avoid":"Used template.HTML for JSON data inside an html/template block, which caused the template engine to JS-escape the value (wrapping in quotes and escaping inner quotes), turning the JSON object into a string literal","prefer":"Use template.JS (not template.HTML) for data injected into blocks in html/template. template.HTML only asserts HTML safety — in JS contexts the engine still JS-escapes it. template.JS asserts JS safety and prevents double-encoding. Combine with json.HTMLEscape for XSS prevention (converts \u003c \u003e \u0026 to unicode escapes, preventing breakout)"},"tags":["javascript","json","security"]},"kind":"preference","name":"learned/use-template-js-not-template-html-for-data-injec","provenance":{"correction_id":"c-1770862037019587237","source_type":"learned"},"when":{"file_path":"visualization/*","language":"go"}},"metadata":{"confidence":0.625,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:38:40-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} +{"id":"behavior-beads-merged","kind":"behavior","content":{"content":{"canonical":"When working with beads: (1) create detailed epics + tasks with dependency graphs after planning, (2) claim work with 'bd update \u003cid\u003e --status in_progress' when starting, (3) close with 'bd close \u003cid\u003e --reason \"...\"' when committing the completed work. Keep bead state synchronized with actual work progress.","tags":["beads"]},"kind":"directive","name":"learned/beads-workflow","provenance":{"source_type":"merged"},"when":{}},"metadata":{"confidence":0.72,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} {"id":"behavior-dfdc5e16b9e0","kind":"behavior","content":{"content":{"canonical":"Consider both global (~/.floop/) and local (./.floop/) scopes - users want personal preferences across ALL projects AND project-specific conventions","expanded":"When working on this type of task, avoid: Only considered project-local behavior storage\n\nInstead: Consider both global (~/.floop/) and local (./.floop/) scopes - users want personal preferences across ALL projects AND project-specific conventions","structured":{"avoid":"Only considered project-local behavior storage","prefer":"Consider both global (~/.floop/) and local (./.floop/) scopes - users want personal preferences across ALL projects AND project-specific conventions"},"tags":["floop"]},"kind":"preference","name":"learned/consider-both-global-~-floop-and-local-flo","provenance":{"correction_id":"c-1769577654747582237","source_type":"learned"},"when":{"file_path":"store/*","language":"go"}},"metadata":{"confidence":0.66,"priority":0,"scope":"local","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} -{"id":"behavior-dbef97df332b","kind":"behavior","content":{"content":{"canonical":"Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template.","expanded":"When working on this type of task, avoid: Used template.HTML for injecting JSON into a script block in html/template. html/template applies JS-encoding on template.HTML values inside script contexts, turning JSON objects into quoted strings.\n\nInstead: Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template.","structured":{"avoid":"Used template.HTML for injecting JSON into a script block in html/template. html/template applies JS-encoding on template.HTML values inside script contexts, turning JSON objects into quoted strings.","prefer":"Use template.JS for values injected into script blocks. Pre-sanitize with json.HTMLEscape to prevent script breakout XSS. template.HTML is only trusted for HTML contexts, not JS contexts in html/template."},"tags":["javascript","json","security"]},"kind":"preference","name":"learned/use-template-js-for-values-injected-into-script-bl","provenance":{"correction_id":"c-1770862232174665584","source_type":"learned"},"when":{"file_path":"visualization/*","language":"go"}},"metadata":{"confidence":0.625,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T21:48:42-08:00","last_activated":"2026-02-11T19:38:40-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T21:48:42-08:00"}}} -{"id":"behavior-floop-scope-merged","kind":"behavior","content":{"content":{"canonical":"Floop has two scopes: global (~/.floop/) for agent personal preferences across ALL projects, and local (./.floop/) for project-specific conventions. Use scope=both to save important learnings to both stores.","tags":["floop"]},"kind":"preference","name":"learned/floop-scope-strategy","provenance":{"source_type":"merged"},"when":{}},"metadata":{"confidence":0.72,"priority":0,"scope":"both","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} {"id":"behavior-b7f5d1378039","kind":"behavior","content":{"content":{"canonical":"Beads export state auto-updates with each commit hash. After final push, restore the export state file instead of committing it again to break the cycle.","expanded":"When working on this type of task, avoid: Committed beads export state changes in a loop, causing infinite commits\n\nInstead: Beads export state auto-updates with each commit hash. After final push, restore the export state file instead of committing it again to break the cycle.","structured":{"avoid":"Committed beads export state changes in a loop, causing infinite commits","prefer":"Beads export state auto-updates with each commit hash. After final push, restore the export state file instead of committing it again to break the cycle."},"tags":["beads","filesystem"]},"kind":"preference","name":"learned/beads-export-state-auto-updates-with-each-commit-h","provenance":{"correction_id":"c-1769581392105512844","source_type":"learned"},"when":{}},"metadata":{"confidence":0.66,"priority":0,"scope":"local","stats":{"created_at":"2026-02-17T22:03:17-08:00","times_activated":0,"times_confirmed":0,"times_followed":0,"times_overridden":0,"updated_at":"2026-02-17T22:03:17-08:00"}}} diff --git a/cmd/floop/cmd_derive_edges.go b/cmd/floop/cmd_derive_edges.go new file mode 100644 index 0000000..3d83ec9 --- /dev/null +++ b/cmd/floop/cmd_derive_edges.go @@ -0,0 +1,369 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/nvandessel/feedback-loop/internal/constants" + "github.com/nvandessel/feedback-loop/internal/models" + "github.com/nvandessel/feedback-loop/internal/ranking" + "github.com/nvandessel/feedback-loop/internal/similarity" + "github.com/nvandessel/feedback-loop/internal/store" + "github.com/spf13/cobra" +) + +// minSharedTagsForEdge is the minimum number of shared tags between two +// behaviors to create a similar-to edge, regardless of overall similarity +// score. Tag co-occurrence is a strong signal for conceptual relatedness — +// if two behaviors both have "git" and "worktree" tags, spreading activation +// needs that edge to associate related concepts. +const minSharedTagsForEdge = 2 + +func newDeriveEdgesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "derive-edges", + Short: "Derive similar-to and overrides edges from behavior similarity", + Long: `Analyze all behaviors pairwise and create edges based on similarity scores. + +For each pair of behaviors: + - If similarity is in [0.5, 0.9): create a similar-to edge (weight 0.8) + - If one behavior's when-conditions are a strict superset: create an overrides edge (weight 1.0) + +Existing edges are preserved unless --clear is used. + +Examples: + floop derive-edges # Derive edges for both stores + floop derive-edges --dry-run # Preview without creating edges + floop derive-edges --scope global # Only process global store + floop derive-edges --clear # Remove existing derived edges first`, + RunE: func(cmd *cobra.Command, args []string) error { + root, _ := cmd.Flags().GetString("root") + jsonOut, _ := cmd.Flags().GetBool("json") + dryRun, _ := cmd.Flags().GetBool("dry-run") + clear, _ := cmd.Flags().GetBool("clear") + scope, _ := cmd.Flags().GetString("scope") + + storeScope := constants.Scope(scope) + if !storeScope.Valid() { + return fmt.Errorf("invalid scope: %s (must be local, global, or both)", scope) + } + + ctx := context.Background() + var allResults []deriveStoreResult + + if storeScope == constants.ScopeLocal || storeScope == constants.ScopeBoth { + floopDir := filepath.Join(root, ".floop") + if _, err := os.Stat(floopDir); os.IsNotExist(err) { + return fmt.Errorf(".floop not initialized. Run 'floop init' first") + } + graphStore, err := store.NewFileGraphStore(root) + if err != nil { + return fmt.Errorf("failed to open local store: %w", err) + } + defer graphStore.Close() + result, err := deriveEdgesForStore(ctx, graphStore, "local", dryRun, clear) + if err != nil { + return fmt.Errorf("local store: %w", err) + } + allResults = append(allResults, result) + } + + if storeScope == constants.ScopeGlobal || storeScope == constants.ScopeBoth { + globalPath, err := store.GlobalFloopPath() + if err != nil { + return fmt.Errorf("failed to get global path: %w", err) + } + if _, err := os.Stat(globalPath); os.IsNotExist(err) { + return fmt.Errorf("global .floop not initialized. Run 'floop init --global' first") + } + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + graphStore, err := store.NewFileGraphStore(homeDir) + if err != nil { + return fmt.Errorf("failed to open global store: %w", err) + } + defer graphStore.Close() + result, err := deriveEdgesForStore(ctx, graphStore, "global", dryRun, clear) + if err != nil { + return fmt.Errorf("global store: %w", err) + } + allResults = append(allResults, result) + } + + if jsonOut { + return json.NewEncoder(os.Stdout).Encode(map[string]interface{}{ + "dry_run": dryRun, + "clear": clear, + "stores": allResults, + }) + } + + for _, r := range allResults { + printDeriveResult(r, dryRun) + } + return nil + }, + } + + cmd.Flags().Bool("dry-run", false, "Show proposed edges without creating them") + cmd.Flags().Bool("clear", false, "Remove existing similar-to and overrides edges before deriving") + cmd.Flags().String("scope", "both", "Store scope: local, global, or both") + + return cmd +} + +// deriveStoreResult holds the output for one store's edge derivation. +type deriveStoreResult struct { + Scope string `json:"scope"` + Behaviors int `json:"behaviors"` + ExistingEdges int `json:"existing_edges"` + ClearedEdges int `json:"cleared_edges"` + ProposedEdges []proposedEdge `json:"proposed_edges"` + CreatedEdges int `json:"created_edges"` + SkippedExisting int `json:"skipped_existing"` + Histogram [10]int `json:"score_histogram"` + Connectivity connectivityInfo `json:"connectivity"` +} + +// proposedEdge represents a single proposed edge. +type proposedEdge struct { + Source string `json:"source"` + Target string `json:"target"` + Kind string `json:"kind"` + Weight float64 `json:"weight"` + Score float64 `json:"score"` +} + +// connectivityInfo describes graph connectivity after edge derivation. +type connectivityInfo struct { + TotalNodes int `json:"total_nodes"` + Islands int `json:"islands"` + Connected int `json:"connected"` +} + +// deriveEdgesForStore runs the edge derivation algorithm on a single store. +func deriveEdgesForStore(ctx context.Context, graphStore store.GraphStore, scope string, dryRun, clear bool) (deriveStoreResult, error) { + result := deriveStoreResult{Scope: scope} + + // Load all non-forgotten behaviors + behaviors, err := loadBehaviorsFromStore(ctx, graphStore) + if err != nil { + return result, fmt.Errorf("failed to load behaviors: %w", err) + } + result.Behaviors = len(behaviors) + + if len(behaviors) == 0 { + return result, nil + } + + // Clear existing derived edges if requested + if clear && !dryRun { + result.ClearedEdges = clearDerivedEdges(ctx, graphStore, behaviors) + } + + // Build existing edge set for dedup + existingEdges := make(map[string]bool) + for _, b := range behaviors { + edges, err := graphStore.GetEdges(ctx, b.ID, store.DirectionOutbound, "") + if err != nil { + continue + } + for _, e := range edges { + key := e.Source + ":" + e.Target + ":" + e.Kind + existingEdges[key] = true + } + result.ExistingEdges += len(edges) + } + + // All-pairs comparison + now := time.Now() + for i := 0; i < len(behaviors); i++ { + for j := i + 1; j < len(behaviors); j++ { + a := &behaviors[i] + b := &behaviors[j] + + // Compute similarity (no LLM) + score := computeBehaviorSimilarity(a, b, nil, false) + + // Record in histogram (10 buckets: [0.0,0.1), [0.1,0.2), ..., [0.9,1.0]) + bucket := int(score * 10) + if bucket >= 10 { + bucket = 9 + } + result.Histogram[bucket]++ + + // Check for overrides edges (specificity) + if similarity.IsMoreSpecific(a.When, b.When) { + pe := proposedEdge{Source: a.ID, Target: b.ID, Kind: "overrides", Weight: 1.0, Score: score} + key := a.ID + ":" + b.ID + ":overrides" + if existingEdges[key] { + result.SkippedExisting++ + } else { + result.ProposedEdges = append(result.ProposedEdges, pe) + existingEdges[key] = true + } + } + if similarity.IsMoreSpecific(b.When, a.When) { + pe := proposedEdge{Source: b.ID, Target: a.ID, Kind: "overrides", Weight: 1.0, Score: score} + key := b.ID + ":" + a.ID + ":overrides" + if existingEdges[key] { + result.SkippedExisting++ + } else { + result.ProposedEdges = append(result.ProposedEdges, pe) + existingEdges[key] = true + } + } + + // Check for similar-to edges: + // 1. Score-based: similarity in [0.5, 0.9) + // 2. Tag-based: behaviors sharing >= 2 tags are conceptually related + // and need edges for spreading activation (git → branch, worktree, etc.) + similarToKey := a.ID + ":" + b.ID + ":similar-to" + shouldConnect := (score >= constants.SimilarToThreshold && score < constants.SimilarToUpperBound) || + similarity.CountSharedTags(a.Content.Tags, b.Content.Tags) >= minSharedTagsForEdge + if shouldConnect { + pe := proposedEdge{Source: a.ID, Target: b.ID, Kind: "similar-to", Weight: 0.8, Score: score} + if existingEdges[similarToKey] { + result.SkippedExisting++ + } else { + result.ProposedEdges = append(result.ProposedEdges, pe) + existingEdges[similarToKey] = true + } + } + } + } + + // Create proposed edges (unless dry-run) + if !dryRun && len(result.ProposedEdges) > 0 { + for _, pe := range result.ProposedEdges { + edge := store.Edge{ + Source: pe.Source, + Target: pe.Target, + Kind: pe.Kind, + Weight: pe.Weight, + CreatedAt: now, + } + if err := graphStore.AddEdge(ctx, edge); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to add edge %s -> %s: %v\n", pe.Source, pe.Target, err) + continue + } + result.CreatedEdges++ + } + + if err := graphStore.Sync(ctx); err != nil { + return result, fmt.Errorf("failed to sync store: %w", err) + } + + // Refresh PageRank + if _, err := ranking.ComputePageRank(ctx, graphStore, ranking.DefaultPageRankConfig()); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to refresh PageRank: %v\n", err) + } + } + + // Compute connectivity + result.Connectivity = computeConnectivity(ctx, graphStore, behaviors) + + return result, nil +} + +// clearDerivedEdges removes all similar-to and overrides outbound edges for behaviors. +// Returns the number of edges removed. Logs warnings on individual failures but +// continues clearing remaining edges. +func clearDerivedEdges(ctx context.Context, graphStore store.GraphStore, behaviors []models.Behavior) int { + cleared := 0 + for _, b := range behaviors { + for _, kind := range []string{"similar-to", "overrides"} { + edges, err := graphStore.GetEdges(ctx, b.ID, store.DirectionOutbound, kind) + if err != nil { + continue + } + for _, e := range edges { + if err := graphStore.RemoveEdge(ctx, e.Source, e.Target, e.Kind); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to remove edge %s -> %s (%s): %v\n", e.Source, e.Target, e.Kind, err) + continue + } + cleared++ + } + } + } + return cleared +} + +// computeConnectivity counts how many behaviors have edges vs. are isolated islands. +func computeConnectivity(ctx context.Context, graphStore store.GraphStore, behaviors []models.Behavior) connectivityInfo { + info := connectivityInfo{TotalNodes: len(behaviors)} + + for _, b := range behaviors { + hasEdge := false + // Check outbound edges + outEdges, err := graphStore.GetEdges(ctx, b.ID, store.DirectionOutbound, "") + if err == nil && len(outEdges) > 0 { + hasEdge = true + } + // Check inbound edges + if !hasEdge { + inEdges, err := graphStore.GetEdges(ctx, b.ID, store.DirectionInbound, "") + if err == nil && len(inEdges) > 0 { + hasEdge = true + } + } + if hasEdge { + info.Connected++ + } else { + info.Islands++ + } + } + + return info +} + +func printDeriveResult(r deriveStoreResult, dryRun bool) { + if dryRun { + fmt.Printf("\n=== %s store (dry run) ===\n", r.Scope) + } else { + fmt.Printf("\n=== %s store ===\n", r.Scope) + } + fmt.Printf("Behaviors: %d\n", r.Behaviors) + + if r.ClearedEdges > 0 { + fmt.Printf("Cleared edges: %d\n", r.ClearedEdges) + } + + // Score histogram + fmt.Println("\nScore distribution:") + bucketLabels := []string{ + "[0.0-0.1)", "[0.1-0.2)", "[0.2-0.3)", "[0.3-0.4)", "[0.4-0.5)", + "[0.5-0.6)", "[0.6-0.7)", "[0.7-0.8)", "[0.8-0.9)", "[0.9-1.0]", + } + for i, count := range r.Histogram { + if count > 0 { + bar := "" + for range count { + if len(bar) < 50 { + bar += "#" + } + } + fmt.Printf(" %s %s (%d)\n", bucketLabels[i], bar, count) + } + } + + // Edge proposals + fmt.Printf("\nProposed edges: %d\n", len(r.ProposedEdges)) + fmt.Printf("Skipped (already exist): %d\n", r.SkippedExisting) + + if !dryRun { + fmt.Printf("Created edges: %d\n", r.CreatedEdges) + } + + // Connectivity + fmt.Printf("\nConnectivity:\n") + fmt.Printf(" Total nodes: %d\n", r.Connectivity.TotalNodes) + fmt.Printf(" Connected: %d\n", r.Connectivity.Connected) + fmt.Printf(" Islands (0 edges): %d\n", r.Connectivity.Islands) +} diff --git a/cmd/floop/cmd_derive_edges_test.go b/cmd/floop/cmd_derive_edges_test.go new file mode 100644 index 0000000..f692ea3 --- /dev/null +++ b/cmd/floop/cmd_derive_edges_test.go @@ -0,0 +1,444 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/nvandessel/feedback-loop/internal/models" + "github.com/nvandessel/feedback-loop/internal/store" +) + +func TestNewDeriveEdgesCmd(t *testing.T) { + cmd := newDeriveEdgesCmd() + + if cmd.Use != "derive-edges" { + t.Errorf("Use = %q, want derive-edges", cmd.Use) + } + + // Verify flags exist + for _, flag := range []string{"dry-run", "clear", "scope"} { + if cmd.Flags().Lookup(flag) == nil { + t.Errorf("missing --%s flag", flag) + } + } + + // Verify default scope + scope, _ := cmd.Flags().GetString("scope") + if scope != "both" { + t.Errorf("default scope = %q, want both", scope) + } +} + +func TestDeriveEdgesProposals(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + // Weighted scoring: when(0.4) + content(0.6) + tags(0.2), normalized by + // sum of present weights. All three present → totalWeight = 1.2. + // + // b-general-go and b-go-related: + // when: both {"language":"go"} → overlap = 1.0 + // content: partial word overlap → ~0.5 + // tags: Jaccard(["go","errors"], ["go","api"]) = 1/3 = 0.33 + // score ≈ 1.0*0.333 + 0.5*0.5 + 0.33*0.167 ≈ 0.64 → in [0.5, 0.9) ✓ + // + // b-specific-go-test overrides b-general-go (superset when): + // when={language:go, task:testing} ⊃ {language:go} + behaviors := []models.Behavior{ + { + ID: "b-general-go", + Name: "Go error conventions", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "use error wrapping with fmt context propagation", + Tags: []string{"go", "errors"}, + }, + Confidence: 0.8, + }, + { + ID: "b-specific-go-test", + Name: "Go test conventions", + When: map[string]interface{}{"language": "go", "task": "testing"}, + Content: models.BehaviorContent{ + Canonical: "use table driven tests with subtests and parallel execution", + Tags: []string{"go", "testing"}, + }, + Confidence: 0.8, + }, + { + ID: "b-python", + Name: "Python conventions", + When: map[string]interface{}{"language": "python"}, + Content: models.BehaviorContent{ + Canonical: "use type hints and dataclasses for data models", + Tags: []string{"python", "typing"}, + }, + Confidence: 0.8, + }, + { + ID: "b-go-related", + Name: "Go error API patterns", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "use error wrapping and custom error types for API context", + Tags: []string{"go", "api"}, + }, + Confidence: 0.8, + }, + } + + for _, b := range behaviors { + node := models.BehaviorToNode(&b) + _, err := s.AddNode(ctx, node) + if err != nil { + t.Fatalf("failed to add node %s: %v", b.ID, err) + } + } + + // Run derive-edges in dry-run mode + result, err := deriveEdgesForStore(ctx, s, "test", true, false) + if err != nil { + t.Fatalf("deriveEdgesForStore failed: %v", err) + } + + if result.Behaviors != 4 { + t.Errorf("behaviors = %d, want 4", result.Behaviors) + } + + // b-specific-go-test has when={language:go, task:testing} which is a + // strict superset of b-general-go's when={language:go}, so overrides. + foundOverrides := false + for _, pe := range result.ProposedEdges { + if pe.Source == "b-specific-go-test" && pe.Target == "b-general-go" && pe.Kind == "overrides" { + foundOverrides = true + if pe.Weight != 1.0 { + t.Errorf("overrides weight = %v, want 1.0", pe.Weight) + } + } + } + if !foundOverrides { + t.Error("expected overrides edge from b-specific-go-test -> b-general-go") + for _, pe := range result.ProposedEdges { + t.Logf("proposed: %s -> %s (%s, score=%.4f)", pe.Source, pe.Target, pe.Kind, pe.Score) + } + } + + // b-general-go and b-go-related have same when, partial content/tag overlap. + // Score should be in [0.5, 0.9) → similar-to edge. + foundSimilarTo := false + for _, pe := range result.ProposedEdges { + if pe.Kind == "similar-to" && + ((pe.Source == "b-general-go" && pe.Target == "b-go-related") || + (pe.Source == "b-go-related" && pe.Target == "b-general-go")) { + foundSimilarTo = true + } + } + if !foundSimilarTo { + t.Error("expected similar-to edge between b-general-go and b-go-related") + for _, pe := range result.ProposedEdges { + t.Logf("proposed: %s -> %s (%s, score=%.4f)", pe.Source, pe.Target, pe.Kind, pe.Score) + } + t.Logf("histogram: %v", result.Histogram) + } + + // Python behavior should NOT have similar-to edges to Go behaviors (different language) + for _, pe := range result.ProposedEdges { + if pe.Kind == "similar-to" && + (pe.Source == "b-python" || pe.Target == "b-python") { + t.Errorf("unexpected similar-to edge involving python behavior: %+v", pe) + } + } + + // Verify no edges were created (dry-run) + if result.CreatedEdges != 0 { + t.Errorf("created edges in dry-run = %d, want 0", result.CreatedEdges) + } +} + +func TestDeriveEdgesSkipsExisting(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + // Two behaviors with same when and partial content overlap → score in [0.5, 0.9) + behaviors := []models.Behavior{ + { + ID: "b-a", + Name: "Behavior A", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "use error wrapping with fmt context propagation", + Tags: []string{"go", "errors"}, + }, + Confidence: 0.8, + }, + { + ID: "b-b", + Name: "Behavior B", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "use error wrapping and custom error types for API context", + Tags: []string{"go", "api"}, + }, + Confidence: 0.8, + }, + } + + now := time.Now() + for _, b := range behaviors { + node := models.BehaviorToNode(&b) + s.AddNode(ctx, node) + } + + // Pre-create a similar-to edge + s.AddEdge(ctx, store.Edge{ + Source: "b-a", + Target: "b-b", + Kind: "similar-to", + Weight: 0.8, + CreatedAt: now, + }) + + // Run derive — should skip the existing edge + result, err := deriveEdgesForStore(ctx, s, "test", true, false) + if err != nil { + t.Fatalf("deriveEdgesForStore failed: %v", err) + } + + // The similar-to edge should be skipped (already exists) + for _, pe := range result.ProposedEdges { + if pe.Source == "b-a" && pe.Target == "b-b" && pe.Kind == "similar-to" { + t.Error("expected similar-to edge b-a -> b-b to be skipped (already exists)") + } + } + if result.SkippedExisting == 0 { + t.Error("expected at least one skipped edge") + t.Logf("histogram: %v", result.Histogram) + for _, pe := range result.ProposedEdges { + t.Logf("proposed: %s -> %s (%s, score=%.4f)", pe.Source, pe.Target, pe.Kind, pe.Score) + } + } +} + +func TestDeriveEdgesConnectivity(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + now := time.Now() + + // Create 3 behaviors + for _, id := range []string{"b-1", "b-2", "b-3"} { + b := models.Behavior{ + ID: id, + Name: id, + Content: models.BehaviorContent{ + Canonical: "unique content for " + id, + }, + Confidence: 0.8, + } + node := models.BehaviorToNode(&b) + s.AddNode(ctx, node) + } + + // Add edge between b-1 and b-2 only + s.AddEdge(ctx, store.Edge{ + Source: "b-1", + Target: "b-2", + Kind: "similar-to", + Weight: 0.8, + CreatedAt: now, + }) + + behaviors := []models.Behavior{ + {ID: "b-1"}, + {ID: "b-2"}, + {ID: "b-3"}, + } + + info := computeConnectivity(ctx, s, behaviors) + + if info.TotalNodes != 3 { + t.Errorf("total nodes = %d, want 3", info.TotalNodes) + } + if info.Connected != 2 { + t.Errorf("connected = %d, want 2 (b-1 and b-2 have edges)", info.Connected) + } + if info.Islands != 1 { + t.Errorf("islands = %d, want 1 (b-3 has no edges)", info.Islands) + } +} + +func TestDeriveEdgesHistogram(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + // Create identical behaviors → score = 1.0 → bucket 9 + behaviors := []models.Behavior{ + { + ID: "b-identical-1", + Name: "Identical A", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "identical content here", + Tags: []string{"go"}, + }, + Confidence: 0.8, + }, + { + ID: "b-identical-2", + Name: "Identical B", + When: map[string]interface{}{"language": "go"}, + Content: models.BehaviorContent{ + Canonical: "identical content here", + Tags: []string{"go"}, + }, + Confidence: 0.8, + }, + } + + for _, b := range behaviors { + node := models.BehaviorToNode(&b) + s.AddNode(ctx, node) + } + + result, err := deriveEdgesForStore(ctx, s, "test", true, false) + if err != nil { + t.Fatalf("deriveEdgesForStore failed: %v", err) + } + + // Identical behaviors should score 1.0, landing in bucket 9 [0.9-1.0] + if result.Histogram[9] != 1 { + t.Errorf("histogram[9] = %d, want 1 (identical pair)", result.Histogram[9]) + t.Logf("full histogram: %v", result.Histogram) + } + + // Score >= 0.9 means NO similar-to edge (above upper bound) + for _, pe := range result.ProposedEdges { + if pe.Kind == "similar-to" { + t.Errorf("unexpected similar-to edge for identical behaviors (score >= 0.9): %+v", pe) + } + } +} + +func TestDeriveEdgesClear(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + now := time.Now() + + // Create two behaviors with a pre-existing edge + for _, id := range []string{"b-x", "b-y"} { + b := models.Behavior{ + ID: id, + Name: id, + Content: models.BehaviorContent{ + Canonical: "unique content for " + id, + }, + Confidence: 0.8, + } + node := models.BehaviorToNode(&b) + s.AddNode(ctx, node) + } + + s.AddEdge(ctx, store.Edge{ + Source: "b-x", Target: "b-y", Kind: "similar-to", + Weight: 0.8, CreatedAt: now, + }) + s.AddEdge(ctx, store.Edge{ + Source: "b-x", Target: "b-y", Kind: "requires", + Weight: 0.5, CreatedAt: now, + }) + + // Run with --clear (not dry-run) + result, err := deriveEdgesForStore(ctx, s, "test", false, true) + if err != nil { + t.Fatalf("deriveEdgesForStore failed: %v", err) + } + + // Should have cleared the similar-to edge but NOT the requires edge + if result.ClearedEdges != 1 { + t.Errorf("cleared edges = %d, want 1 (only similar-to)", result.ClearedEdges) + } + + // Verify requires edge still exists + edges, _ := s.GetEdges(ctx, "b-x", store.DirectionOutbound, "requires") + if len(edges) != 1 { + t.Errorf("requires edges remaining = %d, want 1", len(edges)) + } +} + +func TestDeriveEdgesTagOverlap(t *testing.T) { + ctx := context.Background() + s := store.NewInMemoryGraphStore() + + // Two behaviors with completely different content but 2 shared tags. + // Overall similarity score will be low (~0.1), but the tag-overlap rule + // should still create a similar-to edge because they share "git" and "worktree". + behaviors := []models.Behavior{ + { + ID: "b-git-basics", + Name: "Git branching basics", + When: map[string]interface{}{}, + Content: models.BehaviorContent{ + Canonical: "always create feature branches for new work", + Tags: []string{"git", "worktree", "branching"}, + }, + Confidence: 0.8, + }, + { + ID: "b-worktree-cleanup", + Name: "Worktree cleanup", + When: map[string]interface{}{}, + Content: models.BehaviorContent{ + Canonical: "remove stale worktrees after merging pull requests", + Tags: []string{"git", "worktree", "cleanup"}, + }, + Confidence: 0.8, + }, + { + ID: "b-python-typing", + Name: "Python typing", + When: map[string]interface{}{}, + Content: models.BehaviorContent{ + Canonical: "use type hints for function signatures", + Tags: []string{"python", "typing"}, + }, + Confidence: 0.8, + }, + } + + for _, b := range behaviors { + node := models.BehaviorToNode(&b) + s.AddNode(ctx, node) + } + + result, err := deriveEdgesForStore(ctx, s, "test", true, false) + if err != nil { + t.Fatalf("deriveEdgesForStore failed: %v", err) + } + + // git-basics and worktree-cleanup share 2 tags ("git", "worktree") + // → should get a similar-to edge even with low content similarity + foundTagEdge := false + for _, pe := range result.ProposedEdges { + if pe.Kind == "similar-to" && + ((pe.Source == "b-git-basics" && pe.Target == "b-worktree-cleanup") || + (pe.Source == "b-worktree-cleanup" && pe.Target == "b-git-basics")) { + foundTagEdge = true + } + } + if !foundTagEdge { + t.Error("expected similar-to edge between b-git-basics and b-worktree-cleanup (share 2+ tags)") + for _, pe := range result.ProposedEdges { + t.Logf("proposed: %s -> %s (%s, score=%.4f)", pe.Source, pe.Target, pe.Kind, pe.Score) + } + } + + // python-typing shares 0 tags with git behaviors → no edge + for _, pe := range result.ProposedEdges { + if pe.Kind == "similar-to" && + (pe.Source == "b-python-typing" || pe.Target == "b-python-typing") { + t.Errorf("unexpected similar-to edge involving python-typing: %+v", pe) + } + } +} diff --git a/cmd/floop/main.go b/cmd/floop/main.go index ac80540..3c6d1b4 100644 --- a/cmd/floop/main.go +++ b/cmd/floop/main.go @@ -111,6 +111,7 @@ context-aware behavior activation for consistent agent operation.`, newActivateCmd(), // Graph management commands newConnectCmd(), + newDeriveEdgesCmd(), // Backup/restore commands newBackupCmd(), newRestoreFromBackupCmd(), diff --git a/internal/learning/place.go b/internal/learning/place.go index de09dea..649100c 100644 --- a/internal/learning/place.go +++ b/internal/learning/place.go @@ -293,29 +293,7 @@ func (p *graphPlacer) determineEdges(ctx context.Context, behavior *models.Behav } // isMoreSpecific returns true if a has all of b's conditions plus additional ones. +// Delegates to the public similarity.IsMoreSpecific for reuse by other packages. func (p *graphPlacer) isMoreSpecific(a, b map[string]interface{}) bool { - // a is more specific than b if: - // 1. a has more conditions than b - // 2. a includes all of b's conditions with the same values - if len(a) <= len(b) { - return false - } - - // Empty when means "unscoped" (applies everywhere), not "less specific". - // A scoped behavior is not a specialization of an unscoped one. - if len(b) == 0 { - return false - } - - for key, valueB := range b { - valueA, exists := a[key] - if !exists { - return false - } - if !similarity.ValuesEqual(valueA, valueB) { - return false - } - } - - return true + return similarity.IsMoreSpecific(a, b) } diff --git a/internal/similarity/similarity.go b/internal/similarity/similarity.go index 7b0654b..799f72c 100644 --- a/internal/similarity/similarity.go +++ b/internal/similarity/similarity.go @@ -42,6 +42,24 @@ func ComputeWhenOverlap(a, b map[string]interface{}) float64 { return float64(matches) / float64(total) } +// CountSharedTags returns the number of tags that appear in both slices. +func CountSharedTags(a, b []string) int { + if len(a) == 0 || len(b) == 0 { + return 0 + } + set := make(map[string]bool, len(a)) + for _, t := range a { + set[t] = true + } + count := 0 + for _, t := range b { + if set[t] { + count++ + } + } + return count +} + // ComputeTagSimilarity computes tag Jaccard similarity with a -1.0 sentinel // for missing signals. Returns -1.0 when either slice is empty or nil, // indicating that the tag signal is absent and its weight should be diff --git a/internal/similarity/similarity_test.go b/internal/similarity/similarity_test.go index b064b4c..f52dac9 100644 --- a/internal/similarity/similarity_test.go +++ b/internal/similarity/similarity_test.go @@ -392,6 +392,33 @@ func TestComputeTagSimilarity(t *testing.T) { } } +func TestCountSharedTags(t *testing.T) { + tests := []struct { + name string + a []string + b []string + want int + }{ + {"both nil", nil, nil, 0}, + {"both empty", []string{}, []string{}, 0}, + {"a empty", []string{}, []string{"go"}, 0}, + {"b empty", []string{"go"}, []string{}, 0}, + {"no overlap", []string{"go", "cli"}, []string{"python", "web"}, 0}, + {"one shared", []string{"go", "cli"}, []string{"go", "web"}, 1}, + {"two shared", []string{"go", "cli", "errors"}, []string{"go", "errors", "web"}, 2}, + {"identical", []string{"go", "cli"}, []string{"go", "cli"}, 2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CountSharedTags(tt.a, tt.b) + if got != tt.want { + t.Errorf("CountSharedTags() = %d, want %d", got, tt.want) + } + }) + } +} + func TestValuesEqual(t *testing.T) { tests := []struct { name string diff --git a/internal/similarity/specificity.go b/internal/similarity/specificity.go new file mode 100644 index 0000000..0b96fec --- /dev/null +++ b/internal/similarity/specificity.go @@ -0,0 +1,32 @@ +package similarity + +// IsMoreSpecific returns true if a has all of b's conditions plus additional ones. +// a is more specific than b if: +// 1. a has more conditions than b +// 2. a includes all of b's conditions with the same values +// +// Empty b means "unscoped" (applies everywhere); a scoped behavior is not +// a specialization of an unscoped one, so this returns false. +func IsMoreSpecific(a, b map[string]interface{}) bool { + if len(a) <= len(b) { + return false + } + + // Empty when means "unscoped" (applies everywhere), not "less specific". + // A scoped behavior is not a specialization of an unscoped one. + if len(b) == 0 { + return false + } + + for key, valueB := range b { + valueA, exists := a[key] + if !exists { + return false + } + if !ValuesEqual(valueA, valueB) { + return false + } + } + + return true +} diff --git a/internal/similarity/specificity_test.go b/internal/similarity/specificity_test.go new file mode 100644 index 0000000..89bbee4 --- /dev/null +++ b/internal/similarity/specificity_test.go @@ -0,0 +1,82 @@ +package similarity + +import "testing" + +func TestIsMoreSpecific(t *testing.T) { + tests := []struct { + name string + a map[string]interface{} + b map[string]interface{} + want bool + }{ + { + name: "superset returns true", + a: map[string]interface{}{"language": "go", "task": "testing"}, + b: map[string]interface{}{"language": "go"}, + want: true, + }, + { + name: "equal maps returns false", + a: map[string]interface{}{"language": "go"}, + b: map[string]interface{}{"language": "go"}, + want: false, + }, + { + name: "subset returns false", + a: map[string]interface{}{"language": "go"}, + b: map[string]interface{}{"language": "go", "task": "testing"}, + want: false, + }, + { + name: "empty b returns false", + a: map[string]interface{}{"language": "go"}, + b: map[string]interface{}{}, + want: false, + }, + { + name: "both empty returns false", + a: map[string]interface{}{}, + b: map[string]interface{}{}, + want: false, + }, + { + name: "value mismatch returns false", + a: map[string]interface{}{"language": "python", "task": "testing"}, + b: map[string]interface{}{"language": "go"}, + want: false, + }, + { + name: "a has extra key but missing b key returns false", + a: map[string]interface{}{"task": "testing", "file_path": "*.go"}, + b: map[string]interface{}{"language": "go"}, + want: false, + }, + { + name: "nil b returns false", + a: map[string]interface{}{"language": "go"}, + b: nil, + want: false, + }, + { + name: "nil a returns false", + a: nil, + b: map[string]interface{}{"language": "go"}, + want: false, + }, + { + name: "both nil returns false", + a: nil, + b: nil, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsMoreSpecific(tt.a, tt.b) + if got != tt.want { + t.Errorf("IsMoreSpecific() = %v, want %v", got, tt.want) + } + }) + } +}