nerdsystem 0.0.25 → 0.0.27

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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The sha256 of every version of the starter app's CLAUDE.md this project has shipped (line endings as LF).
3
+ * `nerd upgrade` uses it to tell a CLAUDE.md that is still exactly the platform's text, which it replaces
4
+ * without ceremony, from one a person or an agent has added to, which it keeps beside the new one.
5
+ *
6
+ * When packages/template/CLAUDE.md changes, add the new hash here; a test fails until it is listed.
7
+ * To print it: `tr -d '\\r' < packages/template/CLAUDE.md | sha256sum`.
8
+ */
9
+ export declare const KNOWN_CLAUDE_MD: Set<string>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The sha256 of every version of the starter app's CLAUDE.md this project has shipped (line endings as LF).
3
+ * `nerd upgrade` uses it to tell a CLAUDE.md that is still exactly the platform's text, which it replaces
4
+ * without ceremony, from one a person or an agent has added to, which it keeps beside the new one.
5
+ *
6
+ * When packages/template/CLAUDE.md changes, add the new hash here; a test fails until it is listed.
7
+ * To print it: `tr -d '\\r' < packages/template/CLAUDE.md | sha256sum`.
8
+ */
9
+ export const KNOWN_CLAUDE_MD = new Set([
10
+ "02eb1063cb99fca0106b0a3052872a7839782564d285527ac74a08b1ca19b9ed",
11
+ "30b969614ba066a6faa930d35df67da48c3a692aba66ce009543e7c0d58114ba",
12
+ "31b9481c380810c87dff5e6a0812274cfb7068a35e11941867ad5bd4b2dc8dbf",
13
+ "440751d827c9f112f584157ea08495b56c8ff111024cc52177e5aa98b07622b8",
14
+ "4dc0c5234ad2115e1b932396db2a6efb0329bd93cb4f8ddc9968d978ddd48389",
15
+ "65f97940d79e9d657bc117d1dbbf13f21e84c578301b20b13c726e00739a305b",
16
+ "6f21b6478d50bd599bc8833068ffa82bc7ad5434439636a81b5b35d9d8554947",
17
+ "887cebefb74b2187ccaea027a40074d0cd163c3e6ed6fcdb0edbcbe766c0ccdd",
18
+ ]);
@@ -11,6 +11,12 @@ export interface BuildOutput {
11
11
  compatibilityFlags: string[];
12
12
  }
13
13
  /** Locates the Vite build output: dist/<worker>/wrangler.json names the entry and the assets dir. */
14
+ /**
15
+ * The Vite plugin copies `.dev.vars` next to the built Worker so a local preview can read it. A deploy
16
+ * never uploads it, and it is rewritten by every build, so the copy is removed here instead of being
17
+ * reported on every run with nothing for anyone to do about it. The file in the app's own folder stays.
18
+ */
19
+ export declare function dropLocalSecretCopies(build: BuildOutput): void;
14
20
  export declare function readBuildOutput(root: string): BuildOutput;
