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,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
|
|
package/src/harness/command.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
loadHarnessSession,
|
|
40
40
|
loadHarnessValidateReport,
|
|
41
41
|
readSlopQueue,
|
|
42
|
+
saveHarnessDesignSpecJson,
|
|
42
43
|
saveHarnessSession,
|
|
43
44
|
} from "./storage.js";
|
|
44
45
|
import { computeScore } from "./anti_slop/score.js";
|
|
@@ -55,6 +56,8 @@ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
|
|
|
55
56
|
import { getWorkingTreeStatus } from "../git/status.js";
|
|
56
57
|
import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
57
58
|
import { handlePrComment } from "./pr-comment/handler.js";
|
|
59
|
+
import { runGitVerificationQa } from "./git-verify-qa.js";
|
|
60
|
+
import { getHarnessSessionDir } from "./project-paths.js";
|
|
58
61
|
import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
|
|
59
62
|
|
|
60
63
|
modelRegistry.register({
|
|
@@ -223,12 +226,13 @@ async function runPipelineWithProgress(
|
|
|
223
226
|
gates: HarnessGateMode,
|
|
224
227
|
stageInputs: BuildRunnerInput,
|
|
225
228
|
startStage?: HarnessStage,
|
|
229
|
+
forceStages?: ReadonlySet<HarnessStage>,
|
|
226
230
|
): Promise<PipelineRunOutcome> {
|
|
227
231
|
const harnessProgress = createHarnessProgress(ctx);
|
|
228
232
|
const modelConfig = loadModelConfig(platform.paths, ctx.cwd);
|
|
229
233
|
const outcome = await pipelineDriver({
|
|
230
234
|
platform, paths: platform.paths, cwd: ctx.cwd, sessionId,
|
|
231
|
-
modelConfig, gates, stageInputs, startStage,
|
|
235
|
+
modelConfig, gates, stageInputs, startStage, forceStages,
|
|
232
236
|
onProgress: harnessProgress.onProgress,
|
|
233
237
|
});
|
|
234
238
|
// Single consolidated notification.
|
|
@@ -558,12 +562,12 @@ async function runDesignQa(
|
|
|
558
562
|
|
|
559
563
|
if (choice === "Accept all suggestions") {
|
|
560
564
|
applyDesignAnalysis(base, analysis);
|
|
561
|
-
await askCiAndTooling(ctx, base);
|
|
565
|
+
await askCiAndTooling(platform, ctx, base);
|
|
562
566
|
return base;
|
|
563
567
|
}
|
|
564
568
|
|
|
565
569
|
if (choice === "Skip — use bare defaults") {
|
|
566
|
-
await askCiAndTooling(ctx, base);
|
|
570
|
+
await askCiAndTooling(platform, ctx, base);
|
|
567
571
|
return base;
|
|
568
572
|
}
|
|
569
573
|
}
|
|
@@ -627,7 +631,7 @@ async function runDesignQa(
|
|
|
627
631
|
}
|
|
628
632
|
}
|
|
629
633
|
|
|
630
|
-
await askCiAndTooling(ctx, base);
|
|
634
|
+
await askCiAndTooling(platform, ctx, base);
|
|
631
635
|
return base;
|
|
632
636
|
}
|
|
633
637
|
|
|
@@ -707,7 +711,7 @@ function localCommandOptions(base: HarnessDesignSpec): string[] {
|
|
|
707
711
|
]));
|
|
708
712
|
}
|
|
709
713
|
|
|
710
|
-
async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
714
|
+
async function askCiAndTooling(platform: Platform, ctx: HarnessCommandContext, base: HarnessDesignSpec): Promise<void> {
|
|
711
715
|
if (!ctx.ui.select) return;
|
|
712
716
|
|
|
713
717
|
const triggerChoice = await ctx.ui.select(
|
|
@@ -741,6 +745,84 @@ async function askCiAndTooling(ctx: HarnessCommandContext, base: HarnessDesignSp
|
|
|
741
745
|
} else if (toolChoice) {
|
|
742
746
|
base.ci.localCommand = toolChoice.replace(/\s+\(.+\)$/, "");
|
|
743
747
|
}
|
|
748
|
+
|
|
749
|
+
// After the user picks their CI trigger and local command, offer the optional Git
|
|
750
|
+
// verification flow. This populates `base.ci.git` so the implement stage renders the
|
|
751
|
+
// PR-source guardrail and validate confirms the wiring. Skippable; declining leaves
|
|
752
|
+
// `git` unset and the rest of the pipeline behaves identically to before this feature.
|
|
753
|
+
await runGitVerificationStep(platform, ctx, base);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Adapter around `runGitVerificationQa` that fits the harness command UI. The QA helper
|
|
758
|
+
* expects an `ExecFn`-shaped function plus a `select / input / notify` UI trio; we wrap
|
|
759
|
+
* `platform.exec` and `ctx.ui` so the helper stays independent of the OMP plumbing.
|
|
760
|
+
*
|
|
761
|
+
* Persists any returned `HarnessCiGitConfig` onto the in-memory `base` spec; the design
|
|
762
|
+
* stage runner persists the spec to disk so no extra storage call is needed here. We
|
|
763
|
+
* also widen `base.ci.trigger.branches` to include both `mainBranch` and `devBranch`
|
|
764
|
+
* so the rendered workflow runs CI on both PR targets — matching the rule "CI runs on
|
|
765
|
+
* both the dev branch PR and the main branch".
|
|
766
|
+
*/
|
|
767
|
+
async function runGitVerificationStep(
|
|
768
|
+
platform: Platform,
|
|
769
|
+
ctx: HarnessCommandContext,
|
|
770
|
+
base: HarnessDesignSpec,
|
|
771
|
+
): Promise<void> {
|
|
772
|
+
if (!ctx.ui.select || !ctx.ui.input) return; // No interactive UI — skip silently.
|
|
773
|
+
|
|
774
|
+
const sessionDir = getHarnessSessionDir(platform.paths, ctx.cwd, base.sessionId);
|
|
775
|
+
|
|
776
|
+
const result = await runGitVerificationQa({
|
|
777
|
+
exec: (cmd, args, opts) => platform.exec(cmd, args, opts),
|
|
778
|
+
cwd: ctx.cwd,
|
|
779
|
+
sessionDir,
|
|
780
|
+
ui: {
|
|
781
|
+
select: (title, options) => ctx.ui.select!(title, options as unknown as string[]),
|
|
782
|
+
input: (label) => ctx.ui.input!(label),
|
|
783
|
+
notify: (message) => notifyInfo(ctx, "Git verification", message),
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
if (!result) return;
|
|
788
|
+
base.ci.git = result;
|
|
789
|
+
|
|
790
|
+
// Ensure the CI trigger includes both branches so the workflow runs on PRs targeting
|
|
791
|
+
// either. Preserve any user-customized branches the prior step already picked.
|
|
792
|
+
if (base.ci.trigger.mode === "branches") {
|
|
793
|
+
const next = new Set(base.ci.trigger.branches);
|
|
794
|
+
next.add(result.mainBranch);
|
|
795
|
+
if (result.devBranch) next.add(result.devBranch);
|
|
796
|
+
base.ci.trigger = { mode: "branches", branches: Array.from(next) };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Harden-mode entry point for Git verification. Mutates the persisted design spec in
|
|
802
|
+
* place so the downstream implement stage re-renders the workflow with the new `git`
|
|
803
|
+
* block. Returns true when a new `ci.git` block was captured and persisted so the
|
|
804
|
+
* caller can force-re-run the affected stages; false when the user declined or no UI
|
|
805
|
+
* is available.
|
|
806
|
+
*/
|
|
807
|
+
async function runGitVerificationOnHarden(
|
|
808
|
+
platform: Platform,
|
|
809
|
+
ctx: HarnessCommandContext,
|
|
810
|
+
sessionId: string,
|
|
811
|
+
spec: HarnessDesignSpec,
|
|
812
|
+
): Promise<boolean> {
|
|
813
|
+
await runGitVerificationStep(platform, ctx, spec);
|
|
814
|
+
if (!spec.ci.git) return false; // user declined
|
|
815
|
+
const persisted = saveHarnessDesignSpecJson(platform.paths, ctx.cwd, sessionId, spec);
|
|
816
|
+
if (!persisted.ok) {
|
|
817
|
+
notifyInfo(
|
|
818
|
+
ctx,
|
|
819
|
+
"Git verification persisted partially",
|
|
820
|
+
`In-memory spec updated, but persistence failed: ${persisted.error.message}. ` +
|
|
821
|
+
`Re-run /supi:harness to retry.`,
|
|
822
|
+
);
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
744
826
|
}
|
|
745
827
|
|
|
746
828
|
|
|
@@ -902,7 +984,17 @@ async function handleBareEntry(platform: Platform, ctx: HarnessCommandContext):
|
|
|
902
984
|
if (layerCount >= 2) {
|
|
903
985
|
await promptDocsTierIfNeeded(platform, ctx, sessionId, layerCount);
|
|
904
986
|
}
|
|
905
|
-
|
|
987
|
+
// Offer the optional Git verification flow on harden when the existing spec has no
|
|
988
|
+
// `ci.git` block. Keeps the harden path lightweight by skipping silently when the
|
|
989
|
+
// user previously declined or completed the verification. When the user captures a
|
|
990
|
+
// new `ci.git` block, force implement + validate to re-run so the workflow file is
|
|
991
|
+
// re-rendered with the `verify-pr-source` job and the validate cross-check fires.
|
|
992
|
+
let forceStages: ReadonlySet<HarnessStage> | undefined;
|
|
993
|
+
if (designSpec.ok && !designSpec.value.ci.git) {
|
|
994
|
+
const captured = await runGitVerificationOnHarden(platform, ctx, sessionId, designSpec.value);
|
|
995
|
+
if (captured) forceStages = new Set<HarnessStage>(["implement", "validate"]);
|
|
996
|
+
}
|
|
997
|
+
await runPipelineWithProgress(platform, ctx, sessionId, "auto", {}, undefined, forceStages);
|
|
906
998
|
} else {
|
|
907
999
|
// Rebuild: full regeneration with user gates at each stage.
|
|
908
1000
|
await runRebuildWithGates(platform, ctx, sessionId);
|