supipowers 2.0.0 → 2.0.2
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 +6 -1
- package/package.json +1 -1
- package/src/commands/clear.ts +6 -6
- package/src/commands/release.ts +3 -1
- package/src/commands/update.ts +1 -1
- package/src/context/analyzer.ts +104 -35
- package/src/mempalace/bridge.ts +9 -3
- package/src/mempalace/installer-helper.ts +2 -2
- package/src/mempalace/runtime.ts +2 -2
- package/src/mempalace/schema.ts +5 -1
- package/src/ui-design/session.ts +0 -2
package/README.md
CHANGED
|
@@ -77,8 +77,12 @@ The installer scans for these and offers to install missing tooling where it can
|
|
|
77
77
|
| `/supi:generate` | Documentation drift detection |
|
|
78
78
|
| `/supi:update` | Update supipowers to the latest version |
|
|
79
79
|
| `/supi:agents` | Manage review agents |
|
|
80
|
+
| `/supi:ultraplan` | Multi-stage authoring pipeline (intake → scout → discover → research → synthesize → review → approve) |
|
|
81
|
+
| `/supi:harness` | Harness engineering pipeline and anti-slop guardrails |
|
|
82
|
+
| `/supi:memory` | Manage native MemPalace memory integration (`status`, `setup`) |
|
|
83
|
+
| `/supi:clear` | Clear metrics, cache, session knowledge, and memory |
|
|
80
84
|
|
|
81
|
-
Most commands steer the AI session. These are TUI-only — they open native dialogs without triggering the AI: `/supi`, `/supi:config`, `/supi:status`, `/supi:review`, `/supi:update`, `/supi:doctor`, `/supi:mcp`, `/supi:model`, `/supi:context`, `/supi:optimize-context`, `/supi:commit`, `/supi:release`, `/supi:checks`, `/supi:agents`.
|
|
85
|
+
Most commands steer the AI session. These are TUI-only — they open native dialogs without triggering the AI: `/supi`, `/supi:config`, `/supi:status`, `/supi:review`, `/supi:update`, `/supi:doctor`, `/supi:mcp`, `/supi:model`, `/supi:context`, `/supi:optimize-context`, `/supi:commit`, `/supi:release`, `/supi:checks`, `/supi:agents`, `/supi:ultraplan`, `/supi:harness`, `/supi:memory`, `/supi:clear`.
|
|
82
86
|
|
|
83
87
|
## How it works
|
|
84
88
|
|
|
@@ -213,6 +217,7 @@ Supipowers ships runtime-loaded prompt skills that are also available to the age
|
|
|
213
217
|
| `release` | `/supi:release` |
|
|
214
218
|
| `context-mode` | Context window guidance |
|
|
215
219
|
| `creating-supi-agents` | Agent creation guidance |
|
|
220
|
+
| `harness` | `/supi:harness` |
|
|
216
221
|
|
|
217
222
|
## Development
|
|
218
223
|
|
package/package.json
CHANGED
package/src/commands/clear.ts
CHANGED
|
@@ -269,14 +269,14 @@ function parseArgs(args: string | undefined): ParsedArgs {
|
|
|
269
269
|
return { scope, dryRun };
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
export function handleClear(
|
|
272
|
+
export async function handleClear(
|
|
273
273
|
platform: Platform,
|
|
274
274
|
ctx: PlatformContext,
|
|
275
275
|
args?: string,
|
|
276
|
-
): void {
|
|
276
|
+
): Promise<void> {
|
|
277
277
|
if (!ctx.hasUI) return;
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
try {
|
|
280
280
|
const { scope, dryRun } = parseArgs(args);
|
|
281
281
|
const store = getMetricsStore();
|
|
282
282
|
const sessionId = getSessionId();
|
|
@@ -419,16 +419,16 @@ export function handleClear(
|
|
|
419
419
|
"error",
|
|
420
420
|
);
|
|
421
421
|
}
|
|
422
|
-
}
|
|
422
|
+
} catch (err) {
|
|
423
423
|
ctx.ui.notify(`Clear error: ${(err as Error).message}`, "error");
|
|
424
|
-
}
|
|
424
|
+
}
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
export function registerClearCommand(platform: Platform): void {
|
|
428
428
|
platform.registerCommand("supi:clear", {
|
|
429
429
|
description: "Clear metrics, cache, current-session knowledge, and memory for the active session (or `all` for the project)",
|
|
430
430
|
async handler(args: string | undefined, ctx: any) {
|
|
431
|
-
handleClear(platform, ctx, args);
|
|
431
|
+
await handleClear(platform, ctx, args);
|
|
432
432
|
},
|
|
433
433
|
});
|
|
434
434
|
}
|
package/src/commands/release.ts
CHANGED
|
@@ -51,6 +51,9 @@ import {
|
|
|
51
51
|
import {
|
|
52
52
|
checkDocDrift,
|
|
53
53
|
buildFixPrompt,
|
|
54
|
+
loadState as loadDriftState,
|
|
55
|
+
saveState as saveDriftState,
|
|
56
|
+
getHeadCommit,
|
|
54
57
|
} from "../docs/drift.js";
|
|
55
58
|
import { runQualityGates } from "../quality/runner.js";
|
|
56
59
|
import { REVIEW_GATE_REGISTRY } from "../quality/review-gates.js";
|
|
@@ -492,7 +495,6 @@ export async function handleRelease(platform: Platform, ctx: any, args?: string)
|
|
|
492
495
|
progress.activate("doc-drift", "Fixing documentation");
|
|
493
496
|
notifyInfo(ctx, "Updating documentation", driftResult.summary);
|
|
494
497
|
const fixPrompt = buildFixPrompt(driftResult.findings);
|
|
495
|
-
const { loadState: loadDriftState, saveState: saveDriftState, getHeadCommit } = await import("../docs/drift.js");
|
|
496
498
|
const driftHead = await getHeadCommit(platform, repoRoot);
|
|
497
499
|
const driftState = loadDriftState(platform.paths, repoRoot);
|
|
498
500
|
saveDriftState(platform.paths, repoRoot, { ...driftState, lastCommit: driftHead, lastRunAt: new Date().toISOString() });
|
package/src/commands/update.ts
CHANGED
|
@@ -106,7 +106,7 @@ async function updateSupipowers(
|
|
|
106
106
|
cpSync(binSource, join(extDir, "bin"), { recursive: true });
|
|
107
107
|
}
|
|
108
108
|
// skills/ must live inside the extension dir — src/commands/agents.ts
|
|
109
|
-
// uses a static
|
|
109
|
+
// uses a static skills markdown import resolved relative to src/.
|
|
110
110
|
const skillsDirSource = join(downloadedRoot, "skills");
|
|
111
111
|
if (existsSync(skillsDirSource)) {
|
|
112
112
|
cpSync(skillsDirSource, join(extDir, "skills"), { recursive: true });
|
package/src/context/analyzer.ts
CHANGED
|
@@ -59,41 +59,71 @@ export interface ParsedSkill {
|
|
|
59
59
|
export function parseIndividualSkills(systemPrompt: string): ParsedSkill[] {
|
|
60
60
|
if (!systemPrompt) return [];
|
|
61
61
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
// Locate `# Skills` and bound the section by the next h1 heading or end of
|
|
63
|
+
// text. The legacy bound used `^##\s`, which let the section bleed past
|
|
64
|
+
// sibling h2 headings (e.g. `## MCP Server Instructions`) into unrelated
|
|
65
|
+
// content and misidentify them as skills.
|
|
66
|
+
const headerMatch = systemPrompt.match(/^# Skills\b[^\n]*\n/m);
|
|
67
|
+
if (!headerMatch) return [];
|
|
68
|
+
const bodyStart = headerMatch.index! + headerMatch[0].length;
|
|
69
|
+
const after = systemPrompt.slice(bodyStart);
|
|
70
|
+
const nextH1 = after.search(/^# [^#]/m);
|
|
71
|
+
const skillsBody = nextH1 === -1 ? after : after.slice(0, nextH1);
|
|
72
|
+
|
|
73
|
+
// Modern OMP (≥14.7) renders skills as a bullet list: "- name: description"
|
|
74
|
+
// with descriptions that may wrap across multiple lines.
|
|
75
|
+
const bulletRegex = /^- ([a-zA-Z0-9._-]+):/gm;
|
|
76
|
+
const bullets: { name: string; index: number }[] = [];
|
|
77
|
+
let bm: RegExpExecArray | null;
|
|
78
|
+
while ((bm = bulletRegex.exec(skillsBody)) !== null) {
|
|
79
|
+
bullets.push({ name: bm[1], index: bm.index });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (bullets.length > 0) {
|
|
83
|
+
// Defensive upper bound for the last bullet: stop at any inline markdown
|
|
84
|
+
// heading inside the body. Real OMP prompts already terminate at an h1
|
|
85
|
+
// boundary, but synthetic / older prompts may not.
|
|
86
|
+
let bodyEnd = skillsBody.length;
|
|
87
|
+
const headingScan = /^#{1,6}\s/gm;
|
|
88
|
+
let hsm: RegExpExecArray | null;
|
|
89
|
+
while ((hsm = headingScan.exec(skillsBody)) !== null) {
|
|
90
|
+
if (hsm.index > bullets[bullets.length - 1].index) {
|
|
91
|
+
bodyEnd = hsm.index;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const bulletSkills: ParsedSkill[] = [];
|
|
97
|
+
for (let i = 0; i < bullets.length; i++) {
|
|
98
|
+
const start = bullets[i].index;
|
|
99
|
+
const end = i + 1 < bullets.length ? bullets[i + 1].index : bodyEnd;
|
|
100
|
+
const content = skillsBody.slice(start, end).trimEnd();
|
|
101
|
+
bulletSkills.push({
|
|
102
|
+
name: bullets[i].name,
|
|
103
|
+
bytes: byteLength(content),
|
|
104
|
+
tokens: estimateTokens(content),
|
|
105
|
+
content,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return bulletSkills;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Legacy / synthetic shape: "## name" h2 sub-headings under `# Skills`.
|
|
81
112
|
const headingRegex = /^## (.+)$/gm;
|
|
82
113
|
const headings: { name: string; index: number }[] = [];
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headings.push({ name: match[1].trim(), index: match.index });
|
|
114
|
+
let hm: RegExpExecArray | null;
|
|
115
|
+
while ((hm = headingRegex.exec(skillsBody)) !== null) {
|
|
116
|
+
headings.push({ name: hm[1].trim(), index: hm.index });
|
|
87
117
|
}
|
|
88
118
|
|
|
119
|
+
const skills: ParsedSkill[] = [];
|
|
89
120
|
for (let i = 0; i < headings.length; i++) {
|
|
90
121
|
const start = headings[i].index;
|
|
91
122
|
const end = i + 1 < headings.length ? headings[i + 1].index : skillsBody.length;
|
|
92
123
|
const content = skillsBody.slice(start, end).trimEnd();
|
|
93
|
-
const bytes = byteLength(content);
|
|
94
124
|
skills.push({
|
|
95
125
|
name: headings[i].name,
|
|
96
|
-
bytes,
|
|
126
|
+
bytes: byteLength(content),
|
|
97
127
|
tokens: estimateTokens(content),
|
|
98
128
|
content,
|
|
99
129
|
});
|
|
@@ -218,26 +248,45 @@ function extractXmlSections(
|
|
|
218
248
|
sections: PromptSection[],
|
|
219
249
|
consumed: Set<number>,
|
|
220
250
|
): void {
|
|
221
|
-
// Project section FIRST (so nested <file> tags inside
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
// Project section FIRST (so nested <file> tags inside the wrapper are consumed).
|
|
252
|
+
// Modern OMP uses `<|START_PROJECT|>...<|END_PROJECT|>` pipe markers; older OMP
|
|
253
|
+
// and synthetic test inputs use the legacy `<project>...</project>` XML form.
|
|
254
|
+
const projectPatterns: RegExp[] = [
|
|
255
|
+
/<\|START_PROJECT\|>[\s\S]*?<\|END_PROJECT\|>/,
|
|
256
|
+
/<project>[\s\S]*?<\/project>/,
|
|
257
|
+
];
|
|
258
|
+
for (const pattern of projectPatterns) {
|
|
259
|
+
const projMatch = text.match(pattern);
|
|
260
|
+
if (!projMatch) continue;
|
|
224
261
|
sections.push({
|
|
225
262
|
label: "Project context",
|
|
226
263
|
bytes: byteLength(projMatch[0]),
|
|
227
264
|
content: projMatch[0],
|
|
228
265
|
});
|
|
229
266
|
markConsumed(consumed, projMatch.index!, projMatch.index! + projMatch[0].length);
|
|
267
|
+
break;
|
|
230
268
|
}
|
|
231
269
|
|
|
232
|
-
//
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
270
|
+
// Environment envelope (OMP ≥14.9.3) — workstation, tool catalog, LSP guidance.
|
|
271
|
+
const envMatch = text.match(/<\|START_ENV\|>[\s\S]*?<\|END_ENV\|>/);
|
|
272
|
+
if (envMatch) {
|
|
235
273
|
sections.push({
|
|
236
|
-
label: "
|
|
237
|
-
bytes: byteLength(
|
|
238
|
-
content:
|
|
274
|
+
label: "Environment",
|
|
275
|
+
bytes: byteLength(envMatch[0]),
|
|
276
|
+
content: envMatch[0],
|
|
239
277
|
});
|
|
240
|
-
markConsumed(consumed,
|
|
278
|
+
markConsumed(consumed, envMatch.index!, envMatch.index! + envMatch[0].length);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Contract envelope (OMP ≥14.9.3) — inviolable rules, yielding criteria.
|
|
282
|
+
const contractMatch = text.match(/<\|START_CONTRACT\|>[\s\S]*?<\|END_CONTRACT\|>/);
|
|
283
|
+
if (contractMatch) {
|
|
284
|
+
sections.push({
|
|
285
|
+
label: "Contract",
|
|
286
|
+
bytes: byteLength(contractMatch[0]),
|
|
287
|
+
content: contractMatch[0],
|
|
288
|
+
});
|
|
289
|
+
markConsumed(consumed, contractMatch.index!, contractMatch.index! + contractMatch[0].length);
|
|
241
290
|
}
|
|
242
291
|
|
|
243
292
|
// File sections — skip if already consumed (e.g., nested inside <project>)
|
|
@@ -318,6 +367,26 @@ function extractHeadingSections(
|
|
|
318
367
|
}
|
|
319
368
|
}
|
|
320
369
|
|
|
370
|
+
// Skills aggregate (OMP ≥14.7): markdown bullet list under `# Skills`.
|
|
371
|
+
// The legacy `<skills>` XML form is handled in extractXmlSections; this picks
|
|
372
|
+
// up the modern markdown shape rendered by OMP runtime. Bounded by the next
|
|
373
|
+
// h1/h2 heading so we don't swallow MCP instructions / Tools blocks.
|
|
374
|
+
const skillsHeading = text.match(/^# Skills\b[^\n]*\n/m);
|
|
375
|
+
if (skillsHeading && !consumed.has(skillsHeading.index!)) {
|
|
376
|
+
const start = skillsHeading.index!;
|
|
377
|
+
const afterHeading = text.slice(start + skillsHeading[0].length);
|
|
378
|
+
const nextHeading = afterHeading.search(/^#{1,2}\s/m);
|
|
379
|
+
const end = nextHeading === -1
|
|
380
|
+
? text.length
|
|
381
|
+
: start + skillsHeading[0].length + nextHeading;
|
|
382
|
+
const content = text.slice(start, end);
|
|
383
|
+
const bulletCount = (content.match(/^- [a-zA-Z0-9._-]+:/gm) || []).length;
|
|
384
|
+
if (bulletCount > 0) {
|
|
385
|
+
sections.push({ label: `Skills (${bulletCount})`, bytes: byteLength(content), content });
|
|
386
|
+
markConsumed(consumed, start, end);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
321
390
|
// Also recognize bare memory:// blocks without a heading
|
|
322
391
|
if (!sections.some((s) => s.label === "Memory")) {
|
|
323
392
|
const memoryMatch = text.match(/memory:\/\/\S+/);
|
package/src/mempalace/bridge.ts
CHANGED
|
@@ -54,6 +54,14 @@ function mergeDiagnostics(...parts: Array<Record<string, unknown> | undefined>):
|
|
|
54
54
|
return Object.assign({}, ...parts.filter(Boolean));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function resolveToolTimeoutMs(params: MempalaceParams, bridgeTimeoutMs: number): number {
|
|
58
|
+
if (typeof params.timeout !== "number") return bridgeTimeoutMs;
|
|
59
|
+
|
|
60
|
+
// Public tool schemas express timeouts in seconds, matching the rest of the
|
|
61
|
+
// OMP tool surface. The bridge runner uses milliseconds internally.
|
|
62
|
+
return Math.min(params.timeout * 1000, bridgeTimeoutMs);
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export function createMempalaceBridge(options: CreateMempalaceBridgeOptions): MempalaceBridgeFacade {
|
|
58
66
|
const resolveBridge = options.runtime?.resolveBridgeScriptPath ?? (() => resolveBridgeScriptPath());
|
|
59
67
|
const runBridge = options.runtime?.runBridgeRequest ?? runBridgeRequest;
|
|
@@ -71,9 +79,7 @@ export function createMempalaceBridge(options: CreateMempalaceBridgeOptions): Me
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
const venv = resolveManagedVenvPaths(options.config.managedVenvPath);
|
|
74
|
-
const timeoutMs =
|
|
75
|
-
? Math.min(params.timeout, options.config.timeouts.bridgeMs)
|
|
76
|
-
: options.config.timeouts.bridgeMs;
|
|
82
|
+
const timeoutMs = resolveToolTimeoutMs(params, options.config.timeouts.bridgeMs);
|
|
77
83
|
const palacePath = params.palace ?? options.config.palacePath;
|
|
78
84
|
const runResult = await runBridge({
|
|
79
85
|
pythonPath: venv.python,
|
|
@@ -176,8 +176,8 @@ export function steerMempalaceInitialization(
|
|
|
176
176
|
"",
|
|
177
177
|
"Please initialize and seed memory for this project by running these tool calls in order:",
|
|
178
178
|
"",
|
|
179
|
-
`1. \`mempalace(action="init", dir=".", yes=true)\` — register this project's wing in the palace.`,
|
|
180
|
-
`2. \`mempalace(action="mine", dir=".", limit=20)\` — seed initial drawers from project files.`,
|
|
179
|
+
`1. \`mempalace(action="init", dir=".", yes=true, timeout=30)\` — register this project's wing in the palace.`,
|
|
180
|
+
`2. \`mempalace(action="mine", dir=".", limit=20, timeout=30)\` — seed initial drawers from project files.`,
|
|
181
181
|
"",
|
|
182
182
|
"Step 2 is recommended but optional; skip it if the user prefers an empty wing or is mid-task. After running, summarize what was indexed.",
|
|
183
183
|
].join("\n");
|
package/src/mempalace/runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import type { ResolvedMempalaceConfig } from "./config.js";
|
|
6
6
|
import type { MempalaceAction, MempalaceParams } from "./schema.js";
|
|
7
|
+
import { ensureUv, type UvFetcher } from "./uv.js";
|
|
7
8
|
|
|
8
9
|
export interface MempalaceRuntimeError {
|
|
9
10
|
code: string;
|
|
@@ -111,7 +112,7 @@ export interface SetupMempalaceRuntimeOptions {
|
|
|
111
112
|
managedBinDir: string;
|
|
112
113
|
managedPythonVersion?: string;
|
|
113
114
|
runner?: ProcessRunner;
|
|
114
|
-
fetcher?:
|
|
115
|
+
fetcher?: UvFetcher;
|
|
115
116
|
uvVersion?: string;
|
|
116
117
|
onProgress?: (message: string) => void;
|
|
117
118
|
/**
|
|
@@ -427,7 +428,6 @@ export async function setupMempalaceRuntime(
|
|
|
427
428
|
const runner = options.runner ?? defaultProcessRunner;
|
|
428
429
|
|
|
429
430
|
// 1. Ensure uv is available (download + verify if needed).
|
|
430
|
-
const { ensureUv } = await import("./uv.js");
|
|
431
431
|
const uv = await ensureUv({
|
|
432
432
|
managedBinDir: options.managedBinDir,
|
|
433
433
|
runner,
|
package/src/mempalace/schema.ts
CHANGED
|
@@ -201,7 +201,11 @@ export const mempalaceToolParameters = {
|
|
|
201
201
|
include_ignored: { type: "boolean" },
|
|
202
202
|
no_gitignore: { type: "boolean" },
|
|
203
203
|
yes: { type: "boolean" },
|
|
204
|
-
timeout: {
|
|
204
|
+
timeout: {
|
|
205
|
+
type: "integer",
|
|
206
|
+
minimum: 1,
|
|
207
|
+
description: "Optional bridge timeout in seconds; capped by the configured MemPalace bridge timeout.",
|
|
208
|
+
},
|
|
205
209
|
},
|
|
206
210
|
required: ["action"],
|
|
207
211
|
} as const;
|
package/src/ui-design/session.ts
CHANGED