- Symphony now tracks PR status for any active contribution with PRs 🧭

- Draft PR creation is now deferred until the first commit lands 
- PR info now syncs from contribution metadata into state automatically 🔄
- Creating a draft PR now updates both metadata.json and state.json reliably 🗃️
- Symphony adds new `completed` contribution status across types and UI 
- Active contribution duration now displays real timeSpent-based timing accurately 
- Symphony issue cards now link directly to claimed PRs externally 🔗
- Playbook Exchange adds richer keyboard navigation and cross-platform shortcuts ⌨️
- Playbook imports now include optional assets/ folder and remote-session guidance 📦
- Troubleshooting upgrades: richer logs, process tree monitor, and error recovery 🛠️
This commit is contained in:
Pedram Amini
2026-01-11 13:05:22 -06:00
parent 465a1795c3
commit db6855db28
12 changed files with 289 additions and 99 deletions

11
docs/CLAUDE.md Normal file
View File

@@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #413 | 5:34 AM | 🔵 | History Documentation Content Loaded for Verification | ~412 |
</claude-mem-context>

View File

@@ -81,14 +81,16 @@ View your in-progress Symphony sessions:
![Active Contributions](./screenshots/symphony-active.png)
Each active contribution shows:
- **Issue title and repository** — The GitHub issue being worked on
- **Status badge** — Running, Paused, Creating PR, etc.
- **Progress bar** — Documents completed vs. total
- **Current document** — The document being processed
- **Time elapsed** — How long the contribution has been running
- **Token usage** — Input/output tokens and estimated cost
- **Draft PR link** — Once created on first commit
- **Controls** — Pause/Resume, Cancel, Finalize PR
- Status indicators (Running, Paused, Creating PR, etc.)
- Progress bar showing documents completed vs. total
- Current document being processed
- Token usage (input/output tokens, estimated cost)
- Draft PR link (once created on first commit)
- Controls: Pause/Resume, Cancel, Finalize PR
- **Check PR Status** button to detect merged/closed PRs
Click **Check PR Status** to verify your draft PR on GitHub and detect merged/closed PRs.
### History Tab

View File

