@shadowclone/skills 0.0.35 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "MIT",
9
9
  "homepage": "https://shadowclone.co",
10
- "version": "0.0.35",
10
+ "version": "0.0.36",
11
11
  "files": [
12
12
  "skills",
13
13
  "README.md",
@@ -14,14 +14,16 @@ metadata:
14
14
 
15
15
  A human needs to understand a PR and compare their observations with an agent's findings.
16
16
  The guide explains related behavior and exposes the actual code. It must account for the complete change.
17
+ The reader may not know what the change is for. The guide must tell them before the first reading step.
17
18
 
18
19
  ## Gates
19
20
 
20
21
  1. Does the route account for every changed file, with accessible diffs and truthful coverage?
21
- 2. Did shadowclone-review investigate locally without posting feedback or repeating CI checks?
22
- 3. Do claims, visuals, and dry runs cite committed code and distinguish reasoning from executed checks?
23
- 4. Can the human select prose or code, attach a comment, and save it into the original HTML?
24
- 5. Are agent findings hidden until comparison, with inclusion and dismissal choices preserved?
22
+ 2. Can a reader who never opened the PR descriptions learn the problem, the result, and the approach?
23
+ 3. Did shadowclone-review investigate locally without posting feedback or repeating CI checks?
24
+ 4. Do claims, visuals, and dry runs cite committed code and distinguish reasoning from executed checks?
25
+ 5. Can the human select prose or code, attach a comment, and save it into the original HTML?
26
+ 6. Are agent findings hidden until comparison, with inclusion and dismissal choices preserved?
25
27
 
26
28
  ## Process
27
29
 
@@ -35,13 +37,16 @@ Write in the user's voice. Before you write, read `~/.agents/voice.md`. If it do
35
37
  6. With another supported engine, use the existing local prepare and analyze stages without the checks or publish stages.
36
38
  7. Without the CLI, follow shadowclone-review in the host agent and save its pinned findings locally.
37
39
  8. Follow surrounding callers, writes, consumers, and failure paths to explain how the change affects existing behavior.
38
- 9. Group all changed files into a connected reading route. Start with the main contracts and their signatures.
39
- 10. Connect related PRs and label ownership. Claim combined behavior only when Git establishes a combined head.
40
- 11. Show each definition before its callers and effects. Pair each passage with its actual diffs and visible signature excerpts.
41
- 12. Run a focused check only when it answers a named uncertainty. Record its actual result and limits.
42
- 13. Write the input outside the public checkout, then run `node scripts/build.mjs <input.json> <review.html>`.
43
- 14. Run `node scripts/check.mjs <review.html>` and inspect the real document before delivering it.
44
- 15. Verify the reading order, complete file view, selection comments, and a same-file save and reopen cycle.
40
+ 9. Read each PR description, its linked issues, and its discussions. Compare their claims with the code.
41
+ 10. Write the overview: the problem, what changes, and how it works. For a stack, give each PR its role.
42
+ 11. If a description and the code disagree, follow the code and state the difference in the overview.
43
+ 12. Group all changed files into a connected reading route. Start with the main contracts and their signatures.
44
+ 13. Connect related PRs and label ownership. Claim combined behavior only when Git establishes a combined head.
45
+ 14. Show each definition before its callers and effects. Pair each passage with its actual diffs and visible signature excerpts.
46
+ 15. Run a focused check only when it answers a named uncertainty. Record its actual result and limits.
47
+ 16. Write the input outside the public checkout, then run `node scripts/build.mjs <input.json> <review.html>`.
48
+ 17. Run `node scripts/check.mjs <review.html>` and inspect the real document before delivering it.
49
+ 18. Verify the overview, the reading order, complete file view, selection comments, and a same-file save and reopen cycle.
45
50
 
46
51
  The script paths are relative to this skill. Node and Bun can run them.
47
52
  Place supporting files after the main behavior. Keep every file expanded, including the complete file view.
@@ -5,6 +5,8 @@ let showFindings = false;
5
5
  let currentStep = 0;
6
6
  let currentPr = review.combined ? "stack" : review.pulls[0].id;
7
7
  let mode = "guide";
8
+ const pages = [...document.querySelectorAll(".reading-step")];
9
+ const pageTitle = (index) => pages[index].querySelector("h1").textContent;
8
10
 
9
11
  function renderFileList() {
10
12
  const search = document.getElementById("file-search").value.toLowerCase();
@@ -38,7 +40,7 @@ function refreshGuide() {
38
40
  .forEach((button) =>
39
41
  button.classList.toggle("selected", Number(button.dataset.step) === currentStep),
40
42
  );
41
- document.querySelectorAll(".reading-step").forEach((element, index) => {
43
+ pages.forEach((element, index) => {
42
44
  element.hidden = index !== currentStep;
43
45
  element.querySelectorAll(".file-card").forEach((card) => {
44
46
  const file = getFile(card.dataset.file);
@@ -50,12 +52,10 @@ function refreshGuide() {
50
52
  const previous = document.getElementById("previous");
51
53
  const next = document.getElementById("next");
52
54
  previous.disabled = currentStep === 0;
53
- next.disabled = currentStep === review.groups.length - 1;
54
- previous.textContent = currentStep ? "← " + review.groups[currentStep - 1].title : "First step";
55
+ next.disabled = currentStep === pages.length - 1;
56
+ previous.textContent = currentStep ? "← " + pageTitle(currentStep - 1) : "First step";
55
57
  next.textContent =
56
- currentStep < review.groups.length - 1
57
- ? review.groups[currentStep + 1].title + " →"
58
- : "Last step";
58
+ currentStep < pages.length - 1 ? pageTitle(currentStep + 1) + " →" : "Last step";
59
59
  document.querySelectorAll("[data-reviewed]").forEach((input) => {
60
60
  input.checked = review.annotations.reviewed.includes(input.dataset.reviewed);
61
61
  });
@@ -64,12 +64,11 @@ function refreshGuide() {
64
64
  }
65
65
 
66
66
  function selectStep(index) {
67
- currentStep = Math.max(0, Math.min(review.groups.length - 1, index));
67
+ currentStep = Math.max(0, Math.min(pages.length - 1, index));
68
68
  mode = "guide";
69
69
  document.getElementById("selection-comment").hidden = true;
70
70
  refreshGuide();
71
- const step = document.getElementById("step-" + review.groups[currentStep].id);
72
- step.scrollIntoView({ block: "start" });
71
+ pages[currentStep].scrollIntoView({ block: "start" });
73
72
  }
74
73
 
75
74
  function showSource(id) {
@@ -51,6 +51,11 @@ p { margin: 10px 0; }
51
51
  .flow-proof, .sources { margin: 8px 0 18px; color: var(--muted); font-size: 12px; }
52
52
  .source-link { padding: 0; border: 0; background: none; font-size: 12px; text-decoration: underline; }
53
53
  .passage { margin: 20px 0; }
54
+ .pr-roles { padding-left: 22px; }
55
+ .pr-roles .passage { margin: 12px 0; }
56
+ .pr-roles p, .terms p { margin: 4px 0; }
57
+ .terms dt { margin-top: 12px; font-weight: 600; }
58
+ .terms dd { margin: 0; color: var(--muted); }
54
59
  .step-files { margin: 24px 0; }
55
60
  .file-card { margin: 16px 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
56
61
  .file-card > summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--panel); font: 12px/1.5 ui-monospace, monospace; overflow-wrap: anywhere; }
@@ -11,6 +11,12 @@ Use these fields:
11
11
  {
12
12
  "title": "Review the job retry",
13
13
  "summary": "Follow the result and notification through failures.",
14
+ "overview": {
15
+ "problem": "A failed export job loses its result. The user must start the export again.",
16
+ "outcome": "The job retries up to three times. The user gets a notification after a saved result.",
17
+ "approach": "A retry boundary wraps the job callback. The notification starts after the result write.",
18
+ "terms": [{ "term": "Retry boundary", "meaning": "The code that repeats a failed callback." }]
19
+ },
14
20
  "pulls": [
15
21
  {
16
22
  "id": "pr-1",
@@ -18,6 +24,7 @@ Use these fields:
18
24
  "number": 1,
19
25
  "url": "<PR HTTPS URL>",
20
26
  "title": "Retry the job",
27
+ "role": "Adds the retry boundary around the job callback.",
21
28
  "checkout": "<private local checkout>",
22
29
  "baseSha": "<full SHA>",
23
30
  "headSha": "<full SHA>",
@@ -77,6 +84,23 @@ steps.files orders the actual diffs after that passage. Each file must belong to
77
84
  Without steps.files, the builder follows changed files in steps.evidence order.
78
85
  The group's remaining files follow its main passages. Every file starts expanded and links to its exact PR diff.
79
86
 
87
+ ## Overview
88
+
89
+ The guide opens on the overview page, before the first reading step. Write it for a reader who has not opened the PRs.
90
+ Use the PR descriptions, linked issues, discussions, and the code at the pinned heads as sources.
91
+
92
+ - problem: the gap or failure that the change answers, and who sees it.
93
+ - outcome: what works after the change merges, for users and for the system.
94
+ - approach: the main mechanism and design choice, in a few sentences.
95
+ - terms: optional. Explain each domain word that the reading steps use without a definition.
96
+ - pulls.role: required for a stack. Say what that PR adds and what it builds on. List the pulls in merge order.
97
+
98
+ The summary stays the reading plan: where to start and what the route covers.
99
+ Do not put the reading plan in the overview, and do not copy a description that the code contradicts.
100
+ If a description and the code disagree, follow the code and state the difference.
101
+ The builder refuses an input without problem, outcome, and approach, or a stack PR without a role.
102
+ Human notes can attach to the overview text. Their anchors start with `overview:`.
103
+
80
104
  ## Complete coverage
81
105
 
82
106
  Every changed PR/file pair must appear in groups.files. Repeating a file across related groups does not inflate totals.
@@ -83,6 +83,7 @@ try {
83
83
  id,
84
84
  title: input.title,
85
85
  summary: input.summary,
86
+ overview: input.overview,
86
87
  status: input.status ?? "complete",
87
88
  pulls,
88
89
  groups,
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
+ import { checkOverview } from "./overview.mjs";
3
4
 
4
5
  function requireText(value, label) {
5
6
  if (typeof value !== "string" || !value.trim()) throw new Error(label + " must contain text.");
@@ -88,6 +89,7 @@ export function checkInput(input) {
88
89
  for (const edge of group.diagram?.edges ?? [])
89
90
  if (!edge.evidence?.length) throw new Error("Every diagram transition needs code evidence.");
90
91
  }
92
+ checkOverview(input);
91
93
  }
92
94
 
93
95
  export function checkCoverage(review) {
@@ -0,0 +1,79 @@
1
+ import { escapeHtml, markdown } from "./format.mjs";
2
+
3
+ const overviewTitle = "What this change does";
4
+
5
+ const overviewSections = [
6
+ { key: "problem", title: "The problem" },
7
+ { key: "outcome", title: "What changes" },
8
+ { key: "approach", title: "How it works" },
9
+ ];
10
+
11
+ const hasText = (value) => typeof value === "string" && value.trim() !== "";
12
+
13
+ const pullsWithRoles = (pulls) => (pulls.length > 1 ? pulls : []);
14
+
15
+ export function checkOverview(input) {
16
+ const terms = input.overview?.terms ?? [];
17
+ if (!Array.isArray(terms)) throw new Error("overview.terms must be a list.");
18
+ const missing = [
19
+ ...overviewSections
20
+ .filter((section) => !hasText(input.overview?.[section.key]))
21
+ .map((section) => "overview." + section.key),
22
+ ...pullsWithRoles(input.pulls)
23
+ .filter((pull) => !hasText(pull.role))
24
+ .map((pull) => "the role of PR #" + pull.number),
25
+ ...terms
26
+ .filter((entry) => !hasText(entry?.term) || !hasText(entry?.meaning))
27
+ .map(() => "each term and its meaning"),
28
+ ];
29
+ if (missing.length)
30
+ throw new Error("The overview needs text for " + [...new Set(missing)].join(", ") + ".");
31
+ }
32
+
33
+ function passageMarkup(options) {
34
+ const anchor = "overview:" + options.key;
35
+ return `<${options.element} class="passage" data-anchor="${anchor}" data-prs="${options.prs}">${options.heading}${options.body}<div class="inline-notes" data-notes-at="${anchor}"></div></${options.element}>`;
36
+ }
37
+
38
+ export function overviewRouteMarkup(review) {
39
+ const pullCount = review.pulls.length === 1 ? "1 PR" : review.pulls.length + " PRs";
40
+ return `<button class="route-step" data-step="0" type="button"><span>0</span><span>${overviewTitle}<small>Start here · ${pullCount}</small></span></button>`;
41
+ }
42
+
43
+ export function overviewMarkup(review) {
44
+ const allPulls = review.pulls.map((pull) => pull.id).join(",");
45
+ const sections = overviewSections.map((section) =>
46
+ passageMarkup({
47
+ element: "section",
48
+ key: section.key,
49
+ prs: allPulls,
50
+ heading: "<h3>" + section.title + "</h3>",
51
+ body: markdown(review.overview[section.key]),
52
+ }),
53
+ );
54
+ const roles = pullsWithRoles(review.pulls).map((pull) =>
55
+ passageMarkup({
56
+ element: "li",
57
+ key: pull.id,
58
+ prs: pull.id,
59
+ heading: `<p><a href="${escapeHtml(pull.url)}" target="_blank" rel="noreferrer">#${pull.number}</a> <strong>${escapeHtml(pull.title)}</strong></p>`,
60
+ body: markdown(pull.role),
61
+ }),
62
+ );
63
+ const terms = (review.overview.terms ?? []).map(
64
+ (entry) => "<dt>" + escapeHtml(entry.term) + "</dt><dd>" + markdown(entry.meaning) + "</dd>",
65
+ );
66
+ const rolesMarkup = roles.length
67
+ ? '<h3>What each PR adds</h3><ol class="pr-roles">' + roles.join("") + "</ol>"
68
+ : "";
69
+ const termsMarkup = terms.length
70
+ ? passageMarkup({
71
+ element: "section",
72
+ key: "terms",
73
+ prs: allPulls,
74
+ heading: "<h3>Terms</h3>",
75
+ body: '<dl class="terms">' + terms.join("") + "</dl>",
76
+ })
77
+ : "";
78
+ return `<article class="reading-step overview-page" id="step-overview"><div class="step-meta"><span>Start here, then ${review.groups.length} reading steps</span></div><h1>${overviewTitle}</h1>${sections.join("")}${rolesMarkup}${termsMarkup}</article>`;
79
+ }
@@ -6,6 +6,7 @@ import {
6
6
  sourceExcerptMarkup,
7
7
  } from "../assets/presentation.mjs";
8
8
  import { escapeHtml, jsonForHtml, markdown, fileCard, behaviorMarkup } from "./format.mjs";
9
+ import { overviewMarkup, overviewRouteMarkup } from "./overview.mjs";
9
10
 
10
11
  function sourceLinks(options) {
11
12
  return [...new Set(options.ids)]
@@ -93,7 +94,7 @@ function groupMarkup(options) {
93
94
  })
94
95
  .join("");
95
96
  const remaining = fileSlots(group.files.map((file) => file.path));
96
- return `<article class="reading-step" id="step-${group.id}" data-group="${group.id}" ${index ? "hidden" : ""}><div class="step-meta"><span>Step ${index + 1} of ${review.groups.length} · ${numbers}</span><label><input type="checkbox" data-reviewed="${group.id}"> Reviewed</label></div><h1>${escapeHtml(group.title)}</h1><div class="passage" data-anchor="${group.id}:summary" data-prs="${group.prs.join(",")}">${markdown(group.summary)}<div class="inline-notes" data-notes-at="${group.id}:summary"></div></div><p class="why">${escapeHtml(group.why)}</p><div class="before-after"><div><span>Before</span><p>${escapeHtml(group.before)}</p></div><div><span>After</span><p>${escapeHtml(group.after)}</p></div></div>${flow}${passages}${remaining ? '<div class="step-files"><h3>Remaining file changes</h3>' + remaining + "</div>" : ""}<div class="group-findings" data-findings-for="${group.id}"></div>${dryRuns}</article>`;
97
+ return `<article class="reading-step" id="step-${group.id}" data-group="${group.id}" hidden><div class="step-meta"><span>Step ${index + 1} of ${review.groups.length} · ${numbers}</span><label><input type="checkbox" data-reviewed="${group.id}"> Reviewed</label></div><h1>${escapeHtml(group.title)}</h1><div class="passage" data-anchor="${group.id}:summary" data-prs="${group.prs.join(",")}">${markdown(group.summary)}<div class="inline-notes" data-notes-at="${group.id}:summary"></div></div><p class="why">${escapeHtml(group.why)}</p><div class="before-after"><div><span>Before</span><p>${escapeHtml(group.before)}</p></div><div><span>After</span><p>${escapeHtml(group.after)}</p></div></div>${flow}${passages}${remaining ? '<div class="step-files"><h3>Remaining file changes</h3>' + remaining + "</div>" : ""}<div class="group-findings" data-findings-for="${group.id}"></div>${dryRuns}</article>`;
97
98
  }
98
99
 
99
100
  export async function renderReview(review) {
@@ -120,11 +121,12 @@ export async function renderReview(review) {
120
121
  : "";
121
122
  const sidebar =
122
123
  '<aside class="route"><h2>Reading route</h2>' +
124
+ overviewRouteMarkup(review) +
123
125
  review.groups
124
126
  .map(
125
127
  (group, index) =>
126
128
  '<button class="route-step" data-step="' +
127
- index +
129
+ (index + 1) +
128
130
  '" type="button"><span>' +
129
131
  (index + 1) +
130
132
  "</span><span>" +
@@ -170,7 +172,7 @@ export async function renderReview(review) {
170
172
  );
171
173
  })
172
174
  .join("");
173
- const body = `<header><strong class="brand">◇ shadowclone <span>/ guided review</span></strong><div class="header-actions"><button id="toggle-findings" type="button">Agent findings <span>${findings}</span></button><button id="toggle-notes" type="button">Your notes <span id="note-count">0</span></button><button id="save-review" type="button">Save review</button><span id="save-status" role="status">Saved HTML</span></div></header><div class="document">${sidebar}<main><div class="overview"><h2>${escapeHtml(review.title)}</h2>${markdown(review.summary)}${review.status === "prototype" ? '<p class="prototype">Prototype: one part of the reading route. The full file inventory remains available.</p>' : ""}${prTabs}<div class="view-tabs"><button data-mode="guide" type="button">Guide</button><button data-mode="files" type="button">All changes (${count})</button></div><details class="pr-details"><summary>PR details and CI</summary>${ci}</details></div><div id="guide">${review.groups.map((group, index) => groupMarkup({ group, review, index })).join("")}<nav class="step-navigation"><button id="previous" type="button">Previous</button><button id="next" type="button">Next</button></nav></div><section id="files" hidden><h1>Every changed file</h1><p>These are the actual per-PR changes, including files outside the main reading step.</p><input id="file-search" type="search" placeholder="Find a file" aria-label="Find a changed file"><div id="file-list"></div></section></main></div><aside id="notes-panel" hidden><button id="close-notes" type="button">Close</button><section class="human-notes"><h2>Your review notes</h2><p>Save this HTML, then attach it to your chat and request staging.</p><div id="notes-list"></div></section><section class="agent-findings"><h2 id="findings-heading" hidden>Agent findings</h2><div id="findings-list"></div></section></aside><aside id="composer" hidden><form id="note-form"><div class="composer-title"><strong>Your note</strong><button id="cancel-note" type="button" aria-label="Cancel note">×</button></div><blockquote id="selected-quote"></blockquote><label id="target-label">Post on <select id="target-pr"></select></label><textarea id="note-body" aria-label="Your rough note" placeholder="Write the concern or question in your words." required></textarea><button type="submit">Save note</button></form></aside><button id="selection-comment" type="button" hidden>Comment</button><dialog id="source-dialog"><button id="close-source" type="button">Close</button><h2 id="source-title"></h2><p id="source-pin"></p><pre><code id="source-code"></code></pre><a id="source-link" target="_blank" rel="noreferrer">Open pinned source</a></dialog>`;
175
+ const body = `<header><strong class="brand">◇ shadowclone <span>/ guided review</span></strong><div class="header-actions"><button id="toggle-findings" type="button">Agent findings <span>${findings}</span></button><button id="toggle-notes" type="button">Your notes <span id="note-count">0</span></button><button id="save-review" type="button">Save review</button><span id="save-status" role="status">Saved HTML</span></div></header><div class="document">${sidebar}<main><div class="overview"><h2>${escapeHtml(review.title)}</h2>${markdown(review.summary)}${review.status === "prototype" ? '<p class="prototype">Prototype: one part of the reading route. The full file inventory remains available.</p>' : ""}${prTabs}<div class="view-tabs"><button data-mode="guide" type="button">Guide</button><button data-mode="files" type="button">All changes (${count})</button></div><details class="pr-details"><summary>PR details and CI</summary>${ci}</details></div><div id="guide">${overviewMarkup(review)}${review.groups.map((group, index) => groupMarkup({ group, review, index })).join("")}<nav class="step-navigation"><button id="previous" type="button">Previous</button><button id="next" type="button">Next</button></nav></div><section id="files" hidden><h1>Every changed file</h1><p>These are the actual per-PR changes, including files outside the main reading step.</p><input id="file-search" type="search" placeholder="Find a file" aria-label="Find a changed file"><div id="file-list"></div></section></main></div><aside id="notes-panel" hidden><button id="close-notes" type="button">Close</button><section class="human-notes"><h2>Your review notes</h2><p>Save this HTML, then attach it to your chat and request staging.</p><div id="notes-list"></div></section><section class="agent-findings"><h2 id="findings-heading" hidden>Agent findings</h2><div id="findings-list"></div></section></aside><aside id="composer" hidden><form id="note-form"><div class="composer-title"><strong>Your note</strong><button id="cancel-note" type="button" aria-label="Cancel note">×</button></div><blockquote id="selected-quote"></blockquote><label id="target-label">Post on <select id="target-pr"></select></label><textarea id="note-body" aria-label="Your rough note" placeholder="Write the concern or question in your words." required></textarea><button type="submit">Save note</button></form></aside><button id="selection-comment" type="button" hidden>Comment</button><dialog id="source-dialog"><button id="close-source" type="button">Close</button><h2 id="source-title"></h2><p id="source-pin"></p><pre><code id="source-code"></code></pre><a id="source-link" target="_blank" rel="noreferrer">Open pinned source</a></dialog>`;
174
176
  const format = (await readFile(new URL("./format.mjs", import.meta.url), "utf8")).replace(
175
177
  /^export /gm,
176
178
  "",