nerdsystem 0.0.25 → 0.0.26
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/dist/commands/claude-md-history.d.ts +9 -0
- package/dist/commands/claude-md-history.js +18 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +23 -5
- package/dist/commands/init.js +1 -1
- package/dist/commands/session-review.d.ts +12 -0
- package/dist/commands/session-review.js +23 -17
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +12 -7
- package/dist/commands/upgrade.js +9 -1
- package/dist/commands/verify.js +2 -1
- package/dist/gates/design.js +3 -0
- package/dist/gates/review.js +2 -1
- package/dist/gates/versions.js +3 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +8 -0
- package/package.json +1 -1
- package/template/.claude/agents/nerd-reviewer.md +1 -1
- package/template/.claude/skills/nerdsystem/SKILL.md +2 -2
- package/template/.cursor/rules/nerdsystem.mdc +3 -3
- package/template/.github/copilot-instructions.md +3 -3
- package/template/AGENTS.md +3 -3
- package/template/GEMINI.md +3 -3
|
@@ -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[];
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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());
|
package/dist/commands/init.js
CHANGED
|
@@ -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.
|
|
195
|
+
const SDK_VERSION = "0.0.26";
|
|
@@ -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,
|
|
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" ? " (from 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. 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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
/**
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -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.");
|
package/dist/commands/verify.js
CHANGED
|
@@ -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];
|
package/dist/gates/design.js
CHANGED
|
@@ -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.
|
package/dist/gates/review.js
CHANGED
|
@@ -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
|
|
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
|
},
|
package/dist/gates/versions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
package/template/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated from .claude/skills/nerdsystem/SKILL.md (
|
|
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
|
|
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.
|
package/template/GEMINI.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- generated from .claude/skills/nerdsystem/SKILL.md (
|
|
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
|
|
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.
|