Files
skills/orchestrator/templates/page-ancestry.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 f7a59d43a5 feat(orchestrator): W3 artifacts tree, ancestry, gates and token-free cleanup
Artifacts show the branch tree with ancestry and gate marks. The cleanup runner calls pkgmgr branch cleanup with its real argv and parses its repos[*].items output, refuses any plan with a remote-branch action, marks an item done only when pkgmgr reports it done, and offers no delete for a branch a worktree holds. Every text states what the pkgmgr command does to volumes. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W3 e2e never ran; the client switches to --local once pkgmgr ships it.

🎯 Fix: 90 %
💥 Break: 6 %
💸 Cost: 30 %
⏱️ Time: 40 %
🛡️ Security: 100 %
🏆 Score: 100
🚦 Gate: ⛔ break 6 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:15:14 +00:00

168 lines
8.3 KiB
JavaScript

const gateStage = {items: [], drag: null};
const DRAG_SLOP = 6;
function stageGate(key, change) {
gateStage.items = gateStage.items.filter((item) => item.key !== key);
if (change) {
gateStage.items.push({key, ...change});
}
drawArtifacts();
}
function stageParent(repo, branch, parent, drop) {
const key = `parent\u0000${repo}\u0000${branch}\u0000${parent}`;
const staged = gateStage.items.find((item) => item.key === key);
stageGate(key, staged && staged.drop === drop ? null : {argv: Ancestry.parentArgv(repo, branch, parent, drop), repo, branch, parent, drop});
}
function gatesOf(repo, name) {
return ((tree.data && tree.data.gates) || {})[repo] ? tree.data.gates[repo][name] || null : null;
}
function gateIcons(repo, name) {
const found = gatesOf(repo, name);
const own = (found && found.rules) || {};
return h("span", {class: "gates", role: "group", "aria-label": `Gates of ${name}`}, Ancestry.gateMarks(name, found).map((mark) => {
const key = `rule\u0000${repo}\u0000${name}\u0000${mark.action}`;
const staged = gateStage.items.find((item) => item.key === key);
const current = Object.hasOwn(own, mark.action) ? own[mark.action] : null;
const level = staged ? staged.level || "none" : mark.level;
const title = staged ? `${mark.action}: ${staged.level || "inherited"} · staged` : mark.title;
const cycle = (event) => {
const group = event.currentTarget.closest(".gates");
const next = Ancestry.nextRule(staged ? staged.level : current);
stageGate(key, next === current ? null : {argv: Ancestry.ruleArgv(repo, name, mark.action, next), level: next});
if (group && group.isConnected) {
group.replaceWith(gateIcons(repo, name));
}
};
return h("button", {
type: "button", class: ["gate-mark", "icon-only", mark.inherited && !staged ? "inherited" : "", staged ? "pending" : ""].filter(Boolean).join(" "),
"data-action": mark.action, "data-level": level, "data-source": mark.source, ...named(`${mark.action} gate`, title), on: {click: cycle},
}, icon(Ancestry.LEVEL_ICONS[level]));
}));
}
function gateApply() {
const count = gateStage.items.length;
if (!count) {
return null;
}
return h("span", {class: "hint gate-apply"},
iconBtn("bi-check2-all", "Apply", "btn small", applyGates, `Apply ${count} gate changes with one passkey signature`), counter(count),
iconBtn("bi-x-circle", "Discard", "btn small ghost", () => {
gateStage.items = [];
drawArtifacts();
}, "Discard the staged gate changes"));
}
async function applyGates() {
const items = gateStage.items.map((item) => ({kind: "gate", argv: item.argv}));
if (!items.length) {
return;
}
const signed = await proofFor(items, "Apply");
if (signed === null) {
return;
}
const result = await sendJson("/api/gates", {items, proof: signed.proof});
if (!result.ok || !result.data) {
toast("Gates: not applied", fail(result), false);
return;
}
gateStage.items = [];
toast("Gates: signed", [`${items.length} → runner`], true);
drawArtifacts();
window.setTimeout(fetchTree, 3000);
}
function ancestryChart(plan, label, mark) {
const width = Math.max(plan.width, Ancestry.SPACING.column);
const svg = s("svg", {viewBox: `0 0 ${width} ${plan.height}`, width, height: plan.height, class: "chart ancestry", role: "group", "aria-label": label});
const curve = (link) => `M${fixed(link.x1)},${fixed(link.y1)} C${fixed(link.x1)},${fixed((link.y1 + link.y2) / 2)} ${fixed(link.x2)},${fixed((link.y1 + link.y2) / 2)} ${fixed(link.x2)},${fixed(link.y2)}`;
svg.append(s("g", {}, plan.links.map((link) => mark.edge(link, curve(link)))), s("g", {}, plan.nodes.map((node) => mark.node(node))));
return h("div", {class: "ancestry-frame"}, svg);
}
function branchAncestry(repo) {
const names = new Set(repo.branches.map((entry) => entry.name));
const staged = gateStage.items.filter((item) => item.parent && item.repo === repo.path);
const edges = [];
for (const entry of repo.branches) {
for (const found of (gatesOf(repo.path, entry.name) || {parents: []}).parents) {
if (names.has(found.parent)) {
edges.push({child: entry.name, parent: found.parent, source: found.source});
}
}
}
for (const item of staged.filter((change) => !change.drop && names.has(change.branch) && names.has(change.parent))) {
if (!edges.some((edge) => edge.child === item.branch && edge.parent === item.parent)) {
edges.push({child: item.branch, parent: item.parent, source: "staged"});
}
}
const plan = Ancestry.layout(repo.branches.map((entry) => ({id: entry.name, label: entry.name})), edges);
const dropping = (link) => staged.some((item) => item.drop && item.branch === link.child && item.parent === link.parent);
return ancestryChart(plan, `Ancestry of ${repo.name}`, {
edge: (link, d) => s("path", {
class: ["ancestry-edge", link.derived ? "derived" : "", link.source === "staged" ? "pending" : "", dropping(link) ? "dropping" : ""].filter(Boolean).join(" "),
d, "data-child": link.child, "data-parent": link.parent, "data-source": link.source,
on: {click: () => stageParent(repo.path, link.child, link.parent, link.source !== "staged")},
}, s("title", {}, `${link.parent} → ${link.child} · ${link.source}`)),
node: (node) => s("g", {
class: "node a-branch ancestry-node", transform: `translate(${fixed(node.x)} ${fixed(node.y)})`, tabindex: 0, role: "button", "aria-label": node.label,
"data-repo": repo.path, "data-branch": node.id, "data-generation": node.generation,
on: {
pointerdown: (event) => {
gateStage.drag = event.button === 0 ? {repo: repo.path, branch: node.id, x: event.clientX, y: event.clientY} : null;
},
click: () => artSelect(`branch:${repo.path}\u0000${node.id}`),
keydown: (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
artSelect(`branch:${repo.path}\u0000${node.id}`);
}
},
},
}, s("title", {}, node.label), s("circle", {r: 9}), s("text", {class: "label", y: 26, "text-anchor": "middle"}, short(node.label).slice(0, 24))),
});
}
function agentAncestry(root) {
const flat = spawnFlat(root);
const plan = Ancestry.layout(flat.map(({entry}) => ({id: entry.id, label: entry.label, kind: entry.kind})), flat.filter(({parent}) => parent).map(({entry, parent}) => ({child: entry.id, parent: parent.id, source: "recorded"})));
const open = (node) => {
const found = flat.find(({entry}) => entry.id === node.id);
if (found && found.entry.kind === "twig") {
focusSub(found.entry.session, found.entry.row.id);
} else if (found && found.entry.kind !== "operator") {
select(node.id);
}
};
return ancestryChart(plan, "Ancestry: operator, chair, sessions, subagents", {
edge: (link, d) => s("path", {class: "ancestry-edge", d, "data-child": link.child, "data-parent": link.parent}),
node: (node) => s("g", {
class: `node ancestry-node k-${node.kind}`, transform: `translate(${fixed(node.x)} ${fixed(node.y)})`, tabindex: 0, role: "button", "aria-label": node.label,
"data-agent": node.id, "data-kind": node.kind, "data-generation": node.generation,
on: {click: () => open(node), keydown: (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
open(node);
}
}},
}, s("title", {}, `${node.label} · ${NODE_KINDS[node.kind] || "Subagent"}`), s("circle", {r: 9}), s("text", {class: "label", y: 26, "text-anchor": "middle"}, short(node.label).slice(0, 24))),
});
}
document.addEventListener("pointerup", (event) => {
const from = gateStage.drag;
gateStage.drag = null;
if (!from || Math.hypot(event.clientX - from.x, event.clientY - from.y) < DRAG_SLOP) {
return;
}
const hit = document.elementFromPoint(event.clientX, event.clientY);
const target = hit && hit.closest("[data-branch]");
if (target && target.dataset.repo === from.repo && target.dataset.branch !== from.branch) {
stageParent(from.repo, from.branch, target.dataset.branch, false);
}
});