The hook was passing an inline onSuccess callback to useListData, which included onSuccess in its internal refresh function's dependency array. This caused refresh to be recreated on each render, which triggered the useEffect, which fired the fetch, which completed and caused a re-render, creating an infinite loop. Wrap onSuccess in useCallback with empty dependencies so it maintains a stable reference across renders. This allows refresh to be stable when its dependencies don't change, breaking the cycle. Add documentation to Refactoring.md explaining the onSuccess stability requirement for useListData callers. Also add tests for useJailConfigs to verify it doesn't trigger infinite refetches with stable onSuccess callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
134 lines
4.8 KiB
C#
134 lines
4.8 KiB
C#
#!/usr/bin/env dotnet-script
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
|
|
// ── Ctrl+C: kill active process and exit cleanly ──────────────────────────────
|
|
var cts = new CancellationTokenSource();
|
|
Process? activeProcess = null;
|
|
|
|
Console.CancelKeyPress += (_, e) =>
|
|
{
|
|
e.Cancel = true;
|
|
Console.WriteLine("\n[runner] Interrupted — shutting down...");
|
|
cts.Cancel();
|
|
try { activeProcess?.Kill(entireProcessTree: true); } catch { }
|
|
};
|
|
|
|
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
var repoRoot = Directory.GetCurrentDirectory();
|
|
var tasksFile = Path.Combine(repoRoot, "Docs", "Tasks.md");
|
|
|
|
if (!File.Exists(tasksFile))
|
|
{
|
|
Console.Error.WriteLine($"[runner] ERROR: Tasks.md not found at {tasksFile}");
|
|
Console.Error.WriteLine("[runner] Run this script from the repository root.");
|
|
Environment.Exit(1);
|
|
}
|
|
|
|
// ── Read & split by "---" separator lines ────────────────────────────────────
|
|
var content = File.ReadAllText(tasksFile);
|
|
var items = Regex
|
|
.Split(content, @"\r?\n---\r?\n")
|
|
.Select(s => s.Trim())
|
|
.Where(s => s.Length > 0)
|
|
.ToList();
|
|
|
|
Console.WriteLine($"[runner] Found {items.Count} section(s) in Tasks.md");
|
|
|
|
// ── Helper: run copilot and stream output, return full output ─────────────────
|
|
async Task<string> RunCopilot(IEnumerable<string> extraArgs, string prompt)
|
|
{
|
|
var output = new StringBuilder();
|
|
|
|
var argList = new List<string> { "--model", "claude-haiku-4.5", "--allow-all-tools" };
|
|
argList.AddRange(extraArgs);
|
|
argList.Add("-p");
|
|
argList.Add(prompt);
|
|
|
|
var psi = new ProcessStartInfo("copilot")
|
|
{
|
|
WorkingDirectory = repoRoot,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
};
|
|
foreach (var a in argList)
|
|
psi.ArgumentList.Add(a);
|
|
|
|
activeProcess = new Process { StartInfo = psi };
|
|
|
|
activeProcess.OutputDataReceived += (_, e) =>
|
|
{
|
|
if (e.Data is null) return;
|
|
Console.WriteLine(e.Data);
|
|
output.AppendLine(e.Data);
|
|
};
|
|
activeProcess.ErrorDataReceived += (_, e) =>
|
|
{
|
|
if (e.Data is null) return;
|
|
Console.Error.WriteLine(e.Data);
|
|
output.AppendLine(e.Data);
|
|
};
|
|
|
|
activeProcess.Start();
|
|
activeProcess.BeginOutputReadLine();
|
|
activeProcess.BeginErrorReadLine();
|
|
|
|
await activeProcess.WaitForExitAsync(cts.Token);
|
|
activeProcess = null;
|
|
|
|
return output.ToString();
|
|
}
|
|
|
|
// ── Main loop ─────────────────────────────────────────────────────────────────
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
var item = items[i];
|
|
if (cts.IsCancellationRequested) break;
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("[runner] ══════════════════════════════════════════════");
|
|
Console.WriteLine($"[runner] Task:\n{item}");
|
|
Console.WriteLine("[runner] ══════════════════════════════════════════════");
|
|
Console.WriteLine();
|
|
|
|
// Step 1 — run the task prompt
|
|
await RunCopilot(Enumerable.Empty<string>(), $"read ./Docs/Instructions.md. {item}");
|
|
if (cts.IsCancellationRequested) break;
|
|
|
|
// Step 2 — confirm completion in the same chat session
|
|
Console.WriteLine("\n[runner] Asking for task confirmation...\n");
|
|
var confirmation = await RunCopilot(
|
|
new[] { "--continue" },
|
|
"are you sure tasks is done. reply with yes"
|
|
);
|
|
if (cts.IsCancellationRequested) break;
|
|
|
|
// Step 3 — check for "yes" in the reply
|
|
if (!confirmation.Contains("yes", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine("\n[runner] Task not confirmed as done. Stopping.");
|
|
break;
|
|
}
|
|
|
|
// Step 4 — commit the work
|
|
Console.WriteLine("\n[runner] Task confirmed. Making git commit...\n");
|
|
await RunCopilot(Enumerable.Empty<string>(), "make git commit");
|
|
if (cts.IsCancellationRequested) break;
|
|
|
|
// Step 5 — remove completed task from Tasks.md
|
|
var remaining = items.Skip(i + 1).ToList();
|
|
File.WriteAllText(tasksFile, string.Join("\n\n---\n\n", remaining));
|
|
Console.WriteLine("[runner] Removed completed task from Tasks.md");
|
|
}
|
|
|
|
Console.WriteLine("\n[runner] Finished.");
|