Compare commits

..

11 Commits

Author SHA1 Message Date
Alan Wizemann 3c2d11470f chore: Bump version to 2.2.1 2026-04-23 22:05:50 +02:00
Alan Wizemann dcd2f8f04b docs: v2.2.1 release notes
Covers the four commits landed since v2.2.0:

- New catalog template: awizemann/template-author (scaffolding skill)
- Config sheet fix: EnumControl always uses Menu picker, not Segmented
  (the long-option-label overflow that clipped the form)
- Config sheet fix: maxWidth constraint on inner VStacks so descriptions
  with unbreakable tokens wrap cleanly
- SKILL.md authoring guidance: prefer markdown link syntax over raw URLs
- Devops: scripts/catalog.sh accepts git worktrees

release.sh picks up this file as the GitHub release body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:04:31 +02:00
Alan Wizemann ef3ddcdd7a fix(config-sheet): EnumControl always uses Menu picker, never Segmented
The Configuration sheet's clipping bug persisted after the earlier
VStack maxWidth fix (d616935) and the user's Part-C manifest
rewrite to use [label](url) markdown. Re-diagnosed: the actual
overflow source was EnumControl's `.pickerStyle(.segmented)` branch,
active when options.count ≤ 4.

Segmented pickers on macOS size to the intrinsic width of all their
labels concatenated. They refuse offered width constraints, refuse
to wrap, refuse to truncate. A schema with three long labels like
"Claude Opus 4 (Recommended - Most Capable)" produced a ~650pt
segmented picker that pushed the fieldRow past the sheet's 560pt
viewport. No amount of .frame(maxWidth: .infinity) on parent
containers can rein in a segmented picker — the picker ignores
them.

Fix: remove the segmented branch. Always use the default Menu
picker (dropdown). Dropdowns respect offered width and surface long
labels in the popup list, so the sheet can't overflow regardless of
label length or option count.

Loses the segmented look for short-enum cases like a 3-option
"Daily / Weekly / Monthly" picker — compactness traded for
correctness. If a future template author wants segmented rendering
for a specific short-label enum, we can add a manifest hint
(e.g., "uiHint": "segmented") that explicitly opts in; not worth
the machinery until there's demand.

58/58 Swift tests still pass. No schema changes, no migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:56:36 +02:00
Alan Wizemann 5e207f760d docs(skill): warn authors against raw URLs in field descriptions
Pairs with the config-sheet wrap fix in d616935. Even though the
Configuration sheet now renders raw URLs correctly, markdown link
syntax reads cleaner in the form — the visible text is the label,
not the URL. Teaching this in SKILL.md prevents the scaffolding
skill from generating schemas that look worse than they could.

Additions to SKILL.md:
- New "Writing good descriptions" subsection under Config Schema
  Design. Good/bad examples side by side; rule of thumb to wrap
  long unbreakable strings (URLs, paths) in markdown links or
  inline code.
- New item in the Common Pitfalls checklist: "No raw URLs in
  field descriptions."

Bundle rebuilt, catalog.json regenerated. 24/24 Python tests
still pass; Python validator treats descriptions as opaque strings
so no validator changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:43:45 +02:00
Alan Wizemann d616935296 fix(config-sheet): wrap wide schema descriptions instead of clipping
The Configuration sheet rendered field labels chopped on the left
and description URLs spilling off the right whenever a schema
description contained a raw `https://…` URL. Root cause is layout:
SwiftUI's inline-markdown renderer turns the URL into an
unbreakable AttributedString link token, and without an explicit
maxWidth constraint on the sheet's inner VStack, width resolution
went bottom-up — the description's ideal width became the URL's
character length, the VStack matched it, the ScrollView's content
exceeded the sheet's `.frame(minWidth: 560)` viewport, the window
clipped the grown sheet, and the center-aligned result cut off
both sides.

Added `.frame(maxWidth: .infinity, alignment: .leading)` in two
places:
  - TemplateConfigSheet's inner VStack inside the ScrollView +
    the fieldRow VStack.
  - TemplateInstallSheet's main-preview VStack inside its
    ScrollView — same pattern, same failure mode for raw URLs in
    cron prompts or README blocks (the disclosure-group inner
    ScrollViews already had the modifier).

With the constraint, the description's
`.fixedSize(horizontal: false, vertical: true)` wraps at
whitespace boundaries as intended. The URL stays on its own line,
still clickable, still showing the full href. Long paths and
other unbreakable tokens render the same way.

Found while rendering a user-authored schema with two raw URLs
in descriptions. SKILL.md gets a paired update (separate commit)
teaching authors to prefer `[link text](https://…)` markdown
syntax so the visible description stays short even when the href
is long.

58/58 Swift tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:43:36 +02:00
Alan Wizemann ea4032766b feat(templates): ship awizemann/template-author skill bundle
A new .scarftemplate in the public catalog whose only content is
a Hermes skill that teaches an agent how to scaffold a new
Scarf-compatible project — dashboard, optional configuration
schema, optional cron job, AGENTS.md — from a short conversational
interview. Scaffolded projects are usable locally and cleanly
exportable as .scarftemplate bundles later.

The skill itself (~400 lines of structured markdown at
skills/scarf-template-author/SKILL.md) covers:

- When to invoke vs. when to answer inline
- The on-disk project shape Scarf expects
- A 5-question interview flow
- Full widget catalog (all 7 widget types) with JSON shapes
- Config schema design + hard invariants (no defaults on secrets,
  `contents.config` must match field count, etc.)
- Cron-job design including the {{PROJECT_DIR}} gotcha
- Step-by-step file writing (dashboard, manifest, AGENTS.md, README)
- Testing + catalog validation instructions
- Common pitfalls + source-of-truth references

Delivered as a .scarftemplate so the install flow's normal
safeguards apply: preview sheet shows one project + one skill
+ zero cron jobs + no config step, uninstall drops both the
project dir and the namespaced skill folder via the existing
lock-file mechanism.

Scope per user sign-off: blank-slate / fully conversational for
v1. Pre-baked archetypes (`monitor`, `dev-dashboard`, etc.) are
deferred to v1.1 pending real usage data on what shapes users
actually ask for.

New Swift test exercises the bundle through the installer's
plan builder — asserts manifest shape, that the skill lands at
~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md,
and that no-config templates correctly skip the manifest cache.
58/58 Swift tests pass; 24/24 Python tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 19:41:50 +02:00
Alan Wizemann 3e0d2db4c7 fix(catalog): accept git worktrees for gh-pages check
`need_ghpages` was testing `[[ -d "$GHPAGES_DIR/.git" ]]` — "is .git
a directory?". That's true for a regular clone but FALSE for a
`git worktree add` worktree, where `.git` is a pointer file (contains
`gitdir: …/main-repo/.git/worktrees/<name>`) rather than the
directory itself. `release.sh` creates the gh-pages worktree as
part of its flow; after release the worktree persists with a
`.git` file but `catalog.sh publish` would then refuse to run
because of the dir-only check.

