supipowers 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +71 -12
  2. package/package.json +4 -8
  3. package/skills/ui-design/SKILL.md +2 -2
  4. package/src/ai/final-message.ts +15 -1
  5. package/src/ai/schema-text.ts +60 -40
  6. package/src/ai/schema-validation.ts +88 -0
  7. package/src/ai/structured-output.ts +19 -19
  8. package/src/bootstrap.ts +3 -0
  9. package/src/commands/fix-pr.ts +166 -26
  10. package/src/commands/optimize-context.ts +153 -16
  11. package/src/commands/runbook.ts +511 -0
  12. package/src/config/schema.ts +102 -139
  13. package/src/context/rule-renderer.ts +274 -2
  14. package/src/context/runbook-extension-template.ts +193 -0
  15. package/src/context/startup-check.ts +197 -2
  16. package/src/context/startup-optimizer.ts +133 -10
  17. package/src/docs/contracts.ts +13 -23
  18. package/src/fix-pr/assessment.ts +63 -24
  19. package/src/fix-pr/contracts.ts +15 -23
  20. package/src/fix-pr/fetch-comments.ts +119 -0
  21. package/src/fix-pr/prompt-builder.ts +19 -8
  22. package/src/git/commit-contract.ts +13 -19
  23. package/src/git/commit.ts +168 -6
  24. package/src/harness/command.ts +98 -6
  25. package/src/harness/git-verification.ts +515 -0
  26. package/src/harness/git-verify-qa.ts +406 -0
  27. package/src/harness/pipeline.ts +17 -8
  28. package/src/harness/stages/implement-apply.ts +61 -4
  29. package/src/harness/stages/validate.ts +108 -0
  30. package/src/lsp/capabilities.ts +9 -12
  31. package/src/lsp/contracts.ts +15 -23
  32. package/src/planning/planning-ask-tool.ts +13 -2
  33. package/src/planning/spec.ts +21 -27
  34. package/src/planning/system-prompt.ts +1 -1
  35. package/src/planning/validate.ts +4 -7
  36. package/src/platform/progress.ts +11 -0
  37. package/src/quality/contracts.ts +15 -23
  38. package/src/quality/schemas.ts +40 -67
  39. package/src/release/contracts.ts +19 -28
  40. package/src/review/types.ts +142 -186
  41. package/src/types.ts +45 -2
  42. package/src/ui-design/session.ts +13 -2
  43. package/src/ui-design/system-prompt.ts +2 -2
  44. package/src/ultraplan/contracts.ts +458 -524
