Skip to content

feat: create a tool or a skill from the content of an add - #950

Open
ctawiah wants to merge 1 commit into
ctawiah/scope-commands-1e8afrom
ctawiah/create-attachments-1e8a
Open

ctawiah wants to merge 1 commit into
ctawiah/scope-commands-1e8afrom
ctawiah/create-attachments-1e8a

Conversation

@ctawiah

@ctawiah ctawiah commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Context

sync tools add and sync skills add start syncing a tool or skill that exists in LaunchDarkly. To create a new one, a user must write the file by hand with upsert: true, and then run sync. This PR and the next let add create a missing tool or skill from flags. This PR has the create flow, through Runner.Run. The next PR adds the flags. The work is in two PRs to keep each one small.

What changes

  • AddAction.Create holds the content of a create. For a tool, that is the input schema and the description. For a skill, it is the Markdown, the name, and the description. A nil value is the existing add.
  • The content rules run before any request:
    • The schema must be one JSON object. A pasted tool file or MCP definition, with a top-level inputSchema, schema, or formatVersion key, gets --json-schema is the input schema only. Pass the value of "inputSchema", and pass the description with --description.
    • Skill Markdown must not be blank, and gets a final newline. So content from "$(cat file)" has the fingerprint of the file.
    • --name or --description without the content flag gets --name and --description need --content. add creates a skill only when --content is set.
    • A create takes at most one --key. The key, the name, and the description must pass the create rules of LaunchDarkly.
    • Without a terminal, the error names every missing flag: tool creation requires --project and --key without interactive input.
  • If LaunchDarkly has the resource, and every field that the user passed matches, add starts syncing it through the existing add. It does not write to LaunchDarkly. Fields without a flag, such as tool tags, are not compared. The skill name is never compared, because LaunchDarkly owns it.
  • If a passed field differs, add stops and changes nothing.
  • Otherwise add plans a create_server row in memory, and asks for confirmation unless --yes is set. Only then does it write the file with upsert: true. It applies the plan of that one resource through the normal sync path, so other pending changes stay as they are.
  • If the state changed after the review, or LaunchDarkly rejects the create, add removes the file again. After a write with no response, it keeps the file, so that the next sync creates the resource or finds it.
  • A skill file with upsert now keeps name:, because a create sends that name.

With the flags of the next PR, against a stand-in for the LaunchDarkly API:

$ ldcli sync tools add --project demo --key refund --description "Issue a refund" \
    --json-schema '{"type":"object","properties":{"orderId":{"type":"string"}},"required":["orderId"]}' --no-input -o plaintext
1 change: 1 tool create.

Tools
  create   demo/refund   new   +8
interactive confirmation requires a terminal; rerun with --yes to apply non-interactively

$ ldcli sync tools add ... --yes --no-input -o plaintext
Tools
  created   demo/refund   version 1

1 applied.
Note: Created .launchdarkly/demo/tools/refund.json.

An adopt and a refusal:

Note: skill demo/triage already exists in LaunchDarkly with this content, so add started syncing it.
Note: skill demo/triage keeps its LaunchDarkly name "Triage".

skill demo/summarize already exists in LaunchDarkly with different content, and add does not change it. To sync the LaunchDarkly version, run ldcli sync skills add --project demo --key summarize, then edit the file and run ldcli sync skills.

Design notes

  • The new package internal/sync/create holds the input and its rules, so that prompt imports create and not the other way around. Target asks for the project and the key, Input.Ask asks for the name and the description of a skill, and Input.Attachment builds the resource. The input fields are pointers, so --content "" is a blank value and not a missing flag.
  • runCreate reuses the sync path and adds no second apply. It uses planAttachment for the reviewed row, then reviewAndConfirmPlan. After the write, it uses loadState with a filter for the one ID, checkUnchanged, applyPlan, and RollbackCreation. applyPlan now returns the outcomes. So the create removes the file only when its own row failed with a definite error, not after a failed save.
  • The question comes before the file exists. watch applies a plan without deletes by itself, so a file that exists during the question can let a running watch create the resource first.
  • The name and description questions of a skill come after the LaunchDarkly read. So an adopt asks nothing that it then ignores.
  • The checks for a file and a sync.lock row move from the add into bootstrap.CheckNotSynced, so both use one copy. The create runs them again right before the write, because a file can appear while the question waits.
  • link.requiredValue moves into create as RequiredValue, and its test moves with it.
  • The JSON source of a skill name comes in the next PR, with the flags.

Review focus

  • Can a create change a resource that LaunchDarkly already has? Can it write a file before the user agrees?
  • Is the file removal right after each failure? The failures are a changed state, a definite error, a write with no response, and a failed save after a good create.
  • Is the adopt comparison right for a tagged tool and for a renamed skill?

Verification

  • go test ./... and golangci-lint run (v2.9.0) pass.
  • The new prompt/create_test.go runs each case of the add table through Runner.Run, without a terminal and with an input reader that fails on any read:
    • tool and skill creates, and the next dry run in sync
    • adopts of a tagged tool and of a renamed skill, and the refusals
    • every content rule and both "already synced" errors, with no request
    • a dry run, a cancel, and a missing --yes
    • a state change after the review, a definite 400, a write with no response that the next sync finishes, and a 409 that reads again
    • a create that leaves other pending changes as they are
  • TestCreateAsksBeforeItWritesTheFile records whether the file exists when the question reads its answer. It fails when the write moves before the question. TestCreateKeepsTheFileAfterAWriteWithNoResponse fails when the removal ignores MutationMayHaveSucceeded.
  • The new create/create_test.go covers the content rules, the builders, the adopt comparison, and the error for missing flags.
  • I built ldcli and ran the flag-only script of the sync commands against a stand-in for the LaunchDarkly API. Its output is the same as before this PR. The next PR has the manual checks of the create.

