ytdb-slate 0.2.1 → 0.4.0

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.
@@ -20,12 +20,14 @@
20
20
  * Optional config at <config dir>/slate.json (config dir = CONFIG_DIR_NAME,
21
21
  * ".pi" by default), honored ONLY when the project is trusted — untrusted
22
22
  * projects run on built-in defaults with no project file injection:
23
- * { "episodeModel": "provider/id", "workerTools": [...], "maxConcurrent": 4,
23
+ * { "episodeModel": "provider/id", "workerTools": [...],
24
+ * "workerExtensions": ["regex", ...], "maxConcurrent": 4,
24
25
  * "contextBudget": 256000, "orchestratorModeDefault": true,
25
26
  * "orchestratorPromptDocs": ["docs/orchestrator-guidelines.md"],
26
27
  * "workerPromptDocs": ["docs/thread-guidelines.md"],
27
28
  * "workflow": { "draftPRs": false },
28
29
  * "modelFailover": { "provider/id": "provider/id" },
30
+ * "preserveGlobalModelDefault": true,
29
31
  * "doctrineExtraPath": "docs/project-doctrine.md",
30
32
  * "reviewPerspectivesPath": "docs/review-perspectives.md" }
31
33
  * contextBudget also takes { "tokens": N, "overrides": [{ "match": "regex",
@@ -45,6 +47,12 @@ import { registerSlateMode } from "./mode.ts";
45
47
  import { SlateStore, type SlateConfig } from "./state.ts";
46
48
  import { ThreadManager } from "./threads.ts";
47
49
  import { registerSlateTools } from "./tools.ts";
50
+ import {
51
+ createWorkerExtensionResolver,
52
+ EMPTY_WORKER_EXTENSION_SET,
53
+ sanitizeWorkerExtensions,
54
+ type WorkerExtensionSet,
55
+ } from "./worker-extensions.ts";
48
56
 
49
57
  function loadConfig(cwd: string): SlateConfig {
50
58
  const file = join(cwd, CONFIG_DIR_NAME, "slate.json");
@@ -67,7 +75,16 @@ function loadConfig(cwd: string): SlateConfig {
67
75
 
68
76
  export default function (pi: ExtensionAPI) {
69
77
  const store = new SlateStore(pi);
70
- let manager = new ThreadManager(store, {});
78
+ // One worker-extension resolver per session (AD41), reassigned every
79
+ // session_start below. The DOCTRINE consumer (registerSlateMode) reads it
80
+ // through the live `() => resolveWorkerExtensionSet()` indirection because it
81
+ // always belongs to the CURRENT session. ThreadManager instead binds the
82
+ // resolver instance BY VALUE at construction (CN20): a manager orphaned by a
83
+ // session swap must keep resolving against ITS OWN session's frozen set, not a
84
+ // later session's — the live indirection would leak the newer set into the
85
+ // stale manager. Starts as the empty set (feature off) until session_start.
86
+ let resolveWorkerExtensionSet: () => WorkerExtensionSet = () => EMPTY_WORKER_EXTENSION_SET;
87
+ let manager = new ThreadManager(store, {}, resolveWorkerExtensionSet);
71
88
 
72
89
  registerSlateTools(pi, store, () => manager);
73
90
 
@@ -77,15 +94,24 @@ export default function (pi: ExtensionAPI) {
77
94
  // it is honored only for trusted projects; untrusted → built-in defaults.
78
95
  const config = ctx.isProjectTrusted() ? loadConfig(ctx.cwd) : {};
79
96
  const warn = (msg: string) => (ctx.hasUI ? ctx.ui.notify(msg, "warning") : console.warn(msg));
80
- // modelFailover and contextBudget are validated eagerly: a malformed
81
- // value would otherwise fail silently mid-dispatch / exactly when the
82
- // auto-pause was supposed to save the orchestrator's context.
97
+ // modelFailover, contextBudget and workerExtensions are validated eagerly:
98
+ // a malformed value would otherwise fail silently mid-dispatch / exactly
99
+ // when the auto-pause was supposed to save the orchestrator's context.
83
100
  config.modelFailover = sanitizeModelFailover(config.modelFailover, warn);
84
101
  config.contextBudget = sanitizeContextBudget(config.contextBudget, warn);
102
+ config.workerExtensions = sanitizeWorkerExtensions(config.workerExtensions, warn);
85
103
  if (config.contextBudget !== undefined && config.pauseThresholdPercent !== undefined) {
86
104
  warn("slate: contextBudget is set — the deprecated pauseThresholdPercent is ignored");
87
105
  }
88
- manager = new ThreadManager(store, config);
106
+ // Fresh resolver AFTER sanitization (reads the cleaned patterns) and per
107
+ // session_start (a restart re-resolves). Same warn as the sanitizers so a
108
+ // withheld colliding unit surfaces the same way. First use is later, in a
109
+ // worker open or a doctrine build — after session_start finishes, so tools
110
+ // registered during session_start are captured (AD41).
111
+ resolveWorkerExtensionSet = createWorkerExtensionResolver(pi, () => config.workerExtensions ?? [], warn);
112
+ // Bound BY VALUE (CN20): this manager keeps this session's resolver even if a
113
+ // later session_start replaces the module variable above.
114
+ manager = new ThreadManager(store, config, resolveWorkerExtensionSet);
89
115
  store.restore(ctx);
90
116
  });
91
117
 
@@ -103,5 +129,5 @@ export default function (pi: ExtensionAPI) {
103
129
  // order-critical relative to the handlers above (different trigger events).
104
130
  registerOrchestratorFailover(pi, () => manager.getConfig());
105
131
 
106
- registerSlateMode(pi, store, handoff, () => manager.getConfig());
132
+ registerSlateMode(pi, store, handoff, () => manager.getConfig(), () => resolveWorkerExtensionSet());
107
133
  }
package/extension/mode.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  } from "./paths.ts";
28
28
  import { loadPromptDocs } from "./prompt-docs.ts";
29
29
  import { orchestratorCostUsd, type SlateConfig, type SlateStore } from "./state.ts";
30
+ import type { WorkerExtensionSet, WorkerExtensionUnit } from "./worker-extensions.ts";
30
31
 
31
32
  const ORCHESTRATOR_TOOLS = ["read", "grep", "find", "ls", "thread", "threads", "episode"];
32
33
 
@@ -40,7 +41,68 @@ const ORCHESTRATOR_TOOLS = ["read", "grep", "find", "ls", "thread", "threads", "
40
41
  * the file exists (cwd-resolved) — missing files are skipped silently like
41
42
  * every other project doc.
42
43
  */
43
- function buildDoctrine(cwd: string, config: SlateConfig, trusted: boolean): string {
44
+ // A source spec that names WHICH extension (e.g. "npm:foo") is worth showing;
45
+ // the generic scope-ish sources are not, so rule 11 falls back to the unit path.
46
+ const UNINFORMATIVE_SOURCES = new Set(["", "auto", "local", "cli", "project", "user", "temporary", "builtin", "sdk"]);
47
+
48
+ function unitLabel(unit: WorkerExtensionUnit): string {
49
+ return UNINFORMATIVE_SOURCES.has(unit.source) ? unit.path : unit.source;
50
+ }
51
+
52
+ // Per-field caps for the doctrine interpolations (WB21): a tool name is an
53
+ // identifier, a unit label is a package spec or path, a description is a
54
+ // sentence. Caps keep a pathological 2000-char value from bloating every turn.
55
+ const DOCTRINE_NAME_MAX = 64;
56
+ const DOCTRINE_LABEL_MAX = 128;
57
+ const DOCTRINE_DESC_MAX = 140;
58
+
59
+ // ONE sanitizer for every extension-supplied string interpolated into the
60
+ // orchestrator doctrine — tool names, unit labels and descriptions (WB20/WB21/
61
+ // WB22). The doctrine is a numbered, indented plain-text block inside the
62
+ // system prompt, so a raw value must not be able to: (WB20) inject a newline
63
+ // that forges a new numbered directive; (WB21) run on for thousands of
64
+ // characters; or (WB22) carry backticks/markdown that break the block's
65
+ // structure or read as an instruction. Control characters and newlines collapse
66
+ // to spaces, backticks and markdown structural markers are dropped, remaining
67
+ // whitespace is collapsed, and the result is capped with an ellipsis.
68
+ function sanitizeForDoctrine(value: string, max: number): string {
69
+ const cleaned = value
70
+ .replace(/[\u0000-\u001f\u007f\u009b]/g, " ") // control chars + newlines → space (WB20)
71
+ .replace(/[`*_~#>|]/g, " ") // code fences / markdown structure → space (WB22)
72
+ .replace(/\s+/g, " ")
73
+ .trim();
74
+ return cleaned.length > max ? `${cleaned.slice(0, max - 1)}\u2026` : cleaned;
75
+ }
76
+
77
+ /**
78
+ * Rule 11 (worker-extension awareness): appended ONLY when workers load extra
79
+ * pi extensions. With no units it returns "" so the doctrine is byte-identical
80
+ * to the pre-feature output (feature-off is unchanged). Phrased to match the
81
+ * imperative register of rules 1–10 (WS20/WS21/WS22): it LEADS with the action
82
+ * (delegate to a thread), states the constraint in the doctrine's own "cannot"
83
+ * voice, and CLOSES with an instruction. The extension/tool listing in the
84
+ * middle is data and renders verbatim.
85
+ */
86
+ function buildWorkerExtensionsRule(extensions: WorkerExtensionSet): string {
87
+ if (extensions.units.length === 0) return "";
88
+ const lines = [
89
+ "11. Delegate any action that needs one of these worker-loaded pi extensions to a",
90
+ " thread; you cannot call their tools yourself:",
91
+ ];
92
+ for (const unit of extensions.units) {
93
+ lines.push(` - ${sanitizeForDoctrine(unitLabel(unit), DOCTRINE_LABEL_MAX)}`);
94
+ for (const tool of unit.tools) {
95
+ lines.push(` ${sanitizeForDoctrine(tool.name, DOCTRINE_NAME_MAX)}: ${sanitizeForDoctrine(tool.description, DOCTRINE_DESC_MAX)}`);
96
+ }
97
+ }
98
+ lines.push(
99
+ " Assume every worker already has them — you need not pass them in a thread's",
100
+ " `tools` allowlist.",
101
+ );
102
+ return `\n${lines.join("\n")}`;
103
+ }
104
+
105
+ function buildDoctrine(cwd: string, config: SlateConfig, trusted: boolean, extensions: WorkerExtensionSet): string {
44
106
  // Rule 8 tail: with draft-PR publishing enabled, the umbrella draft PR is
45
107
  // one of the gates; otherwise durable records live in the workflow log.
46
108
  const rule8Tail =
@@ -92,7 +154,7 @@ threads execute. Rules:
92
154
  Read that file only when you must reason about slate itself (explaining
93
155
  it, changing the extension, or an unusual routing/compaction decision) —
94
156
  never for routine dispatching. Skip the read if it is already in your
95
- context.`;
157
+ context.${buildWorkerExtensionsRule(extensions)}`;
96
158
  }
97
159
 
98
160
  /**
@@ -130,6 +192,7 @@ export function registerSlateMode(
130
192
  store: SlateStore,
131
193
  hooks: SlateHandoffHooks,
132
194
  getConfig: () => SlateConfig,
195
+ getExtensions: () => WorkerExtensionSet,
133
196
  ): void {
134
197
  let savedTools: string[] | undefined;
135
198
  let uiCtx: ExtensionContext | undefined;
@@ -229,7 +292,7 @@ export function registerSlateMode(
229
292
  // addendum goes LAST so the pause directive is the final word in the
230
293
  // prompt, undiluted by the role guidelines.
231
294
  const parts = [
232
- buildDoctrine(ctx.cwd, config, trusted),
295
+ buildDoctrine(ctx.cwd, config, trusted, getExtensions()),
233
296
  ...loadDoctrineExtra(ctx.cwd, config, trusted).map((d) => `\n\n${d}`),
234
297
  ...docs.map((d) => `\n\n${d}`),
235
298
  ];