diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts
index adf87487..aa8609e7 100644
--- a/mateclaw-ui/src/api/index.ts
+++ b/mateclaw-ui/src/api/index.ts
@@ -185,6 +185,8 @@ export const skillApi = {
enabled?: boolean
/** 'PASSED' / 'FAILED' — filters by security_scan_status. */
scanStatus?: string
+ /** 'active' / 'stale' / 'archived' — filters by lifecycle_state. */
+ lifecycleState?: string
} = {}) => http.get('/skills', { params }),
/** Tab count aggregate — returns { all, builtin, mcp, dynamic } */
counts: () => http.get('/skills/counts'),
@@ -217,6 +219,35 @@ export const skillApi = {
http.post(`/skills/${id}/secrets`, { key, value }),
deleteSecret: (id: string | number, key: string) =>
http.delete(`/skills/${id}/secrets/${encodeURIComponent(key)}`),
+
+ // ---- Lifecycle curator ----
+ /** Pin / unpin a skill — pinned skills are never auto-archived. */
+ pin: (id: string | number, pinned: boolean) =>
+ http.post(`/skills/${id}/pin`, { pinned }),
+ /**
+ * Manually archive a skill. When the skill is bound to an enabled agent
+ * and {@code force} is false, the backend replies HTTP 409 with
+ * {@code code: 'BOUND_SKILL_CONFIRM_REQUIRED'} and a {@code boundAgents}
+ * list; retry with {@code force: true} to confirm.
+ */
+ archive: (id: string | number, opts: { force?: boolean; reason?: string } = {}) =>
+ http.post(`/skills/${id}/archive`, { reason: opts.reason ?? null },
+ { params: { force: opts.force ?? false } }),
+ /** Restore an archived skill back to active. */
+ restore: (id: string | number) => http.post(`/skills/${id}/restore`),
+ /** Curator control-panel status (config / control / counts / lastReport). */
+ curatorStatus: () => http.get('/skills/curator/status'),
+ /** Run a curator dry-run preview immediately. */
+ curatorDryRun: () => http.post('/skills/curator/dry-run'),
+ /** Activate (apply transitions) or deactivate (preview-only) the curator. */
+ curatorActivate: (activate: boolean) =>
+ http.post('/skills/curator/activate', null, { params: { activate } }),
+ curatorPause: () => http.post('/skills/curator/pause'),
+ curatorResume: () => http.post('/skills/curator/resume'),
+ /** List recent curator run report ids. */
+ curatorReports: () => http.get('/skills/curator/reports'),
+ /** Read one curator run report (parsed run.json). */
+ curatorReport: (runId: string) => http.get(`/skills/curator/reports/${runId}`),
}
/** Shape returned by GET /skills/{id}/secrets. */
diff --git a/mateclaw-ui/src/components/skill/SkillLineIcon.vue b/mateclaw-ui/src/components/skill/SkillLineIcon.vue
new file mode 100644
index 00000000..3c46e7c9
--- /dev/null
+++ b/mateclaw-ui/src/components/skill/SkillLineIcon.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/mateclaw-ui/src/components/skill/lineIcons.ts b/mateclaw-ui/src/components/skill/lineIcons.ts
new file mode 100644
index 00000000..79b4d084
--- /dev/null
+++ b/mateclaw-ui/src/components/skill/lineIcons.ts
@@ -0,0 +1,20 @@
+/**
+ * Feather-style line-art icons for the skill catalog and lifecycle curator.
+ * Each glyph is drawn on a 24x24 canvas and stroked with `currentColor`.
+ * Consumed by SkillLineIcon.vue so the skill surfaces share one icon source
+ * instead of inlining raw SVG or emoji — mirrors the memory module's registry.
+ */
+export const skillLineIcons = {
+ // Catalog source tabs
+ all: '',
+ builtin: '',
+ mcp: '',
+ acp: '',
+ dynamic: '',
+ // Lifecycle
+ clock: '',
+ archive: '',
+ pin: '',
+} as const
+
+export type SkillLineIconName = keyof typeof skillLineIcons
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 650f58b9..605b63b4 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -573,6 +573,7 @@ export default {
featureFlags: 'Feature Flags',
about: 'About',
advanced: 'Advanced',
+ skillCurator: 'Skill Curator',
},
models: {
sidecar: {
@@ -2980,6 +2981,34 @@ export default {
mcp: 'MCP',
acp: 'ACP',
dynamic: 'Dynamic',
+ stale: 'Stale',
+ archived: 'Archived',
+ },
+ lifecycle: {
+ section: 'Lifecycle',
+ state: 'State',
+ lastUsed: 'Last used',
+ neverUsed: 'Never used',
+ today: 'Today',
+ daysAgo: '{n}d ago',
+ states: {
+ active: 'Active',
+ stale: 'Stale',
+ archived: 'Archived',
+ },
+ pin: 'Pin this skill',
+ pinHint: 'Pinned skills are never auto-archived',
+ archive: 'Archive',
+ restore: 'Restore',
+ pinFailed: 'Failed to update pin',
+ archiveTitle: 'Archive skill',
+ archiveConfirm: 'Archive "{name}"? The skill will be disabled and can be restored anytime.',
+ archiveSuccess: 'Skill archived',
+ archiveFailed: 'Failed to archive',
+ restoreSuccess: 'Skill restored',
+ restoreFailed: 'Failed to restore',
+ boundConfirmTitle: 'Skill is bound to agents',
+ boundConfirmBody: '"{name}" is explicitly bound to {count} enabled agent(s) ({agents}). Archiving removes that capability from them. Continue?',
},
search: {
placeholder: 'Search by name, description, or tags...',
@@ -3209,6 +3238,52 @@ export default {
tooLarge: 'File too large (max 50MB)',
},
},
+ skillCurator: {
+ title: 'Skill Curator',
+ desc: 'Automatically archives long-idle skills on a time window to keep the skill catalog tidy.',
+ stateEnabled: 'Deployed',
+ stateActivated: 'Activated',
+ statePreview: 'Preview only',
+ statePaused: 'Paused',
+ config: 'Configuration',
+ scope: 'Scope',
+ staleAfter: 'Stale after (days)',
+ archiveAfter: 'Archive after (days)',
+ cron: 'Schedule',
+ counts: 'Current counts',
+ active: 'Active',
+ stale: 'Stale',
+ archived: 'Archived',
+ pinned: 'Pinned',
+ blockedByBindings: 'Binding-protected',
+ control: 'Runtime control',
+ lastObservedAt: 'First observed',
+ lastDryRunAt: 'Last preview',
+ lastRunAt: 'Last run',
+ nextScheduledRun: 'Next run',
+ activate: 'Activate curator',
+ deactivate: 'Back to preview',
+ pause: 'Pause',
+ resume: 'Resume',
+ runDryRun: 'Preview now',
+ activatedHint: 'The curator is activated and archives idle skills on schedule.',
+ previewHint: 'The curator is in preview-only mode and never archives. Review a preview report, then activate.',
+ pausedHint: 'The curator is paused; the scheduled sweep will not run.',
+ reports: 'Run reports',
+ noReports: 'No run reports yet',
+ reportPlanned: 'Planned',
+ reportApplied: 'Applied',
+ reportScanned: 'Scanned',
+ reportDryRun: 'Preview',
+ reportTransitions: 'Transitions',
+ activateSuccess: 'Skill curator activated',
+ deactivateSuccess: 'Back to preview-only mode',
+ pauseSuccess: 'Skill curator paused',
+ resumeSuccess: 'Skill curator resumed',
+ dryRunSuccess: 'Preview report generated',
+ actionFailed: 'Action failed',
+ loadFailed: 'Failed to load skill curator status',
+ },
onboarding: {
title: 'Welcome to MateClaw',
subtitle: 'Set up your first AI model',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index ed90c4f9..2176baed 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -453,6 +453,7 @@ export default {
featureFlags: '功能开关',
about: '关于',
advanced: '高级',
+ skillCurator: '技能管家',
},
models: {
sidecar: {
@@ -3072,6 +3073,34 @@ export default {
mcp: 'MCP',
acp: 'ACP',
dynamic: '动态',
+ stale: '待归档',
+ archived: '已归档',
+ },
+ lifecycle: {
+ section: '生命周期',
+ state: '状态',
+ lastUsed: '上次使用',
+ neverUsed: '从未使用',
+ today: '今天',
+ daysAgo: '{n} 天前',
+ states: {
+ active: '活跃',
+ stale: '待归档',
+ archived: '已归档',
+ },
+ pin: '钉住此技能',
+ pinHint: '钉住后永不会被自动归档',
+ archive: '归档',
+ restore: '恢复',
+ pinFailed: '钉住操作失败',
+ archiveTitle: '归档技能',
+ archiveConfirm: '确定归档「{name}」吗?归档后该技能将停用,可随时恢复。',
+ archiveSuccess: '技能已归档',
+ archiveFailed: '归档失败',
+ restoreSuccess: '技能已恢复',
+ restoreFailed: '恢复失败',
+ boundConfirmTitle: '该技能已被 Agent 绑定',
+ boundConfirmBody: '「{name}」被 {count} 个启用中的 Agent 显式绑定({agents})。归档后这些 Agent 将失去该能力,确定继续?',
},
search: {
placeholder: '搜索技能名称、描述或标签...',
@@ -3301,6 +3330,52 @@ export default {
tooLarge: '文件过大(最大 50MB)',
},
},
+ skillCurator: {
+ title: '技能管家',
+ desc: '按时间窗口自动归档长期闲置的技能,保持技能目录整洁。',
+ stateEnabled: '已部署',
+ stateActivated: '已激活',
+ statePreview: '仅预览',
+ statePaused: '已暂停',
+ config: '配置',
+ scope: '清理范围',
+ staleAfter: 'stale 阈值(天)',
+ archiveAfter: '归档阈值(天)',
+ cron: '调度表达式',
+ counts: '当前统计',
+ active: '活跃',
+ stale: '待归档',
+ archived: '已归档',
+ pinned: '已钉住',
+ blockedByBindings: '被绑定保护',
+ control: '运行控制',
+ lastObservedAt: '首次观测',
+ lastDryRunAt: '上次预览',
+ lastRunAt: '上次运行',
+ nextScheduledRun: '下次调度',
+ activate: '激活管家',
+ deactivate: '退回仅预览',
+ pause: '暂停',
+ resume: '恢复',
+ runDryRun: '立即预览',
+ activatedHint: '管家已激活,每日按调度真正归档闲置技能。',
+ previewHint: '管家处于仅预览模式,不会真正归档。查看预览报告满意后再激活。',
+ pausedHint: '管家已暂停,定时扫描不会运行。',
+ reports: '运行报告',
+ noReports: '暂无运行报告',
+ reportPlanned: '计划',
+ reportApplied: '执行',
+ reportScanned: '扫描',
+ reportDryRun: '预览',
+ reportTransitions: '状态变更',
+ activateSuccess: '技能管家已激活',
+ deactivateSuccess: '已退回仅预览模式',
+ pauseSuccess: '技能管家已暂停',
+ resumeSuccess: '技能管家已恢复',
+ dryRunSuccess: '预览报告已生成',
+ actionFailed: '操作失败',
+ loadFailed: '加载技能管家状态失败',
+ },
onboarding: {
title: '欢迎使用 MateClaw',
subtitle: '配置你的第一个 AI 模型',
diff --git a/mateclaw-ui/src/router/index.ts b/mateclaw-ui/src/router/index.ts
index 13696a7f..3e05f4e1 100644
--- a/mateclaw-ui/src/router/index.ts
+++ b/mateclaw-ui/src/router/index.ts
@@ -182,6 +182,12 @@ const router = createRouter({
component: () => import('@/views/CronJobs.vue'),
meta: { title: 'Settings - Cron Jobs', requiredCapability: 'manage:agents' },
},
+ {
+ path: 'skill-curator',
+ name: 'SettingsSkillCurator',
+ component: () => import('@/views/Settings/SkillCurator/index.vue'),
+ meta: { title: 'Settings - Skill Curator', requiredCapability: 'manage:settings' },
+ },
{
path: 'workflows',
name: 'SettingsWorkflows',
diff --git a/mateclaw-ui/src/types/index.ts b/mateclaw-ui/src/types/index.ts
index 83b970aa..9d0e6970 100644
--- a/mateclaw-ui/src/types/index.ts
+++ b/mateclaw-ui/src/types/index.ts
@@ -285,6 +285,14 @@ export interface Skill {
securityScanResult?: string
/** RFC-042 §2.3 — wall-clock time of the last scan */
securityScanTime?: string
+ /** Lifecycle curator state: 'active' / 'stale' / 'archived' */
+ lifecycleState?: string
+ /** User-pinned skill — exempt from automatic archival */
+ pinned?: boolean
+ /** Last activity timestamp — drives the curator's idle window */
+ lastActivityAt?: string
+ /** When the curator moved this skill to archived state */
+ archivedAt?: string
}
/** 运行时解析状态(来自 /runtime/status) */
diff --git a/mateclaw-ui/src/views/Settings/Layout.vue b/mateclaw-ui/src/views/Settings/Layout.vue
index 684e990a..ef0635cc 100644
--- a/mateclaw-ui/src/views/Settings/Layout.vue
+++ b/mateclaw-ui/src/views/Settings/Layout.vue
@@ -167,6 +167,12 @@ const sections = computed(() => [
label: t('nav.triggers', 'Triggers'),
icon: '',
},
+ {
+ id: 'skill-curator',
+ path: '/settings/skill-curator',
+ label: t('settings.sections.skillCurator', 'Skill Curator'),
+ icon: '',
+ },
{
id: 'datasources',
path: '/settings/datasources',
diff --git a/mateclaw-ui/src/views/Settings/SkillCurator/index.vue b/mateclaw-ui/src/views/Settings/SkillCurator/index.vue
new file mode 100644
index 00000000..698f024e
--- /dev/null
+++ b/mateclaw-ui/src/views/Settings/SkillCurator/index.vue
@@ -0,0 +1,308 @@
+
+
+
+
+
+
+ {{ t('common.loading') }}
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+ {{ t('skillCurator.stateEnabled') }}
+
+
+ {{ status.control.activated ? t('skillCurator.stateActivated') : t('skillCurator.statePreview') }}
+
+
+ {{ t('skillCurator.statePaused') }}
+
+
+
+ {{ status.control.paused ? t('skillCurator.pausedHint')
+ : status.control.activated ? t('skillCurator.activatedHint')
+ : t('skillCurator.previewHint') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('skillCurator.counts') }}
+
+
{{ status.counts.active }}{{ t('skillCurator.active') }}
+
{{ status.counts.stale }}{{ t('skillCurator.stale') }}
+
{{ status.counts.archived }}{{ t('skillCurator.archived') }}
+
{{ status.counts.pinned }}{{ t('skillCurator.pinned') }}
+
{{ status.counts.blockedByBindings }}{{ t('skillCurator.blockedByBindings') }}
+
+
+
+
+
+
{{ t('skillCurator.config') }}
+
+ - {{ t('skillCurator.scope') }}
{{ status.config.scope }}
+ - {{ t('skillCurator.staleAfter') }}
- {{ status.config.staleAfterDays }}
+ - {{ t('skillCurator.archiveAfter') }}
- {{ status.config.archiveAfterDays }}
+ - {{ t('skillCurator.cron') }}
{{ status.config.cron }}
+ - {{ t('skillCurator.lastObservedAt') }}
- {{ fmt(status.control.lastObservedAt) }}
+ - {{ t('skillCurator.lastDryRunAt') }}
- {{ fmt(status.control.lastDryRunAt) }}
+ - {{ t('skillCurator.lastRunAt') }}
- {{ fmt(status.control.lastRunAt) }}
+ - {{ t('skillCurator.nextScheduledRun') }}
- {{ fmt(status.control.nextScheduledRun) }}
+
+
+
+
+
+
{{ t('skillCurator.reports') }}
+
{{ t('skillCurator.noReports') }}
+
+
+
+
+
+
+ {{ selectedReport.dryRun ? t('skillCurator.reportDryRun') : t('skillCurator.reportApplied') }}
+
+ {{ t('skillCurator.reportScanned') }}: {{ selectedReport.scanned }}
+
+
+ {{ t('skillCurator.reportPlanned') }}: stale {{ selectedReport.planned?.stale ?? 0 }} · archived {{ selectedReport.planned?.archived ?? 0 }} · reactivated {{ selectedReport.planned?.reactivated ?? 0 }}
+ {{ t('skillCurator.reportApplied') }}: stale {{ selectedReport.applied?.stale ?? 0 }} · archived {{ selectedReport.applied?.archived ?? 0 }} · reactivated {{ selectedReport.applied?.reactivated ?? 0 }}
+
+
+
{{ t('skillCurator.reportTransitions') }}
+
+ {{ tr.name }}
+ {{ tr.from }} → {{ tr.to }}
+ {{ tr.daysIdle }}d
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mateclaw-ui/src/views/SkillMarket.vue b/mateclaw-ui/src/views/SkillMarket.vue
index f0ac9d01..ff6b5238 100644
--- a/mateclaw-ui/src/views/SkillMarket.vue
+++ b/mateclaw-ui/src/views/SkillMarket.vue
@@ -41,10 +41,11 @@
@@ -132,6 +133,12 @@
{{ getSourceLabel(skill) }}
v{{ skill.version }}
+ {{ lastUsedLabel(skill) }}