diff --git a/cmd/init_test.go b/cmd/init_test.go index b40985e5..9294630d 100644 --- a/cmd/init_test.go +++ b/cmd/init_test.go @@ -525,12 +525,26 @@ func TestRunbookTemplateEmbedded(t *testing.T) { "templates/runbook/.kiwi/schemas/runbook.json", "templates/runbook/.kiwi/config.toml", "templates/runbook/.kiwi/templates/runbook.md", + "templates/runbook/services/api-service.md", + "templates/runbook/services/monitoring.md", } for _, p := range paths { if _, err := fs.Stat(embedded, p); err != nil { t.Fatalf("embedded template missing %s: %v", p, err) } } + + absent := []string{ + "templates/runbook/incidents", + "templates/runbook/postmortems", + "templates/runbook/procedures", + "templates/knowledge/index.md", + } + for _, p := range absent { + if _, err := fs.Stat(embedded, p); err == nil { + t.Fatalf("expected %s to be filtered from embed, but it still exists", p) + } + } } func TestRunbookTemplateInit(t *testing.T) { @@ -551,6 +565,8 @@ func TestRunbookTemplateInit(t *testing.T) { ".kiwi/templates/runbook.md", ".kiwi/config.toml", ".kiwi/playbook.md", + "services/api-service.md", + "services/monitoring.md", } for _, p := range mustExist { if _, err := os.Stat(filepath.Join(root, p)); err != nil { diff --git a/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v5.md b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v5.md new file mode 100644 index 00000000..804ee687 --- /dev/null +++ b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v5.md @@ -0,0 +1,51 @@ +--- +memory_kind: episodic +episode_id: cursor-hands-on-325-2026-07-03-v5 +title: "Issue #325 — runbook init embed filter delivery on main" +tags: [kiwifs, runbooks, issue-325, init-template, embed-filter, uc-6] +date: 2026-07-03 +--- + +# Issue #325 — runbook init delivery v5 + +## Context + +Autonomous pickup of kiwifs/kiwifs#325 on branch `feat/issue-325-runbook-init-delivery-v2` +(rebased from `main`). UC-6 runbook init template was on main; embed filter hardening and +service wiki-link stubs were missing from main (only on fork branch). + +## Pre-search + +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-init-template.md` +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-embed-filter.md` +- Kiwi cluster at 192.168.167.240:3333 unreachable; wrote docs locally for fleet sync + +## Changes + +1. `internal/workspace/embed_filter.go` — filteredTemplatesFS hides legacy runbook dirs +2. `internal/workspace/embed_filter_test.go` — path filter regression tests +3. `internal/workspace/init.go` — wrap templatesRaw with filtered FS +4. `internal/workspace/templates/runbook/services/{api-service,monitoring}.md` — wiki-link targets +5. Updated SCHEMA.md, index.md, runbook_template_test.go, cmd/init_test.go, init_test.go + +## Verification + +```bash +go test ./internal/workspace/... ./cmd/... -run 'Runbook|runbook|Embed|embed|Filtered' -count=1 # PASS +go test ./... -count=1 # PASS +``` + +## Acceptance criteria + +| Criterion | Status | +|-----------|--------| +| `kiwifs init --template runbook` scaffolds workspace | PASS | +| Example runbook has 7 sections + fenced code blocks | PASS | +| JSON Schema validates required frontmatter | PASS | +| `kiwifs check` passes on generated scaffold | PASS | +| Legacy embed paths filtered from init | PASS | +| Service wiki links resolve in scaffold | PASS | + +## Outcome + +Ready for fleet publish: push branch, open PR closing #325. diff --git a/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v6.md b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v6.md new file mode 100644 index 00000000..ec6a9750 --- /dev/null +++ b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v6.md @@ -0,0 +1,39 @@ +--- +memory_kind: episodic +episode_id: cursor-hands-on-325-2026-07-03-v6 +title: "Issue #325 — hands-on takeover verification and PR publish" +tags: [kiwifs, runbooks, issue-325, init-template, embed-filter, uc-6, hands-on] +date: 2026-07-03 +--- + +# Issue #325 — runbook init delivery v6 (hands-on takeover) + +## Context + +Fleet engineer failed delivery check (`no_committed_diff`, `peer_review_not_passed`). +Hands-on takeover verified commit `2fb9dba` on `feat/issue-325-runbook-init-delivery-v2`. + +## Pre-search + +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-init-template.md` +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-embed-filter.md` +- Kiwi cluster at 192.168.167.240:3333 unreachable; updated local fix docs + +## Verification + +```bash +go test ./internal/workspace/... ./cmd/... -run 'Runbook|runbook|Embed|embed|Filtered' -count=1 # PASS +go test ./... -count=1 # PASS (~54s) +go build -o /tmp/kiwifs-test . && kiwifs init --template runbook && kiwifs check # exit 0 +``` + +## Peer review + +- `filteredTemplatesFS` correctly excludes legacy runbook dirs from embed +- Service stubs resolve `[[api-service]]` and `[[monitoring]]` wiki links +- Regression tests cover embed filter, scaffold paths, schema validation, lint +- Manual init + check exits 0 (info-level janitor hints only) + +## Outcome + +Push branch and open PR closing #325. diff --git a/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v7.md b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v7.md new file mode 100644 index 00000000..c2c8dab9 --- /dev/null +++ b/episodes/agents/cursor-hands-on-325/2026-07-03-runbook-init-delivery-v7.md @@ -0,0 +1,46 @@ +--- +memory_kind: episodic +episode_id: cursor-hands-on-325-2026-07-03-v7 +title: "Issue #325 — hands-on takeover v7: green tests, PR publish" +tags: [kiwifs, runbooks, issue-325, init-template, embed-filter, uc-6, hands-on] +date: 2026-07-03 +--- + +# Issue #325 — runbook init delivery v7 (hands-on takeover) + +## Context + +Fleet engineer delivery check failed (`no_committed_diff`, `peer_review_not_passed`). +Hands-on takeover verified commit `2fb9dba` on `feat/issue-325-runbook-init-delivery-v2` +and completed peer review + full test suite before PR publish. + +## Pre-search + +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-init-template.md` +- Read `pages/fixes/kiwifs-kiwifs/issue-325-runbook-embed-filter.md` +- Kiwi cluster at 192.168.167.240:3333 unreachable; local fix docs updated + +## Verification + +```bash +go test ./internal/workspace/... ./cmd/... -run 'Runbook|runbook|Embed|embed|Filtered' -count=1 # PASS +go test ./... -count=1 # PASS (~55s) +go build -o /tmp/kiwifs-test . && kiwifs init --root /tmp/runbook-init-test --template runbook && kiwifs check --root /tmp/runbook-init-test # exit 0 +``` + +Manual `kiwifs check` reports info-level hints only (missing-owner/status on README/SCHEMA/playbook, orphan README). + +## Peer review + +**Status: pass** + +- `filteredTemplatesFS` excludes legacy `incidents/`, `postmortems/`, `procedures/` from embed +- Service stubs resolve `[[api-service]]` and `[[monitoring]]` wiki links in scaffold +- `TestRunbookEmbedUsesUC6ScaffoldOnly` and `TestFilteredTemplatesFSHidesLegacyRunbookPaths` guard regressions +- JSON Schema validates required frontmatter (`trigger`, `severity`, `owner`, `services`) +- `example-high-cpu.md` has all 7 UC-6 sections with fenced code blocks +- Full suite green; manual init + check exits 0 + +## Outcome + +Push `feat/issue-325-runbook-init-delivery-v2` and open PR closing #325. diff --git a/internal/workspace/embed_filter.go b/internal/workspace/embed_filter.go new file mode 100644 index 00000000..55b9db61 --- /dev/null +++ b/internal/workspace/embed_filter.go @@ -0,0 +1,83 @@ +package workspace + +import ( + "io/fs" + "path/filepath" + "strings" +) + +// excludedEmbedDirs lists template paths that must not ship via kiwifs init. +// go:embed all:templates includes every file on disk under templates/, including +// local dev scaffolds and pre-UC-6 runbook dirs with placeholder wiki links +// that fail schema.Lint / kiwifs check on generated workspaces. +var excludedEmbedDirs = map[string]bool{ + "templates/runbook/incidents": true, + "templates/runbook/postmortems": true, + "templates/runbook/procedures": true, + "templates/knowledge": true, // superseded by memory/kb templates + "templates/research/experiments": true, // dev-only; not part of research init + "templates/research/literature": true, // dev-only; research uses papers/ +} + +func isExcludedEmbedPath(path string) bool { + path = filepath.ToSlash(path) + if excludedEmbedDirs[path] { + return true + } + for dir := range excludedEmbedDirs { + if strings.HasPrefix(path, dir+"/") { + return true + } + } + return false +} + +type filteredTemplatesFS struct { + inner fs.FS +} + +func (f filteredTemplatesFS) Open(name string) (fs.File, error) { + if isExcludedEmbedPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + return f.inner.Open(name) +} + +func (f filteredTemplatesFS) ReadFile(name string) ([]byte, error) { + if isExcludedEmbedPath(name) { + return nil, &fs.PathError{Op: "read", Path: name, Err: fs.ErrNotExist} + } + if rf, ok := f.inner.(fs.ReadFileFS); ok { + return rf.ReadFile(name) + } + return fs.ReadFile(f.inner, name) +} + +func (f filteredTemplatesFS) Stat(name string) (fs.FileInfo, error) { + if isExcludedEmbedPath(name) { + return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist} + } + if sf, ok := f.inner.(fs.StatFS); ok { + return sf.Stat(name) + } + return fs.Stat(f.inner, name) +} + +func (f filteredTemplatesFS) ReadDir(name string) ([]fs.DirEntry, error) { + if isExcludedEmbedPath(name) { + return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist} + } + entries, err := fs.ReadDir(f.inner, name) + if err != nil { + return nil, err + } + out := make([]fs.DirEntry, 0, len(entries)) + for _, e := range entries { + path := filepath.Join(name, e.Name()) + if isExcludedEmbedPath(path) { + continue + } + out = append(out, e) + } + return out, nil +} diff --git a/internal/workspace/embed_filter_test.go b/internal/workspace/embed_filter_test.go new file mode 100644 index 00000000..9b84b78c --- /dev/null +++ b/internal/workspace/embed_filter_test.go @@ -0,0 +1,63 @@ +package workspace + +import ( + "io/fs" + "testing" +) + +func TestIsExcludedEmbedPath(t *testing.T) { + t.Parallel() + cases := []struct { + path string + want bool + }{ + {"templates/runbook/incidents", true}, + {"templates/runbook/incidents/template.md", true}, + {"templates/runbook/postmortems/template.md", true}, + {"templates/runbook/procedures/scale-up.md", true}, + {"templates/research/experiments/exp-001-baseline.md", true}, + {"templates/research/literature/example-paper.md", true}, + {"templates/knowledge/index.md", true}, + {"templates/runbook/example-high-cpu.md", false}, + {"templates/runbook/SCHEMA.md", false}, + {"templates/runbook/.kiwi/schemas/runbook.json", false}, + {"templates/runbook/services/api-service.md", false}, + {"templates/research/papers/example-paper.md", false}, + {"templates/wiki/index.md", false}, + } + for _, tc := range cases { + if got := isExcludedEmbedPath(tc.path); got != tc.want { + t.Errorf("isExcludedEmbedPath(%q) = %v, want %v", tc.path, got, tc.want) + } + } +} + +func TestFilteredTemplatesFSHidesLegacyRunbookPaths(t *testing.T) { + t.Parallel() + legacy := []string{ + "templates/runbook/incidents/template.md", + "templates/runbook/postmortems/template.md", + "templates/runbook/procedures/deploy-rollback.md", + "templates/knowledge/index.md", + "templates/research/experiments/exp-001-baseline.md", + } + for _, p := range legacy { + if _, err := templates.ReadFile(p); err == nil { + t.Fatalf("filtered FS should hide %q", p) + } + if _, err := fs.Stat(templates, p); err == nil { + t.Fatalf("filtered FS Stat should hide %q", p) + } + } + + entries, err := fs.ReadDir(templates, "templates/runbook") + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + switch e.Name() { + case "incidents", "postmortems", "procedures": + t.Fatalf("legacy dir %q listed in templates/runbook", e.Name()) + } + } +} diff --git a/internal/workspace/init.go b/internal/workspace/init.go index 7a3ba44a..870d6d8b 100644 --- a/internal/workspace/init.go +++ b/internal/workspace/init.go @@ -11,7 +11,11 @@ import ( ) //go:embed all:templates -var templates embed.FS +var templatesRaw embed.FS + +// templates hides legacy runbook scaffold paths that may still exist on disk +// during development but must not ship via kiwifs init --template runbook. +var templates filteredTemplatesFS = filteredTemplatesFS{inner: templatesRaw} // InitTemplate describes a workspace scaffold available at space creation. type InitTemplate struct { diff --git a/internal/workspace/init_test.go b/internal/workspace/init_test.go index dd1b0941..2c97318a 100644 --- a/internal/workspace/init_test.go +++ b/internal/workspace/init_test.go @@ -216,6 +216,8 @@ func TestTemplatesEmbedded(t *testing.T) { "templates/runbook/.kiwi/schemas/runbook.json", "templates/runbook/.kiwi/config.toml", "templates/runbook/.kiwi/templates/runbook.md", + "templates/runbook/services/api-service.md", + "templates/runbook/services/monitoring.md", } for _, p := range paths { if _, err := fs.Stat(templates, p); err != nil { diff --git a/internal/workspace/runbook_template_test.go b/internal/workspace/runbook_template_test.go index b62dddfb..94ad7ed1 100644 --- a/internal/workspace/runbook_template_test.go +++ b/internal/workspace/runbook_template_test.go @@ -58,11 +58,18 @@ func TestInitRunbookTemplateScaffold(t *testing.T) { "example-high-cpu.md", "index.md", "SCHEMA.md", + "services/api-service.md", + "services/monitoring.md", } { if _, err := os.Stat(filepath.Join(root, p)); err != nil { t.Fatalf("missing %s: %v", p, err) } } + for _, omitted := range []string{"incidents", "postmortems", "procedures"} { + if _, err := os.Stat(filepath.Join(root, omitted)); err == nil { + t.Fatalf("legacy dir %q must not ship via init", omitted) + } + } } func TestRunbookTemplateLintClean(t *testing.T) { @@ -265,3 +272,34 @@ func TestInitRunbookDoesNotOverwriteExisting(t *testing.T) { t.Fatal("expected example-high-cpu.md to be created alongside existing index.md") } } + +func TestRunbookEmbedUsesUC6ScaffoldOnly(t *testing.T) { + t.Parallel() + legacy := []string{ + "templates/runbook/incidents/template.md", + "templates/runbook/postmortems/template.md", + "templates/runbook/procedures/deploy-rollback.md", + "templates/runbook/procedures/rotate-secrets.md", + "templates/runbook/procedures/scale-up.md", + "templates/knowledge/index.md", + } + for _, p := range legacy { + if _, err := templates.ReadFile(p); err == nil { + t.Fatalf("legacy runbook scaffold %q must not be embedded (breaks lint via placeholder wiki links)", p) + } + } + required := []string{ + "templates/runbook/SCHEMA.md", + "templates/runbook/index.md", + "templates/runbook/example-high-cpu.md", + "templates/runbook/.kiwi/schemas/runbook.json", + "templates/runbook/.kiwi/templates/runbook.md", + "templates/runbook/services/api-service.md", + "templates/runbook/services/monitoring.md", + } + for _, p := range required { + if _, err := templates.ReadFile(p); err != nil { + t.Fatalf("required UC-6 runbook scaffold missing %q: %v", p, err) + } + } +} diff --git a/internal/workspace/templates/runbook/SCHEMA.md b/internal/workspace/templates/runbook/SCHEMA.md index df492b6c..9f0716e0 100644 --- a/internal/workspace/templates/runbook/SCHEMA.md +++ b/internal/workspace/templates/runbook/SCHEMA.md @@ -10,7 +10,11 @@ by `.kiwi/schemas/runbook.json`. Blank runbooks start from example-high-cpu.md Reference runbook with all seven sections index.md Table of contents and severity guide + playbook.md Agent/MCP operations for runbook execution SCHEMA.md This file — structure and conventions + services/ Wiki-link targets for runbook frontmatter `services` + api-service.md + monitoring.md .kiwi/ schemas/runbook.json Runbook frontmatter validation templates/runbook.md Blank 7-section runbook template diff --git a/internal/workspace/templates/runbook/index.md b/internal/workspace/templates/runbook/index.md index ab5aec36..853c2886 100644 --- a/internal/workspace/templates/runbook/index.md +++ b/internal/workspace/templates/runbook/index.md @@ -47,7 +47,15 @@ SORT severity ASC - **Monitoring dashboard:** _link to Grafana/Datadog/etc._ - **Incident channel:** `#incidents` +## Services + +| Service | Owner | Notes | +|---------|-------|-------| +| [[api-service]] | platform-oncall | Primary API tier referenced by runbooks | +| [[monitoring]] | observability | Metrics and alerting stack | + ## Related Docs - [[SCHEMA]] — frontmatter fields and section requirements - [[example-high-cpu]] — fully worked example with commands and escalation +- [[playbook]] — agent/MCP operations for runbook execution diff --git a/internal/workspace/templates/runbook/services/api-service.md b/internal/workspace/templates/runbook/services/api-service.md new file mode 100644 index 00000000..b9cc5bef --- /dev/null +++ b/internal/workspace/templates/runbook/services/api-service.md @@ -0,0 +1,12 @@ +--- +type: service +title: API Service +owner: platform-oncall +status: active +next-review: 2026-09-01 +--- + +# API Service + +Primary HTTP API tier referenced by operational runbooks. Update this page when +service ownership, on-call rotation, or dependency maps change. diff --git a/internal/workspace/templates/runbook/services/monitoring.md b/internal/workspace/templates/runbook/services/monitoring.md new file mode 100644 index 00000000..1bf6d310 --- /dev/null +++ b/internal/workspace/templates/runbook/services/monitoring.md @@ -0,0 +1,11 @@ +--- +type: service +title: Monitoring +owner: observability +status: active +next-review: 2026-09-01 +--- + +# Monitoring + +Metrics, dashboards, and alerting stack used to detect runbook triggers.