@@ -0,0 +1,193 @@
1
+ export const RUNBOOK_EXTENSION_NAME = "supipowers-runbook";
2
+ export const RUNBOOK_EXTENSION_PATH = ".omp/extensions/supipowers-runbook.ts";
3
+
4
+ export const RUNBOOK_EXTENSION_SOURCE = `import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { basename, extname, join } from "node:path";
6
+
7
+ type RuleBucket = "ttsr" | "always" | "rulebook" | "inactive";
8
+
9
+ interface RuleInfo {
10
+ name: string;
11
+ description: string | null;
12
+ condition: string[];
13
+ triggers: string[];
14
+ scope: string[];
15
+ alwaysApply: boolean;
16
+ source: string;
17
+ bucket: RuleBucket;
18
+ }
19
+
20
+ function decodeScalar(raw: string): string {
21
+ const value = raw.trim();
22
+ if (value.length === 0) return "";
23
+ if (value.startsWith('"')) {
24
+ try {
25
+ return JSON.parse(value);
26
+ } catch {
27
+ return value.slice(1, value.endsWith('"') ? -1 : undefined);
28
+ }
29
+ }
30
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
31
+ return value;
32
+ }
33
+
34
+ function parseFrontmatter(text: string): { metadata: Record<string, string | string[]>; body: string } {
35
+ if (!text.startsWith("---\n")) return { metadata: {}, body: text.trim() };
36
+ const close = text.indexOf("\n---", 4);
37
+ if (close === -1) return { metadata: {}, body: text.trim() };
38
+ const raw = text.slice(4, close);
39
+ const bodyStart = text.indexOf("\n", close + 4);
40
+ const body = bodyStart === -1 ? "" : text.slice(bodyStart + 1).trim();
41
+ const metadata: Record<string, string | string[]> = {};
42
+ let currentKey: string | null = null;
43
+
44
+ for (const line of raw.split(/\r?\n/)) {
45
+ const keyMatch = /^(\\w+):\\s*(.*)$/.exec(line);
46
+ if (keyMatch) {
47
+ currentKey = keyMatch[1];
48
+ const value = keyMatch[2].trim();
49
+ metadata[currentKey] = value.length === 0 ? [] : decodeScalar(value);
50
+ continue;
51
+ }
52
+ const listMatch = /^\\s*-\\s*(.*)$/.exec(line);
53
+ if (listMatch && currentKey) {
54
+ const existing = metadata[currentKey];
55
+ const values = Array.isArray(existing) ? existing : existing ? [existing] : [];
56
+ values.push(decodeScalar(listMatch[1]));
57
+ metadata[currentKey] = values;
58
+ }
59
+ }
60
+
61
+ return { metadata, body };
62
+ }
63
+
64
+ function asList(value: string | string[] | undefined): string[] {
65
+ if (Array.isArray(value)) return value.filter(Boolean);
66
+ if (!value) return [];
67
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
68
+ }
69
+
70
+ function nameFromPath(filePath: string): string {
71
+ const base = basename(filePath);
72
+ const ext = extname(base);
73
+ return ext ? base.slice(0, -ext.length) : base;
74
+ }
75
+
76
+ function bucket(rule: Pick<RuleInfo, "condition" | "alwaysApply" | "description">): RuleBucket {
77
+ if (rule.condition.length > 0) return "ttsr";
78
+ if (rule.alwaysApply) return "always";
79
+ if (rule.description) return "rulebook";
80
+ return "inactive";
81
+ }
82
+
83
+ function discoverRules(cwd: string): RuleInfo[] {
84
+ const dir = join(cwd, ".omp", "rules");
85
+ if (!existsSync(dir)) return [];
86
+ const rules: RuleInfo[] = [];
87
+ for (const entry of readdirSync(dir).sort()) {
88
+ const filePath = join(dir, entry);
89
+ if (![".md", ".mdc"].includes(extname(filePath))) continue;
90
+ try {
91
+ if (!statSync(filePath).isFile()) continue;
92
+ const parsed = parseFrontmatter(readFileSync(filePath, "utf8"));
93
+ const info: RuleInfo = {
94
+ name: nameFromPath(filePath),
95
+ description: typeof parsed.metadata.description === "string" ? parsed.metadata.description : null,
96
+ condition: asList(parsed.metadata.condition),
97
+ triggers: asList(parsed.metadata.triggers ?? parsed.metadata.triggerDescription),
98
+ scope: asList(parsed.metadata.scope),
99
+ alwaysApply: parsed.metadata.alwaysApply === "true",
100
+ source: filePath,
101
+ bucket: "inactive",
102
+ };
103
+ info.bucket = bucket(info);
104
+ rules.push(info);
105
+ } catch {
106
+ // Keep runbook display best-effort; unreadable rules should not break the command.
107
+ }
108
+ }
109
+ return rules;
110
+ }
111
+
112
+ function describeScope(rule: RuleInfo): string {
113
+ if (rule.scope.length === 0) return "assistant prose and tool-call text";
114
+ const labels = rule.scope.map((scope) => {
115
+ const normalized = scope.toLowerCase();
116
+ if (normalized === "text") return "assistant prose";
117
+ if (normalized === "thinking") return "assistant thinking";
118
+ if (normalized === "tool" || normalized === "toolcall") return "all tool-call text";
119
+ return "tool scope " + scope;
120
+ });
121
+ return labels.join(", ") + " only";
122
+ }
123
+
124
+ function formatRule(rule: RuleInfo): string[] {
125
+ const lines = [" " + rule.name];
126
+ if (rule.description) lines.push(" Description: " + rule.description);
127
+ if (rule.bucket === "ttsr") {
128
+ lines.push(" Applies: when assistant output matches the trigger phrase(s)");
129
+ if (rule.triggers.length > 0) {
130
+ lines.push(" Triggers: " + rule.triggers.join(", "));
131
+ } else {
132
+ lines.push(" Triggers: exact regex only; add triggers: frontmatter for readability");
133
+ for (const condition of rule.condition) lines.push(" - " + condition);
134
+ }
135
+ lines.push(" Scope: " + describeScope(rule));
136
+ } else if (rule.bucket === "always") {
137
+ lines.push(" Applies: always injected at session start");
138
+ } else if (rule.bucket === "rulebook") {
139
+ lines.push(" Applies: on demand via rule://" + rule.name);
140
+ } else {
141
+ lines.push(" Applies: inactive in prompt surfaces");
142
+ }
143
+ return lines;
144
+ }
145
+
146
+ function formatRules(cwd: string, onlyTtsr: boolean): string {
147
+ const rules = discoverRules(cwd).filter((rule) => !onlyTtsr || rule.bucket === "ttsr");
148
+ const lines = [onlyTtsr ? "/runbook rules ttsr" : "/runbook rules", "", "Rules: " + rules.length, ""];
149
+ if (rules.length === 0) return [...lines, " none"].join("\\n");
150
+ for (const rule of rules.sort((a, b) => a.name.localeCompare(b.name))) {
151
+ lines.push(...formatRule(rule), "");
152
+ }
153
+ return lines.join("\\n").trimEnd();
154
+ }
155
+
156
+ function formatCommands(api: any): string {
157
+ const commands = typeof api.getCommands === "function" ? api.getCommands() : [];
158
+ const lines = ["/runbook commands", "", "Registered slash commands: " + commands.length, ""];
159
+ for (const command of [...commands].sort((a: any, b: any) => String(a.name).localeCompare(String(b.name)))) {
160
+ lines.push(" /" + command.name, " " + (command.description ?? "No description"));
161
+ }
162
+ return lines.join("\\n");
163
+ }
164
+
165
+ function buildReport(api: any, cwd: string, args?: string): string {
166
+ const tokens = (args ?? "").trim().split(/\\s+/).filter(Boolean).map((token) => token.toLowerCase());
167
+ if (tokens[0] === "commands" || tokens[1] === "commands") return formatCommands(api);
168
+ if (tokens[0] === "ttsr" || tokens[1] === "ttsr") return formatRules(cwd, true);
169
+ return formatRules(cwd, false);
170
+ }
171
+
172
+ export default function supipowersRunbook(api: any): void {
173
+ const handle = (args: string | undefined, ctx: any): void => {
174
+ if (!ctx?.hasUI || !ctx.ui?.notify) return;
175
+ ctx.ui.notify(buildReport(api, ctx.cwd ?? process.cwd(), args), "info");
176
+ };
177
+
178
+ api.registerCommand?.("runbook", {
179
+ description: "Show project rules, TTSR triggers, and slash commands without an LLM turn",
180
+ async handler(args: string | undefined, ctx: any): Promise<void> {
181
+ handle(args, ctx);
182
+ },
183
+ });
184
+
185
+ api.on?.("input", (event: any, ctx: any) => {
186
+ const text = String(event?.text ?? "").trim();
187
+ if (!text.startsWith("/runbook")) return;
188
+ const args = text.length > "/runbook".length ? text.slice("/runbook".length).trim() : undefined;
189
+ handle(args, ctx);
190
+ return { handled: true };
191
+ });
192
+ }
193
+ `;
@@ -1,5 +1,5 @@
1
1
  import type { ParsedSkill, PromptSection } from "./analyzer.js";
