@pellux/goodvibes-agent 1.4.4 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +52 -7
- package/dist/package/main.js +7491 -12253
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/live-verification/live-verification.json +13 -13
- package/release/live-verification/live-verification.md +15 -15
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +5 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/shell-passthrough.ts +58 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +129 -90
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ics-timezone.ts — Dependency-free IANA timezone conversion using Intl.
|
|
3
|
+
*
|
|
4
|
+
* Converts between wall-clock date-time strings and UTC milliseconds using
|
|
5
|
+
* Intl.DateTimeFormat with the target timezone. The approach:
|
|
6
|
+
*
|
|
7
|
+
* 1. Treat the wall-clock string as a UTC candidate instant.
|
|
8
|
+
* 2. Format that candidate in the target timezone using Intl.DateTimeFormat.
|
|
9
|
+
* 3. Measure the offset (delta between what Intl says the local time is vs.
|
|
10
|
+
* what we fed in).
|
|
11
|
+
* 4. Correct the candidate by that delta.
|
|
12
|
+
* 5. Iterate once more to handle DST boundaries where step 3 may land on
|
|
13
|
+
* the wrong side of the transition.
|
|
14
|
+
*
|
|
15
|
+
* Handles all three RFC 5545 DTSTART forms:
|
|
16
|
+
* - UTC ('...Z'): caller should not call this; pass through as-is.
|
|
17
|
+
* - Floating (no TZID): kept as local/unzoned — caller treats as UTC.
|
|
18
|
+
* - TZID-qualified: converted here to a correct UTC instant.
|
|
19
|
+
*
|
|
20
|
+
* Unknown or unsupported TZID values fall back to treating the wall-clock as
|
|
21
|
+
* UTC (offset = 0). This is documented behaviour, not silent corruption.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// Intl formatter cache
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
// Maps TZID → Intl.DateTimeFormat (reuse across calls in a session)
|
|
29
|
+
const fmtCache = new Map<string, Intl.DateTimeFormat | null>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a cached Intl.DateTimeFormat for the given IANA timezone, or null
|
|
33
|
+
* if the timezone is unknown/unsupported by this runtime.
|
|
34
|
+
*/
|
|
35
|
+
function getFormatter(tzid: string): Intl.DateTimeFormat | null {
|
|
36
|
+
if (fmtCache.has(tzid)) return fmtCache.get(tzid)!;
|
|
37
|
+
try {
|
|
38
|
+
const fmt = new Intl.DateTimeFormat('en-US', {
|
|
39
|
+
timeZone: tzid,
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: '2-digit',
|
|
42
|
+
day: '2-digit',
|
|
43
|
+
hour: '2-digit',
|
|
44
|
+
minute: '2-digit',
|
|
45
|
+
second: '2-digit',
|
|
46
|
+
hour12: false,
|
|
47
|
+
});
|
|
48
|
+
fmtCache.set(tzid, fmt);
|
|
49
|
+
return fmt;
|
|
50
|
+
} catch {
|
|
51
|
+
// Unknown TZID — Intl throws RangeError for invalid timezone names
|
|
52
|
+
fmtCache.set(tzid, null);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ──────────────────────────────────────────────────────────────────
|
|
58
|
+
// Core conversion helpers
|
|
59
|
+
// ──────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse a wall-clock string "YYYY-MM-DDTHH:MM:SS" into component parts.
|
|
63
|
+
* Returns null if the string does not match the expected form.
|
|
64
|
+
*/
|
|
65
|
+
function parseWallClock(wallClock: string): {
|
|
66
|
+
year: number; month: number; day: number;
|
|
67
|
+
hour: number; minute: number; second: number;
|
|
68
|
+
} | null {
|
|
69
|
+
const m = wallClock.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/);
|
|
70
|
+
if (!m) return null;
|
|
71
|
+
return {
|
|
72
|
+
year: parseInt(m[1]!, 10),
|
|
73
|
+
month: parseInt(m[2]!, 10),
|
|
74
|
+
day: parseInt(m[3]!, 10),
|
|
75
|
+
hour: parseInt(m[4]!, 10),
|
|
76
|
+
minute: parseInt(m[5]!, 10),
|
|
77
|
+
second: parseInt(m[6]!, 10),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format a UTC instant (ms) as its wall-clock representation in the given
|
|
83
|
+
* IANA timezone, returning "YYYY-MM-DDTHH:MM:SS".
|
|
84
|
+
*
|
|
85
|
+
* Uses the cached Intl.DateTimeFormat. Returns null if the formatter is
|
|
86
|
+
* unavailable (unknown TZID).
|
|
87
|
+
*/
|
|
88
|
+
export function utcMsToWallClock(utcMs: number, tzid: string): string | null {
|
|
89
|
+
const fmt = getFormatter(tzid);
|
|
90
|
+
if (!fmt) return null;
|
|
91
|
+
|
|
92
|
+
// Intl.DateTimeFormat.formatToParts gives us named fields.
|
|
93
|
+
const parts = fmt.formatToParts(new Date(utcMs));
|
|
94
|
+
const get = (t: string) => parts.find((p) => p.type === t)?.value ?? '00';
|
|
95
|
+
|
|
96
|
+
// hour12: false gives '00'-'23' for midnight through 11pm; some runtimes
|
|
97
|
+
// return '24' for midnight — normalise to '00'.
|
|
98
|
+
let hour = get('hour');
|
|
99
|
+
if (hour === '24') hour = '00';
|
|
100
|
+
|
|
101
|
+
return `${get('year')}-${get('month')}-${get('day')}T${hour}:${get('minute')}:${get('second')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert a wall-clock date-time string ("YYYY-MM-DDTHH:MM:SS") in the given
|
|
106
|
+
* IANA timezone to a UTC timestamp (milliseconds since epoch).
|
|
107
|
+
*
|
|
108
|
+
* Algorithm (offset-derivation trick with one DST correction pass):
|
|
109
|
+
* 1. Treat wallClock as UTC candidate: candidateMs = Date.parse(wallClock + 'Z')
|
|
110
|
+
* 2. Ask Intl what the local time in tzid IS at that candidateMs → localAtCandidate
|
|
111
|
+
* 3. delta = candidateMs - Date.parse(localAtCandidate + 'Z')
|
|
112
|
+
* 4. correctedMs = candidateMs + delta
|
|
113
|
+
* 5. Verify: ask Intl what local time correctedMs is → should equal wallClock.
|
|
114
|
+
* If not (DST gap/fold), use correctedMs anyway (it's the closest valid instant).
|
|
115
|
+
*
|
|
116
|
+
* Falls back to treating wallClock as UTC (returns Date.parse(wallClock + 'Z'))
|
|
117
|
+
* when:
|
|
118
|
+
* - The TZID is unknown to Intl (not a valid IANA name)
|
|
119
|
+
* - The wallClock string cannot be parsed
|
|
120
|
+
* The fallback is documented: callers see this as offset=0 (UTC treatment).
|
|
121
|
+
*/
|
|
122
|
+
export function wallClockToUtcMs(wallClock: string, tzid: string): number {
|
|
123
|
+
const parsed = parseWallClock(wallClock);
|
|
124
|
+
if (!parsed) {
|
|
125
|
+
// Malformed wall-clock — return as UTC
|
|
126
|
+
return Date.parse(`${wallClock}Z`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const fmt = getFormatter(tzid);
|
|
130
|
+
if (!fmt) {
|
|
131
|
+
// Unknown TZID — fall back to UTC treatment (documented)
|
|
132
|
+
return Date.parse(`${wallClock}Z`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// seed candidate from raw wall-clock treated as UTC
|
|
136
|
+
const candidateMs = Date.UTC(
|
|
137
|
+
parsed.year, parsed.month - 1, parsed.day,
|
|
138
|
+
parsed.hour, parsed.minute, parsed.second,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// first-pass offset correction (used as the gap fallback)
|
|
142
|
+
const firstLocal = utcMsToWallClock(candidateMs, tzid);
|
|
143
|
+
if (!firstLocal) return candidateMs;
|
|
144
|
+
const firstLocalParsed = parseWallClock(firstLocal);
|
|
145
|
+
if (!firstLocalParsed) return candidateMs;
|
|
146
|
+
const firstDelta = candidateMs - Date.UTC(
|
|
147
|
+
firstLocalParsed.year, firstLocalParsed.month - 1, firstLocalParsed.day,
|
|
148
|
+
firstLocalParsed.hour, firstLocalParsed.minute, firstLocalParsed.second,
|
|
149
|
+
);
|
|
150
|
+
const firstCorrected = candidateMs + firstDelta;
|
|
151
|
+
|
|
152
|
+
// iterate to a fixed point (converges <=3 passes for every valid wall-clock,
|
|
153
|
+
// incl. DST-transition-day post-transition hours)
|
|
154
|
+
let guess = firstCorrected;
|
|
155
|
+
for (let i = 0; i < 5; i++) {
|
|
156
|
+
const local = utcMsToWallClock(guess, tzid);
|
|
157
|
+
if (!local) return guess;
|
|
158
|
+
if (local === wallClock) return guess; // exact valid instant
|
|
159
|
+
const lp = parseWallClock(local);
|
|
160
|
+
if (!lp) return guess;
|
|
161
|
+
const delta = candidateMs - Date.UTC(
|
|
162
|
+
lp.year, lp.month - 1, lp.day,
|
|
163
|
+
lp.hour, lp.minute, lp.second,
|
|
164
|
+
);
|
|
165
|
+
if (delta === 0) return guess;
|
|
166
|
+
guess += delta;
|
|
167
|
+
}
|
|
168
|
+
// no fixed point => nonexistent spring-forward gap; return first-pass corrected
|
|
169
|
+
// (conventional forward-shift: 02:30 NY -> 03:30 EDT = 07:30Z), preserving the gap tests
|
|
170
|
+
return firstCorrected;
|
|
171
|
+
}
|
|
172
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared YAML-style frontmatter parser for agent markdown files.
|
|
3
|
+
*
|
|
4
|
+
* Parses the `---\nkey: value\n---` block at the top of a markdown file
|
|
5
|
+
* and optionally strips it from the body.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse the frontmatter block from a markdown string.
|
|
10
|
+
* Returns a flat record of string key/value pairs.
|
|
11
|
+
* Keys inside the block that have no value component are ignored.
|
|
12
|
+
*/
|
|
13
|
+
export function parseMarkdownFrontmatter(content: string): Record<string, string> {
|
|
14
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
15
|
+
if (!match) return {};
|
|
16
|
+
const result: Record<string, string> = {};
|
|
17
|
+
for (const line of match[1].split('\n')) {
|
|
18
|
+
const [key, ...rest] = line.split(':');
|
|
19
|
+
if (key && rest.length > 0) result[key.trim()] = rest.join(':').trim();
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Strip the leading frontmatter block from a markdown string and return the body.
|
|
26
|
+
* Trims the result unless `trim` is false.
|
|
27
|
+
*/
|
|
28
|
+
export function stripMarkdownFrontmatter(content: string, trim = true): string {
|
|
29
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
30
|
+
return trim ? body.trim() : body;
|
|
31
|
+
}
|
|
@@ -92,13 +92,7 @@ function nowIso(): string {
|
|
|
92
92
|
return new Date().toISOString();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
assertNoSecretLikeText(fields);
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error('Notes cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
95
|
+
|
|
102
96
|
|
|
103
97
|
function parseNote(value: unknown): AgentNoteRecord | null {
|
|
104
98
|
if (!isRecord(value)) return null;
|
|
@@ -193,7 +187,7 @@ export class AgentNoteRegistry {
|
|
|
193
187
|
const body = input.body.trim();
|
|
194
188
|
this.validateRequired(title, body);
|
|
195
189
|
const sourceUrl = input.sourceUrl?.trim() || undefined;
|
|
196
|
-
|
|
190
|
+
assertNoSecretLikeText([title, body, sourceUrl ?? '', ...(input.tags ?? [])], 'Notes');
|
|
197
191
|
const duplicate = store.notes.find((note) => note.title.toLowerCase() === title.toLowerCase());
|
|
198
192
|
if (duplicate) throw new Error(`Note already exists ${duplicate.id}`);
|
|
199
193
|
const timestamp = nowIso();
|
|
@@ -221,7 +215,7 @@ export class AgentNoteRegistry {
|
|
|
221
215
|
const body = input.body === undefined ? existing.body : input.body.trim();
|
|
222
216
|
this.validateRequired(title, body);
|
|
223
217
|
const sourceUrl = input.sourceUrl === undefined ? existing.sourceUrl : input.sourceUrl.trim() || undefined;
|
|
224
|
-
|
|
218
|
+
assertNoSecretLikeText([title, body, sourceUrl ?? '', ...(input.tags ?? [])], 'Notes');
|
|
225
219
|
const duplicate = store.notes.find((note) => note.id !== existing.id && note.title.toLowerCase() === title.toLowerCase());
|
|
226
220
|
if (duplicate) throw new Error(`Note already exists ${duplicate.id}`);
|
|
227
221
|
const updated: AgentNoteRecord = {
|
|
@@ -2,6 +2,7 @@ import { promises as fsPromises } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
5
6
|
|
|
6
7
|
export type PersonaOrigin = 'project-local' | 'global' | 'custom';
|
|
7
8
|
|
|
@@ -16,19 +17,6 @@ export interface DiscoveredPersonaRecord {
|
|
|
16
17
|
|
|
17
18
|
const DIRECTORY_MARKERS: readonly string[] = ['PERSONA.md', 'persona.md'];
|
|
18
19
|
|
|
19
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
-
if (!match) return {};
|
|
22
|
-
const result: Record<string, string> = {};
|
|
23
|
-
for (const line of match[1].split('\n')) {
|
|
24
|
-
const [key, ...rest] = line.split(':');
|
|
25
|
-
if (key && rest.length > 0) {
|
|
26
|
-
result[key.trim()] = rest.join(':').trim();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
function getPersonaDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: PersonaOrigin }> {
|
|
33
21
|
return [
|
|
34
22
|
{ root: join(cwd, '.goodvibes', 'personas'), origin: 'project-local' },
|
|
@@ -46,8 +34,8 @@ async function readPersonaFile(path: string, origin: PersonaOrigin): Promise<Dis
|
|
|
46
34
|
return null;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
const frontmatter =
|
|
50
|
-
const markdownBody = content
|
|
37
|
+
const frontmatter = parseMarkdownFrontmatter(content);
|
|
38
|
+
const markdownBody = stripMarkdownFrontmatter(content);
|
|
51
39
|
const body = (frontmatter.system_prompt ?? markdownBody).trim();
|
|
52
40
|
if (!body) return null;
|
|
53
41
|
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'persona';
|
|
@@ -3,6 +3,7 @@ import { dirname } from 'node:path';
|
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
5
|
import { formatAgentRecordReviewState } from './record-labels.ts';
|
|
6
|
+
import { containsSecretLikeText } from './memory-safety.ts';
|
|
6
7
|
|
|
7
8
|
export type AgentPersonaSource = 'user' | 'agent' | 'imported' | 'system';
|
|
8
9
|
export type AgentPersonaReviewState = 'fresh' | 'reviewed' | 'stale';
|
|
@@ -56,12 +57,6 @@ interface PersonaStoreFile {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const STORE_VERSION = 1;
|
|
59
|
-
const SECRET_PATTERNS: readonly RegExp[] = [
|
|
60
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
|
|
61
|
-
/\bsk-[A-Za-z0-9_-]{16,}\b/,
|
|
62
|
-
/\bgh[pousr]_[A-Za-z0-9_]{16,}\b/i,
|
|
63
|
-
/\b(?:password|passwd|api[_-]?key|token|secret)\s*[:=]\s*\S{6,}/i,
|
|
64
|
-
];
|
|
65
60
|
|
|
66
61
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
67
62
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -103,10 +98,6 @@ function nowIso(): string {
|
|
|
103
98
|
return new Date().toISOString();
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
function containsSecretLikeText(text: string): boolean {
|
|
107
|
-
return SECRET_PATTERNS.some((pattern) => pattern.test(text));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
101
|
export function assertNoSecretLikeText(fields: readonly string[], ownerLabel = 'Agent local records'): void {
|
|
111
102
|
if (fields.some((field) => containsSecretLikeText(field))) {
|
|
112
103
|
throw new Error(`${ownerLabel} cannot store secret-looking values. Store a secret reference or remove the sensitive text.`);
|
|
@@ -408,7 +408,11 @@ export class AgentResearchSourceRegistry {
|
|
|
408
408
|
|
|
409
409
|
private readStore(): ResearchSourceStoreFile {
|
|
410
410
|
if (!existsSync(this.storePath)) return { version: STORE_VERSION, sources: [] };
|
|
411
|
-
|
|
411
|
+
try {
|
|
412
|
+
return parseStore(readFileSync(this.storePath, 'utf-8'));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new Error(`Could not read Agent research source store: ${error instanceof Error ? error.message : String(error)}`);
|
|
415
|
+
}
|
|
412
416
|
}
|
|
413
417
|
|
|
414
418
|
private writeStore(store: ResearchSourceStoreFile): void {
|
|
@@ -2,6 +2,7 @@ import { promises as fsPromises } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
5
6
|
|
|
6
7
|
export type RoutineOrigin = 'project-local' | 'global' | 'custom';
|
|
7
8
|
|
|
@@ -16,19 +17,6 @@ export interface DiscoveredRoutineRecord {
|
|
|
16
17
|
|
|
17
18
|
const DIRECTORY_MARKERS: readonly string[] = ['ROUTINE.md', 'routine.md'];
|
|
18
19
|
|
|
19
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
-
if (!match) return {};
|
|
22
|
-
const result: Record<string, string> = {};
|
|
23
|
-
for (const line of match[1].split('\n')) {
|
|
24
|
-
const [key, ...rest] = line.split(':');
|
|
25
|
-
if (key && rest.length > 0) {
|
|
26
|
-
result[key.trim()] = rest.join(':').trim();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
function getRoutineDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: RoutineOrigin }> {
|
|
33
21
|
return [
|
|
34
22
|
{ root: join(cwd, '.goodvibes', 'routines'), origin: 'project-local' },
|
|
@@ -46,8 +34,8 @@ async function readRoutineFile(path: string, origin: RoutineOrigin): Promise<Dis
|
|
|
46
34
|
return null;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
const frontmatter =
|
|
50
|
-
const markdownBody = content
|
|
37
|
+
const frontmatter = parseMarkdownFrontmatter(content);
|
|
38
|
+
const markdownBody = stripMarkdownFrontmatter(content);
|
|
51
39
|
const steps = (frontmatter.steps ?? markdownBody).trim();
|
|
52
40
|
if (!steps) return null;
|
|
53
41
|
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? 'routine';
|
|
@@ -302,6 +302,7 @@ function selectDiscoveredRecords<T extends { readonly name: string; readonly pat
|
|
|
302
302
|
|
|
303
303
|
function discoveredSkillToTemplate(skill: SkillRecord): AgentRuntimeProfileStarterTemplate['skills'][number] {
|
|
304
304
|
if (!skill.body.trim()) throw new Error(`Discovered Agent skill ${skill.name} has no procedure body.`);
|
|
305
|
+
assertNoSecretLikeText([skill.name, skill.description, skill.body], `Discovered skill '${skill.name}'`);
|
|
305
306
|
return {
|
|
306
307
|
name: skill.name,
|
|
307
308
|
description: skill.description || `Imported skill from ${skill.origin} skill file.`,
|
|
@@ -313,6 +314,7 @@ function discoveredSkillToTemplate(skill: SkillRecord): AgentRuntimeProfileStart
|
|
|
313
314
|
|
|
314
315
|
function discoveredRoutineToTemplate(routine: DiscoveredRoutineRecord): AgentRuntimeProfileStarterTemplate['routines'][number] {
|
|
315
316
|
if (!routine.steps.trim()) throw new Error(`Discovered Agent routine ${routine.name} has no steps.`);
|
|
317
|
+
assertNoSecretLikeText([routine.name, routine.description, routine.steps], `Discovered routine '${routine.name}'`);
|
|
316
318
|
return {
|
|
317
319
|
name: routine.name,
|
|
318
320
|
description: routine.description || `Imported routine from ${routine.origin} markdown file.`,
|
|
@@ -323,6 +325,7 @@ function discoveredRoutineToTemplate(routine: DiscoveredRoutineRecord): AgentRun
|
|
|
323
325
|
}
|
|
324
326
|
|
|
325
327
|
function discoveredPersonaToTemplate(persona: DiscoveredPersonaRecord): AgentRuntimeProfileStarterTemplate['persona'] {
|
|
328
|
+
assertNoSecretLikeText([persona.name, persona.description, persona.body], `Discovered persona '${persona.name}'`);
|
|
326
329
|
return {
|
|
327
330
|
name: persona.name,
|
|
328
331
|
description: persona.description || `Imported persona from ${persona.origin} markdown file.`,
|
|
@@ -2,6 +2,7 @@ import { promises as fsPromises } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
5
6
|
|
|
6
7
|
export type SkillOrigin = 'project-local' | 'global' | 'custom';
|
|
7
8
|
|
|
@@ -16,19 +17,6 @@ export interface SkillRecord {
|
|
|
16
17
|
frontmatter: Record<string, string>;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
20
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
-
if (!match) return {};
|
|
22
|
-
const result: Record<string, string> = {};
|
|
23
|
-
for (const line of match[1].split('\n')) {
|
|
24
|
-
const [key, ...rest] = line.split(':');
|
|
25
|
-
if (key && rest.length > 0) {
|
|
26
|
-
result[key.trim()] = rest.join(':').trim();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return result;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
function getSkillDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: SkillOrigin }> {
|
|
33
21
|
return [
|
|
34
22
|
{ root: join(cwd, '.goodvibes', 'skills'), origin: 'project-local' },
|
|
@@ -46,8 +34,8 @@ async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRe
|
|
|
46
34
|
return null;
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
const frontmatter =
|
|
50
|
-
const body = content
|
|
37
|
+
const frontmatter = parseMarkdownFrontmatter(content);
|
|
38
|
+
const body = stripMarkdownFrontmatter(content, false);
|
|
51
39
|
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? 'skill';
|
|
52
40
|
const description = frontmatter.description ?? frontmatter.summary ?? '';
|
|
53
41
|
const dependencies = frontmatter.depends_on
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
2
|
+
import type { LearningCandidate } from '../tools/agent-harness-learning-curator-types.ts';
|
|
3
|
+
import type { AgentSkillCreateInput } from './skill-registry-types.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The create-input payload produced for each proposed skill draft.
|
|
7
|
+
* Passed directly to AgentSkillRegistry.create().
|
|
8
|
+
*/
|
|
9
|
+
export interface SkillDraftPayload extends AgentSkillCreateInput {
|
|
10
|
+
/** Candidate that originated this draft, for ledger bookkeeping. */
|
|
11
|
+
readonly candidateId: string;
|
|
12
|
+
/** Human-readable label carried from the learning candidate. */
|
|
13
|
+
readonly candidateLabel: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProposeSkillDraftsInput {
|
|
17
|
+
/** Full ranked candidate list from buildLearningCandidates. */
|
|
18
|
+
readonly candidates: readonly LearningCandidate[];
|
|
19
|
+
/** Lower-cased name set of skills already in the registry (dedup guard). */
|
|
20
|
+
readonly existingSkillNames: ReadonlySet<string>;
|
|
21
|
+
/** Lower-cased name set of skills proposed in earlier passes (dedup guard). */
|
|
22
|
+
readonly previouslyProposedNames: ReadonlySet<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Maximum draft proposals emitted per pass. */
|
|
26
|
+
const MAX_PROPOSALS = 3;
|
|
27
|
+
|
|
28
|
+
const PROVENANCE = 'auto-proposed-skill-draft';
|
|
29
|
+
|
|
30
|
+
/** Convert an arbitrary string to a URL-safe kebab-case slug. */
|
|
31
|
+
function slugify(value: string): string {
|
|
32
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
33
|
+
return slug || 'skill';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract a one-line description from the proposalFields carried on the candidate.
|
|
38
|
+
* Prefers an explicit `description` field; falls back to the candidate label.
|
|
39
|
+
*/
|
|
40
|
+
function extractDescription(candidate: LearningCandidate): string {
|
|
41
|
+
const fields = candidate.proposalFields ?? {};
|
|
42
|
+
const desc = (fields.description ?? '').trim();
|
|
43
|
+
if (desc) return desc.split('\n')[0]?.trim() || desc;
|
|
44
|
+
return candidate.label.split('->')[0]?.trim() || candidate.label;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a stepwise procedure body from available candidate evidence.
|
|
49
|
+
* Uses proposalFields.notes / detail / triggers when present.
|
|
50
|
+
*/
|
|
51
|
+
function buildProcedureBody(candidate: LearningCandidate): string {
|
|
52
|
+
const fields = candidate.proposalFields ?? {};
|
|
53
|
+
const sections: string[] = [];
|
|
54
|
+
|
|
55
|
+
// Context block — reason and source
|
|
56
|
+
sections.push(`# ${extractDescription(candidate)}`);
|
|
57
|
+
sections.push('');
|
|
58
|
+
if (candidate.reason) {
|
|
59
|
+
sections.push(`## Origin`);
|
|
60
|
+
sections.push(candidate.reason.trim());
|
|
61
|
+
sections.push('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Triggers — derived from proposalFields.triggers or candidate.proposalTarget
|
|
65
|
+
const triggerHint = (fields.triggers ?? '').trim();
|
|
66
|
+
if (triggerHint) {
|
|
67
|
+
sections.push(`## When to use`);
|
|
68
|
+
sections.push(triggerHint);
|
|
69
|
+
sections.push('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Steps / body — prefer notes, then detail
|
|
73
|
+
const notes = (fields.notes ?? '').trim();
|
|
74
|
+
const detail = (fields.detail ?? '').trim();
|
|
75
|
+
const body = notes || detail;
|
|
76
|
+
if (body) {
|
|
77
|
+
sections.push(`## Steps`);
|
|
78
|
+
sections.push(body);
|
|
79
|
+
sections.push('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Tags / extra context
|
|
83
|
+
const tags = (fields.tags ?? '').trim();
|
|
84
|
+
if (tags) {
|
|
85
|
+
sections.push(`## Tags`);
|
|
86
|
+
sections.push(tags);
|
|
87
|
+
sections.push('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
sections.push(`## Notes`);
|
|
91
|
+
sections.push('This skill was auto-proposed from a learning candidate. Review and refine the steps before enabling.');
|
|
92
|
+
|
|
93
|
+
return sections.join('\n').trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Derive inferred trigger phrases from proposal fields and candidate metadata.
|
|
98
|
+
*/
|
|
99
|
+
function buildTriggers(candidate: LearningCandidate): readonly string[] {
|
|
100
|
+
const fields = candidate.proposalFields ?? {};
|
|
101
|
+
const raw = (fields.triggers ?? '').trim();
|
|
102
|
+
if (!raw) return [];
|
|
103
|
+
return raw
|
|
104
|
+
.split(/[,;]+/)
|
|
105
|
+
.map((t) => t.trim())
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Derive tags from proposal fields and provenance.
|
|
111
|
+
*/
|
|
112
|
+
function buildTags(candidate: LearningCandidate): readonly string[] {
|
|
113
|
+
const fields = candidate.proposalFields ?? {};
|
|
114
|
+
const raw = (fields.tags ?? '').trim();
|
|
115
|
+
const base = raw
|
|
116
|
+
? raw.split(/[,;]+/).map((t) => t.trim()).filter(Boolean)
|
|
117
|
+
: [];
|
|
118
|
+
// Always include the auto-proposal marker so it's filterable
|
|
119
|
+
if (!base.includes('auto-proposed')) base.push('auto-proposed');
|
|
120
|
+
return base;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Pure function: given the current learning candidates and name registries, select
|
|
125
|
+
* up to MAX_PROPOSALS skill-worthy candidates and return their draft payloads.
|
|
126
|
+
*
|
|
127
|
+
* Hard rules:
|
|
128
|
+
* - Only 'proposal-ready' or 'ready-to-promote' candidates with proposalTarget === 'skill'
|
|
129
|
+
* - Max 3 proposals per pass
|
|
130
|
+
* - Dedupe against existingSkillNames, previouslyProposedNames, and names proposed this pass
|
|
131
|
+
* (all comparisons use the kebab slug of the name, case-insensitive)
|
|
132
|
+
* - Skip any candidate whose text fields contain secret-like content
|
|
133
|
+
* - Skip any candidate whose derived name or description is empty
|
|
134
|
+
* - All drafts are created with enabled: false (caller's responsibility; registry enforces reviewState 'fresh')
|
|
135
|
+
*/
|
|
136
|
+
export function proposeSkillDrafts(input: ProposeSkillDraftsInput): readonly SkillDraftPayload[] {
|
|
137
|
+
const { candidates, existingSkillNames, previouslyProposedNames } = input;
|
|
138
|
+
|
|
139
|
+
const proposals: SkillDraftPayload[] = [];
|
|
140
|
+
// Within-pass dedup: track slugified names already proposed in this call.
|
|
141
|
+
const proposedThisPass = new Set<string>();
|
|
142
|
+
|
|
143
|
+
// Filter to skill-proposal candidates, sorted by priority desc (candidates
|
|
144
|
+
// from buildLearningCandidates are already sorted but we enforce it here for
|
|
145
|
+
// purity — the function should not depend on caller sort order).
|
|
146
|
+
const eligible = [...candidates]
|
|
147
|
+
.filter(
|
|
148
|
+
(c) =>
|
|
149
|
+
(c.status === 'proposal-ready' || c.status === 'ready-to-promote') &&
|
|
150
|
+
c.proposalTarget === 'skill',
|
|
151
|
+
)
|
|
152
|
+
.sort((a, b) => b.priority - a.priority);
|
|
153
|
+
|
|
154
|
+
for (const candidate of eligible) {
|
|
155
|
+
if (proposals.length >= MAX_PROPOSALS) break;
|
|
156
|
+
|
|
157
|
+
const description = extractDescription(candidate);
|
|
158
|
+
// Guard: skip candidates whose description is empty after extraction.
|
|
159
|
+
if (!description.trim()) continue;
|
|
160
|
+
|
|
161
|
+
const rawName = description.slice(0, 120); // registry normalizes further
|
|
162
|
+
// Guard: skip candidates whose derived name is empty.
|
|
163
|
+
if (!rawName.trim()) continue;
|
|
164
|
+
|
|
165
|
+
const name = slugify(rawName);
|
|
166
|
+
const nameLower = name.toLowerCase(); // slugify already lowercases
|
|
167
|
+
|
|
168
|
+
// Dedup against existing, previously proposed, and within-pass proposed.
|
|
169
|
+
if (
|
|
170
|
+
existingSkillNames.has(nameLower) ||
|
|
171
|
+
previouslyProposedNames.has(nameLower) ||
|
|
172
|
+
proposedThisPass.has(nameLower)
|
|
173
|
+
) continue;
|
|
174
|
+
|
|
175
|
+
// Secret scan — skip candidate rather than throw, as individual dirty
|
|
176
|
+
// candidates should not block the whole pass
|
|
177
|
+
const procedure = buildProcedureBody(candidate);
|
|
178
|
+
const triggers = buildTriggers(candidate);
|
|
179
|
+
const tags = buildTags(candidate);
|
|
180
|
+
const fieldsToScan = [name, description, procedure, ...triggers, ...tags];
|
|
181
|
+
try {
|
|
182
|
+
assertNoSecretLikeText(fieldsToScan, 'Skill draft proposer');
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
proposedThisPass.add(nameLower);
|
|
188
|
+
proposals.push({
|
|
189
|
+
candidateId: candidate.id,
|
|
190
|
+
candidateLabel: candidate.label,
|
|
191
|
+
name,
|
|
192
|
+
description,
|
|
193
|
+
procedure,
|
|
194
|
+
triggers: triggers.length > 0 ? triggers : undefined,
|
|
195
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
196
|
+
enabled: false,
|
|
197
|
+
source: 'agent',
|
|
198
|
+
provenance: PROVENANCE,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return proposals;
|
|
203
|
+
}
|