Switched to `-e` (exists, either file or directory). Updated the
surrounding comment so the next poor soul doesn't delete the
worktree on the script's own (wrong) advice.

Caught when publishing the v2.2.0 template catalog — error told
the user to re-create a worktree that was already there and valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:37:31 +02:00
Alan Wizemann 2b25a9da71 chore: Bump version to 2.2.0 2026-04-23 18:25:18 +02:00
Alan Wizemann 5fb9620631 Merge branch 'project-sharing': v2.2.0 — templates + configuration + catalog
Brings in 22 commits delivering the full v2.2.0 scope:

- Project Templates: .scarftemplate bundle format (install, uninstall,
  export, URL router) + install preview sheet + cross-agent AGENTS.md
- Template Configuration (schemaVersion 2): typed schema with 7 field
  types, Keychain-backed secrets, Configure step in install flow,
  post-install Configuration editor, model recommendations
- Template Catalog: gh-pages site generated from templates/<author>/<name>/,
  stdlib-only Python validator mirroring Swift invariants, PR CI gate,
  install-URL hosting from raw main
- Example template: awizemann/site-status-checker (config + cron + Site
  tab webview updates)
- Site tab: webview widget in any dashboard exposes a second tab
- UX: Remove from List vs. Uninstall Template clarification, preserved-
  files banner, Run Now no longer blocks on long agent runs, markdown
  in install sheet, install-time {{PROJECT_DIR}} token substitution

Release notes at releases/v2.2.0/RELEASE_NOTES.md (94 lines).
Wiki page at https://github.com/awizemann/scarf/wiki/Project-Templates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:20:07 +02:00
Alan Wizemann 97e9beea5f refactor(settings): remove unused providers list
The hardcoded `providers` array in SettingsViewModel was never referenced — no view reads `viewModel.providers`; the Model picker uses the models.dev catalog via `ModelCatalogService.loadProviders()` and Provider is shown as a `ReadOnlyRow` in the General tab. Leaving the dead list around makes issues like #33 look plausible (users reasonably guess a stale enum is normalising `openai-codex` → `openai` on save, which the code does not actually do).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 02:37:50 +02:00
Alan Wizemann 7a99547b22 fix: address code-review findings from Apr 22 commits
Three follow-ups from reviewing 1989fee (sidebar-width persist) and
4163595 (default server on launch):

- `SplitViewAutosaveFinder` hardcoded `"ScarfMainSidebar"` for every
  window. Since Scarf's `WindowGroup` spawns one window per `ServerID`,
  all windows shared the same `NSSplitView.autosaveName` — AppKit
  documents that name as required-unique, and in practice per-window
  widths collapsed onto a single UserDefaults key. Thread the window's
  `ServerContext` in through `@Environment(\.serverContext)` (already
  wired at `WindowGroup` construction) and suffix the name with the
  server UUID.
- `setDefaultServer` fired `onEntriesChanged`, whose sole consumer is
  `ServerLiveStatusRegistry.rebuild()` for menu-bar fanout. Flipping a
  default flag doesn't change the set of servers; the callback was
  semantic noise. Drop the call — SwiftUI views still redraw on the
  flag flip via `@Observable`'s tracking of `entries`.
- The filled-yellow star in `ManageServersView` had a no-op action
  inside `if !isDefault { ... }` but still animated its pressed state
  on click. Replace the conditional with `.disabled(isDefault)` so the
  row is visually inert when it already is the default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 02:37:50 +02:00