2
- import { parseManagedRule } from "./rule-renderer.js";
2
+ import { MANAGED_COMMAND_HEADER, parseManagedCommand, parseManagedExtension, parseManagedRule } from "./rule-renderer.js";
3
3
  import {
4
4
  hashOptimizationSource,
5
5
  type ManualOptimizationAction,
@@ -16,9 +16,32 @@ export interface StartupOptimizerManifestRule {
16
16
  slug: string;
17
17
  sourceBytes: number;
18
18
  condition?: string;
19
+ triggers?: string;
20
+ scope?: string;
19
21
  description?: string;
20
22
  }
21
23
 
24
+ export interface StartupOptimizerManifestCommand {
25
+ path: string;
26
+ sourceId: string;
27
+ sourceName: string;
28
+ sourceHash: string;
29
+ slug: string;
30
+ commandName: string;
31
+ sourceBytes: number;
32
+ description?: string;
33
+ }
34
+
35
+ export interface StartupOptimizerManifestExtension {
36
+ path: string;
37
+ sourceId: string;
38
+ sourceName: string;
39
+ sourceHash: string;
40
+ slug: string;
41
+ extensionName: string;
42
+ sourceBytes: number;
43
+ }
44
+
22
45
  export interface StartupOptimizerManifest {
23
46
  version: 1;
24
47
  targetBytes: number;
@@ -27,6 +50,8 @@ export interface StartupOptimizerManifest {
27
50
  estimatedAfterBytes: number;
28
51
  estimatedSavedBytes: number;
29
52
  rules: StartupOptimizerManifestRule[];
53
+ commands: StartupOptimizerManifestCommand[];
54
+ extensions: StartupOptimizerManifestExtension[];
30
55
  tokenignore: {
31
56
  path: string;
32
57
  entries: string[];
@@ -46,6 +71,16 @@ export type StartupCheckReason =
46
71
  | "rule-drift"
47
72
  | "rule-body-drift"
48
73
  | "tokenignore-drift"
74
+ | "missing-command"
75
+ | "unmanaged-command"
76
+ | "malformed-command"
77
+ | "command-drift"
78
+ | "command-body-drift"
79
+ | "missing-extension"
80
+ | "unmanaged-extension"
81
+ | "malformed-extension"
82
+ | "extension-drift"
83
+ | "extension-body-drift"
49
84
  | "still-loaded-source"
50
85
  | "unresolved-manual-action"
51
86
  | "prompt-over-target"
@@ -64,6 +99,8 @@ export interface StartupCheckInput {
64
99
  manifestPath: string;
65
100
  manifestText: string | null | undefined;
66
101
  ruleFiles: Record<string, string | null | undefined>;
102
+ commandFiles?: Record<string, string | null | undefined>;
103
+ extensionFiles?: Record<string, string | null | undefined>;
67
104
  tokenignorePath: string;
68
105
  tokenignoreText: string | null | undefined;
69
106
  currentPrompt: string | null | undefined;
@@ -153,7 +190,9 @@ export function runStartupCheck(input: StartupCheckInput): StartupCheckReport {
153
190
 
154
191
  const metadata = parsed.metadata;
155
192
  const frontmatterDrift = rule.mode === "ttsr"
156
- ? parsed.frontmatter.condition !== rule.condition
193
+ ? parsed.frontmatter.condition !== rule.condition ||
194
+ (typeof rule.triggers === "string" && parsed.frontmatter.triggers !== rule.triggers) ||
195
+ parsed.frontmatter.scope !== (rule.scope ?? "text")
157
196
  : (typeof rule.description === "string" && parsed.frontmatter.description !== rule.description);
158
197
 
159
198
  if (
@@ -187,6 +226,133 @@ export function runStartupCheck(input: StartupCheckInput): StartupCheckReport {
187
226
  }
188
227
  }
189
228
 
229
+
230
+ for (const command of manifest.commands) {
231
+ const text = input.commandFiles?.[command.path];
232
+ if (text == null) {
233
+ issues.push(issue("missing-command", {
234
+ path: command.path,
235
+ sourceId: command.sourceId,
236
+ message: `Managed command file is missing for ${command.sourceId}.`,
237
+ remediation: "Rerun /supi:optimize-context --apply to regenerate managed commands.",
238
+ }));
239
+ continue;
240
+ }
241
+
242
+ const parsed = parseManagedCommand(text);
243
+ if (parsed.status === "unmanaged") {
244
+ issues.push(issue("unmanaged-command", {
245
+ path: command.path,
246
+ sourceId: command.sourceId,
247
+ message: `Command file ${command.path} is not managed by supipowers.`,
248
+ remediation: "Move the user-authored command aside or choose a different command name before applying again.",
249
+ }));
250
+ continue;
251
+ }
252
+
253
+ if (parsed.status === "malformed") {
254
+ issues.push(issue("malformed-command", {
255
+ path: command.path,
256
+ sourceId: command.sourceId,
257
+ message: `Managed command ${command.path} is malformed: ${parsed.error}.`,
258
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed command.",
259
+ }));
260
+ continue;
261
+ }
262
+
263
+ // Legacy managed commands stored supipowers metadata before command
264
+ // frontmatter. OMP sends that prefix as prompt text, so force a refresh.
265
+ const commandDrift =
266
+ text.startsWith(MANAGED_COMMAND_HEADER) ||
267
+ parsed.metadata.sourceId !== command.sourceId ||
268
+ parsed.metadata.sourceHash !== command.sourceHash ||
269
+ parsed.metadata.slug !== command.slug ||
270
+ parsed.metadata.commandName !== command.commandName ||
271
+ parsed.metadata.sourceBytes !== command.sourceBytes ||
272
+ (typeof command.description === "string" && parsed.frontmatter.description !== command.description);
273
+ if (commandDrift) {
274
+ issues.push(issue("command-drift", {
275
+ path: command.path,
276
+ sourceId: command.sourceId,
277
+ message: `Managed command ${command.path} no longer matches the startup optimizer manifest.`,
278
+ remediation: "Rerun /supi:optimize-context --apply to refresh managed command artifacts.",
279
+ }));
280
+ continue;
281
+ }
282
+
283
+ const actualBodyHash = hashOptimizationSource(parsed.body);
284
+ const actualBodyBytes = byteLength(parsed.body);
285
+ if (actualBodyHash !== command.sourceHash || actualBodyBytes !== command.sourceBytes) {
286
+ issues.push(issue("command-body-drift", {
287
+ path: command.path,
288
+ sourceId: command.sourceId,
289
+ message: `Managed command ${command.path} body has been modified (hash/size no longer matches the manifest).`,
290
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed command from the current prompt source.",
291
+ }));
292
+ }
293
+ }
294
+
295
+ for (const extension of manifest.extensions) {
296
+ const text = input.extensionFiles?.[extension.path];
297
+ if (text == null) {
298
+ issues.push(issue("missing-extension", {
299
+ path: extension.path,
300
+ sourceId: extension.sourceId,
301
+ message: `Managed extension file is missing for ${extension.sourceId}.`,
302
+ remediation: "Rerun /supi:optimize-context --apply to regenerate managed extensions.",
303
+ }));
304
+ continue;
305
+ }
306
+
307
+ const parsed = parseManagedExtension(text);
308
+ if (parsed.status === "unmanaged") {
309
+ issues.push(issue("unmanaged-extension", {
310
+ path: extension.path,
311
+ sourceId: extension.sourceId,
312
+ message: `Extension file ${extension.path} is not managed by supipowers.`,
313
+ remediation: "Move the user-authored extension aside or choose a different extension name before applying again.",
314
+ }));
315
+ continue;
316
+ }
317
+
318
+ if (parsed.status === "malformed") {
319
+ issues.push(issue("malformed-extension", {
320
+ path: extension.path,
321
+ sourceId: extension.sourceId,
322
+ message: `Managed extension ${extension.path} is malformed: ${parsed.error}.`,
323
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed extension.",
324
+ }));
325
+ continue;
326
+ }
327
+
328
+ const extensionDrift =
329
+ parsed.metadata.sourceId !== extension.sourceId ||
330
+ parsed.metadata.sourceHash !== extension.sourceHash ||
331
+ parsed.metadata.slug !== extension.slug ||
332
+ parsed.metadata.extensionName !== extension.extensionName ||
333
+ parsed.metadata.sourceBytes !== extension.sourceBytes;
334
+ if (extensionDrift) {
335
+ issues.push(issue("extension-drift", {
336
+ path: extension.path,
337
+ sourceId: extension.sourceId,
338
+ message: `Managed extension ${extension.path} no longer matches the startup optimizer manifest.`,
339
+ remediation: "Rerun /supi:optimize-context --apply to refresh managed extension artifacts.",
340
+ }));
341
+ continue;
342
+ }
343
+
344
+ const actualBodyHash = hashOptimizationSource(parsed.body);
345
+ const actualBodyBytes = byteLength(parsed.body);
346
+ if (actualBodyHash !== extension.sourceHash || actualBodyBytes !== extension.sourceBytes) {
347
+ issues.push(issue("extension-body-drift", {
348
+ path: extension.path,
349
+ sourceId: extension.sourceId,
350
+ message: `Managed extension ${extension.path} body has been modified (hash/size no longer matches the manifest).`,
351
+ remediation: "Rerun /supi:optimize-context --apply to rewrite the managed extension from the current optimizer template.",
352
+ }));
353
+ }
354
+ }
355
+
190
356
  const tokenignore = parseManagedTokenignore(input.tokenignoreText);
191
357
  if (
192
358
  tokenignore.status !== "managed" ||
@@ -213,6 +379,10 @@ export function runStartupCheck(input: StartupCheckInput): StartupCheckReport {
213
379
  if (rule.sourceId.startsWith("skill:")) skillSourceIdsToVerify.add(rule.sourceId);
214
380
  else if (rule.sourceId.startsWith("section:")) sectionSourceIdsToVerify.add(rule.sourceId);
215
381
  }
382
+ for (const command of manifest.commands) {
383
+ if (command.sourceId.startsWith("skill:")) skillSourceIdsToVerify.add(command.sourceId);
384
+ else if (command.sourceId.startsWith("section:")) sectionSourceIdsToVerify.add(command.sourceId);
385
+ }
216
386
  for (const action of manifest.manualActions) {
217
387
  if (action.kind !== "manual-disable") continue;
218
388
  if (action.sourceId.startsWith("skill:")) skillSourceIdsToVerify.add(action.sourceId);
@@ -310,6 +480,29 @@ export function parseStartupOptimizerManifest(
310
480
  rules.push(candidate as unknown as StartupOptimizerManifestRule);
311
481
  }
312
482
 
483
+ const commands: StartupOptimizerManifestCommand[] = [];
484
+ const rawCommands = Array.isArray((parsed as any).commands) ? (parsed as any).commands : [];
485
+ for (const candidate of rawCommands) {
486
+ if (!isRecord(candidate)) return "Startup optimizer manifest has invalid command entry.";
487
+ for (const key of ["path", "sourceId", "sourceName", "sourceHash", "slug", "commandName"] as const) {
488
+ if (typeof candidate[key] !== "string") return `Startup optimizer manifest command is missing ${key}.`;
489
+ }
490
+ if (!isFiniteNumber(candidate.sourceBytes)) return "Startup optimizer manifest command is missing sourceBytes.";
491
+ commands.push(candidate as unknown as StartupOptimizerManifestCommand);
492
+ }
493
+
494
+ const extensions: StartupOptimizerManifestExtension[] = [];
495
+ const rawExtensions = Array.isArray((parsed as any).extensions) ? (parsed as any).extensions : [];
496
+ for (const candidate of rawExtensions) {
497
+ if (!isRecord(candidate)) return "Startup optimizer manifest has invalid extension entry.";
498
+ for (const key of ["path", "sourceId", "sourceName", "sourceHash", "slug", "extensionName"] as const) {
499
+ if (typeof candidate[key] !== "string") return `Startup optimizer manifest extension is missing ${key}.`;
500
+ }
501
+ if (!isFiniteNumber(candidate.sourceBytes)) return "Startup optimizer manifest extension is missing sourceBytes.";
502
+ extensions.push(candidate as unknown as StartupOptimizerManifestExtension);
503
+ }
504
+
505
+
313
506
  if (typeof parsed.tokenignore.path !== "string") return "Startup optimizer manifest tokenignore is missing path.";
314
507
  if (!Array.isArray(parsed.tokenignore.entries) || !parsed.tokenignore.entries.every((entry) => typeof entry === "string")) {
315
508
  return "Startup optimizer manifest tokenignore has invalid entries.";
@@ -324,6 +517,8 @@ export function parseStartupOptimizerManifest(
324
517
  estimatedAfterBytes: parsed.estimatedAfterBytes,
325
518
  estimatedSavedBytes: parsed.estimatedSavedBytes,
326
519
  rules,
520
+ commands,
521
+ extensions,
327
522
  tokenignore: {
328
523
  path: parsed.tokenignore.path,
329
524
  entries: parsed.tokenignore.entries,
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { ParsedSkill, PromptSection } from "./analyzer.js";
3
+ import { RUNBOOK_EXTENSION_NAME, RUNBOOK_EXTENSION_PATH, RUNBOOK_EXTENSION_SOURCE } from "./runbook-extension-template.js";
3
4
  import type { TechStack } from "./optimizer.js";
4
5
 
5
6
  export const STARTUP_OPTIMIZER_MANIFEST_VERSION = 1;
@@ -33,6 +34,35 @@ export interface WriteRuleAction {
33
34
  sourceContent: string;
34
35
  condition?: string;
35
36
  description?: string;
37
+ triggers?: string;
38
+ scope?: string;
39
+ }
40
+
41
+ export interface WriteCommandAction {
42
+ kind: "write-command";
43
+ sourceId: string;
44
+ sourceName: string;
45
+ sourceHash: string;
46
+ slug: string;
47
+ commandName: string;
48
+ targetPath: string;
49
+ sourceBytes: number;
50
+ estimatedSavedBytes: number;
51
+ sourceContent: string;
52
+ description?: string;
53
+ }
54
+
55
+ export interface WriteExtensionAction {
56
+ kind: "write-extension";
57
+ sourceId: string;
58
+ sourceName: string;
59
+ sourceHash: string;
60
+ slug: string;
61
+ extensionName: string;
62
+ targetPath: string;
63
+ sourceBytes: number;
64
+ estimatedSavedBytes: number;
65
+ sourceContent: string;
36
66
  }
37
67
 
38
68
  export type ManualDisableReason = "source-still-loaded" | "tech-stack-irrelevant";
@@ -58,7 +88,7 @@ export interface ManualAgentsSplitAction {
58
88
  }
59
89
 
60
90
  export type ManualOptimizationAction = ManualDisableAction | ManualAgentsSplitAction;
61
- export type OptimizationAction = WriteRuleAction | ManualOptimizationAction;
91
+ export type OptimizationAction = WriteRuleAction | WriteCommandAction | WriteExtensionAction | ManualOptimizationAction;
62
92
 
63
93
  export interface OptimizationWarning {
64
94
  code: string;
@@ -91,27 +121,44 @@ export interface BuildOptimizationPlanInput {
91
121
  techStack: TechStack;
92
122
  }
93
123
 
124
+ interface TtsrRuleOptions {
125
+ condition: string;
126
+ triggers?: string;
127
+ scope?: string;
128
+ }
129
+
130
+
94
131
  interface BehaviorSkillSpec {
95
132
  mode: "ttsr";
96
133
  condition: string;
134
+ triggers: string;
135
+ scope: string;
97
136
  }
98
137
 
99
138
  const BEHAVIOR_SKILLS: Record<string, BehaviorSkillSpec> = {
100
139
  debugging: {
101
140
  mode: "ttsr",
102
141
  condition: String.raw`\b(?:debug(?:ging)?|root\s+cause|investigate|repro(?:duce|duction)?|failing\s+test)\b`,
142
+ triggers: "debugging, root cause, investigate, reproduce, failing test",
143
+ scope: "text",
103
144
  },
104
145
  tdd: {
105
146
  mode: "ttsr",
106
147
  condition: String.raw`\b(?:tdd|test\s+first|failing\s+test\s+first|red[-\s]+green[-\s]+refactor)\b`,
148
+ triggers: "TDD, test first, failing test first, red-green-refactor",
149
+ scope: "text",
107
150
  },
108
151
  verification: {
109
152
  mode: "ttsr",
110
153
  condition: String.raw`\b(?:verify|verification|evidence|prove|proof|run\s+(?:the\s+)?(?:focused\s+)?tests?)\b`,
154
+ triggers: "verify, verification, evidence, proof, run focused tests",
155
+ scope: "text",
111
156
  },
112
157
  "receiving-code-review": {
113
158
  mode: "ttsr",
114
159
  condition: String.raw`\b(?:pr\s+feedback|code\s+review\s+comments?|reviewer\s+feedback|review\s+comments?)\b`,
160
+ triggers: "PR feedback, code review comments, reviewer feedback",
161
+ scope: "text",
115
162
  },
116
163
  };
117
164
 
@@ -130,10 +177,40 @@ const TECH_STACK_SKILLS: Record<string, { anyOf: Array<keyof TechStack>; values:
130
177
  },
131
178
  };
132
179
 
180
+ const WORKFLOW_COMMAND_SKILLS = new Set([
181
+ "rewrite-page-copy",
182
+ "e2e-test-orchestrator",
183
+ "design-refiner",
184
+ "workflow-extractor",
185
+ "engaging-writing",
186
+ "brand-namer",
187
+ "brand-forge",
188
+ "product-linear-issues",
189
+ "skill-auditor",
190
+ "caveman",
191
+ "caveman-compress",
192
+ "security-threat-model",
193
+ "brand-migrate",
194
+ "audit",
195
+ "setup-tyndale",
196
+ "setup-tyndale-local",
197
+ "writing-commands",
198
+ "playwright",
199
+ "ui-design",
200
+ "quick-setup",
201
+ "planning",
202
+ "fix-pr",
203
+ "create-readme",
204
+ "translate-book",
205
+ ]);
206
+
207
+
133
208
  const ACTION_KIND_ORDER: Record<OptimizationAction["kind"], number> = {
134
209
  "write-rule": 0,
135
- "manual-disable": 1,
136
- "manual-agents-split": 2,
210
+ "write-command": 1,
211
+ "write-extension": 2,
212
+ "manual-disable": 3,
213
+ "manual-agents-split": 4,
137
214
  };
138
215
 
139
216
  export function hashOptimizationSource(content: string): string {
@@ -162,12 +239,24 @@ export function buildOptimizationPlan(input: BuildOptimizationPlanInput): Optimi
162
239
  const actions: OptimizationAction[] = [];
163
240
  const warnings: OptimizationWarning[] = [];
164
241
 
242
+ actions.push(buildRunbookExtensionAction());
243
+
165
244
  for (const source of sources) {
166
245
  if (source.sourceType === "skill") {
167
246
  const canonicalName = source.sourceName.toLowerCase();
168
247
  const behavior = BEHAVIOR_SKILLS[canonicalName];
169
248
  if (behavior) {
170
- actions.push(buildWriteRuleAction(source, behavior.mode, behavior.condition));
249
+ actions.push(buildWriteRuleAction(source, behavior.mode, {
250
+ condition: behavior.condition,
251
+ triggers: behavior.triggers,
252
+ scope: behavior.scope,
253
+ }));
254
+ actions.push(buildManualDisableAction(source, "source-still-loaded"));
255
+ continue;
256
+ }
257
+
258
+ if (WORKFLOW_COMMAND_SKILLS.has(canonicalName)) {
259
+ actions.push(buildWriteCommandAction(source));
171
260
  actions.push(buildManualDisableAction(source, "source-still-loaded"));
172
261
  continue;
173
262
  }
@@ -202,12 +291,12 @@ export function buildOptimizationPlan(input: BuildOptimizationPlanInput): Optimi
202
291
  const beforeBytes = byteLength(input.prompt);
203
292
 
204
293
  // Sources whose content the user is expected to actually remove from the
205
- // startup prompt: write-rule companions and tech-stack manual-disable
206
- // actions. Deduplicated by sourceId because a write-rule action is paired
207
- // with a `source-still-loaded` manual-disable for the same source.
294
+ // startup prompt: write-rule companions, write-command companions, and
295
+ // manual-disable actions. Deduplicated by sourceId because generated artifacts
296
+ // are paired with `source-still-loaded` manual-disable records.
208
297
  const removedSourceIds = new Set<string>();
209
298
  for (const action of actions) {
210
- if (action.kind === "write-rule" || action.kind === "manual-disable") {
299
+ if (action.kind === "write-rule" || action.kind === "write-command" || action.kind === "manual-disable") {
211
300
  removedSourceIds.add(action.sourceId);
212
301
  }
213
302
  }
@@ -289,7 +378,7 @@ function canonicalSourceContent(content: string): string {
289
378
  function buildWriteRuleAction(
290
379
  source: OptimizationSource,
291
380
  mode: RuleMode,
292
- condition?: string,
381
+ ttsr?: TtsrRuleOptions,
293
382
  ): WriteRuleAction {
294
383
  return {
295
384
  kind: "write-rule",
@@ -302,7 +391,41 @@ function buildWriteRuleAction(
302
391
  sourceBytes: source.bytes,
303
392
  estimatedSavedBytes: source.bytes,
304
393
  sourceContent: source.content,
305
- ...(condition ? { condition } : {}),
394
+ ...(ttsr ? { condition: ttsr.condition, triggers: ttsr.triggers, scope: ttsr.scope } : {}),
395
+ };
396
+ }
397
+
398
+ function buildWriteCommandAction(source: OptimizationSource): WriteCommandAction {
399
+ const commandName = slugifyOptimizationSource(source.sourceName);
400
+ return {
401
+ kind: "write-command",
402
+ sourceId: source.sourceId,
403
+ sourceName: source.sourceName,
404
+ sourceHash: source.sourceHash,
405
+ slug: source.slug,
406
+ commandName,
407
+ targetPath: `.omp/commands/${commandName}.md`,
408
+ sourceBytes: source.bytes,
409
+ estimatedSavedBytes: source.bytes,
410
+ sourceContent: source.content,
411
+ description: `Run the ${source.sourceName} workflow on demand.`,
412
+ };
413
+ }
414
+
415
+ function buildRunbookExtensionAction(): WriteExtensionAction {
416
+ const sourceContent = canonicalSourceContent(RUNBOOK_EXTENSION_SOURCE);
417
+ const sourceBytes = byteLength(sourceContent);
418
+ return {
419
+ kind: "write-extension",
420
+ sourceId: "extension:runbook",
421
+ sourceName: RUNBOOK_EXTENSION_NAME,
422
+ sourceHash: hashOptimizationSource(sourceContent),
423
+ slug: "extension-runbook",
424
+ extensionName: RUNBOOK_EXTENSION_NAME,
425
+ targetPath: RUNBOOK_EXTENSION_PATH,
426
+ sourceBytes,
427
+ estimatedSavedBytes: 0,
428
+ sourceContent,
306
429
  };
307
430
  }
308
431