ubume 0.1.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/README.md +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AVAILABLE_BACKENDS, DEFAULT_BACKEND } from "../../config/settings.js";
|
|
2
|
+
import { codexSubprocessProvider } from "./codexSubprocess.js";
|
|
3
|
+
import { openaiNativeProvider } from "./openaiNative.js";
|
|
4
|
+
import type { BackendProvider } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const BACKEND_PROVIDERS: BackendProvider[] = [
|
|
7
|
+
codexSubprocessProvider,
|
|
8
|
+
openaiNativeProvider,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function getBackendProvider(id: string): BackendProvider {
|
|
12
|
+
return (
|
|
13
|
+
BACKEND_PROVIDERS.find((provider) => provider.id === id) ??
|
|
14
|
+
BACKEND_PROVIDERS.find((provider) => provider.id === DEFAULT_BACKEND) ??
|
|
15
|
+
codexSubprocessProvider
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function listBackendSummaries(): string {
|
|
20
|
+
return AVAILABLE_BACKENDS.map((backend, index) => ` ${index + 1}. ${backend.label} (${backend.id})`).join("\n");
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AvailableBackend } from "../../config/settings.js";
|
|
2
|
+
import type { ResolvedRuntimeConfig } from "../../config/runtimeConfig.js";
|
|
3
|
+
import type { ProjectInstructions } from "../workspace/projectInstructions.js";
|
|
4
|
+
import type { RunProgressSource, RunToolActivity } from "../../session/types.js";
|
|
5
|
+
import type { ConversationContextCheckpoint, ConversationMessage, LocalHarnessSessionMetadata } from "../workspace/conversationStore.js";
|
|
6
|
+
import type { ProviderImageAttachment } from "../providerRuntime/types.js";
|
|
7
|
+
|
|
8
|
+
export interface ProviderContextUsage {
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
contextTokens: number;
|
|
12
|
+
contextWindow: number | null;
|
|
13
|
+
exact: boolean;
|
|
14
|
+
compacted?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BackendProgressUpdate {
|
|
18
|
+
id: string;
|
|
19
|
+
source: RunProgressSource;
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ToolApprovalDecision = "allow-once" | "allow-for-run" | "deny";
|
|
24
|
+
|
|
25
|
+
export interface ToolApprovalRequest {
|
|
26
|
+
tool: string;
|
|
27
|
+
signature: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
paths: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type BackendAuthState = "delegated" | "api-key-required" | "coming-soon";
|
|
33
|
+
|
|
34
|
+
export interface BackendRunHandlers {
|
|
35
|
+
onResponse: (response: string) => void;
|
|
36
|
+
onError: (message: string, rawOutput?: string) => void;
|
|
37
|
+
/** Called with each new structured thinking/progress update while the process is still running. */
|
|
38
|
+
onProgress?: (update: BackendProgressUpdate) => void;
|
|
39
|
+
/** Called with each new assistant content delta while the process is still running. */
|
|
40
|
+
onAssistantDelta?: (chunk: string) => void;
|
|
41
|
+
/** Called when the backend indicates the final assistant answer is complete and visible. */
|
|
42
|
+
onFinalAnswerObserved?: (response: string) => void;
|
|
43
|
+
/** Called when the backend starts or finishes a tool/shell action during a run. */
|
|
44
|
+
onToolActivity?: (activity: RunToolActivity) => void;
|
|
45
|
+
/** Requests consent before a local model performs a mutating action. */
|
|
46
|
+
onToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalDecision>;
|
|
47
|
+
/** Persists invisible rolling memory used only by Local context-window rollover. */
|
|
48
|
+
onLocalContextCheckpoint?: (checkpoint: ConversationContextCheckpoint) => void;
|
|
49
|
+
/** Persists the opaque DeepSeek Harness session backing a Local conversation. */
|
|
50
|
+
onLocalHarnessSession?: (session: LocalHarnessSessionMetadata | null, sessionId: string) => void;
|
|
51
|
+
/** Reports authoritative provider token usage when available. */
|
|
52
|
+
onContextUsage?: (usage: ProviderContextUsage) => void;
|
|
53
|
+
/** Called around backend child-process lifecycle boundaries. */
|
|
54
|
+
onProcessLifecycle?: (event: "before-spawn" | "spawned" | "exit" | "error" | "cleanup") => void;
|
|
55
|
+
/** Lightweight hooks used only by headless benchmark diagnostics. */
|
|
56
|
+
benchmarkHooks?: {
|
|
57
|
+
onProviderPromptPrepared?: (context: { policy: "raw" | "wrapped"; characterCount: number }) => void;
|
|
58
|
+
onProviderPrepStart?: () => void;
|
|
59
|
+
onProviderPrepComplete?: () => void;
|
|
60
|
+
onCodexProcessSpawned?: (context: { executable: string; argv: string[] }) => void;
|
|
61
|
+
onFirstStdout?: (observed?: boolean) => void;
|
|
62
|
+
onFirstStderr?: (observed?: boolean) => void;
|
|
63
|
+
onCodexProcessExit?: (exitCode: number | null) => void;
|
|
64
|
+
onCleanupStart?: () => void;
|
|
65
|
+
onCleanupComplete?: (context: { skipped: boolean }) => void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BackendProvider {
|
|
70
|
+
id: AvailableBackend;
|
|
71
|
+
label: string;
|
|
72
|
+
description: string;
|
|
73
|
+
authState: BackendAuthState;
|
|
74
|
+
authLabel: string;
|
|
75
|
+
statusMessage: string;
|
|
76
|
+
supportsModels: (model: string) => boolean;
|
|
77
|
+
run?: (
|
|
78
|
+
prompt: string,
|
|
79
|
+
options: {
|
|
80
|
+
runtime: ResolvedRuntimeConfig;
|
|
81
|
+
workspaceRoot: string;
|
|
82
|
+
projectInstructions?: ProjectInstructions | null;
|
|
83
|
+
promptPolicy?: "raw" | "wrapped";
|
|
84
|
+
runIntent?: "normal" | "plan" | "approved-execution";
|
|
85
|
+
conversationHistory?: readonly ConversationMessage[];
|
|
86
|
+
localContextCheckpoint?: ConversationContextCheckpoint;
|
|
87
|
+
imageAttachments?: readonly ProviderImageAttachment[];
|
|
88
|
+
},
|
|
89
|
+
handlers: BackendRunHandlers,
|
|
90
|
+
) => () => void;
|
|
91
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
isSkippedExternalDependencyPath,
|
|
6
|
+
normalizeDiagnosticPath,
|
|
7
|
+
} from "../workspace/workspaceGuard.js";
|
|
8
|
+
|
|
9
|
+
export const IMAGE_EXTENSIONS = new Set([
|
|
10
|
+
".png",
|
|
11
|
+
".jpg",
|
|
12
|
+
".jpeg",
|
|
13
|
+
".gif",
|
|
14
|
+
".webp",
|
|
15
|
+
".bmp",
|
|
16
|
+
".tiff",
|
|
17
|
+
".tif",
|
|
18
|
+
".svg",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function isImageFile(filePath: string): boolean {
|
|
22
|
+
return IMAGE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
26
|
+
try {
|
|
27
|
+
await access(filePath);
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function resolveAttachmentDestPath(
|
|
35
|
+
srcPath: string,
|
|
36
|
+
attachmentsDir: string,
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const ext = path.extname(srcPath);
|
|
39
|
+
const base = path.basename(srcPath, ext);
|
|
40
|
+
let dest = path.join(attachmentsDir, `${base}${ext}`);
|
|
41
|
+
let counter = 1;
|
|
42
|
+
while (await fileExists(dest)) {
|
|
43
|
+
dest = path.join(attachmentsDir, `${base}-${counter}${ext}`);
|
|
44
|
+
counter++;
|
|
45
|
+
}
|
|
46
|
+
return dest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function importExternalFile(
|
|
50
|
+
srcPath: string,
|
|
51
|
+
attachmentsDir: string,
|
|
52
|
+
): Promise<string | null> {
|
|
53
|
+
const normalized = normalizeDiagnosticPath(srcPath);
|
|
54
|
+
|
|
55
|
+
if (isSkippedExternalDependencyPath(normalized)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (!existsSync(normalized)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const s = await stat(normalized);
|
|
64
|
+
if (!s.isFile()) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
72
|
+
const destPath = await resolveAttachmentDestPath(normalized, attachmentsDir);
|
|
73
|
+
await copyFile(normalized, destPath);
|
|
74
|
+
return destPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function saveClipboardImage(data: Uint8Array, attachmentsDir: string): Promise<string> {
|
|
78
|
+
await mkdir(attachmentsDir, { recursive: true });
|
|
79
|
+
const destPath = await resolveAttachmentDestPath("clipboard-image.png", attachmentsDir);
|
|
80
|
+
await writeFile(destPath, data, { mode: 0o600 });
|
|
81
|
+
return destPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function rewritePromptWithImportedPaths(
|
|
85
|
+
prompt: string,
|
|
86
|
+
replacements: Array<{ rawPath: string; replacementPath: string }>,
|
|
87
|
+
): string {
|
|
88
|
+
let result = prompt;
|
|
89
|
+
for (const { rawPath, replacementPath } of replacements) {
|
|
90
|
+
const needsQuotes = /\s/.test(replacementPath);
|
|
91
|
+
const quotedReplacement = needsQuotes
|
|
92
|
+
? `"${replacementPath}"`
|
|
93
|
+
: replacementPath;
|
|
94
|
+
// Replace quoted forms first so the unquoted pass doesn't double-replace
|
|
95
|
+
result = result.split(`"${rawPath}"`).join(quotedReplacement);
|
|
96
|
+
result = result.split(`'${rawPath}'`).join(quotedReplacement);
|
|
97
|
+
// Replace any remaining unquoted occurrences
|
|
98
|
+
result = result.split(rawPath).join(quotedReplacement);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RunToolActivity } from "../../session/types.js";
|
|
2
|
+
|
|
3
|
+
const DELETE_COMMAND_PATTERN =
|
|
4
|
+
/(?:^|[\s;&|])(?:remove-item|rm|rmdir|del|erase|unlink)\b/i;
|
|
5
|
+
|
|
6
|
+
const BLOCKED_DELETE_CAUSE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
7
|
+
{ pattern: /(?:^|[\\/])\.git[\\/][^\s"'`]*\.lock\b|(?:^|[\\/])?config\.lock\b|\.lock\b/i, label: "lock artifact" },
|
|
8
|
+
{ pattern: /\bEACCES\b/i, label: "access denied" },
|
|
9
|
+
{ pattern: /\bEPERM\b/i, label: "permission denied" },
|
|
10
|
+
{ pattern: /\bEBUSY\b/i, label: "file is busy or locked" },
|
|
11
|
+
{ pattern: /access(?:\s+to\s+the\s+path)?\s+.*?\s+denied|access is denied/i, label: "access denied" },
|
|
12
|
+
{ pattern: /permission denied|operation not permitted/i, label: "permission denied" },
|
|
13
|
+
{ pattern: /being used by another process|file is in use|resource busy|text file busy|device or resource busy/i, label: "file is locked or in use" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const PATH_PATTERNS = [
|
|
17
|
+
/Access to the path ['"]([^'"]+)['"] is denied/i,
|
|
18
|
+
/(?:EPERM|EACCES|EBUSY)[^,\n\r]*,\s*(?:unlink|rmdir|rm|open|scandir)\s+['"]?([^'"\n\r]+)['"]?/i,
|
|
19
|
+
/(?:cannot|can't|failed to|unable to)\s+(?:remove|delete|unlink|rmdir)[^'"\n\r]*['"]([^'"]+)['"]/i,
|
|
20
|
+
/(?:being used by another process|file is in use|permission denied|access is denied)[^'"\n\r]*['"]([^'"]+)['"]/i,
|
|
21
|
+
/((?:\.git[\\/])?[^\s"'`]+\.lock)\b/i,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function normalizeText(value: string | null | undefined): string {
|
|
25
|
+
return (value ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isDeleteCommand(command: string): boolean {
|
|
29
|
+
return DELETE_COMMAND_PATTERN.test(command);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findCause(text: string): string | null {
|
|
33
|
+
for (const { pattern, label } of BLOCKED_DELETE_CAUSE_PATTERNS) {
|
|
34
|
+
if (pattern.test(text)) return label;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findBlockedPath(text: string): string | null {
|
|
40
|
+
for (const pattern of PATH_PATTERNS) {
|
|
41
|
+
const match = pattern.exec(text);
|
|
42
|
+
const path = match?.[1]?.trim();
|
|
43
|
+
if (path) return path.replace(/[.,;:]+$/g, "");
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getBlockedCleanupFailure(activity: RunToolActivity): string | null {
|
|
49
|
+
if (activity.status !== "failed") return null;
|
|
50
|
+
|
|
51
|
+
const command = normalizeText(activity.command);
|
|
52
|
+
const summary = normalizeText(activity.summary);
|
|
53
|
+
const combined = [command, summary].filter(Boolean).join("\n");
|
|
54
|
+
if (!command || !isDeleteCommand(command)) return null;
|
|
55
|
+
|
|
56
|
+
const cause = findCause(combined);
|
|
57
|
+
if (!cause) return null;
|
|
58
|
+
|
|
59
|
+
const blockedPath = findBlockedPath(combined);
|
|
60
|
+
const target = blockedPath ? `\nBlocked item: ${blockedPath}` : "";
|
|
61
|
+
return [
|
|
62
|
+
"Cleanup stopped because a safe generated artifact could not be deleted.",
|
|
63
|
+
`Cause: ${cause}.`,
|
|
64
|
+
target,
|
|
65
|
+
"Ubume stopped after the first clear blocked-delete signal to avoid retrying a doomed cleanup.",
|
|
66
|
+
].filter(Boolean).join("\n");
|
|
67
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { platform } from "os";
|
|
3
|
+
|
|
4
|
+
function trySpawn(cmd: string, args: string[], text: string): Promise<boolean> {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
7
|
+
proc.on("error", () => resolve(false));
|
|
8
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
9
|
+
proc.stdin?.write(text);
|
|
10
|
+
proc.stdin?.end();
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
15
|
+
const os = platform();
|
|
16
|
+
|
|
17
|
+
if (os === "win32") return trySpawn("clip", [], text);
|
|
18
|
+
if (os === "darwin") return trySpawn("pbcopy", [], text);
|
|
19
|
+
|
|
20
|
+
// Linux: try xclip, fall back to xsel
|
|
21
|
+
const ok = await trySpawn("xclip", ["-selection", "clipboard"], text);
|
|
22
|
+
if (ok) return true;
|
|
23
|
+
return trySpawn("xsel", ["--clipboard", "--input"], text);
|
|
24
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const MAX_CLIPBOARD_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
export interface ClipboardImage {
|
|
9
|
+
data: Buffer;
|
|
10
|
+
mediaType: "image/png";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type CommandRunner = (file: string, args: string[]) => Promise<Buffer>;
|
|
14
|
+
|
|
15
|
+
function runBuffer(file: string, args: string[]): Promise<Buffer> {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
execFile(file, args, { encoding: "buffer", maxBuffer: MAX_CLIPBOARD_IMAGE_BYTES + 1024 }, (error, stdout) => {
|
|
18
|
+
if (error) reject(error);
|
|
19
|
+
else resolve(Buffer.from(stdout));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isPng(data: Buffer): boolean {
|
|
25
|
+
return data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validatePng(data: Buffer): ClipboardImage {
|
|
29
|
+
if (!isPng(data)) throw new Error("The clipboard does not contain a supported PNG image.");
|
|
30
|
+
if (data.length > MAX_CLIPBOARD_IMAGE_BYTES) throw new Error("The clipboard image exceeds the 20 MiB limit.");
|
|
31
|
+
return { data, mediaType: "image/png" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function firstSuccessful(attempts: Array<() => Promise<Buffer>>): Promise<Buffer> {
|
|
35
|
+
for (const attempt of attempts) {
|
|
36
|
+
try {
|
|
37
|
+
const data = await attempt();
|
|
38
|
+
if (data.length > 0) return data;
|
|
39
|
+
} catch {
|
|
40
|
+
// Try the next platform-appropriate clipboard bridge.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error("No PNG image is available on the system clipboard.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readMacClipboard(run: CommandRunner): Promise<Buffer> {
|
|
47
|
+
try {
|
|
48
|
+
return await run("pngpaste", ["-"]);
|
|
49
|
+
} catch {
|
|
50
|
+
const tempDir = await mkdtemp(join(tmpdir(), "ubume-clipboard-"));
|
|
51
|
+
const outputPath = join(tempDir, "clipboard.png");
|
|
52
|
+
const applePath = outputPath.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
|
|
53
|
+
try {
|
|
54
|
+
await run("osascript", [
|
|
55
|
+
"-e", "set imageData to the clipboard as «class PNGf»",
|
|
56
|
+
"-e", `set imageFile to open for access POSIX file \"${applePath}\" with write permission`,
|
|
57
|
+
"-e", "set eof imageFile to 0",
|
|
58
|
+
"-e", "write imageData to imageFile",
|
|
59
|
+
"-e", "close access imageFile",
|
|
60
|
+
]);
|
|
61
|
+
return await readFile(outputPath);
|
|
62
|
+
} finally {
|
|
63
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const WINDOWS_CLIPBOARD_SCRIPT = [
|
|
69
|
+
"Add-Type -AssemblyName System.Windows.Forms;",
|
|
70
|
+
"Add-Type -AssemblyName System.Drawing;",
|
|
71
|
+
"$image=[System.Windows.Forms.Clipboard]::GetImage();",
|
|
72
|
+
"if ($null -eq $image) { exit 2 };",
|
|
73
|
+
"$stream=New-Object System.IO.MemoryStream;",
|
|
74
|
+
"$image.Save($stream,[System.Drawing.Imaging.ImageFormat]::Png);",
|
|
75
|
+
"[Console]::Out.Write([Convert]::ToBase64String($stream.ToArray()));",
|
|
76
|
+
].join("");
|
|
77
|
+
|
|
78
|
+
async function readWindowsClipboard(run: CommandRunner, executable = "powershell.exe"): Promise<Buffer> {
|
|
79
|
+
const encoded = await run(executable, ["-NoProfile", "-NonInteractive", "-STA", "-Command", WINDOWS_CLIPBOARD_SCRIPT]);
|
|
80
|
+
return Buffer.from(encoded.toString("utf8").trim(), "base64");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function readClipboardImage(options: {
|
|
84
|
+
platform?: NodeJS.Platform;
|
|
85
|
+
env?: NodeJS.ProcessEnv;
|
|
86
|
+
run?: CommandRunner;
|
|
87
|
+
} = {}): Promise<ClipboardImage> {
|
|
88
|
+
const targetPlatform = options.platform ?? process.platform;
|
|
89
|
+
const env = options.env ?? process.env;
|
|
90
|
+
const run = options.run ?? runBuffer;
|
|
91
|
+
let data: Buffer;
|
|
92
|
+
|
|
93
|
+
if (targetPlatform === "win32") {
|
|
94
|
+
data = await readWindowsClipboard(run);
|
|
95
|
+
} else if (targetPlatform === "darwin") {
|
|
96
|
+
data = await readMacClipboard(run);
|
|
97
|
+
} else if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) {
|
|
98
|
+
data = await firstSuccessful([
|
|
99
|
+
() => readWindowsClipboard(run),
|
|
100
|
+
() => run("wl-paste", ["--no-newline", "--type", "image/png"]),
|
|
101
|
+
() => run("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]),
|
|
102
|
+
]);
|
|
103
|
+
} else {
|
|
104
|
+
data = await firstSuccessful([
|
|
105
|
+
() => run("wl-paste", ["--no-newline", "--type", "image/png"]),
|
|
106
|
+
() => run("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return validatePng(data);
|
|
111
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface RepoIdentity {
|
|
6
|
+
owner: string;
|
|
7
|
+
repo: string;
|
|
8
|
+
provider: "github" | "other";
|
|
9
|
+
remoteUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DiagnosticResult {
|
|
13
|
+
path: string;
|
|
14
|
+
status: "PASS" | "FAIL" | "PARTIAL";
|
|
15
|
+
evidence: string;
|
|
16
|
+
blocker: string | null;
|
|
17
|
+
recommendedUse: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiagnosticsReport {
|
|
21
|
+
repo: RepoIdentity | null;
|
|
22
|
+
defaultBranch: string | null;
|
|
23
|
+
ghCliUser: string | null;
|
|
24
|
+
connectorUser: string | null;
|
|
25
|
+
paths: {
|
|
26
|
+
ghCli: DiagnosticResult;
|
|
27
|
+
localGit: DiagnosticResult;
|
|
28
|
+
localGitWrite: DiagnosticResult;
|
|
29
|
+
connector: DiagnosticResult;
|
|
30
|
+
};
|
|
31
|
+
recommendedFlow:
|
|
32
|
+
| "Local Git + GH CLI"
|
|
33
|
+
| "Local Git + connector PR creation"
|
|
34
|
+
| "Connector-only"
|
|
35
|
+
| "Cannot publish yet";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseRepoIdentity(remoteUrl: string | undefined | null): RepoIdentity | null {
|
|
39
|
+
if (!remoteUrl) return null;
|
|
40
|
+
|
|
41
|
+
const url = remoteUrl.trim();
|
|
42
|
+
|
|
43
|
+
// HTTPS: https://github.com/owner/repo.git or https://github.com/owner/repo
|
|
44
|
+
const httpsMatch = url.match(/^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
|
|
45
|
+
if (httpsMatch) {
|
|
46
|
+
return {
|
|
47
|
+
owner: httpsMatch[1],
|
|
48
|
+
repo: httpsMatch[2],
|
|
49
|
+
provider: "github",
|
|
50
|
+
remoteUrl: url,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// SSH: git@github.com:owner/repo.git or ssh://git@github.com/owner/repo.git
|
|
55
|
+
const sshMatch = url.match(/^(?:ssh:\/\/)?git@github\.com[:\/]([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
|
|
56
|
+
if (sshMatch) {
|
|
57
|
+
return {
|
|
58
|
+
owner: sshMatch[1],
|
|
59
|
+
repo: sshMatch[2],
|
|
60
|
+
provider: "github",
|
|
61
|
+
remoteUrl: url,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
owner: "",
|
|
67
|
+
repo: "",
|
|
68
|
+
provider: "other",
|
|
69
|
+
remoteUrl: url,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getLocalGitRemoteUrl(): string | null {
|
|
74
|
+
try {
|
|
75
|
+
return execSync("git remote get-url origin", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function checkGhCli(): DiagnosticResult {
|
|
82
|
+
const result: DiagnosticResult = {
|
|
83
|
+
path: "GH CLI",
|
|
84
|
+
status: "FAIL",
|
|
85
|
+
evidence: "",
|
|
86
|
+
blocker: null,
|
|
87
|
+
recommendedUse: false,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const version = execSync("gh --version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0];
|
|
92
|
+
result.evidence = version ?? "Unknown version";
|
|
93
|
+
} catch {
|
|
94
|
+
result.blocker = "gh CLI not installed or not in PATH";
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// gh auth status output format is not structured JSON; pattern-match on known strings.
|
|
100
|
+
const authStatus = execSync("gh auth status", { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
101
|
+
result.evidence += " | Authenticated";
|
|
102
|
+
if (authStatus.includes("Token scopes")) {
|
|
103
|
+
const scopes = authStatus.match(/Token scopes: (.*)/)?.[1];
|
|
104
|
+
if (scopes && !scopes.includes("repo")) {
|
|
105
|
+
result.status = "PARTIAL";
|
|
106
|
+
result.blocker = "Token missing 'repo' scope";
|
|
107
|
+
} else {
|
|
108
|
+
result.status = "PASS";
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
result.status = "PASS";
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
result.blocker = "Not logged in to GitHub CLI";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function checkLocalGitRemote(): DiagnosticResult {
|
|
121
|
+
const result: DiagnosticResult = {
|
|
122
|
+
path: "Local git remote",
|
|
123
|
+
status: "FAIL",
|
|
124
|
+
evidence: "",
|
|
125
|
+
blocker: null,
|
|
126
|
+
recommendedUse: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const remote = execSync("git remote -v", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n")[0];
|
|
131
|
+
result.evidence = remote ?? "No remote found";
|
|
132
|
+
|
|
133
|
+
execSync("git ls-remote origin HEAD", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
134
|
+
result.status = "PASS";
|
|
135
|
+
} catch {
|
|
136
|
+
result.blocker = "Cannot reach origin remote (check connectivity or remote URL)";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function checkLocalGitWrite(): DiagnosticResult {
|
|
143
|
+
const result: DiagnosticResult = {
|
|
144
|
+
path: "Local .git write",
|
|
145
|
+
status: "FAIL",
|
|
146
|
+
evidence: "",
|
|
147
|
+
blocker: null,
|
|
148
|
+
recommendedUse: false,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const indexLock = join(".git", "index.lock");
|
|
152
|
+
if (existsSync(indexLock)) {
|
|
153
|
+
result.blocker = ".git/index.lock exists (git process might be running)";
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
execSync("git update-ref refs/heads/ubume-diagnostic-lock-test HEAD", { stdio: "ignore" });
|
|
159
|
+
execSync("git update-ref -d refs/heads/ubume-diagnostic-lock-test", { stdio: "ignore" });
|
|
160
|
+
result.status = "PASS";
|
|
161
|
+
result.evidence = "Can create/delete refs";
|
|
162
|
+
} catch (error) {
|
|
163
|
+
result.blocker = "Failed to create/delete ref lock (permission denied?)";
|
|
164
|
+
result.evidence = error instanceof Error ? error.message : String(error);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function classifyDiagnostics(
|
|
171
|
+
repo: RepoIdentity | null,
|
|
172
|
+
ghCli: DiagnosticResult,
|
|
173
|
+
localGit: DiagnosticResult,
|
|
174
|
+
localGitWrite: DiagnosticResult,
|
|
175
|
+
connector: DiagnosticResult
|
|
176
|
+
): DiagnosticsReport["recommendedFlow"] {
|
|
177
|
+
const isGitHub = repo?.provider === "github";
|
|
178
|
+
if (!isGitHub) return "Cannot publish yet";
|
|
179
|
+
|
|
180
|
+
const ghCliOk = ghCli.status === "PASS";
|
|
181
|
+
const gitRemoteOk = localGit.status === "PASS";
|
|
182
|
+
const gitWriteOk = localGitWrite.status === "PASS";
|
|
183
|
+
const connectorOk = connector.status === "PASS" || (connector.status === "PARTIAL" && !connector.blocker?.includes("auth"));
|
|
184
|
+
|
|
185
|
+
if (ghCliOk && gitRemoteOk && gitWriteOk) {
|
|
186
|
+
return "Local Git + GH CLI";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (connectorOk) {
|
|
190
|
+
if (gitWriteOk && gitRemoteOk) {
|
|
191
|
+
return "Local Git + connector PR creation";
|
|
192
|
+
}
|
|
193
|
+
return "Connector-only";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return "Cannot publish yet";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function printDiagnosticsTable(report: DiagnosticsReport) {
|
|
200
|
+
const rows = [
|
|
201
|
+
report.paths.ghCli,
|
|
202
|
+
report.paths.localGit,
|
|
203
|
+
report.paths.localGitWrite,
|
|
204
|
+
report.paths.connector,
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
console.log("\nPath | Status | Evidence | Blocker");
|
|
208
|
+
console.log("--------------------|---------|-------------------------------|---------------------------");
|
|
209
|
+
for (const row of rows) {
|
|
210
|
+
const p = row.path.padEnd(20);
|
|
211
|
+
const s = row.status.padEnd(8);
|
|
212
|
+
const e = (row.evidence || "").substring(0, 30).padEnd(30);
|
|
213
|
+
const b = row.blocker || "";
|
|
214
|
+
console.log(`${p}| ${s}| ${e}| ${b}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`\nResolved repo: ${report.repo ? `${report.repo.owner}/${report.repo.repo}` : "Unknown"}`);
|
|
218
|
+
console.log(`Default branch: ${report.defaultBranch || "Unknown"}`);
|
|
219
|
+
console.log(`Authenticated GH CLI user: ${report.ghCliUser || "Unknown"}`);
|
|
220
|
+
console.log(`Authenticated connector user: ${report.connectorUser || "Unknown"}`);
|
|
221
|
+
console.log(`Recommended PR flow: ${report.recommendedFlow}`);
|
|
222
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HollowResponseResult } from "../codex/codexPrompt.js";
|
|
2
|
+
|
|
3
|
+
const MESSAGES: Record<string, [string, string]> = {
|
|
4
|
+
greeting: [
|
|
5
|
+
"Task not executed — backend returned a generic greeting.",
|
|
6
|
+
"Retry with a more specific instruction.",
|
|
7
|
+
],
|
|
8
|
+
filler: [
|
|
9
|
+
"Task not executed — backend acknowledged without acting.",
|
|
10
|
+
"Retry with a more specific instruction.",
|
|
11
|
+
],
|
|
12
|
+
clarification: [
|
|
13
|
+
"Task not executed — backend asked for clarification instead of acting.",
|
|
14
|
+
"Rephrase with more detail, or switch to suggest mode.",
|
|
15
|
+
],
|
|
16
|
+
"short-no-action": [
|
|
17
|
+
"No action confirmed — response too brief for a write-intent prompt.",
|
|
18
|
+
"Verify workspace files manually, or retry.",
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function formatHollowResponse(
|
|
23
|
+
result: HollowResponseResult,
|
|
24
|
+
rawResponse?: string,
|
|
25
|
+
verbose?: boolean,
|
|
26
|
+
): string {
|
|
27
|
+
const lines = MESSAGES[result.kind] ?? [
|
|
28
|
+
"Task not executed — unexpected backend response.",
|
|
29
|
+
"Retry with a more specific instruction.",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
let output = lines.join("\n");
|
|
33
|
+
|
|
34
|
+
if (verbose && rawResponse) {
|
|
35
|
+
output += `\n\nBackend response: ${rawResponse}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return output;
|
|
39
|
+
}
|