supipowers 2.2.1 → 2.2.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/package.json +8 -8
- package/src/bootstrap.ts +2 -1
- package/src/commands/doctor.ts +3 -2
- package/src/commands/plan.ts +2 -1
- package/src/commands/update.ts +7 -5
- package/src/harness/anti_slop/fallow-adapter.ts +4 -3
- package/src/harness/command.ts +12 -7
- package/src/harness/pipeline.ts +2 -8
- package/src/harness/stage-runner.ts +3 -0
- package/src/harness/stages/docs.ts +82 -0
- package/src/mempalace/uv.ts +15 -7
- package/src/planning/approval-flow.ts +15 -17
- package/src/types.ts +10 -0
- package/src/utils/exec-cli.ts +106 -0
- package/src/visual/scripts/npm-shrinkwrap.json +878 -0
- package/src/visual/scripts/package-lock.json +878 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supipowers",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Workflow extension for OMP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "bun test --timeout
|
|
7
|
+
"test": "bun test --timeout 60000 --parallel tests/",
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
9
|
-
"test:watch": "bun test --timeout
|
|
10
|
-
"test:evals": "bun test --timeout
|
|
9
|
+
"test:watch": "bun test --timeout 60000 --parallel --watch tests/",
|
|
10
|
+
"test:evals": "bun test --timeout 60000 --parallel tests/evals/",
|
|
11
11
|
"build": "tsc -p tsconfig.build.json",
|
|
12
12
|
"ci": "bun run typecheck && bun run test",
|
|
13
13
|
"install:visual-server": "npm --prefix src/visual/scripts ci --ignore-scripts --no-audit --no-fund",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dev-install": "bun run bin/dev-install.ts"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"bun": ">=1.3.
|
|
19
|
+
"bun": ">=1.3.13"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"omp",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
]
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@clack/prompts": "^
|
|
68
|
+
"@clack/prompts": "^1.4.0",
|
|
69
69
|
"handlebars": "^4.7.8",
|
|
70
70
|
"yaml": "^2.8.3",
|
|
71
71
|
"zod": "^4.3.0"
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"@oh-my-pi/pi-ai": "latest",
|
|
91
91
|
"@oh-my-pi/pi-coding-agent": "latest",
|
|
92
92
|
"@oh-my-pi/pi-tui": "latest",
|
|
93
|
-
"@types/node": "^
|
|
93
|
+
"@types/node": "^25.8.0",
|
|
94
94
|
"bun-types": "^1.3.11",
|
|
95
|
-
"typescript": "^
|
|
95
|
+
"typescript": "^6.0.3"
|
|
96
96
|
}
|
|
97
97
|
}
|
package/src/bootstrap.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { registerAiReviewCommand, handleAiReview } from "./commands/ai-review.js
|
|
|
12
12
|
import { registerQaCommand } from "./commands/qa.js";
|
|
13
13
|
import { registerReleaseCommand, handleRelease } from "./commands/release.js";
|
|
14
14
|
import { registerUpdateCommand, handleUpdate } from "./commands/update.js";
|
|
15
|
+
import { execCli } from "./utils/exec-cli.js";
|
|
15
16
|
import { registerDoctorCommand, handleDoctor } from "./commands/doctor.js";
|
|
16
17
|
import { registerModelCommand, handleModel } from "./commands/model.js";
|
|
17
18
|
import { registerFixPrCommand } from "./commands/fix-pr.js";
|
|
@@ -175,7 +176,7 @@ export function bootstrap(platform: Platform): void {
|
|
|
175
176
|
const currentVersion = getInstalledVersion(platform);
|
|
176
177
|
if (!currentVersion) return;
|
|
177
178
|
|
|
178
|
-
platform.exec("npm", ["view", "supipowers", "version"], { cwd: tmpdir() })
|
|
179
|
+
execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["view", "supipowers", "version"], { cwd: tmpdir() })
|
|
179
180
|
.then((result) => {
|
|
180
181
|
if (result.code !== 0) return;
|
|
181
182
|
const latest = result.stdout.trim();
|
package/src/commands/doctor.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { formatReliabilitySection, loadReliabilitySummaries } from "../storage/r
|
|
|
11
11
|
import { getMetricsStore, getSessionId } from "../context-mode/hooks.js";
|
|
12
12
|
import { getProjectStatePath, getProjectStateDir } from "../workspace/state-paths.js";
|
|
13
13
|
import { basename } from "node:path";
|
|
14
|
+
import { execCli } from "../utils/exec-cli.js";
|
|
14
15
|
|
|
15
16
|
export interface CheckResult {
|
|
16
17
|
name: string;
|
|
@@ -435,14 +436,14 @@ export function checkMetrics(
|
|
|
435
436
|
|
|
436
437
|
export async function checkNpm(platform: Platform): Promise<CheckResult> {
|
|
437
438
|
try {
|
|
438
|
-
const vResult = await platform.exec("npm", ["--version"]);
|
|
439
|
+
const vResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["--version"]);
|
|
439
440
|
if (vResult.code !== 0) {
|
|
440
441
|
return { name: "npm", presence: { ok: false, detail: "npm not found" } };
|
|
441
442
|
}
|
|
442
443
|
const version = vResult.stdout.trim();
|
|
443
444
|
const presence = { ok: true, detail: `v${version}` };
|
|
444
445
|
|
|
445
|
-
const pingResult = await platform.exec("npm", ["ping"]);
|
|
446
|
+
const pingResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["ping"]);
|
|
446
447
|
if (pingResult.code === 0) {
|
|
447
448
|
return { name: "npm", presence, functional: { ok: true, detail: "Registry reachable" } };
|
|
448
449
|
}
|
package/src/commands/plan.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { loadModelConfig } from "../config/model-config.js";
|
|
|
19
19
|
import { getProjectStatePath } from "../workspace/state-paths.js";
|
|
20
20
|
import { cancelPlanTracking, startPlanTracking } from "../planning/approval-flow.js";
|
|
21
21
|
import { stopVisualServer } from "../visual/stop-server.js";
|
|
22
|
+
import { execCli } from "../utils/exec-cli.js";
|
|
22
23
|
|
|
23
24
|
modelRegistry.register({
|
|
24
25
|
id: "plan",
|
|
@@ -111,7 +112,7 @@ export function registerPlanCommand(platform: Platform): void {
|
|
|
111
112
|
const nodeModules = path.join(scriptsDir, "node_modules");
|
|
112
113
|
if (!fs.existsSync(nodeModules)) {
|
|
113
114
|
notifyInfo(ctx, "Installing visual companion dependencies...");
|
|
114
|
-
const installResult = await platform.exec("npm", ["install", "--production"], { cwd: scriptsDir });
|
|
115
|
+
const installResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["install", "--production"], { cwd: scriptsDir });
|
|
115
116
|
if (installResult.code !== 0) {
|
|
116
117
|
notifyError(ctx, "Failed to install visual companion dependencies", installResult.stderr);
|
|
117
118
|
debugLogger.log("visual_companion_dependency_install_failed", {
|
package/src/commands/update.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
steerMempalaceInitialization,
|
|
12
12
|
} from "../mempalace/installer-helper.js";
|
|
13
13
|
import { loadConfig } from "../config/loader.js";
|
|
14
|
+
import { execCli, wrapExecForCli } from "../utils/exec-cli.js";
|
|
14
15
|
|
|
15
16
|
// ── Options builder ──────────────────────────────────────
|
|
16
17
|
|
|
@@ -59,7 +60,7 @@ async function updateSupipowers(
|
|
|
59
60
|
ctx.ui.notify(`Current version: v${currentVersion}`, "info");
|
|
60
61
|
|
|
61
62
|
// Check latest version on npm
|
|
62
|
-
const checkResult = await platform.exec("npm", ["view", "supipowers", "version"], { cwd: tmpdir() });
|
|
63
|
+
const checkResult = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["view", "supipowers", "version"], { cwd: tmpdir() });
|
|
63
64
|
if (checkResult.code !== 0) {
|
|
64
65
|
ctx.ui.notify("Failed to check for updates — npm view failed", "error");
|
|
65
66
|
return null;
|
|
@@ -78,7 +79,8 @@ async function updateSupipowers(
|
|
|
78
79
|
mkdirSync(tempDir, { recursive: true });
|
|
79
80
|
|
|
80
81
|
try {
|
|
81
|
-
const installResult = await
|
|
82
|
+
const installResult = await execCli(
|
|
83
|
+
(cmd, args, opts) => platform.exec(cmd, args, opts),
|
|
82
84
|
"npm", ["install", "--prefix", tempDir, `supipowers@${latestVersion}`],
|
|
83
85
|
{ cwd: tempDir },
|
|
84
86
|
);
|
|
@@ -130,10 +132,10 @@ async function updateSupipowers(
|
|
|
130
132
|
// Install runtime dependencies (handlebars, etc.)
|
|
131
133
|
// Without this, the extension fails to load because node_modules/ was deleted above.
|
|
132
134
|
ctx.ui.notify("Installing dependencies...", "info");
|
|
133
|
-
const bunInstall = await platform.exec("bun", ["install", "--production"], { cwd: extDir });
|
|
135
|
+
const bunInstall = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "bun", ["install", "--production"], { cwd: extDir });
|
|
134
136
|
if (bunInstall.code !== 0) {
|
|
135
137
|
// Fallback to npm if bun is not available (e.g. Windows without global bun)
|
|
136
|
-
const npmInstall = await platform.exec("npm", ["install", "--omit=dev"], { cwd: extDir });
|
|
138
|
+
const npmInstall = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npm", ["install", "--omit=dev"], { cwd: extDir });
|
|
137
139
|
if (npmInstall.code !== 0) {
|
|
138
140
|
ctx.ui.notify(
|
|
139
141
|
"Could not install extension dependencies.\n" +
|
|
@@ -177,7 +179,7 @@ async function updateSupipowers(
|
|
|
177
179
|
|
|
178
180
|
export function handleUpdate(platform: Platform, ctx: PlatformContext): void {
|
|
179
181
|
void (async () => {
|
|
180
|
-
const exec = (cmd: string, args: string[]) => platform.exec(cmd, args);
|
|
182
|
+
const exec = wrapExecForCli((cmd: string, args: string[]) => platform.exec(cmd, args));
|
|
181
183
|
|
|
182
184
|
// 1. Scan all dependencies
|
|
183
185
|
const allStatuses = await scanAll(exec);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SlopBackendResult,
|
|
26
26
|
type SlopFinding,
|
|
27
27
|
} from "./backend.js";
|
|
28
|
+
import { execCli } from "../../utils/exec-cli.js";
|
|
28
29
|
|
|
29
30
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
31
|
|
|
@@ -149,7 +150,7 @@ async function resolveInvocation(
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
try {
|
|
152
|
-
const probe = await platform.exec("npx", ["--no-install", "fallow", "--version"], { timeout: 5000 });
|
|
153
|
+
const probe = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), "npx", ["--no-install", "fallow", "--version"], { timeout: 5000 });
|
|
153
154
|
if (probe.code === 0) {
|
|
154
155
|
availabilityCache = { ok: true, via: "npx" };
|
|
155
156
|
return { ok: true, cmd: "npx", baseArgs: ["--no-install", "fallow"], via: "npx" };
|
|
@@ -187,7 +188,7 @@ async function runFallow(
|
|
|
187
188
|
const startedAt = Date.now();
|
|
188
189
|
let result;
|
|
189
190
|
try {
|
|
190
|
-
result = await platform.exec(invocation.cmd, args, {
|
|
191
|
+
result = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), invocation.cmd, args, {
|
|
191
192
|
cwd: opts.cwd,
|
|
192
193
|
timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
193
194
|
});
|
|
@@ -269,7 +270,7 @@ export class FallowAdapter implements SlopBackend {
|
|
|
269
270
|
if (opts.subtree) args.push("--path", opts.subtree);
|
|
270
271
|
|
|
271
272
|
try {
|
|
272
|
-
const result = await platform.exec(invocation.cmd, args, {
|
|
273
|
+
const result = await execCli((cmd, args, opts) => platform.exec(cmd, args, opts), invocation.cmd, args, {
|
|
273
274
|
cwd: opts.cwd,
|
|
274
275
|
timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
275
276
|
});
|
package/src/harness/command.ts
CHANGED
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
import { computeScore } from "./anti_slop/score.js";
|
|
46
46
|
import {
|
|
47
47
|
type BuildRunnerInput,
|
|
48
|
-
type HarnessPipelineProgressEvent,
|
|
49
48
|
type PipelineRunOutcome,
|
|
50
49
|
HARNESS_STAGE_ORDER,
|
|
51
50
|
runHarnessPipelineUntilGate,
|
|
@@ -58,7 +57,7 @@ import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
|
58
57
|
import { handlePrComment } from "./pr-comment/handler.js";
|
|
59
58
|
import { runGitVerificationQa } from "./git-verify-qa.js";
|
|
60
59
|
import { getHarnessSessionDir } from "./project-paths.js";
|
|
61
|
-
import type { HarnessDesignSpec, HarnessGateMode, HarnessSession, HarnessStage } from "../types.js";
|
|
60
|
+
import type { HarnessDesignSpec, HarnessGateMode, HarnessPipelineProgressEvent, HarnessSession, HarnessStage } from "../types.js";
|
|
62
61
|
|
|
63
62
|
modelRegistry.register({
|
|
64
63
|
id: "harness",
|
|
@@ -133,9 +132,11 @@ function createHarnessProgress(ctx: HarnessCommandContext) {
|
|
|
133
132
|
let done = 0;
|
|
134
133
|
let cur: HarnessStage | null = null;
|
|
135
134
|
const completed: string[] = [];
|
|
135
|
+
let liveDetail: string | null = null;
|
|
136
136
|
|
|
137
137
|
function refresh() {
|
|
138
|
-
const
|
|
138
|
+
const baseLabel = cur ? HARNESS_STAGE_LABELS[cur] : "Complete";
|
|
139
|
+
const label = cur && liveDetail ? `${baseLabel} — ${liveDetail}` : baseLabel;
|
|
139
140
|
const spinner = cur ? "\u25cc" : "\u2713";
|
|
140
141
|
(ctx.ui as any).setStatus?.("supi-harness", ` ${spinner} harness: ${label} (${done}/${SO.length})`);
|
|
141
142
|
}
|
|
@@ -147,22 +148,26 @@ function createHarnessProgress(ctx: HarnessCommandContext) {
|
|
|
147
148
|
case "stage-started":
|
|
148
149
|
cur = event.stage;
|
|
149
150
|
break;
|
|
151
|
+
case "stage-progress":
|
|
152
|
+
cur = event.stage;
|
|
153
|
+
liveDetail = event.detail;
|
|
154
|
+
break;
|
|
150
155
|
case "stage-completed": {
|
|
151
|
-
done += 1; cur = null;
|
|
156
|
+
done += 1; cur = null; liveDetail = null;
|
|
152
157
|
const mark = "\u2713";
|
|
153
158
|
completed.push(`${mark} ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "done"}`);
|
|
154
159
|
break;
|
|
155
160
|
}
|
|
156
161
|
case "stage-skipped":
|
|
157
|
-
done += 1; cur = null;
|
|
162
|
+
done += 1; cur = null; liveDetail = null;
|
|
158
163
|
completed.push(`\u2013 ${HARNESS_STAGE_LABELS[event.stage]}: skipped`);
|
|
159
164
|
break;
|
|
160
165
|
case "awaiting-user":
|
|
161
|
-
done += 1; cur = null;
|
|
166
|
+
done += 1; cur = null; liveDetail = null;
|
|
162
167
|
completed.push(`\u25cb ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "awaiting review"}`);
|
|
163
168
|
break;
|
|
164
169
|
case "stage-failed": case "stage-blocked":
|
|
165
|
-
cur = null;
|
|
170
|
+
cur = null; liveDetail = null;
|
|
166
171
|
completed.push(`\u2717 ${HARNESS_STAGE_LABELS[event.stage]}: ${event.detail || "failed"}`);
|
|
167
172
|
break;
|
|
168
173
|
}
|
package/src/harness/pipeline.ts
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
15
15
|
import type {
|
|
16
16
|
HarnessGateMode,
|
|
17
|
+
HarnessPipelineProgressEvent,
|
|
17
18
|
HarnessStage,
|
|
18
19
|
ModelConfig,
|
|
19
20
|
} from "../types.js";
|
|
@@ -38,14 +39,6 @@ import { buildBackendAdapter } from "./anti_slop/backend-factory.js";
|
|
|
38
39
|
import { DEFAULT_HARNESS_CONFIG } from "./hooks/register.js";
|
|
39
40
|
import { getProjectStatePath } from "../workspace/state-paths.js";
|
|
40
41
|
|
|
41
|
-
/** Progress event emitted by the pipeline driver for UI feedback. */
|
|
42
|
-
export type HarnessPipelineProgressEvent =
|
|
43
|
-
| { type: "stage-started"; stage: HarnessStage }
|
|
44
|
-
| { type: "stage-skipped"; stage: HarnessStage }
|
|
45
|
-
| { type: "stage-completed"; stage: HarnessStage; detail?: string }
|
|
46
|
-
| { type: "stage-blocked"; stage: HarnessStage; detail: string }
|
|
47
|
-
| { type: "stage-failed"; stage: HarnessStage; detail: string }
|
|
48
|
-
| { type: "awaiting-user"; stage: HarnessStage; detail?: string };
|
|
49
42
|
|
|
50
43
|
const STAGE_ORDER: readonly HarnessStage[] = [
|
|
51
44
|
"discover",
|
|
@@ -301,6 +294,7 @@ export async function runHarnessPipelineUntilGate(
|
|
|
301
294
|
sessionId: input.sessionId,
|
|
302
295
|
modelConfig: input.modelConfig,
|
|
303
296
|
gateMode: input.gates,
|
|
297
|
+
onProgress: (event) => input.onProgress?.(event),
|
|
304
298
|
};
|
|
305
299
|
|
|
306
300
|
const result = await runner.run(ctx);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Platform, PlatformPaths } from "../platform/types.js";
|
|
22
22
|
import type {
|
|
23
23
|
HarnessGateMode,
|
|
24
|
+
HarnessPipelineProgressEvent,
|
|
24
25
|
HarnessStage,
|
|
25
26
|
HarnessStageStatus,
|
|
26
27
|
ModelConfig,
|
|
@@ -46,6 +47,8 @@ export interface HarnessStageRunnerContext {
|
|
|
46
47
|
now?: () => string;
|
|
47
48
|
/** Optional override for the agent session model. Tests use this to bypass resolution. */
|
|
48
49
|
modelOverride?: { model: string; thinkingLevel: string | null };
|
|
50
|
+
/** Live progress sink for long-running stage internals such as subagent turns. */
|
|
51
|
+
onProgress?: (event: HarnessPipelineProgressEvent) => void;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export type HarnessStageRunStatus =
|
|
@@ -90,6 +90,7 @@ export interface DocsStageInput {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
interface AgentSessionLike {
|
|
93
|
+
subscribe?: (handler: (event: unknown) => void) => () => void;
|
|
93
94
|
prompt(text: string, opts?: { expandPromptTemplates?: boolean }): Promise<void>;
|
|
94
95
|
dispose(): Promise<void>;
|
|
95
96
|
}
|
|
@@ -536,6 +537,60 @@ async function orchestrateLayerSubagent(input: OrchestrateLayerInput): Promise<O
|
|
|
536
537
|
}
|
|
537
538
|
}
|
|
538
539
|
|
|
540
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
541
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function stringField(record: Record<string, unknown>, key: string): string | null {
|
|
545
|
+
const value = record[key];
|
|
546
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function nestedStringField(record: Record<string, unknown>, objectKey: string, fieldKey: string): string | null {
|
|
550
|
+
const nested = record[objectKey];
|
|
551
|
+
if (!isRecord(nested)) return null;
|
|
552
|
+
return stringField(nested, fieldKey);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function compactDetail(value: string, maxChars = 180): string {
|
|
556
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
557
|
+
if (compact.length <= maxChars) return compact;
|
|
558
|
+
return `${compact.slice(0, maxChars - 1).trimEnd()}…`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function summarizeAgentSessionEvent(event: unknown): string | null {
|
|
562
|
+
if (!isRecord(event)) return null;
|
|
563
|
+
const type = stringField(event, "type") ?? stringField(event, "kind") ?? stringField(event, "event");
|
|
564
|
+
const lowerType = type?.toLowerCase() ?? "";
|
|
565
|
+
const toolName =
|
|
566
|
+
stringField(event, "toolName") ??
|
|
567
|
+
stringField(event, "tool") ??
|
|
568
|
+
stringField(event, "name") ??
|
|
569
|
+
nestedStringField(event, "toolCall", "name") ??
|
|
570
|
+
nestedStringField(event, "tool_call", "name");
|
|
571
|
+
if (toolName && (lowerType.includes("tool") || lowerType.includes("call"))) {
|
|
572
|
+
return `tool ${compactDetail(toolName, 80)}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const text =
|
|
576
|
+
stringField(event, "text") ??
|
|
577
|
+
stringField(event, "delta") ??
|
|
578
|
+
stringField(event, "thought") ??
|
|
579
|
+
stringField(event, "content") ??
|
|
580
|
+
nestedStringField(event, "message", "text") ??
|
|
581
|
+
nestedStringField(event, "message", "content");
|
|
582
|
+
if (text) {
|
|
583
|
+
const prefix = lowerType.includes("thought") || lowerType.includes("reason")
|
|
584
|
+
? "thought"
|
|
585
|
+
: lowerType.includes("tool")
|
|
586
|
+
? "tool"
|
|
587
|
+
: "agent";
|
|
588
|
+
return `${prefix} ${compactDetail(text)}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return type ? compactDetail(type, 80) : null;
|
|
592
|
+
}
|
|
593
|
+
|
|
539
594
|
async function dispatchSubagent(input: {
|
|
540
595
|
platform: Platform;
|
|
541
596
|
ctx: HarnessStageRunnerContext;
|
|
@@ -548,7 +603,13 @@ async function dispatchSubagent(input: {
|
|
|
548
603
|
const agentDisplayName = buildHarnessAgentDisplayName("docs", input.entry.layer.layer);
|
|
549
604
|
|
|
550
605
|
let session: AgentSessionLike | null = null;
|
|
606
|
+
let unsubscribe: (() => void) | null = null;
|
|
551
607
|
try {
|
|
608
|
+
input.ctx.onProgress?.({
|
|
609
|
+
type: "stage-progress",
|
|
610
|
+
stage: "docs",
|
|
611
|
+
detail: `${agentDisplayName}: starting attempt ${input.attempt}`,
|
|
612
|
+
});
|
|
552
613
|
if (input.factory) {
|
|
553
614
|
session = await input.factory(input.platform, {
|
|
554
615
|
cwd: input.ctx.cwd,
|
|
@@ -562,7 +623,21 @@ async function dispatchSubagent(input: {
|
|
|
562
623
|
agentDisplayName,
|
|
563
624
|
});
|
|
564
625
|
}
|
|
626
|
+
unsubscribe = session.subscribe?.((event) => {
|
|
627
|
+
const summary = summarizeAgentSessionEvent(event);
|
|
628
|
+
if (!summary) return;
|
|
629
|
+
input.ctx.onProgress?.({
|
|
630
|
+
type: "stage-progress",
|
|
631
|
+
stage: "docs",
|
|
632
|
+
detail: `${agentDisplayName}: ${summary}`,
|
|
633
|
+
});
|
|
634
|
+
}) ?? null;
|
|
565
635
|
await session.prompt(input.assignment, { expandPromptTemplates: false });
|
|
636
|
+
input.ctx.onProgress?.({
|
|
637
|
+
type: "stage-progress",
|
|
638
|
+
stage: "docs",
|
|
639
|
+
detail: `${agentDisplayName}: attempt ${input.attempt} complete`,
|
|
640
|
+
});
|
|
566
641
|
} catch (error) {
|
|
567
642
|
return {
|
|
568
643
|
ok: false,
|
|
@@ -571,6 +646,13 @@ async function dispatchSubagent(input: {
|
|
|
571
646
|
],
|
|
572
647
|
};
|
|
573
648
|
} finally {
|
|
649
|
+
if (unsubscribe) {
|
|
650
|
+
try {
|
|
651
|
+
unsubscribe();
|
|
652
|
+
} catch {
|
|
653
|
+
/* best-effort */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
574
656
|
if (session) {
|
|
575
657
|
try {
|
|
576
658
|
await session.dispose();
|
package/src/mempalace/uv.ts
CHANGED
|
@@ -49,17 +49,22 @@ export function uvTargetFor(uvPlatform: UvPlatform): UvTarget {
|
|
|
49
49
|
case "linux-arm64":
|
|
50
50
|
return target("aarch64-unknown-linux-gnu", ".tar.gz", "uv");
|
|
51
51
|
case "win32-x64":
|
|
52
|
-
return target("x86_64-pc-windows-msvc", ".zip", "uv.exe");
|
|
52
|
+
return target("x86_64-pc-windows-msvc", ".zip", "uv.exe", "uv.exe");
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function target(
|
|
56
|
+
function target(
|
|
57
|
+
triple: string,
|
|
58
|
+
archiveSuffix: string,
|
|
59
|
+
binary: string,
|
|
60
|
+
archiveBinaryRelativePath = path.join(`uv-${triple}`, binary),
|
|
61
|
+
): UvTarget {
|
|
57
62
|
const archive = `uv-${triple}${archiveSuffix}`;
|
|
58
63
|
return {
|
|
59
64
|
triple,
|
|
60
65
|
archive,
|
|
61
66
|
binary,
|
|
62
|
-
archiveBinaryRelativePath
|
|
67
|
+
archiveBinaryRelativePath,
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -226,11 +231,14 @@ export async function ensureUv(options: EnsureUvOptions): Promise<EnsureUvResult
|
|
|
226
231
|
};
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
// Replace any pre-existing managed binary atomically-ish.
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
// Replace any pre-existing managed binary atomically-ish. Windows uv zips place
|
|
235
|
+
// uv.exe at the archive root, which is already `managedPath` after extraction.
|
|
236
|
+
if (path.resolve(extractedBinary) !== path.resolve(managedPath)) {
|
|
237
|
+
if (fs.existsSync(managedPath)) {
|
|
238
|
+
try { fs.unlinkSync(managedPath); } catch { /* best effort */ }
|
|
239
|
+
}
|
|
240
|
+
fs.renameSync(extractedBinary, managedPath);
|
|
232
241
|
}
|
|
233
|
-
fs.renameSync(extractedBinary, managedPath);
|
|
234
242
|
if (process.platform !== "win32") {
|
|
235
243
|
fs.chmodSync(managedPath, 0o755);
|
|
236
244
|
}
|
|
@@ -278,7 +278,7 @@ async function executeApproveFlow(
|
|
|
278
278
|
debugLogger?.log("execution_handoff_new_session_cancelled", {
|
|
279
279
|
planPath,
|
|
280
280
|
});
|
|
281
|
-
ctx.ui
|
|
281
|
+
ctx.ui?.notify?.("Session start cancelled. Plan saved; run /supi:plan again to execute.");
|
|
282
282
|
return;
|
|
283
283
|
}
|
|
284
284
|
platform.sendUserMessage(prompt);
|
|
@@ -298,7 +298,7 @@ async function executeApproveFlow(
|
|
|
298
298
|
debugLogger?.log("execution_handoff_same_session_steer_sent", {
|
|
299
299
|
planPath,
|
|
300
300
|
});
|
|
301
|
-
ctx.ui
|
|
301
|
+
ctx.ui?.notify?.("Plan approved — starting execution");
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -408,25 +408,23 @@ export function registerPlanApprovalHook(platform: Platform): void {
|
|
|
408
408
|
});
|
|
409
409
|
} catch {}
|
|
410
410
|
if (!ctx?.hasUI) {
|
|
411
|
-
|
|
412
|
-
`Plan saved to \`${planPath}\`.`,
|
|
413
|
-
"Interactive approval is unavailable in this runtime, so no execution was started.",
|
|
414
|
-
`To continue manually, explicitly send: \`Execute the saved plan at ${planPath} step by step; verify each step before proceeding.\``,
|
|
415
|
-
].join("\n");
|
|
416
|
-
debugLogger?.log("approval_flow_no_ui", {
|
|
411
|
+
debugLogger?.log("approval_flow_no_ui_auto_execute", {
|
|
417
412
|
planName,
|
|
418
413
|
planPath,
|
|
419
414
|
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
{
|
|
423
|
-
customType: "supi-plan-awaiting-interactive-approval",
|
|
424
|
-
content: [{ type: "text", text: message }],
|
|
425
|
-
display: true,
|
|
426
|
-
},
|
|
427
|
-
{ deliverAs: "steer", triggerTurn: false },
|
|
428
|
-
);
|
|
415
|
+
const executionNewSession = capturedNewSession;
|
|
416
|
+
const executionModel = capturedResolvedModel;
|
|
429
417
|
cancelPlanTracking();
|
|
418
|
+
await executeApproveFlow(
|
|
419
|
+
platform,
|
|
420
|
+
ctx,
|
|
421
|
+
canonicalContent,
|
|
422
|
+
planPath,
|
|
423
|
+
executionNewSession,
|
|
424
|
+
executionModel,
|
|
425
|
+
debugLogger,
|
|
426
|
+
parsedPlan,
|
|
427
|
+
);
|
|
430
428
|
return;
|
|
431
429
|
}
|
|
432
430
|
|
package/src/types.ts
CHANGED
|
@@ -1550,6 +1550,16 @@ export type HarnessStage =
|
|
|
1550
1550
|
| "docs"
|
|
1551
1551
|
| "validate";
|
|
1552
1552
|
|
|
1553
|
+
/** Progress event emitted by harness pipeline drivers and long-running stages. */
|
|
1554
|
+
export type HarnessPipelineProgressEvent =
|
|
1555
|
+
| { type: "stage-started"; stage: HarnessStage }
|
|
1556
|
+
| { type: "stage-progress"; stage: HarnessStage; detail: string }
|
|
1557
|
+
| { type: "stage-skipped"; stage: HarnessStage }
|
|
1558
|
+
| { type: "stage-completed"; stage: HarnessStage; detail?: string }
|
|
1559
|
+
| { type: "stage-blocked"; stage: HarnessStage; detail: string }
|
|
1560
|
+
| { type: "stage-failed"; stage: HarnessStage; detail: string }
|
|
1561
|
+
| { type: "awaiting-user"; stage: HarnessStage; detail?: string };
|
|
1562
|
+
|
|
1553
1563
|
/** Operational status of a harness stage. Mirrors UltraPlanAuthoringStageStatus. */
|
|
1554
1564
|
export type HarnessStageStatus =
|
|
1555
1565
|
| "pending"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
import type { ExecOptions, ExecResult } from "../platform/types.js";
|
|
5
|
+
import { findExecutable } from "./executable.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cross-platform invocation for npm/npx that survives Windows `.cmd` shims.
|
|
9
|
+
*
|
|
10
|
+
* OMP's `platform.exec` is a thin wrapper over libuv's `uv_spawn`. On Windows
|
|
11
|
+
* that exposes two distinct bugs when the target is an npm-shipped CLI:
|
|
12
|
+
*
|
|
13
|
+
* 1. libuv does not consult `PATHEXT`, so spawning `"npm"` fails with
|
|
14
|
+
* `ENOENT: uv_spawn 'npm'` because the on-disk file is `npm.cmd`.
|
|
15
|
+
* 2. Even when callers resolve the absolute path, Node ≥18.20.2 hard-rejects
|
|
16
|
+
* spawning `.cmd`/`.bat` shims without `shell: true` (CVE-2024-27980).
|
|
17
|
+
* `ExecOptions` does not expose `shell`.
|
|
18
|
+
*
|
|
19
|
+
* Wrapping in `cmd.exe /d /s /c` is the canonical workaround, but only safe
|
|
20
|
+
* when the spawner sets `windowsVerbatimArguments: true` — Node's default
|
|
21
|
+
* CRT escaping double-quotes the command line and cmd's `/s` only strips one
|
|
22
|
+
* pair. We don't control the spawner, so we sidestep the whole problem by
|
|
23
|
+
* resolving the shim to the real `node <cli.js>` invocation, which is exactly
|
|
24
|
+
* what `npm.cmd` does internally. `node.exe` is a plain binary that libuv
|
|
25
|
+
* spawns without ceremony.
|
|
26
|
+
*
|
|
27
|
+
* Non-shim binaries (`bun`, `git`, `node`, `gh`, `rustup`, `go`, `pip`, …
|
|
28
|
+
* all ship as `.exe` on Windows) pass through untouched; POSIX always passes
|
|
29
|
+
* through.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type ExecFn = (
|
|
33
|
+
cmd: string,
|
|
34
|
+
args: string[],
|
|
35
|
+
opts?: ExecOptions,
|
|
36
|
+
) => Promise<ExecResult>;
|
|
37
|
+
|
|
38
|
+
interface ResolvedInvocation {
|
|
39
|
+
cmd: string;
|
|
40
|
+
prefixArgs: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NODE_SHIMS = new Set<string>(["npm", "npx"]);
|
|
44
|
+
const resolutionCache = new Map<string, ResolvedInvocation>();
|
|
45
|
+
|
|
46
|
+
function resolveNodeShim(command: string): ResolvedInvocation | null {
|
|
47
|
+
if (!NODE_SHIMS.has(command)) return null;
|
|
48
|
+
|
|
49
|
+
// node.exe is the executor; npm-cli.js / npx-cli.js sits next to it under
|
|
50
|
+
// node_modules/npm/bin/. We deliberately key off node's location (not the
|
|
51
|
+
// shim's) because `npm.cmd` can live in a user-global dir (e.g. nvm,
|
|
52
|
+
// %AppData%\npm) while the actual CLI bundle stays alongside node.
|
|
53
|
+
const nodeBin = findExecutable("node");
|
|
54
|
+
if (!nodeBin) return null;
|
|
55
|
+
|
|
56
|
+
const cliJs = join(
|
|
57
|
+
dirname(nodeBin),
|
|
58
|
+
"node_modules",
|
|
59
|
+
"npm",
|
|
60
|
+
"bin",
|
|
61
|
+
`${command}-cli.js`,
|
|
62
|
+
);
|
|
63
|
+
if (!existsSync(cliJs)) return null;
|
|
64
|
+
|
|
65
|
+
return { cmd: nodeBin, prefixArgs: [cliJs] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveInvocation(command: string): ResolvedInvocation {
|
|
69
|
+
if (process.platform !== "win32") {
|
|
70
|
+
return { cmd: command, prefixArgs: [] };
|
|
71
|
+
}
|
|
72
|
+
const cached = resolutionCache.get(command);
|
|
73
|
+
if (cached) return cached;
|
|
74
|
+
|
|
75
|
+
const resolved = resolveNodeShim(command) ?? { cmd: command, prefixArgs: [] };
|
|
76
|
+
resolutionCache.set(command, resolved);
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drop-in replacement for `platform.exec` callers that invoke npm/npx by name.
|
|
82
|
+
* Other commands pass through unchanged.
|
|
83
|
+
*/
|
|
84
|
+
export function execCli(
|
|
85
|
+
exec: ExecFn,
|
|
86
|
+
command: string,
|
|
87
|
+
args: string[],
|
|
88
|
+
opts?: ExecOptions,
|
|
89
|
+
): Promise<ExecResult> {
|
|
90
|
+
const resolved = resolveInvocation(command);
|
|
91
|
+
return exec(resolved.cmd, [...resolved.prefixArgs, ...args], opts);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wrap an `ExecFn` so every call routes through `execCli`. Use when threading
|
|
96
|
+
* an exec callback into helpers that dispatch arbitrary tools by string name
|
|
97
|
+
* (e.g. the deps installer which splits install-command strings).
|
|
98
|
+
*/
|
|
99
|
+
export function wrapExecForCli(exec: ExecFn): ExecFn {
|
|
100
|
+
return (cmd, args, opts) => execCli(exec, cmd, args, opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Clears cached CLI resolutions after PATH or tooling changes. */
|
|
104
|
+
export function clearExecCliResolutionCache(): void {
|
|
105
|
+
resolutionCache.clear();
|
|
106
|
+
}
|