supipowers 2.1.0 → 2.2.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.
- package/package.json +1 -1
- package/src/bootstrap.ts +3 -0
- package/src/commands/optimize-context.ts +153 -16
- package/src/commands/runbook.ts +511 -0
- 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/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/types.ts +40 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type { RuleMode, WriteRuleAction } from "./startup-optimizer.js";
|
|
1
|
+
import type { RuleMode, WriteCommandAction, WriteExtensionAction, WriteRuleAction } from "./startup-optimizer.js";
|
|
2
2
|
|
|
3
3
|
export const MANAGED_RULE_HEADER = "<!-- supipowers:managed-rule";
|
|
4
4
|
export const MANAGED_RULE_END = "-->";
|
|
5
|
+
export const MANAGED_COMMAND_HEADER = "<!-- supipowers:managed-command";
|
|
6
|
+
const MANAGED_COMMAND_FRONTMATTER_KEY = "supipowers-managed-command";
|
|
7
|
+
const MANAGED_COMMAND_FRONTMATTER_VERSION = "1";
|
|
8
|
+
export const MANAGED_EXTENSION_HEADER = "/* supipowers:managed-extension";
|
|
9
|
+
export const MANAGED_EXTENSION_END = "*/";
|
|
5
10
|
|
|
6
11
|
export interface ManagedRuleMetadata {
|
|
7
12
|
version: number;
|
|
@@ -13,6 +18,26 @@ export interface ManagedRuleMetadata {
|
|
|
13
18
|
sourceBytes: number;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
export interface ManagedCommandMetadata {
|
|
22
|
+
version: number;
|
|
23
|
+
sourceId: string;
|
|
24
|
+
sourceName: string;
|
|
25
|
+
sourceHash: string;
|
|
26
|
+
slug: string;
|
|
27
|
+
commandName: string;
|
|
28
|
+
sourceBytes: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ManagedExtensionMetadata {
|
|
32
|
+
version: number;
|
|
33
|
+
sourceId: string;
|
|
34
|
+
sourceName: string;
|
|
35
|
+
sourceHash: string;
|
|
36
|
+
slug: string;
|
|
37
|
+
extensionName: string;
|
|
38
|
+
sourceBytes: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
export type ParsedManagedRule =
|
|
17
42
|
| { status: "unmanaged"; managed: false }
|
|
18
43
|
| {
|
|
@@ -29,6 +54,27 @@ export type ParsedManagedRule =
|
|
|
29
54
|
}
|
|
30
55
|
| { status: "malformed"; managed: true; error: string };
|
|
31
56
|
|
|
57
|
+
export type ParsedManagedCommand =
|
|
58
|
+
| { status: "unmanaged"; managed: false }
|
|
59
|
+
| {
|
|
60
|
+
status: "managed";
|
|
61
|
+
managed: true;
|
|
62
|
+
metadata: ManagedCommandMetadata;
|
|
63
|
+
frontmatter: Record<string, string>;
|
|
64
|
+
body: string;
|
|
65
|
+
}
|
|
66
|
+
| { status: "malformed"; managed: true; error: string };
|
|
67
|
+
|
|
68
|
+
export type ParsedManagedExtension =
|
|
69
|
+
| { status: "unmanaged"; managed: false }
|
|
70
|
+
| {
|
|
71
|
+
status: "managed";
|
|
72
|
+
managed: true;
|
|
73
|
+
metadata: ManagedExtensionMetadata;
|
|
74
|
+
body: string;
|
|
75
|
+
}
|
|
76
|
+
| { status: "malformed"; managed: true; error: string };
|
|
77
|
+
|
|
32
78
|
/**
|
|
33
79
|
* Render a managed rule file.
|
|
34
80
|
*
|
|
@@ -56,6 +102,47 @@ export function renderManagedRule(action: WriteRuleAction): string {
|
|
|
56
102
|
return `${metadata}\n---\n${frontmatter}\n---\n${body}`;
|
|
57
103
|
}
|
|
58
104
|
|
|
105
|
+
export function renderManagedCommand(action: WriteCommandAction): string {
|
|
106
|
+
const description = action.description ?? `Run ${action.sourceName} on demand.`;
|
|
107
|
+
const body = action.sourceContent.endsWith("\n")
|
|
108
|
+
? action.sourceContent
|
|
109
|
+
: `${action.sourceContent}\n`;
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
"---",
|
|
113
|
+
`description: ${frontmatterScalarLiteral(description)}`,
|
|
114
|
+
`${MANAGED_COMMAND_FRONTMATTER_KEY}: ${frontmatterScalarLiteral(MANAGED_COMMAND_FRONTMATTER_VERSION)}`,
|
|
115
|
+
`sourceId: ${frontmatterScalarLiteral(action.sourceId)}`,
|
|
116
|
+
`sourceName: ${frontmatterScalarLiteral(action.sourceName)}`,
|
|
117
|
+
`sourceHash: ${frontmatterScalarLiteral(action.sourceHash)}`,
|
|
118
|
+
`slug: ${frontmatterScalarLiteral(action.slug)}`,
|
|
119
|
+
`commandName: ${frontmatterScalarLiteral(action.commandName)}`,
|
|
120
|
+
`sourceBytes: ${action.sourceBytes}`,
|
|
121
|
+
"---",
|
|
122
|
+
body,
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderManagedExtension(action: WriteExtensionAction): string {
|
|
127
|
+
const metadata = [
|
|
128
|
+
MANAGED_EXTENSION_HEADER,
|
|
129
|
+
"version: 1",
|
|
130
|
+
`sourceId: ${action.sourceId}`,
|
|
131
|
+
`sourceName: ${action.sourceName}`,
|
|
132
|
+
`sourceHash: ${action.sourceHash}`,
|
|
133
|
+
`slug: ${action.slug}`,
|
|
134
|
+
`extensionName: ${action.extensionName}`,
|
|
135
|
+
`sourceBytes: ${action.sourceBytes}`,
|
|
136
|
+
MANAGED_EXTENSION_END,
|
|
137
|
+
].join("\n");
|
|
138
|
+
const body = action.sourceContent.endsWith("\n")
|
|
139
|
+
? action.sourceContent
|
|
140
|
+
: `${action.sourceContent}\n`;
|
|
141
|
+
|
|
142
|
+
return `${metadata}\n${body}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
59
146
|
export function parseManagedRule(text: string): ParsedManagedRule {
|
|
60
147
|
if (!text.startsWith(MANAGED_RULE_HEADER)) {
|
|
61
148
|
return { status: "unmanaged", managed: false };
|
|
@@ -87,12 +174,98 @@ export function parseManagedRule(text: string): ParsedManagedRule {
|
|
|
87
174
|
};
|
|
88
175
|
}
|
|
89
176
|
|
|
177
|
+
export function parseManagedCommand(text: string): ParsedManagedCommand {
|
|
178
|
+
if (text.startsWith(MANAGED_COMMAND_HEADER)) {
|
|
179
|
+
const headerEnd = text.indexOf(MANAGED_RULE_END, MANAGED_COMMAND_HEADER.length);
|
|
180
|
+
if (headerEnd === -1) {
|
|
181
|
+
return { status: "malformed", managed: true, error: "managed header is not closed" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const headerText = text.slice(0, headerEnd).trimEnd();
|
|
185
|
+
const metadataResult = parseCommandMetadata(headerText);
|
|
186
|
+
if (typeof metadataResult === "string") {
|
|
187
|
+
return { status: "malformed", managed: true, error: metadataResult };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const afterHeader = text.slice(headerEnd + MANAGED_RULE_END.length).replace(/^\r?\n/, "");
|
|
191
|
+
const frontmatterResult = parseFrontmatter(afterHeader);
|
|
192
|
+
if (typeof frontmatterResult === "string") {
|
|
193
|
+
return { status: "malformed", managed: true, error: frontmatterResult };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
status: "managed",
|
|
198
|
+
managed: true,
|
|
199
|
+
metadata: metadataResult,
|
|
200
|
+
frontmatter: frontmatterResult.frontmatter,
|
|
201
|
+
body: stripTrailingNewline(frontmatterResult.body),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const frontmatterResult = parseFrontmatter(text);
|
|
206
|
+
if (typeof frontmatterResult === "string") {
|
|
207
|
+
if (
|
|
208
|
+
(text.startsWith("---\n") || text.startsWith("---\r\n")) &&
|
|
209
|
+
text.includes(`${MANAGED_COMMAND_FRONTMATTER_KEY}:`)
|
|
210
|
+
) {
|
|
211
|
+
return { status: "malformed", managed: true, error: frontmatterResult };
|
|
212
|
+
}
|
|
213
|
+
return { status: "unmanaged", managed: false };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!frontmatterResult.frontmatter[MANAGED_COMMAND_FRONTMATTER_KEY]) {
|
|
217
|
+
return { status: "unmanaged", managed: false };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const metadataResult = parseCommandFrontmatterMetadata(frontmatterResult.frontmatter);
|
|
221
|
+
if (typeof metadataResult === "string") {
|
|
222
|
+
return { status: "malformed", managed: true, error: metadataResult };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
status: "managed",
|
|
227
|
+
managed: true,
|
|
228
|
+
metadata: metadataResult,
|
|
229
|
+
frontmatter: frontmatterResult.frontmatter,
|
|
230
|
+
body: stripTrailingNewline(frontmatterResult.body),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function parseManagedExtension(text: string): ParsedManagedExtension {
|
|
235
|
+
if (!text.startsWith(MANAGED_EXTENSION_HEADER)) {
|
|
236
|
+
return { status: "unmanaged", managed: false };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const headerEnd = text.indexOf(MANAGED_EXTENSION_END, MANAGED_EXTENSION_HEADER.length);
|
|
240
|
+
if (headerEnd === -1) {
|
|
241
|
+
return { status: "malformed", managed: true, error: "managed extension header is not closed" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const headerText = text.slice(0, headerEnd).trimEnd();
|
|
245
|
+
const metadataResult = parseExtensionMetadata(headerText);
|
|
246
|
+
if (typeof metadataResult === "string") {
|
|
247
|
+
return { status: "malformed", managed: true, error: metadataResult };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const body = text.slice(headerEnd + MANAGED_EXTENSION_END.length).replace(/^\r?\n/, "");
|
|
251
|
+
return {
|
|
252
|
+
status: "managed",
|
|
253
|
+
managed: true,
|
|
254
|
+
metadata: metadataResult,
|
|
255
|
+
body: stripTrailingNewline(body),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
90
259
|
function renderFrontmatter(action: WriteRuleAction): string {
|
|
91
260
|
if (action.mode === "ttsr") {
|
|
92
261
|
if (!action.condition) {
|
|
93
262
|
throw new Error(`TTSR rule ${action.slug} is missing a condition`);
|
|
94
263
|
}
|
|
95
|
-
return
|
|
264
|
+
return [
|
|
265
|
+
`condition: ${frontmatterScalarLiteral(action.condition)}`,
|
|
266
|
+
...(action.triggers ? [`triggers: ${frontmatterScalarLiteral(action.triggers)}`] : []),
|
|
267
|
+
`scope: ${frontmatterScalarLiteral(action.scope ?? "text")}`,
|
|
268
|
+
].join("\n");
|
|
96
269
|
}
|
|
97
270
|
|
|
98
271
|
const description = action.description ?? deriveRuleDescription(action.sourceContent) ?? `Use ${action.sourceName} when relevant.`;
|
|
@@ -165,6 +338,105 @@ function parseMetadata(headerText: string): ManagedRuleMetadata | string {
|
|
|
165
338
|
};
|
|
166
339
|
}
|
|
167
340
|
|
|
341
|
+
function parseCommandMetadata(headerText: string): ManagedCommandMetadata | string {
|
|
342
|
+
const lines = headerText.split(/\r?\n/);
|
|
343
|
+
if (lines[0] !== MANAGED_COMMAND_HEADER) {
|
|
344
|
+
return "managed command header marker is invalid";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const raw: Record<string, string> = {};
|
|
348
|
+
for (const line of lines.slice(1)) {
|
|
349
|
+
const idx = line.indexOf(":");
|
|
350
|
+
if (idx === -1) continue;
|
|
351
|
+
raw[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return parseCommandMetadataFields(raw);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseCommandFrontmatterMetadata(frontmatter: Record<string, string>): ManagedCommandMetadata | string {
|
|
358
|
+
if (frontmatter[MANAGED_COMMAND_FRONTMATTER_KEY] !== MANAGED_COMMAND_FRONTMATTER_VERSION) {
|
|
359
|
+
return "managed command metadata has invalid version";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return parseCommandMetadataFields({
|
|
363
|
+
version: frontmatter[MANAGED_COMMAND_FRONTMATTER_KEY],
|
|
364
|
+
sourceId: frontmatter.sourceId,
|
|
365
|
+
sourceName: frontmatter.sourceName,
|
|
366
|
+
sourceHash: frontmatter.sourceHash,
|
|
367
|
+
slug: frontmatter.slug,
|
|
368
|
+
commandName: frontmatter.commandName,
|
|
369
|
+
sourceBytes: frontmatter.sourceBytes,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function parseCommandMetadataFields(raw: Record<string, string | undefined>): ManagedCommandMetadata | string {
|
|
374
|
+
const versionRaw = raw.version;
|
|
375
|
+
const sourceId = raw.sourceId;
|
|
376
|
+
const sourceName = raw.sourceName;
|
|
377
|
+
const sourceHash = raw.sourceHash;
|
|
378
|
+
const slug = raw.slug;
|
|
379
|
+
const commandName = raw.commandName;
|
|
380
|
+
const sourceBytesRaw = raw.sourceBytes;
|
|
381
|
+
if (!versionRaw) return "managed command metadata missing version";
|
|
382
|
+
if (!sourceId) return "managed command metadata missing sourceId";
|
|
383
|
+
if (!sourceName) return "managed command metadata missing sourceName";
|
|
384
|
+
if (!sourceHash) return "managed command metadata missing sourceHash";
|
|
385
|
+
if (!slug) return "managed command metadata missing slug";
|
|
386
|
+
if (!commandName) return "managed command metadata missing commandName";
|
|
387
|
+
if (!sourceBytesRaw) return "managed command metadata missing sourceBytes";
|
|
388
|
+
|
|
389
|
+
const version = Number(versionRaw);
|
|
390
|
+
const sourceBytes = Number(sourceBytesRaw);
|
|
391
|
+
if (!Number.isInteger(version) || version <= 0) return "managed command metadata has invalid version";
|
|
392
|
+
if (!Number.isInteger(sourceBytes) || sourceBytes < 0) return "managed command metadata has invalid sourceBytes";
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
version,
|
|
396
|
+
sourceId,
|
|
397
|
+
sourceName,
|
|
398
|
+
sourceHash,
|
|
399
|
+
slug,
|
|
400
|
+
commandName,
|
|
401
|
+
sourceBytes,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseExtensionMetadata(headerText: string): ManagedExtensionMetadata | string {
|
|
406
|
+
const lines = headerText.split(/\r?\n/);
|
|
407
|
+
if (lines[0] !== MANAGED_EXTENSION_HEADER) {
|
|
408
|
+
return "managed extension header marker is invalid";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const raw: Record<string, string> = {};
|
|
412
|
+
for (const line of lines.slice(1)) {
|
|
413
|
+
const idx = line.indexOf(":");
|
|
414
|
+
if (idx === -1) continue;
|
|
415
|
+
raw[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const required = ["version", "sourceId", "sourceName", "sourceHash", "slug", "extensionName", "sourceBytes"];
|
|
419
|
+
for (const key of required) {
|
|
420
|
+
if (!raw[key]) return `managed extension metadata missing ${key}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const version = Number(raw.version);
|
|
424
|
+
const sourceBytes = Number(raw.sourceBytes);
|
|
425
|
+
if (!Number.isInteger(version) || version <= 0) return "managed extension metadata has invalid version";
|
|
426
|
+
if (!Number.isInteger(sourceBytes) || sourceBytes < 0) return "managed extension metadata has invalid sourceBytes";
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
version,
|
|
430
|
+
sourceId: raw.sourceId,
|
|
431
|
+
sourceName: raw.sourceName,
|
|
432
|
+
sourceHash: raw.sourceHash,
|
|
433
|
+
slug: raw.slug,
|
|
434
|
+
extensionName: raw.extensionName,
|
|
435
|
+
sourceBytes,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
168
440
|
function parseFrontmatter(text: string): { frontmatter: Record<string, string>; body: string } | string {
|
|
169
441
|
if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) {
|
|
170
442
|
return "managed frontmatter is missing opening delimiter";
|
|
@@ -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
|
+
`;
|