Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

### Changed

- **Skill token footprint cut ~55% via progressive disclosure.** The flow
skill's `SKILL.md` is loaded resident by the Skill tool and re-billed as
cached tokens on *every* turn, making it the dominant contributor to
flow's per-session cost (~28K tokens, and multiplied across a long
session). The skill is now a **lean resident core** (~50 KB, down from
~111 KB / ~27.7K → ~12.6K tokens) plus twelve on-demand
`references/*.md` files that hold rarely-needed workflows (playbooks,
owners, upgrade, weekly review/archive, tagging, session-binding
detail, project intake, first-run setup, brief templates, the work_dir
recipe, cross-task transcripts & field-edit semantics). The core keeps a
one-line trigger pointing at each reference, so the model loads a
workflow's full text only when it's actually needed — paid once, not
every turn. No workflow content was removed; sections were relocated and
the resident prose tightened. The embed changed from a single
`//go:embed skill/SKILL.md` to the whole `skill/` directory, and
`Harness.InstallSkill` now writes the tree (`SKILL.md` + `references/`)
under `~/.claude/skills/flow/`. New guards: `TestSkillCoreIsLean` caps
the resident core, `TestSkillReferencesAreReachable` forbids orphan
references, and content assertions run against the full corpus so a
future relocation can't silently drop a section.

## [0.1.0-alpha.24] — 2026-07-06

### Added
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ flow/
│ │ ├── bootstrap.go # UUID gen, session file scanning
│ │ ├── resolve.go # task/project slug resolution
│ │ ├── slug.go # name-to-slug conversion
│ │ ├── skill/SKILL.md # embedded skill (//go:embed)
│ │ ├── skill/SKILL.md # embedded lean skill core (//go:embed skill)
│ │ ├── skill/references/*.md # on-demand workflow references (embedded)
│ │ └── *_test.go
│ ├── flowdb/ # SQLite data layer
│ │ ├── db.go # schema, models, CRUD queries
Expand Down Expand Up @@ -93,7 +94,8 @@ flow/
- **Tests:** Table-driven where possible. Command tests live alongside source in `internal/app/`. `e2e_test.go` exercises the full command surface in sequence.
- **No mocks for DB.** Tests use real SQLite in a temp directory. Only osascript is mocked (via `iterm.Runner` function var).
- **Skill file is the source of truth** for how Claude sessions interact with flow. If the skill says something, the code must support it.
- **Skill embed path:** `internal/app/skill/SKILL.md` is embedded at compile time via `//go:embed` in `internal/app/skill.go`. After editing, rebuild for `flow skill update` to pick up changes.
- **Skill embed path:** the entire `internal/app/skill/` directory — the lean resident `SKILL.md` plus `references/*.md` — is embedded at compile time via `//go:embed skill` (an `embed.FS`) in `internal/app/skill.go`. `Harness.InstallSkill(fs.FS)` walks the tree and writes it under `~/.claude/skills/flow/`, so `SKILL.md` lands next to `references/`. After editing any of them, rebuild for `flow skill update` to pick up changes.
- **Progressive disclosure / lean core:** `SKILL.md` is loaded resident by the Skill tool and re-billed as cache on every turn, so it's the dominant token cost — keep it lean. Rarely-needed workflows live in `references/*.md` (loaded on demand via Read), each reachable from a one-line trigger in `SKILL.md`. `TestSkillCoreIsLean` caps `SKILL.md` size; `TestSkillReferencesAreReachable` forbids orphan references; content assertions run against the full corpus (`skillCorpus`) so relocating a section never loses it. Move new low-frequency workflows into `references/`, not the core.

## Data directory layout

Expand Down
2 changes: 1 addition & 1 deletion internal/app/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func cmdInit(args []string) int {
return 1
}
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
if err := h.InstallSkill(embeddedSkill); err != nil {
if err := h.InstallSkill(skillFiles()); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
Expand Down
30 changes: 25 additions & 5 deletions internal/app/skill.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
package app

import (
_ "embed"
"embed"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)

//go:embed skill/SKILL.md
var embeddedSkill []byte
// embeddedSkillFS holds the entire skill directory — SKILL.md (the lean
// resident core loaded by the Skill tool) plus references/*.md (rarely-
// needed workflows loaded on demand). Embedding the whole tree lets the
// installer lay the reference files down alongside SKILL.md so the core
// can point at them.
//
//go:embed skill
var embeddedSkillFS embed.FS

// skillFiles returns the embedded skill tree rooted at the skill/
// directory, so walking it yields "SKILL.md" and "references/<x>.md"
// (not "skill/SKILL.md"). Passed to Harness.InstallSkill.
func skillFiles() fs.FS {
sub, err := fs.Sub(embeddedSkillFS, "skill")
if err != nil {
// The embed directive guarantees skill/ exists; this is
// unreachable in a correctly-built binary.
return embeddedSkillFS
}
return sub
}

// hookCommand is the exact string the harness's settings.json
// (settings.json / hooks.json depending on harness) records as the
Expand Down Expand Up @@ -84,7 +104,7 @@ func maybeAutoUpgradeSkill() {
return
}
// Version mismatch — refresh skill bytes and the SessionStart hook.
if err := h.InstallSkill(embeddedSkill); err != nil {
if err := h.InstallSkill(skillFiles()); err != nil {
return
}
_ = writeSkillVersion(Version)
Expand Down Expand Up @@ -134,7 +154,7 @@ func skillInstall(args []string, forceDefault bool) int {
fmt.Fprintf(os.Stderr, "error: stat %s: %v\n", dest, err)
return 1
}
if err := h.InstallSkill(embeddedSkill); err != nil {
if err := h.InstallSkill(skillFiles()); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}
Expand Down
Loading
Loading