15
21
  export declare function applyRemoteMigrations(root: string, api: ApiClient): Promise<{
16
22
  applied: string[];
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { ApiClient } from "../api.js";
5
5
  import { findProjectRoot, projectPath, readDevVars, readNerdJson } from "../project.js";
@@ -8,6 +8,7 @@ import { CliError, log, printJson, runOrThrow, table, dashboardUrl, ensureDeps }
8
8
  import { runChecks } from "./check.js";
9
9
  import { appendLineage, cliVersion, LOCAL_ONLY_NOTE, packProject, readLineage, uploadSnapshot, uploadStopMessage } from "./source.js";
10
10
  import { lockedGetPaths } from "../gates/tests.js";
11
+ import { ENV_FILES } from "../gates/text.js";
11
12
  import { platformScan } from "./platform-scan.js";
12
13
  import { platformReview, printReview } from "./platform-review.js";
13
14
  import { BUILD_GATES, gateSummary, knownSecretsFrom, probeDeployment, runGates, skippedProbe, SOURCE_GATES, waitForDeployment } from "../gates/index.js";
@@ -24,6 +25,18 @@ function walk(dir, base = dir) {
24
25
  return out;
25
26
  }
26
27
  /** Locates the Vite build output: dist/<worker>/wrangler.json names the entry and the assets dir. */
28
+ /**
29
+ * The Vite plugin copies `.dev.vars` next to the built Worker so a local preview can read it. A deploy
30
+ * never uploads it, and it is rewritten by every build, so the copy is removed here instead of being
31
+ * reported on every run with nothing for anyone to do about it. The file in the app's own folder stays.
32
+ */
33
+ export function dropLocalSecretCopies(build) {
34
+ if (!existsSync(build.workerDir))
35
+ return;
36
+ for (const name of readdirSync(build.workerDir))
37
+ if (ENV_FILES.test(name))
38
+ rmSync(path.join(build.workerDir, name), { force: true });
39
+ }
27
40
  export function readBuildOutput(root) {
28
41
  const dist = path.join(root, "dist");
29
42
  if (!existsSync(dist))
@@ -76,10 +89,11 @@ async function probeAfterDeploy(root, deployment, url, skip) {
76
89
  return skippedProbe("the app had not started answering within 30 seconds; open the URL to check, or run nerd deploy again to probe it");
77
90
  return probeDeployment(url, undefined, { lockedGets: lockedGetPaths(root) });
78
91
  }
79
- function printProbe(url, probes) {
92
+ function printProbe(url, probes, ownDomain) {
80
93
  if (probes.status === "skip")
81
94
  return log.info(`Live URL not probed: ${probes.summary}`);
82
- const line = `Probed ${url}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
95
+ // With the owner's domain on the app, the address above is theirs and this one is not: say why it is the one tried.
96
+ const line = `Probed ${url}${ownDomain ? " (the app's platform address, which keeps answering whatever happens to the domain's DNS)" : ""}: ${probes.status === "pass" ? "all probes passed" : probes.summary}`;
83
97
  if (probes.status === "pass")
84
98
  log.ok(line);
85
99
  else
@@ -224,6 +238,7 @@ export async function deploy(opts) {
224
238
  await runOrThrow("npx", ["vite", "build"], { cwd: root, stdoutToStderr: true });
225
239
  }
226
240
  const build = readBuildOutput(root);
241
+ dropLocalSecretCopies(build);
227
242
  const built = await runGates(BUILD_GATES, { root, build, knownSecrets, log: (l) => log.info(l) }, { stopOnFail: true });
228
243
  if (!built.ok)
229
244
  throw new CliError(`Blocked by: ${built.failed.map((g) => g.title).join(", ")}.`, 2);
@@ -235,7 +250,7 @@ export async function deploy(opts) {
235
250
  const review = wantReview ? await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage) : null;
236
251
  return printJson({ deployment: r.deployment, url: r.url, customUrl: r.customUrl, migrations: r.migrations, probes: r.probes, source: r.source, sourceSkipped: r.sourceSkipped, lineage: r.lineage.passed, warnings: r.warnings, review });
237
252
  }
238
- printProbe(r.url, r.probes);
253
+ printProbe(r.url, r.probes, Boolean(r.customUrl));
239
254
  if (r.deployment.status === "live") {
240
255
  if (r.deployment.sourceId)
241
256
  log.ok(sourceLine(r));
@@ -246,7 +261,10 @@ export async function deploy(opts) {
246
261
  log.step("Your app is live. Checking in on its review (now and then nerdsystem reads the code itself, which takes a minute or two; safe to stop waiting)");
247
262
  printReview(await platformReview(api, `${projectPath(nerd)}/sources/${r.source.id}`, reviewPage));
248
263
  }
249
- log.info(`First time? Sign up on your app at ${r.customUrl ?? r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
264
+ // Said once, after the app's first deploy, which is when it is news.
265
+ const earlier = await api.get(projectPath(nerd, "/deployments?limit=2")).catch(() => null);
266
+ if (earlier && earlier.deployments.length <= 1)
267
+ log.info(`First time? Sign up on your app at ${r.customUrl ?? r.url}, then make yourself admin in the dashboard's Auth tab: ${dashboardUrl(api.apiUrl)}/app/${nerd.org}/projects/${nerd.project}/auth`);
250
268
  }
251
269
  export async function deployments(opts) {
252
270
  const nerd = readNerdJson(findProjectRoot());
@@ -192,4 +192,4 @@ export async function init(dir, args) {
192
192
  log.ok(`Ready. Next:\n cd ${dir}\n pnpm install\n nerd dev\n\nThen open the folder in your coding agent and ask it to build the first feature; the nerdsystem skill walks it through.\nDeploys go to ${project.url}`);
193
193
  }
194
194
  // Kept in sync with packages/sdk/package.json by the release script.
195
- const SDK_VERSION = "0.0.25";
195
+ const SDK_VERSION = "0.0.27";
@@ -16,6 +16,8 @@ export type ReviewOutcome = {
16
16
  platformRead: boolean;
17
17
  /** Whose review the findings on file come from. */
18
18
  kind: "platform" | "session";
19
+ /** Findings recorded as real and deliberately left as they are. Counted, not listed: there is nothing to do about them. */
20
+ leftAsIs: number;
19
21
  } | {
20
22
  status: "skipped";
21
23
  reason: string;
@@ -33,16 +33,22 @@ export async function platformReview(api, sourcePath, dashboard) {
33
33
  }
34
34
  const kind = review.kind === "session" ? "session" : "platform";
35
35
  if (review.status !== "done")
36
- return { status: "declined", findings: [], dashboard, platformRead, kind };
36
+ return { status: "declined", findings: [], dashboard, platformRead, kind, leftAsIs: 0 };
37
37
  const findings = [];
38
+ let leftAsIs = 0;
38
39
  for (const f of review.findings ?? []) {
39
40
  const rule = typeof f.rule_id === "string" && f.rule_id.startsWith("review/") ? RULES[f.rule_id] : undefined;
40
41
  // A type this version of nerd does not know is left out rather than shown with words from elsewhere.
41
42
  if (!rule || typeof f.file !== "string" || !/^[\w./@-]{1,200}$/.test(f.file) || !Number.isInteger(f.line))
42
43
  continue;
44
+ // Recorded as deliberately left: not open, and not something to act on after a deploy.
45
+ if (f.leftAsIs === true) {
46
+ leftAsIs++;
47
+ continue;
48
+ }
43
49
  findings.push({ rule_id: f.rule_id, severity: rule.severity, audience: rule.audience, file: f.file, line: f.line, risk: rule.risk, fix: rule.fix });
44
50
  }
45
- return { status: "done", findings, dashboard, platformRead, kind };
51
+ return { status: "done", findings, dashboard, platformRead, kind, leftAsIs };
46
52
  }
47
53
  export function printReview(r) {
48
54
  if (r.status === "skipped") {
@@ -54,11 +60,12 @@ export function printReview(r) {
54
60
  return;
55
61
  }
56
62
  const whose = r.platformRead ? "nerdsystem read the code itself after this deploy" : r.kind === "session" ? "from the review this app's agent last reported" : "from nerdsystem's last own read of the code";
63
+ const left = r.leftAsIs ? `; ${r.leftAsIs} known and left as ${r.leftAsIs === 1 ? "it is" : "they are"}` : "";
57
64
  if (!r.findings.length) {
58
- log.ok(`Review: nothing open (${whose}). A plain-language summary for the person is at ${r.dashboard}`);
65
+ log.ok(`Review: nothing open (${whose}${left}). A plain-language summary for the person is at ${r.dashboard}`);
59
66
  return;
60
67
  }
61
- log.warn(`Review: ${r.findings.length} thing(s) open (${whose}). A review is a second opinion and can be wrong, so check each one against the code before changing anything. Say what you did about them in the next \`nerd review report\`.`);
68
+ log.warn(`Review: ${r.findings.length} thing(s) open (${whose}${left}). A review is a second opinion and can be wrong, so check each one against the code before changing anything. Say what you did about them in the next \`nerd review report\`.`);
62
69
  for (const f of r.findings) {
63
70
  log.info(` ${f.severity === "info" ? "·" : "!"} ${f.rule_id} ${f.file}:${f.line}${f.audience === "human" ? " (for the person to decide)" : ""}`);
64
71
  log.info(` ${f.risk}`);
@@ -33,6 +33,8 @@ interface OpenFinding {
33
33
  file: string;
34
34
  line: number;
35
35
  origin: "platform" | "session";
36
+ /** Recorded earlier as real and deliberately left as it is. */
37
+ leftAsIs: boolean;
36
38
  }
37
39
  declare const FORMAT: {
38
40
  summary: string;
@@ -43,6 +45,11 @@ declare const FORMAT: {
43
45
  file: string;
44
46
  line: number;
45
47
  }[];
48
+ leftAsIs: {
49
+ rule: string;
50
+ file: string;
51
+ line: number;
52
+ }[];
46
53
  fixed: {
47
54
  rule: string;
48
55
  file: string;
@@ -78,6 +85,11 @@ export interface Report {
78
85
  file: string;
79
86
  line: number;
80
87
  }[];
88
+ leftAsIs: {
89
+ rule: string;
90
+ file: string;
91
+ line: number;
92
+ }[];
81
93
  fixed: {
82
94
  rule: string;
83
95
  file: string;
@@ -79,7 +79,7 @@ async function openFindings(api, base) {
79
79
  for (const f of got?.review?.findings ?? []) {
80
80
  if (typeof f.rule_id !== "string" || !REVIEW_RULES.includes(f.rule_id) || typeof f.file !== "string" || !SAFE_FILE.test(f.file) || !Number.isInteger(f.line))
81
81
  continue;
82
- out.push({ rule_id: f.rule_id, file: f.file, line: f.line, origin: f.origin === "session" ? "session" : "platform" });
82
+ out.push({ rule_id: f.rule_id, file: f.file, line: f.line, origin: f.origin === "session" ? "session" : "platform", leftAsIs: f.leftAsIs === true });
83
83
  }
84
84
  return out;
85
85
  }
@@ -88,6 +88,7 @@ const FORMAT = {
88
88
  does: ["Up to six short phrases, each something a person can do in the app today"],
89
89
  missing: ["Up to five short phrases, each something the owner asked for or the description promises that is not built yet"],
90
90
  findings: [{ rule: "review/<id>", file: "src/server/index.ts", line: 42 }],
91
+ leftAsIs: [{ rule: "review/<id>", file: "src/server/index.ts", line: 42 }],
91
92
  fixed: [{ rule: "review/<id>", file: "src/server/index.ts" }],
92
93
  dismissed: [{ rule: "review/<id>", file: "src/server/index.ts" }],
93
94
  };
@@ -98,7 +99,7 @@ export function checklistText(c) {
98
99
  "This is the review of this app's own code for what tests cannot see. You do it, as part of the work. nerdsystem reads the code itself only now and then, and compares what it finds with what you report here.",
99
100
  "",
100
101
  "How to do it:",
101
- "1. Use a reviewer with fresh eyes when you have one. In Claude Code, hand this whole output to the `nerd-reviewer` subagent that ships with the app (if it is not there yet, any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself and read the files again from disk, not from memory.",
102
+ "1. Use a reviewer with fresh eyes when you have one. In Claude Code, give this output to the `nerd-reviewer` subagent that ships with the app, either pasted or saved to a file it can read (`nerd review checklist > .nerd/checklist.txt`). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself and read the files again from disk, not from memory.",
102
103
  c.first
103
104
  ? "2. No review has been reported from this folder yet, so read all of the app's own code: `src/server` and the database schema first, then `src/client`, plus README.md."
104
105
  : c.changed.length
@@ -114,14 +115,14 @@ export function checklistText(c) {
114
115
  if (c.open.length) {
115
116
  lines.push("Open from earlier reviews. Check each against the code as it is now and answer it in the report:");
116
117
  for (const f of c.open)
117
- lines.push(`- ${f.rule_id} at ${f.file}:${f.line}${f.origin === "platform" ? " (from nerdsystem's own read)" : ""}`);
118
+ lines.push(`- ${f.rule_id} at ${f.file}:${f.line}${f.origin === "platform" ? " (first raised by nerdsystem's own read)" : ""}${f.leftAsIs ? " (recorded as left as it is; it stays that way unless you report it differently)" : ""}`);
118
119
  lines.push("");
119
120
  }
120
121
  lines.push("What to look for:");
121
122
  for (const r of c.rules) {
122
123
  lines.push(`- ${r.rule_id}: ${r.title}${r.audience === "human" ? " [for the person to decide]" : ""}`, ` Look for: ${r.lookFor}`, ` Fix: ${r.fix}`);
123
124
  }
124
- lines.push("", `The report, written to ${c.reportFile}:`, JSON.stringify(c.format, null, 2), "", "- findings: what is still in the code after your fixes, each with the rule, the file, and the line that shows it. Leave it empty when there is nothing.", "- fixed: what this review found and you then fixed, including findings from the open list above.", "- dismissed: only for a finding from nerdsystem's own read that you checked against the code and that does not apply. The owner sees this as your judgement, so use it when you are sure.", "- A finding from nerdsystem's own read that the report does not mention stays open.", "- summary, does, missing are for the owner and are shown on their dashboard as written. Plain words only.", "", "Report what you found, not what would look good. The report is stored as this agent's own review, it never decides whether a deploy goes out, and a clean report that nerdsystem's own read later contradicts is recorded as a miss.");
125
+ lines.push("", `The report, written to ${c.reportFile}:`, JSON.stringify(c.format, null, 2), "", "- findings: what is still in the code after your fixes and still needs someone to act or decide, each with the rule, the file, and the line that shows it. Leave it empty when there is nothing. A finding marked [for the person to decide] stays here until they have answered.", "- leftAsIs: what is real and deliberately left as it is, because the person decided so or because the rule's own advice is not to change working code for it. Any finding can go here, whoever first raised it. It is shown to the owner, kept on later reports, and not announced again to each new session.", "- fixed: what this review found and you then fixed, including findings from the open list above. If you fixed only part of one, list what remains under findings (or leftAsIs) at the line that still shows it, and do not list it as fixed.", "- dismissed: only for a finding from nerdsystem's own read that you checked against the code and that does not apply. The owner sees this as your judgement, so use it when you are sure.", "- A finding from nerdsystem's own read that the report does not mention stays open.", "- summary, does, missing are for the owner and are shown on their dashboard as written. Plain words only.", "", "Report what you found, not what would look good. The report is stored as this agent's own review, it never decides whether a deploy goes out, and a clean report that nerdsystem's own read later contradicts is recorded as a miss.");
125
126
  return lines.join("\n");
126
127
  }
127
128
  /** `nerd review checklist`: what the agent reads before it reviews. */
@@ -182,17 +183,22 @@ export function parseReport(text, files, lineCount) {
182
183
  }
183
184
  return v.map((x) => (x && typeof x === "object" ? x : {}));
184
185
  };
185
- const findings = [];
186
- list("findings").forEach((f, i) => {
187
- const where = `findings[${i}]`;
188
- const id = rule(f.rule, where);
189
- if (typeof f.file !== "string" || !(f.file in files))
190
- return void problems.push(`${where}: "${String(f.file).slice(0, 80)}" is not a file a review reads (the app's own code under src/, README.md, package.json), or it does not exist`);
191
- if (typeof f.line !== "number" || !Number.isInteger(f.line) || f.line < 1 || f.line > lineCount(f.file))
192
- return void problems.push(`${where}: line ${String(f.line)} is not a line of ${f.file}`);
193
- if (id)
194
- findings.push({ rule: id, file: f.file, line: f.line });
195
- });
186
+ const placed = (key) => {
187
+ const out = [];
188
+ list(key).forEach((f, i) => {
189
+ const where = `${key}[${i}]`;
190
+ const id = rule(f.rule, where);
191
+ if (typeof f.file !== "string" || !(f.file in files))
192
+ return void problems.push(`${where}: "${String(f.file).slice(0, 80)}" is not a file a review reads (the app's own code under src/, README.md, package.json), or it does not exist`);
193
+ if (typeof f.line !== "number" || !Number.isInteger(f.line) || f.line < 1 || f.line > lineCount(f.file))
194
+ return void problems.push(`${where}: line ${String(f.line)} is not a line of ${f.file}`);
195
+ if (id)
196
+ out.push({ rule: id, file: f.file, line: f.line });
197
+ });
198
+ return out;
199
+ };
200
+ const findings = placed("findings");
201
+ const leftAsIs = placed("leftAsIs");
196
202
  const notes = (key) => {
197
203
  const out = [];
198
204
  list(key).forEach((f, i) => {
@@ -222,7 +228,7 @@ export function parseReport(text, files, lineCount) {
222
228
  problems.push("summary must be a sentence or two of plain text");
223
229
  if (problems.length)
224
230
  throw new CliError(`${REPORT_FILE} needs ${problems.length === 1 ? "a correction" : "corrections"} before it can be sent:\n${problems.map((p) => ` - ${p}`).join("\n")}\nFix the file and run \`nerd review report\` again.`);
225
- return { findings, fixed, dismissed, ...(typeof r.summary === "string" && r.summary.trim() ? { summary: r.summary.slice(0, 2000) } : {}), ...(does ? { does } : {}), ...(missing ? { missing } : {}) };
231
+ return { findings, leftAsIs, fixed, dismissed, ...(typeof r.summary === "string" && r.summary.trim() ? { summary: r.summary.slice(0, 2000) } : {}), ...(does ? { does } : {}), ...(missing ? { missing } : {}) };
226
232
  }
227
233
  /** The name sent with a report. Only an agent this CLI can recognise for certain is named without being told. */
228
234
  function agentName(given) {
@@ -253,7 +259,7 @@ export async function reviewReport(opts) {
253
259
  if (opts.json)
254
260
  return printJson({ accepted: answer.accepted, dropped: answer.dropped, carried: answer.carried, source: saved.source.id, dashboard: page });
255
261
  const a = answer.accepted;
256
- log.ok(`Review reported for ${nerd.org}/${nerd.project}: ${a.findings} open, ${a.fixed} fixed${a.dismissed ? `, ${a.dismissed} judged not to apply` : ""}.`);
262
+ log.ok(`Review reported for ${nerd.org}/${nerd.project}: ${a.findings} open, ${a.fixed} fixed${a.leftAsIs ? `, ${a.leftAsIs} left as they are` : ""}${a.dismissed ? `, ${a.dismissed} judged not to apply` : ""}.`);
257
263
  if (answer.dropped)
258
264
  log.warn(`${answer.dropped} finding(s) were left out because the rule, the file, or the line did not match the stored code.`);
259
265
  if (answer.carried)
@@ -19,6 +19,7 @@ export interface Attention {
19
19
  /** What is open from the latest review of this app (its agent's own, or the platform's read), mapped through this CLI's own catalog. */
20
20
  export declare function reviewItems(api: ApiClient, base: string): Promise<{
21
21
  items: AttentionItem[];
22
+ left: AttentionItem[];
22
23
  reviewedAt?: string;
23
24
  }>;
24
25
  /**
@@ -4,7 +4,7 @@ import { ApiClient } from "../api.js";
4
4
  import { RULES } from "../gates/rules.js";
5
5
  import { currentVersions, pinned, versionFindings } from "../gates/versions.js";
6
6
  import { findProjectRoot, projectPath, readNerdJson } from "../project.js";
7
- import { log } from "../util.js";
7
+ import { localDay, log } from "../util.js";
8
8
  import { cliVersion } from "./source.js";
9
9
  const within = (p, ms) => Promise.race([p.catch(() => null), new Promise((r) => setTimeout(() => r(null), ms))]);
10
10
  const item = (rule_id, extra = {}) => {
@@ -16,16 +16,18 @@ export async function reviewItems(api, base) {
16
16
  const got = await within(api.get(`${base}/review`), 5000);
17
17
  const review = got?.review;
18
18
  if (!review || review.status !== "done")
19
- return { items: [] };
19
+ return { items: [], left: [] };
20
20
  const items = [];
21
+ // Recorded as real and deliberately left as they are: kept for whoever asks, not announced to every new session.
22
+ const left = [];
21
23
  for (const f of review.findings ?? []) {
22
24
  if (typeof f.rule_id !== "string" || !f.rule_id.startsWith("review/") || typeof f.file !== "string" || !/^[\w./@-]{1,200}$/.test(f.file) || !Number.isInteger(f.line))
23
25
  continue;
24
26
  const it = item(f.rule_id, { where: `${f.file}:${f.line}` });
25
27
  if (it)
26
- items.push(it);
28
+ (f.leftAsIs === true ? left : items).push(it);
27
29
  }
28
- return { items, reviewedAt: review.createdAt };
30
+ return { items, left, reviewedAt: review.createdAt };
29
31
  }
30
32
  /**
31
33
  * Everything the platform has noticed about this app that someone should act on, gathered in a few
@@ -92,7 +94,7 @@ export function attentionText(a) {
92
94
  }
93
95
  }
94
96
  if (a.reviewedAt)
95
- lines.push("", `Findings with a file and a line come from the latest review of this app (${a.reviewedAt.slice(0, 10)}): the one its coding agent reported, or nerdsystem's own read of the code. A review is a second opinion and can be wrong, and code changed since may already have fixed one: read the code at each place before changing anything, and say what you did about each in the next \`nerd review report\`.`);
97
+ lines.push("", `Findings with a file and a line come from the latest review of this app (${localDay(a.reviewedAt)}): the one its coding agent reported, or nerdsystem's own read of the code. A review is a second opinion and can be wrong, and code changed since may already have fixed one: read the code at each place before changing anything, and say what you did about each in the next \`nerd review report\`.`);
96
98
  lines.push("", "Before starting on what the person asked for, tell them about these in one or two plain sentences, with no file names or jargon, and offer to take care of them. If they say yes, fix what is real, run `nerd check`, and deploy. Anything marked for the person to decide, put to them as a plain question and wait for the answer. If they would rather get on with their own request, do that first and remind them once at the end.");
97
99
  return lines.join("\n");
98
100
  }
@@ -132,9 +134,12 @@ export async function review(opts) {
132
134
  log.info("No review of this app yet. Run `nerd review checklist` to do one before the next deploy.");
133
135
  return;
134
136
  }
137
+ const leftLines = r.left.length ? ["", "Known and deliberately left as they are (recorded in an earlier review; nothing to do unless the code around them changes):", ...r.left.map((it) => `- ${it.title} (${it.where})`)] : [];
135
138
  if (!r.items.length) {
136
- log.ok(`Nothing is open from the latest review (${r.reviewedAt.slice(0, 10)}).`);
139
+ log.ok(`Nothing is open from the latest review (${localDay(r.reviewedAt)}).`);
140
+ if (leftLines.length)
141
+ process.stdout.write(`${leftLines.slice(1).join("\n")}\n`);
137
142
  return;
138
143
  }
139
- process.stdout.write(`${attentionText({ app: `${nerd.org}/${nerd.project}`, items: r.items, reviewedAt: r.reviewedAt })}\n`);
144
+ process.stdout.write(`${attentionText({ app: `${nerd.org}/${nerd.project}`, items: r.items, reviewedAt: r.reviewedAt })}${leftLines.length ? `\n${leftLines.join("\n")}` : ""}\n`);
140
145
  }
@@ -1,8 +1,10 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { findProjectRoot } from "../project.js";
4
5
  import { log } from "../util.js";
5
6
  import { renderAgentFiles } from "./agents.js";
7
+ import { KNOWN_CLAUDE_MD } from "./claude-md-history.js";
6
8
  import { templateDir } from "./init.js";
7
9
  import { cliVersion } from "./source.js";
8
10
  /** Files that are the platform's, not the app's: replaced with the current version every time. */
@@ -75,9 +77,13 @@ export function upgradeProject(root, tpl, version) {
75
77
  // purpose, so it is carried over word for word.
76
78
  const seeded = oldClaude ? /^(# [^\n]*\n\n[\s\S]*?\n\n)## This app runs on nerdsystem\n/.exec(oldClaude) : null;
77
79
  const newClaude = tplClaude !== null && seeded ? seeded[1] + tplClaude.replace(/^# This app runs on nerdsystem\n/, "## This app runs on nerdsystem\n") : tplClaude;
80
+ // The platform's part of the old file, as the template had it: when that is word for word a version this
81
+ // project shipped, nobody has added anything, and there is nothing to keep.
82
+ const oldPlatformText = oldClaude === null ? null : seeded ? oldClaude.slice(seeded[1].length).replace(/^## This app runs on nerdsystem\n/, "# This app runs on nerdsystem\n") : oldClaude;
83
+ const untouched = oldPlatformText !== null && KNOWN_CLAUDE_MD.has(createHash("sha256").update(oldPlatformText).digest("hex"));
78
84
  let keptOld = false;
79
85
  if (newClaude !== null && oldClaude !== newClaude) {
80
- if (oldClaude !== null && !existsSync(path.join(root, "CLAUDE.previous.md"))) {
86
+ if (oldClaude !== null && !untouched && !existsSync(path.join(root, "CLAUDE.previous.md"))) {
81
87
  renameSync(path.join(root, "CLAUDE.md"), path.join(root, "CLAUDE.previous.md"));
82
88
  changed.push("CLAUDE.previous.md");
83
89
  keptOld = true;
@@ -113,6 +119,8 @@ export function upgradeProject(root, tpl, version) {
113
119
  const next = ["Run `pnpm install`.", "Run `nerd check` and work through what it reports. It names what this app's own code should adopt (for example sign-in for every route, checked input, access tests)."];
114
120
  if (startersWritten.includes("test/access.test.ts"))
115
121
  next.push("`test/access.test.ts` is the starter's: keep the first test, and rewrite the owner test for this app's own routes. Then set `\"tests\": \"required\"` in nerd.json.");
122
+ if (changed.includes(".claude/agents/nerd-reviewer.md"))
123
+ next.push("A reviewer for Claude Code was added or updated at `.claude/agents/nerd-reviewer.md`. Claude Code loads it when a session starts, so a session that was already open will say it is not found: until the next session, give a read-only subagent that file as its instructions.");
116
124
  if (keptOld)
117
125
  next.push("Your earlier `CLAUDE.md` is now `CLAUDE.previous.md`. Move anything the person added into the new `CLAUDE.md`, then delete the old file.");
118
126
  next.push("Tell the person in plain words what changed, then run `nerd deploy` when the check passes.");
@@ -4,7 +4,7 @@ import { ApiClient } from "../api.js";
4
4
  import { BUILD_GATES, gateSummary, knownSecretsFrom, runGates, skippedGates, SOURCE_GATES } from "../gates/index.js";
5
5
  import { findProjectRoot, projectPath, readDevVars, readNerdJson } from "../project.js";
6
6
  import { CliError, log } from "../util.js";
7
- import { readBuildOutput } from "./deploy.js";
7
+ import { dropLocalSecretCopies, readBuildOutput } from "./deploy.js";
8
8
  /** Suppressions a human approved in the dashboard, so they stop being reported. Best effort. */
9
9
  export async function approvedSuppressions(root) {
10
10
  try {
@@ -28,6 +28,7 @@ export async function runVerify(root, opts) {
28
28
  let stop = source.capped;
29
29
  if (opts.withBuild && source.ok && existsSync(path.join(root, "dist"))) {
30
30
  const build = readBuildOutput(root);
31
+ dropLocalSecretCopies(build);
31
32
  const built = await runGates(BUILD_GATES.filter((g) => g.name !== "policy"), { ...ctxBase, build }, { stopOnFail: opts.stopOnFail, trackAttempts: false });
32
33
  results = [...results, ...built.results];
33
34
  failures = [...failures, ...built.failures];
@@ -108,6 +108,9 @@ export function designFindings(root, files, routes, authPath = "/api/auth") {
108
108
  for (const p of patterns) {
109
109
  if (SYSTEM.has(p.path))
110
110
  continue;
111
+ // A webhook or a callback is called by another service, never by a screen.
112
+ if (/\/(webhooks?|callbacks?)(\/|$)/.test(p.path))
113
+ continue;
111
114
  if (endings.some((e) => p.path.endsWith(e)))
112
115
  continue;
113
116
  // The route's name passed the same strict pattern as every other route name read from the report.
@@ -3,6 +3,7 @@ import { reviewDue } from "../commands/session-review.js";
3
3
  import { reviewItems } from "../commands/status.js";
4
4
  import { projectPath, readNerdJson } from "../project.js";
5
5
  import { finding } from "./secrets.js";
6
+ import { localDay } from "../util.js";
6
7
  import { counts, result, verdict } from "./types.js";
7
8
  /**
8
9
  * Brings the review of this app into the same list as every other check, so an agent meets it in the
@@ -44,7 +45,7 @@ export const reviewGate = {
44
45
  }
45
46
  if (due)
46
47
  findings.push(finding("session/review-due", { verify: "nerd review report" }));
47
- const when = got.reviewedAt?.slice(0, 10);
48
+ const when = got.reviewedAt ? localDay(got.reviewedAt) : undefined;
48
49
  const said = [open ? `${open} open from the review of ${when}; a second opinion that can be wrong, so read the code first` : when ? `Nothing open from the review of ${when}` : "No review yet", ...(due ? [first ? "no review has been reported from this folder: run nerd review checklist" : "code has changed since the last review: run nerd review checklist"] : [])];
49
50
  return result(this, verdict(findings), findings.length ? `${counts(findings)}: ${said.join("; ")}` : said.join("; "), findings, t);
50
51
  },
@@ -114,7 +114,9 @@ export const versionsGate = {
114
114
  // no package.json: the rules gate says so
115
115
  }
116
116
  const findings = versionFindings({ cli: installedCli(), sdk, versions, inBuilder: Boolean(ctx.committed) });
117
- const summary = findings.length ? findings.map((f) => f.rule_id.split("/")[1]).join(", ") : versions.latest ? `nerd and the SDK are current (${versions.latest})` : "Nothing older than the platform accepts";
117
+ // The platform's answer is kept for a day, so straight after an update this copy can be newer than the "latest" it remembers.
118
+ const shown = versions.latest && compareVersions(installedCli(), versions.latest) > 0 ? installedCli() : versions.latest;
119
+ const summary = findings.length ? findings.map((f) => f.rule_id.split("/")[1]).join(", ") : shown ? `nerd and the SDK are current (${shown})` : "Nothing older than the platform accepts";
118
120
  return result(this, verdict(findings), summary, findings, t);
119
121
  },
120
122
  };
package/dist/util.d.ts CHANGED
@@ -29,6 +29,8 @@ export declare function runOrThrow(cmd: string, args: string[], opts?: RunOption
29
29
  export declare function printJson(value: unknown): void;
30
30
  export declare function table(rows: Record<string, unknown>[], columns: string[]): void;
31
31
  /** The dashboard that goes with an API origin (https://api.nerdsystem.com -> https://nerdsystem.com). */
32
+ /** The calendar day of a timestamp where the person is, as YYYY-MM-DD. The platform's times are UTC, which is tomorrow for part of every evening in the Americas. */
33
+ export declare function localDay(iso: string): string;
32
34
  export declare function dashboardUrl(apiUrl: string): string;
33
35
  /** Stops with one sentence instead of npx noise when `pnpm install` has not been run. */
34
36
  export declare function ensureDeps(root: string): void;
package/dist/util.js CHANGED
@@ -49,6 +49,14 @@ export function table(rows, columns) {
49
49
  process.stdout.write(`${line(columns.map((c) => String(r[c] ?? "")))}\n`);
50
50
  }
51
51
  /** The dashboard that goes with an API origin (https://api.nerdsystem.com -> https://nerdsystem.com). */
52
+ /** The calendar day of a timestamp where the person is, as YYYY-MM-DD. The platform's times are UTC, which is tomorrow for part of every evening in the Americas. */
53
+ export function localDay(iso) {
54
+ const d = new Date(iso);
55
+ if (Number.isNaN(d.getTime()))
56
+ return iso.slice(0, 10);
57
+ const two = (n) => String(n).padStart(2, "0");
58
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}`;
59
+ }
52
60
  export function dashboardUrl(apiUrl) {
53
61
  return apiUrl.replace(/\/$/, "").replace(/^https?:\/\/api\./, "https://");
54
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nerdsystem",
3
- "version": "0.0.25",
3
+ "version": "0.0.27",
4
4
  "description": "nerd: start, check, and deploy a web app on nerdsystem from the terminal or a coding agent.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@ tools: Read, Grep, Glob
6
6
 
7
7
  You review the source code of a small web app that a coding agent built for someone who cannot read code. The app runs on nerdsystem, where every app has one shape: a React client in `src/client`, a Hono server in `src/server` whose routes live under `/api`, a SQLite database described by the Drizzle schema in `src/server/db/schema.ts`, and sign-in provided by the platform (the signed-in user is `c.var.user`; `requireAuth()` and `requireRole()` guard routes).
8
8
 
9
- You are given the output of `nerd review checklist`: the files to read, the findings still open from earlier reviews, and the kinds of problem to look for. If you were not given it, say so and stop.
9
+ You are given the output of `nerd review checklist`, or the path of a file that holds it: the files to read, the findings still open from earlier reviews, and the kinds of problem to look for. If you were given neither, say so and stop.
10
10
 
11
11
  ## How to review
12
12
 
@@ -135,8 +135,8 @@ What the tests do not prove: that the feature is the one the person wanted, or t
135
135
  Tests show the app does what was checked. They cannot show that the server trusts a price the browser sent, that any signed-in person can approve what only staff should, or that two people can book the same slot. Looking for those is your job, before every deploy that changes the server, the data model, or what a screen does. Nobody else reads each change: nerdsystem reads the code itself only now and then.
136
136
 
137
137
  1. Run `nerd review checklist`. It prints what to look for (the `review/` rules), the files that changed since the last review, the findings still open from earlier reviews, and the format of the report.
138
- 2. Have it done with fresh eyes. In Claude Code, give the whole checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/` (it only reads; if this session does not have it yet, use any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
139
- 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer.
138
+ 2. Have it done with fresh eyes. In Claude Code, give the checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/`, pasted or saved to a file it can read (it only reads). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
139
+ 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer. Something real that the person decides to leave, or that the rule itself says is not worth changing working code for, goes in the report as `leftAsIs`: it is shown to them, kept, and not raised again at the start of every session.
140
140
  4. Write `.nerd/review.json` as the checklist describes and run `nerd review report`. Then `nerd check` and `nerd deploy`.
141
141
 
142
142
  The report is stored as your review, and the person sees it in their dashboard under Review together with the plain-language summary you wrote for them, so write that for someone who cannot read code. Report what you found, including what you fixed. When nerdsystem does read the code itself, it compares: a finding from its read stays open until your report says it is still there, fixed, or checked and not applicable, and a clean report it then contradicts is recorded as a miss. Never report a review you did not do, and never use `dismissed` to make a finding go away.
@@ -2,7 +2,7 @@
2
2
  description: How to build, run, and deploy this app on nerdsystem. Applies to server, database, auth, and deploy work.
3
3
  alwaysApply: true
4
4
  ---
5
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (1e4962fc7891); edit the skill, then run: nerd agents sync -->
5
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (0649a59fb02c); edit the skill, then run: nerd agents sync -->
6
6
 
7
7
  # Building on nerdsystem
8
8
 
@@ -136,8 +136,8 @@ What the tests do not prove: that the feature is the one the person wanted, or t
136
136
  Tests show the app does what was checked. They cannot show that the server trusts a price the browser sent, that any signed-in person can approve what only staff should, or that two people can book the same slot. Looking for those is your job, before every deploy that changes the server, the data model, or what a screen does. Nobody else reads each change: nerdsystem reads the code itself only now and then.
137
137
 
138
138
  1. Run `nerd review checklist`. It prints what to look for (the `review/` rules), the files that changed since the last review, the findings still open from earlier reviews, and the format of the report.
139
- 2. Have it done with fresh eyes. In Claude Code, give the whole checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/` (it only reads; if this session does not have it yet, use any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
140
- 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer.
139
+ 2. Have it done with fresh eyes. In Claude Code, give the checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/`, pasted or saved to a file it can read (it only reads). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
140
+ 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer. Something real that the person decides to leave, or that the rule itself says is not worth changing working code for, goes in the report as `leftAsIs`: it is shown to them, kept, and not raised again at the start of every session.
141
141
  4. Write `.nerd/review.json` as the checklist describes and run `nerd review report`. Then `nerd check` and `nerd deploy`.
142
142
 
143
143
  The report is stored as your review, and the person sees it in their dashboard under Review together with the plain-language summary you wrote for them, so write that for someone who cannot read code. Report what you found, including what you fixed. When nerdsystem does read the code itself, it compares: a finding from its read stays open until your report says it is still there, fixed, or checked and not applicable, and a clean report it then contradicts is recorded as a miss. Never report a review you did not do, and never use `dismissed` to make a finding go away.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (1e4962fc7891); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (0649a59fb02c); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -132,8 +132,8 @@ What the tests do not prove: that the feature is the one the person wanted, or t
132
132
  Tests show the app does what was checked. They cannot show that the server trusts a price the browser sent, that any signed-in person can approve what only staff should, or that two people can book the same slot. Looking for those is your job, before every deploy that changes the server, the data model, or what a screen does. Nobody else reads each change: nerdsystem reads the code itself only now and then.
133
133
 
134
134
  1. Run `nerd review checklist`. It prints what to look for (the `review/` rules), the files that changed since the last review, the findings still open from earlier reviews, and the format of the report.
135
- 2. Have it done with fresh eyes. In Claude Code, give the whole checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/` (it only reads; if this session does not have it yet, use any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
- 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer.
135
+ 2. Have it done with fresh eyes. In Claude Code, give the checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/`, pasted or saved to a file it can read (it only reads). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
+ 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer. Something real that the person decides to leave, or that the rule itself says is not worth changing working code for, goes in the report as `leftAsIs`: it is shown to them, kept, and not raised again at the start of every session.
137
137
  4. Write `.nerd/review.json` as the checklist describes and run `nerd review report`. Then `nerd check` and `nerd deploy`.
138
138
 
139
139
  The report is stored as your review, and the person sees it in their dashboard under Review together with the plain-language summary you wrote for them, so write that for someone who cannot read code. Report what you found, including what you fixed. When nerdsystem does read the code itself, it compares: a finding from its read stays open until your report says it is still there, fixed, or checked and not applicable, and a clean report it then contradicts is recorded as a miss. Never report a review you did not do, and never use `dismissed` to make a finding go away.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (1e4962fc7891); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (0649a59fb02c); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -132,8 +132,8 @@ What the tests do not prove: that the feature is the one the person wanted, or t
132
132
  Tests show the app does what was checked. They cannot show that the server trusts a price the browser sent, that any signed-in person can approve what only staff should, or that two people can book the same slot. Looking for those is your job, before every deploy that changes the server, the data model, or what a screen does. Nobody else reads each change: nerdsystem reads the code itself only now and then.
133
133
 
134
134
  1. Run `nerd review checklist`. It prints what to look for (the `review/` rules), the files that changed since the last review, the findings still open from earlier reviews, and the format of the report.
135
- 2. Have it done with fresh eyes. In Claude Code, give the whole checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/` (it only reads; if this session does not have it yet, use any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
- 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer.
135
+ 2. Have it done with fresh eyes. In Claude Code, give the checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/`, pasted or saved to a file it can read (it only reads). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
+ 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer. Something real that the person decides to leave, or that the rule itself says is not worth changing working code for, goes in the report as `leftAsIs`: it is shown to them, kept, and not raised again at the start of every session.
137
137
  4. Write `.nerd/review.json` as the checklist describes and run `nerd review report`. Then `nerd check` and `nerd deploy`.
138
138
 
139
139
  The report is stored as your review, and the person sees it in their dashboard under Review together with the plain-language summary you wrote for them, so write that for someone who cannot read code. Report what you found, including what you fixed. When nerdsystem does read the code itself, it compares: a finding from its read stays open until your report says it is still there, fixed, or checked and not applicable, and a clean report it then contradicts is recorded as a miss. Never report a review you did not do, and never use `dismissed` to make a finding go away.
@@ -1,4 +1,4 @@
1
- <!-- generated from .claude/skills/nerdsystem/SKILL.md (1e4962fc7891); edit the skill, then run: nerd agents sync -->
1
+ <!-- generated from .claude/skills/nerdsystem/SKILL.md (0649a59fb02c); edit the skill, then run: nerd agents sync -->
2
2
 
3
3
  # Building on nerdsystem
4
4
 
@@ -132,8 +132,8 @@ What the tests do not prove: that the feature is the one the person wanted, or t
132
132
  Tests show the app does what was checked. They cannot show that the server trusts a price the browser sent, that any signed-in person can approve what only staff should, or that two people can book the same slot. Looking for those is your job, before every deploy that changes the server, the data model, or what a screen does. Nobody else reads each change: nerdsystem reads the code itself only now and then.
133
133
 
134
134
  1. Run `nerd review checklist`. It prints what to look for (the `review/` rules), the files that changed since the last review, the findings still open from earlier reviews, and the format of the report.
135
- 2. Have it done with fresh eyes. In Claude Code, give the whole checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/` (it only reads; if this session does not have it yet, use any read-only subagent). In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
- 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer.
135
+ 2. Have it done with fresh eyes. In Claude Code, give the checklist to the `nerd-reviewer` subagent that ships in `.claude/agents/`, pasted or saved to a file it can read (it only reads). If the session says that subagent is not found, it was added after the session started: give a read-only subagent the file `.claude/agents/nerd-reviewer.md` as its instructions. In another agent, use a subagent or a new task that has not seen this conversation. With neither, do it yourself, reading the files again from disk and not from memory.
136
+ 3. Check each thing raised against the code, because a review can be wrong. Fix what is real. A finding marked "for the person to decide" is theirs: ask it as a plain question, with no file names, and wait for the answer. Something real that the person decides to leave, or that the rule itself says is not worth changing working code for, goes in the report as `leftAsIs`: it is shown to them, kept, and not raised again at the start of every session.
137
137
  4. Write `.nerd/review.json` as the checklist describes and run `nerd review report`. Then `nerd check` and `nerd deploy`.
138
138
 
139
139
  The report is stored as your review, and the person sees it in their dashboard under Review together with the plain-language summary you wrote for them, so write that for someone who cannot read code. Report what you found, including what you fixed. When nerdsystem does read the code itself, it compares: a finding from its read stays open until your report says it is still there, fixed, or checked and not applicable, and a clean report it then contradicts is recorded as a miss. Never report a review you did not do, and never use `dismissed` to make a finding go away.