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 flags (wired through AddAction.Create; CLI flags land in a follow-up PR). A new internal/sync/create package validates input before any API call, resolves --project/--key (and skill name/description) interactively, builds upsert attachments, and compares only flag-provided fields when LaunchDarkly already has the resource.

When Create is set, runAdd runs runCreate: refuse if already synced (bootstrap.CheckNotSynced), adopt into normal add if remote content matches flags, error if it differs, otherwise plan create_server, confirm (unless --yes), write the local file, and apply via the existing sync path for that resource only. The file is written after confirmation; it is rolled back on definite create failure or post-review state drift, but kept on ambiguous write errors so a later sync can finish.

CheckNotSynced is extracted from bootstrap add for shared use. Skill files with upsert: true now persist name in front matter. applyPlan returns outcomes so create can remove the file only when its row failed with a definite error.

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

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 b34a204 to 3eff865 Compare October 23, 2026 14:27
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.

1 participant