17 changed files with 775 additions and 37 deletions
+38
View File
@@ -0,0 +1,38 @@
## What's New in 2.2.1
A patch release covering Template Configuration rendering fixes reported against v2.2.0, plus a new catalog template that packages a Hermes skill for scaffolding new Scarf projects.
### Configuration sheet — no more clipping
Two independent rendering fixes to the post-install Configuration editor and the install-flow Configure step:
- **Enum fields with long option labels.** An enum with three or four options whose labels exceeded ~20 characters — e.g. a Claude-model picker with labels like *"Claude Opus 4 (Recommended - Most Capable)"* — rendered as a segmented picker that sized to the intrinsic width of all labels concatenated. On macOS, `.pickerStyle(.segmented)` refuses to respect offered width, refuses to wrap, refuses to truncate. The result was a ~650pt picker that overflowed the sheet's 560pt viewport and clipped the entire form on both sides. Enum fields now always render as a dropdown Menu picker, which surfaces long labels in the popup list and respects the parent's offered width regardless of option count or label length.
- **Descriptions with unbreakable content.** Field descriptions rendered via inline AttributedString markdown can contain tokens SwiftUI's `Text` refuses to break mid-token (raw URLs, long paths). Added `.frame(maxWidth: .infinity, alignment: .leading)` on the sheet's inner VStack and on each field row as a secondary constraint, so description text wraps at whitespace boundaries instead of expanding the sheet width. Applied the same modifier to `TemplateInstallSheet`'s main preview VStack for symmetry — installs with README blocks or cron prompts containing long URLs now wrap cleanly too.
### New catalog entry — `awizemann/template-author`
A `.scarftemplate` whose only content is a Hermes skill (`scarf-template-author`) plus a minimal dashboard that points users at it. Installing the template drops the skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`, discoverable by Claude Code, Cursor, Codex, Aider, and every other agent that reads the standard `~/.hermes/skills/` directory.
The skill teaches agents how to scaffold a new Scarf-compatible project through a short interview — purpose, data source, cadence, widgets, config, secrets — then write `<project>/.scarf/dashboard.json`, `<project>/.scarf/manifest.json`, `<project>/AGENTS.md`, and `<project>/README.md`. Scaffolded projects are usable locally and cleanly exportable as `.scarftemplate` bundles via Scarf's Export flow later. [Catalog detail page →](https://awizemann.github.io/scarf/templates/awizemann-template-author/)
v1 is fully conversational / blank-slate. Pre-baked archetypes (monitor, dev-dashboard, personal-log) are deferred to a future release pending real usage data.
### Authoring guidance — SKILL.md
The `scarf-template-author` skill now tells scaffolding agents to prefer markdown link syntax (`[label](https://…)`) over raw URLs in schema field descriptions. Raw URLs work now (v2.2.1's description wrap fix above handles them gracefully), but `[Anthropic console](https://console.anthropic.com)` reads cleaner in the form than a dumped URL. Same rule extended to long paths or other unbreakable strings — wrap in inline code if they have to appear verbatim, prefer markdown links otherwise.
### Under the hood
- **`scripts/catalog.sh publish` fix.** The pre-flight `need_ghpages` check tested `[[ -d "$GHPAGES_DIR/.git" ]]` — "is `.git` a directory?" — which is true for a regular clone but false for a `git worktree add` worktree (where `.git` is a pointer file). `release.sh` creates and leaves the gh-pages worktree around, so after any release the subsequent catalog-publish call was rejected with a misleading "run `git worktree add`" error on a worktree that was already there and valid. Switched to `-e` (exists, either file or directory). Unblocks publishing the catalog immediately after a release.
### Migrating from 2.2.0
Sparkle will offer the update automatically. No config migration needed. Existing template installs are untouched.
If you've already installed `awizemann/template-author` from a pre-release build, no action needed — the catalog and bundle content are forward-compatible.
### Documentation
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — installing, exporting, configuring, authoring, uninstalling.
- [Catalog site](https://awizemann.github.io/scarf/templates/) — two templates live: `awizemann/site-status-checker` and `awizemann/template-author`.
- [`templates/CONTRIBUTING.md`](https://github.com/awizemann/scarf/blob/main/templates/CONTRIBUTING.md) — how to submit a template via PR.
+12 -12
View File
@@ -436,7 +436,7 @@
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO;
@@ -449,7 +449,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -471,7 +471,7 @@
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO;
@@ -484,7 +484,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -502,12 +502,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -524,12 +524,12 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -545,11 +545,11 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -565,11 +565,11 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 22;
CURRENT_PROJECT_VERSION = 24;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 2.1.0;
MARKETING_VERSION = 2.2.1;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -82,6 +82,11 @@ final class ServerRegistry {
/// Flip the default server to `id`. Passing `ServerContext.local.id`
/// clears the flag on every remote entry, making Local the implicit
/// default. Passing an unknown ID is a no-op. Persisted on return.
///
/// Intentionally doesn't fire `onEntriesChanged` that hook means "the
/// set of servers changed" and drives the menu-bar fanout rebuild. A
/// default-flag flip doesn't change the set; SwiftUI views reading
/// `defaultServerID` redraw via `@Observable`'s tracking of `entries`.
func setDefaultServer(_ id: ServerID) {
var changed = false
for idx in entries.indices {
@@ -93,7 +98,6 @@ final class ServerRegistry {
}
if changed {
save()
onEntriesChanged?()
}
}
@@ -143,19 +143,20 @@ struct ManageServersView: View {
}
/// A star button that marks the open-on-launch default. Filled + yellow
/// on the current default row (and non-interactive clicking it is a
/// no-op since the flag is already set); outline + secondary elsewhere,
/// clicking promotes that row to default.
/// on the current default row (disabled, since clicking would be a
/// no-op); outline + secondary elsewhere, clicking promotes that row
/// to default.
@ViewBuilder
private func defaultStar(for id: ServerID, currentDefault: ServerID) -> some View {
let isDefault = id == currentDefault
Button {
if !isDefault { registry.setDefaultServer(id) }
registry.setDefaultServer(id)
} label: {
Image(systemName: isDefault ? "star.fill" : "star")
.foregroundStyle(isDefault ? .yellow : .secondary)
}
.buttonStyle(.borderless)
.disabled(isDefault)
.help(isDefault ? "Opens on launch" : "Set as default — open this server when Scarf launches.")
}
@@ -20,7 +20,6 @@ final class SettingsViewModel {
var hermesRunning = false
var rawConfigYAML = ""
var personalities: [String] = []
var providers = ["anthropic", "openrouter", "nous", "openai-codex", "google-ai-studio", "xai", "ollama-cloud", "zai", "kimi-coding", "minimax"]
var terminalBackends = ["local", "docker", "singularity", "modal", "daytona", "ssh"]
var browserBackends = ["browseruse", "firecrawl", "local"]
var ttsProviders = ["edge", "elevenlabs", "openai", "minimax", "mistral", "neutts"]
@@ -23,6 +23,20 @@ struct TemplateConfigSheet: View {
header
Divider()
ScrollView {
// `.frame(maxWidth: .infinity, alignment: .leading)` is
// load-bearing: without it, SwiftUI resolves width
// bottom-up and an unbreakable token in a child (e.g. a
// raw URL inside a field description rendered via
// AttributedString markdown) sets the whole VStack's
// ideal width to that token's length. ScrollView's
// content then exceeds the sheet's viewport, the outer
// `.frame(minWidth: 560)` grows to content width, and
// the window clips the result with labels cut off on
// the left + URL spilling off the right. With the
// explicit maxWidth, the ScrollView's offered width
// propagates down and the description Text's
// `.fixedSize(horizontal: false, vertical: true)`
// wraps at whitespace boundaries as intended.
VStack(alignment: .leading, spacing: 18) {
if viewModel.schema.fields.isEmpty {
ContentUnavailableView(
@@ -40,6 +54,7 @@ struct TemplateConfigSheet: View {
modelRecommendation(rec)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(20)
}
Divider()
@@ -116,7 +131,11 @@ struct TemplateConfigSheet: View {
// Inline markdown so descriptions can include
// `[Create one](https://)`-style links to token
// generation pages, **bold** emphasis on important
// prerequisites, etc.
// prerequisites, etc. Raw URLs (not wrapped in
// markdown link syntax) will still render but can't
// word-break mid-token keep the parent maxWidth
// constraint below so a rogue raw URL wraps cleanly
// instead of expanding the entire sheet.
TemplateMarkdown.inlineText(description)
.font(.caption)
.foregroundStyle(.secondary)
@@ -129,6 +148,12 @@ struct TemplateConfigSheet: View {
.foregroundStyle(.red)
}
}
// maxWidth: .infinity forces this row to span the column's
// full width so its internal description Text wraps instead
// of expanding the outer VStack when a description contains
// a long unbreakable token (raw URL, path, etc.). See the
// comment on the parent ScrollView's inner VStack.
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(
RoundedRectangle(cornerRadius: 8)
@@ -287,24 +312,23 @@ private struct EnumControl: View {
let options: [TemplateConfigField.EnumOption]
@Binding var value: String
var body: some View {
// Segmented for 4 options, dropdown otherwise fits Scarf's
// existing settings UI.
if options.count <= 4 {
Picker("", selection: $value) {
ForEach(options) { opt in
Text(opt.label).tag(opt.value)
}
// Always use the default Menu picker (dropdown). An earlier
// version switched to `.pickerStyle(.segmented)` when
// `options.count 4` for a more compact look, but on macOS
// segmented pickers size to the intrinsic width of all their
// labels concatenated they refuse offered width constraints
// and refuse to wrap. A schema with three long labels like
// "Claude Opus 4 (Recommended - Most Capable)" produced a
// ~650pt picker that overflowed the 560pt sheet viewport,
// clipping the entire form. Menu pickers respect the fieldRow's
// offered width and show long labels in the popup list, so the
// sheet can't overflow regardless of label length.
Picker("", selection: $value) {
ForEach(options) { opt in
Text(opt.label).tag(opt.value)
}
.pickerStyle(.segmented)
.labelsHidden()
} else {
Picker("", selection: $value) {
ForEach(options) { opt in
Text(opt.label).tag(opt.value)
}
}
.labelsHidden()
}
.labelsHidden()
}
}
@@ -126,6 +126,16 @@ struct TemplateInstallSheet: View {
.padding(.bottom, 8)
Divider()
ScrollView {
// `.frame(maxWidth: .infinity, alignment: .leading)`
// without it, a subsection containing an unbreakable
// token (raw URL in a cron prompt or README block, a
// long file path in the project-files list, a schema
// description with a bare URL, etc.) sets the VStack's
// ideal width to that token's length; the sheet grows
// past its `.frame(minWidth: 620)` and gets clipped by
// the window. Same fix as `TemplateConfigSheet`'s
// inner VStack propagate the ScrollView's width down
// so inner Text wraps instead of expanding outward.
VStack(alignment: .leading, spacing: 16) {
projectFilesSection(plan: plan)
if plan.skillsNamespaceDir != nil {
@@ -142,6 +152,7 @@ struct TemplateInstallSheet: View {
}
readmeSection
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical)
}
Divider()
+2 -1
View File
@@ -2,6 +2,7 @@ import SwiftUI
struct SidebarView: View {
@Environment(AppCoordinator.self) private var coordinator
@Environment(\.serverContext) private var serverContext
var body: some View {
@Bindable var coordinator = coordinator
@@ -59,6 +60,6 @@ struct SidebarView: View {
}
.listStyle(.sidebar)
.navigationTitle("Scarf")
.splitViewAutosaveName("ScarfMainSidebar")
.splitViewAutosaveName("ScarfMainSidebar.\(serverContext.id)")
}
}
@@ -1063,6 +1063,68 @@ final class TestRegistryLock: @unchecked Sendable {
#expect(cronPrompt.contains("{{PROJECT_DIR}}"))
}
/// Exercises the second shipped template `awizemann/template-author`
/// which is a skill-only bundle (no config, no cron, no memory). The
/// shape is deliberately different from site-status-checker so a
/// regression in the installer's "no config, no cron" path can't hide
/// behind the richer example template. Also asserts the skill lands
/// under the expected namespaced path so Hermes's recursive skill
/// discovery finds it.
@Test func templateAuthorParsesAndPlans() throws {
let bundle = try Self.locateExample(author: "awizemann", name: "template-author")
let service = ProjectTemplateService(context: .local)
let inspection = try service.inspect(zipPath: bundle)
defer { service.cleanupTempDir(inspection.unpackedDir) }
// Manifest shape: schemaVersion 2 (contains `skills` claim, which
// wasn't part of v1), no config, no cron, one skill.
#expect(inspection.manifest.id == "awizemann/template-author")
#expect(inspection.manifest.name == "Scarf Template Author")
#expect(inspection.manifest.version == "1.0.0")
#expect(inspection.manifest.schemaVersion == 2)
#expect(inspection.manifest.contents.dashboard)
#expect(inspection.manifest.contents.agentsMd)
#expect(inspection.manifest.contents.cron == nil)
#expect(inspection.manifest.contents.config == nil)
#expect(inspection.manifest.contents.memory == nil)
#expect(inspection.manifest.contents.skills == ["scarf-template-author"])
#expect(inspection.manifest.config == nil)
#expect(inspection.cronJobs.isEmpty)
// Plan: empty config, empty cron, but one skill queued for install
// under the template's namespaced dir. The namespace path has to
// match what the uninstaller wipes `skills/templates/<slug>`
// or uninstall leaves orphan skill files.
let scratch = try ProjectTemplateServiceTests.makeTempDir()
defer { try? FileManager.default.removeItem(atPath: scratch) }
let plan = try service.buildPlan(inspection: inspection, parentDir: scratch)
#expect(plan.projectDir.hasSuffix("awizemann-template-author"))
#expect(plan.cronJobs.isEmpty)
#expect(plan.configSchema == nil)
#expect(plan.configValues.isEmpty)
#expect(plan.memoryAppendix == nil)
// The skill should land at
// `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`
// namespace dir + skill folder + SKILL.md. Anything else
// breaks Hermes's recursive discovery or the uninstaller's
// `rm -rf` on the namespace dir.
let namespaceDir = try #require(plan.skillsNamespaceDir)
#expect(namespaceDir.hasSuffix("/skills/templates/awizemann-template-author"))
#expect(plan.skillsFiles.count == 1)
let skillDest = try #require(plan.skillsFiles.first?.destinationPath)
#expect(skillDest.hasSuffix("/scarf-template-author/SKILL.md"))
#expect(skillDest.hasPrefix(namespaceDir))
// No-config templates deliberately skip the manifest cache
// the dashboard's Configuration button only shows up when
// `.scarf/manifest.json` exists, so a skill-only template
// like this one correctly doesn't surface that button.
// (See ProjectTemplateService.buildPlan lines 198227.)
#expect(plan.manifestCachePath == nil)
}
/// Resolve the example bundle path robustly. Unit-test working dirs
/// differ between `xcodebuild test` (project root) and an Xcode IDE
/// run (build-output dir), so we walk up from this source file until
+6 -1
View File
@@ -45,7 +45,12 @@ need_builder() {
}
need_ghpages() {
[[ -d "$GHPAGES_DIR/.git" ]] || die "no gh-pages worktree at $GHPAGES_DIR
# `.git` is a directory in a regular clone but a pointer FILE in a
# `git worktree add` worktree — `-e` covers both. The earlier `-d`
# check falsely rejected worktrees, so the script's own error
# message told users to re-run `git worktree add` on a worktree
# that was already there and valid.
[[ -e "$GHPAGES_DIR/.git" ]] || die "no gh-pages worktree at $GHPAGES_DIR
Run: git worktree add .gh-pages-worktree gh-pages"
}
@@ -0,0 +1,55 @@
# Template Author — Agent Instructions
This project is a help surface for the `scarf-template-author` Hermes skill. The same instructions apply whether you're Claude Code, Cursor, Codex, Aider, or any other agent that reads `AGENTS.md`.
## What this project is
Two things:
1. A minimal dashboard (`.scarf/dashboard.json`) the user lands on after install. It's a Quick Start text widget + an empty list widget. The list is an optional scratchpad where you can log projects you've scaffolded for the user, giving them a running audit trail. That's nice-to-have, not mandatory.
2. A skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`. The skill is the real value — it teaches you how to interview the user and scaffold a new Scarf-compatible project.
## What this project is NOT
- Not a running service. No cron jobs, no background tasks, no secrets.
- Not a dashboard you need to keep updated. The dashboard is documentation; the only mutation worth doing is appending to the Scaffolded Projects list after you scaffold something.
## When the user asks to create a Scarf project
The primary trigger. Phrases that should activate the full scaffolding flow:
- "Create a new Scarf project that …"
- "Scaffold a dashboard for …"
- "Set up a project to watch / track / report on …"
- "Help me author a Scarf template."
- "Build me a project that runs daily and …"
When you hear those:
1. Load the skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` and follow its interview flow. Do not improvise — the skill encodes the specific invariants Scarf enforces (widget types, field-type constraints, the `{{PROJECT_DIR}}` token, the paused-on-install cron rule, the secret-fields-have-no-defaults rule).
2. Scaffold into a directory the user picks. Use absolute paths.
3. After writing files, tell the user to register the project: click **+** in Scarf's Projects sidebar and pick the directory. Do not try to edit `~/.hermes/scarf/projects.json` yourself — Scarf reloads the registry on its own and the UI path is safer.
4. Optionally append to the Scaffolded Projects list in this project's `dashboard.json` so the user has a local record of what you've built for them. Preserve every other field in the dashboard as-is.
## When the user asks reference questions
If the user asks something like "what widget types does Scarf support?" or "how do I add a secret field?", you don't need to scaffold anything — answer inline. The skill's reference sections cover:
- The seven widget types (`stat`, `progress`, `text`, `table`, `chart`, `list`, `webview`) and their required fields.
- The seven config field types (`string`, `text`, `number`, `bool`, `enum`, `list`, `secret`) and their constraint keys.
- The `AGENTS.md` contract that every scaffolded project should honour.
Point them at the skill file if they want to read it directly. It's ~400 lines of structured markdown.
## What not to do
- Don't scaffold without asking the user where the project should live. The interview always asks for a parent directory.
- Don't register secrets in `<project>/.scarf/config.json`. Secret field values go through the macOS Keychain at install time; `config.json` stores `keychain://…` URIs, never plaintext. A scaffolded project that hasn't been installed yet has no secrets on disk at all.
- Don't claim dashboard widget titles the cron job doesn't actually update. The scaffolded `AGENTS.md` is a contract — if it says "the cron updates Sites Up / Sites Down", the cron prompt must match.
- Don't skip `{{PROJECT_DIR}}` token substitution in cron prompts. Hermes doesn't set a CWD for cron runs, so relative paths resolve against the agent's own dir — the installer swaps `{{PROJECT_DIR}}` for the absolute project path at install time.
## Reference
- `SKILL.md` at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` — the full scaffolding playbook.
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — user-facing docs.
- [`awizemann/site-status-checker`](https://awizemann.github.io/scarf/templates/awizemann-site-status-checker/) — a complete working example covering dashboard stats, a configurable list, a cron job, a Site-tab webview, and a full AGENTS.md contract. Read it when you're unsure how a piece should look.
@@ -0,0 +1,46 @@
# Scarf Template Author
A Hermes skill that teaches your agent how to scaffold a new Scarf project — and, because Scarf's `.scarftemplate` format is symmetric with a live project on disk, how to shape it so you can publish it to the catalog later if you want.
## What you get
Installing this template drops a skill at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md` and a minimal "how to use" project in a folder of your choice. Every agent that reads the standard `~/.hermes/skills/` directory — Claude Code, Cursor, Codex, Aider, and the rest of the [agents.md](https://agents.md/) family — picks the skill up automatically.
## How to use it
After install, open your agent in any directory and say something like:
- *"Create a new Scarf project that watches the number of open PRs in my GitHub repo."*
- *"Scaffold a Scarf dashboard that tracks daily focus time from my Toggl logs."*
- *"Set up a project that runs a cron job to summarise my inbox each morning."*
- *"Help me author a Scarf template I can share."*
The agent will ask four or five questions (purpose, data source, cadence, what to display, any secrets) and then write:
- `<your-dir>/.scarf/dashboard.json`
- `<your-dir>/.scarf/manifest.json` — only if you're going to use a configuration form or want to export later
- `<your-dir>/AGENTS.md`
- `<your-dir>/README.md`
- Optionally a cron job registered via `hermes cron create` (always created paused — you enable it from Scarf's Cron sidebar when ready).
When it's done, click **+** in Scarf's Projects sidebar and pick the directory. Your dashboard appears. Iterate on it by asking your agent to tweak widgets or add fields.
## Turning a local project into a shareable template
Once you're happy with the result, Scarf → Projects → Templates → *Export "&lt;name&gt;" as Template…* produces a `.scarftemplate` anyone can install. The exporter carries the configuration *schema* but never your filled-in values — so your secrets and personal settings stay local.
## About this template's own dashboard
The installed project itself is tiny — a single Quick Start text widget and an empty list widget meant to serve as a scratchpad for tracking which scaffolded projects you've created. Its only purpose is to give you a place to land after install and a reminder of the trigger phrases above. The real value is the skill.
## Reference
- [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) — full spec + troubleshooting.
- [`awizemann/site-status-checker`](https://awizemann.github.io/scarf/templates/awizemann-site-status-checker/) — a complete, non-trivial example the skill studies and references.
- Dashboard / configuration schemas are Swift-authoritative at `scarf/scarf/Core/Models/ProjectDashboard.swift` and `scarf/scarf/Core/Models/TemplateConfig.swift` in the Scarf repo.
## What this template intentionally is not
- Not an archetype picker. v1 is blank-slate conversational; pre-baked starters (`monitor`, `dev-dashboard`, `personal-log`, etc.) may land in v1.1 once we see what shapes people ask for most often.
- Not a graphical wizard. The conversational agent path is strictly richer than a fixed form, and dogfoods Scarf's agent-first philosophy.
- Not a remote-scaffolding tool. It writes files into a directory on the machine where the agent runs; pair with Scarf's remote-server mode if you want to scaffold onto another box.
@@ -0,0 +1,33 @@
{
"version": 1,
"title": "Template Author",
"description": "A Hermes skill that helps your agent scaffold new Scarf projects — ask in chat, answer a short interview, and land a working dashboard with the right shape to export as a .scarftemplate later. The Scaffolded Projects list below grows as you use the skill.",
"theme": { "accent": "blue" },
"sections": [
{
"title": "Quick Start",
"columns": 1,
"widgets": [
{
"type": "text",
"title": "Ask your agent",
"format": "markdown",
"content": "**This project gives you a skill, not a service.** There are no cron jobs running, no dashboards to maintain. The real value lives at `~/.hermes/skills/templates/awizemann-template-author/scarf-template-author/SKILL.md`.\n\n**Trigger phrases** your agent listens for:\n\n- *\"Create a new Scarf project that watches …\"*\n- *\"Scaffold a dashboard to track …\"*\n- *\"Set up a project that runs a daily check on …\"*\n- *\"Help me author a Scarf template.\"*\n\nThe agent will interview you (purpose → data source → cadence → widgets → config → secrets), write `<your-dir>/.scarf/dashboard.json`, `<your-dir>/.scarf/manifest.json`, `<your-dir>/AGENTS.md`, and `<your-dir>/README.md`, then tell you to click **+** in Scarf's Projects sidebar to register the directory.\n\nWhen you're happy with the result, **Projects → Templates → Export** turns it into a `.scarftemplate` you can share.\n\nSee the [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates) for the full spec."
}
]
},
{
"title": "Scaffolded Projects",
"columns": 1,
"widgets": [
{
"type": "list",
"title": "Projects this skill has built for you",
"items": [
{ "text": "Nothing yet — ask your agent to scaffold a project and it'll optionally log entries here.", "status": "pending" }
]
}
]
}
]
}
@@ -0,0 +1,409 @@
---
name: scarf-template-author
description: Scaffold a new Scarf project — dashboard, optional configuration schema, optional cron job, and AGENTS.md — from a short conversational interview with the user. Output is immediately usable locally and cleanly exportable as a .scarftemplate bundle.
version: 1.0.0
author: Alan Wizemann
license: MIT
platforms: [macos]
metadata:
hermes:
tags: [Scarf, templates, scaffolding, dashboard, authoring]
homepage: https://github.com/awizemann/scarf/wiki/Project-Templates
prerequisites:
commands: [hermes]
---
# Scarf Template Author
Scaffold a new Scarf-compatible project from a conversational interview. The output is both (a) a working project on disk the user can register with Scarf and use immediately, and (b) correctly shaped to be exported as a `.scarftemplate` bundle via Scarf's Export flow later.
## When to invoke this skill
Activate when the user says things like:
- *"Create a new Scarf project that watches / tracks / reports on …"*
- *"Scaffold a dashboard for …"*
- *"Set up a project that runs a daily check on …"*
- *"Help me author a Scarf template."*
- *"Build me a Scarf project to monitor …"*
Do **not** activate for pure reference questions like *"what widget types does Scarf support?"* or *"how does Scarf handle secrets?"* — answer those inline from the reference sections below.
Also do not activate when the user explicitly wants to edit an existing project's dashboard — that's a plain file edit, not a scaffold.
## How a Scarf project is shaped on disk
A Scarf project is just a directory registered in `~/.hermes/scarf/projects.json`. For Scarf to render a useful dashboard and for the project to be exportable as a `.scarftemplate`, it needs these files at minimum:
```
<project>/
├── .scarf/
│ ├── dashboard.json # REQUIRED for dashboard rendering
│ └── manifest.json # OPTIONAL — required only if the project declares a config schema or you want to export cleanly
├── AGENTS.md # Cross-agent instructions (agents.md standard) — ship this for every project
└── README.md # User-facing explanation
```
If the project will have a scheduled job, ALSO register a cron entry via `hermes cron create`. For an exportable bundle, also author `cron/jobs.json` in the staging directory — that's where Scarf's exporter will pick jobs up from.
Secrets never land in `dashboard.json` or `config.json`. At install time, Scarf routes secret-type config values to the macOS Keychain; `config.json` stores `keychain://service/account` URIs. When scaffolding from scratch (no install), the user either manages secrets via the post-install Configuration editor after export, or stashes them in their `~/.hermes/config.yaml` if they're Hermes-level secrets rather than project-level.
## The interview
Ask these questions in order. Don't batch. Each answer shapes the next question.
### 1. Purpose and data source
- *"In one sentence — what does this project do?"*
- *"Where does its data come from? Files, a URL, a shell command's output, an API call, a database, a spreadsheet?"*
Goal: figure out whether the project is **passive** (user maintains some files, dashboard reflects them), **pull-based** (we fetch from an HTTP endpoint or CLI tool on a schedule), or **push-based** (something external writes to a file we watch).
### 2. Refresh cadence
- *"How often should it refresh? Every hour? Daily? Weekly? Only when I ask?"*
If "only when I ask" → no cron job; user invokes the agent manually. If any scheduled cadence → cron job.
Map to cron expressions:
- Every hour: `0 * * * *`
- Daily at 9 AM: `0 9 * * *`
- Weekly Monday 9 AM: `0 9 * * 1`
- Every 15 minutes: `*/15 * * * *`
### 3. What the dashboard shows
Explain the seven widget types (see Widget Catalog below) in plain English, then ask which ones feel right. Offer concrete suggestions based on the purpose:
- Counting things (open PRs, failing tests, up/down sites) → `stat` widgets.
- A list of items with status → `list` with `text` + `status` per item.
- Time-series data → `chart` with `line` or `bar` type.
- Rows × columns of heterogeneous data → `table`.
- A live URL (useful for monitoring a site) → `webview`. **Including a webview widget exposes a Site tab** next to the Dashboard tab — worth noting to the user.
- A progress bar for something with a clear 0-to-N scale → `progress`.
- Static help / markdown → `text` with `format: "markdown"`.
### 4. Configuration needs
- *"Does this project need anything configurable by the user — URLs to watch, API tokens, thresholds, a list of accounts?"*
If yes → design a config schema. Fields map to seven types (see Config Schema Design below). Remember: **secret fields never have defaults**; that's a hard validator rule.
If no → skip `.scarf/manifest.json`; the project works but won't have a Configuration form.
### 5. Target agents
- *"Which agents will operate this project? Just Claude Code? Also Cursor / Codex / Aider / other?"*
For v1 just write `AGENTS.md` — every modern agent reads it, and if you need a specific shim (CLAUDE.md, GEMINI.md, .cursorrules), add it as a symlink to AGENTS.md so content stays in sync.
## Widget Catalog (JSON shapes)
All widgets require `type` and `title`. Type-specific fields:
### `stat` — single metric
```json
{ "type": "stat", "title": "Sites Up", "value": 0,
"icon": "checkmark.circle.fill", "color": "green", "subtitle": "responded 2xx/3xx" }
```
`value` accepts number OR string (`WidgetValue` enum). `icon` is an SF Symbol name. `color` is one of: `green`, `red`, `blue`, `orange`, `yellow`, `purple`, `gray`.
### `progress` — 0.0 to 1.0 progress bar
```json
{ "type": "progress", "title": "Test Coverage", "value": 0.72, "label": "72% of statements" }
```
### `text` — markdown or plain text block
```json
{ "type": "text", "title": "Quick Start", "format": "markdown",
"content": "**1.** Click + in the Projects sidebar.\n\n**2.** ..." }
```
`format` is `"markdown"` or `"plain"`.
### `table` — columns × rows of strings
```json
{ "type": "table", "title": "Failing Tests",
"columns": ["Test", "Duration", "Last Passed"],
"rows": [["testFoo", "4.2s", "Apr 20"], ["testBar", "0.9s", "Apr 18"]] }
```
Every row MUST have the same length as `columns`.
### `chart` — line / bar / area / pie with series
```json
{ "type": "chart", "title": "Requests / day", "chartType": "line",
"xLabel": "Date", "yLabel": "Count",
"series": [{
"name": "staging",
"color": "blue",
"data": [{"x": "Apr 20", "y": 142}, {"x": "Apr 21", "y": 189}]
}]
}
```
`chartType` is `"line"`, `"bar"`, `"area"`, or `"pie"`.
### `list` — items with optional status badge
```json
{ "type": "list", "title": "Watched Sites",
"items": [
{ "text": "https://example.com", "status": "up" },
{ "text": "https://example.org", "status": "down" }
]
}
```
`status` values: `"up"`, `"down"`, `"pending"`, `"ok"`, `"warn"`, `"error"` — render as coloured badges.
### `webview` — embedded live URL
```json
{ "type": "webview", "title": "First Watched Site",
"url": "https://awizemann.github.io/scarf/", "height": 420 }
```
**Important:** including any `webview` widget in a dashboard exposes a **Site** tab next to the Dashboard tab in the project view. Useful for templates that watch something renderable. The agent can update `url` on cron runs to keep the Site tab in sync with config (e.g., set it to `values.sites[0]`).
## Config Schema Design
If the project needs user-configurable values, design a schema. Put it in `<project>/.scarf/manifest.json` with this shape:
```json
{
"schemaVersion": 2,
"id": "author/project",
"name": "My Project",
"version": "1.0.0",
"description": "Short one-liner.",
"contents": { "dashboard": true, "agentsMd": true, "config": 2 },
"config": {
"schema": [
{ "key": "sites", "type": "list", "itemType": "string", "label": "Sites",
"required": true, "minItems": 1, "maxItems": 25,
"default": ["https://example.com"] },
{ "key": "api_token", "type": "secret", "label": "API Token", "required": true }
],
"modelRecommendation": {
"preferred": "claude-haiku-4",
"rationale": "Short-running, tool-light workload — haiku is plenty."
}
}
}
```
Note: `contents.config` is the **count of schema fields**, not a boolean. In the example above it's `2` because there are two fields.
### Field types and constraints
| Type | Rendered as | Constraint keys |
|---|---|---|
| `string` | Text field | `pattern` (regex), `minLength`, `maxLength` |
| `text` | Multi-line editor | `minLength`, `maxLength` |
| `number` | Number field | `min`, `max` |
| `bool` | Toggle | — |
| `enum` | Segmented (≤4) / Dropdown (>4) | `options: [{value, label}]` (REQUIRED) |
| `list` | Repeatable rows | `itemType: "string"` (required), `minItems`, `maxItems` |
| `secret` | Password field, routes to Keychain | — |
Every field takes `key` (required), `label` (required), `description` (optional — markdown), `required` (bool), `default` (optional; type matches the field type).
### Writing good descriptions
Descriptions render inline with markdown support (bold, italic, code, links). Keep them short — a single line or two is ideal.
**Always use markdown link syntax for URLs**, never bare `https://…` — the Configuration sheet's inline text renderer doesn't word-break mid-URL, so a raw URL in a description will force that whole description's width to the URL's character length. Older Scarf versions clipped the sheet in that case; current versions wrap correctly, but the visible text is still cleaner with named links.
```json
// ✓ Good — short label, URL in the href
"description": "Token with `repo` scope. Get one [from the GitHub tokens page](https://github.com/settings/tokens)."
// ✗ Bad — raw URL bloats the visible text
"description": "Token with `repo` scope. Get one at https://github.com/settings/tokens"
```
Same rule for long file paths, API endpoints, or any other unbreakable token — wrap them in inline code (backticks) if they have to appear verbatim, and prefer markdown links otherwise.
### Hard rules
- **Secret fields MUST NOT have a `default`.** The validator rejects the manifest if they do — a default makes no sense because the Keychain entry doesn't exist yet at install time.
- **Enum fields MUST have non-empty `options`.**
- **List fields MUST have `itemType: "string"`** in v1 (only itemType supported).
- **Field keys MUST be unique** within a schema.
- **`schemaVersion` MUST be 2** when a `config` block is present; it stays 1 if there's no config.
- **`contents.config`** must equal the actual count of schema fields — a claim mismatch is rejected.
## Cron Job Design
If the project has a scheduled task, register a cron job via `hermes cron create` AND — if you expect the user to export this as a `.scarftemplate` — author a `cron/jobs.json` in the staging layout so the exporter picks it up.
### Staging shape (for exportable templates)
```
<project>/
├── .scarf/
├── AGENTS.md
├── README.md
└── cron/
└── jobs.json
```
Where `cron/jobs.json` is:
```json
[
{
"name": "Check site status",
"schedule": "0 9 * * *",
"prompt": "Read {{PROJECT_DIR}}/.scarf/config.json — get values.sites and values.timeout_seconds — then HTTP GET each URL with that timeout, write the results to {{PROJECT_DIR}}/status-log.md, and update {{PROJECT_DIR}}/.scarf/dashboard.json's stat widgets by title (Sites Up, Sites Down, Last Checked). Reply with a one-line summary."
}
]
```
### Gotchas
- **Hermes does not set a CWD when firing cron jobs.** Relative paths in the prompt resolve against wherever the Hermes process happens to be running, not the project. Always use `{{PROJECT_DIR}}` in the prompt — the installer substitutes the absolute path at install time. This is THE most common template-author mistake.
- **Cron jobs created by the installer start paused.** Their name is auto-prefixed with `[tmpl:<template-id>]`. The user enables them from Scarf's Cron sidebar when ready.
- **Registering a cron job for a user's local (non-exported) project:** run `hermes cron create --name "<descriptive name>" "<schedule>" "<prompt>"` directly, substituting the absolute `<project>` path for `{{PROJECT_DIR}}` yourself. Then `hermes cron pause <id>` so it doesn't run until the user opts in.
### Schedule quick reference
| Cadence | Expression |
|---|---|
| Every 15 minutes | `*/15 * * * *` |
| Hourly at :00 | `0 * * * *` |
| Daily at 9 AM | `0 9 * * *` |
| Weekly Monday 9 AM | `0 9 * * 1` |
| First of the month, 9 AM | `0 9 1 * *` |
## Writing the files
After the interview, write files in this order.
### Step 1 — confirm parent directory
Ask: *"Where should I create the project? Give me an absolute path — I'll make a `<project-name>` directory inside it."*
Make sure the parent exists and is writable. Make sure `<parent>/<project-name>` does NOT already exist. If it does, ask whether to pick a different name or bail.
### Step 2 — create the skeleton
```bash
mkdir -p <parent>/<project-name>/.scarf
```
### Step 3 — write `dashboard.json`
Use the Widget Catalog above. Always include:
- `version: 1`
- `title` (the project's display name)
- `description` (a one-liner shown under the title)
- `sections` (array; each has `title`, optional `columns` (14, default 3), `widgets`)
Keep section titles short. Group related widgets. First section is usually "Current Status" or similar with the key stats.
### Step 4 — write `manifest.json` (only if the project has a config schema)
Put the full manifest shape from Config Schema Design above. Use `schemaVersion: 2`, match `contents.config` to the actual field count, and ensure every secret field has no `default`.
If there's no config schema, skip this file — the project still works, it just won't have a Configuration button. You can add it later.
### Step 5 — write `AGENTS.md`
Every scaffolded project needs an `AGENTS.md` that covers:
- **Purpose** — what the project does.
- **Layout** — which files exist and what they're for.
- **Configuration** — if there's a config schema, document every field: what it's for, what valid values look like, what happens when it's missing.
- **Dashboard** — list every widget the cron job (if any) updates, by title. If the cron updates a webview widget's URL, document that explicitly.
- **Cron behaviour** — what the cron job does, what it reads, what it writes, what its exit criteria are.
- **Chat prompts** — common user questions and how to answer them (e.g., *"What's the status of my sites?"* → "read the top section of `status-log.md` and summarise").
- **What NOT to do** — e.g., *don't modify `.scarf/config.json` yourself; tell the user to open the Configuration button.*
Use `{{PROJECT_DIR}}` placeholders in AGENTS.md only if the template will be installed through the installer (which substitutes the token). For a hand-scaffolded local-only project, substitute the absolute path yourself — `{{PROJECT_DIR}}` only resolves at install time.
### Step 6 — write `README.md`
User-facing. Keep it short:
- One-paragraph purpose.
- How to install / first run (for an unexported project: "click + in Scarf's Projects sidebar").
- How to trigger the cron job manually (Cron sidebar → Run Now).
- A pointer at `AGENTS.md` for agents.
### Step 7 — register the cron job (if any)
For a local non-exported project:
```bash
hermes cron create --name "<descriptive name>" "<schedule>" "<prompt with absolute project dir substituted>"
# Then pause it so it doesn't fire until the user's ready:
hermes cron pause <newly-created-job-id>
```
Read the id back from `hermes cron list --json` or parse the create output.
For an exportable template (one you're staging in `templates/<author>/<name>/staging/`): just author `cron/jobs.json` — the installer registers + pauses at install time, and prefixes the name with `[tmpl:<id>]`.
### Step 8 — register the project with Scarf
Tell the user: *"I've written the files. Click the **+** button in Scarf's Projects sidebar and pick `<absolute-project-dir>`. The dashboard will appear."*
Do NOT edit `~/.hermes/scarf/projects.json` directly — Scarf owns that file and reloads it on its own. The UI path is safer.
### Step 9 (optional) — log to the Template Author project's list
If the user has the `awizemann/template-author` project installed (the one that shipped this skill), append an entry to its `dashboard.json`'s `Scaffolded Projects` list widget:
```json
{ "text": "<absolute-project-dir> — <one-line purpose>", "status": "ok" }
```
This gives the user a running audit trail of everything you've scaffolded for them. Preserve every other field in the dashboard as-is.
## Testing your scaffold
### Minimum smoke test
1. Tell the user to click **+** in Scarf's Projects sidebar and pick the directory.
2. Dashboard appears — sanity check every widget renders correctly.
3. If there's a cron job: click the job in Scarf's Cron sidebar → **Run Now**. The agent executes the prompt; dashboard updates when it finishes.
### Configuration-form test (only if schema was declared)
To verify the Configuration form renders, you need to *install* the project as a template — scaffolded projects don't go through the installer, so the form never runs. Export the project first:
1. Projects → Templates → **Export "&lt;name&gt;" as Template…** → save the `.scarftemplate` somewhere.
2. Projects → Templates → **Install from File…** → pick the bundle → the Configure step should render the form you designed.
3. Cancel the install (the preview sheet has a Cancel button) — you just wanted to verify the form shape.
### Catalog validation (only if publishing)
If the user plans to submit this to the public catalog at `awizemann.github.io/scarf/templates/`:
```bash
# From the repo root
./scripts/catalog.sh check
```
Validates every template in `templates/<author>/<name>/` against the Python validator — the same one the PR CI uses. Catches schema issues, claim mismatches, size violations, common secret patterns.
## Common pitfalls
Things to check before declaring the scaffold done:
- [ ] Every cron prompt uses `{{PROJECT_DIR}}` (for exported) OR an absolute path (for local-only). Relative paths will fail.
- [ ] `contents.config` in the manifest equals the actual field count. Claim mismatch = rejected.
- [ ] No `default` on any `secret` field.
- [ ] Every enum field has non-empty `options`.
- [ ] Every list field has `itemType: "string"`.
- [ ] Every table widget has rows of length equal to `columns`.
- [ ] Every webview widget has an https URL that renders something meaningful even pre-first-run (Scarf homepage is a decent placeholder).
- [ ] `dashboard.json` has `version: 1` at the top.
- [ ] `AGENTS.md` documents every config field, every updated widget, and the cron behaviour — the user relies on it as the source of truth when things drift.
- [ ] **No raw URLs in field descriptions.** Use `[link text](https://…)` markdown syntax instead — raw URLs read as long unbreakable tokens in the Configuration sheet. Same rule for long paths and other unbreakable strings; wrap in `` ` `` if they must appear verbatim.
## Reference — source of truth files
- **Dashboard widget schema**`scarf/scarf/Core/Models/ProjectDashboard.swift` in the Scarf repo. If you need exact field types or defaults, read it.
- **Config schema + validation**`scarf/scarf/Core/Models/TemplateConfig.swift` and `scarf/scarf/Core/Services/ProjectConfigService.swift`.
- **Exporter behaviour**`scarf/scarf/Core/Services/ProjectTemplateExporter.swift`. Verifies what files the exporter will pick up from a live project and what it'll carry into a bundle.
- **Installer contract**`scarf/scarf/Core/Services/ProjectTemplateInstaller.swift`. Verifies what `{{PROJECT_DIR}}` substitution covers and where installed files land.
- **Catalog validator**`tools/build-catalog.py` in the Scarf repo. Run with `./scripts/catalog.sh check` for the same rules CI uses.
- **Worked example**`templates/awizemann/site-status-checker/staging/` in the Scarf repo. Complete end-to-end: dashboard with stats + list + webview, a config schema with a list + a number, a cron job, an AGENTS.md that documents every moving part. Read it first whenever you're unsure how a piece should look.
- **User-facing docs** — [Project Templates wiki page](https://github.com/awizemann/scarf/wiki/Project-Templates).
@@ -0,0 +1,19 @@
{
"schemaVersion": 2,
"id": "awizemann/template-author",
"name": "Scarf Template Author",
"version": "1.0.0",
"description": "Install this to give your agent a skill that scaffolds new Scarf projects — dashboards, optional configuration schemas, cron jobs, and AGENTS.md — from a short conversational interview. Scaffolded projects are usable locally and cleanly exportable as .scarftemplate bundles.",
"minScarfVersion": "2.2.0",
"author": {
"name": "Alan Wizemann",
"url": "https://github.com/awizemann"
},
"category": "developer-tools",
"tags": ["meta", "authoring", "skill", "scaffolding"],
"contents": {
"dashboard": true,
"agentsMd": true,
"skills": ["scarf-template-author"]
}
}
+31
View File
@@ -63,6 +63,37 @@
"configurable"
],
"version": "1.1.0"
},
{
"author": {
"name": "Alan Wizemann",
"url": "https://github.com/awizemann"
},
"bundleSha256": "bebc30551dc92717da96608bbdf448c5d7c47bdb66807037b139a242ef8c3b74",
"bundleSize": 14423,
"category": "developer-tools",
"config": null,
"contents": {
"agentsMd": true,
"dashboard": true,
"skills": [
"scarf-template-author"
]
},
"description": "Install this to give your agent a skill that scaffolds new Scarf projects \u2014 dashboards, optional configuration schemas, cron jobs, and AGENTS.md \u2014 from a short conversational interview. Scaffolded projects are usable locally and cleanly exportable as .scarftemplate bundles.",
"detailSlug": "awizemann-template-author",
"id": "awizemann/template-author",
"installUrl": "https://raw.githubusercontent.com/awizemann/scarf/main/templates/awizemann/template-author/template-author.scarftemplate",
"minHermesVersion": null,
"minScarfVersion": "2.2.0",
"name": "Scarf Template Author",
"tags": [
"meta",
"authoring",
"skill",
"scaffolding"
],
"version": "1.0.0"
}
]
}