Skip to content

feat(link-canopy): bidirectional fleet sync — Daintree presets ↔ nForma slots (#138)#142

Merged
jobordu merged 13 commits intomainfrom
feature/issue-138-bidirectional-fleet-sync
May 1, 2026
Merged

feat(link-canopy): bidirectional fleet sync — Daintree presets ↔ nForma slots (#138)#142
jobordu merged 13 commits intomainfrom
feature/issue-138-bidirectional-fleet-sync

Conversation

@jobordu
Copy link
Copy Markdown

@jobordu jobordu commented May 1, 2026

Closes #138.

Summary

/nf:link-canopy now bidirectionally syncs between a Daintree (formerly Canopy) IDE install and nForma's quorum slots:

  • Import: every Daintree preset becomes its own nForma quorum slot, cloned from the vanilla provider for that agent and overlaid with the preset's allowlisted env.
  • Export: nForma's quorum slots are written into Daintree's per-agent customPresets[] arrays so the user can launch them from Daintree's preset palette, with provider-specific brand colors.

Example: with two Daintree claude presets (Z.AI, MiniMax), the import produces:

claude-1          (vanilla, untouched)
claude-z-ai       (cloned from claude-1 + Z.AI env)
claude-minimax    (cloned from claude-1 + MiniMax env)

Why the fan-out design

The original implementation in this branch used "active-preset-only merge" — it took whichever Daintree preset was selected and applied its env to every nForma provider with mainTool === agentName. Live testing surfaced two problems:

  1. Schema mismatch. The code targeted agentSettings.customPresets (object map) but Daintree v20 stores them at agentSettings.agents.<agent>.customPresets[] (per-agent arrays). The discovery banner always reported 0 customPresets regardless of what was configured. Same for globalEnv (real key: globalEnvironmentVariables, top-level) and providerTemplates (does not exist in v20).

  2. Wrong fan-out target. With the schema fixed, the active-preset import gave Z.AI's ANTHROPIC_AUTH_TOKEN to every mainTool=claude provider — including the ccr-* Together.xyz-routed slots, which expect TOGETHER_API_KEY. Verified by running it once and rolling back via the timestamped backup.

The fan-out design solves both: each preset gets its own dedicated slot, family gate (provider.mainTool === agentName AND provider.provider === inferredFamily(preset.env)) ensures Z.AI doesn't leak into Together-routed slots, and the user can switch between presets by picking which slot to invoke.

Slot naming

  • Format: {agentName}-{slug(preset.name)} — e.g. claude-z-ai, claude-minimax.
  • Slugifier: lowercase → non-[a-z0-9]- → collapse → trim. Matches nForma's canonical slot regex ^[a-z][a-z0-9-]*$.
  • Collisions: -2, -3 suffix.

What gets written per new slot

File Change
~/.claude/nf/bin/providers.json Append new provider entry, cloned from the vanilla, with daintree_preset_id for re-import idempotency.
~/.claude.json mcpServers Clone vanilla's MCP entry with PROVIDER_SLOT + UNIFIED_PROVIDERS_CONFIG pointing at the providers.json we wrote (otherwise unified-mcp-server.mjs reads the repo source by default and exits Unknown PROVIDER_SLOT).
~/.claude/nf.json quorum_active Append slot name.

All three files are backed up with a timestamped suffix before any change.

Idempotency (AC5)

  • Existing slot with matching daintree_preset_id → updated in place, name stable, env replaced.
  • Preset that Daintree no longer has → its slot is preserved by default (option b: explicit user removal only). Avoids destroying slots the user may have wired into prompts/scripts.

Allowlist (AC5 safety guardrail)

Both preset env and globalEnvironmentVariables keys are filtered through:

^(ANTHROPIC_|OPENAI_|GOOGLE_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|MODEL$|.*_BASE_URL$|.*_API_KEY$)

Generic free-form keys (e.g. API_TIMEOUT_MS, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) are blocked.

Onboard.md

Detection counts customPresets across ALL agent buckets in the v20 schema, not the (non-existent) flat agentSettings.customPresets object.

Test plan

  • Run discovery against live Daintree v20 install — banner reports 2 presets across 1 agent (claude: Z.AI, MiniMax)
  • Run fan-out import — produces claude-z-ai and claude-minimax slots
  • Verify all 7 resulting slots respond to MCP initialize handshake (~95-220ms each)
  • Probe wrote ~/.claude/nf/slot-health.json correctly via the new statusline probe (feat(statusline): show quorum-slot responsiveness #141)
  • Verify backups created for providers.json, ~/.claude.json, ~/.claude/nf.json before any change
  • All 5 Node heredoc blocks in link-canopy.md syntax-check clean
  • Step ordering preserved: 1 → 2a → 2b → 2c → 2d → 3a → 3b → 3c → 3d → 3e → 4
  • Re-run idempotency check on a second invocation — daintree_preset_id lookup matches existing entries (defer until re-test on user's machine)

Backwards compatibility

canopy-app legacy paths preserved as fallback in Step 1, banner labels, mcp.json fallback, plugins dir fallback, and mcpServers.canopy fallback key. Legacy installs without Daintree but with canopy-app paths still detect successfully.

Related issues / follow-ups (not in this PR)

  1. install.js clobbers user-customized providers.json — observed during testing: running install.js --claude --global overwrote the fan-out additions with the repo's vanilla providers list. User-specific entries should be preserved on install.
  2. pr-merge-autopilot.sh bash bugset -e + ((var++)) exits silently on the first pending check. Patched locally to land this PR; needs its own fix-PR.
  3. CCR slots removal from repo bin/providers.json — user instruction during testing said they aren't supposed to exist anymore. Live install was cleaned but the repo source still has them.

Summary by CodeRabbit

  • New Features

    • Daintree-first IDE detection added to onboarding; legacy Canopy remains supported.
    • /nf:link-canopy fans out each Daintree custom preset into its own provider slot and exports per-agent custom presets back to Daintree.
    • Exported presets copy model/base URLs, replace API keys with placeholders, and assign provider colors.
  • Bug Fixes / Reliability

    • Idempotent, non-destructive syncs that preserve vanilla slots and existing presets; clearer Daintree-specific status messages.
  • Documentation

    • Onboarding dashboard and help text updated with detection results, sync guidance, and idempotency notes.

jobordu and others added 7 commits May 1, 2026 10:23
…back

- Add resolvePath() helper for platform-specific Daintree config paths (macOS/Win/Linux)
- Detect Daintree first, fall back to canopy-app paths for backwards compatibility
- Surface productName, customPresets, globalEnv, providerTemplates in CANOPY_INFO
- Update NOT-FOUND banner to mention both Daintree and legacy canopy-app paths
- Update DISCOVERED banner with Product line and three new sections (custom presets, global env keys, provider templates)
- mcp.json and plugins dir also try ~/.daintree/ first, fall back to ~/.canopy/
- mcpServers lookup tries 'daintree' key first, then legacy 'canopy'

Issue 138 AC1: Discovery banner shows customPresets, globalEnv, providerTemplates
…ets export with brand colors

Step 2d - Import preset env into providers.json (issue 138 AC2 + AC5):
- Match Daintree presets to providers.json entries via exact/prefix/model heuristic
- Idempotent merge: never overwrites non-empty existing env values
- globalEnv allowlist: ^(ANTHROPIC_|OPENAI_|GOOGLE_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|MODEL$|.*_BASE_URL$|.*_API_KEY$) covers all BRAND_COLORS providers
- Backup providers.json before write at every candidate path
- Pass CANOPY_INFO and SELECTED_MATCHES_JSON via env vars (no interpolation)

Step 3e - Export quorum slots as Daintree customPresets (issue 138 AC3 + AC4 + AC5):
- Implemented inside Step 3d atomic write to avoid double-backup
- BRAND_COLORS map: openai, google, anthropic, github, xai, opencode, together(.xyz), openrouter, deepseek, ollama; default #6366f1
- Derives env from providers.json: MODEL, ANTHROPIC_BASE_URL, *_API_KEY as ${VAR} placeholders
- Existing customPresets show 'unchanged' status - never overwritten by default
- Result block distinguishes userAgentRegistry vs customPresets entries (two columns)

Closing summary updated with: Product detected, Preset env imported, customPresets added/unchanged counts.
success_criteria section updated to cover all 5 issue 138 acceptance criteria.
…py-app fallback

- Step 1 NF_DETECT script: add Daintree config detection block before project state, populates result.daintree = { installed, product, config_path, custom_preset_count } with try/catch around config read so unreadable configs do not throw
- Step 2 dashboard: insert 'Daintree IDE' section between 'MCP Servers' and 'Project'
- Step 3 section C: add bidirectional bridge mention - if Daintree installed AND nForma installed, recommend /nf:link-canopy with current custom_preset_count from detection result

All edits are additive; zero deletions of existing CLI/MCP detection logic. Bullet idempotency note clarifies safe re-runs (issue 138 AC5).
Live-testing the previous implementation against an actual Daintree
v20 install (~/Library/Application Support/Daintree/config.json,
_schemaVersion=20) surfaced a schema mismatch and a design issue:

1. Schema correction
   - customPresets is a per-agent ARRAY at agentSettings.agents.<a>.customPresets[]
     (not an object map at agentSettings.customPresets)
   - globalEnv → globalEnvironmentVariables, lives at config root
     (not under agentSettings)
   - providerTemplates does not exist in v20 — removed all references
   - Step 1 reader now aggregates per-agent arrays + reads top-level
     globalEnvironmentVariables; banner shows per-agent breakdown

2. Step 2d redesign — fan-out import
   The previous "active-preset-only merge" semantics would silently
   overwrite a vanilla provider's env from a single Daintree preset
   AND fan out to all mainTool=claude providers (including
   together-routed ccr-* slots that should NOT receive Anthropic env).
   Tested locally: this gave ccr-1..6 wrong env values and would have
   broken the quorum.

   New design: each Daintree preset becomes its OWN nForma quorum slot
   alongside the vanilla. Example with two claude presets:
     claude-1                  (vanilla, untouched)
     claude-z-ai               (clone of claude-1 + Z.AI env)
     claude-minimax            (clone of claude-1 + MiniMax env)

   Slot name = {agentName}-{slug(preset.name)} — e.g. "claude-z-ai".
   Slug rule: lowercase, non-[a-z0-9] → "-", collapse, trim.
   Family gate: provider.mainTool === agentName AND
   provider.provider === inferredFamily(preset.env). Family inferred
   from env-key prefixes (ANTHROPIC_*, OPENAI_*, GOOGLE_/GEMINI_*,
   etc.). Falls back to mainTool-only when no family signal.

3. Step 2d writes to all 3 live files (each backed up first):
   - ~/.claude/nf/bin/providers.json — appends new provider entry per
     preset, cloning the vanilla and overlaying allowlisted preset env.
   - ~/.claude.json mcpServers — clones vanilla MCP entry with
     PROVIDER_SLOT={newName} AND UNIFIED_PROVIDERS_CONFIG pointing at
     the providers.json we wrote (otherwise unified-mcp-server.mjs
     reads the repo source by default and exits "Unknown PROVIDER_SLOT"
     for the new entries — found via runtime probe).
   - ~/.claude/nf.json quorum_active — appends the new slot name.

4. Idempotency (AC5)
   Each new provider carries daintree_preset_id = preset.id. On re-run:
   - Existing entry with same id → updated in place (env, description,
     family) but name stays stable.
   - Preset that Daintree no longer has → preserved by default
     (option b: explicit removal only). Avoids destroying slots the
     user may have wired into prompts/scripts.

5. Step 3e (export) reworked for v20
   Writes to agentSettings.agents.<targetAgent>.customPresets[] (array
   push, not object key set). targetAgent = providerEntry.mainTool,
   falling back to slot.cli, then 'claude'. Idempotency by id-in-array
   lookup. Correct entry shape: {id, name, description, env, color,
   fallbacks: [], dangerousEnabled: false}.

6. onboard.md detection updated
   Counts customPresets across ALL agent buckets (per-agent arrays)
   instead of treating agentSettings.customPresets as a flat object
   map.

Allowlist regex unchanged:
^(ANTHROPIC_|OPENAI_|GOOGLE_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|MODEL$|.*_BASE_URL$|.*_API_KEY$)

Verified end-to-end against a live Daintree install: 2 customPresets
(Z.AI, MiniMax) under the claude agent fan out cleanly to claude-z-ai
and claude-minimax slots. All 7 resulting slots respond to MCP
initialize handshake (~95-220ms each) per nf-slot-health-probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 1, 2026 16:08
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds Daintree-first discovery to /nf:link-canopy, parses per-agent customPresets[] and root globalEnvironmentVariables, fans out presets into nForma provider slots (tagged with daintree_preset_id) with idempotent updates, and exports nForma providers back into Daintree customPresets. Onboarding now detects Daintree/legacy Canopy and reports per-agent preset counts.

Changes

Cohort / File(s) Summary
Daintree-first discovery & preset sync
commands/nf/link-canopy.md
Prefers Daintree config (Daintree/config.json, mcpServers.daintree) with legacy canopy-app fallback. Parses Daintree v20 agentSettings.agents[*].customPresets[], agentCfg.presetId, and top-level globalEnvironmentVariables. Adds Step 2d to fan-out each Daintree preset into its own nForma provider slot by cloning matching vanilla provider (match on mainTool === agentName, infer family from allowlisted env prefixes), overlay allowlisted preset + global env, tag slots with daintree_preset_id (stable normalized IDs), clone/link MCP server entries (PROVIDER_SLOT) and append to quorum_active. Re-runs preserve vanilla slots and update only preset-linked slots in place; summary output is sanitized to avoid leaking secrets. Extends registration (Step 3) to also export providers into Daintree agentSettings.agents.<agent>.customPresets[] (derive env from providers.json: copy MODEL and *_BASE_URL, emit *_API_KEY as ${KEY} placeholders, set color via fixed BRAND_COLORS mapping, route target agent by providerEntry.mainTool with fallbacks), enforce no-overwrite idempotency for existing preset id (report unchanged), and update final reporting counts.
Onboarding diagnostic enhancement
onboard.md
Adds cross-platform Daintree/legacy Canopy detection (searches platform config paths for config.json), parses agentSettings.agents to count per-agent customPresets, and emits result.daintree fields (installed, product, config_path, custom_preset_count). Updates Step 2 dashboard to display detection and recommend /nf:link-canopy, and extends Step 3 guidance with a conditional Daintree bridge message describing fan-out/export flow and idempotent re-run behavior.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "CLI / nForma"
    participant Discovery as "Discovery Phase"
    participant Daintree as "Daintree Config"
    participant Import as "Import / Fan-out"
    participant Providers as "providers.json"
    participant Claude as "~/.claude.json"
    participant Registration as "Registration / Export"

    User->>CLI: Run /nf:link-canopy
    CLI->>Discovery: Detect Daintree / legacy Canopy (platform paths)
    Discovery->>Daintree: Read config.json (agentSettings, globalEnvironmentVariables)
    Daintree-->>Discovery: Return agents + customPresets
    Discovery-->>CLI: Show detection banner

    CLI->>Import: Parse presets per agent
    Import->>Import: Filter env via allowlist, infer provider family
    Import->>Import: Create/clone provider slot per preset, tag daintree_preset_id
    Import->>Providers: Write/update providers.json
    Import->>Claude: Clone/append MCP entries (~/.claude.json)
    Import-->>CLI: Report import results

    CLI->>Registration: Map providers to Daintree customPresets
    Registration->>Providers: Read providers.json entries
    Registration->>Daintree: Write per-agent customPresets (placeholders, colors)
    Registration-->>CLI: Report export results
    CLI-->>User: Final summary (imported, exported, idempotent statuses)
Loading
sequenceDiagram
    participant Daintree as "Daintree\n(agentSettings)"
    participant nForma as "nForma\n(providers.json)"
    participant MCP as "MCP Servers\n(~/.claude.json)"

    rect rgba(100,150,200,0.5)
    Note over Daintree,nForma: Import: Presets → Providers
    Daintree->>nForma: customPresets[].env per agent
    nForma->>nForma: Create/update provider slots (daintree_preset_id)
    nForma->>MCP: Register MCP entries for daintree servers
    end

    rect rgba(200,150,100,0.5)
    Note over nForma,Daintree: Export: Providers → Presets
    nForma->>nForma: Read providers.json
    nForma->>Daintree: Convert to customPresets format (placeholders, colors)
    Daintree->>Daintree: Store per-agent presets (idempotent)
    end

    rect rgba(150,200,100,0.5)
    Note over Daintree,nForma: Idempotency
    Daintree->>nForma: Check daintree_preset_id / preset id
    nForma->>nForma: Skip unchanged writes, report "unchanged"
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: bidirectional fleet sync between Daintree presets and nForma slots, which is the core objective of this PR.
Description check ✅ Passed The description covers all required template sections: Summary, Why, Testing (with platform coverage), Checklist items (GSD style, CHANGELOG, dependencies, Windows support, workflows), and Breaking Changes.
Linked Issues check ✅ Passed The PR implements all key objectives from #138: discovery of Daintree v20 presets, import via fan-out slots, export to customPresets arrays, idempotency with daintree_preset_id, and allowlist-based env filtering.
Out of Scope Changes check ✅ Passed All changes are scoped to the stated objectives: updates to link-canopy.md and onboard.md for discovery, import/export logic, and idempotency. No unrelated code modifications are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-138-bidirectional-fleet-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds bidirectional syncing between a local Daintree (legacy Canopy) install and nForma quorum slots, and surfaces Daintree detection in onboarding so users discover /nf:link-canopy.

Changes:

  • Extend /nf:link-canopy to detect Daintree-first (with canopy-app fallback), import Daintree presets into new nForma slots (fan-out), and export nForma slots back into Daintree customPresets with brand colors.
  • Update onboard.md to detect Daintree installs + count custom presets, and recommend /nf:link-canopy in the onboarding routing text.
  • Add/update planning artifacts for quick task 405 and update .planning/STATE.md.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
onboard.md Adds Daintree detection + dashboard section + onboarding routing hint for /nf:link-canopy.
commands/nf/link-canopy.md Implements Daintree-first discovery, preset fan-out import into nForma slots, and export of quorum slots as Daintree custom presets with brand colors.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-PLAN.md Captures the intended implementation plan for task 405.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md Summarizes the task outcome and manual verification checklist.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md Records verification claims/checklist for task 405.
.planning/STATE.md Resolves conflict markers and records quick task 405 entry + last activity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commands/nf/link-canopy.md
Comment thread onboard.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
commands/nf/link-canopy.md (4)

930-943: ⚡ Quick win

Add language to the Register result output example block (MD040).

markdownlint flags around Line 932. Add a text language label to avoid the MD040 warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/nf/link-canopy.md` around lines 930 - 943, The markdown example for
REGISTER_RESULT lacks a fenced code block language tag causing MD040; update the
example code fence that shows the "Agents registered in Canopy" output to use a
language label (e.g., add "text" after the opening ``` so it becomes ```text) to
satisfy markdownlint, ensuring the block that demonstrates REGISTER_RESULT
output remains unchanged except for the added language label.

616-633: ⚡ Quick win

Add language to the “Fan-out import complete” output example block (MD040).

markdownlint flags the fenced block around Line 618. Add text for the banner/output examples.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/nf/link-canopy.md` around lines 616 - 633, The fenced code block
that begins with "✓ Fan-out import complete" is missing a language tag which
triggers markdownlint MD040; update the opening fence from ``` to ```text so the
block is explicitly marked as plain text (locate the block containing the "✓
Fan-out import complete" output example in the link-canopy documentation and
change its opening fence accordingly).

366-381: ⚡ Quick win

Add language to the second allowlist regex block (MD040).

Also flagged by markdownlint at/around Line 368. Adding a language (e.g., text) should remove the warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/nf/link-canopy.md` around lines 366 - 381, Banner code block lacks a
language tag which triggers markdownlint MD040; update the fenced code block
shown in the diff (the triple-backtick block that begins the "Display the REVIEW
FAN-OUT IMPORT banner" sample) to include an explicit language identifier (e.g.,
change the opening ``` to ```text) so the second allowlist regex block warning
is suppressed; ensure only the opening fence is modified to include the language
and leave the block content unchanged.

356-361: ⚡ Quick win

Add language to the allowlist regex fenced code block (MD040).

Your markdownlint output flags the fenced block at/around Line 358 for missing a language identifier. Add text (or regex) for consistent rendering and lint cleanliness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/nf/link-canopy.md` around lines 356 - 361, The fenced code block
showing the Allowlist regex under the "Allowlist (AC5 safety guardrail)" section
is missing a language identifier; update the block that contains the regex
^(ANTHROPIC_|OPENAI_|GOOGLE_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|MODEL$|.*_BASE_URL$|.*_API_KEY$)
by adding a language tag such as text or regex immediately after the opening
triple backticks (e.g., ```text) so markdownlint MD040 is satisfied and the
block renders consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/nf/link-canopy.md`:
- Around line 856-895: The code skips adding customPresets when the computed
targetAgent (from targetAgentFor(slot, providerEntry)) has no agentBucket;
change this to attempt a safe fallback to 'claude' before skipping: after
computing targetAgent and agentBucket, if (!agentBucket) set targetAgent =
'claude' and re-resolve agentBucket = config.agentSettings.agents[targetAgent];
if that resolves, proceed to add the preset under that agentBucket using the
existing presetId logic and push a registered entry indicating status 'added
(fallback to claude)' or 'skipped (no agent: claude)' as appropriate; keep
existing idempotency/never-overwrite behavior for customPresets.
- Around line 901-909: The export currently only copies providerEntry.model into
env.MODEL and special-cases providerEntry.env.ANTHROPIC_BASE_URL into
env.ANTHROPIC_BASE_URL while only placeholdering keys that match /_API_KEY$/;
update the logic around providerEntry.env so that any key matching /_BASE_URL$/
is also copied into env (e.g., env[key] = providerEntry.env[key]) rather than
only handling ANTHROPIC_BASE_URL, keeping the existing API key placeholder
behavior and preserving MODEL; modify the block that constructs env (the const
env = {} / if (providerEntry.model) ... / if (providerEntry.env && ...) / for
(const k of Object.keys(providerEntry.env || {})) ...) to include exporting all
*_BASE_URL entries.
- Around line 517-523: The fallback slug construction for baseSlug can throw if
preset.id isn't iterable because it spreads preset.id; update the fallback to
coerce preset.id to a string before iterating so it's safe for numbers and other
non-iterables. Locate the expression that builds baseSlug (the line using
slugify(preset.name) || ('preset-' +
Math.abs([...preset.id].reduce(...)).toString(36))) and change it to use
String(preset.id) (e.g., iterate over String(preset.id) or otherwise call
String(preset.id) before the reduce) so candidate formation and subsequent
SLOT_NAME_RE checks remain safe; ensure variable names baseSlug, preset.id,
slugify, candidate, and SLOT_NAME_RE are preserved.
- Around line 542-559: The code is replacing provider.env wholesale with
item.env which contains only allowlisted keys and thus wipes out other necessary
env entries; instead, for the add branch (newProvider) start from the vanilla
env (v.env) and overlay the allowlisted keys from item.env onto that base, and
for the update branch merge item.env into existing.env (preserving existing
keys) rather than assigning it wholesale; update the logic around
newProvider.env and existing.env to perform a shallow merge of allowlisted keys
onto the base env (v.env or existing.env) so only allowlisted keys are changed
while other env entries remain intact.
- Around line 509-559: The import step populates provider.env but never updates
the provider.model from item.env.MODEL, so preset model overrides are lost;
update both the 'add' branch (where newProvider is created from v) and the
'update' branch (where existing is found) to set provider.model = item.env.MODEL
if present (falling back to the vanilla/model that was already there) so
providerEntry.model reflects the preset and preserves round-trip idempotency;
reference the plan handling for item.kind === 'add' and item.kind === 'update',
the newProvider/existing objects, and the item.env.MODEL and providerEntry.model
fields when making this change.

---

Nitpick comments:
In `@commands/nf/link-canopy.md`:
- Around line 930-943: The markdown example for REGISTER_RESULT lacks a fenced
code block language tag causing MD040; update the example code fence that shows
the "Agents registered in Canopy" output to use a language label (e.g., add
"text" after the opening ``` so it becomes ```text) to satisfy markdownlint,
ensuring the block that demonstrates REGISTER_RESULT output remains unchanged
except for the added language label.
- Around line 616-633: The fenced code block that begins with "✓ Fan-out import
complete" is missing a language tag which triggers markdownlint MD040; update
the opening fence from ``` to ```text so the block is explicitly marked as plain
text (locate the block containing the "✓ Fan-out import complete" output example
in the link-canopy documentation and change its opening fence accordingly).
- Around line 366-381: Banner code block lacks a language tag which triggers
markdownlint MD040; update the fenced code block shown in the diff (the
triple-backtick block that begins the "Display the REVIEW FAN-OUT IMPORT banner"
sample) to include an explicit language identifier (e.g., change the opening ```
to ```text) so the second allowlist regex block warning is suppressed; ensure
only the opening fence is modified to include the language and leave the block
content unchanged.
- Around line 356-361: The fenced code block showing the Allowlist regex under
the "Allowlist (AC5 safety guardrail)" section is missing a language identifier;
update the block that contains the regex
^(ANTHROPIC_|OPENAI_|GOOGLE_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|MODEL$|.*_BASE_URL$|.*_API_KEY$)
by adding a language tag such as text or regex immediately after the opening
triple backticks (e.g., ```text) so markdownlint MD040 is satisfied and the
block renders consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 22dfc490-bd25-43dd-859f-4ea2d5f260d3

📥 Commits

Reviewing files that changed from the base of the PR and between 308c34e and aeeeda3.

⛔ Files ignored due to path filters (4)
  • .planning/STATE.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-PLAN.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md is excluded by !.planning/**
📒 Files selected for processing (2)
  • commands/nf/link-canopy.md
  • onboard.md

Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md Outdated
jobordu and others added 2 commits May 1, 2026 17:42
…irectional-fleet-sync

# Conflicts:
#	.planning/STATE.md
Code fixes (commands/nf/link-canopy.md):
- Step 2d add: overlay allowlisted preset env onto vanilla provider env
  instead of wholesale replacement (preserves runtime fields the user
  may have hand-added on the slot)
- Step 2d update: same overlay rule, plus refresh `description` so a
  renamed preset shows the new name (was documented but not implemented)
- Both branches now propagate `env.MODEL` to `provider.model` so Step 3d
  export round-trips correctly when a preset overrides MODEL
- baseSlug fallback: String(preset.id) coercion so non-string ids
  (numbers, etc.) don't throw during the spread
- userAgentRegistry command: treat slot.cli === 'unknown' as missing
  (Step 3a sets that literal when provider.cli is null) — prefer
  providerEntry.mainTool over the sentinel
- Step 3e targetAgentFor: same 'unknown' guard
- Step 3e: fall back to the 'claude' Daintree bucket when the resolved
  targetAgent doesn't exist in agentSettings.agents (covers wrapper /
  router slots like CCR that drive the claude CLI but route through a
  different upstream provider). Status reports
  'added (fallback to claude)' for visibility.
- Step 3e: export ALL *_BASE_URL keys verbatim, not just
  ANTHROPIC_BASE_URL. Endpoints aren't secrets and per-provider
  overrides need to round-trip back to Daintree.

Documentation fixes (commands/nf/link-canopy.md):
- Slot-naming description corrected to {agentName}-{slug} (was
  {vanillaSlotName}-{slug}, didn't match the implementation)
- Step 2d idempotency block now explicitly distinguishes vanilla
  (never touched) from preset-linked (replaced-in-place) slots, plus
  the export side's no-overwrite rule
- Closing summary text rewritten to match actual semantics — drops the
  blanket "NEVER overwritten" claim and lists the three idempotency
  rules separately
- Step 3e implementation notes updated for *_BASE_URL exports + claude
  fallback + 'unknown' cli handling
- success_criteria entry rewritten to describe fan-out, not merge

Documentation fixes (onboard.md):
- Daintree IDE bridge text now uses the v20 schema path
  (agentSettings.agents.<agent>.customPresets[]) instead of the stale
  agentSettings.customPresets reference, and clarifies the fan-out +
  per-direction idempotency rules

Planning docs (405-PLAN/SUMMARY/VERIFICATION.md):
- Each frontmatter now has an `implementation_evolution` block that
  flags the body as historical and points at the shipped design
  changes. Body kept intact for record. Verification block adds a
  runtime-verification subsection summarizing what was tested live.

Conflict resolution: merged origin/main (slot-statusline #141) into
the branch — single conflict in .planning/STATE.md (both sides added
a row 405) resolved by keeping both rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/nf/link-canopy.md`:
- Around line 103-113: The active preset check only accepts strings; change the
guard in the agent loop (where config.agentSettings.agents is iterated and
result.activePresetIdByAgent is set) to accept any non-null/undefined presetId
(e.g., use agentCfg.presetId != null) instead of typeof === 'string', then
assign agentCfg.presetId to result.activePresetIdByAgent[agentName]; ensure the
stored type remains consistent with customPresets[].id across the codebase.
- Around line 542-635: The final process.stdout.write currently includes the
full plan (variable plan) which may contain secret env values; remove plan from
the emitted object (or replace it with a redacted version) before writing to
stdout. Locate the process.stdout.write(...) call and change the payload to
exclude plan (e.g., only include written, providersPath, claudeJsonPath,
nfJsonPath, summary) or construct a sanitizedPlan that strips any env keys
matching /API_KEY$/ or other sensitive patterns and include that instead; ensure
no other references to plan are printed thereafter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c5ad57e-2937-44c2-8697-f00b9396c44f

📥 Commits

Reviewing files that changed from the base of the PR and between aeeeda3 and 017c754.

⛔ Files ignored due to path filters (4)
  • .planning/STATE.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-PLAN.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md is excluded by !.planning/**
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md is excluded by !.planning/**
📒 Files selected for processing (2)
  • commands/nf/link-canopy.md
  • onboard.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • onboard.md

Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md
- CRITICAL (security): drop `plan` from the Step 2d stdout payload.
  plan[].env carries raw preset.env values including *_API_KEY tokens
  (the allowlist passes raw values, not redactions); emitting it would
  leak secrets to terminal/logs/CI artifacts. The `summary` field
  already carries only key NAMES (envKeys arrays), no values.
- MAJOR: normalize daintree_preset_id to String() everywhere — plan
  match, lookup, and stored field. Without this, a Daintree schema
  change between runs (string id vs number id) would fail strict
  equality and produce duplicate preset-linked providers instead of
  updating in place. AC5 idempotency violation.
- MINOR: drop `typeof === 'string'` guard on agentCfg.presetId. Use
  != null so non-string presetIds (numbers, etc.) still mark the
  active preset correctly in the discovery banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 1, 2026 16:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/nf/link-canopy.md`:
- Line 364: Several fenced code blocks in the link-canopy markdown are missing
language identifiers (plain "```") which triggers markdownlint MD040; replace
those bare fences with language-tagged fences (e.g., "```text" or the
appropriate language like "```json"/"```bash") for each occurrence (the fences
currently shown as "```" at the flagged locations) so the linter recognizes the
block language and the files pass MD040. Ensure you update every similar bare
fence in the document (including the additional occurrences noted) to the
correct language identifier.
- Around line 524-525: The plan.push call currently sets vanilla: vanilla ||
existing which causes re-imports to repeatedly append the "— Daintree preset:
..." suffix because later code rebuilds existing.description from that vanilla;
fix by ensuring the vanilla used for updates is the original unsuffixed source
(e.g., clone or derive a vanillaWithoutSuffix from existing with the preset
suffix removed) or by changing the description-rebuild logic in the update path
to first strip any existing "— Daintree preset:" suffix before appending; target
the plan.push site (where vanilla is chosen) and the description-rebuild code
that mutates existing.description to prevent double-appending.
- Around line 452-456: The ALLOWLIST regex used by filterAllowlisted is missing
the GEMINI_ prefix so GEMINI_API_KEY and related keys get dropped; update the
ALLOWLIST constant (referenced by filterAllowlisted) to include GEMINI_ (e.g.,
add GEMINI_ to the alternation), then run a quick check against inferFamily
usage to confirm GEMINI_* env keys are preserved during import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c36f460-5925-402d-89c1-7193a3c29b40

📥 Commits

Reviewing files that changed from the base of the PR and between 017c754 and d28104c.

📒 Files selected for processing (1)
  • commands/nf/link-canopy.md

Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the /nf:link-canopy workflow to support Daintree (formerly Canopy) v20 by detecting Daintree installs, reading/writing v20 preset schema, and performing bidirectional sync between Daintree presets and nForma quorum slots.

Changes:

  • Add Daintree (and legacy canopy-app) install detection + preset counting to onboard.md.
  • Update /nf:link-canopy discovery to read Daintree v20 per-agent customPresets[] plus top-level globalEnvironmentVariables.
  • Add a fan-out import step that creates/updates nForma provider slots per Daintree preset and exports nForma slots back into Daintree’s per-agent customPresets[] with brand colors.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
onboard.md Adds Daintree install detection and surfaces custom preset counts in the onboarding dashboard + routing guidance.
commands/nf/link-canopy.md Implements Daintree-first path resolution, v20 schema parsing, fan-out preset→slot import, and slot→Daintree customPresets export.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md Adds verification notes and human test checklist for the bidirectional sync work.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md Records implementation decisions and historical plan deltas for task 405.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-PLAN.md Captures the original plan and documents implementation-evolution deltas.
.planning/STATE.md Adds task 405 entry for the bidirectional sync work and resolves prior conflict markers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Comment thread .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md Outdated
…ix dup

- BUG: GEMINI_ and GROK_ prefixes were matched by inferFamily but
  missing from ALLOWLIST. Presets keying only on GEMINI_*/GROK_* would
  have their env silently dropped before family inference could run on
  them. Added both to the allowlist regex (updated in code, banner
  text, and success_criteria).
- BUG: description suffix duplication on re-import. Plan stored
  `vanilla: vanilla || existing`, then update branch rebuilt
  description from item.vanilla.description — but when vanilla was
  null, item.vanilla === existing (the cloned slot, already suffixed).
  Each re-run would compound " — Daintree preset: X". Fix: pass
  vanilla as-is (may be null), and only refresh description when a
  fresh non-cloned vanilla (no daintree_preset_id) is available.
- BUG/doc-mismatch: env layering. Doc said globalEnv merges
  non-overwrite, but code did `{...vanilla.env, ...item.env}` where
  item.env was already globalEnv-overlaid-with-preset. Effective
  precedence was preset > globalEnv > vanilla — i.e. globalEnv
  overwrote vanilla. Restructured plan to carry presetEnv and
  globalEnv separately; apply now composes as
  `{...globalEnv, ...vanilla.env, ...presetEnv}` so the documented
  precedence (vanilla wins over globalEnv, preset wins over both)
  actually holds. Same fix on the update path.
- Stale comment: activePresetIdByAgent description now correctly
  notes Step 2d uses fan-out (every preset → new slot), so the
  active-preset id is informational/banner-only, not an import gate.
- Stale verification doc: human_verification.expected entries
  rewritten to describe the fan-out + 3-layer env composition, with
  the actual idempotency rules (vanilla untouched, preset-linked
  in-place by daintree_preset_id String() match, description-suffix
  guard via vanilla check).

Acknowledged but deferred: markdown lint MD040 (fenced blocks without
language identifiers) — pure lint, will land in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
commands/nf/link-canopy.md (1)

364-400: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix markdownlint MD040: add language identifiers to fenced code blocks (allowlist + “REVIEW FAN-OUT IMPORT” banner).

Several fenced blocks are started with bare ``` which triggers MD040. Add a language (e.g., text) to the opening fence(s).

🛠️ Proposed patch
-```
+```text
 ^(ANTHROPIC_|OPENAI_|GOOGLE_|GEMINI_|TOGETHER_|DEEPSEEK_|OLLAMA_|OPENROUTER_|XAI_|GROK_|MODEL$|.*_BASE_URL$|.*_API_KEY$)

@@
- +text
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
nForma ► REVIEW FAN-OUT IMPORT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@
Each new slot also gets:

  • cloned MCP server entry in ~/.claude.json (PROVIDER_SLOT={newName})
  • appended to quorum_active in ~/.claude/nf.json
  • daintree_preset_id field for re-import idempotency
    @@
    Backups (timestamped) are written for all three files before any change.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/nf/link-canopy.md` around lines 364 - 400, The markdown has fenced
code blocks without language identifiers causing MD040; update the two blocks
containing the allowlist regex (the block starting with ^(ANTHROPIC_|OPENAI_...
) and the "REVIEW FAN-OUT IMPORT" banner block that begins with the long line of
━ characters and the text "nForma ► REVIEW FAN-OUT IMPORT") to use a language
tag (e.g., change ``` to ```text) on their opening fences so markdownlint MD040
is satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/nf/link-canopy.md`:
- Around line 667-684: Change the opening code fence for the "Fan-out import
complete" rendered output block from an unannotated ``` to a language-tagged
fence (```text) so markdownlint MD040 is satisfied; locate the banner block
containing the line "✓ Fan-out import complete" and update its opening triple
backticks to ```text while leaving the block contents and closing fence
unchanged.
- Around line 999-1012: The fenced code block that begins with "✓ Agents
registered in Canopy" is missing a language identifier which triggers
markdownlint MD040; update the opening fence from ``` to ```text so the block is
declared as plain text (look for the code block containing the sample two-column
output and the line "⚠ Restart Daintree (or canopy-app) for changes to take
effect." and add the language identifier to the opening backticks).
- Around line 196-205: The active preset matching uses strict === between
preset.id and activeId which can fail if types differ; update the comparisons in
the Step 1 discovery banner so both sides are coerced to strings before
comparing (e.g., when computing presets.find(p => ...?.name) and when setting
marker = ... ? "★" : " "), using the existing symbols activePresetIdByAgent,
customPresets (or presets), preset.id, activeId, and the marker assignment so
the displayed active name and star correctly reflect matches regardless of id
type.

---

Duplicate comments:
In `@commands/nf/link-canopy.md`:
- Around line 364-400: The markdown has fenced code blocks without language
identifiers causing MD040; update the two blocks containing the allowlist regex
(the block starting with ^(ANTHROPIC_|OPENAI_... ) and the "REVIEW FAN-OUT
IMPORT" banner block that begins with the long line of ━ characters and the text
"nForma ► REVIEW FAN-OUT IMPORT") to use a language tag (e.g., change ``` to
```text) on their opening fences so markdownlint MD040 is satisfied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7817950d-cf2a-4c19-a5f3-c2097b975059

📥 Commits

Reviewing files that changed from the base of the PR and between d28104c and f395beb.

⛔ Files ignored due to path filters (1)
  • .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md is excluded by !.planning/**
📒 Files selected for processing (1)
  • commands/nf/link-canopy.md

Comment thread commands/nf/link-canopy.md
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md
- Discovery banner: use String() coercion on both sides of the active
  preset comparison (presets.find(p => String(p.id) === activeId) and
  the marker assignment). Mirrors the data-side normalization done for
  daintree_preset_id; without it the active marker silently shows
  "(none)" when Daintree emits id as a number while customPresets[].id
  is a string (or vice-versa).
- MD040: added `text` language to the two banner code fences flagged
  by markdownlint (Fan-out import complete and Agents registered in
  Canopy result blocks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 1, 2026 17:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the /nf:link-canopy command and onboarding flow to support Daintree (formerly Canopy) v20 by detecting Daintree installs first (with legacy canopy-app fallback) and enabling bidirectional sync between Daintree presets and nForma quorum slots/providers.

Changes:

  • Add Daintree install + preset discovery to onboard.md and surface it in the onboarding dashboard/routing text.
  • Update /nf:link-canopy discovery to read Daintree v20 schema (agentSettings.agents.*.customPresets[], globalEnvironmentVariables) and add a new fan-out import step that creates/updates one nForma slot per Daintree preset.
  • Extend /nf:link-canopy export to write nForma slots back into Daintree per-agent customPresets[] with provider brand colors and idempotent “no-overwrite” semantics.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
onboard.md Adds Daintree detection (Daintree-first, canopy-app fallback), preset counting across per-agent customPresets[], and onboarding guidance to run /nf:link-canopy.
commands/nf/link-canopy.md Implements Daintree-first discovery, fan-out preset→slot import (providers.json + ~/.claude.json + ~/.claude/nf.json), and slot→Daintree customPresets export with brand colors/idempotency.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-VERIFICATION.md Adds verification artifact describing runtime checks and historical plan evolution notes (currently still mixes stale “truths” with “shipped” behavior).
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md Adds summary artifact with an implementation-evolution disclaimer, but still contains top-level text that describes the superseded design.
.planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-PLAN.md Adds plan artifact with frontmatter “implementation_evolution” corrections documenting schema + design changes.
.planning/STATE.md Records completion entry for the new quick task 405 (issue 138) and resolves prior conflict markers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .planning/quick/405-bidirectional-fleet-sync-canopy-presets-/405-SUMMARY.md Outdated
Comment thread commands/nf/link-canopy.md Outdated
Comment thread commands/nf/link-canopy.md
… HISTORICAL banners

- BUG: customPreset export only emitted ${KEY} placeholders for keys
  ending in _API_KEY, leaving ANTHROPIC_AUTH_TOKEN (and other
  *_AUTH_TOKEN / *_TOKEN keys) unprotected. Result: those auth keys
  never round-tripped to the exported Daintree preset, so a user who
  imported a Z.AI preset (which uses ANTHROPIC_AUTH_TOKEN, not
  ANTHROPIC_API_KEY) and then re-exported back would get a customPreset
  missing its auth. Broadened the secret-bearing pattern to
  /_(API_KEY|AUTH_TOKEN|TOKEN)$/. Also extended the verbatim-pass-
  through to other allowlisted prefixes (ANTHROPIC_*/OPENAI_*/GOOGLE_*/
  GEMINI_*/XAI_*/GROK_*) — provider-prefixed env keys round-trip even
  when not endpoint or secret.
- BUG (edge): daintree_preset_family wasn't cleared on update when
  inferFamily returned null. A preset whose env mutated to drop its
  family signal would keep its old family field stale forever.
  Explicitly delete existing.daintree_preset_family in that case.

Planning docs (405-PLAN/SUMMARY/VERIFICATION.md):
  Frontmatter `implementation_evolution` blocks were already added in
  earlier rounds, but bots kept reading the body as authoritative and
  flagging stale assertions. Added prominent HISTORICAL banners at
  the top of each body that explicitly: name the doc as historical,
  point at the authoritative location (link-canopy.md Step 2d preamble
  + frontmatter), and call out the specific schema/design corrections.
  SUMMARY also gets a "Shipped design (one-liner)" inline so the
  current behavior is the FIRST thing readers see.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jobordu jobordu merged commit 82ef4b8 into main May 1, 2026
21 checks passed
jobordu added a commit that referenced this pull request May 1, 2026
…es (MD040) (#144)

markdownlint-cli2 flagged 10 bare ```...``` fences in commands/nf/link-canopy.md.
The two banner-output blocks were fixed during PR #142 review; this commit
sweeps the remaining 10 (all rendered output / regex / banner text — `text` is
the right tag).

CodeRabbit previously deferred this as a separate PR to keep #142 focused on
the bidirectional fleet sync redesign.

Co-authored-by: jobordu <jonathan@jonathanborduas.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jobordu added a commit that referenced this pull request May 1, 2026
…#148)

Before: install.js unconditionally copyFileSync'd bin/providers.json
over the top of ~/.claude/nf/bin/providers.json. Any provider entries
the user had added — fan-out preset slots from /nf:link-canopy
(carrying daintree_preset_id), ccr-* slots manually re-added via
/nf:mcp-setup after the default-fleet retirement, hand-rolled custom
slots — were silently destroyed on every npm update / install.js run.

This was observed in practice: PR #142's bidirectional-sync testing
required restoring claude-z-ai and claude-minimax slots multiple
times because intermediate install.js runs kept clobbering them.

After: a new mergeProvidersJson() helper does name-keyed merging:
  - Repo-shipped slots (codex-1, gemini-1, etc.): REPLACED with the
    repo version so metadata bumps (description, mainTool, model
    defaults) propagate on update.
  - User-only slots (no name match in repo): PRESERVED verbatim,
    appended after the repo entries. This covers fan-out preset
    slots, manually-added ccr-* slots, and any user customization.
  - Top-level fields merged shallowly (repo wins; today the file is
    just `{providers: [...]}` so this is effectively a no-op).
  - Atomic write via .merge.tmp + rename so a power-cut mid-merge
    never leaves a half-written file.
  - Fail-open: corrupt user JSON → fall back to repo overwrite (so
    installs never wedge); corrupt repo JSON → leave user file
    untouched.

The merge logic lives in a new module bin/install-helpers.cjs so it
can be exercised by node --test without spawning a real install.
9 new tests cover: fresh install, single preserved fan-out slot,
metadata bump on shared slot, multiple user extras, corrupt user
JSON fallback, corrupt repo JSON bail, atomic-write cleanup,
malformed user.providers handling, and ordering invariants.

install.js wraps the helper with its color-coded log adapter so the
install output reads the same as before for repo-only updates,
plus a `✓ providers.json: merged repo defaults; preserved N
user-added slot(s): <names>` line when there are extras.

Co-authored-by: jobordu <jonathan@jonathanborduas.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bidirectional fleet sync: Canopy presets <-> nForma providers

2 participants