@mengyuly/dsh-ponytail 0.3.1 → 0.3.3

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/lib/index.js CHANGED
@@ -1,1795 +1,1748 @@
1
- import { createRequire } from "node:module";
2
- import { Service } from "@deepseek-ai/cordis";
3
- import z from "@deepseek-ai/schemastery";
4
- import { mkdirSync, readFileSync, renameSync, unlinkSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { dirname, join } from "node:path";
7
- //#region ../../llm/llm/src/brand.ts
8
- /**
9
- * Brand a message identifier.
10
- * @param id - the opaque message identifier.
11
- * @returns the same string, branded; no validation is performed.
12
- */
13
- function MessageId(id) {
14
- return id;
15
- }
16
- //#endregion
17
- //#region ../../llm/llm/src/call-config.ts
18
- /**
19
- * Deep-freeze a value in place with an iterative traversal, guarding cycles,
20
- * so later mutation throws without imposing a JavaScript call-stack depth cap.
21
- * {@link AbortSignal} objects are deliberately skipped because they are the
22
- * request's live cancellation channel and freezing them breaks abort.
23
- * @param value - the value to freeze in place.
24
- * @returns the same value, frozen.
25
- */
26
- function deepFreeze(value) {
27
- const seen = /* @__PURE__ */ new WeakSet();
28
- const pending = [{
29
- kind: "visit",
30
- node: value
31
- }];
32
- while (pending.length > 0) {
33
- const task = pending.pop();
34
- /* v8 ignore next -- the loop condition guarantees one pending task. */
35
- if (task === void 0) continue;
36
- if (task.kind === "property") {
37
- pending.push({
38
- kind: "visit",
39
- node: task.source[task.key]
40
- });
41
- continue;
42
- }
43
- const node = task.node;
44
- if (node === null || typeof node !== "object") continue;
45
- if (node instanceof AbortSignal) continue;
46
- if (seen.has(node)) continue;
47
- seen.add(node);
48
- Object.freeze(node);
49
- const keys = Object.keys(node);
50
- for (let index = keys.length - 1; index >= 0; index--) {
51
- const key = keys[index];
52
- /* v8 ignore next -- the loop is bounded by the captured key count. */
53
- if (key === void 0) continue;
54
- pending.push({
55
- kind: "property",
56
- source: node,
57
- key
58
- });
59
- }
60
- }
61
- return value;
62
- }
63
- //#endregion
64
- //#region ../../llm/llm/src/message.ts
65
- /** Message value types, identity, and immutable construction helpers. */
66
- /**
67
- * Detach and deep-freeze a message whose identity already exists.
68
- * @param message - complete message, including its stable identity.
69
- * @returns an immutable snapshot that preserves the identity.
70
- */
71
- function freezeMessage(message) {
72
- return deepFreeze(structuredClone(message));
73
- }
74
- /**
75
- * Create one identified message and freeze it before publication.
76
- * @param input - complete role, content, and source for a new message.
77
- * @returns an immutable message with a fresh stable identity.
78
- */
79
- function createMessage(input) {
80
- return freezeMessage({
81
- ...input,
82
- id: MessageId(crypto.randomUUID())
83
- });
84
- }
85
- /**
86
- * Create one identified user-role message and freeze it before publication.
87
- * @param input - complete content and source for a new user message.
88
- * @returns an immutable user message with a fresh stable identity.
89
- */
90
- function createUserMessage(input) {
91
- return createMessage({
92
- ...input,
93
- role: "user"
94
- });
95
- }
96
- //#endregion
97
- //#region ../../util/timeout/src/index.ts
98
- /** Largest delay Node schedules without clamping it to one millisecond. */
99
- const MAX_TIMER_DELAY_MS = 2147483647;
100
- //#endregion
101
- //#region ../../llm/llm/src/error.ts
102
- /**
103
- * Canonical provider-neutral code for a response that completed normally but
104
- * carried no content blocks at all. Providers occasionally emit a degenerate
105
- * completion (a terminal stop with zero output); adapters classify it as this
106
- * failure instead of yielding an empty assistant message, because an empty
107
- * message silently ends the turn with nothing for the user or the loop to act
108
- * on. The attempt produced nothing durable, so retry policy treats it as safe
109
- * to repeat.
110
- */
111
- const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
112
- new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
113
- new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
114
- new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
115
- //#endregion
116
- //#region ../../llm/llm/src/retry-policy.ts
117
- /**
118
- * Provider-owned request-retry policy configuration and resolution.
119
- *
120
- * Adapters expose one resolved policy per registered provider route; the
121
- * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
122
- *
123
- * @module @deepseek-ai/dsh-llm/retry-policy
124
- */
125
- const DEFAULT_MAX_RETRIES = 5;
126
- const DEFAULT_INITIAL_DELAY_MS = 500;
127
- const DEFAULT_MAX_DELAY_MS = 1e4;
128
- const DEFAULT_JITTER_RATIO = .1;
129
- const DEFAULT_RETRYABLE_CODES = Object.freeze([
130
- EMPTY_RESPONSE_CODE,
131
- "RATE_LIMIT",
132
- "SERVER",
133
- "TIMEOUT",
134
- "TRANSPORT"
135
- ]);
136
- const backoffSchema = z.object({
137
- initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
138
- maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
139
- jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
140
- });
141
- const normalPolicySchema = z.object({
142
- mode: z.const("normal").required(),
143
- maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
144
- retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
145
- backoff: backoffSchema
146
- });
147
- const alwaysPolicySchema = z.object({
148
- mode: z.const("always").required(),
149
- backoff: backoffSchema
150
- });
151
- z.union([normalPolicySchema, alwaysPolicySchema]);
152
- //#endregion
153
- //#region ../../llm/llm/src/attribution.ts
154
- /**
155
- * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
156
- * adapters from drifting. See
157
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
158
- *
159
- * App-attribution vocabulary for provider requests.
160
- * @module @deepseek-ai/dsh-llm/attribution
161
- */
162
- const { version } = createRequire(import.meta.url)("../package.json");
163
- //#endregion
164
- //#region ../../llm/llm/src/never.ts
165
- /**
166
- * Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
167
- * new variant fails compilation at every required handler. Do not use it for declaration-merged
168
- * unions such as session events or content blocks: handle known variants and explicitly fall
169
- * through because plugins may add valid unknown cases.
170
- * @module @deepseek-ai/dsh-llm/never
171
- */
172
- /**
173
- * Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
174
- * a value that escaped its type throws with diagnostics at runtime.
175
- * @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
176
- * @param context - optional label (e.g. the switch site) prefixed into the throw message.
177
- * @returns never — it always throws, with the offending value JSON-rendered in the message.
178
- */
179
- function assertNever(value, context) {
180
- const rendered = JSON.stringify(value) ?? String(value);
181
- throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
182
- }
183
- //#endregion
184
- //#region ../../core/scope/src/store.ts
185
- /**
186
- * Insertion-ordered named entries with caller-owned duplicate diagnostics.
187
- *
188
- * Values are borrowed. Iterators are live within one nonempty table
189
- * generation; draining the table detaches them from later insertions. Each
190
- * successful insertion returns an idempotent undo for that exact entry.
191
- */
192
- var NamedEntries = class {
193
- duplicateError;
194
- data = /* @__PURE__ */ new Map();
195
- constructor(duplicateError) {
196
- this.duplicateError = duplicateError;
197
- }
198
- /**
199
- * Insert one unique name.
200
- * @param name - name unique within this table.
201
- * @param value - borrowed value to retain.
202
- * @returns an idempotent undo that removes only this insertion.
203
- */
204
- insert(name, value) {
205
- const data = this.data;
206
- if (data.has(name)) throw this.duplicateError(name);
207
- data.set(name, value);
208
- let active = true;
209
- return () => {
210
- if (!active) return;
211
- active = false;
212
- data.delete(name);
213
- if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
214
- };
215
- }
216
- /**
217
- * Read one named value.
218
- * @param name - name to resolve.
219
- * @returns the retained value, or `undefined` when absent.
220
- */
221
- get(name) {
222
- return this.data.get(name);
223
- }
224
- /**
225
- * Test one name for membership.
226
- * @param name - name to test.
227
- * @returns whether the table contains that name.
228
- */
229
- has(name) {
230
- return this.data.has(name);
231
- }
232
- /**
233
- * Iterate live names in insertion order.
234
- * @returns the native live key iterator.
235
- */
236
- keys() {
237
- return this.data.keys();
238
- }
239
- /**
240
- * Iterate live entries in insertion order.
241
- * @returns the native live entry iterator.
242
- */
243
- entries() {
244
- return this.data.entries();
245
- }
246
- /**
247
- * Iterate live values in insertion order.
248
- * @returns the native live value iterator.
249
- */
250
- values() {
251
- return this.data.values();
252
- }
253
- /**
254
- * Test whether this table has no entries.
255
- * @returns whether the table is empty.
256
- */
257
- isEmpty() {
258
- return this.data.size === 0;
259
- }
260
- };
261
- /**
262
- * Own the global and exact-scope layers for one registry.
263
- *
264
- * Reads never create scoped layers. Registrations derive both visibility and
265
- * effect ownership from the supplied Cordis context, collect undo before
266
- * notification, and reclaim only a completely empty aggregate layer.
267
- */
268
- var ScopedLayers = class {
269
- createLayer;
270
- onChange;
271
- /** The eagerly constructed context-global layer. */
272
- global;
273
- scoped = /* @__PURE__ */ new Map();
274
- constructor(createLayer, onChange) {
275
- this.createLayer = createLayer;
276
- this.onChange = onChange;
277
- this.global = createLayer(void 0);
278
- }
279
- /**
280
- * Read an existing exact-scope overlay. Deliberately chain-blind: callers
281
- * addressing one scope's OWN contributions (its restrictions, its guards)
282
- * must not silently pick up an ancestor's — use {@link chainLayers} where
283
- * inheritance is the point.
284
- * @param scope - exact scope key; `undefined` denotes no overlay.
285
- * @returns the existing scoped layer, or `undefined` without creating one.
286
- */
287
- peek(scope) {
288
- if (scope === void 0) return void 0;
289
- return this.scoped.get(scope);
290
- }
291
- /**
292
- * Existing overlays along the scope's parent chain ({@link scopeChainOf}),
293
- * farthest ancestor first and the exact scope last, so a caller layering
294
- * them in order gives the nearest scope the final word.
295
- * @param scope - viewing scope, or `undefined` for no overlays.
296
- * @returns the existing layers, nearest last; absent overlays are skipped.
297
- */
298
- chainLayers(scope) {
299
- const layers = [];
300
- for (const key of scopeChainOf(scope).reverse()) {
301
- const layer = this.scoped.get(key);
302
- if (layer !== void 0) layers.push(layer);
303
- }
304
- return layers;
305
- }
306
- /**
307
- * Materialize global named entries followed by scope-chain shadows,
308
- * farthest ancestor first, so the nearest scope's entry wins a name.
309
- * @param scope - viewing scope, or `undefined` for the global view.
310
- * @param pick - select the named table from a layer.
311
- * @returns an insertion-ordered effective map.
312
- */
313
- merge(scope, pick) {
314
- const merged = new Map(pick(this.global).entries());
315
- for (const layer of this.chainLayers(scope)) for (const [name, value] of pick(layer).entries()) merged.set(name, value);
316
- return merged;
317
- }
318
- /**
319
- * Attach one synchronous layer mutation to its registration context.
320
- * @param ctx - context that determines both scope visibility and effect ownership.
321
- * @param action - atomic mutation returning its synchronous undo.
322
- * @param options - Cordis effect label and optional change notification.
323
- * @returns the exact disposer returned by `ctx.effect()`.
324
- */
325
- effect(ctx, action, options) {
326
- const scope = scopeOf(ctx);
327
- const notify = options.notify ?? true;
328
- return ctx.effect(function* () {
329
- let layer;
330
- let created = false;
331
- if (scope === void 0) layer = this.global;
332
- else {
333
- const existing = this.scoped.get(scope);
334
- if (existing === void 0) {
335
- layer = this.createLayer(scope);
336
- this.scoped.set(scope, layer);
337
- created = true;
338
- } else layer = existing;
339
- }
340
- let undo;
341
- try {
342
- undo = action(layer);
343
- } catch (error) {
344
- if (scope !== void 0 && created && layer.isEmpty()) this.scoped.delete(scope);
345
- throw error;
346
- }
347
- yield () => {
348
- undo();
349
- if (scope !== void 0 && layer.isEmpty()) this.scoped.delete(scope);
350
- if (notify) this.onChange();
351
- };
352
- if (notify) this.onChange();
353
- }.bind(this), options.label);
354
- }
355
- };
356
- //#endregion
357
- //#region ../../core/scope/src/index.ts
358
- /** Context tag written by {@link createScope}. */
359
- const kScope = Symbol("dsh.scope");
360
- /**
361
- * The enclosing scope of each key. One relation powers both directions of
362
- * scope nesting: registration views inherit DOWN the chain (a child scope
363
- * sees its ancestors' layers — {@link ScopedLayers}), and event admission
364
- * extends UP it (a listener tagged with an ancestor receives events dispatched
365
- * to a descendant key — {@link scopeTarget}).
366
- */
367
- const scopeParents = /* @__PURE__ */ new WeakMap();
368
- /**
369
- * The chain from a key to its root ancestor.
370
- * @param key - the starting key, or `undefined` for the empty chain.
371
- * @returns keys nearest-first: `[key, parent, grandparent, …]`.
372
- */
373
- function scopeChainOf(key) {
374
- const chain = [];
375
- for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) chain.push(cursor);
376
- return chain;
377
- }
378
- /**
379
- * Read the nearest scope tag inherited by a context.
380
- * @param ctx - context to inspect.
381
- * @returns its scope key, or `undefined` for an unscoped context.
382
- */
383
- function scopeOf(ctx) {
384
- return ctx[kScope];
385
- }
386
- //#endregion
387
- //#region ../../skill/skill/src/index.ts
388
- /**
389
- * Agent skill provider registry.
390
- *
391
- * This package owns the Service Definition role of the skill capability seam.
392
- * Concrete
393
- * providers such as `@deepseek-ai/dsh-skill-filesystem` decide where skills come
394
- * from; this service only merges provider catalogs, resolves the winning skill
395
- * for a name, and exposes the winning summaries and definitions to consumers.
396
- *
397
- * @module @deepseek-ai/dsh-skill
398
- */
399
- const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
400
- const DEFAULT_COLLECT_CACHE_ENTRIES = 128;
401
- const MAX_COLLECT_ATTEMPTS = 2;
402
- const RUNTIME_PROVIDER = "runtime";
403
- const RUNTIME_RANK = 250;
404
- /**
405
- * Return whether a string is a valid kebab-case skill name.
406
- * @param name - candidate skill name to validate.
407
- * @returns whether the name matches the public skill-name grammar.
408
- */
409
- function isSkillName(name) {
410
- return SKILL_NAME.test(name);
411
- }
412
- /**
413
- * Render one loaded skill for the model. The output is shared verbatim by the
414
- * `skill` tool result and the user-explicit invocation injection, so the model
415
- * sees one canonical `<skill_content>` shape on both paths. The name rides an
416
- * escaped attribute; the body is embedded verbatim (skills are trusted local
417
- * content, and user-supplied invocation text stays outside this wrapper).
418
- * @param skill - name, provider, optional resource base, and body to render.
419
- * @returns the complete model-facing `<skill_content>` block.
420
- */
421
- function renderSkillContent(skill) {
422
- const resourceHint = renderResourceHint(skill);
423
- return [
424
- `<skill_content name="${escapeAttr(skill.name)}">`,
425
- "<skill_resources>",
426
- ...resourceHint,
427
- "</skill_resources>",
428
- "",
429
- "<skill_instructions>",
430
- skill.content,
431
- "</skill_instructions>",
432
- "</skill_content>"
433
- ].join("\n");
434
- }
435
- function renderResourceHint(skill) {
436
- const base = skill.resourceBase;
437
- if (base === void 0) return [`Resources for this skill are managed by provider "${escapeText(skill.provider)}".`, "Load referenced resources only as needed."];
438
- switch (base.kind) {
439
- case "directory": return [`Base directory for this skill: ${escapeText(base.path)}`, "Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed."];
440
- case "url": return [`Base URL for this skill: ${escapeText(base.url)}`, "Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed."];
441
- case "opaque": return [`Resources for this skill: ${escapeText(base.description)}`, "Load referenced resources only as needed."];
442
- /* v8 ignore start -- SkillResourceBase is a closed union; a future kind must fail compilation here. */
443
- default: return assertNever(base, "SkillResourceBase.kind");
444
- }
445
- }
446
- function escapeAttr(value) {
447
- return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
448
- }
449
- /**
450
- * Escape model-facing prose embedded inside skill markup so provider-supplied
451
- * text cannot open or close framing tags.
452
- * @param value - raw prose to embed.
453
- * @returns the escaped text.
454
- */
455
- function escapeText(value) {
456
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
457
- }
458
- /** One scope's complete skill-registry contribution. */
459
- var SkillLayer = class {
460
- /** Providers registered through contexts carrying this scope, insertion-ordered. */
461
- providers;
462
- /** Runtime skills registered through contexts carrying this scope. */
463
- runtime = /* @__PURE__ */ new Map();
464
- constructor(scope) {
465
- this.providers = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `a skill provider named "${name}" is already registered` : `a skill provider named "${name}" is already registered in this scope`));
466
- }
467
- /** Whether every contribution table in this aggregate layer is empty. */
468
- isEmpty() {
469
- return this.providers.isEmpty() && this.runtime.size === 0;
470
- }
471
- };
472
- (class extends Service {
473
- static Config = z.object({ collectCacheMaxEntries: z.number().default(DEFAULT_COLLECT_CACHE_ENTRIES) });
474
- collectCacheMaxEntries;
475
- layers = new ScopedLayers((scope) => new SkillLayer(scope), () => {
476
- this.invalidateCache();
477
- });
478
- collectCache = /* @__PURE__ */ new Map();
479
- revision = 0;
480
- nextProviderOrder = 0;
481
- /** Stable identities for cache keys; scope keys are opaque identity-compared objects. */
482
- scopeIds = /* @__PURE__ */ new WeakMap();
483
- nextScopeId = 1;
484
- constructor(ctx, config = {}) {
485
- super(ctx, "skills");
486
- this.collectCacheMaxEntries = config.collectCacheMaxEntries ?? DEFAULT_COLLECT_CACHE_ENTRIES;
487
- assertPositiveInteger("collectCacheMaxEntries", this.collectCacheMaxEntries);
488
- }
489
- /**
490
- * Register a borrowed same-process provider synchronously during plugin
491
- * apply, into the calling context's layer: a scoped context (an agent
492
- * preset's standing mount) registers for that scope alone, an unscoped
493
- * context registers globally. Duplicate names within one layer and reserved
494
- * names throw; remote initialization belongs in `list()`. Fiber disposal
495
- * unregisters the provider and invalidates catalog caches.
496
- * @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
497
- * @returns the exact Cordis effect disposer that unregisters this provider;
498
- * composite effects may yield it directly to preserve teardown ordering.
499
- */
500
- registerProvider(create) {
501
- const lifecycle = new AbortController();
502
- let registration;
503
- let provider;
504
- const control = {
505
- signal: lifecycle.signal,
506
- invalidate: () => {
507
- const active = registration;
508
- if (active !== void 0 && active.layer.providers.get(active.name)?.provider === provider) this.invalidateCache();
509
- }
510
- };
511
- try {
512
- provider = create(control);
513
- const name = provider.name;
514
- if (name === RUNTIME_PROVIDER) throw new Error(`"${RUNTIME_PROVIDER}" is reserved for runtime skill registrations`);
515
- const order = this.nextProviderOrder;
516
- this.nextProviderOrder += 1;
517
- return this.layers.effect(this.ctx, (layer) => {
518
- const undo = layer.providers.insert(name, {
519
- provider,
520
- order
521
- });
522
- registration = {
523
- layer,
524
- name
525
- };
526
- return () => {
527
- registration = void 0;
528
- undo();
529
- lifecycle.abort(/* @__PURE__ */ new Error(`skill provider "${name}" disposed`));
530
- };
531
- }, { label: "skills.registerProvider()" });
532
- } catch (error) {
533
- lifecycle.abort(error);
534
- throw error;
535
- }
536
- }
537
- /**
538
- * Register a borrowed readonly runtime skill into the calling context's
539
- * layer. Project entries outrank runtime entries, which outrank user
540
- * entries, within one layer. Same-name runtime entries in one layer are
541
- * first-wins; a duplicate logs a warning and receives a no-op disposer so
542
- * it cannot remove the winner.
543
- * @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
544
- * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
545
- */
546
- register(skill) {
547
- validateRuntimeSkill(skill);
548
- const scope = scopeOf(this.ctx);
549
- const existingLayer = scope === void 0 ? this.layers.global : this.layers.peek(scope);
550
- if (existingLayer !== void 0 && existingLayer.runtime.has(skill.name)) {
551
- this.ctx.logger.warn(`runtime skill "${skill.name}" ignored because it is already registered`);
552
- return () => {};
553
- }
554
- const definition = {
555
- ...skill,
556
- invocation: skill.invocation ?? {
557
- modelInvocable: true,
558
- userInvocable: true
559
- },
560
- provider: skill.provider ?? RUNTIME_PROVIDER
561
- };
562
- return this.layers.effect(this.ctx, (layer) => {
563
- layer.runtime.set(definition.name, definition);
564
- return () => {
565
- layer.runtime.delete(definition.name);
566
- };
567
- }, { label: "skills.register()" });
568
- }
569
- /**
570
- * List invocation-neutral skill summaries for a workspace. Consumers apply
571
- * model or user invocation policy at their operational boundary. Lookup
572
- * options and provider candidates are readonly same-process values borrowed
573
- * throughout discovery.
574
- * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
575
- * @returns all sorted winning summaries.
576
- */
577
- async list(options = {}) {
578
- return (await this.snapshot(options)).skills;
579
- }
580
- /**
581
- * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
582
- * Incomplete observations are never cached, allowing consumers to retain last-good state and
583
- * retry on their next request boundary.
584
- * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
585
- * @returns sorted summaries plus discovery-completeness state.
586
- */
587
- async snapshot(options = {}) {
588
- const collected = await this.collect(options);
589
- return {
590
- skills: [...collected.entries.values()].map((entry) => toSummary(entry.candidate)).sort(compareSkillSummary),
591
- complete: collected.cacheable
592
- };
593
- }
594
- /**
595
- * Load and validate the winning candidate, passing its opaque discovery locator back to the
596
- * provider. Cancellation is rechecked after selection, including cache hits, and raced against
597
- * loading so an uncooperative provider cannot hang the caller.
598
- * @param name - kebab-case skill name.
599
- * @param options - view options; `scope` selects the viewing agent's layers,
600
- * `cwd` selects workspace-sensitive skills, and `signal` cancels work.
601
- * @returns the full skill, including body content, or `undefined`.
602
- */
603
- async get(name, options = {}) {
604
- if (!isSkillName(name)) return void 0;
605
- const collected = await this.collect(options);
606
- throwIfAborted(options.signal);
607
- const match = collected.entries.get(name);
608
- if (match === void 0) return void 0;
609
- const definition = await waitWithAbort(match.provider.get(match.candidate, options), options.signal);
610
- if (definition === void 0) return void 0;
611
- validateDefinition(definition);
612
- if (definition.name !== match.candidate.name) {
613
- this.invalidateEntry(match);
614
- return;
615
- }
616
- return definition;
617
- }
618
- async collect(options) {
619
- throwIfAborted(options.signal);
620
- let attempt = 1;
621
- while (true) {
622
- const revision = this.revision;
623
- const key = this.collectCacheKey(options.cwd, scopeChainOf(options.scope), revision);
624
- const cached = this.collectCache.get(key);
625
- if (cached !== void 0) return {
626
- entries: cached,
627
- cacheable: true
628
- };
629
- const result = await this.collectFresh(options);
630
- throwIfAborted(options.signal);
631
- if (revision !== this.revision) {
632
- if (attempt < MAX_COLLECT_ATTEMPTS) {
633
- attempt += 1;
634
- continue;
635
- }
636
- return {
637
- entries: result.entries,
638
- cacheable: false
639
- };
640
- }
641
- if (result.cacheable) {
642
- this.collectCache.set(key, result.entries);
643
- if (this.collectCache.size > this.collectCacheMaxEntries) {
644
- const oldest = this.collectCache.keys().next();
645
- this.collectCache.delete(oldest.value);
646
- }
647
- }
648
- return result;
649
- }
650
- }
651
- async collectFresh(options) {
652
- const layers = [this.layers.global, ...this.layers.chainLayers(options.scope)];
653
- const merged = /* @__PURE__ */ new Map();
654
- let cacheable = true;
655
- for (const layer of layers) {
656
- const collected = await this.collectLayer(layer, options);
657
- if (!collected.cacheable) cacheable = false;
658
- for (const entry of collected.entries) merged.set(entry.candidate.name, entry);
659
- }
660
- return {
661
- entries: merged,
662
- cacheable
663
- };
664
- }
665
- async collectLayer(layer, options) {
666
- const collected = await this.listLayerCandidates(layer, options);
667
- collected.entries.sort(compareIndexedCandidates);
668
- const seen = /* @__PURE__ */ new Set();
669
- const result = [];
670
- for (const entry of collected.entries) {
671
- const skill = entry.candidate;
672
- if (seen.has(skill.name)) {
673
- this.ctx.logger.warn(`skill "${skill.name}" from ${skill.source} ignored because a higher-priority skill already exists`);
674
- continue;
675
- }
676
- seen.add(skill.name);
677
- result.push(entry);
678
- }
679
- return {
680
- entries: result,
681
- cacheable: collected.cacheable
682
- };
683
- }
684
- async listLayerCandidates(layer, options) {
685
- throwIfAborted(options.signal);
686
- const candidates = [];
687
- let cacheable = true;
688
- let runtimeOrder = 0;
689
- for (const skill of [...layer.runtime.values()].sort((a, b) => compareCodePoints(a.name, b.name))) {
690
- candidates.push({
691
- candidate: runtimeCandidate(skill),
692
- provider: RUNTIME_SKILL_PROVIDER,
693
- providerOrder: -1,
694
- localOrder: runtimeOrder,
695
- layer
696
- });
697
- runtimeOrder += 1;
698
- }
699
- for (const { provider, order } of [...layer.providers.values()]) {
700
- let localOrder = 0;
701
- let output;
702
- try {
703
- output = await waitWithAbort(provider.list(options), options.signal);
704
- } catch (error) {
705
- if (options.signal?.aborted === true) throw toError(options.signal.reason);
706
- cacheable = false;
707
- this.ctx.logger.warn(`skill provider "${provider.name}" skipped: ${errorMessage(error)}`);
708
- }
709
- if (output === void 0) continue;
710
- const observation = normalizeProviderObservation(output, provider.name);
711
- if (!observation.complete) cacheable = false;
712
- for (const candidate of observation.candidates) {
713
- validateCandidate(candidate, provider.name);
714
- candidates.push({
715
- candidate,
716
- provider,
717
- providerOrder: order,
718
- localOrder,
719
- layer
720
- });
721
- localOrder += 1;
722
- }
723
- }
724
- return {
725
- entries: candidates,
726
- cacheable
727
- };
728
- }
729
- invalidateCache() {
730
- this.revision += 1;
731
- this.collectCache.clear();
732
- this.notifyChange();
733
- }
734
- /** Invalidate after a stale definition load, only while the exact registration that produced the entry is still live. */
735
- invalidateEntry(entry) {
736
- /* v8 ignore else -- A definition load can outlive the exact provider registration it selected. */
737
- if (entry.layer.providers.get(entry.provider.name)?.provider === entry.provider) this.invalidateCache();
738
- }
739
- scopeId(key) {
740
- let id = this.scopeIds.get(key);
741
- if (id === void 0) {
742
- id = this.nextScopeId;
743
- this.nextScopeId += 1;
744
- this.scopeIds.set(key, id);
745
- }
746
- return id;
747
- }
748
- collectCacheKey(cwd, chain, revision) {
749
- return JSON.stringify({
750
- cwd,
751
- scopes: chain.map((key) => this.scopeId(key)),
752
- revision
753
- });
754
- }
755
- /** Notify catalog observers without making their refresh work load-bearing. */
756
- notifyChange() {
757
- for (const callback of this.ctx.events.dispatch("emit", ["skills/change"])) try {
758
- const returned = callback();
759
- Promise.resolve(returned).catch((error) => {
760
- this.ctx.logger.warn(`skills/change listener rejected: ${errorMessage(error)}`);
761
- });
762
- } catch (error) {
763
- this.ctx.logger.warn(`skills/change listener threw: ${errorMessage(error)}`);
764
- }
765
- }
766
- });
767
- function normalizeProviderObservation(output, providerName) {
768
- if (Array.isArray(output)) return {
769
- candidates: output,
770
- complete: true
771
- };
772
- if (output === null || typeof output !== "object") throw invalidProviderObservation(providerName);
773
- const observation = output;
774
- if (!Array.isArray(observation.candidates) || typeof observation.complete !== "boolean") throw invalidProviderObservation(providerName);
775
- return observation;
776
- }
777
- function invalidProviderObservation(providerName) {
778
- return /* @__PURE__ */ new TypeError(`skill provider "${providerName}" list() must return an array or { candidates, complete } observation`);
779
- }
780
- const RUNTIME_SKILL_PROVIDER = {
781
- name: RUNTIME_PROVIDER,
782
- /* v8 ignore next -- Runtime skills are injected directly by the registry; this provider only owns `get()`. */
783
- list() {
784
- return Promise.resolve([]);
785
- },
786
- get(candidate) {
787
- return Promise.resolve(candidate.locator);
788
- }
789
- };
790
- function runtimeCandidate(skill) {
791
- return {
792
- name: skill.name,
793
- description: skill.description,
794
- ...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
795
- invocation: skill.invocation,
796
- source: skill.source,
797
- provider: skill.provider,
798
- ...skill.resourceBase !== void 0 ? { resourceBase: skill.resourceBase } : {},
799
- rank: RUNTIME_RANK,
800
- locator: skill,
801
- ...skill.path !== void 0 ? { path: skill.path } : {},
802
- ...skill.metadata !== void 0 ? { metadata: skill.metadata } : {}
803
- };
804
- }
805
- function validateCandidate(candidate, providerName) {
806
- if (typeof candidate.name !== "string") throw new TypeError(`skill provider "${providerName}" returned a non-string skill name`);
807
- if (!SKILL_NAME.test(candidate.name)) throw new Error(`skill provider "${providerName}" returned invalid skill name "${candidate.name}"`);
808
- if (typeof candidate.description !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string description`);
809
- if (candidate.description.length === 0) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" without a description`);
810
- validateInvocation(candidate.invocation, `skill provider "${providerName}" returned skill "${candidate.name}"`);
811
- if (candidate.whenToUse !== void 0 && typeof candidate.whenToUse !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string whenToUse`);
812
- if (typeof candidate.source !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string source`);
813
- if (typeof candidate.rank !== "number" || !Number.isFinite(candidate.rank)) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" with an invalid rank`);
814
- if (typeof candidate.provider !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string provider`);
815
- if (candidate.provider !== providerName) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" for provider "${candidate.provider}"`);
816
- if (candidate.path !== void 0 && typeof candidate.path !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string path`);
817
- }
818
- function validateRuntimeSkill(skill) {
819
- if (!SKILL_NAME.test(skill.name)) throw new Error(`invalid skill name "${skill.name}"`);
820
- if (skill.description.length === 0) throw new Error(`skill "${skill.name}" requires a description`);
821
- validateInvocation(skill.invocation, `runtime skill "${skill.name}"`);
822
- }
823
- /** Validate a definition loaded from a provider-controlled parser or remote source. */
824
- function validateDefinition(skill) {
825
- const name = skill.name;
826
- const description = skill.description;
827
- const whenToUse = skill.whenToUse;
828
- const invocation = skill.invocation;
829
- const source = skill.source;
830
- const provider = skill.provider;
831
- const content = skill.content;
832
- const path = skill.path;
833
- if (typeof name !== "string") throw new TypeError("loaded skill name must be a string");
834
- if (!SKILL_NAME.test(name)) throw new Error(`loaded skill has invalid name "${name}"`);
835
- if (typeof description !== "string") throw new TypeError(`loaded skill "${name}" description must be a string`);
836
- if (description.length === 0) throw new Error(`loaded skill "${name}" requires a description`);
837
- validateInvocation(invocation, `loaded skill "${name}"`);
838
- if (whenToUse !== void 0 && typeof whenToUse !== "string") throw new TypeError(`loaded skill "${name}" whenToUse must be a string`);
839
- if (typeof source !== "string") throw new TypeError(`loaded skill "${name}" source must be a string`);
840
- if (typeof provider !== "string") throw new TypeError(`loaded skill "${name}" provider must be a string`);
841
- if (typeof content !== "string") throw new TypeError(`loaded skill "${name}" content must be a string`);
842
- if (path !== void 0 && typeof path !== "string") throw new TypeError(`loaded skill "${name}" path must be a string`);
843
- }
844
- function toSummary(skill) {
845
- const { name, description, whenToUse, invocation, source, provider, resourceBase } = skill;
846
- return {
847
- name,
848
- description,
849
- ...whenToUse !== void 0 ? { whenToUse } : {},
850
- invocation,
851
- source,
852
- provider,
853
- ...resourceBase !== void 0 ? { resourceBase } : {}
854
- };
855
- }
856
- function validateInvocation(invocation, subject) {
857
- if (invocation === void 0) return;
858
- if (typeof invocation !== "object" || invocation === null || Array.isArray(invocation)) throw new TypeError(`${subject} with a non-object invocation policy`);
859
- const policy = invocation;
860
- if (typeof policy.modelInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.modelInvocable`);
861
- if (typeof policy.userInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.userInvocable`);
862
- }
863
- function compareSkillSummary(left, right) {
864
- return compareCodePoints(left.name, right.name);
865
- }
866
- function compareCodePoints(left, right) {
867
- if (left < right) return -1;
868
- if (left > right) return 1;
869
- return 0;
870
- }
871
- function compareIndexedCandidates(left, right) {
872
- return left.candidate.rank - right.candidate.rank || left.providerOrder - right.providerOrder || left.localOrder - right.localOrder;
873
- }
874
- function assertPositiveInteger(name, value, minimum = 1) {
875
- if (!Number.isInteger(value) || value < minimum) throw new Error(`skill: ${name} must be an integer greater than or equal to ${minimum}`);
876
- }
877
- function waitWithAbort(promise, signal) {
878
- if (signal === void 0) return promise;
879
- throwIfAborted(signal);
880
- return new Promise((resolve, reject) => {
881
- const cleanup = () => {
882
- signal.removeEventListener("abort", onAbort);
883
- };
884
- const onAbort = () => {
885
- cleanup();
886
- reject(toError(signal.reason));
887
- };
888
- signal.addEventListener("abort", onAbort, { once: true });
889
- promise.then((value) => {
890
- cleanup();
891
- resolve(value);
892
- }, (error) => {
893
- cleanup();
894
- reject(toError(error));
895
- });
896
- });
897
- }
898
- /** Throw a total Error for an already-aborted lookup. */
899
- function throwIfAborted(signal) {
900
- if (signal?.aborted === true) throw toError(signal.reason);
901
- }
902
- /** Normalize an arbitrary abort or provider failure without trusting coercion. */
903
- function toError(error) {
904
- try {
905
- if (error instanceof Error) return error;
906
- } catch {}
907
- return new Error(errorMessage(error));
908
- }
909
- /** Render an arbitrary provider failure without letting coercion escape containment. */
910
- function errorMessage(error) {
911
- try {
912
- return String(error);
913
- } catch {
914
- return "[unrenderable thrown value]";
915
- }
916
- }
917
- //#endregion
918
- //#region lib/types/content.js
919
- /**
920
- * Ponytail skill bodies, ported from github.com/DietrichGebert/ponytail and
921
- * lightly adapted to the DeepSeek Harness surface (slash commands and the
922
- * `skill` tool). The `ponytail` skill is a mode-aware pointer card: the actual
923
- * ruleset is injected per session as the mode-filtered `PONYTAIL MODE ACTIVE`
924
- * section (see `instructions.ts`) and must not be duplicated here. The other
925
- * five skills ship verbatim as runtime skills.
926
- *
927
- * @module @deepseek-ai/dsh-ponytail
928
- */
929
- /** The always-on lazy-senior-dev ruleset: also registered as a loadable skill. */
930
- const PONYTAIL_SKILL_BODY = `
931
- You are the ponytail persona — the lazy senior developer. Your active ruleset
932
- is ALREADY injected every turn as the "PONYTAIL MODE ACTIVE — level: <mode>"
933
- system-prompt section, filtered to this session's intensity. Follow exactly
934
- that section; do NOT reload, replace, or re-derive the ruleset from anywhere
935
- else — the section is the single source of truth and it is mode-aware.
936
-
937
- - Switch level: \`/ponytail lite|full|ultra|off\` (session-scoped)
938
- - Query: \`/ponytail status\`
939
- - Deactivate: "stop ponytail" / "normal mode"
940
- - One-shot skills: \`/ponytail-review\`, \`/ponytail-audit\`, \`/ponytail-debt\`,
941
- \`/ponytail-gain\`, \`/ponytail-help\`
942
- - Reference: https://github.com/DietrichGebert/ponytail
943
- `;
944
- const PONYTAIL_DESCRIPTION = "Ponytail activation, modes, configuration, and help reference. The active ruleset is injected every turn by the system prompt; this skill is a pointer card. Use only when the user asks about Ponytail activation, modes, configuration, or help. Coding tasks already receive the active ruleset from the system prompt.";
945
- const REVIEW_SKILL_BODY = `
946
- Review diffs for unnecessary complexity. One line per finding: location, what
947
- to cut, what replaces it. The diff's best outcome is getting shorter.
1
+ import { createRequire } from "node:module";
2
+ import { Service } from "@deepseek-ai/cordis";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ //#region ../../llm/llm/src/brand.ts
8
+ /**
9
+ * Brand a message identifier.
10
+ * @param id - the opaque message identifier.
11
+ * @returns the same string, branded; no validation is performed.
12
+ */
13
+ function MessageId(id) {
14
+ return id;
15
+ }
16
+ //#endregion
17
+ //#region ../../llm/llm/src/call-config.ts
18
+ /**
19
+ * Deep-freeze a value in place with an iterative traversal, guarding cycles,
20
+ * so later mutation throws without imposing a JavaScript call-stack depth cap.
21
+ * {@link AbortSignal} objects are deliberately skipped because they are the
22
+ * request's live cancellation channel and freezing them breaks abort.
23
+ * @param value - the value to freeze in place.
24
+ * @returns the same value, frozen.
25
+ */
26
+ function deepFreeze(value) {
27
+ const seen = /* @__PURE__ */ new WeakSet();
28
+ const pending = [{
29
+ kind: "visit",
30
+ node: value
31
+ }];
32
+ while (pending.length > 0) {
33
+ const task = pending.pop();
34
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
35
+ if (task === void 0) continue;
36
+ if (task.kind === "property") {
37
+ pending.push({
38
+ kind: "visit",
39
+ node: task.source[task.key]
40
+ });
41
+ continue;
42
+ }
43
+ const node = task.node;
44
+ if (node === null || typeof node !== "object") continue;
45
+ if (node instanceof AbortSignal) continue;
46
+ if (seen.has(node)) continue;
47
+ seen.add(node);
48
+ Object.freeze(node);
49
+ const keys = Object.keys(node);
50
+ for (let index = keys.length - 1; index >= 0; index--) {
51
+ const key = keys[index];
52
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
53
+ if (key === void 0) continue;
54
+ pending.push({
55
+ kind: "property",
56
+ source: node,
57
+ key
58
+ });
59
+ }
60
+ }
61
+ return value;
62
+ }
63
+ //#endregion
64
+ //#region ../../llm/llm/src/message.ts
65
+ /** Message value types, identity, and immutable construction helpers. */
66
+ /**
67
+ * Detach and deep-freeze a message whose identity already exists.
68
+ * @param message - complete message, including its stable identity.
69
+ * @returns an immutable snapshot that preserves the identity.
70
+ */
71
+ function freezeMessage(message) {
72
+ return deepFreeze(structuredClone(message));
73
+ }
74
+ /**
75
+ * Create one identified message and freeze it before publication.
76
+ * @param input - complete role, content, and source for a new message.
77
+ * @returns an immutable message with a fresh stable identity.
78
+ */
79
+ function createMessage(input) {
80
+ return freezeMessage({
81
+ ...input,
82
+ id: MessageId(crypto.randomUUID())
83
+ });
84
+ }
85
+ /**
86
+ * Create one identified user-role message and freeze it before publication.
87
+ * @param input - complete content and source for a new user message.
88
+ * @returns an immutable user message with a fresh stable identity.
89
+ */
90
+ function createUserMessage(input) {
91
+ return createMessage({
92
+ ...input,
93
+ role: "user"
94
+ });
95
+ }
96
+ //#endregion
97
+ //#region ../../util/timeout/src/index.ts
98
+ /** Largest delay Node schedules without clamping it to one millisecond. */
99
+ const MAX_TIMER_DELAY_MS = 2147483647;
100
+ //#endregion
101
+ //#region ../../llm/llm/src/error.ts
102
+ /**
103
+ * Canonical provider-neutral code for a response that completed normally but
104
+ * carried no content blocks at all. Providers occasionally emit a degenerate
105
+ * completion (a terminal stop with zero output); adapters classify it as this
106
+ * failure instead of yielding an empty assistant message, because an empty
107
+ * message silently ends the turn with nothing for the user or the loop to act
108
+ * on. The attempt produced nothing durable, so retry policy treats it as safe
109
+ * to repeat.
110
+ */
111
+ const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
112
+ new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
113
+ new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
114
+ new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
115
+ //#endregion
116
+ //#region ../../llm/llm/src/retry-policy.ts
117
+ /**
118
+ * Provider-owned request-retry policy configuration and resolution.
119
+ *
120
+ * Adapters expose one resolved policy per registered provider route; the
121
+ * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
122
+ *
123
+ * @module @deepseek-ai/dsh-llm/retry-policy
124
+ */
125
+ const DEFAULT_MAX_RETRIES = 5;
126
+ const DEFAULT_INITIAL_DELAY_MS = 500;
127
+ const DEFAULT_MAX_DELAY_MS = 1e4;
128
+ const DEFAULT_JITTER_RATIO = .1;
129
+ const DEFAULT_RETRYABLE_CODES = Object.freeze([
130
+ EMPTY_RESPONSE_CODE,
131
+ "RATE_LIMIT",
132
+ "SERVER",
133
+ "TIMEOUT",
134
+ "TRANSPORT"
135
+ ]);
136
+ const backoffSchema = z.object({
137
+ initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
138
+ maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
139
+ jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
140
+ });
141
+ const normalPolicySchema = z.object({
142
+ mode: z.const("normal").required(),
143
+ maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
144
+ retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
145
+ backoff: backoffSchema
146
+ });
147
+ const alwaysPolicySchema = z.object({
148
+ mode: z.const("always").required(),
149
+ backoff: backoffSchema
150
+ });
151
+ z.union([normalPolicySchema, alwaysPolicySchema]);
152
+ //#endregion
153
+ //#region ../../llm/llm/src/attribution.ts
154
+ /**
155
+ * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
156
+ * adapters from drifting. See
157
+ * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
158
+ *
159
+ * App-attribution vocabulary for provider requests.
160
+ * @module @deepseek-ai/dsh-llm/attribution
161
+ */
162
+ const { version } = createRequire(import.meta.url)("../package.json");
163
+ //#endregion
164
+ //#region ../../llm/llm/src/never.ts
165
+ /**
166
+ * Exhaustiveness helper for closed core unions. Use {@link assertNever} at the default branch so a
167
+ * new variant fails compilation at every required handler. Do not use it for declaration-merged
168
+ * unions such as session events or content blocks: handle known variants and explicitly fall
169
+ * through because plugins may add valid unknown cases.
170
+ * @module @deepseek-ai/dsh-llm/never
171
+ */
172
+ /**
173
+ * Mark an unreachable closed-union branch. A newly unhandled typed variant fails at the call site;
174
+ * a value that escaped its type throws with diagnostics at runtime.
175
+ * @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
176
+ * @param context - optional label (e.g. the switch site) prefixed into the throw message.
177
+ * @returns never — it always throws, with the offending value JSON-rendered in the message.
178
+ */
179
+ function assertNever(value, context) {
180
+ const rendered = JSON.stringify(value) ?? String(value);
181
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
182
+ }
183
+ //#endregion
184
+ //#region ../../core/scope/src/store.ts
185
+ /**
186
+ * Insertion-ordered named entries with caller-owned duplicate diagnostics.
187
+ *
188
+ * Values are borrowed. Iterators are live within one nonempty table
189
+ * generation; draining the table detaches them from later insertions. Each
190
+ * successful insertion returns an idempotent undo for that exact entry.
191
+ */
192
+ var NamedEntries = class {
193
+ duplicateError;
194
+ data = /* @__PURE__ */ new Map();
195
+ constructor(duplicateError) {
196
+ this.duplicateError = duplicateError;
197
+ }
198
+ /**
199
+ * Insert one unique name.
200
+ * @param name - name unique within this table.
201
+ * @param value - borrowed value to retain.
202
+ * @returns an idempotent undo that removes only this insertion.
203
+ */
204
+ insert(name, value) {
205
+ const data = this.data;
206
+ if (data.has(name)) throw this.duplicateError(name);
207
+ data.set(name, value);
208
+ let active = true;
209
+ return () => {
210
+ if (!active) return;
211
+ active = false;
212
+ data.delete(name);
213
+ if (data.size === 0 && this.data === data) this.data = /* @__PURE__ */ new Map();
214
+ };
215
+ }
216
+ /**
217
+ * Read one named value.
218
+ * @param name - name to resolve.
219
+ * @returns the retained value, or `undefined` when absent.
220
+ */
221
+ get(name) {
222
+ return this.data.get(name);
223
+ }
224
+ /**
225
+ * Test one name for membership.
226
+ * @param name - name to test.
227
+ * @returns whether the table contains that name.
228
+ */
229
+ has(name) {
230
+ return this.data.has(name);
231
+ }
232
+ /**
233
+ * Iterate live names in insertion order.
234
+ * @returns the native live key iterator.
235
+ */
236
+ keys() {
237
+ return this.data.keys();
238
+ }
239
+ /**
240
+ * Iterate live entries in insertion order.
241
+ * @returns the native live entry iterator.
242
+ */
243
+ entries() {
244
+ return this.data.entries();
245
+ }
246
+ /**
247
+ * Iterate live values in insertion order.
248
+ * @returns the native live value iterator.
249
+ */
250
+ values() {
251
+ return this.data.values();
252
+ }
253
+ /**
254
+ * Test whether this table has no entries.
255
+ * @returns whether the table is empty.
256
+ */
257
+ isEmpty() {
258
+ return this.data.size === 0;
259
+ }
260
+ };
261
+ /**
262
+ * Own the global and exact-scope layers for one registry.
263
+ *
264
+ * Reads never create scoped layers. Registrations derive both visibility and
265
+ * effect ownership from the supplied Cordis context, collect undo before
266
+ * notification, and reclaim only a completely empty aggregate layer.
267
+ */
268
+ var ScopedLayers = class {
269
+ createLayer;
270
+ onChange;
271
+ /** The eagerly constructed context-global layer. */
272
+ global;
273
+ scoped = /* @__PURE__ */ new Map();
274
+ constructor(createLayer, onChange) {
275
+ this.createLayer = createLayer;
276
+ this.onChange = onChange;
277
+ this.global = createLayer(void 0);
278
+ }
279
+ /**
280
+ * Read an existing exact-scope overlay. Deliberately chain-blind: callers
281
+ * addressing one scope's OWN contributions (its restrictions, its guards)
282
+ * must not silently pick up an ancestor's — use {@link chainLayers} where
283
+ * inheritance is the point.
284
+ * @param scope - exact scope key; `undefined` denotes no overlay.
285
+ * @returns the existing scoped layer, or `undefined` without creating one.
286
+ */
287
+ peek(scope) {
288
+ if (scope === void 0) return void 0;
289
+ return this.scoped.get(scope);
290
+ }
291
+ /**
292
+ * Existing overlays along the scope's parent chain ({@link scopeChainOf}),
293
+ * farthest ancestor first and the exact scope last, so a caller layering
294
+ * them in order gives the nearest scope the final word.
295
+ * @param scope - viewing scope, or `undefined` for no overlays.
296
+ * @returns the existing layers, nearest last; absent overlays are skipped.
297
+ */
298
+ chainLayers(scope) {
299
+ const layers = [];
300
+ for (const key of scopeChainOf(scope).reverse()) {
301
+ const layer = this.scoped.get(key);
302
+ if (layer !== void 0) layers.push(layer);
303
+ }
304
+ return layers;
305
+ }
306
+ /**
307
+ * Materialize global named entries followed by scope-chain shadows,
308
+ * farthest ancestor first, so the nearest scope's entry wins a name.
309
+ * @param scope - viewing scope, or `undefined` for the global view.
310
+ * @param pick - select the named table from a layer.
311
+ * @returns an insertion-ordered effective map.
312
+ */
313
+ merge(scope, pick) {
314
+ const merged = new Map(pick(this.global).entries());
315
+ for (const layer of this.chainLayers(scope)) for (const [name, value] of pick(layer).entries()) merged.set(name, value);
316
+ return merged;
317
+ }
318
+ /**
319
+ * Attach one synchronous layer mutation to its registration context.
320
+ * @param ctx - context that determines both scope visibility and effect ownership.
321
+ * @param action - atomic mutation returning its synchronous undo.
322
+ * @param options - Cordis effect label and optional change notification.
323
+ * @returns the exact disposer returned by `ctx.effect()`.
324
+ */
325
+ effect(ctx, action, options) {
326
+ const scope = scopeOf(ctx);
327
+ const notify = options.notify ?? true;
328
+ return ctx.effect(function* () {
329
+ let layer;
330
+ let created = false;
331
+ if (scope === void 0) layer = this.global;
332
+ else {
333
+ const existing = this.scoped.get(scope);
334
+ if (existing === void 0) {
335
+ layer = this.createLayer(scope);
336
+ this.scoped.set(scope, layer);
337
+ created = true;
338
+ } else layer = existing;
339
+ }
340
+ let undo;
341
+ try {
342
+ undo = action(layer);
343
+ } catch (error) {
344
+ if (scope !== void 0 && created && layer.isEmpty()) this.scoped.delete(scope);
345
+ throw error;
346
+ }
347
+ yield () => {
348
+ undo();
349
+ if (scope !== void 0 && layer.isEmpty()) this.scoped.delete(scope);
350
+ if (notify) this.onChange();
351
+ };
352
+ if (notify) this.onChange();
353
+ }.bind(this), options.label);
354
+ }
355
+ };
356
+ //#endregion
357
+ //#region ../../core/scope/src/index.ts
358
+ /** Context tag written by {@link createScope}. */
359
+ const kScope = Symbol("dsh.scope");
360
+ /**
361
+ * The enclosing scope of each key. One relation powers both directions of
362
+ * scope nesting: registration views inherit DOWN the chain (a child scope
363
+ * sees its ancestors' layers — {@link ScopedLayers}), and event admission
364
+ * extends UP it (a listener tagged with an ancestor receives events dispatched
365
+ * to a descendant key — {@link scopeTarget}).
366
+ */
367
+ const scopeParents = /* @__PURE__ */ new WeakMap();
368
+ /**
369
+ * The chain from a key to its root ancestor.
370
+ * @param key - the starting key, or `undefined` for the empty chain.
371
+ * @returns keys nearest-first: `[key, parent, grandparent, …]`.
372
+ */
373
+ function scopeChainOf(key) {
374
+ const chain = [];
375
+ for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) chain.push(cursor);
376
+ return chain;
377
+ }
378
+ /**
379
+ * Read the nearest scope tag inherited by a context.
380
+ * @param ctx - context to inspect.
381
+ * @returns its scope key, or `undefined` for an unscoped context.
382
+ */
383
+ function scopeOf(ctx) {
384
+ return ctx[kScope];
385
+ }
386
+ //#endregion
387
+ //#region ../../skill/skill/src/index.ts
388
+ /**
389
+ * Agent skill provider registry.
390
+ *
391
+ * This package owns the Service Definition role of the skill capability seam.
392
+ * Concrete
393
+ * providers such as `@deepseek-ai/dsh-skill-filesystem` decide where skills come
394
+ * from; this service only merges provider catalogs, resolves the winning skill
395
+ * for a name, and exposes the winning summaries and definitions to consumers.
396
+ *
397
+ * @module @deepseek-ai/dsh-skill
398
+ */
399
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
400
+ const DEFAULT_COLLECT_CACHE_ENTRIES = 128;
401
+ const MAX_COLLECT_ATTEMPTS = 2;
402
+ const RUNTIME_PROVIDER = "runtime";
403
+ const RUNTIME_RANK = 250;
404
+ /**
405
+ * Return whether a string is a valid kebab-case skill name.
406
+ * @param name - candidate skill name to validate.
407
+ * @returns whether the name matches the public skill-name grammar.
408
+ */
409
+ function isSkillName(name) {
410
+ return SKILL_NAME.test(name);
411
+ }
412
+ /**
413
+ * Render one loaded skill for the model. The output is shared verbatim by the
414
+ * `skill` tool result and the user-explicit invocation injection, so the model
415
+ * sees one canonical `<skill_content>` shape on both paths. The name rides an
416
+ * escaped attribute; the body is embedded verbatim (skills are trusted local
417
+ * content, and user-supplied invocation text stays outside this wrapper).
418
+ * @param skill - name, provider, optional resource base, and body to render.
419
+ * @returns the complete model-facing `<skill_content>` block.
420
+ */
421
+ function renderSkillContent(skill) {
422
+ const resourceHint = renderResourceHint(skill);
423
+ return [
424
+ `<skill_content name="${escapeAttr(skill.name)}">`,
425
+ "<skill_resources>",
426
+ ...resourceHint,
427
+ "</skill_resources>",
428
+ "",
429
+ "<skill_instructions>",
430
+ skill.content,
431
+ "</skill_instructions>",
432
+ "</skill_content>"
433
+ ].join("\n");
434
+ }
435
+ function renderResourceHint(skill) {
436
+ const base = skill.resourceBase;
437
+ if (base === void 0) return [`Resources for this skill are managed by provider "${escapeText(skill.provider)}".`, "Load referenced resources only as needed."];
438
+ switch (base.kind) {
439
+ case "directory": return [`Base directory for this skill: ${escapeText(base.path)}`, "Resolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed."];
440
+ case "url": return [`Base URL for this skill: ${escapeText(base.url)}`, "Resolve relative URLs mentioned by this skill against the base URL before using them. Load referenced resources only as needed."];
441
+ case "opaque": return [`Resources for this skill: ${escapeText(base.description)}`, "Load referenced resources only as needed."];
442
+ /* v8 ignore start -- SkillResourceBase is a closed union; a future kind must fail compilation here. */
443
+ default: return assertNever(base, "SkillResourceBase.kind");
444
+ }
445
+ }
446
+ function escapeAttr(value) {
447
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
448
+ }
449
+ /**
450
+ * Escape model-facing prose embedded inside skill markup so provider-supplied
451
+ * text cannot open or close framing tags.
452
+ * @param value - raw prose to embed.
453
+ * @returns the escaped text.
454
+ */
455
+ function escapeText(value) {
456
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
457
+ }
458
+ /** One scope's complete skill-registry contribution. */
459
+ var SkillLayer = class {
460
+ /** Providers registered through contexts carrying this scope, insertion-ordered. */
461
+ providers;
462
+ /** Runtime skills registered through contexts carrying this scope. */
463
+ runtime = /* @__PURE__ */ new Map();
464
+ constructor(scope) {
465
+ this.providers = new NamedEntries((name) => /* @__PURE__ */ new Error(scope === void 0 ? `a skill provider named "${name}" is already registered` : `a skill provider named "${name}" is already registered in this scope`));
466
+ }
467
+ /** Whether every contribution table in this aggregate layer is empty. */
468
+ isEmpty() {
469
+ return this.providers.isEmpty() && this.runtime.size === 0;
470
+ }
471
+ };
472
+ (class extends Service {
473
+ static Config = z.object({ collectCacheMaxEntries: z.number().default(DEFAULT_COLLECT_CACHE_ENTRIES) });
474
+ collectCacheMaxEntries;
475
+ layers = new ScopedLayers((scope) => new SkillLayer(scope), () => {
476
+ this.invalidateCache();
477
+ });
478
+ collectCache = /* @__PURE__ */ new Map();
479
+ revision = 0;
480
+ nextProviderOrder = 0;
481
+ /** Stable identities for cache keys; scope keys are opaque identity-compared objects. */
482
+ scopeIds = /* @__PURE__ */ new WeakMap();
483
+ nextScopeId = 1;
484
+ constructor(ctx, config = {}) {
485
+ super(ctx, "skills");
486
+ this.collectCacheMaxEntries = config.collectCacheMaxEntries ?? DEFAULT_COLLECT_CACHE_ENTRIES;
487
+ assertPositiveInteger("collectCacheMaxEntries", this.collectCacheMaxEntries);
488
+ }
489
+ /**
490
+ * Register a borrowed same-process provider synchronously during plugin
491
+ * apply, into the calling context's layer: a scoped context (an agent
492
+ * preset's standing mount) registers for that scope alone, an unscoped
493
+ * context registers globally. Duplicate names within one layer and reserved
494
+ * names throw; remote initialization belongs in `list()`. Fiber disposal
495
+ * unregisters the provider and invalidates catalog caches.
496
+ * @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
497
+ * @returns the exact Cordis effect disposer that unregisters this provider;
498
+ * composite effects may yield it directly to preserve teardown ordering.
499
+ */
500
+ registerProvider(create) {
501
+ const lifecycle = new AbortController();
502
+ let registration;
503
+ let provider;
504
+ const control = {
505
+ signal: lifecycle.signal,
506
+ invalidate: () => {
507
+ const active = registration;
508
+ if (active !== void 0 && active.layer.providers.get(active.name)?.provider === provider) this.invalidateCache();
509
+ }
510
+ };
511
+ try {
512
+ provider = create(control);
513
+ const name = provider.name;
514
+ if (name === RUNTIME_PROVIDER) throw new Error(`"${RUNTIME_PROVIDER}" is reserved for runtime skill registrations`);
515
+ const order = this.nextProviderOrder;
516
+ this.nextProviderOrder += 1;
517
+ return this.layers.effect(this.ctx, (layer) => {
518
+ const undo = layer.providers.insert(name, {
519
+ provider,
520
+ order
521
+ });
522
+ registration = {
523
+ layer,
524
+ name
525
+ };
526
+ return () => {
527
+ registration = void 0;
528
+ undo();
529
+ lifecycle.abort(/* @__PURE__ */ new Error(`skill provider "${name}" disposed`));
530
+ };
531
+ }, { label: "skills.registerProvider()" });
532
+ } catch (error) {
533
+ lifecycle.abort(error);
534
+ throw error;
535
+ }
536
+ }
537
+ /**
538
+ * Register a borrowed readonly runtime skill into the calling context's
539
+ * layer. Project entries outrank runtime entries, which outrank user
540
+ * entries, within one layer. Same-name runtime entries in one layer are
541
+ * first-wins; a duplicate logs a warning and receives a no-op disposer so
542
+ * it cannot remove the winner.
543
+ * @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
544
+ * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
545
+ */
546
+ register(skill) {
547
+ validateRuntimeSkill(skill);
548
+ const scope = scopeOf(this.ctx);
549
+ const existingLayer = scope === void 0 ? this.layers.global : this.layers.peek(scope);
550
+ if (existingLayer !== void 0 && existingLayer.runtime.has(skill.name)) {
551
+ this.ctx.logger.warn(`runtime skill "${skill.name}" ignored because it is already registered`);
552
+ return () => {};
553
+ }
554
+ const definition = {
555
+ ...skill,
556
+ invocation: skill.invocation ?? {
557
+ modelInvocable: true,
558
+ userInvocable: true
559
+ },
560
+ provider: skill.provider ?? RUNTIME_PROVIDER
561
+ };
562
+ return this.layers.effect(this.ctx, (layer) => {
563
+ layer.runtime.set(definition.name, definition);
564
+ return () => {
565
+ layer.runtime.delete(definition.name);
566
+ };
567
+ }, { label: "skills.register()" });
568
+ }
569
+ /**
570
+ * List invocation-neutral skill summaries for a workspace. Consumers apply
571
+ * model or user invocation policy at their operational boundary. Lookup
572
+ * options and provider candidates are readonly same-process values borrowed
573
+ * throughout discovery.
574
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
575
+ * @returns all sorted winning summaries.
576
+ */
577
+ async list(options = {}) {
578
+ return (await this.snapshot(options)).skills;
579
+ }
580
+ /**
581
+ * Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
582
+ * Incomplete observations are never cached, allowing consumers to retain last-good state and
583
+ * retry on their next request boundary.
584
+ * @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
585
+ * @returns sorted summaries plus discovery-completeness state.
586
+ */
587
+ async snapshot(options = {}) {
588
+ const collected = await this.collect(options);
589
+ return {
590
+ skills: [...collected.entries.values()].map((entry) => toSummary(entry.candidate)).sort(compareSkillSummary),
591
+ complete: collected.cacheable
592
+ };
593
+ }
594
+ /**
595
+ * Load and validate the winning candidate, passing its opaque discovery locator back to the
596
+ * provider. Cancellation is rechecked after selection, including cache hits, and raced against
597
+ * loading so an uncooperative provider cannot hang the caller.
598
+ * @param name - kebab-case skill name.
599
+ * @param options - view options; `scope` selects the viewing agent's layers,
600
+ * `cwd` selects workspace-sensitive skills, and `signal` cancels work.
601
+ * @returns the full skill, including body content, or `undefined`.
602
+ */
603
+ async get(name, options = {}) {
604
+ if (!isSkillName(name)) return void 0;
605
+ const collected = await this.collect(options);
606
+ throwIfAborted(options.signal);
607
+ const match = collected.entries.get(name);
608
+ if (match === void 0) return void 0;
609
+ const definition = await waitWithAbort(match.provider.get(match.candidate, options), options.signal);
610
+ if (definition === void 0) return void 0;
611
+ validateDefinition(definition);
612
+ if (definition.name !== match.candidate.name) {
613
+ this.invalidateEntry(match);
614
+ return;
615
+ }
616
+ return definition;
617
+ }
618
+ async collect(options) {
619
+ throwIfAborted(options.signal);
620
+ let attempt = 1;
621
+ while (true) {
622
+ const revision = this.revision;
623
+ const key = this.collectCacheKey(options.cwd, scopeChainOf(options.scope), revision);
624
+ const cached = this.collectCache.get(key);
625
+ if (cached !== void 0) return {
626
+ entries: cached,
627
+ cacheable: true
628
+ };
629
+ const result = await this.collectFresh(options);
630
+ throwIfAborted(options.signal);
631
+ if (revision !== this.revision) {
632
+ if (attempt < MAX_COLLECT_ATTEMPTS) {
633
+ attempt += 1;
634
+ continue;
635
+ }
636
+ return {
637
+ entries: result.entries,
638
+ cacheable: false
639
+ };
640
+ }
641
+ if (result.cacheable) {
642
+ this.collectCache.set(key, result.entries);
643
+ if (this.collectCache.size > this.collectCacheMaxEntries) {
644
+ const oldest = this.collectCache.keys().next();
645
+ this.collectCache.delete(oldest.value);
646
+ }
647
+ }
648
+ return result;
649
+ }
650
+ }
651
+ async collectFresh(options) {
652
+ const layers = [this.layers.global, ...this.layers.chainLayers(options.scope)];
653
+ const merged = /* @__PURE__ */ new Map();
654
+ let cacheable = true;
655
+ for (const layer of layers) {
656
+ const collected = await this.collectLayer(layer, options);
657
+ if (!collected.cacheable) cacheable = false;
658
+ for (const entry of collected.entries) merged.set(entry.candidate.name, entry);
659
+ }
660
+ return {
661
+ entries: merged,
662
+ cacheable
663
+ };
664
+ }
665
+ async collectLayer(layer, options) {
666
+ const collected = await this.listLayerCandidates(layer, options);
667
+ collected.entries.sort(compareIndexedCandidates);
668
+ const seen = /* @__PURE__ */ new Set();
669
+ const result = [];
670
+ for (const entry of collected.entries) {
671
+ const skill = entry.candidate;
672
+ if (seen.has(skill.name)) {
673
+ this.ctx.logger.warn(`skill "${skill.name}" from ${skill.source} ignored because a higher-priority skill already exists`);
674
+ continue;
675
+ }
676
+ seen.add(skill.name);
677
+ result.push(entry);
678
+ }
679
+ return {
680
+ entries: result,
681
+ cacheable: collected.cacheable
682
+ };
683
+ }
684
+ async listLayerCandidates(layer, options) {
685
+ throwIfAborted(options.signal);
686
+ const candidates = [];
687
+ let cacheable = true;
688
+ let runtimeOrder = 0;
689
+ for (const skill of [...layer.runtime.values()].sort((a, b) => compareCodePoints(a.name, b.name))) {
690
+ candidates.push({
691
+ candidate: runtimeCandidate(skill),
692
+ provider: RUNTIME_SKILL_PROVIDER,
693
+ providerOrder: -1,
694
+ localOrder: runtimeOrder,
695
+ layer
696
+ });
697
+ runtimeOrder += 1;
698
+ }
699
+ for (const { provider, order } of [...layer.providers.values()]) {
700
+ let localOrder = 0;
701
+ let output;
702
+ try {
703
+ output = await waitWithAbort(provider.list(options), options.signal);
704
+ } catch (error) {
705
+ if (options.signal?.aborted === true) throw toError(options.signal.reason);
706
+ cacheable = false;
707
+ this.ctx.logger.warn(`skill provider "${provider.name}" skipped: ${errorMessage(error)}`);
708
+ }
709
+ if (output === void 0) continue;
710
+ const observation = normalizeProviderObservation(output, provider.name);
711
+ if (!observation.complete) cacheable = false;
712
+ for (const candidate of observation.candidates) {
713
+ validateCandidate(candidate, provider.name);
714
+ candidates.push({
715
+ candidate,
716
+ provider,
717
+ providerOrder: order,
718
+ localOrder,
719
+ layer
720
+ });
721
+ localOrder += 1;
722
+ }
723
+ }
724
+ return {
725
+ entries: candidates,
726
+ cacheable
727
+ };
728
+ }
729
+ invalidateCache() {
730
+ this.revision += 1;
731
+ this.collectCache.clear();
732
+ this.notifyChange();
733
+ }
734
+ /** Invalidate after a stale definition load, only while the exact registration that produced the entry is still live. */
735
+ invalidateEntry(entry) {
736
+ /* v8 ignore else -- A definition load can outlive the exact provider registration it selected. */
737
+ if (entry.layer.providers.get(entry.provider.name)?.provider === entry.provider) this.invalidateCache();
738
+ }
739
+ scopeId(key) {
740
+ let id = this.scopeIds.get(key);
741
+ if (id === void 0) {
742
+ id = this.nextScopeId;
743
+ this.nextScopeId += 1;
744
+ this.scopeIds.set(key, id);
745
+ }
746
+ return id;
747
+ }
748
+ collectCacheKey(cwd, chain, revision) {
749
+ return JSON.stringify({
750
+ cwd,
751
+ scopes: chain.map((key) => this.scopeId(key)),
752
+ revision
753
+ });
754
+ }
755
+ /** Notify catalog observers without making their refresh work load-bearing. */
756
+ notifyChange() {
757
+ for (const callback of this.ctx.events.dispatch("emit", ["skills/change"])) try {
758
+ const returned = callback();
759
+ Promise.resolve(returned).catch((error) => {
760
+ this.ctx.logger.warn(`skills/change listener rejected: ${errorMessage(error)}`);
761
+ });
762
+ } catch (error) {
763
+ this.ctx.logger.warn(`skills/change listener threw: ${errorMessage(error)}`);
764
+ }
765
+ }
766
+ });
767
+ function normalizeProviderObservation(output, providerName) {
768
+ if (Array.isArray(output)) return {
769
+ candidates: output,
770
+ complete: true
771
+ };
772
+ if (output === null || typeof output !== "object") throw invalidProviderObservation(providerName);
773
+ const observation = output;
774
+ if (!Array.isArray(observation.candidates) || typeof observation.complete !== "boolean") throw invalidProviderObservation(providerName);
775
+ return observation;
776
+ }
777
+ function invalidProviderObservation(providerName) {
778
+ return /* @__PURE__ */ new TypeError(`skill provider "${providerName}" list() must return an array or { candidates, complete } observation`);
779
+ }
780
+ const RUNTIME_SKILL_PROVIDER = {
781
+ name: RUNTIME_PROVIDER,
782
+ /* v8 ignore next -- Runtime skills are injected directly by the registry; this provider only owns `get()`. */
783
+ list() {
784
+ return Promise.resolve([]);
785
+ },
786
+ get(candidate) {
787
+ return Promise.resolve(candidate.locator);
788
+ }
789
+ };
790
+ function runtimeCandidate(skill) {
791
+ return {
792
+ name: skill.name,
793
+ description: skill.description,
794
+ ...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
795
+ invocation: skill.invocation,
796
+ source: skill.source,
797
+ provider: skill.provider,
798
+ ...skill.resourceBase !== void 0 ? { resourceBase: skill.resourceBase } : {},
799
+ rank: RUNTIME_RANK,
800
+ locator: skill,
801
+ ...skill.path !== void 0 ? { path: skill.path } : {},
802
+ ...skill.metadata !== void 0 ? { metadata: skill.metadata } : {}
803
+ };
804
+ }
805
+ function validateCandidate(candidate, providerName) {
806
+ if (typeof candidate.name !== "string") throw new TypeError(`skill provider "${providerName}" returned a non-string skill name`);
807
+ if (!SKILL_NAME.test(candidate.name)) throw new Error(`skill provider "${providerName}" returned invalid skill name "${candidate.name}"`);
808
+ if (typeof candidate.description !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string description`);
809
+ if (candidate.description.length === 0) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" without a description`);
810
+ validateInvocation(candidate.invocation, `skill provider "${providerName}" returned skill "${candidate.name}"`);
811
+ if (candidate.whenToUse !== void 0 && typeof candidate.whenToUse !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string whenToUse`);
812
+ if (typeof candidate.source !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string source`);
813
+ if (typeof candidate.rank !== "number" || !Number.isFinite(candidate.rank)) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" with an invalid rank`);
814
+ if (typeof candidate.provider !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string provider`);
815
+ if (candidate.provider !== providerName) throw new Error(`skill provider "${providerName}" returned skill "${candidate.name}" for provider "${candidate.provider}"`);
816
+ if (candidate.path !== void 0 && typeof candidate.path !== "string") throw new TypeError(`skill provider "${providerName}" returned skill "${candidate.name}" with a non-string path`);
817
+ }
818
+ function validateRuntimeSkill(skill) {
819
+ if (!SKILL_NAME.test(skill.name)) throw new Error(`invalid skill name "${skill.name}"`);
820
+ if (skill.description.length === 0) throw new Error(`skill "${skill.name}" requires a description`);
821
+ validateInvocation(skill.invocation, `runtime skill "${skill.name}"`);
822
+ }
823
+ /** Validate a definition loaded from a provider-controlled parser or remote source. */
824
+ function validateDefinition(skill) {
825
+ const name = skill.name;
826
+ const description = skill.description;
827
+ const whenToUse = skill.whenToUse;
828
+ const invocation = skill.invocation;
829
+ const source = skill.source;
830
+ const provider = skill.provider;
831
+ const content = skill.content;
832
+ const path = skill.path;
833
+ if (typeof name !== "string") throw new TypeError("loaded skill name must be a string");
834
+ if (!SKILL_NAME.test(name)) throw new Error(`loaded skill has invalid name "${name}"`);
835
+ if (typeof description !== "string") throw new TypeError(`loaded skill "${name}" description must be a string`);
836
+ if (description.length === 0) throw new Error(`loaded skill "${name}" requires a description`);
837
+ validateInvocation(invocation, `loaded skill "${name}"`);
838
+ if (whenToUse !== void 0 && typeof whenToUse !== "string") throw new TypeError(`loaded skill "${name}" whenToUse must be a string`);
839
+ if (typeof source !== "string") throw new TypeError(`loaded skill "${name}" source must be a string`);
840
+ if (typeof provider !== "string") throw new TypeError(`loaded skill "${name}" provider must be a string`);
841
+ if (typeof content !== "string") throw new TypeError(`loaded skill "${name}" content must be a string`);
842
+ if (path !== void 0 && typeof path !== "string") throw new TypeError(`loaded skill "${name}" path must be a string`);
843
+ }
844
+ function toSummary(skill) {
845
+ const { name, description, whenToUse, invocation, source, provider, resourceBase } = skill;
846
+ return {
847
+ name,
848
+ description,
849
+ ...whenToUse !== void 0 ? { whenToUse } : {},
850
+ invocation,
851
+ source,
852
+ provider,
853
+ ...resourceBase !== void 0 ? { resourceBase } : {}
854
+ };
855
+ }
856
+ function validateInvocation(invocation, subject) {
857
+ if (invocation === void 0) return;
858
+ if (typeof invocation !== "object" || invocation === null || Array.isArray(invocation)) throw new TypeError(`${subject} with a non-object invocation policy`);
859
+ const policy = invocation;
860
+ if (typeof policy.modelInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.modelInvocable`);
861
+ if (typeof policy.userInvocable !== "boolean") throw new TypeError(`${subject} with a non-boolean invocation.userInvocable`);
862
+ }
863
+ function compareSkillSummary(left, right) {
864
+ return compareCodePoints(left.name, right.name);
865
+ }
866
+ function compareCodePoints(left, right) {
867
+ if (left < right) return -1;
868
+ if (left > right) return 1;
869
+ return 0;
870
+ }
871
+ function compareIndexedCandidates(left, right) {
872
+ return left.candidate.rank - right.candidate.rank || left.providerOrder - right.providerOrder || left.localOrder - right.localOrder;
873
+ }
874
+ function assertPositiveInteger(name, value, minimum = 1) {
875
+ if (!Number.isInteger(value) || value < minimum) throw new Error(`skill: ${name} must be an integer greater than or equal to ${minimum}`);
876
+ }
877
+ function waitWithAbort(promise, signal) {
878
+ if (signal === void 0) return promise;
879
+ throwIfAborted(signal);
880
+ return new Promise((resolve, reject) => {
881
+ const cleanup = () => {
882
+ signal.removeEventListener("abort", onAbort);
883
+ };
884
+ const onAbort = () => {
885
+ cleanup();
886
+ reject(toError(signal.reason));
887
+ };
888
+ signal.addEventListener("abort", onAbort, { once: true });
889
+ promise.then((value) => {
890
+ cleanup();
891
+ resolve(value);
892
+ }, (error) => {
893
+ cleanup();
894
+ reject(toError(error));
895
+ });
896
+ });
897
+ }
898
+ /** Throw a total Error for an already-aborted lookup. */
899
+ function throwIfAborted(signal) {
900
+ if (signal?.aborted === true) throw toError(signal.reason);
901
+ }
902
+ /** Normalize an arbitrary abort or provider failure without trusting coercion. */
903
+ function toError(error) {
904
+ try {
905
+ if (error instanceof Error) return error;
906
+ } catch {}
907
+ return new Error(errorMessage(error));
908
+ }
909
+ /** Render an arbitrary provider failure without letting coercion escape containment. */
910
+ function errorMessage(error) {
911
+ try {
912
+ return String(error);
913
+ } catch {
914
+ return "[unrenderable thrown value]";
915
+ }
916
+ }
917
+ //#endregion
918
+ //#region lib/types/content.js
919
+ /**
920
+ * Ponytail skill bodies, ported from github.com/DietrichGebert/ponytail and
921
+ * lightly adapted to the DeepSeek Harness surface (slash commands and the
922
+ * `skill` tool). The `ponytail` skill is a mode-aware pointer card: the actual
923
+ * ruleset is injected per session as the mode-filtered `PONYTAIL MODE ACTIVE`
924
+ * section (see `instructions.ts`) and must not be duplicated here. The other
925
+ * five skills ship verbatim as runtime skills.
926
+ *
927
+ * @module @mengyuly/dsh-ponytail
928
+ */
929
+ /** The always-on lazy-senior-dev ruleset: also registered as a loadable skill. */
930
+ const PONYTAIL_SKILL_BODY = `
931
+ You are the ponytail persona — the lazy senior developer. Your active ruleset
932
+ is ALREADY injected every turn as the "PONYTAIL MODE ACTIVE — level: <mode>"
933
+ system-prompt section, filtered to this session's intensity. Follow exactly
934
+ that section; do NOT reload, replace, or re-derive the ruleset from anywhere
935
+ else — the section is the single source of truth and it is mode-aware.
936
+
937
+ - Switch level: \`/ponytail lite|full|ultra|off\` (session-scoped)
938
+ - Query: \`/ponytail status\`
939
+ - Deactivate: "stop ponytail" / "normal mode"
940
+ - One-shot skills: \`/ponytail-review\`, \`/ponytail-audit\`, \`/ponytail-debt\`,
941
+ \`/ponytail-gain\`, \`/ponytail-help\`
942
+ - Reference: https://github.com/DietrichGebert/ponytail
943
+ `;
944
+ const PONYTAIL_DESCRIPTION = "Ponytail activation, modes, configuration, and help reference. The active ruleset is injected every turn by the system prompt; this skill is a pointer card. Use only when the user asks about Ponytail activation, modes, configuration, or help. Coding tasks already receive the active ruleset from the system prompt.";
945
+ const REVIEW_SKILL_BODY = `
946
+ Review the current diff for unnecessary complexity. Inspect affected callers
947
+ and tests first. Report only when code evidence shows the replacement preserves
948
+ required behavior.
949
+
950
+ ## Format
951
+
952
+ \`<file>:L<start>-<end>: <tag> <what>. Replacement: <simpler form>. Evidence: <observable reason>.\`
953
+
954
+ Tags:
955
+
956
+ - \`delete:\` dead code, unused flexibility, speculative feature. Replacement: nothing.
957
+ - \`stdlib:\` hand-rolled thing the standard library ships. Name the function.
958
+ - \`native:\` dependency or code doing what the platform already does. Name the feature.
959
+ - \`yagni:\` abstraction with one implementation, config nobody sets, layer with one caller.
960
+ - \`shrink:\` same logic, fewer lines. Show the shorter form.
961
+
962
+ Evidence must name an actual caller count, unused export, duplicate branch, or
963
+ equivalent stdlib/native behavior. A name that merely looks abstract is not
964
+ evidence. If preserving behavior is uncertain, omit the finding.
965
+
966
+ ## Scoring
967
+
968
+ End with \`net: -<N> lines countable.\` only when concrete ranges make the total
969
+ countable; otherwise say \`net: uncounted.\` Nothing to cut: \`Lean already. Ship.\`
970
+
971
+ ## Boundaries
972
+
973
+ Scope: over-engineering and complexity only. Correctness bugs, security holes,
974
+ and performance are explicitly out of scope. Route them to a normal review
975
+ pass, not this one. A single smoke test or \`assert\`-based
976
+ self-check is the ponytail minimum, not bloat, never flag it for deletion.
977
+ Does not apply the fixes, only lists them.
978
+ "stop ponytail-review" or "normal mode": revert to verbose review style.
979
+ `;
980
+ const REVIEW_DESCRIPTION = "Code review focused exclusively on over-engineering. Finds what to delete: reinvented standard library, unneeded dependencies, speculative abstractions, dead flexibility. One line per finding: location, what to cut, what replaces it. Use when the user says \"review for over-engineering\", \"what can we delete\", \"is this over-engineered\", \"simplify review\", or invokes /ponytail-review. Complements correctness-focused review, this one only hunts complexity.";
981
+ const AUDIT_SKILL_BODY = `
982
+ Audit repository complexity. Skip generated, vendored, dependency, and build
983
+ output. Inspect manifests, callers, and tests. Return the top 10 findings.
948
984
 
949
985
  ## Format
950
986
 
951
- \`L<line>: <tag> <what>. <replacement>.\`, or \`<file>:L<line>: ...\` for
952
- multi-file diffs.
953
-
954
- Tags:
955
-
956
- - \`delete:\` dead code, unused flexibility, speculative feature. Replacement: nothing.
957
- - \`stdlib:\` hand-rolled thing the standard library ships. Name the function.
958
- - \`native:\` dependency or code doing what the platform already does. Name the feature.
959
- - \`yagni:\` abstraction with one implementation, config nobody sets, layer with one caller.
960
- - \`shrink:\` same logic, fewer lines. Show the shorter form.
961
-
962
- ## Examples
963
-
964
- ❌ "This EmailValidator class might be more complex than necessary, have you
965
- considered whether all these validation rules are needed at this stage?"
966
-
967
- ✅ \`L12-38: stdlib: 27-line validator class. "@" in email, 1 line, real validation is the confirmation mail.\`
968
-
969
- ✅ \`L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps.\`
970
-
971
- ✅ \`repo.py:L88: yagni: AbstractRepository with one implementation. Inline it until a second one exists.\`
972
-
973
- ✅ \`L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it.\`
974
-
975
- ✅ \`L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line.\`
976
-
977
- ## Scoring
978
-
979
- End with the only metric that matters: \`net: -<N> lines possible.\`
980
-
981
- If there is nothing to cut, say \`Lean already. Ship.\` and stop.
982
-
983
- ## Boundaries
984
-
985
- Scope: over-engineering and complexity only. Correctness bugs, security holes,
986
- and performance are explicitly out of scope. Route them to a normal review
987
- pass, not this one. A single smoke test or \`assert\`-based
988
- self-check is the ponytail minimum, not bloat, never flag it for deletion.
989
- Does not apply the fixes, only lists them.
990
- "stop ponytail-review" or "normal mode": revert to verbose review style.
991
- `;
992
- const REVIEW_DESCRIPTION = "Code review focused exclusively on over-engineering. Finds what to delete: reinvented standard library, unneeded dependencies, speculative abstractions, dead flexibility. One line per finding: location, what to cut, what replaces it. Use when the user says \"review for over-engineering\", \"what can we delete\", \"is this over-engineered\", \"simplify review\", or invokes /ponytail-review. Complements correctness-focused review, this one only hunts complexity.";
993
- const AUDIT_SKILL_BODY = `
994
- ponytail-review, repo-wide. Scan the whole tree instead of a diff. Rank
995
- findings biggest cut first.
996
-
997
- ## Tags
998
-
999
- Same as ponytail-review:
1000
-
1001
- - \`delete:\` dead code, unused flexibility, speculative feature. Replacement: nothing.
1002
- - \`stdlib:\` hand-rolled thing the standard library ships. Name the function.
1003
- - \`native:\` dependency or code doing what the platform already does. Name the feature.
1004
- - \`yagni:\` abstraction with one implementation, config nobody sets, layer with one caller.
1005
- - \`shrink:\` same logic, fewer lines. Show the shorter form.
1006
-
1007
- ## Hunt
1008
-
1009
- Deps the stdlib or platform already ships, single-implementation interfaces,
1010
- factories with one product, wrappers that only delegate, files exporting one
1011
- thing, dead flags and config, hand-rolled stdlib.
1012
-
1013
- ## Output
1014
-
1015
- One line per finding, ranked: \`<tag> <what to cut>. <replacement>. [path]\`.
1016
- End with \`net: -<N> lines, -<M> deps possible.\` Nothing to cut: \`Lean already. Ship.\`
1017
-
1018
- ## Boundaries
1019
-
1020
- Scope: over-engineering and complexity only. Correctness bugs, security holes,
1021
- and performance are explicitly out of scope. Route them to a normal review
1022
- pass. Lists findings, applies nothing. One-shot.
1023
- "stop ponytail-audit" or "normal mode" to revert.
1024
- `;
1025
- const AUDIT_DESCRIPTION = "Whole-repo audit for over-engineering. Like ponytail-review, but scans the entire codebase instead of a diff: a ranked list of what to delete, simplify, or replace with stdlib/native equivalents. Use when the user says \"audit this codebase\", \"audit for over-engineering\", \"what can I delete from this repo\", \"find bloat\", \"ponytail-audit\", or /ponytail-audit. One-shot report, does not apply fixes.";
1026
- const DEBT_SKILL_BODY = `
1027
- Every deliberate ponytail shortcut is marked with a \`ponytail:\` comment naming
1028
- its ceiling and upgrade path. This collects them into one ledger so a deferral
1029
- can't quietly become permanent.
1030
-
1031
- ## Scan
1032
-
1033
- Grep the repo for comment markers, skipping \`node_modules\`, \`.git\`, and build
1034
- output:
1035
-
1036
- \`grep -rnE '(#|//) ?ponytail:' .\` (add other comment prefixes if your stack uses them)
987
+ \`<safe-delete|verify-first> <tag> <what>. Replace: <simpler form>. Evidence: <path:line + observed use>.\`
1037
988
 
1038
- Each hit is one ledger row. The comment prefix keeps prose that merely mentions
1039
- the convention out of the ledger.
1040
-
1041
- ## Output
1042
-
1043
- One row per marker, grouped by file:
1044
-
1045
- \`<file>:<line>, <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.\`
1046
-
1047
- The convention is \`ponytail: <ceiling>, <upgrade path>\`, so pull the ceiling
1048
- and the trigger straight from the comment. Want an owner per row too? add
1049
- \`git blame -L<line>,<line>\`.
1050
-
1051
- Flag the rot risk: any \`ponytail:\` comment that names no upgrade path or
1052
- trigger gets a \`no-trigger\` tag, those are the ones that silently rot.
1053
-
1054
- End with \`<N> markers, <M> with no trigger.\` Nothing found: \`No ponytail: debt. Clean ledger.\`
1055
-
1056
- ## Boundaries
1057
-
1058
- Reads and reports only, changes nothing. To persist it, ask and it writes the
1059
- ledger to a file (e.g. \`PONYTAIL-DEBT.md\`). One-shot. "stop ponytail-debt" or
1060
- "normal mode" to revert.
1061
- `;
1062
- const DEBT_DESCRIPTION = "Harvest every `ponytail:` comment in the codebase into a debt ledger, so the deliberate shortcuts and deferrals ponytail leaves behind get tracked instead of rotting into \"later means never\". Use when the user says \"ponytail debt\", \"/ponytail-debt\", \"what did ponytail defer\", \"list the shortcuts\", \"ponytail ledger\", or \"what did we mark to do later\". One-shot report, changes nothing.";
1063
- const GAIN_SKILL_BODY = `
1064
- Display this scoreboard when invoked. One-shot: do NOT change mode, write flag
1065
- files, or persist anything.
1066
-
1067
- These are upstream Ponytail results, not measured guarantees for this DSH
1068
- adapter.
1069
-
1070
- Savings depend on model, workload, prompt caching, tool usage, and execution
1071
- path. Already-minimal tasks may show little or no savings. Some reasoning
1072
- models may become more expensive because prompt and reasoning overhead can
1073
- exceed the saved output.
1074
-
1075
- ## 1. Upstream agentic reference
1076
-
1077
- Real Claude Code sessions on real repositories; 12 feature tasks:
1078
-
1079
- - Source LOC: ~\u221254%
1080
- - Tokens: ~\u221222%
1081
- - Cost: ~\u221220%
1082
- - Time: ~\u221227%
1083
- - Over-build tasks: \u221260\u201394%
1084
- - Safety tests: 100%
1085
-
1086
- ## 2. Upstream single-shot reference
1087
-
1088
- 5 everyday tasks (email validator, debounce, CSV sum, countdown timer, rate
1089
- limiter); 3 Claude models; single generation per task:
1090
-
1091
- - Lines of code: \u221280\u201394%
1092
- - Cost (Claude): \u221242\u201375%
1093
- - Latency: ~3.1\u20135.8\u00d7 faster
1094
-
1095
- ## 3. DSH adapter status
1096
-
1097
- Current DSH smoke tests provide directional evidence only. Stable token,
1098
- cost, and latency savings have not been established.
1099
-
1100
- See the repository's DSH smoke reports for limited, non-statistical
1101
- directional evidence (docs/dsh-smoke-summary.md).
1102
-
1103
- ## 4. Honesty boundary
1104
-
1105
- These are upstream benchmark medians, not this repo and not this DSH
1106
- adapter. NEVER print a per-repo savings number ("you saved X lines/tokens
1107
- here"): the unbuilt version was never written, so there is no real baseline
1108
- to subtract from in a live repo. The only real per-repo figures come from
1109
- \`/ponytail-debt\` (a counted ledger), and this card points there instead of
1110
- inventing one. Never claim "Ponytail always saves tokens/cost" or that this
1111
- adapter reproduces the upstream percentages. A missing cost figure (null) is
1112
- not a zero cost.
1113
-
1114
- ## Boundaries
1115
-
1116
- One-shot display. Edits nothing, changes no mode.
1117
- "stop ponytail" or "normal mode": revert.
1118
- `;
1119
- const GAIN_DESCRIPTION = "Less unnecessary work; token, cost, and latency effects depend on model and workload. Upstream benchmark reference, not a DSH-adapter guarantee. One-shot display, not a persistent mode, and not a per-repo number. Trigger: /ponytail-gain, \"ponytail gain\", \"what does ponytail save\", \"show ponytail impact\", \"ponytail scoreboard\".";
1120
- const HELP_SKILL_BODY = `
1121
- Display this reference card when invoked. One-shot, do NOT change mode,
1122
- write flag files, or persist anything.
1123
-
1124
- ## Levels
1125
-
1126
- | Level | Trigger | What change |
1127
- |-------|---------|-------------|
1128
- | **Lite** | \`/ponytail lite\` | Build what's asked, name the lazier alternative in one line. |
1129
- | **Full** | \`/ponytail\` | The ladder enforced: YAGNI → stdlib → native → one line → minimum. Default. |
1130
- | **Ultra** | \`/ponytail ultra\` | YAGNI extremist: deletion first, questions speculation — never cuts explicit requirements. |
1131
- | **Off** | \`/ponytail off\` | Ponytail stops injecting its ruleset for this session. |
1132
-
1133
- Level is session-scoped until changed.
1134
-
1135
- ## Choosing a level
1136
-
1137
- - **Lite**: Use for small, explicit changes or when the implementation is
1138
- already clear. Completes explicit requirements without actively
1139
- challenging them.
1140
- Lite:小改动、需求明确时使用。
1141
- - **Full**: Use for new features, refactors, root-cause bug fixes, or tasks
1142
- likely to invite unnecessary abstractions, dependencies, or custom
1143
- components.
1144
- Full:新功能、重构、根因修复、容易过度设计时使用。
1145
- - **Ultra**: Use for deliberate code cleanup and over-engineering removal.
1146
- It questions speculative scope, but never removes explicit requirements,
1147
- security, validation, accessibility, or data-loss protection.
1148
- Ultra:专门清理冗余和过度抽象时使用。
1149
- - **Off**: Use when the task is non-coding, already fully specified, or when
1150
- the fixed prompt overhead is not worthwhile.
1151
- Off:非编码任务或已经明确到无需额外编码判断的任务。
1152
-
1153
- Ponytail is not a guaranteed token-saving switch. It trades a small fixed
1154
- prompt cost for a chance to reduce unnecessary work. Do not default every
1155
- task to Ultra.
1156
-
1157
- ## Skills
1158
-
1159
- | Skill | Trigger | What it does |
1160
- |-------|---------|--------------|
1161
- | **ponytail** | \`/ponytail\` | Lazy mode itself. Simplest solution that works. |
1162
- | **ponytail-review** | \`/ponytail-review\` | Over-engineering review: \`L42: yagni: factory, one product. Inline.\` |
1163
- | **ponytail-audit** | \`/ponytail-audit\` | Whole-repo over-engineering audit: ranked list of what to delete. |
1164
- | **ponytail-debt** | \`/ponytail-debt\` | Harvest \`ponytail:\` shortcut comments into a tracked ledger. |
1165
- | **ponytail-gain** | \`/ponytail-gain\` | Upstream benchmark reference: less unnecessary work; token/cost/latency effects depend on model and workload. |
1166
- | **ponytail-help** | \`/ponytail-help\` | This card. |
1167
-
1168
- You can also load any of these with the \`skill\` tool.
989
+ - \`safe-delete\`: no required consumer or behavior is lost.
990
+ - \`verify-first\`: consumers may remain; name the check needed before deletion.
1169
991
 
992
+ Tags:
993
+
994
+ - \`delete:\` dead code or speculative flexibility.
995
+ - \`stdlib:\` hand-rolled standard-library behavior.
996
+ - \`native:\` dependency or code replaced by a platform feature.
997
+ - \`yagni:\` one-use abstraction, unset config, or one-caller layer.
998
+ - \`shrink:\` identical behavior in fewer lines.
999
+
1000
+ Count only concrete ranges and manifest entries. End with
1001
+ \`net: -<N> lines, -<M> deps countable.\` Use \`uncounted\` for either unknown.
1002
+ Nothing to cut: \`Lean already. Ship.\`
1003
+
1004
+ ## Boundaries
1005
+
1006
+ Scope: over-engineering and complexity only. Correctness bugs, security holes,
1007
+ and performance are explicitly out of scope. Route them to a normal review
1008
+ pass. Lists findings, applies nothing. One-shot.
1009
+ "stop ponytail-audit" or "normal mode" to revert.
1010
+ `;
1011
+ const AUDIT_DESCRIPTION = "Whole-repo audit for over-engineering. Like ponytail-review, but scans the entire codebase instead of a diff: a ranked list of what to delete, simplify, or replace with stdlib/native equivalents. Use when the user says \"audit this codebase\", \"audit for over-engineering\", \"what can I delete from this repo\", \"find bloat\", \"ponytail-audit\", or /ponytail-audit. One-shot report, does not apply fixes.";
1012
+ const DEBT_SKILL_BODY = `
1013
+ Collect \`ponytail:\` shortcut comments into a debt ledger. Each should name its
1014
+ ceiling and upgrade path.
1015
+
1016
+ ## Scan
1017
+
1018
+ Prefer ripgrep and exclude generated or dependency trees:
1019
+
1020
+ \`rg -n --hidden --glob '!node_modules/**' --glob '!.git/**' --glob '!lib/**' --glob '!dist/**' --glob '!build/**' '(#|//) ?ponytail:' .\`
1021
+
1022
+ If \`rg\` is unavailable, scan tracked files only:
1023
+ \`git grep -n -E '(#|//) ?ponytail:'\`
1024
+
1025
+ Each hit is one row; the comment prefix excludes prose mentions.
1026
+
1027
+ ## Output
1028
+
1029
+ One row per marker, grouped by file:
1030
+
1031
+ \`<file>:<line>, <what was simplified>. ceiling: <the limit named>. upgrade: <the trigger to revisit>.\`
1032
+
1033
+ The convention is \`ponytail: <ceiling>, <upgrade path>\`; copy both from the
1034
+ comment. Add an owner only when requested, using
1035
+ \`git blame -L<line>,<line> <file>\`.
1036
+
1037
+ Flag the rot risk: any \`ponytail:\` comment that names no upgrade path or
1038
+ trigger gets a \`no-trigger\` tag, those are the ones that silently rot.
1039
+
1040
+ End with \`<N> markers, <M> with no trigger.\` Nothing found: \`No ponytail: debt. Clean ledger.\`
1041
+
1042
+ ## Boundaries
1043
+
1044
+ Reads and reports only, changes nothing. To persist it, ask and it writes the
1045
+ ledger to a file (e.g. \`PONYTAIL-DEBT.md\`). One-shot. "stop ponytail-debt" or
1046
+ "normal mode" to revert.
1047
+ `;
1048
+ const DEBT_DESCRIPTION = "Harvest every `ponytail:` comment in the codebase into a debt ledger, so the deliberate shortcuts and deferrals ponytail leaves behind get tracked instead of rotting into \"later means never\". Use when the user says \"ponytail debt\", \"/ponytail-debt\", \"what did ponytail defer\", \"list the shortcuts\", \"ponytail ledger\", or \"what did we mark to do later\". One-shot report, changes nothing.";
1049
+ const GAIN_SKILL_BODY = `
1050
+ Display this scoreboard when invoked. One-shot: do NOT change mode, write flag
1051
+ files, or persist anything.
1052
+
1053
+ These are upstream Ponytail results, not measured guarantees for this DSH
1054
+ adapter.
1055
+
1056
+ Savings depend on model, workload, prompt caching, tool usage, and execution
1057
+ path. Already-minimal tasks may show little or no savings. Some reasoning
1058
+ models may become more expensive because prompt and reasoning overhead can
1059
+ exceed the saved output.
1060
+
1061
+ ## 1. Upstream agentic reference
1062
+
1063
+ Real Claude Code sessions on real repositories; 12 feature tasks:
1064
+
1065
+ - Source LOC: ~\u221254%
1066
+ - Tokens: ~\u221222%
1067
+ - Cost: ~\u221220%
1068
+ - Time: ~\u221227%
1069
+ - Over-build tasks: \u221260\u201394%
1070
+ - Safety tests: 100%
1071
+
1072
+ ## 2. Upstream single-shot reference
1073
+
1074
+ 5 everyday tasks (email validator, debounce, CSV sum, countdown timer, rate
1075
+ limiter); 3 Claude models; single generation per task:
1076
+
1077
+ - Lines of code: \u221280\u201394%
1078
+ - Cost (Claude): \u221242\u201375%
1079
+ - Latency: ~3.1\u20135.8\u00d7 faster
1080
+
1081
+ ## 3. DSH adapter status
1082
+
1083
+ Current DSH smoke tests provide directional evidence only. Stable token,
1084
+ cost, and latency savings have not been established.
1085
+
1086
+ See the repository's DSH smoke reports for limited, non-statistical
1087
+ directional evidence (docs/dsh-smoke-summary.md).
1088
+
1089
+ ## 4. Honesty boundary
1090
+
1091
+ These are upstream benchmark medians, not this repo and not this DSH
1092
+ adapter. NEVER print a per-repo savings number ("you saved X lines/tokens
1093
+ here"): the unbuilt version was never written, so there is no real baseline
1094
+ to subtract from in a live repo. The only real per-repo figures come from
1095
+ \`/ponytail-debt\` (a counted ledger), and this card points there instead of
1096
+ inventing one. Never claim "Ponytail always saves tokens/cost" or that this
1097
+ adapter reproduces the upstream percentages. A missing cost figure (null) is
1098
+ not a zero cost.
1099
+
1100
+ ## Boundaries
1101
+
1102
+ One-shot display. Edits nothing, changes no mode.
1103
+ "stop ponytail" or "normal mode": revert.
1104
+ `;
1105
+ const GAIN_DESCRIPTION = "Less unnecessary work; token, cost, and latency effects depend on model and workload. Upstream benchmark reference, not a DSH-adapter guarantee. One-shot display, not a persistent mode, and not a per-repo number. Trigger: /ponytail-gain, \"ponytail gain\", \"what does ponytail save\", \"show ponytail impact\", \"ponytail scoreboard\".";
1106
+ const HELP_SKILL_BODY = `
1107
+ Display this one-shot reference card. Do not change mode or persist anything.
1108
+
1109
+ ## Levels
1110
+
1111
+ | Level | Trigger | What change |
1112
+ |-------|---------|-------------|
1113
+ | **Lite** | \`/ponytail lite\` | Build what's asked, name the lazier alternative in one line. |
1114
+ | **Full** | \`/ponytail\` | The ladder enforced: YAGNI → stdlib → native → one line → minimum. Default. |
1115
+ | **Ultra** | \`/ponytail ultra\` | YAGNI extremist: deletion first, questions speculation — never cuts explicit requirements. |
1116
+ | **Off** | \`/ponytail off\` | Ponytail stops injecting its ruleset for this session. |
1117
+
1118
+ Level is session-scoped until changed.
1119
+
1120
+ ## Choosing a level
1121
+
1122
+ - **Lite**: Use for small, explicit changes or when the implementation is
1123
+ already clear. Completes explicit requirements without actively
1124
+ challenging them.
1125
+ Lite:小改动、需求明确时使用。
1126
+ - **Full**: Use for new features, refactors, root-cause bug fixes, or tasks
1127
+ likely to invite unnecessary abstractions, dependencies, or custom
1128
+ components.
1129
+ Full:新功能、重构、根因修复、容易过度设计时使用。
1130
+ - **Ultra**: Use for deliberate code cleanup and over-engineering removal.
1131
+ It questions speculative scope, but never removes explicit requirements,
1132
+ security, validation, accessibility, or data-loss protection.
1133
+ Ultra:专门清理冗余和过度抽象时使用。
1134
+ - **Off**: Use when the task is non-coding, already fully specified, or when
1135
+ the fixed prompt overhead is not worthwhile.
1136
+ Off:非编码任务或已经明确到无需额外编码判断的任务。
1137
+
1138
+ Ponytail is not a guaranteed token-saving switch. It trades a small fixed
1139
+ prompt cost for a chance to reduce unnecessary work. Do not default every
1140
+ task to Ultra.
1141
+
1142
+ ## Skills
1143
+
1144
+ | Skill | Trigger | What it does |
1145
+ |-------|---------|--------------|
1146
+ | **ponytail** | \`/ponytail\` | Lazy mode itself. Simplest solution that works. |
1147
+ | **ponytail-review** | \`/ponytail-review\` | Over-engineering review: \`L42: yagni: factory, one product. Inline.\` |
1148
+ | **ponytail-audit** | \`/ponytail-audit\` | Whole-repo over-engineering audit: ranked list of what to delete. |
1149
+ | **ponytail-debt** | \`/ponytail-debt\` | Harvest \`ponytail:\` shortcut comments into a tracked ledger. |
1150
+ | **ponytail-gain** | \`/ponytail-gain\` | Upstream benchmark reference: less unnecessary work; token/cost/latency effects depend on model and workload. |
1151
+ | **ponytail-help** | \`/ponytail-help\` | This card. |
1152
+
1170
1153
  ## Deactivate
1171
1154
 
1172
- Say "stop ponytail" or "normal mode". Resume anytime with \`/ponytail\` —
1173
- it re-enables at the effective default (or \`full\` when that is off too).
1174
- \`/ponytail status\` only shows the current level, never changes it.
1175
- \`/ponytail off\` also works. Level is session-scoped; a new session starts
1176
- from the configured default.
1177
-
1178
- ## Configure Default Mode
1179
-
1180
- Default mode = \`full\`, auto-active every session. Change it:
1181
-
1182
- **Environment variable** (highest priority):
1183
- \`\`\`bash
1184
- export PONYTAIL_DEFAULT_MODE=ultra
1185
- \`\`\`
1186
-
1187
- **Config file** (\`~/.config/ponytail/config.json\`, Windows: \`%APPDATA%\\ponytail\\config.json\`):
1188
- \`\`\`json
1189
- { "defaultMode": "lite" }
1190
- \`\`\`
1191
-
1192
- **Profile config** (per DSH profile, via the bundle row's \`config\` — e.g.
1193
- \`tui\` → lite):
1194
-
1195
- \`\`\`yaml
1196
- - insert:
1197
- - id: ponytail
1198
- name: '@mengyuly/dsh-ponytail'
1199
- config:
1200
- defaultMode: lite
1201
- \`\`\`
1202
-
1203
- Set \`"off"\` to disable auto-activation on session start, activate manually
1204
- with \`/ponytail\` when wanted. \`/ponytail default <mode>\` persists a new
1205
- default to the user config file; an exported \`PONYTAIL_DEFAULT_MODE\` or a
1206
- profile \`defaultMode\` still outranks the saved value for new sessions.
1207
-
1208
- Resolution: session override > env var > profile config > config file > \`full\`.
1209
-
1210
- ## More
1211
-
1212
- Full docs + examples: https://github.com/DietrichGebert/ponytail
1213
- `;
1214
- const HELP_DESCRIPTION = "Quick-reference card for all ponytail modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /ponytail-help, \"ponytail help\", \"what ponytail commands\", \"how do I use ponytail\".";
1215
- /** Ordered set of runtime skills surfaced to the model catalog and `/` menu. */
1216
- function ponytailSkills() {
1217
- return [
1218
- {
1219
- name: "ponytail",
1220
- source: "runtime",
1221
- description: PONYTAIL_DESCRIPTION,
1222
- whenToUse: "Use only when the user asks about Ponytail activation, modes, configuration, or help. Coding tasks already receive the active ruleset from the system prompt.",
1223
- content: PONYTAIL_SKILL_BODY,
1224
- invocation: {
1225
- modelInvocable: false,
1226
- userInvocable: true
1227
- }
1228
- },
1229
- {
1230
- name: "ponytail-review",
1231
- source: "runtime",
1232
- description: REVIEW_DESCRIPTION,
1233
- content: REVIEW_SKILL_BODY,
1234
- invocation: {
1235
- modelInvocable: true,
1236
- userInvocable: true
1237
- }
1238
- },
1239
- {
1240
- name: "ponytail-audit",
1241
- source: "runtime",
1242
- description: AUDIT_DESCRIPTION,
1243
- content: AUDIT_SKILL_BODY,
1244
- invocation: {
1245
- modelInvocable: true,
1246
- userInvocable: true
1247
- }
1248
- },
1249
- {
1250
- name: "ponytail-debt",
1251
- source: "runtime",
1252
- description: DEBT_DESCRIPTION,
1253
- content: DEBT_SKILL_BODY,
1254
- invocation: {
1255
- modelInvocable: true,
1256
- userInvocable: true
1257
- }
1258
- },
1259
- {
1260
- name: "ponytail-gain",
1261
- source: "runtime",
1262
- description: GAIN_DESCRIPTION,
1263
- content: GAIN_SKILL_BODY,
1264
- invocation: {
1265
- modelInvocable: true,
1266
- userInvocable: true
1267
- }
1268
- },
1269
- {
1270
- name: "ponytail-help",
1271
- source: "runtime",
1272
- description: HELP_DESCRIPTION,
1273
- content: HELP_SKILL_BODY,
1274
- invocation: {
1275
- modelInvocable: true,
1276
- userInvocable: true
1277
- }
1278
- }
1279
- ];
1280
- }
1281
- //#endregion
1282
- //#region lib/types/modes.js
1283
- /**
1284
- * Ponytail mode resolution: the effective default comes from, in order, the
1285
- * `PONYTAIL_DEFAULT_MODE` environment variable, the Cordis profile
1286
- * `defaultMode`, the optional user config file
1287
- * `~/.config/ponytail/config.json` (`defaultMode`), then `full`. Setting a
1288
- * level via the `/ponytail` command is session-scoped and lives in an
1289
- * in-memory, per-agent {@link ModeStore}.
1290
- *
1291
- * @module @deepseek-ai/dsh-ponytail
1292
- */
1293
- const DEFAULT_MODE = "full";
1294
- const RUNTIME_MODES = [
1295
- "off",
1296
- "lite",
1297
- "full",
1298
- "ultra"
1299
- ];
1300
- /** Strip a UTF-8 BOM that Windows editors prepend before JSON.parse. */
1301
- function stripBom(text) {
1302
- return text.replace(/^\uFEFF/, "");
1303
- }
1304
- /**
1305
- * Normalize free-form input to a runtime intensity. `null` for anything that
1306
- * is not exactly `off`, `lite`, `full`, or `ultra`.
1307
- */
1308
- function normalizeRuntimeMode(mode) {
1309
- if (typeof mode !== "string") return null;
1310
- const normalized = mode.trim().toLowerCase();
1311
- return RUNTIME_MODES.includes(normalized) ? normalized : null;
1312
- }
1313
- /**
1314
- * Deactivation commands only match when the whole message is the command,
1315
- * ignoring case and trailing punctuation. Matching the phrase anywhere would
1316
- * turn ponytail off mid-task for ordinary requests like "add a normal mode
1317
- * toggle".
1318
- */
1319
- function isDeactivationCommand(text) {
1320
- const normalized = (typeof text === "string" ? text : "").trim().toLowerCase().replace(/[\s.!?。?!]+$/, "");
1321
- return normalized === "stop ponytail" || normalized === "normal mode";
1322
- }
1323
- /** Config directory: `$XDG_CONFIG_HOME/ponytail`, `%APPDATA%\ponytail`, else `~/.config/ponytail`. */
1324
- function configDir(env = process.env) {
1325
- if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "ponytail");
1326
- if (process.platform === "win32") return join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "ponytail");
1327
- return join(homedir(), ".config", "ponytail");
1328
- }
1329
- /** Absolute path of the optional `config.json`. */
1330
- function configPath(env = process.env) {
1331
- return join(configDir(env), "config.json");
1332
- }
1333
- /**
1334
- * Read the configured default with diagnostics. Priority:
1335
- * `PONYTAIL_DEFAULT_MODE` → Cordis profile `defaultMode` → user config file →
1336
- * `full`. A missing config file is normal and yields no issue; a broken one
1337
- * yields the fallback mode plus one issue for the caller to warn about once.
1338
- * @param env - the process environment to read.
1339
- * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
1340
- * when the profile config is absent or invalid (invalid values are reported
1341
- * by the caller; this function only consumes valid ones).
1342
- */
1343
- function readDefaultModeInfo(env = process.env, profileMode = null) {
1344
- const path = configPath(env);
1345
- const envMode = normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE);
1346
- let configText;
1347
- try {
1348
- configText = readFileSync(path, "utf8");
1349
- } catch (error) {
1350
- if (error.code === "ENOENT") return { mode: envMode ?? profileMode ?? "full" };
1351
- return {
1352
- mode: envMode ?? profileMode ?? "full",
1353
- issue: {
1354
- kind: "read",
1355
- detail: `${path}: ${error.message}`
1356
- }
1357
- };
1358
- }
1359
- let configIssue;
1360
- let fromConfig = null;
1361
- try {
1362
- const parsed = JSON.parse(stripBom(configText));
1363
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) configIssue = {
1364
- kind: "shape",
1365
- detail: `${path}: root must be a JSON object`
1366
- };
1367
- else if ("defaultMode" in parsed) {
1368
- fromConfig = normalizeRuntimeMode(parsed.defaultMode);
1369
- if (!fromConfig) configIssue = {
1370
- kind: "value",
1371
- detail: `${path}: defaultMode is not lite|full|ultra|off`
1372
- };
1373
- }
1374
- } catch (error) {
1375
- configIssue = {
1376
- kind: "json",
1377
- detail: `${path}: ${error.message}`
1378
- };
1379
- }
1380
- if (envMode) return {
1381
- mode: envMode,
1382
- ...configIssue ? { issue: configIssue } : {}
1383
- };
1384
- if (profileMode) return {
1385
- mode: profileMode,
1386
- ...configIssue ? { issue: configIssue } : {}
1387
- };
1388
- if (configIssue) return {
1389
- mode: DEFAULT_MODE,
1390
- issue: configIssue
1391
- };
1392
- return { mode: fromConfig ?? "full" };
1393
- }
1394
- /**
1395
- * Read the configured default for this host: environment variable first, then
1396
- * the Cordis profile `defaultMode`, then the user config file, then `full`.
1397
- */
1398
- function readDefaultMode(env = process.env, profileMode = null) {
1399
- return readDefaultModeInfo(env, profileMode).mode;
1400
- }
1401
- /**
1402
- * Why a `saved` default is not the effective one — for the `/ponytail default`
1403
- * result message. `null` means the saved value is effective.
1404
- */
1405
- function defaultOverrideReason(env, profileMode) {
1406
- if (normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE)) return "PONYTAIL_DEFAULT_MODE";
1407
- if (profileMode) return "profile configuration";
1408
- return null;
1409
- }
1410
- /**
1411
- * Persist a new default level to the config file, preserving other fields.
1412
- * Returns the normalized mode, or `null` when the value is not a runtime mode.
1413
- * Throws when the write itself fails, so callers never report success for a
1414
- * file that was not written.
1415
- */
1416
- function writeDefaultMode(mode, env = process.env) {
1417
- const normalized = normalizeRuntimeMode(mode);
1418
- if (!normalized) return null;
1419
- const path = configPath(env);
1420
- let config = {};
1421
- try {
1422
- const parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
1423
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) config = parsed;
1424
- } catch {}
1425
- config.defaultMode = normalized;
1426
- const text = `${JSON.stringify(config, null, 2)}\n`;
1427
- mkdirSync(dirname(path), { recursive: true });
1428
- const temp = join(dirname(path), `.config-${process.pid}-${Date.now()}.tmp`);
1429
- try {
1430
- writeFileSync(temp, text, "utf8");
1431
- renameSync(temp, path);
1432
- } catch (error) {
1433
- try {
1434
- unlinkSync(temp);
1435
- } catch {}
1436
- throw new Error(`failed to write ${path}: ${error.message}`);
1437
- }
1438
- return normalized;
1439
- }
1440
- /**
1441
- * Session-scoped live mode. The absence of an entry means "use the configured
1442
- * default", which matches the upstream behavior where each session starts from
1443
- * the default until the user switches it.
1444
- */
1445
- var ModeStore = class {
1446
- modes = /* @__PURE__ */ new Map();
1447
- /** The mode in force for one agent, or the configured default. */
1155
+ Say "stop ponytail", "normal mode", "停止 ponytail", or "正常模式".
1156
+ \`/ponytail\` re-enables at the effective default (or \`full\` if it is \`off\`).
1157
+ \`/ponytail reset\` clears the session override and follows the configured default.
1158
+ \`/ponytail status\` reports mode and source; \`/ponytail off\` disables it.
1159
+
1160
+ ## Configure Default Mode
1161
+
1162
+ The built-in fallback is \`full\`; check the effective mode with
1163
+ \`/ponytail status\`. Change the default:
1164
+
1165
+ **Environment variable** (highest priority):
1166
+ \`\`\`bash
1167
+ export PONYTAIL_DEFAULT_MODE=ultra
1168
+ \`\`\`
1169
+
1170
+ **Config file** (\`~/.config/ponytail/config.json\`, Windows: \`%APPDATA%\\ponytail\\config.json\`):
1171
+ \`\`\`json
1172
+ { "defaultMode": "lite" }
1173
+ \`\`\`
1174
+
1175
+ **Profile config** (per DSH profile, via the bundle row's \`config\` — e.g.
1176
+ \`tui\` → lite):
1177
+
1178
+ \`\`\`yaml
1179
+ - insert:
1180
+ - id: ponytail
1181
+ name: '@mengyuly/dsh-ponytail'
1182
+ config:
1183
+ defaultMode: lite
1184
+ \`\`\`
1185
+
1186
+ Set \`"off"\` to disable auto-activation on session start, activate manually
1187
+ with \`/ponytail\` when wanted. \`/ponytail default <mode>\` persists a new
1188
+ default to the user config file; an exported \`PONYTAIL_DEFAULT_MODE\` or a
1189
+ profile \`defaultMode\` still outranks the saved value for new sessions.
1190
+
1191
+ Resolution: session override > env var > profile config > config file > \`full\`.
1192
+
1193
+ ## More
1194
+
1195
+ Full docs + examples: https://github.com/DietrichGebert/ponytail
1196
+ `;
1197
+ const HELP_DESCRIPTION = "Quick-reference card for all ponytail modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /ponytail-help, \"ponytail help\", \"what ponytail commands\", \"how do I use ponytail\".";
1198
+ /** Ordered set of runtime skills surfaced to the model catalog and `/` menu. */
1199
+ function ponytailSkills() {
1200
+ return [
1201
+ {
1202
+ name: "ponytail",
1203
+ source: "runtime",
1204
+ description: PONYTAIL_DESCRIPTION,
1205
+ whenToUse: "Use only when the user asks about Ponytail activation, modes, configuration, or help. Coding tasks already receive the active ruleset from the system prompt.",
1206
+ content: PONYTAIL_SKILL_BODY,
1207
+ invocation: {
1208
+ modelInvocable: false,
1209
+ userInvocable: true
1210
+ }
1211
+ },
1212
+ {
1213
+ name: "ponytail-review",
1214
+ source: "runtime",
1215
+ description: REVIEW_DESCRIPTION,
1216
+ content: REVIEW_SKILL_BODY,
1217
+ invocation: {
1218
+ modelInvocable: true,
1219
+ userInvocable: true
1220
+ }
1221
+ },
1222
+ {
1223
+ name: "ponytail-audit",
1224
+ source: "runtime",
1225
+ description: AUDIT_DESCRIPTION,
1226
+ content: AUDIT_SKILL_BODY,
1227
+ invocation: {
1228
+ modelInvocable: true,
1229
+ userInvocable: true
1230
+ }
1231
+ },
1232
+ {
1233
+ name: "ponytail-debt",
1234
+ source: "runtime",
1235
+ description: DEBT_DESCRIPTION,
1236
+ content: DEBT_SKILL_BODY,
1237
+ invocation: {
1238
+ modelInvocable: true,
1239
+ userInvocable: true
1240
+ }
1241
+ },
1242
+ {
1243
+ name: "ponytail-gain",
1244
+ source: "runtime",
1245
+ description: GAIN_DESCRIPTION,
1246
+ content: GAIN_SKILL_BODY,
1247
+ invocation: {
1248
+ modelInvocable: true,
1249
+ userInvocable: true
1250
+ }
1251
+ },
1252
+ {
1253
+ name: "ponytail-help",
1254
+ source: "runtime",
1255
+ description: HELP_DESCRIPTION,
1256
+ content: HELP_SKILL_BODY,
1257
+ invocation: {
1258
+ modelInvocable: true,
1259
+ userInvocable: true
1260
+ }
1261
+ }
1262
+ ];
1263
+ }
1264
+ //#endregion
1265
+ //#region lib/types/modes.js
1266
+ /**
1267
+ * Ponytail mode resolution: the effective default comes from, in order, the
1268
+ * `PONYTAIL_DEFAULT_MODE` environment variable, the Cordis profile
1269
+ * `defaultMode`, the optional user config file
1270
+ * `~/.config/ponytail/config.json` (`defaultMode`), then `full`. Setting a
1271
+ * level via the `/ponytail` command is session-scoped and lives in an
1272
+ * in-memory, per-agent {@link ModeStore}.
1273
+ *
1274
+ * @module @mengyuly/dsh-ponytail
1275
+ */
1276
+ const DEFAULT_MODE = "full";
1277
+ const RUNTIME_MODES = [
1278
+ "off",
1279
+ "lite",
1280
+ "full",
1281
+ "ultra"
1282
+ ];
1283
+ /** Strip a UTF-8 BOM that Windows editors prepend before JSON.parse. */
1284
+ function stripBom(text) {
1285
+ return text.replace(/^\uFEFF/, "");
1286
+ }
1287
+ /**
1288
+ * Normalize free-form input to a runtime intensity. `null` for anything that
1289
+ * is not exactly `off`, `lite`, `full`, or `ultra`.
1290
+ */
1291
+ function normalizeRuntimeMode(mode) {
1292
+ if (typeof mode !== "string") return null;
1293
+ const normalized = mode.trim().toLowerCase();
1294
+ return RUNTIME_MODES.includes(normalized) ? normalized : null;
1295
+ }
1296
+ /**
1297
+ * English and Chinese deactivation commands match only when the whole message is the command,
1298
+ * ignoring case and trailing punctuation. Matching the phrase anywhere would
1299
+ * turn ponytail off mid-task for ordinary requests like "add a normal mode
1300
+ * toggle".
1301
+ */
1302
+ function isDeactivationCommand(text) {
1303
+ const normalized = (typeof text === "string" ? text : "").trim().toLowerCase().replace(/[\s.!?。?!]+$/, "");
1304
+ return normalized === "stop ponytail" || normalized === "normal mode" || normalized === "停止 ponytail" || normalized === "关闭 ponytail" || normalized === "普通模式" || normalized === "正常模式";
1305
+ }
1306
+ /** Config directory: `$XDG_CONFIG_HOME/ponytail`, `%APPDATA%\ponytail`, else `~/.config/ponytail`. */
1307
+ function configDir(env = process.env) {
1308
+ if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "ponytail");
1309
+ if (process.platform === "win32") return join(env.APPDATA || join(homedir(), "AppData", "Roaming"), "ponytail");
1310
+ return join(homedir(), ".config", "ponytail");
1311
+ }
1312
+ /** Absolute path of the optional `config.json`. */
1313
+ function configPath(env = process.env) {
1314
+ return join(configDir(env), "config.json");
1315
+ }
1316
+ /**
1317
+ * Read the configured default with diagnostics. Priority:
1318
+ * `PONYTAIL_DEFAULT_MODE` → Cordis profile `defaultMode` → user config file →
1319
+ * `full`. A missing config file is normal and yields no issue; a broken one
1320
+ * yields the fallback mode plus one issue for the caller to warn about once.
1321
+ * @param env - the process environment to read.
1322
+ * @param profileMode - the validated Cordis profile `defaultMode`, or `null`
1323
+ * when the profile config is absent or invalid (invalid values are reported
1324
+ * by the caller; this function only consumes valid ones).
1325
+ */
1326
+ function readDefaultModeInfo(env = process.env, profileMode = null) {
1327
+ const path = configPath(env);
1328
+ const envMode = normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE);
1329
+ let configText;
1330
+ try {
1331
+ configText = readFileSync(path, "utf8");
1332
+ } catch (error) {
1333
+ if (error.code === "ENOENT") return { mode: envMode ?? profileMode ?? "full" };
1334
+ return {
1335
+ mode: envMode ?? profileMode ?? "full",
1336
+ issue: {
1337
+ kind: "read",
1338
+ detail: `${path}: ${error.message}`
1339
+ }
1340
+ };
1341
+ }
1342
+ let configIssue;
1343
+ let fromConfig = null;
1344
+ try {
1345
+ const parsed = JSON.parse(stripBom(configText));
1346
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) configIssue = {
1347
+ kind: "shape",
1348
+ detail: `${path}: root must be a JSON object`
1349
+ };
1350
+ else if ("defaultMode" in parsed) {
1351
+ fromConfig = normalizeRuntimeMode(parsed.defaultMode);
1352
+ if (!fromConfig) configIssue = {
1353
+ kind: "value",
1354
+ detail: `${path}: defaultMode is not lite|full|ultra|off`
1355
+ };
1356
+ }
1357
+ } catch (error) {
1358
+ configIssue = {
1359
+ kind: "json",
1360
+ detail: `${path}: ${error.message}`
1361
+ };
1362
+ }
1363
+ if (envMode) return {
1364
+ mode: envMode,
1365
+ ...configIssue ? { issue: configIssue } : {}
1366
+ };
1367
+ if (profileMode) return {
1368
+ mode: profileMode,
1369
+ ...configIssue ? { issue: configIssue } : {}
1370
+ };
1371
+ if (configIssue) return {
1372
+ mode: DEFAULT_MODE,
1373
+ issue: configIssue
1374
+ };
1375
+ return { mode: fromConfig ?? "full" };
1376
+ }
1377
+ /**
1378
+ * Read the configured default for this host: environment variable first, then
1379
+ * the Cordis profile `defaultMode`, then the user config file, then `full`.
1380
+ */
1381
+ function readDefaultMode(env = process.env, profileMode = null) {
1382
+ return readDefaultModeInfo(env, profileMode).mode;
1383
+ }
1384
+ /**
1385
+ * Why a `saved` default is not the effective one — for the `/ponytail default`
1386
+ * result message. `null` means the saved value is effective.
1387
+ */
1388
+ function defaultOverrideReason(env, profileMode) {
1389
+ if (normalizeRuntimeMode(env.PONYTAIL_DEFAULT_MODE)) return "PONYTAIL_DEFAULT_MODE";
1390
+ if (profileMode) return "profile configuration";
1391
+ return null;
1392
+ }
1393
+ /**
1394
+ * Persist a new default level to the config file, preserving other fields.
1395
+ * Returns the normalized mode, or `null` when the value is not a runtime mode.
1396
+ * Throws when the write itself fails, so callers never report success for a
1397
+ * file that was not written.
1398
+ */
1399
+ function writeDefaultMode(mode, env = process.env) {
1400
+ const normalized = normalizeRuntimeMode(mode);
1401
+ if (!normalized) return null;
1402
+ const path = configPath(env);
1403
+ let config = {};
1404
+ try {
1405
+ const parsed = JSON.parse(stripBom(readFileSync(path, "utf8")));
1406
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) config = parsed;
1407
+ } catch {}
1408
+ config.defaultMode = normalized;
1409
+ const text = `${JSON.stringify(config, null, 2)}\n`;
1410
+ mkdirSync(dirname(path), { recursive: true });
1411
+ const temp = join(dirname(path), `.config-${process.pid}-${Date.now()}.tmp`);
1412
+ try {
1413
+ writeFileSync(temp, text, "utf8");
1414
+ renameSync(temp, path);
1415
+ } catch (error) {
1416
+ try {
1417
+ unlinkSync(temp);
1418
+ } catch {}
1419
+ throw new Error(`failed to write ${path}: ${error.message}`);
1420
+ }
1421
+ return normalized;
1422
+ }
1423
+ /**
1424
+ * Session-scoped live mode. The absence of an entry means "use the configured
1425
+ * default", which matches the upstream behavior where each session starts from
1426
+ * the default until the user switches it.
1427
+ */
1428
+ var ModeStore = class {
1429
+ modes = /* @__PURE__ */ new Map();
1430
+ /** The mode in force for one agent, or the configured default. */
1448
1431
  modeFor(agentId, fallback) {
1449
1432
  return this.modes.get(agentId) ?? fallback;
1450
1433
  }
1451
- /** Set the mode for one agent's session (session-scoped, survives until changed or disposal). */
1452
- set(agentId, mode) {
1453
- this.modes.set(agentId, mode);
1454
- }
1455
- /** Forget a session-scoped override so the next lookup returns the default. */
1456
- clear(agentId) {
1457
- this.modes.delete(agentId);
1458
- }
1459
- };
1460
- /**
1461
- * Compile `PONYTAIL_SUBAGENT_MATCHER` into a case-insensitive regex. An unset
1462
- * matcher yields `null`; an invalid pattern stays fail-open (every agent gets
1463
- * the ruleset) but is reported so the caller can warn exactly once.
1464
- */
1465
- function compileSubagentMatcher(raw) {
1466
- if (!raw) return {
1467
- matcher: null,
1468
- invalid: false
1469
- };
1470
- try {
1471
- return {
1472
- matcher: new RegExp(raw, "i"),
1473
- invalid: false
1474
- };
1475
- } catch {
1476
- return {
1477
- matcher: null,
1478
- invalid: true
1479
- };
1434
+ /** Whether this session currently overrides the configured default. */
1435
+ has(agentId) {
1436
+ return this.modes.has(agentId);
1480
1437
  }
1481
- }
1482
- /**
1483
- * The stable per-session identity backing every mode override. DSH's `Agent`
1484
- * type documents `id` as "the single identity shared with session", so the
1485
- * agent id IS the SessionId: one entry per live session, never shared between
1486
- * two sessions, and stable across the session's lifetime. Centralized so the
1487
- * key choice lives in exactly one place.
1488
- */
1489
- function sessionKey(agent) {
1490
- return agent.id;
1491
- }
1492
- /** Whether a session is a subagent child (origin, or any delegation depth with no origin). */
1493
- function isSubagentSession(header) {
1494
- return header.origin === "subagent" || (header.delegationDepth ?? 0) > 0;
1495
- }
1496
- //#endregion
1497
- //#region lib/types/instructions.js
1498
- /**
1499
- * Structured ponytail ruleset composition. Each intensity is built from
1500
- * explicit fragments — common rules, a never-cut safety boundary list, and
1501
- * the mode's own rules — instead of filtering one Markdown body with regexes.
1502
- * The three intensities therefore differ in their actual instructions, not
1503
- * just in a table row.
1504
- *
1505
- * @module @deepseek-ai/dsh-ponytail
1506
- */
1507
- /** Shared identity line, carried by every non-`off` mode. */
1508
- const INTRO = "You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.";
1509
- /**
1510
- * Understanding-and-reuse baseline, identical in every non-`off` mode.
1511
- */
1438
+ /** Set the mode for one agent's session (session-scoped, survives until changed or disposal). */
1439
+ set(agentId, mode) {
1440
+ this.modes.set(agentId, mode);
1441
+ }
1442
+ /** Forget a session-scoped override so the next lookup returns the default. */
1443
+ clear(agentId) {
1444
+ this.modes.delete(agentId);
1445
+ }
1446
+ };
1447
+ /**
1448
+ * Compile `PONYTAIL_SUBAGENT_MATCHER` into a case-insensitive regex. An unset
1449
+ * matcher yields `null`; an invalid pattern stays fail-open (every agent gets
1450
+ * the ruleset) but is reported so the caller can warn exactly once.
1451
+ */
1452
+ function compileSubagentMatcher(raw) {
1453
+ if (!raw) return {
1454
+ matcher: null,
1455
+ invalid: false
1456
+ };
1457
+ try {
1458
+ return {
1459
+ matcher: new RegExp(raw, "i"),
1460
+ invalid: false
1461
+ };
1462
+ } catch {
1463
+ return {
1464
+ matcher: null,
1465
+ invalid: true
1466
+ };
1467
+ }
1468
+ }
1469
+ /**
1470
+ * The stable per-session identity backing every mode override. DSH's `Agent`
1471
+ * type documents `id` as "the single identity shared with session", so the
1472
+ * agent id IS the SessionId: one entry per live session, never shared between
1473
+ * two sessions, and stable across the session's lifetime. Centralized so the
1474
+ * key choice lives in exactly one place.
1475
+ */
1476
+ function sessionKey(agent) {
1477
+ return agent.id;
1478
+ }
1479
+ /** Whether a session is a subagent child (origin, or any delegation depth with no origin). */
1480
+ function isSubagentSession(header) {
1481
+ return header.origin === "subagent" || (header.delegationDepth ?? 0) > 0;
1482
+ }
1483
+ //#endregion
1484
+ //#region lib/types/instructions.js
1485
+ /**
1486
+ * Structured ponytail ruleset composition. Each intensity is built from
1487
+ * explicit fragments — common rules, a never-cut safety boundary list, and
1488
+ * the mode's own rules — instead of filtering one Markdown body with regexes.
1489
+ * The three intensities therefore differ in their actual instructions, not
1490
+ * just in a table row.
1491
+ *
1492
+ * @module @mengyuly/dsh-ponytail
1493
+ */
1494
+ /** Shared identity line, carried by every non-`off` mode. */
1495
+ const INTRO = "You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.";
1496
+ /**
1497
+ * Understanding-and-reuse baseline, identical in every non-`off` mode.
1498
+ */
1512
1499
  const COMMON_RULES = [
1513
- "Understand the problem before choosing a solution: read the code the change touches and trace the real flow end to end. Laziness that skips comprehension ships a confident wrong fix.",
1514
- "Reuse what already exists in this codebase before writing anything new.",
1515
- "Reach for the standard library, platform-native features, and already-installed dependencies before custom code.",
1516
- "A non-trivial change leaves ONE minimal runnable check behind (an assert-based self-check or one small test file; no frameworks). Trivial one-liners need no test.",
1517
- "Explain briefly, but never omit the key decisions."
1518
- ].join("\n");
1519
- /**
1520
- * The never-cut list. Every non-`off` mode keeps these; intensities tune how
1521
- * aggressively code is minimized, never what may be dropped.
1522
- */
1523
- const SAFETY_BOUNDARIES = [
1524
- "Never cut, in any mode:",
1525
- "- Input validation at trust boundaries.",
1526
- "- Error handling that prevents data loss.",
1527
- "- Security measures.",
1528
- "- Accessibility basics.",
1529
- "- Explicit acceptance criteria the user asked for.",
1530
- "- Understanding the problem and tracing the real flow first.",
1531
- "- The real end-to-end data flow: no UI-only field, unused state, placeholder path, or disconnected payload.",
1532
- "- Necessary tests for non-trivial changes.",
1533
- "- Root-cause fixes over symptom patches.",
1534
- "- \"Minimal diff\" is not a substitute for \"correct fix\"."
1535
- ].join("\n");
1536
- /** Lite: complete the explicit ask; reuse; suggest, do not challenge. */
1537
- const LITE_RULES = [
1538
- "Complete everything explicitly requested, including every acceptance criterion.",
1539
- "Prefer existing code, standard-library features, native platform features, and already-installed dependencies.",
1540
- "You may mention a simpler alternative briefly, but do not challenge or reject an explicit requirement.",
1541
- "Do not change the existing architecture merely to reduce line count.",
1542
- "Keep the smallest reasonable validation for non-trivial changes."
1500
+ "Before editing, define a concrete, observable done condition; preserve explicit acceptance criteria.",
1501
+ "Read touched code and trace the relevant flow. The ladder is a reflex, not a research project: investigate only what can change the solution, then stop.",
1502
+ "Resolve uncertainty with evidence from code, tools, or authoritative docs; never invent facts or checks.",
1503
+ "Reuse existing code, native features, or installed dependencies before custom code.",
1504
+ "Choose the smallest complete change compatible with existing contracts, not the smallest local diff.",
1505
+ "Loop: inspect, change, run the narrowest relevant check, inspect the final diff. On failure, fix the cause. Do not weaken a test to pass.",
1506
+ "Report only verified results, checks run, and uncertainty; be brief."
1543
1507
  ].join("\n");
