@a5c-ai/babysitter-paperclip 0.0.2-staging.02a0ee21

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/src/types.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Shared types for the Babysitter Paperclip plugin.
3
+ */
4
+
5
+ /** Adapter type mapping from Paperclip agent adapterType to babysitter harness name. */
6
+ export const ADAPTER_TYPE_MAP: Record<string, string> = {
7
+ claude_local: "claude-code",
8
+ codex_local: "codex",
9
+ gemini_local: "gemini-cli",
10
+ cursor_local: "cursor",
11
+ github_copilot: "github-copilot",
12
+ opencode_local: "opencode",
13
+ pi_local: "pi",
14
+ omp_local: "oh-my-pi",
15
+ };
16
+
17
+ /** A babysitter run tracked by the plugin. */
18
+ export interface TrackedRun {
19
+ runId: string;
20
+ processId: string;
21
+ agentId: string;
22
+ companyId: string;
23
+ harnessName: string;
24
+ status: "running" | "waiting" | "completed" | "failed";
25
+ createdAt: string;
26
+ lastIteratedAt?: string;
27
+ pendingBreakpoints: PendingBreakpoint[];
28
+ }
29
+
30
+ /** A pending breakpoint awaiting user approval.
31
+ *
32
+ * Breakpoint metadata comes from the SDK's breakpoint intrinsic
33
+ * (runtime/intrinsics/breakpoint.ts). The task.json for a breakpoint has:
34
+ * kind: "breakpoint"
35
+ * metadata.payload: { question, title, options, expert, tags, strategy, ... }
36
+ *
37
+ * The stop hook in the underlying harness (e.g., Claude Code) checks if only
38
+ * breakpoints are pending and allows the agent to exit (approve decision),
39
+ * pausing the orchestration loop until the breakpoint is resolved externally.
40
+ *
41
+ * CRITICAL: Both approve and reject use --status ok.
42
+ * Rejection sends { approved: false, feedback: "..." }.
43
+ * Never use --status error for rejection -- that triggers RUN_FAILED.
44
+ */
45
+ export interface PendingBreakpoint {
46
+ effectId: string;
47
+ title: string;
48
+ question?: string;
49
+ description?: string;
50
+ options?: string[];
51
+ expert?: string | string[];
52
+ tags?: string[];
53
+ strategy?: "single" | "first-response-wins" | "collect-all" | "quorum";
54
+ previousFeedback?: string;
55
+ attempt?: number;
56
+ requestedAt: string;
57
+ }
58
+
59
+ /** Overview data returned by the runs-overview data handler. */
60
+ export interface RunsOverview {
61
+ activeRuns: TrackedRun[];
62
+ pendingBreakpoints: number;
63
+ totalRuns: number;
64
+ }
65
+
66
+ /** Detail data for a single run. */
67
+ export interface RunDetail {
68
+ run: TrackedRun;
69
+ events: RunEvent[];
70
+ pendingEffects: PendingEffect[];
71
+ }
72
+
73
+ /** A journal event from a babysitter run. */
74
+ export interface RunEvent {
75
+ seq: number;
76
+ type: string;
77
+ recordedAt: string;
78
+ data: Record<string, unknown>;
79
+ }
80
+
81
+ /** A pending effect from a babysitter run. */
82
+ export interface PendingEffect {
83
+ effectId: string;
84
+ kind: string;
85
+ label: string;
86
+ taskId: string;
87
+ requestedAt: string;
88
+ }
89
+
90
+ /** Result of detecting which harness an agent uses. */
91
+ export interface HarnessDetectionResult {
92
+ harnessName: string;
93
+ detectionTier: "agent-metadata" | "env-probe" | "config" | "fallback";
94
+ confidence: "high" | "medium" | "low";
95
+ }
96
+
97
+ /**
98
+ * Breakpoint task definition metadata as written by the SDK.
99
+ * Extracted from task.json files in runs/<runId>/tasks/<effectId>/.
100
+ */
101
+ export interface BreakpointTaskDef {
102
+ kind: "breakpoint";
103
+ title: string;
104
+ metadata: {
105
+ payload: {
106
+ question?: string;
107
+ title?: string;
108
+ options?: string[];
109
+ expert?: string | string[];
110
+ tags?: string[];
111
+ strategy?: string;
112
+ previousFeedback?: string;
113
+ attempt?: number;
114
+ [key: string]: unknown;
115
+ };
116
+ requestedAt: string;
117
+ label: string;
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Harness plugin installation status.
123
+ * Used to verify the underlying harness has its babysitter plugin installed.
124
+ */
125
+ export interface HarnessPluginStatus {
126
+ harnessName: string;
127
+ cliAvailable: boolean;
128
+ pluginInstalled: boolean;
129
+ installCommand?: string;
130
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Dashboard widget showing babysitter run overview.
3
+ *
4
+ * Displays active runs, pending breakpoints count, and quick status badges.
5
+ */
6
+
7
+ import { usePluginData } from "@paperclipai/plugin-sdk/ui";
8
+ import type { PluginWidgetProps } from "@paperclipai/plugin-sdk/ui";
9
+
10
+ interface RunsOverview {
11
+ activeRuns: Array<{
12
+ runId: string;
13
+ processId: string;
14
+ status: string;
15
+ pendingBreakpoints: Array<{ effectId: string; title: string }>;
16
+ }>;
17
+ pendingBreakpoints: number;
18
+ totalRuns: number;
19
+ }
20
+
21
+ export function BabysitterDashboard({ context }: PluginWidgetProps) {
22
+ const { data, loading, error, refresh } = usePluginData<RunsOverview>(
23
+ "runs-overview",
24
+ { companyId: context.companyId }
25
+ );
26
+
27
+ if (loading) {
28
+ return <div style={{ padding: 16 }}>Loading runs...</div>;
29
+ }
30
+
31
+ if (error) {
32
+ return (
33
+ <div style={{ padding: 16, color: "var(--destructive)" }}>
34
+ Error: {error.message}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ if (!data) return null;
40
+
41
+ return (
42
+ <div style={{ display: "grid", gap: 12, padding: 16 }}>
43
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
44
+ <strong>Babysitter Orchestration</strong>
45
+ <button onClick={refresh} style={{ fontSize: 12 }}>
46
+ Refresh
47
+ </button>
48
+ </div>
49
+
50
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8 }}>
51
+ <StatCard label="Active" value={data.activeRuns.length} />
52
+ <StatCard label="Breakpoints" value={data.pendingBreakpoints} highlight={data.pendingBreakpoints > 0} />
53
+ <StatCard label="Total" value={data.totalRuns} />
54
+ </div>
55
+
56
+ {data.activeRuns.length > 0 && (
57
+ <div style={{ display: "grid", gap: 4 }}>
58
+ {data.activeRuns.slice(0, 5).map((run) => (
59
+ <div
60
+ key={run.runId}
61
+ style={{
62
+ display: "flex",
63
+ justifyContent: "space-between",
64
+ alignItems: "center",
65
+ padding: "4px 8px",
66
+ borderRadius: 4,
67
+ background: "var(--muted)",
68
+ fontSize: 12,
69
+ }}
70
+ >
71
+ <span style={{ fontFamily: "monospace" }}>
72
+ {run.processId}
73
+ </span>
74
+ <StatusBadge status={run.status} />
75
+ </div>
76
+ ))}
77
+ </div>
78
+ )}
79
+
80
+ {data.activeRuns.length === 0 && (
81
+ <div style={{ color: "var(--muted-foreground)", fontSize: 13 }}>
82
+ No active runs
83
+ </div>
84
+ )}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ function StatCard({
90
+ label,
91
+ value,
92
+ highlight,
93
+ }: {
94
+ label: string;
95
+ value: number;
96
+ highlight?: boolean;
97
+ }) {
98
+ return (
99
+ <div
100
+ style={{
101
+ textAlign: "center",
102
+ padding: "8px 4px",
103
+ borderRadius: 6,
104
+ background: highlight ? "var(--destructive)" : "var(--muted)",
105
+ color: highlight ? "var(--destructive-foreground)" : "inherit",
106
+ }}
107
+ >
108
+ <div style={{ fontSize: 20, fontWeight: 600 }}>{value}</div>
109
+ <div style={{ fontSize: 11, opacity: 0.7 }}>{label}</div>
110
+ </div>
111
+ );
112
+ }
113
+
114
+ function StatusBadge({ status }: { status: string }) {
115
+ const colors: Record<string, string> = {
116
+ running: "#22c55e",
117
+ waiting: "#eab308",
118
+ completed: "#6b7280",
119
+ failed: "#ef4444",
120
+ };
121
+
122
+ return (
123
+ <span
124
+ style={{
125
+ display: "inline-flex",
126
+ alignItems: "center",
127
+ gap: 4,
128
+ fontSize: 11,
129
+ }}
130
+ >
131
+ <span
132
+ style={{
133
+ width: 6,
134
+ height: 6,
135
+ borderRadius: "50%",
136
+ background: colors[status] ?? "#6b7280",
137
+ }}
138
+ />
139
+ {status}
140
+ </span>
141
+ );
142
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Sidebar panel showing compact babysitter status.
3
+ *
4
+ * Displays active run count, pending breakpoints badge, and quick links.
5
+ */
6
+
7
+ import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
8
+ import type { PluginSidebarProps } from "@paperclipai/plugin-sdk/ui";
9
+
10
+ interface RunsOverview {
11
+ activeRuns: Array<{
12
+ runId: string;
13
+ processId: string;
14
+ status: string;
15
+ pendingBreakpoints: Array<{ effectId: string }>;
16
+ }>;
17
+ pendingBreakpoints: number;
18
+ totalRuns: number;
19
+ }
20
+
21
+ export function BabysitterSidebar({ context }: PluginSidebarProps) {
22
+ const { data, loading, refresh } = usePluginData<RunsOverview>(
23
+ "runs-overview",
24
+ { companyId: context.companyId }
25
+ );
26
+
27
+ if (loading) {
28
+ return <div style={{ padding: 8, fontSize: 12 }}>Loading...</div>;
29
+ }
30
+
31
+ if (!data) return null;
32
+
33
+ return (
34
+ <div style={{ padding: 12, display: "grid", gap: 8 }}>
35
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
36
+ <strong style={{ fontSize: 13 }}>Babysitter</strong>
37
+ <button onClick={refresh} style={{ fontSize: 10, padding: "2px 6px" }}>
38
+ Refresh
39
+ </button>
40
+ </div>
41
+
42
+ <div style={{ display: "grid", gap: 4 }}>
43
+ <SidebarRow
44
+ label="Active runs"
45
+ value={String(data.activeRuns.length)}
46
+ />
47
+ <SidebarRow
48
+ label="Breakpoints"
49
+ value={String(data.pendingBreakpoints)}
50
+ highlight={data.pendingBreakpoints > 0}
51
+ />
52
+ <SidebarRow
53
+ label="Total runs"
54
+ value={String(data.totalRuns)}
55
+ />
56
+ </div>
57
+
58
+ {data.activeRuns.length > 0 && (
59
+ <div style={{ borderTop: "1px solid var(--border)", paddingTop: 8, marginTop: 4 }}>
60
+ {data.activeRuns.slice(0, 3).map((run) => (
61
+ <div
62
+ key={run.runId}
63
+ style={{
64
+ fontSize: 11,
65
+ padding: "3px 0",
66
+ display: "flex",
67
+ justifyContent: "space-between",
68
+ }}
69
+ >
70
+ <span style={{ fontFamily: "monospace", opacity: 0.8 }}>
71
+ {run.processId.length > 20
72
+ ? run.processId.slice(0, 20) + "..."
73
+ : run.processId}
74
+ </span>
75
+ <span
76
+ style={{
77
+ fontSize: 10,
78
+ padding: "1px 4px",
79
+ borderRadius: 3,
80
+ background:
81
+ run.status === "waiting" ? "#eab308" : "#22c55e",
82
+ color: "white",
83
+ }}
84
+ >
85
+ {run.status}
86
+ </span>
87
+ </div>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ function SidebarRow({
96
+ label,
97
+ value,
98
+ highlight,
99
+ }: {
100
+ label: string;
101
+ value: string;
102
+ highlight?: boolean;
103
+ }) {
104
+ return (
105
+ <div
106
+ style={{
107
+ display: "flex",
108
+ justifyContent: "space-between",
109
+ fontSize: 12,
110
+ }}
111
+ >
112
+ <span style={{ color: "var(--muted-foreground)" }}>{label}</span>
113
+ <span
114
+ style={{
115
+ fontWeight: highlight ? 700 : 400,
116
+ color: highlight ? "var(--destructive)" : "inherit",
117
+ }}
118
+ >
119
+ {value}
120
+ </span>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Breakpoint approval/rejection UI component.
3
+ *
4
+ * Renders the breakpoint details with approve/reject actions.
5
+ * Critical: both approve and reject use --status ok with the babysitter CLI.
6
+ * Rejection sends { approved: false, feedback: "..." }.
7
+ */
8
+
9
+ import { useState } from "react";
10
+ import { usePluginAction } from "@paperclipai/plugin-sdk/ui";
11
+
12
+ interface BreakpointApprovalProps {
13
+ runId: string;
14
+ effectId: string;
15
+ title: string;
16
+ /** The breakpoint question from task.json metadata.payload.question */
17
+ question?: string;
18
+ description?: string;
19
+ /** Explicit options from the process (e.g., ["Approve", "Request changes"]) */
20
+ options?: string[];
21
+ /** Domain expert routing from breakpoint routing fields */
22
+ expert?: string | string[];
23
+ /** Tags for categorization */
24
+ tags?: string[];
25
+ /** Previous rejection feedback (shown on retry) */
26
+ previousFeedback?: string;
27
+ /** Current retry attempt number */
28
+ attempt?: number;
29
+ companyId: string;
30
+ onResolved?: () => void;
31
+ }
32
+
33
+ export function BreakpointApproval({
34
+ runId,
35
+ effectId,
36
+ title,
37
+ question,
38
+ description,
39
+ options,
40
+ expert,
41
+ tags,
42
+ previousFeedback,
43
+ attempt,
44
+ companyId,
45
+ onResolved,
46
+ }: BreakpointApprovalProps) {
47
+ const approve = usePluginAction("approve-breakpoint");
48
+ const reject = usePluginAction("reject-breakpoint");
49
+
50
+ const [feedback, setFeedback] = useState("");
51
+ const [submitting, setSubmitting] = useState(false);
52
+ const [error, setError] = useState<string | null>(null);
53
+ const [resolved, setResolved] = useState(false);
54
+
55
+ async function handleApprove(response?: string) {
56
+ setSubmitting(true);
57
+ setError(null);
58
+ try {
59
+ await approve({ runId, effectId, response, companyId });
60
+ setResolved(true);
61
+ onResolved?.();
62
+ } catch (err) {
63
+ setError(err instanceof Error ? err.message : "Approval failed");
64
+ } finally {
65
+ setSubmitting(false);
66
+ }
67
+ }
68
+
69
+ async function handleReject() {
70
+ if (!feedback.trim()) {
71
+ setError("Feedback is required when rejecting a breakpoint");
72
+ return;
73
+ }
74
+ setSubmitting(true);
75
+ setError(null);
76
+ try {
77
+ await reject({ runId, effectId, feedback: feedback.trim(), companyId });
78
+ setResolved(true);
79
+ onResolved?.();
80
+ } catch (err) {
81
+ setError(err instanceof Error ? err.message : "Rejection failed");
82
+ } finally {
83
+ setSubmitting(false);
84
+ }
85
+ }
86
+
87
+ if (resolved) {
88
+ return (
89
+ <div style={{ padding: 12, background: "var(--muted)", borderRadius: 6 }}>
90
+ Breakpoint resolved.
91
+ </div>
92
+ );
93
+ }
94
+
95
+ return (
96
+ <div
97
+ style={{
98
+ border: "1px solid var(--border)",
99
+ borderRadius: 8,
100
+ padding: 16,
101
+ display: "grid",
102
+ gap: 12,
103
+ }}
104
+ >
105
+ <div>
106
+ <strong>{question ?? title}</strong>
107
+ {description && (
108
+ <p style={{ margin: "4px 0 0", fontSize: 13, color: "var(--muted-foreground)" }}>
109
+ {description}
110
+ </p>
111
+ )}
112
+ {/* Show retry context when this is a re-ask after rejection */}
113
+ {previousFeedback && (
114
+ <div
115
+ style={{
116
+ marginTop: 8,
117
+ padding: "6px 10px",
118
+ background: "var(--destructive)",
119
+ color: "var(--destructive-foreground)",
120
+ borderRadius: 4,
121
+ fontSize: 12,
122
+ }}
123
+ >
124
+ Previous feedback (attempt {attempt ?? "?"}): {previousFeedback}
125
+ </div>
126
+ )}
127
+ {/* Show routing info */}
128
+ {(expert || (tags && tags.length > 0)) && (
129
+ <div style={{ marginTop: 4, fontSize: 11, color: "var(--muted-foreground)" }}>
130
+ {expert && (
131
+ <span>
132
+ Expert: {Array.isArray(expert) ? expert.join(", ") : expert}
133
+ </span>
134
+ )}
135
+ {tags && tags.length > 0 && (
136
+ <span style={{ marginLeft: expert ? 12 : 0 }}>
137
+ Tags: {tags.join(", ")}
138
+ </span>
139
+ )}
140
+ </div>
141
+ )}
142
+ </div>
143
+
144
+ {options && options.length > 0 && (
145
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
146
+ {options.map((opt) => (
147
+ <button
148
+ key={opt}
149
+ onClick={() => handleApprove(opt)}
150
+ disabled={submitting}
151
+ style={{ fontSize: 13 }}
152
+ >
153
+ {opt}
154
+ </button>
155
+ ))}
156
+ </div>
157
+ )}
158
+
159
+ <div style={{ display: "grid", gap: 8 }}>
160
+ <textarea
161
+ value={feedback}
162
+ onChange={(e) => setFeedback(e.target.value)}
163
+ placeholder="Feedback (required for rejection)..."
164
+ rows={2}
165
+ style={{
166
+ width: "100%",
167
+ padding: 8,
168
+ borderRadius: 4,
169
+ border: "1px solid var(--border)",
170
+ resize: "vertical",
171
+ fontSize: 13,
172
+ }}
173
+ />
174
+
175
+ <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
176
+ <button
177
+ onClick={handleReject}
178
+ disabled={submitting}
179
+ style={{
180
+ background: "var(--destructive)",
181
+ color: "var(--destructive-foreground)",
182
+ padding: "6px 16px",
183
+ borderRadius: 4,
184
+ border: "none",
185
+ cursor: submitting ? "not-allowed" : "pointer",
186
+ }}
187
+ >
188
+ {submitting ? "..." : "Reject"}
189
+ </button>
190
+ <button
191
+ onClick={() => handleApprove(feedback || undefined)}
192
+ disabled={submitting}
193
+ style={{
194
+ background: "var(--primary)",
195
+ color: "var(--primary-foreground)",
196
+ padding: "6px 16px",
197
+ borderRadius: 4,
198
+ border: "none",
199
+ cursor: submitting ? "not-allowed" : "pointer",
200
+ }}
201
+ >
202
+ {submitting ? "..." : "Approve"}
203
+ </button>
204
+ </div>
205
+ </div>
206
+
207
+ {error && (
208
+ <div style={{ color: "var(--destructive)", fontSize: 12 }}>{error}</div>
209
+ )}
210
+ </div>
211
+ );
212
+ }