Related changes

This stack builds on #937 to #941, which are merged.

  1. fix: exclude expanded tools from prompt diffs #957: Exclude expanded tools from prompt diffs (ctawiah/fix-prompt-tool-diff-5b1c)
  2. fix: plan a tool or skill row when a variation newly references it #945: Plan a tool or skill row when a variation newly references it (ctawiah/plan-referenced-attachments-1e8a)
  3. feat: keep unused tools and skills, and detach any resource kind #946: Keep unused tools and skills, and detach any resource kind (ctawiah/keep-unused-attachments-1e8a)
  4. feat: add tools and skills from LaunchDarkly #947: Add tools and skills from LaunchDarkly (ctawiah/add-attachments-1e8a)
  5. feat: route sync, add, and detach by scope #948: Route sync, add, and detach by scope (ctawiah/route-scopes-1e8a)
  6. feat: add the sync all, sync tools, and sync skills commands #949: Add the sync all, sync tools, and sync skills commands (ctawiah/scope-commands-1e8a)
  7. This PR: create a tool or a skill from the content of an add (ctawiah/create-attachments-1e8a)
  8. feat: add the create flags of sync tools add and sync skills add #951: Add the create flags of sync tools add and sync skills add (ctawiah/create-attachment-flags-1e8a)
  9. refactor: share the selection of a new variation, and check any kind before an add #952: Share the selection of a new variation, and check any kind before an add (ctawiah/share-variation-selection-1e8a)
  10. feat: create a prompt variation from the content of an add #953: Create a prompt variation from the content of an add (ctawiah/create-variations-1e8a)
  11. feat: add the create flags of sync prompts add #954: Add the create flags of sync prompts add (ctawiah/create-variation-flags-1e8a)

Note

Overview
sync tools add / sync skills add can create a missing tool or skill from create input (wired through AddAction.Create; CLI flags land in a follow-up PR). A new internal/sync/create package validates flags before any API call, builds upsert attachments, and supports interactive project/key/skill metadata.

The add runner plans a single create_server row, confirms before writing the local file, then syncs only that resource. If LaunchDarkly already has matching content (flag fields only), it adopts via the existing add path; mismatches are refused with no writes. Failures after review roll back the new file unless the mutation may have succeeded; concurrent creates are detected in attachmentResolver.publish.

Shared bootstrap.CheckNotSynced guards add and create; skill files with upsert now persist name: in front matter. “Already synced” hints mention ldcli sync all.

Reviewed by Cursor Bugbot for commit 4cd1c89. Bugbot is set up for automated code reviews on this repo. Configure here.

@ctawiah
ctawiah added this pull request to stack #955 October 22, 2026 22:19
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch from c69f654 to b34a204 Compare October 23, 2026 00:13
@ctawiah
ctawiah removed this pull request from stack #955 October 23, 2026 00:15
@ctawiah
ctawiah added this pull request to stack #960 October 23, 2026 00:16
@ctawiah
ctawiah marked this pull request as ready for review October 23, 2026 14:10
@ctawiah
ctawiah requested a review from a team as a code owner October 23, 2026 14:10
@ctawiah
ctawiah requested review from nkester-ld and zakk-verrilli-ld and removed request for a team October 23, 2026 14:10
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch 2 times, most recently from 3eff865 to a02b3bc Compare October 23, 2026 14:40
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch from a02b3bc to f5c0cc0 Compare October 23, 2026 14:44
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch 2 times, most recently from c2dd645 to 919f852 Compare October 23, 2026 17:07
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch from 919f852 to c9b5ec1 Compare October 23, 2026 21:17
Comment thread internal/sync/bootstrap/attachments.go Outdated
Comment on lines +114 to +116
return fmt.Errorf("%s is already synced. Edit %s and run ldcli sync %ss.", id.Label(), path, id.Kind)
case slices.ContainsFunc(lock.Resources, func(resource syncmanifest.Resource) bool { return resource.ID() == id }):
return fmt.Errorf("%s has a sync.lock entry but no local file. Run ldcli sync %ss to restore it.", id.Label(), id.Kind)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

do we want to mention sync all here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes. Both refusals here, and the refusal for a LaunchDarkly resource with other content, now end with or ldcli sync all. TestCreateRefusesAResourceThatIsAlreadySynced now also runs sync tools and sync all after the refusal and checks that each one restores the standalone file. Fixed in ba4f410.

One limit I found while checking: if a local variation uses the tool, every sync fails before planning, because compiling the variation needs that tool file. That behavior is older than this stack.

@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch from c9b5ec1 to ba4f410 Compare October 23, 2026 21:26

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ba4f410. Configure here.

Comment thread internal/sync/prompt/create.go
An add of a tool or a skill can now carry the content of a new resource:
the input schema and the description of a tool, or the Markdown, the
name, and the description of a skill. The new package
internal/sync/create checks that content before any request, asks in a
terminal for the values that are missing, and builds the resource with
upsert.

If LaunchDarkly has the resource with the content that the user passed,
add starts syncing it through the existing add. If the content differs,
add stops and changes nothing. Otherwise add plans the create in memory,
asks for confirmation, and only then writes the file. It then applies
the plan of that one resource through the normal sync path. A changed
state or a rejected create removes the file again. A write with no
response keeps the file, so that the next sync finishes the create.

A skill file with upsert now keeps its name, because the create sends
it. The checks of an add for a file or a sync.lock entry move into
CheckNotSynced, so that the create uses the same checks. requiredValue
moves from link into create. The command flags come in the next change.
@cursor
cursor Bot force-pushed the ctawiah/create-attachments-1e8a branch from ba4f410 to 4cd1c89 Compare October 23, 2026 21:39
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.

2 participants