1544
- /** Smallest complete end-to-end change: shared by Full and Ultra. */
1545
- const E2E_RULES = [
1546
- "Smallest complete end-to-end change:",
1547
- "- Prefer the smallest complete end-to-end change compatible with the existing architecture, not merely the fewest lines in one file.",
1548
- "- Before creating a component, abstraction, protocol, migration, transport format, storage format, or dependency, inspect the repository’s existing path and preserve its current contract.",
1549
- "- Do not redesign transport, storage, API shape, or persistence when the task only asks for a local UI or behavior change.",
1550
- "- A locally smaller implementation that changes the system contract is not smaller overall.",
1551
- "- Prefer the smallest complete change across the real data flow: input → state → validation → payload → API → persistence → response/UI.",
1552
- "- It does not mean every layer must change: the change must be complete across the layers it touches.",
1553
- "- Do not leave a UI-only field, unused state, placeholder path, or disconnected payload merely because it produces a smaller diff."
1554
- ].join("\n");
1555
- const MODE_RULES = {
1556
- lite: LITE_RULES,
1557
- full: [
1558
- "Use the complete ladder stop at the first rung that holds:",
1559
- "1. Does this need to exist at all? (YAGNI)",
1560
- "2. Does it already exist in this codebase? Reuse it.",
1561
- "3. Does the standard library do it? Use it.",
1562
- "4. Does a native platform feature cover it? Use it.",
1563
- "5. Does an already-installed dependency solve it? Use it.",
1564
- "6. Can the solution be reduced to a small expression? Make it that small.",
1565
- "7. Only then: write the minimum new implementation.",
1566
- "Default to the shortest correct implementation; prefer deletion and reuse, but do not trade away correctness, security, tests, explicit requirements, or the existing system contract.",
1567
- "Fix root causes, not symptoms: one guard in the shared function beats a guard in every caller.",
1568
- "",
1569
- E2E_RULES
1570
- ].join("\n"),
1571
- ultra: [
1572
- "Delete before adding.",
1573
- "Actively question speculative features, caches, abstractions, configuration, migrations, transport changes, storage changes, and new dependencies.",
1574
- "Prefer the smallest complete end-to-end change, not the smallest local diff.",
1575
- "Do not change an existing contract merely to reduce lines.",
1576
- "For complex requests, ship the smallest correct complete version and state what would justify a larger version.",
1577
- "Ultra is not refusal: explicit requirements, safety, validation, accessibility, data protection, and acceptance criteria remain mandatory.",
1578
- "",
1579
- E2E_RULES
1580
- ].join("\n")
1581
- };
1582
- const MODE_LABELS = {
1583
- lite: "Lite",
1584
- full: "Full",
1585
- ultra: "Ultra"
1586
- };
1587
- /** Compose the complete section text for one intensity. */
1588
- function render(effective) {
1589
- return [
1590
- `PONYTAIL MODE ACTIVE — level: ${effective}`,
1591
- "",
1592
- INTRO,
1593
- "",
1594
- "## Common rules (all modes)",
1595
- COMMON_RULES,
1596
- "",
1597
- "## Safety boundaries (never cut)",
1598
- SAFETY_BOUNDARIES,
1599
- "",
1600
- `## ${MODE_LABELS[effective]} rules`,
1601
- MODE_RULES[effective]
1602
- ].join("\n");
1603
- }
1604
- /**
1605
- * The injected ruleset for one intensity, composed from the structured
1606
- * fragments above. Returns an empty string for `off` (ponytail contributes
1607
- * nothing). Renders are pure per mode and cached so every turn's bytes stay
1608
- * identical.
1609
- */
1610
- function getPonytailInstructions(mode) {
1611
- const effective = normalizeRuntimeMode(mode) ?? "full";
1612
- if (effective === "off") return "";
1613
- const cached = instructionCache.get(effective);
1614
- if (cached !== void 0) return cached;
1615
- const rendered = render(effective);
1616
- instructionCache.set(effective, rendered);
1617
- return rendered;
1618
- }
1619
- /** Rendered rulesets are pure per mode; cache to keep every turn's bytes identical. */
1620
- const instructionCache = /* @__PURE__ */ new Map();
1621
- //#endregion
1622
- //#region lib/types/index.js
1623
- /**
1624
- * Ponytail: the "lazy senior developer" persona as a DeepSeek Harness plugin.
1625
- *
1626
- * One system-prompt section injects the mode-filtered ruleset every turn (the
1627
- * always-on adapter), six runtime skills surface the review/audit/debt/gain/
1628
- * help one-shots, six slash commands drive them from the command plane, and an
1629
- * `agent/pre-step` listener honors the plain-text deactivation phrases.
1630
- *
1631
- * Mode is session-scoped and held in memory; the configured default resolves
1632
- * from `PONYTAIL_DEFAULT_MODE`, then the Cordis profile `defaultMode`, then
1633
- * `~/.config/ponytail/config.json` (see {@link readDefaultMode}), then
1634
- * `full`. A session override via `/ponytail` outranks all of them.
1635
- *
1636
- * @module @deepseek-ai/dsh-ponytail
1637
- */
1638
- const name = "ponytail";
1639
- const inject = ["systemPrompt", "skills"];
1640
- /** Prompt-section order: after the deployment persona (0), before tool guidance (100–199). */
1641
- const SECTION_ORDER = 40;
1642
- /** Build the one text-line notification a mode switch leaves for the model. */
1643
- function modeNotice(mode) {
1644
- return mode === "off" ? "PONYTAIL MODE OFF" : `PONYTAIL MODE CHANGED — level: ${mode}`;
1645
- }
1646
- /** Extract the plain text of one user message (only its text blocks). */
1647
- function messageText(message) {
1648
- const parts = [];
1649
- for (const block of message.content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
1650
- return parts.join("\n");
1651
- }
1652
- /** Whether any message in a claimed batch is exactly a deactivation command. */
1653
- function containsDeactivation(messages) {
1654
- return messages.some((message) => isDeactivationCommand(messageText(message)));
1655
- }
1656
- /** Mode visible to one agent: its session override, else the configured default. */
1657
- function modeFor(deps, agent) {
1658
- return deps.store.modeFor(sessionKey(agent), deps.defaultMode());
1659
- }
1660
- /**
1661
- * Queue one skill's full `<skill_content>` rendering as the model's next
1662
- * ordinary turn, with the same user-explicit `skill-invocation` source the
1663
- * built-in gesture boundary uses.
1664
- */
1665
- async function queueSkill(deps, invocation, skill) {
1666
- const loaded = await deps.ctx.skills.get(skill, {
1667
- cwd: invocation.agent.session.header.cwd,
1668
- signal: invocation.signal
1669
- });
1670
- if (loaded === void 0) return {
1671
- kind: "error",
1672
- text: `skill "${skill}" is not available`
1673
- };
1674
- const notes = invocation.rawInput.trim();
1675
- const text = renderSkillContent(loaded) + (notes === "" ? "" : `\n\n${notes}`);
1676
- invocation.agent.followup(createUserMessage({
1677
- content: [{
1678
- type: "text",
1679
- text
1680
- }],
1681
- source: {
1682
- kind: "skill-invocation",
1683
- name: skill,
1684
- form: "instructions"
1685
- }
1686
- }));
1687
- return {
1688
- kind: "success",
1689
- text: `Queued ${skill} for the agent.`
1690
- };
1691
- }
1692
- function registerCommands(deps, commandCtx) {
1693
- commandCtx.commands.register({
1694
- name: "ponytail",
1695
- description: "Set or show Ponytail lazy senior dev intensity",
1696
- input: { hint: "[status|default <mode>|lite|full|ultra|off]" },
1697
- handler: ({ agent, rawInput }) => {
1698
- const input = rawInput.trim().toLowerCase();
1699
- const [head, ...rest] = input.split(/\s+/).filter(Boolean);
1700
- if ((head ?? "") === "default") {
1701
- let written;
1702
- try {
1703
- written = writeDefaultMode(rest[0]);
1704
- } catch (error) {
1705
- return {
1706
- kind: "error",
1707
- text: `Failed to save default: ${error.message}`
1708
- };
1709
- }
1710
- if (!written) return {
1711
- kind: "error",
1712
- text: "Usage: /ponytail default [lite|full|ultra|off]"
1713
- };
1714
- const effective = readDefaultMode(process.env, deps.profileMode);
1715
- deps.setDefault(effective);
1716
- const reason = defaultOverrideReason(process.env, deps.profileMode);
1717
- if (reason !== null) {
1718
- agent.steer(createUserMessage({
1719
- content: [{
1720
- type: "text",
1721
- text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (${reason}).`
1722
- }],
1723
- source: {
1724
- kind: "plugin",
1725
- plugin: name
1726
- }
1727
- }));
1728
- return {
1729
- kind: "success",
1730
- text: `Saved default: ${written}. Effective default: ${effective}, overridden by ${reason}.`
1731
- };
1732
- }
1733
- agent.steer(createUserMessage({
1734
- content: [{
1735
- type: "text",
1736
- text: `PONYTAIL DEFAULT SET — new sessions start in ${written}.`
1737
- }],
1738
- source: {
1739
- kind: "plugin",
1740
- plugin: name
1741
- }
1742
- }));
1508
+ /**
1509
+ * The never-cut list. Every non-`off` mode keeps these; intensities tune how
1510
+ * aggressively code is minimized, never what may be dropped.
1511
+ */
1512
+ const SAFETY_BOUNDARIES = [
1513
+ "Never cut, in any mode:",
1514
+ "- Input validation at trust boundaries.",
1515
+ "- Error handling that prevents data loss.",
1516
+ "- Security measures.",
1517
+ "- Accessibility basics.",
1518
+ "- Explicit acceptance criteria the user asked for.",
1519
+ "- Understanding the problem and tracing the real flow first.",
1520
+ "- The real end-to-end data flow: no UI-only field, unused state, placeholder path, or disconnected payload.",
1521
+ "- Non-trivial logic leaves one minimal runnable check: the smallest assert or test that catches breakage; no framework or fixtures unless asked.",
1522
+ "- Root-cause fixes over symptom patches.",
1523
+ "- \"Minimal diff\" is not a substitute for \"correct fix\"."
1524
+ ].join("\n");
1525
+ /** Lite: complete the explicit ask; reuse; suggest, do not challenge. */
1526
+ const LITE_RULES = [
1527
+ "Execute the direct request without ceremony; complete every explicit acceptance criterion.",
1528
+ "You may mention a simpler alternative briefly, but do not challenge or reject an explicit requirement.",
1529
+ "Do not change the existing architecture merely to reduce line count.",
1530
+ "Keep the smallest reasonable validation for non-trivial changes."
1531
+ ].join("\n");
1532
+ /** Smallest complete end-to-end change: shared by Full and Ultra. */
1533
+ const E2E_RULES = [
1534
+ "Smallest complete end-to-end change:",
1535
+ "- Prefer the smallest complete end-to-end change compatible with the existing architecture, not merely the fewest lines in one file.",
1536
+ "- Before creating a component, abstraction, protocol, migration, transport format, storage format, or dependency, inspect the repository’s existing path and preserve its current contract.",
1537
+ "- Do not redesign transport, storage, API shape, or persistence when the task only asks for a local UI or behavior change.",
1538
+ "- Prefer the smallest complete change across the real data flow: input → state → validation → payload → API → persistence → response/UI.",
1539
+ "- It does not mean every layer must change: the change must be complete across the layers it touches.",
1540
+ "- Do not leave a UI-only field, unused state, placeholder path, or disconnected payload merely because it produces a smaller diff."
1541
+ ].join("\n");
1542
+ const MODE_RULES = {
1543
+ lite: LITE_RULES,
1544
+ full: [
1545
+ "Use the seven-rung ladder; stop at the first rung that holds:",
1546
+ "1. Does this need to exist at all? (YAGNI)",
1547
+ "2. Does it already exist in this codebase? Reuse it.",
1548
+ "3. Does the standard library do it? Use it.",
1549
+ "4. Does a native platform feature cover it? Use it.",
1550
+ "5. Does an already-installed dependency solve it? Use it.",
1551
+ "6. Can the solution be reduced to a small expression? Make it that small.",
1552
+ "7. Only then: write the minimum new implementation.",
1553
+ "Default to the shortest correct implementation; prefer deletion and reuse. Fix root causes, not symptoms (one shared guard beats one per caller).",
1554
+ E2E_RULES
1555
+ ].join("\n"),
1556
+ ultra: [
1557
+ "Require evidence before adding. Prefer deletion or reuse; challenge speculative features, caches, abstractions, configuration, migrations, and dependencies.",
1558
+ "For complex requests, ship the smallest correct complete version and state what would justify a larger version.",
1559
+ "Ultra is not refusal: explicit requirements and the safety boundaries remain mandatory.",
1560
+ E2E_RULES
1561
+ ].join("\n")
1562
+ };
1563
+ const MODE_LABELS = {
1564
+ lite: "Lite",
1565
+ full: "Full",
1566
+ ultra: "Ultra"
1567
+ };
1568
+ /** Compose the complete section text for one intensity. */
1569
+ function render(effective) {
1570
+ return [
1571
+ `PONYTAIL MODE ACTIVE level: ${effective}`,
1572
+ "",
1573
+ INTRO,
1574
+ "",
1575
+ "## Common rules (all modes)",
1576
+ COMMON_RULES,
1577
+ "",
1578
+ "## Safety boundaries (never cut)",
1579
+ SAFETY_BOUNDARIES,
1580
+ "",
1581
+ `## ${MODE_LABELS[effective]} rules`,
1582
+ MODE_RULES[effective]
1583
+ ].join("\n");
1584
+ }
1585
+ /**
1586
+ * The injected ruleset for one intensity, composed from the structured
1587
+ * fragments above. Returns an empty string for `off` (ponytail contributes
1588
+ * nothing). Renders are pure per mode and cached so every turn's bytes stay
1589
+ * identical.
1590
+ */
1591
+ function getPonytailInstructions(mode) {
1592
+ const effective = normalizeRuntimeMode(mode) ?? "full";
1593
+ if (effective === "off") return "";
1594
+ const cached = instructionCache.get(effective);
1595
+ if (cached !== void 0) return cached;
1596
+ const rendered = render(effective);
1597
+ instructionCache.set(effective, rendered);
1598
+ return rendered;
1599
+ }
1600
+ /** Rendered rulesets are pure per mode; cache to keep every turn's bytes identical. */
1601
+ const instructionCache = /* @__PURE__ */ new Map();
1602
+ //#endregion
1603
+ //#region lib/types/index.js
1604
+ /**
1605
+ * Ponytail: the "lazy senior developer" persona as a DeepSeek Harness plugin.
1606
+ *
1607
+ * One system-prompt section injects the mode-filtered ruleset every turn (the
1608
+ * always-on adapter), six runtime skills surface the review/audit/debt/gain/
1609
+ * help one-shots, six slash commands drive them from the command plane, and an
1610
+ * `agent/pre-step` listener honors the plain-text deactivation phrases.
1611
+ *
1612
+ * Mode is session-scoped and held in memory; the configured default resolves
1613
+ * from `PONYTAIL_DEFAULT_MODE`, then the Cordis profile `defaultMode`, then
1614
+ * `~/.config/ponytail/config.json` (see {@link readDefaultMode}), then
1615
+ * `full`. A session override via `/ponytail` outranks all of them.
1616
+ *
1617
+ * @module @mengyuly/dsh-ponytail
1618
+ */
1619
+ const name = "ponytail";
1620
+ const inject = ["systemPrompt", "skills"];
1621
+ /** Prompt-section order: after the deployment persona (0), before tool guidance (100–199). */
1622
+ const SECTION_ORDER = 40;
1623
+ /** Build the one text-line notification a mode switch leaves for the model. */
1624
+ function modeNotice(mode) {
1625
+ return mode === "off" ? "PONYTAIL MODE OFF" : `PONYTAIL MODE CHANGED — level: ${mode}`;
1626
+ }
1627
+ /** Extract the plain text of one user message (only its text blocks). */
1628
+ function messageText(message) {
1629
+ const parts = [];
1630
+ for (const block of message.content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
1631
+ return parts.join("\n");
1632
+ }
1633
+ /** Whether any message in a claimed batch is exactly a deactivation command. */
1634
+ function containsDeactivation(messages) {
1635
+ return messages.some((message) => isDeactivationCommand(messageText(message)));
1636
+ }
1637
+ /** Mode visible to one agent: its session override, else the configured default. */
1638
+ function modeFor(deps, agent) {
1639
+ return deps.store.modeFor(sessionKey(agent), deps.defaultMode());
1640
+ }
1641
+ /**
1642
+ * Queue one skill's full `<skill_content>` rendering as the model's next
1643
+ * ordinary turn, with the same user-explicit `skill-invocation` source the
1644
+ * built-in gesture boundary uses.
1645
+ */
1646
+ async function queueSkill(deps, invocation, skill) {
1647
+ const loaded = await deps.ctx.skills.get(skill, {
1648
+ cwd: invocation.agent.session.header.cwd,
1649
+ signal: invocation.signal
1650
+ });
1651
+ if (loaded === void 0) return {
1652
+ kind: "error",
1653
+ text: `skill "${skill}" is not available`
1654
+ };
1655
+ const notes = invocation.rawInput.trim();
1656
+ const text = renderSkillContent(loaded) + (notes === "" ? "" : `\n\n${notes}`);
1657
+ invocation.agent.followup(createUserMessage({
1658
+ content: [{
1659
+ type: "text",
1660
+ text
1661
+ }],
1662
+ source: {
1663
+ kind: "skill-invocation",
1664
+ name: skill,
1665
+ form: "instructions"
1666
+ }
1667
+ }));
1668
+ return {
1669
+ kind: "success",
1670
+ text: `Queued ${skill} for the agent.`
1671
+ };
1672
+ }
1673
+ function registerCommands(deps, commandCtx) {
1674
+ commandCtx.commands.register({
1675
+ name: "ponytail",
1676
+ description: "Set or show Ponytail lazy senior dev intensity",
1677
+ input: { hint: "[status|reset|default <mode>|lite|full|ultra|off]" },
1678
+ handler: ({ agent, rawInput }) => {
1679
+ const input = rawInput.trim().toLowerCase();
1680
+ const [head, ...rest] = input.split(/\s+/).filter(Boolean);
1681
+ if ((head ?? "") === "default") {
1682
+ let written;
1683
+ try {
1684
+ written = writeDefaultMode(rest[0]);
1685
+ } catch (error) {
1686
+ return {
1687
+ kind: "error",
1688
+ text: `Failed to save default: ${error.message}`
1689
+ };
1690
+ }
1691
+ if (!written) return {
1692
+ kind: "error",
1693
+ text: "Usage: /ponytail default [lite|full|ultra|off]"
1694
+ };
1695
+ const effective = readDefaultMode(process.env, deps.profileMode);
1696
+ deps.setDefault(effective);
1697
+ const reason = defaultOverrideReason(process.env, deps.profileMode);
1698
+ if (reason !== null) {
1699
+ agent.steer(createUserMessage({
1700
+ content: [{
1701
+ type: "text",
1702
+ text: `PONYTAIL DEFAULT SET — saved ${written}, effective ${effective} (${reason}).`
1703
+ }],
1704
+ source: {
1705
+ kind: "plugin",
1706
+ plugin: name
1707
+ }
1708
+ }));
1709
+ return {
1710
+ kind: "success",
1711
+ text: `Saved default: ${written}. Effective default: ${effective}, overridden by ${reason}.`
1712
+ };
1713
+ }
1714
+ agent.steer(createUserMessage({
1715
+ content: [{
1716
+ type: "text",
1717
+ text: `PONYTAIL DEFAULT SET — new sessions start in ${written}.`
1718
+ }],
1719
+ source: {
1720
+ kind: "plugin",
1721
+ plugin: name
1722
+ }
1723
+ }));
1724
+ return {
1725
+ kind: "success",
1726
+ text: `Ponytail default set — new sessions start in ${written}.`
1727
+ };
1728
+ }
1729
+ if (input === "status") {
1730
+ const current = modeFor(deps, agent);
1731
+ const source = deps.store.has(sessionKey(agent)) ? "session override" : "configured default";
1743
1732
  return {
1744
1733
  kind: "success",
1745
- text: `Ponytail default set new sessions start in ${written}.`
1734
+ text: `Ponytail mode: ${current} (${source}). Use /ponytail reset|lite|full|ultra|off.`
1746
1735
  };
1747
1736
  }
1748
- if (input === "status") return {
1749
- kind: "success",
1750
- text: `Ponytail mode: ${modeFor(deps, agent)}. Use /ponytail lite|full|ultra|off.`
1751
- };
1752
- if (input === "") {
1753
- const current = modeFor(deps, agent);
1754
- const effectiveDefault = deps.defaultMode();
1755
- if (current === "off") {
1756
- if (effectiveDefault === "off") {
1757
- deps.store.set(sessionKey(agent), "full");
1758
- agent.steer(createUserMessage({
1759
- content: [{
1760
- type: "text",
1761
- text: "PONYTAIL MODE CHANGED — level: full"
1762
- }],
1763
- source: {
1764
- kind: "plugin",
1765
- plugin: name
1766
- }
1767
- }));
1768
- return {
1769
- kind: "success",
1770
- text: "Ponytail re-enabled at full (the effective default is off)."
1771
- };
1772
- }
1773
- deps.store.clear(sessionKey(agent));
1774
- agent.steer(createUserMessage({
1775
- content: [{
1776
- type: "text",
1777
- text: `PONYTAIL MODE ACTIVE — level: ${effectiveDefault}`
1778
- }],
1779
- source: {
1780
- kind: "plugin",
1781
- plugin: name
1782
- }
1783
- }));
1784
- return {
1785
- kind: "success",
1786
- text: `Ponytail re-enabled. Effective default: ${effectiveDefault}.`
1787
- };
1788
- }
1789
- agent.steer(createUserMessage({
1737
+ if (input === "reset") {
1738
+ const key = sessionKey(agent);
1739
+ const changed = deps.store.has(key);
1740
+ deps.store.clear(key);
1741
+ const current = deps.defaultMode();
1742
+ if (changed) agent.steer(createUserMessage({
1790
1743
  content: [{
1791
1744
  type: "text",
1792
- text: `PONYTAIL MODE ACTIVE — level: ${current}`
1745
+ text: modeNotice(current)
1793
1746
  }],
1794
1747
  source: {
1795
1748
  kind: "plugin",
@@ -1798,139 +1751,191 @@ function registerCommands(deps, commandCtx) {
1798
1751
  }));
1799
1752
  return {
1800
1753
  kind: "success",
1801
- text: `Ponytail mode: ${current}. Use /ponytail lite|full|ultra|off.`
1754
+ text: changed ? `Ponytail session override cleared. Effective mode: ${current}.` : `Ponytail already follows the configured default: ${current}.`
1802
1755
  };
1803
1756
  }
1804
- const mode = normalizeRuntimeMode(input);
1805
- if (!mode) return {
1806
- kind: "error",
1807
- text: "Usage: /ponytail [status|default <mode>|lite|full|ultra|off]"
1808
- };
1809
- deps.store.set(sessionKey(agent), mode);
1810
- agent.steer(createUserMessage({
1811
- content: [{
1812
- type: "text",
1813
- text: modeNotice(mode)
1814
- }],
1815
- source: {
1816
- kind: "plugin",
1817
- plugin: name
1818
- }
1819
- }));
1820
- return {
1821
- kind: "success",
1822
- text: mode === "off" ? "Ponytail mode off." : `Ponytail mode set to ${mode}.`
1823
- };
1824
- }
1825
- });
1826
- for (const skill of [
1827
- "ponytail-review",
1828
- "ponytail-audit",
1829
- "ponytail-debt",
1830
- "ponytail-gain",
1831
- "ponytail-help"
1832
- ]) commandCtx.commands.register({
1833
- name: skill,
1834
- description: descriptionFor(skill),
1835
- input: { hint: "[notes]" },
1836
- handler: (invocation) => queueSkill(deps, invocation, skill)
1837
- });
1838
- }
1839
- /** One-line command catalog copy, kept beside the skills for discovery parity. */
1840
- function descriptionFor(skill) {
1841
- switch (skill) {
1842
- case "ponytail-review": return "Over-engineering review of the current changes";
1843
- case "ponytail-audit": return "Whole-repo over-engineering audit (what can be deleted)";
1844
- case "ponytail-debt": return "Harvest ponytail: comments into a tracked debt ledger";
1845
- case "ponytail-gain": return "Show ponytail measured-impact scoreboard (less code, cost, time)";
1846
- case "ponytail-help": return "Quick reference for ponytail levels, skills, and commands";
1847
- default: return `Run the ${skill} skill`;
1848
- }
1849
- }
1850
- /**
1851
- * Register the always-on ruleset section, the runtime skills, the slash
1852
- * commands, and the plain-text deactivation listener.
1853
- */
1854
- function apply(ctx, config = {}) {
1855
- const profileMode = normalizeRuntimeMode(config.defaultMode);
1856
- if (config.defaultMode !== void 0 && profileMode === null) ctx.logger.warn(`[ponytail] profile config defaultMode is not lite|full|ultra|off: ${JSON.stringify(config.defaultMode)}; falling back`);
1857
- let defaultMode = null;
1858
- const warned = /* @__PURE__ */ new Set();
1859
- const warnOnce = (key, message) => {
1860
- if (warned.has(key)) return;
1861
- warned.add(key);
1862
- ctx.logger.warn(`[ponytail] ${message}`);
1863
- };
1864
- const refreshDefault = () => {
1865
- const resolution = readDefaultModeInfo(process.env, profileMode);
1866
- if (resolution.issue) warnOnce(`default:${resolution.issue.kind}`, `${resolution.issue.detail}; using ${resolution.mode}`);
1867
- defaultMode = resolution.mode;
1868
- return defaultMode;
1869
- };
1870
- const readDefault = () => defaultMode ?? refreshDefault();
1871
- const setDefault = (mode) => {
1872
- defaultMode = mode;
1873
- };
1874
- const store = new ModeStore();
1875
- const matcherResult = compileSubagentMatcher(process.env.PONYTAIL_SUBAGENT_MATCHER);
1876
- const matcher = matcherResult.matcher;
1877
- if (matcherResult.invalid) warnOnce("matcher:invalid", "PONYTAIL_SUBAGENT_MATCHER is not a valid regular expression; ignoring it (fail-open).");
1878
- const configFile = configPath();
1879
- const onConfigChange = () => {
1880
- const resolution = readDefaultModeInfo(process.env, profileMode);
1881
- if (resolution.issue) {
1882
- warnOnce(`config:${resolution.issue.kind}`, `${resolution.issue.detail}; keeping the previous default`);
1883
- return;
1884
- }
1885
- defaultMode = resolution.mode;
1886
- };
1887
- watchFile(configFile, { interval: 1e3 }, onConfigChange).unref();
1888
- ctx.effect(() => () => {
1889
- unwatchFile(configFile, onConfigChange);
1890
- }, "ponytail: config hot reload");
1891
- ctx.on("agent/disposed", ({ agent }) => {
1892
- store.clear(sessionKey(agent));
1893
- });
1894
- ctx.systemPrompt.section({
1895
- name: "ponytail",
1896
- order: SECTION_ORDER,
1897
- text: ({ agent }) => {
1898
- if (agent && matcher && isSubagentSession(agent.session.header)) {
1899
- const preset = agent.session.header.agentPreset;
1900
- if (preset && !matcher.test(preset)) return "";
1901
- }
1902
- return getPonytailInstructions(agent ? store.modeFor(sessionKey(agent), readDefault()) : readDefault());
1903
- }
1904
- });
1905
- for (const skill of ponytailSkills()) ctx.skills.register(skill);
1906
- ctx.inject(["commands"], (commandCtx) => {
1907
- registerCommands({
1908
- ctx,
1909
- store,
1910
- profileMode,
1911
- defaultMode: readDefault,
1912
- setDefault
1913
- }, commandCtx);
1914
- });
1915
- ctx.on("agent/pre-step", async (payload, next) => {
1916
- const deactivated = containsDeactivation(payload.messages);
1917
- if (deactivated) store.set(sessionKey(payload.agent), "off");
1918
- const decision = await next();
1919
- if (deactivated && decision.kind === "enter") return {
1920
- kind: "enter",
1921
- messages: [...decision.messages, createUserMessage({
1922
- content: [{
1923
- type: "text",
1924
- text: "PONYTAIL MODE OFF"
1925
- }],
1926
- source: {
1927
- kind: "plugin",
1928
- plugin: name
1929
- }
1930
- })]
1931
- };
1932
- return decision;
1933
- });
1934
- }
1935
- //#endregion
1936
- export { apply, containsDeactivation, inject, messageText, name };
1757
+ if (input === "") {
1758
+ const current = modeFor(deps, agent);
1759
+ const effectiveDefault = deps.defaultMode();
1760
+ if (current === "off") {
1761
+ if (effectiveDefault === "off") {
1762
+ deps.store.set(sessionKey(agent), "full");
1763
+ agent.steer(createUserMessage({
1764
+ content: [{
1765
+ type: "text",
1766
+ text: "PONYTAIL MODE CHANGED — level: full"
1767
+ }],
1768
+ source: {
1769
+ kind: "plugin",
1770
+ plugin: name
1771
+ }
1772
+ }));
1773
+ return {
1774
+ kind: "success",
1775
+ text: "Ponytail re-enabled at full (the effective default is off)."
1776
+ };
1777
+ }
1778
+ deps.store.clear(sessionKey(agent));
1779
+ agent.steer(createUserMessage({
1780
+ content: [{
1781
+ type: "text",
1782
+ text: `PONYTAIL MODE ACTIVE — level: ${effectiveDefault}`
1783
+ }],
1784
+ source: {
1785
+ kind: "plugin",
1786
+ plugin: name
1787
+ }
1788
+ }));
1789
+ return {
1790
+ kind: "success",
1791
+ text: `Ponytail re-enabled. Effective default: ${effectiveDefault}.`
1792
+ };
1793
+ }
1794
+ agent.steer(createUserMessage({
1795
+ content: [{
1796
+ type: "text",
1797
+ text: `PONYTAIL MODE ACTIVE level: ${current}`
1798
+ }],
1799
+ source: {
1800
+ kind: "plugin",
1801
+ plugin: name
1802
+ }
1803
+ }));
1804
+ return {
1805
+ kind: "success",
1806
+ text: `Ponytail mode: ${current}. Use /ponytail reset|lite|full|ultra|off.`
1807
+ };
1808
+ }
1809
+ const mode = normalizeRuntimeMode(input);
1810
+ if (!mode) return {
1811
+ kind: "error",
1812
+ text: "Usage: /ponytail [status|reset|default <mode>|lite|full|ultra|off]"
1813
+ };
1814
+ deps.store.set(sessionKey(agent), mode);
1815
+ agent.steer(createUserMessage({
1816
+ content: [{
1817
+ type: "text",
1818
+ text: modeNotice(mode)
1819
+ }],
1820
+ source: {
1821
+ kind: "plugin",
1822
+ plugin: name
1823
+ }
1824
+ }));
1825
+ return {
1826
+ kind: "success",
1827
+ text: mode === "off" ? "Ponytail mode off." : `Ponytail mode set to ${mode}.`
1828
+ };
1829
+ }
1830
+ });
1831
+ for (const skill of [
1832
+ "ponytail-review",
1833
+ "ponytail-audit",
1834
+ "ponytail-debt",
1835
+ "ponytail-gain",
1836
+ "ponytail-help"
1837
+ ]) commandCtx.commands.register({
1838
+ name: skill,
1839
+ description: descriptionFor(skill),
1840
+ input: { hint: "[notes]" },
1841
+ handler: (invocation) => queueSkill(deps, invocation, skill)
1842
+ });
1843
+ }
1844
+ /** One-line command catalog copy, kept beside the skills for discovery parity. */
1845
+ function descriptionFor(skill) {
1846
+ switch (skill) {
1847
+ case "ponytail-review": return "Over-engineering review of the current changes";
1848
+ case "ponytail-audit": return "Whole-repo over-engineering audit (what can be deleted)";
1849
+ case "ponytail-debt": return "Harvest ponytail: comments into a tracked debt ledger";
1850
+ case "ponytail-gain": return "Show ponytail measured-impact scoreboard (less code, cost, time)";
1851
+ case "ponytail-help": return "Quick reference for ponytail levels, skills, and commands";
1852
+ default: return `Run the ${skill} skill`;
1853
+ }
1854
+ }
1855
+ /**
1856
+ * Register the always-on ruleset section, the runtime skills, the slash
1857
+ * commands, and the plain-text deactivation listener.
1858
+ */
1859
+ function apply(ctx, config = {}) {
1860
+ const profileMode = normalizeRuntimeMode(config.defaultMode);
1861
+ if (config.defaultMode !== void 0 && profileMode === null) ctx.logger.warn(`[ponytail] profile config defaultMode is not lite|full|ultra|off: ${JSON.stringify(config.defaultMode)}; falling back`);
1862
+ let defaultMode = null;
1863
+ const warned = /* @__PURE__ */ new Set();
1864
+ const warnOnce = (key, message) => {
1865
+ if (warned.has(key)) return;
1866
+ warned.add(key);
1867
+ ctx.logger.warn(`[ponytail] ${message}`);
1868
+ };
1869
+ const refreshDefault = () => {
1870
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1871
+ if (resolution.issue) warnOnce(`default:${resolution.issue.kind}`, `${resolution.issue.detail}; using ${resolution.mode}`);
1872
+ defaultMode = resolution.mode;
1873
+ return defaultMode;
1874
+ };
1875
+ const readDefault = () => defaultMode ?? refreshDefault();
1876
+ const setDefault = (mode) => {
1877
+ defaultMode = mode;
1878
+ };
1879
+ const store = new ModeStore();
1880
+ const matcherResult = compileSubagentMatcher(process.env.PONYTAIL_SUBAGENT_MATCHER);
1881
+ const matcher = matcherResult.matcher;
1882
+ if (matcherResult.invalid) warnOnce("matcher:invalid", "PONYTAIL_SUBAGENT_MATCHER is not a valid regular expression; ignoring it (fail-open).");
1883
+ const configFile = configPath();
1884
+ const onConfigChange = () => {
1885
+ const resolution = readDefaultModeInfo(process.env, profileMode);
1886
+ if (resolution.issue) {
1887
+ warnOnce(`config:${resolution.issue.kind}`, `${resolution.issue.detail}; keeping the previous default`);
1888
+ return;
1889
+ }
1890
+ defaultMode = resolution.mode;
1891
+ };
1892
+ watchFile(configFile, { interval: 1e3 }, onConfigChange).unref();
1893
+ ctx.effect(() => () => {
1894
+ unwatchFile(configFile, onConfigChange);
1895
+ }, "ponytail: config hot reload");
1896
+ ctx.on("agent/disposed", ({ agent }) => {
1897
+ store.clear(sessionKey(agent));
1898
+ });
1899
+ ctx.systemPrompt.section({
1900
+ name: "ponytail",
1901
+ order: SECTION_ORDER,
1902
+ text: ({ agent }) => {
1903
+ if (agent && matcher && isSubagentSession(agent.session.header)) {
1904
+ const preset = agent.session.header.agentPreset;
1905
+ if (preset && !matcher.test(preset)) return "";
1906
+ }
1907
+ return getPonytailInstructions(agent ? store.modeFor(sessionKey(agent), readDefault()) : readDefault());
1908
+ }
1909
+ });
1910
+ for (const skill of ponytailSkills()) ctx.skills.register(skill);
1911
+ ctx.inject(["commands"], (commandCtx) => {
1912
+ registerCommands({
1913
+ ctx,
1914
+ store,
1915
+ profileMode,
1916
+ defaultMode: readDefault,
1917
+ setDefault
1918
+ }, commandCtx);
1919
+ });
1920
+ ctx.on("agent/pre-step", async (payload, next) => {
1921
+ const deactivated = containsDeactivation(payload.messages);
1922
+ if (deactivated) store.set(sessionKey(payload.agent), "off");
1923
+ const decision = await next();
1924
+ if (deactivated && decision.kind === "enter") return {
1925
+ kind: "enter",
1926
+ messages: [...decision.messages, createUserMessage({
1927
+ content: [{
1928
+ type: "text",
1929
+ text: "PONYTAIL MODE OFF"
1930
+ }],
1931
+ source: {
1932
+ kind: "plugin",
1933
+ plugin: name
1934
+ }
1935
+ })]
1936
+ };
1937
+ return decision;
1938
+ });
1939
+ }
1940
+ //#endregion
1941
+ export { apply, containsDeactivation, inject, messageText, name };