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.
- package/README.md +71 -12
- package/package.json +4 -8
- package/skills/ui-design/SKILL.md +2 -2
- package/src/ai/final-message.ts +15 -1
- package/src/ai/schema-text.ts +60 -40
- package/src/ai/schema-validation.ts +88 -0
- package/src/ai/structured-output.ts +19 -19
- package/src/bootstrap.ts +3 -0
- package/src/commands/fix-pr.ts +166 -26
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- package/src/config/schema.ts +102 -139
- package/src/context/rule-renderer.ts +274 -2
- package/src/context/runbook-extension-template.ts +193 -0
- package/src/context/startup-check.ts +197 -2
- package/src/context/startup-optimizer.ts +133 -10
- package/src/docs/contracts.ts +13 -23
- package/src/fix-pr/assessment.ts +63 -24
- package/src/fix-pr/contracts.ts +15 -23
- package/src/fix-pr/fetch-comments.ts +119 -0
- package/src/fix-pr/prompt-builder.ts +19 -8
- package/src/git/commit-contract.ts +13 -19
- package/src/git/commit.ts +168 -6
- package/src/harness/command.ts +98 -6
- package/src/harness/git-verification.ts +515 -0
- package/src/harness/git-verify-qa.ts +406 -0
- package/src/harness/pipeline.ts +17 -8
- package/src/harness/stages/implement-apply.ts +61 -4
- package/src/harness/stages/validate.ts +108 -0
- package/src/lsp/capabilities.ts +9 -12
- package/src/lsp/contracts.ts +15 -23
- package/src/planning/planning-ask-tool.ts +13 -2
- package/src/planning/spec.ts +21 -27
- package/src/planning/system-prompt.ts +1 -1
- package/src/planning/validate.ts +4 -7
- package/src/platform/progress.ts +11 -0
- package/src/quality/contracts.ts +15 -23
- package/src/quality/schemas.ts +40 -67
- package/src/release/contracts.ts +19 -28
- package/src/review/types.ts +142 -186
- package/src/types.ts +45 -2
- package/src/ui-design/session.ts +13 -2
- package/src/ui-design/system-prompt.ts +2 -2
- 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
|
-
"
|
|
136
|
-
"
|
|
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,
|
|
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
|
|
206
|
-
// actions. Deduplicated by sourceId because
|
|
207
|
-
// with
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|