Skip to content

Commit 446b377

Browse files
committed
🤖 feat: interactive harness init approval flow
Change-Id: Icf5963d92a65300117de0c264272f8ca3952c4e0 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 7644cae commit 446b377

27 files changed

Lines changed: 800 additions & 37 deletions

docs/agents/index.mdx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,58 @@ Example JSON object:
441441

442442
</Accordion>
443443

444+
### Harness Init (internal)
445+
446+
**Interactive harness generation + approval (internal)**
447+
448+
<Accordion title="View harness-init.md">
449+
450+
```md
451+
---
452+
name: Harness Init
453+
description: Interactive harness generation + approval (internal)
454+
base: exec
455+
ui:
456+
hidden: true
457+
color: var(--color-harness-init-mode)
458+
subagent:
459+
runnable: false
460+
---
461+
462+
You are in Harness Init mode.
463+
464+
Your job is to create or refine a Ralph harness for this workspace based on the current plan and the repository.
465+
466+
=== CRITICAL: LIMITED EDIT MODE ===
467+
468+
- You may ONLY create/edit files under: `.mux/harness/*.jsonc`
469+
- Do NOT modify source code or other repo files.
470+
- Use bash only for read-only investigation (rg, ls, cat, git diff/show/log, etc.).
471+
- No redirects/heredocs, no installs, no git add/commit, no rm/mv/cp/mkdir/touch.
472+
473+
Repo-aware investigation:
474+
475+
- Identify which commands should be used as gates by checking repo-native entrypoints:
476+
- `Makefile`, `package.json` scripts, `.github/workflows/*`, etc.
477+
- Map the plan’s changes to impacted subsystems by tracing callsites/imports.
478+
479+
Gates:
480+
481+
- Prefer a small set of safe, single commands.
482+
- Do NOT use shell chaining, pipes, redirects, or quotes.
483+
484+
Delegation:
485+
486+
- You may spawn only read-only exploration subagents via `task` with `agentId: "explore"`.
487+
488+
When the harness file is ready for user review:
489+
490+
- Call `propose_harness` exactly once.
491+
- Do NOT start the Ralph loop yourself; the UI will start it after user approval.
492+
```
493+
494+
</Accordion>
495+
444496
{/* END BUILTIN_AGENTS */}
445497

446498
## Related Docs

src/browser/components/ChatInput/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,9 +2480,11 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
24802480
aria-label="Send message"
24812481
className={cn(
24822482
"inline-flex items-center gap-1 rounded-sm border border-border-light px-1.5 py-0.5 text-[11px] font-medium text-white transition-colors duration-200 disabled:opacity-50",
2483-
mode === "plan"
2484-
? "bg-plan-mode hover:bg-plan-mode-hover disabled:hover:bg-plan-mode"
2485-
: "bg-exec-mode hover:bg-exec-mode-hover disabled:hover:bg-exec-mode"
2483+
agentId === "harness-init"
2484+
? "bg-harness-init-mode hover:bg-harness-init-mode-hover disabled:hover:bg-harness-init-mode"
2485+
: mode === "plan"
2486+
? "bg-plan-mode hover:bg-plan-mode-hover disabled:hover:bg-plan-mode"
2487+
: "bg-exec-mode hover:bg-exec-mode-hover disabled:hover:bg-exec-mode"
24862488
)}
24872489
>
24882490
<SendHorizontal className="h-3.5 w-3.5" strokeWidth={2.5} />

src/browser/components/ChatPane.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,17 @@ export const ChatPane: React.FC<ChatPaneProps> = (props) => {
527527
}
528528
}
529529

530+
// Find the ID of the latest propose_harness tool call for external edit detection
531+
// Only the latest harness should fetch fresh content from disk
532+
let latestProposeHarnessId: string | null = null;
533+
for (let i = transformedMessages.length - 1; i >= 0; i--) {
534+
const msg = transformedMessages[i];
535+
if (msg.type === "tool" && msg.toolName === "propose_harness") {
536+
latestProposeHarnessId = msg.id;
537+
break;
538+
}
539+
}
540+
530541
return (
531542
<div
532543
ref={chatAreaRef}
@@ -625,6 +636,11 @@ export const ChatPane: React.FC<ChatPaneProps> = (props) => {
625636
msg.toolName === "propose_plan" &&
626637
msg.id === latestProposePlanId
627638
}
639+
isLatestProposeHarness={
640+
msg.type === "tool" &&
641+
msg.toolName === "propose_harness" &&
642+
msg.id === latestProposeHarnessId
643+
}
628644
bashOutputGroup={bashOutputGroup}
629645
/>
630646
</div>

src/browser/components/Messages/MessageRenderer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ interface MessageRendererProps {
2424
onReviewNote?: (data: ReviewNoteData) => void;
2525
/** Whether this message is the latest propose_plan tool call (for external edit detection) */
2626
isLatestProposePlan?: boolean;
27+
/** Whether this message is the latest propose_harness tool call (for external edit detection) */
28+
isLatestProposeHarness?: boolean;
2729
/** Optional bash_output grouping info (computed at render-time) */
2830
bashOutputGroup?: BashOutputGroupInfo;
2931
}
@@ -38,6 +40,7 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
3840
isCompacting,
3941
onReviewNote,
4042
isLatestProposePlan,
43+
isLatestProposeHarness,
4144
bashOutputGroup,
4245
}) => {
4346
// Route based on message type
@@ -68,6 +71,7 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
6871
workspaceId={workspaceId}
6972
onReviewNote={onReviewNote}
7073
isLatestProposePlan={isLatestProposePlan}
74+
isLatestProposeHarness={isLatestProposeHarness}
7175
bashOutputGroup={bashOutputGroup}
7276
/>
7377
);

src/browser/components/Messages/ToolMessage.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface ToolMessageProps {
1717
onReviewNote?: (data: ReviewNoteData) => void;
1818
/** Whether this is the latest propose_plan in the conversation */
1919
isLatestProposePlan?: boolean;
20+
/** Whether this is the latest propose_harness in the conversation */
21+
isLatestProposeHarness?: boolean;
2022
/** Optional bash_output grouping info */
2123
bashOutputGroup?: BashOutputGroupInfo;
2224
}
@@ -27,6 +29,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
2729
workspaceId,
2830
onReviewNote,
2931
isLatestProposePlan,
32+
isLatestProposeHarness,
3033
bashOutputGroup,
3134
}) => {
3235
const { toolName, args, result, status, toolCallId } = message;
@@ -40,6 +43,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
4043
? bashOutputGroup.position
4144
: undefined;
4245

46+
const isLatest =
47+
toolName === "propose_plan"
48+
? isLatestProposePlan
49+
: toolName === "propose_harness"
50+
? isLatestProposeHarness
51+
: undefined;
4352
// Extract hook output if present (only shown when hook produced output)
4453
const hookOutput = extractHookOutput(result);
4554
const hookDuration = extractHookDuration(result);
@@ -59,8 +68,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
5968
startedAt={message.timestamp}
6069
// FileEdit-specific
6170
onReviewNote={onReviewNote}
62-
// ProposePlan-specific
63-
isLatest={isLatestProposePlan}
71+
// ProposePlan/ProposeHarness-specific
72+
isLatest={isLatest}
6473
// BashOutput-specific
6574
groupPosition={groupPosition}
6675
// CodeExecution-specific

0 commit comments

Comments
 (0)