@@ -3110,7 +3110,7 @@ describe('Symphony IPC handlers', () => {
});
describe('active contribution checking', () => {
it('should check active ready_for_review contributions', async () => {
it('should check all active contributions with a draft PR', async () => {
const state = {
active: [
{
@@ -3124,7 +3124,7 @@ describe('Symphony IPC handlers', () => {
draftPrNumber: 500,
draftPrUrl: 'https://github.com/owner/repo/pull/500',
startedAt: '2024-01-01T00:00:00Z',
status: 'ready_for_review', // Only this status is checked
status: 'ready_for_review',
progress: { totalDocuments: 1, completedDocuments: 1, totalTasks: 5, completedTasks: 5 },
tokenUsage: { inputTokens: 1000, outputTokens: 500, estimatedCost: 0.10 },
timeSpent: 60000,
@@ -3137,7 +3137,15 @@ describe('Symphony IPC handlers', () => {
repoName: 'repo',
issueNumber: 2,
draftPrNumber: 501,
status: 'running', // Not ready_for_review - should not be checked
status: 'running', // Running contributions with PR should also be checked
},
{
id: 'active_3',
repoSlug: 'owner/repo',
repoName: 'repo',
issueNumber: 3,
// No draftPrNumber - should not be checked
status: 'running',
},
],
history: [],
@@ -3153,8 +3161,8 @@ describe('Symphony IPC handlers', () => {
const handler = getCheckPRStatusesHandler();
const result = await handler!({} as any);
// Should only check the ready_for_review contribution
expect(result.checked).toBe(1);
// Should check all contributions with a draft PR (both ready_for_review and running)
expect(result.checked).toBe(2);
});
it('should move merged active contributions to history', async () => {
@@ -4025,10 +4033,23 @@ describe('Symphony IPC handlers', () => {
describe('commit counting', () => {
it('should count commits on branch vs base branch', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args, cwd) => {
@@ -4090,10 +4111,23 @@ describe('Symphony IPC handlers', () => {
describe('PR creation', () => {
it('should push branch and create draft PR when commits exist', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => {
@@ -4135,10 +4169,23 @@ describe('Symphony IPC handlers', () => {
describe('metadata updates', () => {
it('should update metadata.json with PR info', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => {
@@ -4165,15 +4212,75 @@ describe('Symphony IPC handlers', () => {
expect(updatedMetadata.draftPrNumber).toBe(77);
expect(updatedMetadata.draftPrUrl).toBe('https://github.com/owner/repo/pull/77');
});
it('should update state.json active contribution with PR info', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => {
if (cmd === 'gh' && args?.[0] === 'auth') return { stdout: 'Logged in', stderr: '', exitCode: 0 };
if (cmd === 'git' && args?.[0] === 'symbolic-ref') return { stdout: 'refs/remotes/origin/main', stderr: '', exitCode: 0 };
if (cmd === 'git' && args?.[0] === 'rev-list') return { stdout: '1', stderr: '', exitCode: 0 };
if (cmd === 'git' && args?.[0] === 'rev-parse') return { stdout: 'symphony/issue-42-abc123', stderr: '', exitCode: 0 };
if (cmd === 'git' && args?.[0] === 'push') return { stdout: '', stderr: '', exitCode: 0 };
if (cmd === 'gh' && args?.[0] === 'pr') return { stdout: 'https://github.com/owner/repo/pull/100', stderr: '', exitCode: 0 };
return { stdout: '', stderr: '', exitCode: 0 };
});
const handler = getCreateDraftPRHandler();
await handler!({} as any, { contributionId: 'contrib_draft_test' });
// Verify state.json was updated with PR info
const stateWriteCall = vi.mocked(fs.writeFile).mock.calls.find(
call => (call[0] as string).includes('state.json')
);
expect(stateWriteCall).toBeDefined();
const updatedState = JSON.parse(stateWriteCall![1] as string);
const activeContrib = updatedState.active.find((c: any) => c.id === 'contrib_draft_test');
expect(activeContrib).toBeDefined();
expect(activeContrib.draftPrNumber).toBe(100);
expect(activeContrib.draftPrUrl).toBe('https://github.com/owner/repo/pull/100');
});
});
describe('event broadcasting', () => {
it('should broadcast symphony:prCreated event', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => {
@@ -4205,10 +4312,23 @@ describe('Symphony IPC handlers', () => {
describe('return values', () => {
it('should return draftPrNumber and draftPrUrl on success', async () => {
const metadata = createValidMetadata();
const stateWithActiveContrib = {
active: [{
id: 'contrib_draft_test',
repoSlug: 'owner/repo',
issueNumber: 42,
status: 'running',
}],
history: [],
stats: {},
};
vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
if ((filePath as string).includes('metadata.json')) {
return JSON.stringify(metadata);
}
if ((filePath as string).includes('state.json')) {
return JSON.stringify(stateWithActiveContrib);
}
throw new Error('ENOENT');
});
vi.mocked(execFileNoThrow).mockImplementation(async (cmd, args) => {

View File

@@ -110,6 +110,7 @@ describe('shared/symphony-types', () => {
'creating_pr',
'running',
'paused',
'completed',
'completing',
'ready_for_review',
'failed',
@@ -121,8 +122,8 @@ describe('shared/symphony-types', () => {
expect(testStatus).toBe(status);
});
it('should have 8 valid contribution statuses', () => {
expect(validStatuses).toHaveLength(8);
it('should have 9 valid contribution statuses', () => {
expect(validStatuses).toHaveLength(9);
});
});

11
src/main/CLAUDE.md Normal file
View File

@@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #421 | 5:35 AM | 🔵 | History Manager Implementation Details Verified | ~454 |
</claude-mem-context>

View File

@@ -1522,12 +1522,42 @@ This PR will be updated automatically when the Auto Run completes.`;
}
}
// Also check active contributions that are ready_for_review
// First, sync PR info from metadata.json for any active contributions missing it
// This handles cases where PR was created but state.json wasn't updated (migration)
let prInfoSynced = false;
for (const contribution of state.active) {
if (!contribution.draftPrNumber) {
try {
const metadataPath = path.join(getSymphonyDir(app), 'contributions', contribution.id, 'metadata.json');
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(metadataContent) as {
prCreated?: boolean;
draftPrNumber?: number;
draftPrUrl?: string;
};
if (metadata.prCreated && metadata.draftPrNumber) {
// Sync PR info from metadata to state
contribution.draftPrNumber = metadata.draftPrNumber;
contribution.draftPrUrl = metadata.draftPrUrl;
prInfoSynced = true;
logger.info('Synced PR info from metadata to state', LOG_CONTEXT, {
contributionId: contribution.id,
draftPrNumber: metadata.draftPrNumber,
});
}
} catch {
// Metadata file might not exist - that's okay
}
}
}
// Also check active contributions that have a draft PR
// These might have been merged/closed externally
const activeToMove: number[] = [];
for (let i = 0; i < state.active.length; i++) {
const contribution = state.active[i];
if (!contribution.draftPrNumber || contribution.status !== 'ready_for_review') continue;
// Check any active contribution with a PR (not just ready_for_review)
if (!contribution.draftPrNumber) continue;
results.checked++;
@@ -1601,11 +1631,11 @@ This PR will be updated automatically when the Auto Run completes.`;
await writeState(app, state);
if (results.merged > 0 || results.closed > 0) {
if (results.merged > 0 || results.closed > 0 || prInfoSynced) {
broadcastSymphonyUpdate(getMainWindow);
}
logger.info('PR status check complete', LOG_CONTEXT, results);
logger.info('PR status check complete', LOG_CONTEXT, { ...results, prInfoSynced });
return results;
}
@@ -1950,6 +1980,16 @@ This PR will be updated automatically when the Auto Run completes.`;
metadata.draftPrUrl = prResult.prUrl;
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
// Also update the active contribution in state with PR info
// This is critical for checkPRStatuses to find the PR
const state = await readState(app);
const activeContrib = state.active.find(c => c.id === contributionId);
if (activeContrib) {
activeContrib.draftPrNumber = prResult.prNumber;
activeContrib.draftPrUrl = prResult.prUrl;
await writeState(app, state);
}
// Broadcast PR creation event
const mainWindow = getMainWindow?.();
if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -0,0 +1,15 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #444 | 5:37 AM | 🔵 | History Detail Modal Implementation Verified | ~517 |
| #443 | " | 🔵 | History Help Modal Content Verified | ~418 |
| #441 | " | 🔵 | Activity Graph Time Range Options Verified | ~292 |
| #437 | 5:36 AM | 🔵 | History Default Setting UI Label Verified | ~314 |
| #426 | " | 🔵 | History Panel UI Implementation Verified | ~529 |
</claude-mem-context>

View File

@@ -93,6 +93,7 @@ const STATUS_COLORS: Record<string, string> = {
creating_pr: COLORBLIND_AGENT_PALETTE[0], // #0077BB
running: COLORBLIND_AGENT_PALETTE[2], // #009988 (Teal - success)
paused: COLORBLIND_AGENT_PALETTE[1], // #EE7733 (Orange - warning)
completed: COLORBLIND_AGENT_PALETTE[2], // #009988 (Teal - success)
completing: COLORBLIND_AGENT_PALETTE[0], // #0077BB
ready_for_review: COLORBLIND_AGENT_PALETTE[8], // #AA4499 (Purple)
failed: COLORBLIND_AGENT_PALETTE[3], // #CC3311 (Vermillion - error)
@@ -113,12 +114,11 @@ function formatCacheAge(cacheAgeMs: number | null): string {
return 'just now';
}
function formatDuration(startedAt: string): string {
const start = new Date(startedAt).getTime();
const diff = Math.floor((Date.now() - start) / 1000);
if (diff < 60) return `${diff}s`;
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`;
function formatDurationMs(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
if (totalSeconds < 60) return `${totalSeconds}s`;
if (totalSeconds < 3600) return `${Math.floor(totalSeconds / 60)}m`;
return `${Math.floor(totalSeconds / 3600)}h ${Math.floor((totalSeconds % 3600) / 60)}m`;
}
function formatDate(isoString: string): string {
@@ -135,6 +135,7 @@ function getStatusInfo(status: ContributionStatus): { label: string; color: stri
creating_pr: <Loader2 className="w-3 h-3 animate-spin" />,
running: <Play className="w-3 h-3" />,
paused: <Pause className="w-3 h-3" />,
completed: <CheckCircle className="w-3 h-3" />,
completing: <Loader2 className="w-3 h-3 animate-spin" />,
ready_for_review: <GitPullRequest className="w-3 h-3" />,
failed: <AlertCircle className="w-3 h-3" />,
@@ -145,6 +146,7 @@ function getStatusInfo(status: ContributionStatus): { label: string; color: stri
creating_pr: 'Creating PR',
running: 'Running',
paused: 'Paused',
completed: 'Completed',
completing: 'Completing',
ready_for_review: 'Ready for Review',
failed: 'Failed',
@@ -298,18 +300,22 @@ function IssueCard({
{issue.documentPaths.length} {issue.documentPaths.length === 1 ? 'document' : 'documents'}
</span>
{isClaimed && issue.claimedByPr && (
<span
<a
href={issue.claimedByPr.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 cursor-pointer hover:underline"
style={{ color: theme.colors.accent }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.maestro.shell?.openExternal?.(issue.claimedByPr!.url);
window.maestro.shell.openExternal(issue.claimedByPr!.url);
}}
>
<GitPullRequest className="w-3 h-3" />
{issue.claimedByPr.isDraft ? 'Draft ' : ''}PR #{issue.claimedByPr.number} by @{issue.claimedByPr.author}
<ExternalLink className="w-2.5 h-2.5" />
</span>
</a>
)}
</div>
@@ -383,7 +389,7 @@ function RepositoryDetailView({
() =>
createMarkdownComponents({
theme,
onExternalLinkClick: (href) => window.maestro.shell?.openExternal?.(href),
onExternalLinkClick: (href) => window.maestro.shell.openExternal(href),
}),
[theme]
);
@@ -446,7 +452,7 @@ function RepositoryDetailView({
};
const handleOpenExternal = useCallback((url: string) => {
window.maestro.shell?.openExternal?.(url);
window.maestro.shell.openExternal(url);
}, []);
return (
@@ -766,16 +772,10 @@ function RepositoryDetailView({
function ActiveContributionCard({
contribution,
theme,
onPause,
onResume,
onCancel,
onFinalize,
}: {
contribution: ActiveContribution;
theme: Theme;
onPause: () => void;
onResume: () => void;
onCancel: () => void;
onFinalize: () => void;
}) {
const statusInfo = getStatusInfo(contribution.status);
@@ -783,13 +783,10 @@ function ActiveContributionCard({
? Math.round((contribution.progress.completedDocuments / contribution.progress.totalDocuments) * 100)
: 0;
const canPause = contribution.status === 'running';
const canResume = contribution.status === 'paused';
const canFinalize = contribution.status === 'ready_for_review';
const canCancel = !['ready_for_review', 'completing', 'cancelled'].includes(contribution.status);
const handleOpenExternal = useCallback((url: string) => {
window.maestro.shell?.openExternal?.(url);
window.maestro.shell.openExternal(url);
}, []);
return (
@@ -841,7 +838,7 @@ function ActiveContributionCard({
</span>
<span style={{ color: theme.colors.textDim }}>
<Clock className="w-3 h-3 inline mr-1" />
{formatDuration(contribution.startedAt)}
{formatDurationMs(contribution.timeSpent)}
</span>
</div>
<div className="h-1.5 rounded-full overflow-hidden" style={{ backgroundColor: theme.colors.bgMain }}>
@@ -871,45 +868,15 @@ function ActiveContributionCard({
</p>
)}
<div className="flex items-center gap-2">
{canPause && (
<button
onClick={onPause}
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1 hover:bg-white/10"
style={{ backgroundColor: theme.colors.bgMain, color: theme.colors.textDim }}
>
<Pause className="w-3 h-3" /> Pause
</button>
)}
{canResume && (
<button
onClick={onResume}
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1"
style={{ backgroundColor: theme.colors.accent + '20', color: theme.colors.accent }}
>
<Play className="w-3 h-3" /> Resume
</button>
)}
{canFinalize && (
<button
onClick={onFinalize}
className="flex-1 py-1.5 rounded text-xs flex items-center justify-center gap-1"
style={{ backgroundColor: theme.colors.accent, color: theme.colors.accentForeground }}
>
<GitPullRequest className="w-3 h-3" /> Finalize PR
</button>
)}
{canCancel && (
<button
onClick={onCancel}
className="py-1.5 px-2 rounded text-xs hover:bg-white/10"
style={{ color: theme.colors.textDim }}
title="Cancel"
>
<X className="w-3 h-3" />
</button>
)}
</div>
{canFinalize && (
<button
onClick={onFinalize}
className="w-full py-1.5 rounded text-xs flex items-center justify-center gap-1"
style={{ backgroundColor: theme.colors.accent, color: theme.colors.accentForeground }}
>
<GitPullRequest className="w-3 h-3" /> Finalize PR
</button>
)}
</div>
);
}
@@ -926,7 +893,7 @@ function CompletedContributionCard({
theme: Theme;
}) {
const handleOpenPR = useCallback(() => {
window.maestro.shell?.openExternal?.(contribution.prUrl);
window.maestro.shell.openExternal(contribution.prUrl);
}, [contribution.prUrl]);
return (
@@ -991,11 +958,15 @@ function AchievementCard({
}) {
return (
<div
className={`p-3 rounded-lg border ${achievement.earned ? '' : 'opacity-50'}`}
style={{ backgroundColor: theme.colors.bgActivity, borderColor: theme.colors.border }}
className="p-3 rounded-lg border"
style={{
backgroundColor: theme.colors.bgActivity,
borderColor: achievement.earned ? theme.colors.accent : theme.colors.border,
opacity: achievement.earned ? 1 : 0.5,
}}
>
<div className="flex items-center gap-3">
<div className="text-2xl">{achievement.icon}</div>
<div className="text-2xl" style={{ opacity: achievement.earned ? 1 : 0.7 }}>{achievement.icon}</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-sm" style={{ color: theme.colors.textMain }}>
{achievement.title}
@@ -1056,7 +1027,6 @@ export function SymphonyModal({
startContribution,
activeContributions,
completedContributions,
cancelContribution,
finalizeContribution,
} = useSymphony();
@@ -1230,18 +1200,6 @@ export function SymphonyModal({
}, [selectedRepo, selectedIssue, startContribution, onStartContribution, handleBack]);
// Contribution actions
const handlePause = useCallback(async (contributionId: string) => {
await window.maestro.symphony.updateStatus({ contributionId, status: 'paused' });
}, []);
const handleResume = useCallback(async (contributionId: string) => {
await window.maestro.symphony.updateStatus({ contributionId, status: 'running' });
}, []);
const handleCancel = useCallback(async (contributionId: string) => {
await cancelContribution(contributionId, true);
}, [cancelContribution]);
const handleFinalize = useCallback(async (contributionId: string) => {
await finalizeContribution(contributionId);
}, [finalizeContribution]);
@@ -1672,9 +1630,6 @@ export function SymphonyModal({
key={contribution.id}
contribution={contribution}
theme={theme}
onPause={() => handlePause(contribution.id)}
onResume={() => handleResume(contribution.id)}
onCancel={() => handleCancel(contribution.id)}
onFinalize={() => handleFinalize(contribution.id)}
/>
))}

View File

@@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #433 | 5:36 AM | 🔵 | History Default Setting Verified in Settings Hook | ~323 |
</claude-mem-context>

View File

@@ -0,0 +1,11 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #442 | 5:37 AM | 🔵 | Achievement Action Extension Verified in Renderer Types | ~311 |
</claude-mem-context>

12
src/shared/CLAUDE.md Normal file
View File

@@ -0,0 +1,12 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 11, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #428 | 5:36 AM | 🔵 | HistoryEntry Interface Fields Verified | ~378 |
| #419 | 5:35 AM | 🔵 | History Constants and Types Verified in Shared Module | ~384 |
</claude-mem-context>

View File

@@ -216,6 +216,7 @@ export type ContributionStatus =
| 'creating_pr' // Creating draft PR
| 'running' // Auto Run in progress
| 'paused' // User paused
| 'completed' // Auto Run finished, PR still in draft
| 'completing' // Pushing final changes
| 'ready_for_review' // PR marked ready
| 'failed' // Failed (see error field)