generated from nhcarrigan/template
feat: add auto-commit after task completion in Task Loop
This commit is contained in:
@@ -158,6 +158,16 @@ pub struct HikariConfig {
|
||||
|
||||
#[serde(default)]
|
||||
pub custom_ui_font_family: Option<String>,
|
||||
|
||||
// Task Loop auto-commit settings
|
||||
#[serde(default)]
|
||||
pub task_loop_auto_commit: bool,
|
||||
|
||||
#[serde(default = "default_task_loop_commit_prefix")]
|
||||
pub task_loop_commit_prefix: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub task_loop_include_summary: bool,
|
||||
}
|
||||
|
||||
impl Default for HikariConfig {
|
||||
@@ -201,6 +211,9 @@ impl Default for HikariConfig {
|
||||
custom_font_family: None,
|
||||
custom_ui_font_path: None,
|
||||
custom_ui_font_family: None,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat".to_string(),
|
||||
task_loop_include_summary: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +254,10 @@ fn default_background_image_opacity() -> f32 {
|
||||
0.3
|
||||
}
|
||||
|
||||
fn default_task_loop_commit_prefix() -> String {
|
||||
"feat".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BudgetAction {
|
||||
@@ -332,6 +349,9 @@ mod tests {
|
||||
assert!(config.custom_font_family.is_none());
|
||||
assert!(config.custom_ui_font_path.is_none());
|
||||
assert!(config.custom_ui_font_family.is_none());
|
||||
assert!(!config.task_loop_auto_commit);
|
||||
assert_eq!(config.task_loop_commit_prefix, "feat");
|
||||
assert!(!config.task_loop_include_summary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,6 +395,9 @@ mod tests {
|
||||
custom_font_family: Some("MyFont".to_string()),
|
||||
custom_ui_font_path: None,
|
||||
custom_ui_font_family: None,
|
||||
task_loop_auto_commit: true,
|
||||
task_loop_commit_prefix: "fix".to_string(),
|
||||
task_loop_include_summary: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
@@ -389,6 +412,9 @@ mod tests {
|
||||
deserialized.greeting_custom_prompt,
|
||||
Some("Hello!".to_string())
|
||||
);
|
||||
assert!(deserialized.task_loop_auto_commit);
|
||||
assert_eq!(deserialized.task_loop_commit_prefix, "fix");
|
||||
assert!(deserialized.task_loop_include_summary);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
});
|
||||
|
||||
let showCustomThemeEditor = $state(false);
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
});
|
||||
|
||||
let streamerModeActive = $state(false);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
computeWaves,
|
||||
isTaskBlocked,
|
||||
buildTaskPrompt,
|
||||
buildAutoCommitPrompt,
|
||||
normalizeToUnixPath,
|
||||
type TaskLoopTask,
|
||||
} from "$lib/stores/taskLoop";
|
||||
@@ -28,13 +29,17 @@
|
||||
const sourceFile = $derived(taskLoopStore.sourceFile);
|
||||
const conversations = $derived(claudeStore.conversations);
|
||||
const concurrencyLimit = $derived(taskLoopStore.concurrencyLimit);
|
||||
const config = $derived(configStore.config);
|
||||
|
||||
// Per-task orchestration phases (panel-local, not persisted)
|
||||
type LoopPhase = "waiting_for_connection" | "waiting_for_completion";
|
||||
type LoopPhase = "waiting_for_connection" | "waiting_for_completion" | "waiting_for_auto_commit";
|
||||
let activePhases = $state<Record<number, LoopPhase>>({});
|
||||
let taskEverStartedMap = $state<Record<number, boolean>>({});
|
||||
let commitEverStartedMap = $state<Record<number, boolean>>({});
|
||||
let isLoading = $state(false);
|
||||
let errorMessage = $state<string | null>(null);
|
||||
let sessionTimestamp = $state("");
|
||||
let showSettings = $state(false);
|
||||
|
||||
const completedCount = $derived($tasks.filter((t) => t.status === "completed").length);
|
||||
const failedCount = $derived($tasks.filter((t) => t.status === "failed").length);
|
||||
@@ -71,11 +76,34 @@
|
||||
taskEverStartedMap = { ...taskEverStartedMap, [taskIdx]: true };
|
||||
}
|
||||
if (taskEverStartedMap[taskIdx] && conv.characterState === "idle") {
|
||||
taskEverStartedMap = Object.fromEntries(
|
||||
Object.entries(taskEverStartedMap).filter(([k]) => Number(k) !== taskIdx)
|
||||
);
|
||||
|
||||
const autoCommit = get(configStore.config).task_loop_auto_commit;
|
||||
if (autoCommit) {
|
||||
activePhases = { ...activePhases, [taskIdx]: "waiting_for_auto_commit" };
|
||||
commitEverStartedMap = { ...commitEverStartedMap, [taskIdx]: false };
|
||||
void sendAutoCommitPrompt(currentTask, taskIdx);
|
||||
} else {
|
||||
activePhases = Object.fromEntries(
|
||||
Object.entries(activePhases).filter(([k]) => Number(k) !== taskIdx)
|
||||
);
|
||||
void onTaskCompleted(taskIdx, "completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (phase === "waiting_for_auto_commit") {
|
||||
if (workingStates.includes(conv.characterState)) {
|
||||
commitEverStartedMap = { ...commitEverStartedMap, [taskIdx]: true };
|
||||
}
|
||||
if (commitEverStartedMap[taskIdx] && conv.characterState === "idle") {
|
||||
activePhases = Object.fromEntries(
|
||||
Object.entries(activePhases).filter(([k]) => Number(k) !== taskIdx)
|
||||
);
|
||||
taskEverStartedMap = Object.fromEntries(
|
||||
Object.entries(taskEverStartedMap).filter(([k]) => Number(k) !== taskIdx)
|
||||
commitEverStartedMap = Object.fromEntries(
|
||||
Object.entries(commitEverStartedMap).filter(([k]) => Number(k) !== taskIdx)
|
||||
);
|
||||
void onTaskCompleted(taskIdx, "completed");
|
||||
}
|
||||
@@ -99,6 +127,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function sendAutoCommitPrompt(task: TaskLoopTask, taskIdx: number): Promise<void> {
|
||||
const cfg = get(configStore.config);
|
||||
const prompt = buildAutoCommitPrompt(
|
||||
task,
|
||||
cfg.task_loop_commit_prefix || "feat",
|
||||
cfg.task_loop_include_summary,
|
||||
sessionTimestamp
|
||||
);
|
||||
try {
|
||||
await invoke("send_prompt", {
|
||||
conversationId: task.conversationId,
|
||||
message: prompt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send auto-commit prompt:", error);
|
||||
// Non-blocking: still mark task as completed even if commit prompt fails
|
||||
activePhases = Object.fromEntries(
|
||||
Object.entries(activePhases).filter(([k]) => Number(k) !== taskIdx)
|
||||
);
|
||||
void onTaskCompleted(taskIdx, "completed");
|
||||
}
|
||||
}
|
||||
|
||||
async function onTaskCompleted(taskIdx: number, status: "completed" | "failed"): Promise<void> {
|
||||
taskLoopStore.setTaskStatus(taskIdx, status);
|
||||
|
||||
@@ -137,9 +188,9 @@
|
||||
|
||||
async function startTask(taskIdx: number, taskList: TaskLoopTask[]): Promise<void> {
|
||||
const task = taskList[taskIdx];
|
||||
const config = get(configStore.config);
|
||||
const cfg = get(configStore.config);
|
||||
const allAllowedTools = [
|
||||
...new Set([...get(claudeStore.grantedTools), ...(config.auto_granted_tools ?? [])]),
|
||||
...new Set([...get(claudeStore.grantedTools), ...(cfg.auto_granted_tools ?? [])]),
|
||||
];
|
||||
|
||||
const filePath = get(taskLoopStore.sourceFile);
|
||||
@@ -159,14 +210,14 @@
|
||||
conversationId,
|
||||
options: {
|
||||
working_dir: workingDir,
|
||||
model: config.model ?? null,
|
||||
api_key: config.api_key ?? null,
|
||||
custom_instructions: (config.custom_instructions ?? "") + PROJECT_CONTEXT_SYSTEM_ADDENDUM,
|
||||
mcp_servers_json: config.mcp_servers_json ?? null,
|
||||
model: cfg.model ?? null,
|
||||
api_key: cfg.api_key ?? null,
|
||||
custom_instructions: (cfg.custom_instructions ?? "") + PROJECT_CONTEXT_SYSTEM_ADDENDUM,
|
||||
mcp_servers_json: cfg.mcp_servers_json ?? null,
|
||||
allowed_tools: allAllowedTools,
|
||||
use_worktree: config.use_worktree ?? false,
|
||||
disable_1m_context: config.disable_1m_context ?? false,
|
||||
max_output_tokens: config.max_output_tokens ?? null,
|
||||
use_worktree: cfg.use_worktree ?? false,
|
||||
disable_1m_context: cfg.disable_1m_context ?? false,
|
||||
max_output_tokens: cfg.max_output_tokens ?? null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -203,6 +254,7 @@
|
||||
const readyIndices = getReadyTasks(taskList, limit);
|
||||
if (readyIndices.length === 0) return;
|
||||
|
||||
sessionTimestamp = new Date().toISOString();
|
||||
taskLoopStore.setLoopStatus("running");
|
||||
await Promise.all(readyIndices.map((i) => startTask(i, taskList)));
|
||||
}
|
||||
@@ -241,13 +293,16 @@
|
||||
|
||||
activePhases = {};
|
||||
taskEverStartedMap = {};
|
||||
commitEverStartedMap = {};
|
||||
}
|
||||
|
||||
function handleReset(): void {
|
||||
taskLoopStore.reset();
|
||||
activePhases = {};
|
||||
taskEverStartedMap = {};
|
||||
commitEverStartedMap = {};
|
||||
errorMessage = null;
|
||||
sessionTimestamp = "";
|
||||
}
|
||||
|
||||
function statusColour(status: TaskLoopTask["status"]): string {
|
||||
@@ -292,6 +347,20 @@
|
||||
}
|
||||
|
||||
const hasPendingTasks = $derived($tasks.some((t) => t.status === "pending"));
|
||||
|
||||
async function toggleAutoCommit(): Promise<void> {
|
||||
await configStore.updateConfig({ task_loop_auto_commit: !$config.task_loop_auto_commit });
|
||||
}
|
||||
|
||||
async function toggleIncludeSummary(): Promise<void> {
|
||||
await configStore.updateConfig({
|
||||
task_loop_include_summary: !$config.task_loop_include_summary,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateCommitPrefix(value: string): Promise<void> {
|
||||
await configStore.updateConfig({ task_loop_commit_prefix: value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -346,6 +415,27 @@
|
||||
← Workflow
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => (showSettings = !showSettings)}
|
||||
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
aria-label="Toggle settings"
|
||||
aria-pressed={showSettings}
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
@@ -363,6 +453,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings panel (collapsible) -->
|
||||
{#if showSettings}
|
||||
<div
|
||||
class="px-6 py-4 border-b border-[var(--border-color)] bg-[var(--bg-secondary)] flex flex-col gap-3"
|
||||
>
|
||||
<p class="text-xs font-semibold text-[var(--text-tertiary)] uppercase tracking-wide">
|
||||
Auto-commit Settings
|
||||
</p>
|
||||
|
||||
<!-- Auto-commit toggle -->
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<div
|
||||
class="relative w-9 h-5 rounded-full transition-colors {$config.task_loop_auto_commit
|
||||
? 'bg-[var(--accent-primary)]'
|
||||
: 'bg-[var(--bg-tertiary)] border border-[var(--border-color)]'}"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform {$config.task_loop_auto_commit
|
||||
? 'left-4'
|
||||
: 'left-0.5'}"
|
||||
></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
checked={$config.task_loop_auto_commit}
|
||||
onchange={toggleAutoCommit}
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-[var(--text-primary)]">Auto-commit on task completion</span>
|
||||
</label>
|
||||
|
||||
{#if $config.task_loop_auto_commit}
|
||||
<!-- Commit prefix -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label
|
||||
class="text-sm text-[var(--text-secondary)] shrink-0 w-28"
|
||||
for="commit-prefix-input"
|
||||
>
|
||||
Commit prefix
|
||||
</label>
|
||||
<input
|
||||
id="commit-prefix-input"
|
||||
type="text"
|
||||
value={$config.task_loop_commit_prefix}
|
||||
onchange={(e) => updateCommitPrefix((e.target as HTMLInputElement).value)}
|
||||
placeholder="feat"
|
||||
class="flex-1 px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent-primary)]"
|
||||
/>
|
||||
<span class="text-xs text-[var(--text-tertiary)]">: task title</span>
|
||||
</div>
|
||||
|
||||
<!-- Include SUMMARY.md toggle -->
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<div
|
||||
class="relative w-9 h-5 rounded-full transition-colors {$config.task_loop_include_summary
|
||||
? 'bg-[var(--accent-primary)]'
|
||||
: 'bg-[var(--bg-tertiary)] border border-[var(--border-color)]'}"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform {$config.task_loop_include_summary
|
||||
? 'left-4'
|
||||
: 'left-0.5'}"
|
||||
></div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
checked={$config.task_loop_include_summary}
|
||||
onchange={toggleIncludeSummary}
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-[var(--text-primary)]">Generate SUMMARY.md before commit</span
|
||||
>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-4 min-h-0">
|
||||
{#if isLoading}
|
||||
@@ -455,9 +622,15 @@
|
||||
{task.priority}
|
||||
</span>
|
||||
{#if task.status === "running"}
|
||||
<span class="text-xs text-blue-400 animate-pulse shrink-0"
|
||||
>● running</span
|
||||
>
|
||||
{#if activePhases[taskIdx] === "waiting_for_auto_commit"}
|
||||
<span class="text-xs text-violet-400 animate-pulse shrink-0"
|
||||
>● committing</span
|
||||
>
|
||||
{:else}
|
||||
<span class="text-xs text-blue-400 animate-pulse shrink-0"
|
||||
>● running</span
|
||||
>
|
||||
{/if}
|
||||
{:else if task.status === "blocked"}
|
||||
<span class="text-xs text-[var(--text-tertiary)] shrink-0">blocked</span
|
||||
>
|
||||
|
||||
@@ -217,6 +217,9 @@ describe("config store", () => {
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
};
|
||||
|
||||
expect(config.model).toBe("claude-sonnet-4");
|
||||
@@ -273,6 +276,9 @@ describe("config store", () => {
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
};
|
||||
|
||||
expect(config.model).toBeNull();
|
||||
@@ -884,6 +890,9 @@ describe("config store", () => {
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
};
|
||||
|
||||
const mockInvokeImpl = vi.mocked(invoke);
|
||||
|
||||
@@ -77,6 +77,10 @@ export interface HikariConfig {
|
||||
// Custom UI font settings
|
||||
custom_ui_font_path: string | null;
|
||||
custom_ui_font_family: string | null;
|
||||
// Task Loop auto-commit settings
|
||||
task_loop_auto_commit: boolean;
|
||||
task_loop_commit_prefix: string;
|
||||
task_loop_include_summary: boolean;
|
||||
}
|
||||
|
||||
const defaultConfig: HikariConfig = {
|
||||
@@ -127,6 +131,9 @@ const defaultConfig: HikariConfig = {
|
||||
custom_font_family: null,
|
||||
custom_ui_font_path: null,
|
||||
custom_ui_font_family: null,
|
||||
task_loop_auto_commit: false,
|
||||
task_loop_commit_prefix: "feat",
|
||||
task_loop_include_summary: false,
|
||||
};
|
||||
|
||||
function createConfigStore() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
findNextPendingIndex,
|
||||
countByStatus,
|
||||
buildTaskPrompt,
|
||||
buildAutoCommitPrompt,
|
||||
normalizeToUnixPath,
|
||||
isTaskBlocked,
|
||||
getReadyTasks,
|
||||
@@ -104,6 +105,58 @@ describe("buildTaskPrompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAutoCommitPrompt", () => {
|
||||
const task = makeTask("task-1");
|
||||
|
||||
it("includes the git add and commit commands", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("git add -A");
|
||||
expect(result).toContain("git commit -m");
|
||||
});
|
||||
|
||||
it("uses the provided prefix in the commit message", () => {
|
||||
const result = buildAutoCommitPrompt(task, "fix", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("fix:");
|
||||
});
|
||||
|
||||
it("includes the task title in the commit message", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("Task task-1");
|
||||
});
|
||||
|
||||
it("includes the task id in the commit body", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("task-1");
|
||||
});
|
||||
|
||||
it("includes the session timestamp in the commit body", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("2026-03-07T12:00:00");
|
||||
});
|
||||
|
||||
it("does not include SUMMARY.md instructions when includeSummary is false", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).not.toContain("SUMMARY.md");
|
||||
});
|
||||
|
||||
it("includes SUMMARY.md instructions when includeSummary is true", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", true, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("SUMMARY.md");
|
||||
});
|
||||
|
||||
it("mentions non-blocking failure handling", () => {
|
||||
const result = buildAutoCommitPrompt(task, "feat", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain("do not retry");
|
||||
});
|
||||
|
||||
it("escapes double quotes in the task title", () => {
|
||||
const quotedTask = makeTask("q1");
|
||||
quotedTask.title = 'Fix "quoted" title';
|
||||
const result = buildAutoCommitPrompt(quotedTask, "fix", false, "2026-03-07T12:00:00");
|
||||
expect(result).toContain('\\"quoted\\"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeToUnixPath", () => {
|
||||
it("converts a WSL UNC path with wsl.localhost to a Unix path", () => {
|
||||
expect(
|
||||
|
||||
@@ -118,6 +118,34 @@ export function normalizeToUnixPath(path: string): string {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the prompt sent to Claude after a task completes to commit the changes.
|
||||
* If `includeSummary` is true, Claude is also asked to write/append to SUMMARY.md first.
|
||||
*/
|
||||
export function buildAutoCommitPrompt(
|
||||
task: TaskLoopTask,
|
||||
prefix: string,
|
||||
includeSummary: boolean,
|
||||
sessionTimestamp: string
|
||||
): string {
|
||||
const escapedTitle = task.title.replaceAll('"', '\\"');
|
||||
const commitMsg = `${prefix}: ${escapedTitle}\\n\\nAuto-committed by Hikari Task Loop\\nTask ID: ${task.id}\\nLoop session: ${sessionTimestamp}`;
|
||||
|
||||
const gitCommands = `git add -A && git commit -m "${commitMsg}"`;
|
||||
|
||||
const summaryRequest = includeSummary
|
||||
? `\n\nBefore committing, please write or append to \`SUMMARY.md\` in the working directory with:\n- What was implemented\n- Key decisions made\n- Files changed\n- Any caveats or follow-up work\n\nInclude SUMMARY.md in the commit.\n`
|
||||
: "";
|
||||
|
||||
return `[Auto-commit] Please run the following in the current working directory:${summaryRequest}
|
||||
|
||||
\`\`\`bash
|
||||
${gitCommands}
|
||||
\`\`\`
|
||||
|
||||
If this fails (e.g. nothing to commit, no git repository), acknowledge it briefly and do not retry.`;
|
||||
}
|
||||
|
||||
/** Builds the prompt sent to Claude Code for an automated task. */
|
||||
export function buildTaskPrompt(
|
||||
task: TaskLoopTask,
|
||||
|
||||
Reference in New Issue
Block a user