mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
ec506d465203806b4e4a8617f2a7b3ace50b5db1
170 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ec506d4652 |
docs(v2.3): add Tool Gateway + Nous Portal sign-in to release notes + README
v2.3 now lands two themes together: Projects Grow Up (existing) and Hermes v0.10.0 Tool Gateway support (new, just merged on the feature branch). The release notes and the repo README's "What's New" section are updated to reflect both. Release notes: - Headline intro rewritten to frame both themes as the v2.3 story. - New "Tool Gateway — Nous Portal support" section between "Icon tweak" and "Migrating from 2.2.x": picker overlay merge surfacing 6 previously-invisible providers, in-app device-code sign-in sheet, per-task Nous routing in the Auxiliary tab, Health card, Credential Pools dead-end fix + auth-type gating, Messaging Gateway rename. - "Under the hood" gains the Tool Gateway services paragraph (NousSubscriptionService, NousAuthFlow, NousSignInSheet, CredentialPoolsOAuthGate) + the PYTHONUNBUFFERED=1 subprocess-env fix note. Test count bumped from 93 → 120 (14 new tests in ToolGatewayTests, NousAuthFlowParserTests, CredentialPoolsGatingTests). - "Migrating from 2.2.x" gains a Hermes version paragraph spelling out that v0.10.0 is required for the Tool Gateway features (rest of 2.3 works on earlier Hermes, just without Nous in the picker or subscription data in Health). - "Documentation" section lists the new Hermes Version Compatibility + Core Services wiki updates that accompany this release. README: - v2.3 "What's New" bullet list gains a Tool Gateway bullet positioned between the chat-indicator bullet and the window-layout bullet. - Trailing "See the full release notes" line expanded to reference the Hermes Version Compatibility wiki page so users on Hermes v0.9 know why they don't see Nous in their picker. Companion wiki update already pushed in 741b253 on the wiki repo (Hermes-Version-Compatibility, Core-Services, Home). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
38226fea2c |
fix(nous): force PYTHONUNBUFFERED=1 so device-code output surfaces
The sign-in sheet was stuck on the "Contacting Nous Portal…" spinner
even though hermes was running correctly. Root cause: Python
block-buffers stdout when it's a pipe instead of a TTY, and
`hermes auth add nous` enters a 15-minute polling loop after printing
the device-code block without ever calling `input()` — so nothing
flushes the buffer. Our readability handler never receives the URL +
user_code lines.
PKCE doesn't hit this because hermes calls `input("Authorization
code: ")`, which flushes stdout before blocking. Device-code has no
equivalent trigger.
Setting PYTHONUNBUFFERED=1 in the subprocess environment forces
line-buffered stdout for the duration of the flow — the device-code
block surfaces immediately, our regex extracts the URL and code, and
the sheet transitions into the waitingForApproval state as intended.
Local-only fix; remote SSH contexts get the remote's login env
untouched (the user's remote shell config owns buffering behavior
there).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
257772e2d1 |
feat(nous): in-app sign-in + credential pools auth-type gating
The Tool Gateway feature shipped the Nous Portal provider in Scarf's
picker, a subscription-state detector, and a per-task aux toggle — but
there was no way to actually sign in. `hermes auth` in a terminal took
six steps, and Credential Pools' "Start OAuth" button silently stalled
for `nous` because it tried to run the PKCE flow against a device-code
provider.
Changes:
- NousAuthFlow: new @Observable MainActor service that spawns
`hermes auth add nous --no-browser`, parses the device-code block
(verification_uri_complete + user_code) with two line-anchored
regexes, opens the verification URL via NSWorkspace.shared.open,
and confirms success by re-reading auth.json via
NousSubscriptionService. Detects the `subscription_required`
failure and extracts the billing URL so the UI can offer a
Subscribe link.
- NousSignInSheet: four-state sheet (starting / waitingForApproval /
success / failure). Shows the user code in a large monospaced
badge with Copy + re-open-browser affordances, auto-dismisses
1.2s after success, Subscribe + Try again + Copy error buttons
on failure.
- Wired three entry points (per user-approved plan):
1. ModelPickerSheet's Nous Portal subscription summary — replaces
the stale "Run hermes auth" caption with a primary
"Sign in to Nous Portal" button.
2. AuxiliaryTab's per-task Nous toggle — inline "Sign in first"
button when not subscribed, instead of a dead-end caption.
3. Credential Pools "Add Credential" sheet — when provider is
`nous`, replaces the broken Start OAuth button with
"Sign in to Nous Portal".
- CredentialPoolsOAuthGate: testable helper that routes provider IDs
to the right OAuth flow based on the overlay table. Closes the
silent-fail dead-end for openai-codex, qwen-oauth,
google-gemini-cli, and copilot-acp too — disables the generic
button with an inline "run hermes auth add <provider> in a
terminal" hint. PKCE providers (anthropic, etc.) and unknown
providers still pass through as `.ok` — this gate is strictly
additive.
Tests: 14 new tests across two suites (NousAuthFlowParserTests,
CredentialPoolsGatingTests). Full suite 120/120 green on top of
v2.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
115bc16b14 |
feat: Nous Portal + Tool Gateway support for Hermes v0.10.0
Hermes v0.10.0 (v2026.4.16) introduces the Tool Gateway — paid Nous Portal subscribers route web search, image generation, TTS, and browser automation through their subscription without separate API keys. - ModelCatalogService merges HERMES_OVERLAYS on top of the models.dev cache, surfacing 6 overlay-only providers (Nous Portal, OpenAI Codex, Qwen OAuth, Google Gemini CLI, GitHub Copilot ACP, Arcee) that were previously invisible in Scarf's picker. Subscription-gated providers sort first. - NousSubscriptionService reads ~/.hermes/auth.json -> providers.nous to detect subscription state. Read-only; Hermes owns the write path. - ModelPickerSheet renders a "Subscription" pill, auth-type-aware instructions, and free-form model-ID entry for overlay providers (no models.dev catalog for them). - AuxiliaryTab gains a per-task "Nous Portal" toggle that flips auxiliary.<task>.provider between "nous" and "auto". Hermes derives gateway routing from provider selection; there's no separate use_gateway key in the source. - HermesConfig + HermesFileService parse platform_toolsets. - HealthViewModel adds a synthetic "Tool Gateway" section showing subscription state, platform_toolsets, and which aux tasks are routed through Nous. - Gateway -> Messaging Gateway rename (sidebar, dashboard card, menu bar, log-source filter, Settings/Agent/Gateway section header) to disambiguate from the new Tool Gateway. - CLAUDE.md bumped to Hermes v0.10.0 (v2026.4.16) with a keep-overlayOnlyProviders-in-sync reminder. - 13 new tests covering overlay merge, subscription detection, and platform_toolsets parsing; full suite (106 tests, 19 suites) green on top of v2.3 projects branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eda5e467f9 |
Merge branch 'v2.3-projects': v2.3 — Projects Grow Up
Brings in 17 commits delivering the full v2.3 scope: - Projects sidebar hierarchy: folders, rename, archive/unarchive, fuzzy search (⌘F), ⌘1–⌘9 keyboard jumps. Registry schema v2 (optional folder + archived fields); backward-compatible with v2.2.1 readers. - Per-project Sessions tab alongside Dashboard / Site. "New Chat" spawns hermes acp with the project's directory as cwd and attributes the resulting session via a Scarf-owned sidecar at ~/.hermes/scarf/session_project_map.json (Hermes's state.db has no cwd column, so Scarf owns the mapping). - Agent context injection: ProjectAgentContextService writes a Scarf-managed block into <project>/AGENTS.md between <!-- scarf-project:begin/end --> markers. Hermes auto-reads AGENTS.md at session boot, so the agent now actually knows the project name, dashboard path, template id, configuration field NAMES (secret-safe — never values), registered cron jobs, and uninstall-manifest presence. Template-author content outside the markers is preserved byte-identical across refreshes. - Chat indicator: folder chip in SessionInfoBar + "Chat · <ProjectName>" nav title when scoped. Resumed project- attributed sessions automatically re-surface the indicator via the attribution lookup at resume time. - Window-layout cleanup: .windowResizability(.contentMinSize) + idealHeight caps on Chat/Sessions subtrees so the window stops growing past the screen when switching to content-heavy sections. Pre-existing issue surfaced by the new per-project surfaces. 22 new Swift tests across ProjectRegistryMigrationTests (7), ProjectsViewModelTests (7), SessionAttributionServiceTests (7), and ProjectAgentContextServiceTests (13) — total suite size is now 93/93. Release notes at releases/v2.3.0/RELEASE_NOTES.md (9.4 KB). README "What's New in 2.3" block prepended; prior v2.2 block demoted to "Previously, in 2.2." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9127aef682 |
docs: v2.3.0 release notes + README What's New
Prep commit for the v2.3 release. Covers the 16 feature + fix commits landed on the v2.3-projects branch: - releases/v2.3.0/RELEASE_NOTES.md — new file. release.sh picks this up automatically as the GitHub release body at tag time. Sections: sidebar grows up (folders/rename/archive/search/ keyboard jumps), per-project Sessions tab + sidecar, the AGENTS.md marker-block injection (with the invariants — secret-safe, idempotent, bounded, non-fatal, bare-project friendly — called out explicitly), chat-UI project awareness (folder chip + nav title), window-layout cleanup, under-the- hood (new services, 22 new tests), migration, thanks. - README.md — "What's New in 2.3" block at the top; demotes the prior 2.2 block to "Previously, in 2.2" (condensed to the four most user-facing points since the full 2.2 notes live at the release link). - Localizable.xcstrings — Xcode auto-regen from the new string literals introduced across the v2.3 feature commits (folder chip tooltip, Sessions tab header, etc.). Riding along. 93/93 Swift tests still pass. No code change here — pure docs. Wiki Home + Release-Notes-Index updates land as a separate wiki commit after the release is cut (standard post-release chore per CLAUDE.md). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5ae8db25c3 |
fix(chat): resume session on coordinator.selectedSessionId, not just pendingProjectChat
Clicking a session in the Projects Sessions tab routed to the Chat section (correct — we want interactive resume, not the read-only Sessions browser), but the session didn't actually load and the project chip didn't appear. Root cause: ChatView only observed `coordinator.pendingProjectChat` (for new chats), not `selectedSessionId` (for resumes). Setting the id had no effect because no consumer existed on the Chat side. Every other session-click site in Scarf routes to `.sessions`, and SessionsView consumes selectedSessionId at its `.task` + clears it. Projects is the exception — the whole point of the per-project Sessions tab is to resume chats interactively rather than browse them, so we route to `.chat`. That routing was right; the Chat side just needed to grow the symmetrical consumer. This commit adds two handoff paths in ChatView (mirrors the existing `pendingProjectChat` pattern): - `.task` picks up a selectedSessionId that was set before ChatView mounted (cold-launch handoff from Projects). - `.onChange(of: coord.selectedSessionId)` picks up mid-session navigation (user clicks a session while already in Chat). Both call `viewModel.resumeSession(id)` then clear the coordinator field. The project chip rendering + navTitle update then happen automatically inside ChatViewModel.resumeSession -> startACPSession, which already looks up attribution via SessionAttributionService.projectPath(for: resolvedSessionId) — that plumbing was in from Part B. The bug was entirely in the trigger, not the side-effect. `else if` between pendingProjectChat and selectedSessionId makes precedence explicit — new-chat wins over resume if both are somehow set. In practice only one is ever populated per navigation, but the explicit ordering avoids surprise. No race with SessionsView's own consumer: `coordinator.selectedSection` ensures only one view is rendering at a time, and both consumers clear the field on consume. 93/93 Swift tests still pass. No test change — this is a view- wiring integration fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb833d4a0a |
fix(projects): open HermesDataService before filtering sessions
Sessions tab was showing "This project has N attributed sessions,
but none are in the recent history. They may have been deleted
from Hermes." on projects with valid sidecar entries and actual
sessions present in state.db. Root cause: the VM never opened
the DB handle.
`HermesDataService` is an actor with a lazily-initialised SQLite
pointer. Every query method short-circuits to `[]` when
`db == nil`. Callers have to open/refresh the handle explicitly
— InsightsViewModel does it (line 106), ActivityViewModel does
it (line 60). ProjectSessionsViewModel was constructed fresh
per project, never inherited a shared service, and never called
refresh() itself, so fetchSessions returned empty on every load
and the filter against the (correctly-populated) sidecar map
produced zero matches. The empty-state message ("may have been
deleted") fired on that false-negative.
The data was fine all along: sqlite3 ~/.hermes/state.db confirmed
both attributed sessions with source='acp', parent_session_id
IS NULL — they pass fetchSessions's WHERE clause cleanly. The
sidecar mappings were correct. The file watcher was firing. The
only missing piece was the DB-open precondition.
Fix: `_ = await dataService.refresh()` before fetchSessions,
mirroring the pattern used by every other feature VM that
consumes HermesDataService. Also adds a `close()` on the VM + an
onDisappear handler on the view, so the handle doesn't dangle
once the tab isn't visible — same cleanup ActivityView has.
This is NOT forward-only. Existing sidecar entries that
currently show the misleading empty-state will surface
correctly as soon as users rebuild — no data migration, no
re-create-the-chat, no backfill. The bug was "couldn't read what
was already there," not "lost old data."
93/93 Swift tests still pass. No test change — the fix is an
integration-level call-ordering detail that isn't meaningfully
testable without mocking HermesDataService (overkill for a
two-line fix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7656ad8052 |
docs(v2.3): document how agents see Scarf projects
Three doc updates covering the AGENTS.md context-injection pattern introduced in the previous commit. CLAUDE.md — new "Project-scoped chat + Scarf-managed AGENTS.md context (v2.3)" subsection under Project Templates. Covers: - The session-project sidecar at ~/.hermes/scarf/session_project_map.json (why it exists, what manages it) - How Hermes picks up project context: cwd-based auto-load of the first matching context file (priority order, 20KB cap) - Exact marker format and block shape - Invariants that future edits must preserve: secret-safe, idempotent, bounded-region, non-fatal, refresh-before-session-start ordering - Template-author contract: leave the region alone, put instructions below - Known caveat: parent-directory `.hermes.md` shadowing (deferred to v2.4) scarf-template-author SKILL.md — new pitfall bullet in the "Common pitfalls" checklist telling scaffolding agents to preserve the `<!-- scarf-project -->` region and put template- specific instructions below it. Rebuilt the bundle so installs from the catalog pick up the guidance; regenerated catalog.json. Wiki update (Project-Templates page) lands next via scripts/wiki.sh. 93/93 Swift + 24/24 Python tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5b1481f33f |
feat(projects): Scarf-managed project-context block in AGENTS.md
Hermes has no native "project" concept and the ACP wire protocol drops extra params at `session/new`. But Hermes DOES auto-read AGENTS.md from the session's cwd at startup (research confirmed: priority order `.hermes.md` → `HERMES.md` → AGENTS.md → CLAUDE.md → .cursorrules; 20KB cap; first match wins). So the agent- awareness path is file-based, not protocol-based. This commit adds `ProjectAgentContextService` — a one-job service that writes a Scarf-managed block into `<project>/AGENTS.md` between `<!-- scarf-project:begin -->` and `<!-- scarf-project:end -->` markers. Same pattern as the v2.2 memory-block appendix: bounded, self-declaring, re-generable, safe on hand-authored content outside the markers. ## Block contents - Project name (from registry) - Project directory path - Dashboard.json path - Template id + version (when template-installed) - Configuration field NAMES with type hints — never VALUES. Secrets always render as `field_key (secret — name only, value stored in Keychain)`. Config.json values never appear in the block, so the injected context is safe to drop into any agent regardless of what's in Keychain. - Registered cron jobs attributed to this project (matched via the `[tmpl:<id>] …` prefix convention) - Uninstall manifest reference (when `.scarf/template.lock.json` exists) - A note to the agent: cwd is the project dir, respect template content below the block. ## Integration point `ChatViewModel.startACPSession(resume:projectPath:)` refreshes the block BEFORE `client.start()` — Hermes reads AGENTS.md during session boot, so it has to land on disk first. `try?` with a warning log: a failed refresh doesn't block the chat, the session just starts without the extra context. ## Idempotency + safety - Two consecutive refreshes produce byte-identical output - Hand-edits outside the markers survive every refresh - Empty project dir → AGENTS.md created with just the block - Existing AGENTS.md without markers → block prepended; rest preserved below - Orphaned begin-marker (no end) → treated as "no block present," new block prepended, orphan left in place (likely hand-typed, not a Scarf corruption) ## Tests 13 new tests in ProjectAgentContextServiceTests: - applyBlock pure-text transform: prepend / replace / idempotency / empty input / orphaned-marker fallback - renderBlock content: identity fields, template presence, config field names (and CRITICALLY: no values leak for secret fields) - refresh end-to-end on isolated temp dirs: file creation, user content preservation, idempotency across runs, stale-block rewrite 93/93 Swift tests pass (was 80; +13 new). ## Deferred TERMINAL_CWD env-var plumbing in ACPClient was scoped in the plan but skipped — ACPClient.start() doesn't know the cwd at launch (it's per-session), and plumbing it would restructure the actor's lifecycle. Hermes already receives the cwd via ACP's `session/new` params and uses it for context-file discovery there, so TERMINAL_CWD is belt-and-suspenders we can add later without breaking anything. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e4920538d2 |
feat(chat): show active-project indicator in SessionInfoBar + nav title
Adds a visible cue telling the user when their chat is scoped to a Scarf project. Two surfaces: - **SessionInfoBar** gets a folder-fill icon + project name chip at the start of the bar (before the working dot + title). Rendered with `.tint` foregroundStyle so it's visually anchored as the first piece of context. Hidden for non-project chats — the bar looks identical to v2.2.1 when projectName is nil. - **Navigation title** becomes `Chat · <ProjectName>` when scoped, stays as plain `Chat` otherwise. Matches macOS conventions for "subject — detail" titles. ChatViewModel gains two `@Observable` properties: - `currentProjectPath: String?` — absolute path, source of truth for attribution lookups - `currentProjectName: String?` — resolved via the projects registry at session-start; stored to avoid disk reads on every render. Falls back to the raw path (rather than nil) when a session's attribution points at a project no longer in the registry — the user still sees *something* rather than silently losing the indicator. Both are populated in `startACPSession(resume:projectPath:)` from two sources: 1. If the caller passed `projectPath` — fresh project-chat case 2. Otherwise, SessionAttributionService.projectPath(for: resolvedSessionId) — resumed-session case. Means clicking an old project-attributed session from ANY surface (the project's Sessions tab, the global Resume menu) re-surfaces the indicator. When the user starts a non-project session, both fields reset to nil explicitly so the indicator doesn't leak between chats. Files: - ChatViewModel.swift — new properties + resolve logic - SessionInfoBar.swift — new `projectName: String?` parameter + chip rendering - RichChatView.swift — passes chatViewModel.currentProjectName through to SessionInfoBar - ChatView.swift — navTitle reflects the active project 80/80 Swift tests still pass. Visual change only; no test change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5340e70dd3 |
fix(projects): watch session-project-map so Sessions tab refreshes
ProjectSessionsView's `.onChange(of: fileWatcher.lastChangeDate)` was silently never firing when a new chat attributed a session to a project — the sidecar was written correctly, the session was in state.db correctly, attribution IDs matched exactly, but the per- project Sessions list didn't auto-refresh. Root cause: HermesFileWatcher.watchedCorePaths was missing `paths.sessionProjectMap` (`~/.hermes/scarf/session_project_map.json`, introduced in the v2.3 feature commit). Since the watcher didn't observe that file, writes from SessionAttributionService.persist produced no `lastChangeDate` change, the VM's onChange never ran, and the Sessions tab stayed empty until the user navigated away and back (triggering .task(id: project.id) to re-fire). One-line fix: add the sidecar to the watched-paths array. Now the flow works end-to-end: 1. User clicks "New Chat" on a project 2. ChatViewModel starts ACP session with cwd=project.path 3. SessionAttributionService.attribute writes the sidecar 4. HermesFileWatcher detects the change, bumps lastChangeDate 5. ProjectSessionsView's onChange fires, VM reloads, new session appears in the list immediately 80/80 tests still pass. No test change needed — the sidecar's direct tests are in SessionAttributionServiceTests; this is a file-watching integration fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7ad78a5492 |
fix(layout): cap RichChatView/ProjectSessions idealHeight; revert broken detail wrap
Prior commits tried to solve the "window grows whenever Chat or
Sessions is selected" bug by wrapping NavigationSplitView's detail
slot with an explicit frame (`205bb2c`). That broke the HSplitView
layout in Projects — the project list column, dashboard header,
tab bar, and Sessions-tab header all vanished. Scarf's convention
(PlatformsView.swift:12 calls it out explicitly) is to apply
size constraints on individual HSplitView columns, never on an
outer wrapper.
This commit:
- Reverts the broken ContentView.swift outer frame from `205bb2c`.
NavigationSplitView.detail goes back to its v2.2.1 shape.
- Caps the subtrees whose natural ideal heights are what was
actually pushing the window past the screen:
- RichChatView: `.frame(minHeight: 0, idealHeight: 500, maxHeight: .infinity)`
on the outer VStack. The message list uses a plain VStack
(deliberately, to dodge the LazyVStack whitespace bug — see
RichChatMessageList.swift:13-24), so its natural ideal grows
with every message. Capping idealHeight at 500 gives the
window a screen-safe starting size without limiting how tall
the view can flex when the user drags the window bigger.
- ProjectSessionsView: same treatment with `idealHeight: 400`.
Replaces the earlier `.frame(maxWidth: .infinity, maxHeight:
.infinity)` which set MAX but didn't influence what got
reported upward as ideal.
- Xcode regenerated Localizable.xcstrings during builds; riding
along.
`.frame(idealHeight:)` is the specific SwiftUI knob that overrides
a child's reported ideal on the way up — `maxHeight: .infinity`
alone doesn't. With `.windowResizability(.contentMinSize)` (still
in scarfApp, left alone), the window sizes itself to the reported
ideal on open and respects user drags above the content min. With
a screen-safe ideal, the window opens at a usable size and never
pushes past the desktop.
User-verified: window behaves correctly across section switches,
resize persists, chat input bar always visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
205bb2c56e |
fix(window): pin detail column's reported frame so Chat/Sessions stop resizing window
Prior fixes ( |
||
|
|
d9688781ee |
fix(app): windowResizability(.contentMinSize) so window stops auto-resizing
Root cause of the "window grows whenever I switch to Chat / the v2.3 Sessions tab" bug. Prior commits ( |
||
|
|
9aad9051c4 |
fix(chat,projects): clamp detail-column views so they don't grow the window
Two sibling fixes to the one landed in |
||
|
|
4baa3d4d28 |
fix(projects): clamp Sessions tab height so it doesn't push the window
The new Sessions tab's outer VStack had no maxHeight constraint.
Its inner `List(sessions) { … }` uses intrinsic content size — which
grows with the row count — and with enough sessions the enclosing
VStack would push the project window past the bottom of the screen.
Fixed by adding `.frame(maxWidth: .infinity, maxHeight: .infinity)`
to the outer VStack in `ProjectSessionsView.body`, matching the
pattern `siteTab` uses for its webview. Now the List fills the
available tab area and scrolls internally as expected.
Other v2.3 tabs already self-constrain (`widgetsTab` via ScrollView,
`siteTab` via explicit maxHeight). This brings Sessions in line.
80/80 Swift tests still pass. Visual-only fix; no test change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
799cdb19e1 |
feat(projects): per-project Sessions tab + sidecar attribution
Third and final v2.3 commit. Adds the Sessions tab alongside Dashboard and Site, and introduces the attribution sidecar that makes per-project session filtering possible without any upstream Hermes change. ## Sidecar Hermes's state.db has no cwd column on sessions — the cwd passed to `hermes acp` at session create is ephemeral from its side. Scarf now records session_id → project_path in ~/.hermes/scarf/session_project_map.json, owned end-to-end by Scarf. Written atomically on session creation; read by the per- project Sessions tab. Missing file = empty map; corrupt file = empty map (logged warning, no crash). Forward-only attribution: only sessions Scarf starts with a project context get mapped; CLI- started sessions still surface in the global Sessions sidebar unchanged. New pieces: - Core/Models/SessionProjectMap.swift — Codable sidecar shape (mappings dict + updatedAt timestamp). - Core/Services/SessionAttributionService.swift — load / attribute / forget / reverse-lookup, all idempotent, all going through atomic write. - HermesPathSet.sessionProjectMap — canonical path resolution. ## Chat plumbing ChatViewModel.startNewSession and the private startACPSession gain an optional projectPath parameter. When non-nil it overrides the default cwd = context.resolvedUserHome() and, on successful session creation, SessionAttributionService.attribute is called. Default-nil call sites keep v2.2 behavior exactly — terminal-mode chats and the global "New Chat" button are unaffected. ## Coordinator handoff AppCoordinator gains pendingProjectChat: String?. The per-project Sessions tab sets it + switches selectedSection = .chat. ChatView observes it (.task cold-launch + .onChange live), consumes the path by calling startNewSession(projectPath:), and clears the field. Clean separation: the Projects feature never reaches into ChatViewModel directly. ## UI - New DashboardTab.sessions case in ProjectsView. Tab bar now always renders when a dashboard is loaded (was gated on siteWidget before); .site still filters out when there's no webview widget. - ProjectSessionsView — per-project session list with a "New Chat" button. Empty-state hint distinguishes "no attributions yet" from "stale sidecar entries". Reuses HermesDataService.fetchSessions and filters by the attribution map. - ProjectSessionRow — local row view independent of the global sessions sidebar so the two can evolve separately. ## Tests SessionAttributionServiceTests (7 tests): - Missing file → empty map - attribute writes + persists via fresh service instance - attribute is idempotent (same pair twice doesn't bump timestamp) - re-attribute changes mapping (session moves between projects) - reverse lookup returns all + distinguishes by project - forget removes mapping, is idempotent on missing sessions - Corrupted JSON → empty map, no crash 80/80 Swift tests pass (was 73; 7 new). 24/24 Python tests still pass. Both prep + feature commits stand independently; commit 3 depends on commit 1 (folder/archive fields) and commit 2 (sidebar UI) only for the full flow to work end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
585d035fe8 |
feat(projects): folder hierarchy + rename/archive/search in the sidebar
Second of three v2.3 commits. Replaces the flat projects sidebar with a hierarchical view that honors the folder + archived fields introduced in commit 1. ProjectsView's inline 70-line `projectList` becomes a one-call invocation of a new extracted `ProjectsSidebar` view. The parent keeps all sheet state (add / rename / move / uninstall / remove- from-list confirmation); the sidebar routes user intent up via closures. That separation means future sidebar changes (drag- and-drop, tags, color labels from the roadmap) don't need to touch ProjectsView's sheet wiring. ProjectsSidebar.swift renders, top to bottom: - Search field (filters by name / path / folder label, live) - Top-level projects (folder is nil or empty, not archived) - One DisclosureGroup per folder, alphabetically sorted, expanded by default on first render; collapsed state persists per view instance. Newly-created folders auto-expand so moves are visibly reflected. - An "Archived (N)" DisclosureGroup at the bottom, surfaced only when the Show Archived toggle in the bottom bar is on. Archived rows render at 0.7 opacity for a subtle visual cue. Bottom bar gains a Show Archived toggle next to the existing + button, using the archivebox SF Symbol (filled when on). Context menu gets three new entries alongside the existing ones: - Rename… — opens RenameProjectSheet with duplicate-name + empty-name validation. - Move to Folder… — opens MoveToFolderSheet with current folder pre-selected; picker lists Top Level, existing folders, and a "New folder…" option that gates on a text field. - Archive / Unarchive — flips the archived bit via the VM. Both new sheets live as standalone files (RenameProjectSheet, MoveToFolderSheet) for reuse — the wiki doesn't need updating; these are pure UI refinements. Selection binding round-trips through `viewModel.selectedProject` unchanged, so the existing dashboard / Site tab routing is unaffected. Sidebar matches use localizedCaseInsensitiveCompare so folder labels and project names sort the way users expect in non-English locales. 73/73 Swift tests still pass (no new tests in this commit — the VM verbs already exercised in ProjectsViewModelTests; the UI is visual and will be validated by the manual smoke test at the end of the branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f1e8f3070f |
feat(projects): registry schema v2 — folder + archived fields
First of three v2.3 commits. Adds the data model + view-model plumbing for folder grouping and soft-archive; no UI changes yet (sidebar still renders a flat list). ProjectEntry gains two optional fields: - `folder: String?` — opaque single-level label for sidebar grouping; nil means top-level. Custom Codable decodeIfPresent so v2.2 registry files parse cleanly. - `archived: Bool` — soft-delete flag; defaults to false via custom decoder. Archived projects stay on disk and in the registry; the v2.3 sidebar just hides them unless Show Archived is toggled on. Custom encode(to:) omits both fields when they're at their default values. Keeps registry files clean for the common (top-level, unarchived) case and means v2.2 Scarf still loads a v2.3-written registry of projects that never used the new features — forward + backward compat by construction. ProjectsViewModel grows four verbs: - moveProject(_:toFolder:) — update the folder assignment - renameProject(_:to:) — rename with duplicate-name + empty-name rejection; preserves selection across the rename so the user stays on the same project - archiveProject(_:) — sets archived=true, clears selection if the archived project was selected (avoids lingering on a hidden view) - unarchiveProject(_:) — sets archived=false; does NOT re-select (unhiding ≠ focusing) - `folders: [String]` computed property — distinct folder labels, sorted, for the sidebar + move-to-folder sheet Two new test suites: - ProjectRegistryMigrationTests: round-trips v2.2 → v2.3 and back, asserts encoder cleanliness (defaults omitted), identity stability under folder / archive changes. - ProjectsViewModelTests: verbs hit the real ~/.hermes/scarf/projects.json via TestRegistryLock for cross-suite serialization. Covers happy paths, duplicate / empty-name rename rejection, and folder dedup. 73/73 Swift tests pass (was 58; 15 new). No behavior change on v2.2 registry files yet — the sidebar UI lands in commit 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f366057cfd |
docs(roadmap): add Projects System Evolution section
Captures the backlog discussed during v2.3 planning so future sessions can pick up items without re-deriving the terrain: - v2.3 (planned, in this branch): folders + rename/archive/search + per-project Sessions tab via a sidecar attribution file. - v2.4+: per-project activity feed, token rollup, cron filter, desktop notifications — all "filter existing data via the sidecar" work, unblocked once v2.3 ships. - v2.5+: platform bets (Hermes upstream sessions.cwd column, per-project memory slice, per-project skills namespace, cross-project meta-dashboards, project backup/restore). - Continuous polish: drag-and-drop, tags, favorites, recents, color labels, starter dashboards, opportunistic backfill. - Known research gaps to chase when relevant. No code change; pure docs. Commits to the feature branch because the v2.3 planning context originated there; lands on main with the merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fd0d923c0b |
chore(assets): switch AppIcon set to macOS-native filenames
Exported from Apple Configurator / Icon Composer with the macOS naming template instead of the iOS one (rose from having the wrong template selected in the asset-set's original export). The actual PNG contents match the sizes the macOS AppIcon expects at every 1x/2x density; Contents.json reorders to reference the new names. No visual change for users — the Finder / Dock / about-box icon render identically because the rendered pixels are unchanged at each size. File replacement is purely naming / organizational. Uploaded as a prep commit on the v2.3-projects feature branch since the icon tweak was sitting in the working tree and shipping it separately from the feature work would require an extra release cycle for no benefit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3c2d11470f | chore: Bump version to 2.2.1 v2.2.1 | ||
|
|
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> |
||
|
|
ef3ddcdd7a |
fix(config-sheet): EnumControl always uses Menu picker, never Segmented
The Configuration sheet's clipping bug persisted after the earlier
VStack maxWidth fix (
|
||
|
|
5e207f760d |
docs(skill): warn authors against raw URLs in field descriptions
Pairs with the config-sheet wrap fix in
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
2b25a9da71 | chore: Bump version to 2.2.0 v2.2.0 | ||
|
|
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>
|
||
|
|
de5b278da4 |
docs: expand v2.2.0 release notes + README for full 2.2 scope
The pre-existing release notes and README "What's New in 2.2" block
only covered the original Project Templates feature. This expands
both to reflect everything that's actually shipping in 2.2:
- Template Configuration (schemaVersion 2): typed schema, 7 field
types, Keychain-backed secrets, configure step in install flow,
post-install Configuration editor, model recommendations.
- Template Catalog: gh-pages site with live dashboard previews,
stdlib-only Python validator mirroring Swift invariants, PR CI
gate, install-URL hosting from raw main.
- Example template `awizemann/site-status-checker` exercising every
v2.2 surface — config form, cron, Site tab webview, dashboard
updates.
- Site tab — a webview widget in any dashboard exposes a second
tab next to Dashboard, rendering a live URL.
- UX clarifications: Remove from List (keep files) vs. Uninstall
Template (remove installed files), preserved-files banner on
uninstall success, Run Now no longer blocks on long agent runs.
- Install-time {{PROJECT_DIR}} / {{TEMPLATE_ID}} / {{TEMPLATE_SLUG}}
token substitution in cron prompts.
Release-notes link + wiki link surfaced at the bottom of the README
block so readers have a jump to full details.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fb7a80f191 |
fix: Run Now agent-run timing + non-404 webview placeholder
Two independent fixes that both blocked the "install → Run Now → see the Site tab render" loop. 1. CronViewModel.runNow stopped blocking on `cron tick`. Previously the UI waited up to 60 s on the tick before deciding whether the job succeeded, so any agent run that did real work (an LLM call + a few HTTP GETs + a file write = easily 90 s+) surfaced a false "Run failed" toast while the job kept running in the background. Dashboard updates landed minutes later, confusing the user. New shape: show "Agent started — dashboard will update when it finishes" the instant `cron run` queues the job, then call `cron tick` with a 300 s timeout to force execution. Tick failures are logged but don't overwrite the started-toast — HermesFileWatcher picks up the dashboard.json rewrite automatically when the agent finishes. 2. site-status-checker's webview widget pointed at `github.com/awizemann/scarf/tree/main/templates/awizemann/...`. The templates/ path only exists on project-sharing, not main, so GitHub returned 404 in the Site tab until the first cron run replaced the URL with the user's configured site. Switched the placeholder to `awizemann.github.io/scarf/` which always renders. Bundle + catalog rebuilt against the updated dashboard.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
18640293f7 |
fix(projects): clarify remove-vs-uninstall UX
Three UX changes addressing user feedback that "Remove from Scarf" and "Uninstall Template…" looked interchangeable, and that users were surprised when uninstall left the project folder behind. - Rename sidebar menu entries: "Uninstall Template…" → "Uninstall Template (remove installed files)…" "Remove from Scarf" → "Remove from List (keep files)…" The expanded labels carry the scope difference at the point of click. - Add a confirmation dialog for Remove from List. The sidebar's "-" button and the context-menu entry both route through it. Dialog copy explicitly spells out "Nothing on disk is touched — the folder, cron job, skills, and memory block all stay. To actually remove installed files, use 'Uninstall Template…' instead." Sidebar "-" also gains a help tooltip saying the same thing. - Post-uninstall preserved-files banner. When the uninstaller keeps the project directory (because the cron wrote a status-log.md or the user dropped files in there), the success view now shows an orange banner listing up to 8 preserved paths with a "+N more…" tail, plus a one-line explanation and a pointer to delete the folder from Finder if the user doesn't want those files. VM captures the preservation shape before nil'ing `plan` on success. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
19750597cd |
feat(site-status-checker): add Live Site Preview webview for Site tab
A Scarf project dashboard that includes at least one webview widget automatically exposes a Site tab next to the Dashboard tab. Adding a "Live Site Preview" section with a webview widget gives this template that tab out of the box. The cron job + AGENTS.md now tell the agent to rewrite the webview's `url` field to the first entry in `values.sites` on each run, so the Site tab renders whatever the user actually configured instead of the GitHub placeholder. If `values.sites` is empty, the webview URL is left untouched. Swift example test updated to assert 4 sections (was 3) plus the new webview widget's presence + title; bundle + catalog rebuilt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
69e9cc6c7b |
fix(cron): Run now now actually runs + markdown rendering in install sheet
Two fixes chained from manually testing site-status-checker v1.1.0.
---
Cron Run now was a no-op when the Hermes gateway scheduler wasn't
already running. `hermes cron run <id>` only marks a job as due on
the next scheduler tick — it doesn't execute. During dev or right
after install (gateway stopped, as the logs the user pasted showed),
the user's click resulted in nothing happening: job queued, tick
never comes, zero agent sessions, zero output, dashboard never
updates. Exactly the failure mode they hit.
Fix: CronViewModel.runNow now calls `hermes cron run <id>` followed
by `hermes cron tick` after a short delay. `tick` runs all due jobs
once and exits — so the just-queued job actually executes, and
exits cleanly whether the scheduler is running or not. Redundant
(not duplicative) when the gateway is live. The user sees a status
message whether it succeeded or failed instead of silent nothing.
---
Markdown rendering in install-sheet screens. Template READMEs,
manifest descriptions, field help text, and cron prompts all
reasonably contain markdown — but the install preview sheet was
rendering everything as plain text, so `[Create one](https://…)`
would appear verbatim instead of as a link, `# Site Status Checker`
as a literal pound sign, etc.
New Features/Templates/Views/TemplateMarkdown.swift — a tiny,
dependency-free markdown renderer scoped to what template authors
actually write:
- Headings (#..######) → larger bold Text with vertical spacing
- Bullet and numbered lists → hanging-indent rows with •/1. prefix
- Fenced code blocks (```) → monospaced with quaternary background
- Paragraphs → regular Text, with inline formatting via SwiftUI's
built-in AttributedString(markdown:) so **bold**, *italic*,
`code`, and [links](urls) work
- Blank lines separate blocks
Two entry points: `TemplateMarkdown.render(_ source:)` returns a
View for multi-block content (README preview), and
`TemplateMarkdown.inlineText(_ source:)` returns a Text for
one-line strings where block structure doesn't apply (field
descriptions, manifest tagline).
Wired into:
- TemplateInstallSheet.readmeSection — was plain Text(readme), now
renders the full README with structure.
- TemplateInstallSheet.manifestHeader description — inline-only
(taglines rarely have block structure).
- TemplateInstallSheet.cronSection — new DisclosureGroup per cron
job exposes the full prompt with markdown rendering. Users can
now verify what the installer will register with Hermes before
clicking Install. {{PROJECT_DIR}} / {{TEMPLATE_ID}} tokens show
unresolved here; they get substituted when the installer calls
hermes cron create.
- TemplateConfigSheet field descriptions — inline markdown so
`[Create a token](https://...)`-style links render as real links.
Not a full CommonMark implementation — no tables, no blockquotes,
no images, no HTML passthrough. Those can evolve as templates need
them. Safe with untrusted input: never executes scripts or renders
raw HTML.
Scope stays tight: 57/57 Swift tests + 24/24 Python tests still pass.
No new tests for the markdown helper itself — rendering is visual,
hard to unit-test meaningfully without snapshot-testing infra, and
the surface is small enough that changes would be caught by the
visual regression of any template install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
03bf5262bb |
feat(templates): install-time {{PROJECT_DIR}} substitution in cron prompts
Hermes doesn't set a working directory when firing cron jobs, so any
relative path in a template's cron prompt (`.scarf/config.json`,
`status-log.md`, etc.) resolves against whatever dir Hermes happens
to be in — NOT the installed project. Practical effect: site-status-
checker's cron job fires, agent runs with relative paths, finds
nothing to read, silently bails. User sees "Run now" complete with
zero output and nothing updated on disk.
Fix: the installer now substitutes template-author placeholders in
cron prompts at install time, before calling `hermes cron create`.
The registered cron job carries a fully-qualified, CWD-independent
prompt.
Supported tokens (deliberately few — each is part of the template
format contract from now on):
- `{{PROJECT_DIR}}` — absolute path of the installed project dir.
The one that was motivating this fix; required for any cron prompt
that reads or writes project files.
- `{{TEMPLATE_ID}}` — the `owner/name` from the manifest, for
templates that want to tag delivery payloads or log lines.
- `{{TEMPLATE_SLUG}}` — the sanitised slug used by the installer for
dir name + skills namespace, for templates that want to reference
their skills install path.
Implemented as a static `ProjectTemplateInstaller.substituteCronTokens`
so it's testable as a pure function. Unsupported placeholders pass
through verbatim — template authors notice in testing that their
token didn't get replaced and either use a supported one or file
a request.
Site Status Checker v1.1.0 updated to use the tokens:
- cron/jobs.json prompt now opens with "Run the site status check
for the Scarf project at {{PROJECT_DIR}}" and references
{{PROJECT_DIR}}/.scarf/config.json, {{PROJECT_DIR}}/status-log.md,
and {{PROJECT_DIR}}/.scarf/dashboard.json explicitly.
- AGENTS.md gains a note explaining that the cron-registered prompt
carries absolute paths (installer substitutes at install time),
while interactive-chat agents can keep using relative paths.
- bundle rebuilt, catalog regenerated.
templates/CONTRIBUTING.md documents the three supported tokens under
the cron/jobs.json bullet so future authors don't have to discover
this by hitting the same CWD bug.
Tests:
- ProjectTemplateExampleTemplateTests.siteStatusCheckerParsesAndPlans
extended to assert the bundled prompt contains {{PROJECT_DIR}}
UNRESOLVED. If someone accidentally bakes an absolute path into
the template (their install dir), every user of that template
would get the wrong path — this test catches that.
- Four new substitution tests in ProjectTemplateInstallerTests:
resolves PROJECT_DIR / resolves ID + SLUG / leaves unknown tokens
untouched / substitutes repeated occurrences. All go through the
static helper directly; no install round-trip needed.
57/57 Swift tests + 24/24 Python tests pass. Catalog check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3af99d9d9c |
fix(templates): site-status-checker dashboard no longer lies before first run
The template's dashboard shipped with two hardcoded example URLs (https://example.com + https://example.org) baked into a "Configured Sites" list widget, and the widget title still said "from sites.txt" — stale from the v1.0.0 layout before we moved to config.json. After the v1.1.0 configure-on-install flow lands, the user fills in a real sites list through the Configure form (which correctly lands in `.scarf/config.json` — the editor modal confirms that), but the dashboard still rendered the baked-in example URLs. The agent would overwrite them on the first cron run, but until then the dashboard misrepresents reality. Two orthogonal paths to fix this — populate the dashboard's items from config.json at install time (requires Scarf-side template-value interpolation, which is a v2.3.1 feature), or ship a dashboard that clearly advertises "nothing has run yet." Taking the second path for v1.1.0: replace the example URLs with a single placeholder row with status "pending" pointing the user at running the check. The agent replaces the row with real data on the first cron run. Also: widget title fixed ("Watched Sites (populated after first run)" instead of the stale sites.txt reference), top-of-dashboard description updated, and the Quick Start text now mentions the Configuration button as the way to set sites, not the long-gone sites.txt. Bundle + catalog rebuilt; ProjectTemplateExampleTemplateTests still passes (it asserts against cron prompt + schema shape, not dashboard content, so the dashboard edit doesn't affect it). --- Secondary fix: test deflake from the saveRegistry throw change. Making saveRegistry throw exposed a pre-existing parallel-test race: three suites (ProjectTemplateInstallerTests, ProjectTemplateUninstallerTests, ProjectTemplateConfigInstallTests) all write to the real `~/.hermes/scarf/projects.json`. Swift Testing's `.serialized` trait only serializes within a single suite — multiple suites still run in parallel. Before, writes silently failed on the racing-loser side and tests passed by accident; now the loser's test throws "couldn't be saved in the folder 'scarf'". Added TestRegistryLock — a module-level NSLock that all three suites' snapshotRegistry/restoreRegistry helpers share. acquireAndSnapshot() locks + reads; restore(_:) writes + unlocks. The paired snapshot-in-test-body / defer-restore pattern keeps acquire + release balanced. Replaced the three per-suite copies of the helpers with thin delegates to the shared lock. Verified by running the full test suite 3 consecutive times: 53/53 tests pass each run, no flakes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3bd95de8f4 |
fix(config): install sheet silently closed after Continue in config step
Two bugs chained into the observed "install completed but project didn't show up" report. Either one would have been enough on its own; both are here so both are fixed. Primary bug: TemplateConfigSheet's Cancel + Continue buttons each called `@Environment(\.dismiss)` after their state-update callbacks. That was fine when the sheet is presented standalone (the post-install Configuration button uses it this way and wants dismissal), but Phase C also INLINED the same view inside TemplateInstallSheet.configureView for the install flow's .awaitingConfig stage — there's no intermediate .sheet() presenter there, so `dismiss()` resolved to the OUTER install sheet. Clicking Continue → configure form's `onCommit` fired `installerViewModel.submitConfig(values:)` which advanced stage to .planned, then the dismiss() closed the whole install sheet before the preview ever rendered. install() was never called. Fix: remove both dismiss() calls from TemplateConfigSheet. Dismissal is now the caller's responsibility. ConfigEditorSheet (standalone mode) already calls `dismiss()` inside its own onCancel closure and lets the .succeeded state's Done button handle commit-dismissal, so nothing breaks there. The install flow's state machine advances to the preview stage where the existing Install/Cancel buttons drive everything from there. Secondary bug (latent, same class): ProjectDashboardService.saveRegistry swallowed both directory-creation and file-write errors with `try?`. If the `~/.hermes/scarf/` dir creation or projects.json write ever failed for any reason (permissions, readonly filesystem, sandbox), the installer's registerProject returned a valid-looking ProjectEntry while the registry on disk never received the row. Same symptom surface as the primary bug: install "succeeds," project invisible. Fix: saveRegistry now throws. Updated all four callers: - ProjectTemplateInstaller.registerProject: `try` — a registry write failure aborts install with a user-visible failure screen. This is the critical path; silent success on a destructive op is the exact failure mode we want to eliminate. - ProjectTemplateUninstaller: `do/catch` + logger.warning — we're at the final step of uninstall after every other side effect has already completed (files removed, skills removed, cron removed, memory stripped, Keychain cleared). Leaving a stale registry row pointing at a deleted project is cosmetic and easy to fix from the sidebar minus button. - ProjectsViewModel.addProject + removeProject: `do/catch` + logger.error. The VM doesn't currently have a surface for user-visible errors (no toast/alert on this view), but the failure now at least lands in the unified log instead of disappearing. Proper in-UI error surface is tracked as follow-up. - ProjectDashboardService.loadRegistry: switched its stale `print` to `logger.error` while I was in the file. Tests: added TemplateInstallerViewModelTests suite (3 tests) covering the install VM's configure-step state transitions: - submitConfigStashesValuesAndTransitionsToPlanned — .awaitingConfig → .planned + configValues stash on the plan. The exact transition that the dismiss() bug tore down mid-flight. - cancelConfigReturnsToAwaitingParentDirectory — back-button behaviour with plan preserved so re-entry doesn't re-run buildPlan. - submitConfigNoOpWhenPlanIsNil — defensive guard. These won't catch a view-level regression (Swift Testing doesn't do UI tests in this project), but they lock in the VM state-machine contract so the next refactor can't silently break submitConfig or cancelConfig without failing CI. 53/53 Swift tests + 24/24 Python tests + catalog validator clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
81e8da91d6 |
feat(templates): upgrade site-status-checker to v1.1.0 with config schema
First real exercise of the v2.3 configuration feature. The template no longer asks the agent to bootstrap sites.txt on first run — instead, users enter their list of URLs through the Configure form during install, and change them later via the dashboard's Configuration button. This makes the template a complete round-trip test of the new feature end-to-end. Schema (manifest.config.schema): - `sites` — list<string>, required, 1–25 items, default two example URLs. This is the list the cron job hits. - `timeout_seconds` — number, 1–60, default 10. Per-URL HTTP timeout. - `modelRecommendation.preferred = claude-haiku-4` — rationale: simple tool-use task, Haiku is cost-effective for daily cron. Manifest bumped: schemaVersion 1 → 2, version 1.0.0 → 1.1.0, minScarfVersion 2.2.0 → 2.3.0, contents.config = 2. AGENTS.md rewritten for the config-driven flow: - Reads values from `.scarf/config.json` at run time (values.sites + values.timeout_seconds). No more sites.txt bootstrap. - "Add a site" / "Remove a site" no longer mean the agent edits a file — they mean "open the Configuration button on the dashboard." The agent points the user there rather than trying to mutate config.json itself. A future Scarf release may expose a tool for agents to write config programmatically; until then, config is strictly a user action. - First-run bootstrap now only creates status-log.md (if absent). README.md rewritten to walk users through the new form-based flow, explain the Configuration button, and document the model recommendation. Uninstall instructions point at the right-click Uninstall Template action rather than manual steps. Cron prompt updated to reference config.json (values.sites, values.timeout_seconds) instead of sites.txt. ProjectTemplateExampleTemplateTests.siteStatusCheckerParsesAndPlans extended with v2-specific assertions: manifest.schemaVersion == 2, contents.config == 2, schema.fields.count == 2, per-field constraints (sites type/itemType/minItems/maxItems, timeout min/max), modelRecommendation.preferred, plan.configSchema + plan.manifestCachePath are populated, plan.projectFiles includes both config.json + manifest.json destinations. Cron-prompt assertion swapped from sites.txt to config.json/values.sites. Three suites that touch ~/.hermes/scarf/projects.json now carry .serialized — the new Phase B install-with-config tests stressed the parallel-execution race in the snapshot/restore helpers. Serializing within each suite deflakes without any architectural change. Swift 50/50, Python 24/24, catalog validator accepts the upgraded bundle. Site detail page now has manifest.json for renderConfigSchema to pick up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb750e237e |
docs: CLAUDE.md — add Template Configuration section
Documents the v2.3 configuration feature for future agent sessions: manifest schemaVersion 2 shape, supported field types, Keychain storage conventions (service/account naming with project-path hash suffix), the uninstaller's config-items cleanup path, exporter behaviour (schema forwarded, values stripped), and the catalog site's schema display. Includes the "Schema is Swift-primary" note so future edits to TemplateConfigField.FieldType go through the right order of updates — Swift first, then Python mirror, then widgets.js, then UI controls, then tests on both sides. Schema drift between Swift + Python validator would accept bundles the app later refuses at install time, which is a catastrophic UX failure for the catalog. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
68f6b98fcf |
feat(catalog-config): mirror manifest v2 schema in validator + site
Phase D of v2.3 template configuration — closes the loop between the Swift app and the catalog pipeline. Authors can now ship schemaful bundles; the Python validator enforces the same invariants the Swift installer does; the catalog site displays the schema so visitors see what they'll need to configure before installing. Python validator (tools/build-catalog.py): - SUPPORTED_SCHEMA_VERSIONS accepts both 1 and 2 (v1 bundles are unchanged; v2 adds optional manifest.config). - New _validate_config_schema function mirrors the Swift ProjectConfigService.validateSchema rules: unique keys, supported types, enum option presence + unique values, list itemType == "string", secret-field cannot declare a default, modelRecommendation.preferred non-empty when present. - _validate_contents_claim cross-checks contents.config (field count) against config.schema actual length — mismatch refused. - TemplateRecord.to_catalog_entry exposes `config` in catalog.json so the site can render the schema. - render_site copies each bundle's template.json to the detail dir as manifest.json (only when the manifest has a config block — keeps the served tree lean and makes "no manifest.json" a meaningful 404 signal in the frontend). - catalog.json's own schemaVersion stays at 1 (independent of per- template manifest schemaVersion). Python tests (tools/test_build_catalog.py): 8 new cases in a new ConfigSchemaValidationTests suite — accepts schemaful bundle, rejects duplicate keys, rejects secret-with-default, rejects enum-without- options, rejects unsupported field type, rejects contents.config count mismatch, rejects unsupported list itemType, legacy v1 manifests pass unchanged. 24/24 Python tests total. Site (site/widgets.js): - New renderConfigSchema(container, config) — mirrors the display on the Scarf install preview. Renders each field as a <dt>/<dd> pair with type + required badges; enum shows choice labels; list fields show min/max bounds; string fields show pattern/length; secret fields get a "Stored in Keychain" reassurance. Optional modelRecommendation panel at the bottom with preferred + rationale + alternatives. - The renderer is display-only — the site never collects values; that's the Scarf app's job. template.html.tmpl adds a #config-schema <section>. The inline script fetches manifest.json from the detail dir; on success hands the config block to ScarfWidgets.renderConfigSchema; on 404 (schema-less templates) silently leaves the section empty. CSS in styles.css adds a config-schema panel matching the accent-green aesthetic. 24/24 Python + 50/50 Swift tests pass. site-status-checker still renders correctly (schema-less; manifest.json isn't copied for it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f8c086ee7a |
feat(config): configure-step UI + post-install Configuration editor
Adds the user-facing side of v2.3 template configuration. Install-time flow: templates with a non-empty config.schema get a Configure step between the parent-directory pick and the preview sheet. Post-install flow: a Configuration button on the dashboard header + a context-menu entry on the project list opens the same form pre-filled with current values. New files: - Features/Templates/ViewModels/TemplateConfigViewModel.swift — drives the form. Keeps freshly-entered secret bytes in `pendingSecrets` in-memory until commit() succeeds, then calls ProjectConfigService.storeSecret for each one. Cancelling never leaves orphan Keychain entries — the form is transactional. Validates via ProjectConfigService.validateValues on commit and populates per-field `errors` the sheet surfaces inline. Two modes: .install (needs a project passed at commit time) and .edit(project:) (VM already holds the target). - Features/Templates/Views/TemplateConfigSheet.swift — the form. One row per field with a control dispatched by type: TextField (string), TextEditor (text), number input, Toggle (bool), segmented/dropdown Picker (enum, picks form by option count), add/remove list editor, SecureField with show/hide toggle (secret). Required-field asterisk + per-field error display. Optional modelRecommendation panel at the bottom — informational badge; no auto-switch. - Features/Templates/ViewModels/TemplateConfigEditorViewModel.swift — loads <project>/.scarf/manifest.json + config.json, hands a TemplateConfigViewModel to the sheet, writes edited values back on commit. Has a .notConfigurable stage for projects without a manifest cache (hand-added projects, schema-less templates). - Features/Templates/Views/ConfigEditorSheet.swift — thin wrapper that owns the editor VM and routes its stages to loading / form / saving / success / error / not-configurable views. Wiring: - TemplateInstallerViewModel gains an .awaitingConfig stage between .awaitingParentDirectory and .planned. pickParentDirectory() now inspects plan.configSchema and either routes to .awaitingConfig (non-empty schema) or straight to .planned (schema-less). New submitConfig(values:) stashes finalized values in plan.configValues and advances; cancelConfig() returns to .awaitingParentDirectory. - TemplateInstallSheet renders a new `configureView` that inlines TemplateConfigSheet into the install flow for .awaitingConfig. The existing preview (.planned) gains a new "Configuration" section listing each field + its display value (secrets shown as "•••••• (Keychain)", lists shown as "first + N more", "(not set)" for missing values). - ProjectsView adds an isConfigurable(_:) check (transport.fileExists on .scarf/manifest.json), a new @State configEditorProject for sheet presentation, a new "Configuration…" context-menu entry on project list rows (for configurable projects), and a new slider.horizontal.3 button on the dashboard header next to the existing Uninstall button. 50/50 tests still pass. This commit is UI-only — no new Phase C tests (sheet behaviour is hard to unit-test without UI automation and the underlying VM logic is exercised by Phase A/B's config-round-trip tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eb34aec1f1 |
feat(config): template-config UI forms (configure sheet + editor)
Introduces the TemplateConfigSheet form and its view models, plus the install-flow integration points: a new .awaitingConfig stage in TemplateInstallerViewModel, the configureView step in the install sheet, and the dashboard-header Configuration button wiring in ProjectsView. This is the schemaful-template v2.3 UI that every subsequent config commit builds on. Originally landed alongside scaffolding for an iOS target in b289a83; this is the split that keeps the template-config work and drops the iOS scaffolding (the real iOS port is on scarf-mobile-development). |
||
|
|
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> |
||
|
|
7a99547b22 |
fix: address code-review findings from Apr 22 commits
Three follow-ups from reviewing |
||
|
|
64b7d3beaf |
feat(config): manifest schemaVersion 2 + installer/uninstaller/exporter wiring
Extends the template format to schemaVersion 2 (schema-less bundles at v1 keep working unchanged) and threads TemplateConfigSchema through inspect → buildPlan → install → uninstall → export end-to-end. Model additions (ProjectTemplate.swift): - ProjectTemplateManifest gains optional `config: TemplateConfigSchema?`. - TemplateContents gains optional `config: Int?` claim (field count) cross-checked against the schema by `verifyClaims` so a manifest can't hide its configuration from the preview sheet. - TemplateInstallPlan gains `configSchema`, `configValues` (populated by the VM just before install()), and `manifestCachePath`. New fields also feed totalWriteCount so the preview footer is honest. - TemplateLock gains optional `configKeychainItems: [String]?` and `configFields: [String]?`. Optional so pre-2.3 lock files still uninstall cleanly — Codable's default decoding skips missing fields. Service changes: - ProjectTemplateService.inspect now accepts schemaVersion 1 or 2. When the manifest declares a config block, the service validates it immediately via ProjectConfigService.validateSchema and fails the install with a manifestParseFailed before the preview sheet ever renders. verifyClaims cross-checks contents.config count against the actual schema length. - ProjectTemplateService.buildPlan populates configSchema and queues two new entries in projectFiles: .scarf/config.json (synthesized by the installer from configValues at write time, using an empty sourceRelativePath sentinel) and .scarf/manifest.json (copy of the bundle's template.json so the post-install Configuration editor can render offline). - ProjectTemplateInstaller.createProjectFiles now special-cases the empty-source sentinel: for .scarf/config.json, it encodes plan.configValues into a ProjectConfigFile on the fly. Secrets in that file are keychain:// refs — the raw bytes were routed into the Keychain by the VM before install() was called. - ProjectTemplateInstaller.writeLockFile records every keychainRef URI from configValues in lock.configKeychainItems and the schema field keys in lock.configFields. - ProjectTemplateUninstaller.uninstall adds a new step 4a: iterate lock.configKeychainItems, parse each URI into a TemplateKeychainRef, SecItemDelete each one. Absent items are no-ops (the Keychain wrapper already handles errSecItemNotFound silently). - ProjectTemplateExporter now reads the source project's .scarf/manifest.json (if present) and forwards the SCHEMA through to the exported bundle while zeroing values. schemaVersion bumps to 2 only when a schema is carried; schema-less exports stay at 1 for byte-compatibility with v2.2 catalog validators. Tests (ProjectTemplateTests.swift): 5 new tests in 1 new suite. - inspectAcceptsSchemaV2Bundle: v2 manifest unpacks cleanly. - buildPlanSurfacesSchemaAndQueuesConfigFiles: plan carries the schema; projectFiles contains both config.json + manifest.json. - verifyClaimsRejectsConfigCountMismatch: a manifest lying about contents.config vs. schema.fields.count is refused at inspect. - installWritesConfigJsonAndManifestCache: install round-trip writes config.json (with non-secret values inline + secret as keychainRef), manifest.json cache, and lock with configKeychainItems + configFields. Real Keychain is exercised; the test cleans up the single item it creates. - uninstallDeletesKeychainItemsViaLock: install + then uninstall, verify the Keychain entry is gone via SecItemCopyMatching. sampleManifest test helper gains `configFieldCount` and `configSchema` params so tests that want schemaful bundles don't need to rebuild the whole manifest record. schemaVersion auto-bumps to 2 when a schema is present so the fixture mirrors real bundle shape. 50/50 tests in 13 suites pass; pre-existing 45 from v2.2 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
385c3a2e4d |
feat(config): template-config models + Keychain wrapper + ProjectConfigService
Groundwork for v2.3 template configuration. No user-visible behaviour yet — this commit adds the data structures, storage layer, and validation rules that the installer/uninstaller/UI will integrate with in the next two commits. Models (Core/Models/TemplateConfig.swift): - TemplateConfigSchema + TemplateConfigField for the author-declared manifest.config block. 7 field types: string, text, number, bool, enum, list, secret. Type-specific constraints (pattern, min/max, min/maxLength, min/maxItems, enum options) are all optional and the validator enforces only those applicable to the field's type. - TemplateModelRecommendation for the author's model-of-choice hint (preferred + rationale + alternatives). Purely advisory — Scarf never auto-switches the active model. - TemplateConfigValue enum: string / number / bool / list / keychainRef. Custom Codable preserves keychain:// refs on round-trip — a round through save/load never demotes a secret ref to plaintext. - ProjectConfigFile is the on-disk shape at <project>/.scarf/config.json. - TemplateKeychainRef: derives (service, account) from templateSlug + fieldKey + project-path hash. The 32-bit FNV-1a suffix prevents two installs of the same template in different dirs from colliding in the login Keychain. uri <-> parse round-trips losslessly. Keychain layer (Core/Services/ProjectConfigKeychain.swift): - Thin wrapper over kSecClassGenericPassword. set() tries update-first then add-if-missing so we don't trip "already exists" on a race. - kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: no iCloud sync, but cron triggers can still read after the user's first unlock. - testServiceSuffix lets unit tests route items under a distinct service prefix so nothing leaks into the user's real Keychain. Service layer (Core/Services/ProjectConfigService.swift): - load/save for <project>/.scarf/config.json through the ServerContext transport (so remote-ready for when installer goes remote). - cacheManifest/loadCachedManifest: the installer copies template.json into <project>/.scarf/manifest.json so the post-install "Configuration" button can render the form offline. - resolveSecret / storeSecret / deleteSecrets: the three Keychain paths any caller needs. Non-secret values never pass through these. - validateSchema: author-facing invariants (unique keys, known types, enum opts present/unique, no defaults on secrets, non-empty model preferred). Called by ProjectTemplateService during inspect. - validateValues: user-facing invariants (required, pattern, numeric range, list bounds, enum membership). Returns one error per problem so the UI can surface them inline with the offending field. Tests (scarfTests/TemplateConfigTests.swift): 23 tests in 5 suites. - Schema validation: happy path + every rejection rule. - Value validation: required, pattern, numeric range, list bounds, enum membership, secret-via-keychain-ref acceptance. - Keychain ref: uri round-trip, parse rejection of malformed input, path-hash differs across project dirs but is stable for same path. - ProjectConfigFile round-trips non-secret values cleanly AND preserves keychain:// refs (the bug that would silently demote secrets to plaintext if the Codable were wrong). - Real Keychain integration: store+resolve+delete, set overwrites, delete of missing item is a no-op, bulk delete clears all. Tests use unique testServiceSuffix per run so no cross-contamination. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e76fbf9937 |
chore: audit follow-ups from plan review
Four small fixes surfaced by a side-by-side plan-vs-shipped pass: - README.md: adds the Template Catalog section the plan called out — links to the live site URL, the install flows (web / file / Finder), and templates/CONTRIBUTING.md for authors. Placed right before the existing Contributing section, with a catalog-specific cross-link at the end of that section too. - CLAUDE.md: adds the Template Catalog section so future agent sessions know the regenerator pipeline exists, how it relates to release.sh + wiki.sh, and what the schema-sync rule is when DashboardWidget or ProjectTemplateManifest change. - scarf/scarfTests/ProjectTemplateTests.swift: fixes the stale ProjectTemplateExampleTemplateTests docstring still referencing `examples/templates/` (the example moved to `templates/awizemann/` in 70f7cea). - .github/workflows/validate-template-pr.yml: untangles the self- contradictory Python-version comment. The validator is 3.9+ compatible; CI uses 3.11 for faster runner caching. Same stdlib surface, same code paths — just clearer about why. All tests still green: 22 Swift tests in 7 suites, 16 Python tests, catalog check passes on the site-status-checker example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c9b8da9ec5 |
feat(ci): validate template submissions on PR + tailored checklist
Adds the CI gate that runs on every PR touching templates/, the catalog validator, or its tests. The Action: - runs tools/test_build_catalog.py (catches drift between validator + its own test suite on the same PR that introduces the drift) - runs tools/build-catalog.py --check (validates every shipped .scarftemplate against the same invariants ProjectTemplateService.verifyClaims enforces at install time) - posts a PR comment with the last 3 KB of the validator log on failure, so contributors see the specific mismatch without hunting through the Actions UI .github/PULL_REQUEST_TEMPLATE/template-submission.md is the author-facing checklist that mirrors templates/CONTRIBUTING.md. Opt-in via the ?template=template-submission.md compare URL (documented in the contribution guide). CONTRIBUTING.md now links both the PR template and the workflow file so authors know what to expect. Phase 4 closes the community loop — from this commit on, a stranger can fork the repo, follow templates/CONTRIBUTING.md, push a PR, and get deterministic green/red feedback before a maintainer ever looks at it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |