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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Workflow extension for OMP coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
- void (async () => {
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
- })().catch((err) => {
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
  }
@@ -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() });
@@ -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 `import from "../../skills/..."` resolved relative to src/.
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 });
@@ -59,41 +59,71 @@ export interface ParsedSkill {
59
59
  export function parseIndividualSkills(systemPrompt: string): ParsedSkill[] {
60
60
  if (!systemPrompt) return [];
61
61
 
62
- // OMP renders skills as markdown headings under "# Skills":
63
- // ## skill-name
64
- // Description text...
65
- // Find the Skills section bounded by the next h1 heading or end of text.
66
- const skillsSectionMatch = systemPrompt.match(
67
- /^# Skills\n[\s\S]*?\n(?=##\s)/m,
68
- );
69
- if (!skillsSectionMatch) return [];
70
-
71
- // Extract the region from first ## to the next # (h1) or end of text
72
- const sectionStart = skillsSectionMatch.index! + skillsSectionMatch[0].length;
73
- const afterSection = systemPrompt.slice(sectionStart);
74
- const nextH1 = afterSection.search(/^# [^#]/m);
75
- const skillsBody =
76
- skillsSectionMatch[0].slice(skillsSectionMatch[0].indexOf("\n## ") + 1) +
77
- (nextH1 === -1 ? afterSection : afterSection.slice(0, nextH1));
78
-
79
- // Split on ## headings
80
- const skills: ParsedSkill[] = [];
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 match;
84
-
85
- while ((match = headingRegex.exec(skillsBody)) !== null) {
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 <project> are consumed)
222
- const projMatch = text.match(/<project>([\s\S]*?)<\/project>/);
223
- if (projMatch) {
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
- // Instructions section
233
- const instrMatch = text.match(/<instructions>([\s\S]*?)<\/instructions>/);
234
- if (instrMatch) {
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: "Extension instructions",
237
- bytes: byteLength(instrMatch[0]),
238
- content: instrMatch[0],
274
+ label: "Environment",
275
+ bytes: byteLength(envMatch[0]),
276
+ content: envMatch[0],
239
277
  });
240
- markConsumed(consumed, instrMatch.index!, instrMatch.index! + instrMatch[0].length);
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+/);
@@ -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 = typeof params.timeout === "number"
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");
@@ -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?: import("./uv.js").UvFetcher;
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,
@@ -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: { type: "integer", minimum: 1 },
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;
@@ -784,8 +784,6 @@ function getUiDesignWritePaths(toolName: string, input: Record<string, unknown>)
784
784
  : "",
785
785
  );
786
786
  }
787
- case "notebook":
788
- return [typeof input.notebook_path === "string" ? input.notebook_path : ""];
789
787
  default:
790
788
  return undefined;
791
789
  }