From feab25bfa191e0de69665672b985b6e57ac75703 Mon Sep 17 00:00:00 2001
From: "Tom Liu (Liu Hsiu-Chi)" <0xtomlord1122@gmail.com>
Date: Sun, 18 Oct 2026 22:41:00 +0800
Subject: [PATCH 1/3] fix(skills): clear the skills badge when opening skills
The sidebar dot stayed on after the user opened Skills because only an
opened live skill was marked seen, and the page defaults to drafts.
Opening Skills now clears the update badge. Parent write_skill also
publishes the same update event so a direct skill write lights the dot.
Signed-off-by: Tom Liu (Liu Hsiu-Chi) <0xtomlord1122@gmail.com>
---
.../shell/app-shell-shortcuts.svelte.ts | 2 +
.../components/sidebar/SidebarFooter.svelte | 5 ++-
.../lib/stores/skill-updates.svelte.test.ts | 5 +++
cometmind/internal/tools/registry_add.go | 2 +-
.../internal/tools/skills/write_skill.go | 19 +++++++-
.../internal/tools/skills/write_skill_test.go | 43 +++++++++++++++++++
6 files changed, 72 insertions(+), 4 deletions(-)
create mode 100644 cometmind/internal/tools/skills/write_skill_test.go
diff --git a/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts b/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts
index 5c3f6075..95ffd9ef 100644
--- a/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts
+++ b/cometline/src/lib/features/shell/app-shell-shortcuts.svelte.ts
@@ -11,6 +11,7 @@ import { openSettings } from '#lib/actions/open-settings.js';
import { shouldUseWorkspacePanelHistory } from '#lib/features/shell/focus-nav.js';
import { isReloadShortcut, matchesShortcut, type ShortcutAction } from '#lib/keyboard-shortcuts.js';
import { inboxStore } from '#lib/stores/inbox.svelte.js';
+import { skillUpdatesStore } from '#lib/stores/skill-updates.svelte.js';
import { sessionStore } from '#lib/stores/session.svelte.js';
import { settingsStore } from '#lib/stores/settings.svelte.js';
import { shellStore } from '#lib/stores/shell.svelte.js';
@@ -218,6 +219,7 @@ export function createAppShellShortcuts(deps: {
case 'openSkillDrafts':
if (shellStore.settingsOpen) shellStore.closeSettings();
inboxStore.closeDrawer();
+ skillUpdatesStore.markSeen();
void goto(resolve('skills'));
return;
case 'openGallery':
diff --git a/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte b/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte
index 29d119b8..1bc5623c 100644
--- a/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte
+++ b/cometline/src/lib/features/sidebar/components/sidebar/SidebarFooter.svelte
@@ -36,7 +36,10 @@
class="nav-badge"
class:has-badge={skillDraftsStore.hasDrafts || skillUpdatesStore.hasUnseen}
class:active={page.url.pathname === '/skills' || page.url.pathname === '/skill-drafts'}
- onclick={() => goto(resolve('skills'))}
+ onclick={() => {
+ skillUpdatesStore.markSeen();
+ void goto(resolve('skills'));
+ }}
>
diff --git a/cometline/src/lib/stores/skill-updates.svelte.test.ts b/cometline/src/lib/stores/skill-updates.svelte.test.ts
index b53f7e0f..1fb98d67 100644
--- a/cometline/src/lib/stores/skill-updates.svelte.test.ts
+++ b/cometline/src/lib/stores/skill-updates.svelte.test.ts
@@ -22,5 +22,10 @@ describe('skillUpdatesStore', () => {
skillUpdatesStore.note(['ship-checklist']);
expect(skillUpdatesStore.hasUnseen).toBe(false);
+
+ skillUpdatesStore.note(['ship-checklist', 'retry-pattern']);
+ expect(skillUpdatesStore.unseen).toEqual(['retry-pattern']);
+ skillUpdatesStore.markSeen();
+ expect(skillUpdatesStore.hasUnseen).toBe(false);
});
});
diff --git a/cometmind/internal/tools/registry_add.go b/cometmind/internal/tools/registry_add.go
index 9c6ac96d..93a18e31 100644
--- a/cometmind/internal/tools/registry_add.go
+++ b/cometmind/internal/tools/registry_add.go
@@ -53,7 +53,7 @@ func (r *Registry) addSkillTools(surface ToolSurface, opt RegistryOptions) {
r.Add(ReadSkillDraft{})
}
if surface.SkillMut {
- r.Add(WriteSkill{})
+ r.Add(WriteSkill{Events: opt.MemoryEvents})
r.Add(PromoteSkillDraft{})
}
}
diff --git a/cometmind/internal/tools/skills/write_skill.go b/cometmind/internal/tools/skills/write_skill.go
index b962b97d..7da33cd3 100644
--- a/cometmind/internal/tools/skills/write_skill.go
+++ b/cometmind/internal/tools/skills/write_skill.go
@@ -3,13 +3,17 @@ package skills
import (
"context"
"encoding/json"
+ "strings"
+ "github.com/Cometline/cometline/cometmind/internal/event"
"github.com/Cometline/cometline/cometmind/internal/skills"
"github.com/Cometline/cometline/cometmind/internal/tools/toolkit"
)
// WriteSkill creates or updates an Agent Skill under ~/.cometmind/skills.
-type WriteSkill struct{}
+type WriteSkill struct {
+ Events *event.Hub
+}
func (WriteSkill) Spec() ToolSpec {
return ToolSpec{
@@ -27,7 +31,7 @@ func (WriteSkill) Spec() ToolSpec {
}
}
-func (WriteSkill) Execute(_ context.Context, input json.RawMessage) (Result, error) {
+func (t WriteSkill) Execute(ctx context.Context, input json.RawMessage) (Result, error) {
var in struct {
Name *string `json:"name"`
Content *string `json:"content"`
@@ -47,5 +51,16 @@ func (WriteSkill) Execute(_ context.Context, input json.RawMessage) (Result, err
if err := skills.WriteSkill(name, content, in.Overwrite); err != nil {
return Result{OK: false, Output: err.Error()}, nil
}
+ if t.Events != nil {
+ action := "created"
+ if in.Overwrite {
+ action = "updated"
+ }
+ t.Events.Publish(event.SkillReviewUpdated(toolkit.ToolSessionFrom(ctx), []event.SkillReviewChange{{
+ Name: name,
+ Action: action,
+ Description: strings.TrimSpace(skills.SkillMarkdownDescription(content)),
+ }}))
+ }
return Result{OK: true, Output: "skill " + name + " written to ~/.cometmind/skills/" + name + "/SKILL.md"}, nil
}
diff --git a/cometmind/internal/tools/skills/write_skill_test.go b/cometmind/internal/tools/skills/write_skill_test.go
new file mode 100644
index 00000000..b59a5516
--- /dev/null
+++ b/cometmind/internal/tools/skills/write_skill_test.go
@@ -0,0 +1,43 @@
+package skills
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/Cometline/cometline/cometmind/internal/event"
+)
+
+func TestWriteSkillPublishesUpdate(t *testing.T) {
+ t.Setenv("HOME", t.TempDir())
+ hub := event.NewHub()
+ sub := hub.Subscribe()
+ defer sub.Close()
+
+ content := "---\nname: ship-checklist\ndescription: Ship a reviewed change\n---\n\n# Steps\n"
+ payload, err := json.Marshal(map[string]any{"name": "ship-checklist", "content": content})
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := WithToolSession(t.Context(), "sess-1")
+ res, err := (WriteSkill{Events: hub}).Execute(ctx, payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !res.OK {
+ t.Fatalf("write failed: %s", res.Output)
+ }
+
+ select {
+ case ev := <-sub.Events:
+ if ev.Kind != event.KindSkillReviewUpdated || ev.SessionID != "sess-1" || len(ev.SkillReviews) != 1 {
+ t.Fatalf("event = %+v", ev)
+ }
+ got := ev.SkillReviews[0]
+ if got.Name != "ship-checklist" || got.Action != "created" || got.Description != "Ship a reviewed change" {
+ t.Fatalf("change = %+v", got)
+ }
+ case <-time.After(time.Second):
+ t.Fatal("missing skill update event")
+ }
+}
From f4aad4c1852915d9c2c80c5f6a712d2ab4a95057 Mon Sep 17 00:00:00 2001
From: "Tom Liu (Liu Hsiu-Chi)" <0xtomlord1122@gmail.com>
Date: Sun, 18 Oct 2026 22:41:03 +0800
Subject: [PATCH 2/3] feat(skills): show when self-improvement skills expire
Unpinned self-improvement skills archive 30 days after last use, then
delete 10 days after archive. The skills page now shows that countdown
on the list, preview, and archived rows.
Signed-off-by: Tom Liu (Liu Hsiu-Chi) <0xtomlord1122@gmail.com>
---
.../skills-page/SkillsBrowseView.svelte | 32 +++++++++++++++++--
.../lib/features/skills/skill-expiry.test.ts | 23 +++++++++++++
.../src/lib/features/skills/skill-expiry.ts | 15 +++++++++
.../lib/generated/cometmind-api/types.gen.ts | 4 +++
cometmind/internal/apigen/types.gen.go | 3 ++
.../internal/server/skill_curator_handlers.go | 3 ++
cometmind/internal/server/skill_handlers.go | 8 ++++-
cometmind/internal/skillcurator/service.go | 29 +++++++++++++++--
cometmind/internal/skills/curator.go | 26 +++++++++++++++
cometmind/internal/skills/curator_test.go | 24 ++++++++++++++
cometmind/openapi.yaml | 4 +++
11 files changed, 166 insertions(+), 5 deletions(-)
create mode 100644 cometline/src/lib/features/skills/skill-expiry.test.ts
create mode 100644 cometline/src/lib/features/skills/skill-expiry.ts
diff --git a/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte b/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte
index 2f8525a1..7011879e 100644
--- a/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte
+++ b/cometline/src/lib/features/skills/components/skills-page/SkillsBrowseView.svelte
@@ -1,6 +1,7 @@