diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml
new file mode 100644
index 0000000..a841bf7
--- /dev/null
+++ b/.github/workflows/publish-vscode.yml
@@ -0,0 +1,27 @@
+name: Publish VS Code extension
+
+# Manual, vscode-only publish — decoupled from the tag-driven npm release so it
+# never tries to republish the @loop-lang/* libs (which fail on a duplicate
+# version). Bump packages/vscode/package.json `version`, then run this from the
+# Actions tab. Uses the same VSCE_PAT secret as release.yml.
+on:
+ workflow_dispatch:
+
+jobs:
+ vscode:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm ci
+ - run: npm run build # build all workspaces so parser/runtime/viz types exist
+ - run: npm run build -w loop-vscode # syncs the template library + bundles
+ - run: npx --yes @vscode/vsce@latest publish --no-dependencies
+ working-directory: packages/vscode
+ env:
+ VSCE_PAT: ${{ secrets.VSCE_PAT }}
diff --git a/.gitignore b/.gitignore
index b3b5114..855dc84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ packages/cli/assets/
packages/cli/AGENTS.md
packages/cli/.claude/
.vercel
+
+# Synced from the repo template library at build (single source of truth: /templates)
+packages/vscode/templates/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca8d943..403069b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,19 @@ Versions track the `@loop-lang/loop` installer package.
- `demo.loop` — a copy-and-edit sample pipeline (plan → implement → ship) at the
repo root for the README/tutorial.
+### Changed
+- **`loop-vscode` 0.4.0** — *New from template* now offers the full best-practice
+ library (`bugfix`, `feature`, `brownfield-feature`, `refactor`, `cicd-check`,
+ `security`, `clean-architecture`, `test-coverage`, `review-diff`, plus the
+ multi-file `greenfield-app`/`load-spec` flows), synced from the repo's
+ `templates/` at build (single source of truth) — replacing the stale
+ `bmad`/`foreach` bundles. Tutorial now states what the extension gives you and
+ lists the latest vocabulary it understands. *New Loop from template* is now
+ reachable from **File ▸ New File…** and an Explorer folder **right-click** (drops
+ the template straight into that folder), not just the Command Palette.
+- A manual `Publish VS Code extension` workflow (`workflow_dispatch`) publishes
+ the extension on its own, decoupled from the tag-driven npm release.
+
## [0.5.0] — 2026-06-28
> `@loop-lang/loop` 0.5.0 · `@loop-lang/{parser,runtime,stdlib,viz}` 0.2.0
diff --git a/docs/index.html b/docs/index.html
index bdd4a9f..aa2068e 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -737,12 +737,12 @@
Watch it run — live dashboard interm
The page streams over Server-Sent Events (each event carries an id; the server replays on connect and dedupes on reconnect), is self-contained — no external assets — and binds to 127.0.0.1 only.
In VS Code optional — for hand-authoring
- Secondary to the chat — reach for the extension only when you want to write .loop files by hand in an editor.
+ Secondary to the chat — reach for the extension only when you want to write .loop files by hand in an editor. It turns a plain text file into a real authoring surface: it knows the grammar, catches a loop that can't finish before you run it, lets you launch a run without leaving the editor, and drops in a ready-made best-practice loop to start from. What you get:
- - Syntax highlighting, hover docs, and tab-completion for every construct.
+ - Syntax highlighting, hover docs, and context-aware tab-completion for every construct — including the latest vocabulary (
use skills, remember in, flow, for each, and done when the skill "…" approves / scores N or more).
- A ▶ Run CodeLens above each definition. When you click it, you choose where it runs (the
loop.runMode setting): open a Claude Code session — an interactive run in the integrated terminal where you watch every step and answer gates in chat — or the VS Code output panel (headless, with a live trace and native gate dialogs). Set loop.runMode to ask (prompt each time), session, or output.
- A soft linter that nudges (never blocks): "this loop has no way to verify it's done", "add a thrash guard".
- - LoopFlow: New from template — scaffold a ready-to-edit method bundle (a generic
for each setup, or a BMAD A-to-Z one) into your workspace in one pick.
+ - New Loop from template — reachable from File ▸ New File…, an Explorer folder right-click, or the Command Palette — scaffolds a ready-to-edit, best-practice loop into your workspace in one pick, from the same library
loop init ships: bugfix, feature, brownfield-feature, refactor; cicd-check, security, clean-architecture, test-coverage, review-diff; and the multi-file flows greenfield-app and load-spec (which pull in their supporting discover/design/story-template + sprint.yaml). Each lands with its # TODO lines marked for you to fill in.
Troubleshooting
diff --git a/packages/vscode/.vscodeignore b/packages/vscode/.vscodeignore
index d0b63a7..06f23b3 100644
--- a/packages/vscode/.vscodeignore
+++ b/packages/vscode/.vscodeignore
@@ -1,5 +1,6 @@
src/**
test/**
+scripts/**
.vscode/**
tsconfig.json
**/*.ts
diff --git a/packages/vscode/package.json b/packages/vscode/package.json
index be75f08..091e2e8 100644
--- a/packages/vscode/package.json
+++ b/packages/vscode/package.json
@@ -1,7 +1,7 @@
{
"name": "loop-vscode",
"displayName": "LoopFlow",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Syntax, formatting, best-practice templates, completion, and ▶ Run / Claude-session buttons for the LoopFlow (.loop) DSL.",
"license": "Apache-2.0",
"publisher": "loop-lang",
@@ -73,9 +73,18 @@
},
{
"command": "loop.newFromTemplate",
- "title": "LoopFlow: New from template"
+ "title": "New Loop from template…",
+ "category": "LoopFlow"
}
],
+ "menus": {
+ "file/newFile": [
+ { "command": "loop.newFromTemplate", "group": "loop" }
+ ],
+ "explorer/context": [
+ { "command": "loop.newFromTemplate", "when": "explorerResourceIsFolder", "group": "navigation@20" }
+ ]
+ },
"configuration": {
"title": "LoopFlow",
"properties": {
@@ -114,7 +123,8 @@
},
"scripts": {
"vscode:prepublish": "npm run build",
- "build": "tsc -p tsconfig.json && npm run bundle",
+ "sync:templates": "node scripts/sync-templates.mjs",
+ "build": "npm run sync:templates && tsc -p tsconfig.json && npm run bundle",
"bundle": "esbuild src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --target=node18 --minify",
"package": "npx --yes @vscode/vsce@latest package --no-dependencies",
"test": "node --test test/*.test.mjs"
diff --git a/packages/vscode/scripts/sync-templates.mjs b/packages/vscode/scripts/sync-templates.mjs
new file mode 100644
index 0000000..5ffca32
--- /dev/null
+++ b/packages/vscode/scripts/sync-templates.mjs
@@ -0,0 +1,15 @@
+// Copy the repo's canonical template library into the extension so the .vsix
+// ships it. Single source of truth = ../../../templates. Run in `build` before
+// packaging. ponytail: a copy, not a registry — the picker's file list lives in
+// src/extension.ts.
+import { cp, rm } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import { dirname, join } from "node:path";
+
+const here = dirname(fileURLToPath(import.meta.url));
+const src = join(here, "..", "..", "..", "templates");
+const dest = join(here, "..", "templates");
+
+await rm(dest, { recursive: true, force: true });
+await cp(src, dest, { recursive: true });
+console.log(`synced templates → ${dest}`);
diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts
index 4245756..1a6bcc3 100644
--- a/packages/vscode/src/extension.ts
+++ b/packages/vscode/src/extension.ts
@@ -26,7 +26,7 @@ export function activate(context: vscode.ExtensionContext) {
if (!target) return void vscode.window.showErrorMessage("Loop: no .loop file to run.");
runInSession(target);
}),
- vscode.commands.registerCommand("loop.newFromTemplate", () => newFromTemplate(context))
+ vscode.commands.registerCommand("loop.newFromTemplate", (uri?: vscode.Uri) => newFromTemplate(context, uri))
);
// Live parse diagnostics (red squiggles) — debounced.
@@ -47,45 +47,56 @@ export function activate(context: vscode.ExtensionContext) {
export function deactivate() {}
-async function newFromTemplate(context: vscode.ExtensionContext): Promise {
+// The bundled best-practice library (synced from the repo's /templates at build).
+// `deps` are the shared support files a multi-file flow needs alongside its entry.
+interface Tmpl { file: string; label: string; blurb: string; deps?: string[] }
+const TEMPLATES: Tmpl[] = [
+ // Spec-driven — a whole app / a written backlog
+ { file: "greenfield-app.loop", label: "greenfield-app", blurb: "Build an app A→Z: discover → design → per-story", deps: ["discover.loop", "design.loop", "story-template.loop", "sprint.yaml", "plan.md"] },
+ { file: "load-spec.loop", label: "load-spec", blurb: "Deliver an existing backlog story by story", deps: ["story-template.loop", "sprint.yaml", "plan.md"] },
+ // Change — build / fix / restructure
+ { file: "feature.loop", label: "feature", blurb: "Ship one feature: build → regression → security → 👤 review" },
+ { file: "brownfield-feature.loop", label: "brownfield-feature", blurb: "Add a feature to an existing codebase without breaking it" },
+ { file: "bugfix.loop", label: "bugfix", blurb: "Fix one bug, proven by a named test" },
+ { file: "refactor.loop", label: "refactor", blurb: "Improve structure, behavior unchanged" },
+ // Quality gates — drive existing checks to green
+ { file: "cicd-check.loop", label: "cicd-check", blurb: "Make every CI check pass locally" },
+ { file: "security.loop", label: "security", blurb: "Security pass before shipping: sast → deps → 👤 secrets" },
+ { file: "clean-architecture.loop", label: "clean-architecture", blurb: "Enforce architecture boundaries — deps point inward" },
+ { file: "test-coverage.loop", label: "test-coverage", blurb: "Raise coverage to a threshold with real tests" },
+ { file: "review-diff.loop", label: "review-diff", blurb: "Review + clean the current branch diff" },
+];
+
+async function newFromTemplate(context: vscode.ExtensionContext, folder?: vscode.Uri): Promise {
const fs = await import("node:fs/promises");
const { join } = await import("node:path");
const templatesDir = join(context.extensionPath, "templates");
- let names: string[];
- try {
- const ents = await fs.readdir(templatesDir, { withFileTypes: true });
- names = ents.filter((d) => d.isDirectory()).map((d) => d.name);
- } catch {
- vscode.window.showErrorMessage("Loop: no templates bundled with the extension.");
- return;
- }
- if (names.length === 0) { vscode.window.showErrorMessage("Loop: no templates found."); return; }
-
- const blurbs: Record = {
- foreach: "Generic: iterate a plan, run a checklist per item",
- bmad: "BMAD A-to-Z: discover → design → per-story checklist",
- };
const pick = await vscode.window.showQuickPick(
- names.map((n) => ({ label: n, description: blurbs[n] ?? "" })),
+ TEMPLATES.map((t) => ({ label: t.label, description: t.blurb, tmpl: t })),
{ placeHolder: "Choose a Loop template" }
);
if (!pick) return;
- const ws = vscode.workspace.workspaceFolders?.[0];
- if (!ws) { vscode.window.showErrorMessage("Loop: open a folder first."); return; }
-
- const sub = await vscode.window.showInputBox({
- prompt: "Copy the template into which folder (relative to the workspace)?",
- value: pick.label,
- });
- if (sub === undefined) return;
-
- const srcDir = join(templatesDir, pick.label);
- const destDir = join(ws.uri.fsPath, sub);
+ // Right-clicked an Explorer folder → drop it there, no prompt. Otherwise ask,
+ // relative to the workspace root.
+ let destDir: string;
+ if (folder) {
+ destDir = folder.fsPath;
+ } else {
+ const ws = vscode.workspace.workspaceFolders?.[0];
+ if (!ws) { vscode.window.showErrorMessage("Loop: open a folder first."); return; }
+ const sub = await vscode.window.showInputBox({
+ prompt: "Copy into which folder (relative to the workspace)?",
+ value: ".",
+ });
+ if (sub === undefined) return;
+ destDir = join(ws.uri.fsPath, sub);
+ }
await fs.mkdir(destDir, { recursive: true });
+ const where = vscode.workspace.asRelativePath(destDir);
- const files = await fs.readdir(srcDir);
+ const files = [pick.tmpl.file, ...(pick.tmpl.deps ?? [])];
let copied = 0;
let skipped = 0;
for (const f of files) {
@@ -93,21 +104,22 @@ async function newFromTemplate(context: vscode.ExtensionContext): Promise
let exists = false;
try { await fs.access(dest); exists = true; } catch { /* missing → copy */ }
if (exists) {
- const ans = await vscode.window.showWarningMessage(`${f} already exists in ${sub}. Overwrite?`, "Overwrite", "Skip");
+ const ans = await vscode.window.showWarningMessage(`${f} already exists in ${where}. Overwrite?`, "Overwrite", "Skip");
if (ans !== "Overwrite") { skipped++; continue; }
}
- await fs.copyFile(join(srcDir, f), dest);
- copied++;
+ try {
+ await fs.copyFile(join(templatesDir, f), dest);
+ copied++;
+ } catch {
+ vscode.window.showWarningMessage(`Loop: template file "${f}" is missing from the extension bundle.`);
+ }
}
- vscode.window.showInformationMessage(`Loop: copied ${copied} file(s) to ${sub}${skipped ? `, skipped ${skipped}` : ""}.`);
+ vscode.window.showInformationMessage(`Loop: copied ${copied} file(s) to ${where}${skipped ? `, skipped ${skipped}` : ""}. Edit the # TODO lines before running.`);
// Open the entry .loop so the user lands on the thing to run.
- const entry = files.find((f) => /^(epic|deliver)\.loop$/.test(f)) ?? files.find((f) => f.endsWith(".loop"));
- if (entry) {
- const doc = await vscode.workspace.openTextDocument(join(destDir, entry));
- await vscode.window.showTextDocument(doc);
- }
+ const doc = await vscode.workspace.openTextDocument(join(destDir, pick.tmpl.file));
+ await vscode.window.showTextDocument(doc);
}
/** ▶ Run lens above every loop/pipeline definition. */
diff --git a/packages/vscode/templates/bmad/README.md b/packages/vscode/templates/bmad/README.md
deleted file mode 100644
index 5030561..0000000
--- a/packages/vscode/templates/bmad/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# BMAD A-to-Z — discover → design → implement every story
-
-This bundle shows the full A-to-Z shape with BMAD as one example method.
-The same pattern works with any method — BMAD is not special to the runtime.
-
-## Shape
-
-```
-discover (interview) → design (human approves) → for each story → story checklist
-```
-
-1. **discover** — the agent interviews you and writes `sprint.yaml` (done when the file exists).
-2. **design** — the agent turns `sprint.yaml` into an architecture note; a human approves before stopping.
-3. **for each story** — runs `story-template.loop` once per story in `sprint.yaml` (implement → security → manual QA).
-
-## Files
-
-| File | Purpose |
-|---|---|
-| `sprint.yaml` | The story list written by `discover`. Seed data is provided; `discover` overwrites it. |
-| `discover.loop` | Interview loop — done when `sprint.yaml` exists. |
-| `design.loop` | Architecture loop — done when `design.md` exists; human reviews before stop. |
-| `story-template.loop` | The checklist every story runs through (implement → security → manual QA). |
-| `epic.loop` | The full flow: discover → design → for each story → template. |
-
-## Run it
-
-```bash
-/loopflow run epic.loop # inside Claude Code conversation
-loop run epic.loop # via the CLI
-```
-
-## BMAD is one example
-
-`for each story in "sprint.yaml": run "story-template.loop"` is plain Loop syntax.
-Replace the discover/design loops with your method's equivalents, or skip them
-and point `for each` at any YAML list of work items.
diff --git a/packages/vscode/templates/bmad/design.loop b/packages/vscode/templates/bmad/design.loop
deleted file mode 100644
index ddee950..0000000
--- a/packages/vscode/templates/bmad/design.loop
+++ /dev/null
@@ -1,7 +0,0 @@
-loop "design":
- goal: turn sprint.yaml into an architecture note the team approves
- look at: sprint.yaml, docs, and the last failure
- allow edits automatically
- each cycle: plan, then act, then observe
- done when "test -s design.md" passes
- a human reviews before stopping
diff --git a/packages/vscode/templates/bmad/discover.loop b/packages/vscode/templates/bmad/discover.loop
deleted file mode 100644
index 7322b87..0000000
--- a/packages/vscode/templates/bmad/discover.loop
+++ /dev/null
@@ -1,9 +0,0 @@
-# Interactive discovery — run in conversation (/loopflow run). The agent interviews you
-# and writes sprint.yaml. It is DONE when sprint.yaml exists (the artifact check),
-# NOT when the chat goes quiet. Point the check at a real validator for stricter gating.
-loop "discover":
- goal: interview the team and capture the epic as a sprint.yaml with a stories list
- look at: docs, and the last failure
- allow edits automatically
- each cycle: plan, then act, then observe
- done when "test -s sprint.yaml" passes
diff --git a/packages/vscode/templates/bmad/epic.loop b/packages/vscode/templates/bmad/epic.loop
deleted file mode 100644
index eac8347..0000000
--- a/packages/vscode/templates/bmad/epic.loop
+++ /dev/null
@@ -1,7 +0,0 @@
-# A-to-Z, run in conversation: /loopflow run epic.loop
-# discover (interview) -> design (approve) -> run the checklist for each story.
-flow "epic: authentication":
- run "discover.loop"
- then run "design.loop"
- then for each story in "sprint.yaml":
- run "story-template.loop"
diff --git a/packages/vscode/templates/bmad/sprint.yaml b/packages/vscode/templates/bmad/sprint.yaml
deleted file mode 100644
index 9797166..0000000
--- a/packages/vscode/templates/bmad/sprint.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-stories:
- - title: User can log in
- acceptance: a registered user logs in with email + password
- - title: User can sign up
- acceptance: a new user creates an account and gets a verification email
- - title: User can reset their password
- acceptance: user requests a reset, gets a link, and sets a new password
diff --git a/packages/vscode/templates/bmad/story-template.loop b/packages/vscode/templates/bmad/story-template.loop
deleted file mode 100644
index 08edf42..0000000
--- a/packages/vscode/templates/bmad/story-template.loop
+++ /dev/null
@@ -1,21 +0,0 @@
-# Runs once per story in sprint.yaml. The current story text arrives as context.
-pipeline "story":
-
- stage implement:
- goal: implement the story described in the provided context
- look at: src, and the last failure
- allow edits automatically, but ask me before migrations or pushes
- each cycle: plan, then act, then observe
- done when "pnpm test" passes
- when it fails: reflect on which layer broke, then plan again
- after 6 tries: stop and warn "thrashing on this story"
-
- stage security:
- goal: no high or critical vulnerabilities introduced
- each cycle: plan, then act, then observe
- done when "semgrep --config auto --severity=high" finds nothing
-
- stage manual:
- goal: the story works end-to-end when a human tries it
- each cycle: observe
- a human reviews before stopping
diff --git a/packages/vscode/templates/foreach/README.md b/packages/vscode/templates/foreach/README.md
deleted file mode 100644
index b2c02b9..0000000
--- a/packages/vscode/templates/foreach/README.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# foreach — iterate a plan, run a template per item
-
-This bundle shows the core `for each` pattern: a list of work items lives in
-`plan.yaml`; the same checklist (`item-template.loop`) runs once per item.
-
-## Files
-
-| File | Purpose |
-|---|---|
-| `plan.yaml` | The list of work items (a YAML list under `items:`). Edit this to change the work. |
-| `item-template.loop` | The checklist every item runs through (implement → security → review). |
-| `deliver.loop` | The flow: `for each item in "plan.yaml": run "item-template.loop"`. |
-
-## How `for each` works
-
-`for each in "":` reads the list from the YAML file and runs the
-template once per entry. Each entry's text (title + detail) becomes the template's
-context — what to build. If an item fails its checklist, the runtime pauses and
-asks whether to continue with the next item or stop.
-
-## Commands
-
-```bash
-loop parse deliver.loop # inspect the parsed AST
-loop run deliver.loop # execute — one template run per item
-```
diff --git a/packages/vscode/templates/foreach/deliver.loop b/packages/vscode/templates/foreach/deliver.loop
deleted file mode 100644
index 9c3e262..0000000
--- a/packages/vscode/templates/foreach/deliver.loop
+++ /dev/null
@@ -1,5 +0,0 @@
-# The whole point of `for each`: run the same checklist for every item in a plan,
-# without naming each one. Edit plan.yaml to change the work; this file stays put.
-flow "deliver":
- for each item in "plan.yaml":
- run "item-template.loop"
diff --git a/packages/vscode/templates/foreach/item-template.loop b/packages/vscode/templates/foreach/item-template.loop
deleted file mode 100644
index f74632e..0000000
--- a/packages/vscode/templates/foreach/item-template.loop
+++ /dev/null
@@ -1,23 +0,0 @@
-# Runs once per item in plan.yaml. The current item's text arrives as context
-# (what to build); the checks below are the SAME for every item.
-pipeline "work item":
-
- stage implement:
- goal: implement the work item described in the provided context
- look at: src, and the last failure
- allow edits automatically, but ask me before migrations or pushes
- each cycle: plan, then act, then observe
- done when "pnpm test" passes
- when it fails: reflect on which layer broke, then plan again
- after 6 tries: stop and warn "thrashing on this item"
-
- stage security:
- goal: no high or critical vulnerabilities introduced
- each cycle: plan, then act, then observe
- done when "semgrep --config auto --severity=high" finds nothing
- after 4 tries: stop and warn "security needs a human"
-
- stage review:
- goal: lints clean and types check
- each cycle: act, then observe
- done when "pnpm lint && pnpm typecheck" passes
diff --git a/packages/vscode/templates/foreach/plan.yaml b/packages/vscode/templates/foreach/plan.yaml
deleted file mode 100644
index d6c865e..0000000
--- a/packages/vscode/templates/foreach/plan.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-# A plain list of work items. `for each` runs the template once per entry;
-# each entry's text becomes the template's context (what to build).
-items:
- - title: Parse the uploaded CSV into rows
- detail: accept a multipart upload, stream-parse, reject malformed input
- - title: Validate rows against the schema
- detail: required fields, types, and sane per-row error messages
- - title: Export the validated rows as JSON
- detail: stable key order; gzip when the payload is large