mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-14 18:11:01 +00:00
65ba0b3922
* Add Java Modernization Studio canvas extension A canvas extension that drives the GitHub Copilot App Modernization for Java workflow from an interactive dashboard: environment readiness checks, repo assessment, prioritized plan/progress, validation gates (CVE scan, test generation), and one-click predefined-task runs — all grounded in the repo's real artifacts (.appmod/assessment.json, plan.md, progress.md). Includes a node:test suite (109 tests) for the grounding logic. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com> * Secure cockpit loopback server with a per-instance token Address Copilot review feedback on the Java Modernization Studio canvas: - Mint a per-instance secret in createInstanceServer, embed it in the iframe URL, and validate it on every loopback request (/, /state, /events, /action). Other local processes can no longer read repo state or dispatch agent actions just by guessing the random port. Mirrors the existing diagram-viewer token pattern; the client echoes the token from its boot payload. The guard is a no-op when no token is set, so direct makeHandler unit tests are unaffected. - Handle async request-handler rejections in createServer with a .catch that returns 500 and logs, instead of leaking an unhandled rejection that could destabilize the extension process. - Tests: token guard 403s unauthenticated /, /state, /events and /action and allows valid-token requests; the end-to-end test asserts the tokenized URL and a tokenless 403. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix CI: add preview screenshot and clear codespell hits Add the required assets/preview.png screenshot for the canvas-extension validator, and resolve two codespell findings in the cockpit: rename the planSim helper's `nd` variable to `notDone` and reword a catalog comment to avoid the `invokable`/`invocable` flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden cockpit loopback server per Copilot review Address the second-round Copilot review comments on the loopback server: - broadcast(): drop an SSE client whose write() throws instead of keeping it in the Set, so a uncleanly-disconnected client can't cause repeated exceptions or leak dead entries on every subsequent broadcast. - POST /action: treat a malformed JSON body or a missing/invalid "kind" as a 400 client error (with an application/json body) instead of a 200 carrying { ok:false, error:"Unknown action: undefined" }. - Handler catch: set Content-Type: application/json on the 500 error response so it is consistent with the other JSON routes. Adds four tests covering dead-client eviction, the two 400 paths, and the JSON-typed 500. Full suite: 115 passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com> * Add canvas.json gallery metadata for Java Modernization Studio Aaron requested a canvas.json so the awesome-copilot website/gallery can list this extension. Generated via `npm run website:data` (writePerExtensionCanvasManifests), which derives id/name/description/ version/keywords/screenshots from package.json + the createCanvas source + assets, and carries the author through. Output committed verbatim so a CI regeneration produces no diff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com> * Add author tag to package.json Declare the author in the package manifest (object form, matching the author already carried in canvas.json so it includes the profile URL). The published gallery sources author from canvas.json; this adds the conventional npm author tag to package.json for completeness. Verified `npm run website:data` still emits the author on the extensions record and leaves canvas.json byte-identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com> --------- Signed-off-by: Ayan Gupta <74832088+ayangupt@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
163 lines
5.9 KiB
JavaScript
163 lines
5.9 KiB
JavaScript
// autopilot.mjs — Sequencing engine for "Run on autopilot".
|
|
//
|
|
// Given the live plan, Autopilot drives the agent through the checklist one
|
|
// step at a time: pick the next eligible step (respecting phase ordering),
|
|
// hand it to the agent, wait for the turn to finish, re-scan, and repeat —
|
|
// streaming progress to the canvas after every step.
|
|
//
|
|
// All I/O is injected (`snapshot`, `runTurn`, `buildStepPrompt`, `onProgress`)
|
|
// so the loop is unit-testable without a live session or HTTP server. The pure
|
|
// helpers below are the same selection logic the renderer uses for "Continue
|
|
// here", which keeps the visible recommendation and the automated run in sync.
|
|
|
|
// How long to wait for a single step's turn to go idle. Generous: one
|
|
// modernization step can involve edits, a build, and tests. The wait does not
|
|
// abort in-flight agent work; it only bounds how long Autopilot blocks before
|
|
// treating the step as failed.
|
|
export const AUTOPILOT_TURN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
|
export const AUTOPILOT_MAX_STEPS = 25;
|
|
|
|
/** The checklist Autopilot follows: progress.md when present, else plan.md. */
|
|
export function currentSteps(state) {
|
|
if (!state) return [];
|
|
if (state.progress && state.progress.steps && state.progress.steps.length) return state.progress.steps;
|
|
if (state.plan && state.plan.steps) return state.plan.steps;
|
|
return [];
|
|
}
|
|
|
|
/** Stable identity for a step across re-scans (phase + title). */
|
|
export function stepKey(step) {
|
|
if (!step) return "";
|
|
return (step.section || "") + "::" + (step.title || "");
|
|
}
|
|
|
|
/**
|
|
* The next step Autopilot should run: the first not-done step in the active
|
|
* phase, falling back to the first not-done step overall. Mirrors the renderer's
|
|
* "Continue here" so the automated run never jumps ahead of the safe next step.
|
|
* @returns {object|null}
|
|
*/
|
|
export function selectNextStep(state) {
|
|
const steps = currentSteps(state);
|
|
if (!steps.length) return null;
|
|
const ord = (state && state.ordering) || { activeRank: null };
|
|
const inPhase = steps.find(
|
|
(x) => x.status !== "done" && (ord.activeRank == null || x.rank === ord.activeRank)
|
|
);
|
|
return inPhase || steps.find((x) => x.status !== "done") || null;
|
|
}
|
|
|
|
/** Whether the given step is now checked off in a freshly scanned state. */
|
|
export function isStepDone(state, step) {
|
|
const k = stepKey(step);
|
|
const match = currentSteps(state).find((s) => stepKey(s) === k);
|
|
return !!(match && match.status === "done");
|
|
}
|
|
|
|
/** Construct the mutable, serializable run record broadcast to the canvas. */
|
|
export function makeRun({ scope, maxSteps, startRank } = {}) {
|
|
return {
|
|
running: true,
|
|
cancelled: false,
|
|
scope: scope === "all" ? "all" : "phase",
|
|
maxSteps: maxSteps || AUTOPILOT_MAX_STEPS,
|
|
startRank: startRank == null ? null : startRank,
|
|
status: "running",
|
|
current: null,
|
|
completed: [],
|
|
startedAt: Date.now(),
|
|
finishedAt: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Drive the run to completion. Mutates `run` in place and calls
|
|
* `deps.onProgress(run, state|null)` whenever something changes so the caller
|
|
* can broadcast. Resolves with the final `run`.
|
|
*
|
|
* @param {object} run from makeRun()
|
|
* @param {{
|
|
* snapshot: () => Promise<object>,
|
|
* runTurn: (prompt: string) => Promise<any>,
|
|
* buildStepPrompt: (step: object) => string,
|
|
* onProgress: (run: object, state: object|null) => void,
|
|
* log?: Function,
|
|
* }} deps
|
|
*/
|
|
export async function runAutopilot(run, deps) {
|
|
const onProgress = deps.onProgress || (() => {});
|
|
let lastKey = null;
|
|
try {
|
|
while (true) {
|
|
if (run.cancelled) {
|
|
run.status = "cancelled";
|
|
break;
|
|
}
|
|
if (run.completed.length >= run.maxSteps) {
|
|
run.status = "capped";
|
|
break;
|
|
}
|
|
const state = await deps.snapshot();
|
|
const step = selectNextStep(state);
|
|
if (!step) {
|
|
run.status = "completed";
|
|
break;
|
|
}
|
|
// Phase scope: stop once the active phase advances past where we began.
|
|
if (run.scope === "phase" && run.startRank != null && step.rank != null && step.rank > run.startRank) {
|
|
run.status = "phase_done";
|
|
break;
|
|
}
|
|
const key = stepKey(step);
|
|
// Selecting the same step twice running means the previous attempt did
|
|
// not check it off — the agent is stuck or waiting on a decision. Stop
|
|
// and hand control back rather than loop on it.
|
|
if (key === lastKey) {
|
|
run.status = "stuck";
|
|
run.stuck = step.title;
|
|
break;
|
|
}
|
|
lastKey = key;
|
|
|
|
run.current = { title: step.title, section: step.section || null };
|
|
onProgress(run, state);
|
|
if (deps.log) deps.log("Autopilot → " + step.title, { ephemeral: true });
|
|
|
|
let stepError = null;
|
|
try {
|
|
await deps.runTurn(deps.buildStepPrompt(step));
|
|
} catch (e) {
|
|
stepError = (e && e.message) || String(e);
|
|
}
|
|
|
|
const after = await deps.snapshot();
|
|
const done = isStepDone(after, step);
|
|
run.completed.push({
|
|
title: step.title,
|
|
section: step.section || null,
|
|
done,
|
|
error: stepError,
|
|
at: Date.now(),
|
|
});
|
|
run.current = null;
|
|
onProgress(run, after);
|
|
|
|
if (stepError) {
|
|
run.status = "error";
|
|
run.error = stepError;
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
run.status = "error";
|
|
run.error = (e && e.message) || String(e);
|
|
} finally {
|
|
if (run.status === "running") run.status = "completed";
|
|
run.running = false;
|
|
run.finishedAt = Date.now();
|
|
onProgress(run, null);
|
|
}
|
|
return run;
|
|
}
|