@nanopm/cli 0.1.13 → 0.1.16
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/index.js +158 -46
- package/package.json +1 -1
- package/skills/build/SKILL.md +3 -1
package/dist/index.js
CHANGED
|
@@ -5420,10 +5420,16 @@ var isDecideForMe = (answer) => /^\s*decide(\s+for\s+me)?\s*\.?\s*$/i.test(answe
|
|
|
5420
5420
|
function answerForAgent(answer) {
|
|
5421
5421
|
return isDecideForMe(answer) ? DECIDE_FOR_ME : answer;
|
|
5422
5422
|
}
|
|
5423
|
-
var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr"];
|
|
5423
|
+
var BUILD_HISTORY_KINDS = ["worktree", "plan", "step", "question", "answer", "paused", "pr", "handed"];
|
|
5424
|
+
var BUILD_CONFLICT = { resolve: "Let Nano resolve it", mine: "I\u2019ll do it in my terminal" };
|
|
5425
|
+
var chose = (answer, option) => {
|
|
5426
|
+
const plain = (s) => s.trim().toLowerCase().replace(/[’‘]/g, "'");
|
|
5427
|
+
return plain(answer) === plain(option);
|
|
5428
|
+
};
|
|
5429
|
+
var BUILD_REFUSED = { again: "Try again", open: "Open it on GitHub" };
|
|
5424
5430
|
function trustedPlace(p, at) {
|
|
5425
5431
|
const [branch, base, fork] = [p.branch, p.base, p.fork].map((v) => typeof v === "string" ? v : "");
|
|
5426
|
-
if (!/^nano\/[a-z0-9][a-z0-9-]*$/.test(branch) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(base) || !/^[0-9a-f]{7,40}$/.test(fork)) return null;
|
|
5432
|
+
if (!/^nano\/[a-z0-9][a-z0-9-]*$/.test(branch) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(base) || /^refs\/|HEAD$/.test(base) || !/^[0-9a-f]{7,40}$/.test(fork)) return null;
|
|
5427
5433
|
return { dir: buildDir(at.home, at.jobId), branch, base, fork };
|
|
5428
5434
|
}
|
|
5429
5435
|
function buildHistory(events, at) {
|
|
@@ -5431,20 +5437,30 @@ function buildHistory(events, at) {
|
|
|
5431
5437
|
let plan = null;
|
|
5432
5438
|
const done = /* @__PURE__ */ new Set();
|
|
5433
5439
|
let last = null;
|
|
5440
|
+
let report = "";
|
|
5441
|
+
let handed = false;
|
|
5434
5442
|
const answers = /* @__PURE__ */ new Map();
|
|
5435
5443
|
for (const e of events) {
|
|
5436
|
-
if (e.kind === "worktree") place = trustedPlace(e.payload, at);
|
|
5444
|
+
if (e.kind === "worktree" && !place) place = trustedPlace(e.payload, at);
|
|
5437
5445
|
else if (e.kind === "plan" && Array.isArray(e.payload.steps)) plan = e.payload.steps;
|
|
5438
5446
|
else if (e.kind === "step" && typeof e.payload.step === "number") done.add(e.payload.step - 1);
|
|
5439
5447
|
else if (e.kind === "answer") answers.set(e.seq, String(e.payload.answer ?? ""));
|
|
5448
|
+
else if (e.kind === "handed") handed = true;
|
|
5449
|
+
if (e.kind === "question" && (e.payload.about === "conflict" || e.payload.about === "refused")) {
|
|
5450
|
+
if (typeof e.payload.report === "string") report = e.payload.report;
|
|
5451
|
+
if (e.payload.about === "conflict") handed = false;
|
|
5452
|
+
}
|
|
5440
5453
|
if (e.kind === "question" || e.kind === "paused") last = e;
|
|
5441
|
-
else if (e.kind === "plan" || e.kind === "step") last = null;
|
|
5454
|
+
else if (e.kind === "plan" || e.kind === "step" || e.kind === "handed") last = null;
|
|
5442
5455
|
}
|
|
5443
|
-
|
|
5444
|
-
if (
|
|
5445
|
-
|
|
5446
|
-
if (last
|
|
5447
|
-
|
|
5456
|
+
const found = (resume2) => ({ place, plan, done, resume: resume2, report, handed });
|
|
5457
|
+
if (!place) return found(null);
|
|
5458
|
+
const answered2 = last?.kind === "question" && answers.has(last.seq) ? answers.get(last.seq) : null;
|
|
5459
|
+
if (answered2 !== null && last.payload.about === "usage") return found({ why: "limit" });
|
|
5460
|
+
if (answered2 !== null && (last.payload.about === "conflict" || last.payload.about === "refused")) return found({ why: last.payload.about, answer: answered2 });
|
|
5461
|
+
if (answered2 !== null) return found({ why: "answer", answer: answered2 });
|
|
5462
|
+
if (last?.kind === "paused") return found({ why: "limit" });
|
|
5463
|
+
return found({ why: "lost" });
|
|
5448
5464
|
}
|
|
5449
5465
|
function wasOf(params) {
|
|
5450
5466
|
return params?.was === "proposed" ? "proposed" : "accepted";
|
|
@@ -5461,11 +5477,18 @@ var execFileAsync3 = promisify3(execFile3);
|
|
|
5461
5477
|
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
5462
5478
|
var TIMEOUT_MS = 6e4;
|
|
5463
5479
|
var LOCKED = {
|
|
5464
|
-
GIT_CONFIG_COUNT: "
|
|
5480
|
+
GIT_CONFIG_COUNT: "5",
|
|
5465
5481
|
GIT_CONFIG_KEY_0: "core.hooksPath",
|
|
5466
5482
|
GIT_CONFIG_VALUE_0: "/dev/null",
|
|
5467
5483
|
GIT_CONFIG_KEY_1: "core.fsmonitor",
|
|
5468
|
-
GIT_CONFIG_VALUE_1: "false"
|
|
5484
|
+
GIT_CONFIG_VALUE_1: "false",
|
|
5485
|
+
// A fetch never opens a nested repository the agent could have planted where a submodule goes.
|
|
5486
|
+
GIT_CONFIG_KEY_2: "fetch.recurseSubmodules",
|
|
5487
|
+
GIT_CONFIG_VALUE_2: "false",
|
|
5488
|
+
GIT_CONFIG_KEY_3: "submodule.recurse",
|
|
5489
|
+
GIT_CONFIG_VALUE_3: "false",
|
|
5490
|
+
GIT_CONFIG_KEY_4: "protocol.ext.allow",
|
|
5491
|
+
GIT_CONFIG_VALUE_4: "never"
|
|
5469
5492
|
};
|
|
5470
5493
|
async function exec(cmd, repo, args, timeoutMs = TIMEOUT_MS) {
|
|
5471
5494
|
if (typeof repo === "string") return execFileAsync3(cmd, cmd === "git" ? ["-C", repo, ...args] : args, { cwd: repo, maxBuffer: MAX_BUFFER, timeout: timeoutMs });
|
|
@@ -5483,7 +5506,7 @@ var RepoError = class extends Error {
|
|
|
5483
5506
|
async function commitAll(cwd, message, note) {
|
|
5484
5507
|
await git(cwd, ["add", "-A"]);
|
|
5485
5508
|
const staged = await git(cwd, ["diff", "--cached", "--name-only"]).catch(() => "");
|
|
5486
|
-
if (!staged) return null;
|
|
5509
|
+
if (!staged && !await merging(cwd)) return null;
|
|
5487
5510
|
await git(cwd, ["commit", "--no-verify", "-m", message, ...note ? ["-m", note] : [], "-m", `Written by NanoPM. Review before merging.
|
|
5488
5511
|
|
|
5489
5512
|
${BUILD_TRAILER}`]);
|
|
@@ -5564,6 +5587,42 @@ async function rebaseOnto(cwd, base) {
|
|
|
5564
5587
|
return { ok: false, files };
|
|
5565
5588
|
}
|
|
5566
5589
|
}
|
|
5590
|
+
async function containsBase(cwd, base) {
|
|
5591
|
+
const onto = await baseRef(cwd, base);
|
|
5592
|
+
return git(cwd, ["merge-base", "--is-ancestor", onto, "HEAD"]).then(
|
|
5593
|
+
() => true,
|
|
5594
|
+
() => false
|
|
5595
|
+
);
|
|
5596
|
+
}
|
|
5597
|
+
async function merging(cwd) {
|
|
5598
|
+
return Boolean(await git(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]).catch(() => ""));
|
|
5599
|
+
}
|
|
5600
|
+
async function mergedSince(cwd, fork) {
|
|
5601
|
+
return Boolean(await git(cwd, ["rev-list", "--merges", "-n", "1", `${fork}..HEAD`]).catch(() => ""));
|
|
5602
|
+
}
|
|
5603
|
+
async function mergeBase(cwd, base) {
|
|
5604
|
+
if (await merging(cwd)) return { files: await conflictsLeft(cwd, base) };
|
|
5605
|
+
const onto = await baseRef(cwd, base);
|
|
5606
|
+
try {
|
|
5607
|
+
await git(cwd, ["merge", "--no-ff", "--no-commit", "--no-verify", onto], 12e4);
|
|
5608
|
+
return { files: [] };
|
|
5609
|
+
} catch (err) {
|
|
5610
|
+
const files = (await git(cwd, ["diff", "--name-only", "--diff-filter=U"]).catch(() => "")).split("\n").filter(Boolean);
|
|
5611
|
+
if (!files.length) throw new RepoError(`merging ${base} failed: ${errorMessage(err).split("\n").filter(Boolean).slice(-2).join(" ")}`);
|
|
5612
|
+
return { files };
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
async function conflictsLeft(cwd, base) {
|
|
5616
|
+
const onto = await baseRef(cwd, base);
|
|
5617
|
+
const changed = (await git(cwd, ["diff", "--name-only", onto]).catch(() => "")).split("\n").filter(Boolean);
|
|
5618
|
+
if (!changed.length) return [];
|
|
5619
|
+
const marked = await git(cwd, ["grep", "-l", "-E", "^(<<<<<<<|>>>>>>>)( |$)", "--", ...changed]).catch(() => "");
|
|
5620
|
+
return marked.split("\n").filter(Boolean).sort();
|
|
5621
|
+
}
|
|
5622
|
+
async function newPullRequestUrl(cwd, branch) {
|
|
5623
|
+
const repo = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await originUrl(cwd))?.[1];
|
|
5624
|
+
return repo ? `https://github.com/${repo}/pull/new/${branch}` : null;
|
|
5625
|
+
}
|
|
5567
5626
|
async function gh(cwd, args) {
|
|
5568
5627
|
return (await exec("gh", cwd, args, 12e4)).stdout.trim();
|
|
5569
5628
|
}
|
|
@@ -11160,16 +11219,16 @@ async function runBuildJob(input) {
|
|
|
11160
11219
|
let plan = history.plan;
|
|
11161
11220
|
const done = new Set(history.done);
|
|
11162
11221
|
let usage;
|
|
11163
|
-
let said =
|
|
11222
|
+
let said = history.report;
|
|
11164
11223
|
const stop2 = async (message, opts = { keep: true }) => {
|
|
11165
11224
|
await unstartSolution(client, job.project_id, solution.ref, was).catch(() => void 0);
|
|
11166
|
-
if (opts.keep) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
11225
|
+
if (opts.keep && !await merging(worktree)) await commitAll(worktree, `${solution.ref}: unfinished \u2014 ${solution.title}`).catch(() => null);
|
|
11167
11226
|
else await removeWorktree(clone, dir);
|
|
11168
11227
|
return { ...failed(opts.keep ? `${message}. What was done is on ${branch}, in ${dir}.` : message), usage };
|
|
11169
11228
|
};
|
|
11170
11229
|
const park = async (why) => {
|
|
11171
11230
|
if ("ask" in why) {
|
|
11172
|
-
await events.write("question", { ...why.ask, ...plan ? {} : { about: "plan" } });
|
|
11231
|
+
await events.write("question", { ...why.ask, ...why.ask.about ? { report: said } : plan ? {} : { about: "plan" } });
|
|
11173
11232
|
await log("build.ask", `${solution.ref} waits on the founder`);
|
|
11174
11233
|
} else if (why.resetsAt) {
|
|
11175
11234
|
await events.write("paused", { why: "limit", resets_at: new Date(why.resetsAt * 1e3).toISOString() });
|
|
@@ -11208,57 +11267,110 @@ async function runBuildJob(input) {
|
|
|
11208
11267
|
}
|
|
11209
11268
|
return end;
|
|
11210
11269
|
};
|
|
11270
|
+
const resolving = (files) => `\`${base}\` moved while you were building, and it is merged into your branch: ${files.map((f) => `\`${f}\``).join(", ")} still have conflict markers. Resolve each so both sides' intent holds, remove every marker, run the project's check again, then give your report again, whole and in the same form, with what you kept under *Decisions I made*. Do not call build_step, and do not commit: NanoPM commits the merge.`;
|
|
11271
|
+
const left = async () => {
|
|
11272
|
+
if (!finished()) return carryOn(plan, done);
|
|
11273
|
+
if (history.handed) return null;
|
|
11274
|
+
const files = await conflictsLeft(worktree, base);
|
|
11275
|
+
return files.length ? resolving(files) : null;
|
|
11276
|
+
};
|
|
11277
|
+
const openPr = async (reviewUrl) => {
|
|
11278
|
+
const [noPr, page] = await Promise.all([ghReady(worktree), solutionPage(client, job.project_id, solution.ref)]);
|
|
11279
|
+
if (noPr) return pushed(null, reviewUrl, noPr);
|
|
11280
|
+
const pr = await openPullRequest(worktree, { branch, base, title: `${solution.ref}: ${solution.title}`, body: prBody(said, page) });
|
|
11281
|
+
if (!pr.ok) return park({ ask: { question: "The branch is pushed, but GitHub refused the pull request.", why: pr.reason, options: [BUILD_REFUSED.again, BUILD_REFUSED.open], about: "refused" } });
|
|
11282
|
+
return pushed(pr, null);
|
|
11283
|
+
};
|
|
11284
|
+
const pushed = async (pr, reviewUrl, why = "not yet") => {
|
|
11285
|
+
if (pr) {
|
|
11286
|
+
await events.write("pr", { number: pr.number, url: pr.url });
|
|
11287
|
+
await log("build.pr", `draft pull request #${pr.number} for ${solution.ref}`);
|
|
11288
|
+
}
|
|
11289
|
+
await removeWorktree(clone, dir);
|
|
11290
|
+
return {
|
|
11291
|
+
status: "succeeded",
|
|
11292
|
+
result: [said.trim(), "", pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${why}.${reviewUrl ? ` ${reviewUrl}` : ""}`].filter(Boolean).join("\n"),
|
|
11293
|
+
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: reviewUrl ?? null } },
|
|
11294
|
+
usage
|
|
11295
|
+
};
|
|
11296
|
+
};
|
|
11211
11297
|
const resume2 = history.resume;
|
|
11212
11298
|
const check = verifyCommand2 ? `
|
|
11213
11299
|
|
|
11214
11300
|
The project's own check is \`${verifyCommand2}\`. Run it before your last build_step, fix what it reports, and give its result under *How I checked it*.` : "";
|
|
11215
11301
|
const finished = () => Boolean(plan && !stepsLeft(plan, done).length);
|
|
11302
|
+
if (resume2?.why === "conflict" && chose(resume2.answer, BUILD_CONFLICT.mine)) {
|
|
11303
|
+
const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
|
|
11304
|
+
if ("error" in opened) return stop2(opened.error);
|
|
11305
|
+
await events.write("handed", {});
|
|
11306
|
+
return stop2(`It is yours to resolve: ${opened.files.join(", ")} carry conflict markers in that copy. Edit them, then Continue (nanopm build ${solution.ref} --continue): NanoPM commits the merge`);
|
|
11307
|
+
}
|
|
11308
|
+
if (resume2?.why === "refused" && chose(resume2.answer, BUILD_REFUSED.open)) return pushed(null, await newPullRequestUrl(worktree, branch), "you open it on GitHub");
|
|
11216
11309
|
if (!resume2) {
|
|
11217
11310
|
await log("build.start", `building ${solution.ref} on ${branch}, in a copy of your code`);
|
|
11218
11311
|
await events.write("text", { text: `Working on ${branch}, in a copy of your code. Your own checkout is not touched.` });
|
|
11219
11312
|
} else {
|
|
11220
|
-
await events.write("text", { text: AGAIN[resume2.why]
|
|
11313
|
+
await events.write("text", { text: AGAIN[resume2.why] });
|
|
11221
11314
|
if (resume2.why === "answer" && spec2.answerFile) await writeFile5(spec2.answerFile, answerForAgent(resume2.answer), { mode: 384 });
|
|
11222
11315
|
}
|
|
11223
|
-
|
|
11224
|
-
|
|
11316
|
+
if (resume2?.why === "refused") return openPr();
|
|
11317
|
+
let turn = null;
|
|
11318
|
+
if (!resume2) turn = await session2(spec2.prompt + check, false);
|
|
11319
|
+
else if (resume2.why === "answer") turn = await session2("", true);
|
|
11320
|
+
else if (resume2.why === "conflict") {
|
|
11321
|
+
const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
|
|
11322
|
+
if ("error" in opened) return stop2(opened.error);
|
|
11323
|
+
if (opened.files.length) turn = await session2(resolving(opened.files), true);
|
|
11324
|
+
} else {
|
|
11325
|
+
const next = await left();
|
|
11326
|
+
if (next) turn = await session2(`${resume2.why === "limit" ? "Your Claude limit has reset." : "You were interrupted: the machine stopped."} ${next}`, true);
|
|
11327
|
+
}
|
|
11328
|
+
for (let resumes = 0; !turn; resumes += 1) {
|
|
11329
|
+
const next = await left();
|
|
11330
|
+
if (!next) break;
|
|
11225
11331
|
if (resumes >= BUILD_RESUMES_MAX) {
|
|
11226
|
-
|
|
11227
|
-
|
|
11332
|
+
if (finished()) break;
|
|
11333
|
+
const steps = stepsLeft(plan, done).length;
|
|
11334
|
+
return stop2(plan ? `Nano stopped with ${steps} step${steps === 1 ? "" : "s"} of its plan left` : "Nano stopped without saying its plan");
|
|
11228
11335
|
}
|
|
11229
|
-
turn = await session2(
|
|
11336
|
+
turn = await session2(next, true);
|
|
11230
11337
|
}
|
|
11231
11338
|
if (turn && "park" in turn) return park(turn.park);
|
|
11232
11339
|
if (turn) return stop2(turn.stop.error?.message ?? "the build stopped");
|
|
11340
|
+
const marked = await conflictsLeft(worktree, base);
|
|
11341
|
+
if (marked.length) return park({ ask: conflictAsk(base, marked) });
|
|
11233
11342
|
await commitAll(worktree, `${solution.ref}: ${solution.title}`);
|
|
11234
11343
|
if (!await commitsSinceFork(worktree, fork)) return stop2("the build finished without changing anything", { keep: false });
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
"",
|
|
11251
|
-
pr ? `Draft pull request #${pr.number} \xB7 ${pr.url}` : `Pushed ${branch}. No pull request: ${noPr}.${pushed.reviewUrl ? ` ${pushed.reviewUrl}` : ""}`
|
|
11252
|
-
].filter(Boolean).join("\n"),
|
|
11253
|
-
data: { branch, session: spec2.sessionId, ...pr ? { pr: { number: pr.number, url: pr.url } } : { review_url: pushed.reviewUrl ?? null } },
|
|
11254
|
-
usage
|
|
11255
|
-
};
|
|
11344
|
+
if (!await containsBase(worktree, base)) {
|
|
11345
|
+
if (await mergedSince(worktree, fork)) {
|
|
11346
|
+
const opened = await mergeBase(worktree, base).catch((err) => ({ error: errorMessage(err) }));
|
|
11347
|
+
if ("error" in opened) return stop2(opened.error);
|
|
11348
|
+
if (opened.files.length) return park({ ask: conflictAsk(base, opened.files) });
|
|
11349
|
+
await commitAll(worktree, `${solution.ref}: ${base} merged in`);
|
|
11350
|
+
} else {
|
|
11351
|
+
const rebased = await rebaseOnto(worktree, base);
|
|
11352
|
+
if (!rebased.ok && !rebased.files.length) return stop2(`the rebase onto ${base} failed: see git status`);
|
|
11353
|
+
if (!rebased.ok) return park({ ask: conflictAsk(base, rebased.files) });
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
const push = await pushBranch(worktree, branch);
|
|
11357
|
+
if (!push.pushed) return stop2(`the push failed: ${push.reason}`);
|
|
11358
|
+
return openPr(push.reviewUrl);
|
|
11256
11359
|
}
|
|
11360
|
+
var conflictAsk = (base, files) => ({
|
|
11361
|
+
question: `Someone changed the same files on ${base} while Nano was building.`,
|
|
11362
|
+
why: "Nano can try to merge them and run your checks again, or you resolve it yourself in your terminal. Nothing is pushed until it is resolved.",
|
|
11363
|
+
files,
|
|
11364
|
+
options: [BUILD_CONFLICT.resolve, BUILD_CONFLICT.mine],
|
|
11365
|
+
about: "conflict"
|
|
11366
|
+
});
|
|
11257
11367
|
var failed = (message) => ({ status: "failed", error: { code: "harness_error", message } });
|
|
11258
11368
|
var AGAIN = {
|
|
11259
|
-
answer:
|
|
11260
|
-
limit:
|
|
11261
|
-
lost:
|
|
11369
|
+
answer: "Your answer is in: carrying on.",
|
|
11370
|
+
limit: "Your Claude limit has reset: carrying on.",
|
|
11371
|
+
lost: "Picked up again where it stopped.",
|
|
11372
|
+
conflict: "Merging the main branch in, and resolving it.",
|
|
11373
|
+
refused: "Asking GitHub for the pull request again."
|
|
11262
11374
|
};
|
|
11263
11375
|
function askOf(input) {
|
|
11264
11376
|
const { question, options, why } = input ?? {};
|
|
@@ -14492,7 +14604,7 @@ import { promisify as promisify8 } from "util";
|
|
|
14492
14604
|
// package.json
|
|
14493
14605
|
var package_default = {
|
|
14494
14606
|
name: "@nanopm/cli",
|
|
14495
|
-
version: "0.1.
|
|
14607
|
+
version: "0.1.16",
|
|
14496
14608
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14497
14609
|
type: "module",
|
|
14498
14610
|
license: "MIT",
|
package/package.json
CHANGED
package/skills/build/SKILL.md
CHANGED
|
@@ -52,7 +52,9 @@ question per `ask_user`, with the options, and what you looked at in `why`.
|
|
|
52
52
|
stops to wait on a background task is a build that stops.
|
|
53
53
|
- The analytics events the spec names ship with the change, named as it says. No word a user
|
|
54
54
|
typed goes in an event.
|
|
55
|
-
- Do not touch secrets. `.env` files and keys are denied to you, deliberately.
|
|
55
|
+
- Do not touch secrets. `.env` files and keys are denied to you, deliberately. Your sandbox keeps
|
|
56
|
+
you from reading them, so `git status` and `git diff` show them as deleted (`D`) and print
|
|
57
|
+
"Operation not permitted": they are not deleted. Never report them, restore them or stage them.
|
|
56
58
|
- Do not reformat, rename or tidy anything the spec did not ask for. A diff the founder cannot
|
|
57
59
|
review in one sitting is a diff they will not merge.
|
|
58
60
|
- Install a new dependency only if the spec allows it; otherwise ask.
|