@nanopm/cli 0.1.8 → 0.1.10
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 +107 -78
- package/package.json +1 -1
- package/skills/spec/SKILL.md +2 -2
- package/skills/spec/skill.json +1 -1
- package/skills/spec-agent/SKILL.md +16 -14
- package/skills/spec-agent/skill.json +1 -1
- package/skills/spec-review/SKILL.md +24 -16
- package/skills/spec-review/skill.json +2 -2
- package/skills/{spec-brief → spec-solution}/SKILL.md +47 -29
- package/skills/spec-solution/skill.json +9 -0
- package/skills/spec-brief/skill.json +0 -9
package/dist/index.js
CHANGED
|
@@ -9234,6 +9234,9 @@ function gateOne(ctx, w, leaf) {
|
|
|
9234
9234
|
return { draft, refused };
|
|
9235
9235
|
}
|
|
9236
9236
|
function draftOf(ctx, w) {
|
|
9237
|
+
return { ...draftFields(w), sources: resolve3(ctx, w.restsOn) };
|
|
9238
|
+
}
|
|
9239
|
+
function draftFields(w) {
|
|
9237
9240
|
const headline = w.changelog.headline.trim().replace(/\s+/g, " ");
|
|
9238
9241
|
const room = CHANGELOG_MAX - headline.length - 1;
|
|
9239
9242
|
return {
|
|
@@ -9243,8 +9246,7 @@ function draftOf(ctx, w) {
|
|
|
9243
9246
|
steps: w.steps.map((s) => ({ do: s.do.trim().replace(/\s+/g, " "), who: s.who, ...s.checks ? { checks: true } : {} })),
|
|
9244
9247
|
size: w.size,
|
|
9245
9248
|
assumption: clipOrNull(w.worksIf, WORKS_IF_MAX),
|
|
9246
|
-
reasoning: wholeSentences(w.why, WHY_THIS_MAX)
|
|
9247
|
-
sources: resolve3(ctx, w.restsOn)
|
|
9249
|
+
reasoning: wholeSentences(w.why, WHY_THIS_MAX)
|
|
9248
9250
|
};
|
|
9249
9251
|
}
|
|
9250
9252
|
function resolve3(ctx, ids) {
|
|
@@ -9312,14 +9314,13 @@ async function readRoadmap(client, projectId) {
|
|
|
9312
9314
|
var SPEC_SKILL = "spec";
|
|
9313
9315
|
var SPEC_MINUTES = 5;
|
|
9314
9316
|
var SPEC_STEPS = {
|
|
9315
|
-
|
|
9316
|
-
agent: { says: "Turning the
|
|
9317
|
+
solution: { says: "Writing what your users will see, and how we\u2019ll know", label: "Writing what your users will see" },
|
|
9318
|
+
agent: { says: "Turning the solution into a spec a coding agent can build from", label: "Writing the agent spec" },
|
|
9317
9319
|
review: { says: "Reading both as you would, then as a coding agent would", label: "Checking both as a coding agent would" }
|
|
9318
9320
|
};
|
|
9319
|
-
function
|
|
9320
|
-
if (!
|
|
9321
|
-
if (!
|
|
9322
|
-
if (!brief2.measure.signal.trim()) return "it does not say how we will know";
|
|
9321
|
+
function specRefusal(spec2) {
|
|
9322
|
+
if (!spec2.experience.some((s) => s.trim())) return "it does not walk through the experience";
|
|
9323
|
+
if (!spec2.measure.signal.trim()) return "it does not say how we will know";
|
|
9323
9324
|
return null;
|
|
9324
9325
|
}
|
|
9325
9326
|
function agentSpecRefusal(markdown) {
|
|
@@ -9327,19 +9328,25 @@ function agentSpecRefusal(markdown) {
|
|
|
9327
9328
|
if (!/^##\s+Acceptance criteria\b/im.test(markdown)) return "it has no acceptance criteria";
|
|
9328
9329
|
return null;
|
|
9329
9330
|
}
|
|
9330
|
-
function specsRefusal({
|
|
9331
|
-
const b =
|
|
9332
|
-
if (b) return `the
|
|
9331
|
+
function specsRefusal({ spec: spec2, markdown }) {
|
|
9332
|
+
const b = specRefusal(spec2);
|
|
9333
|
+
if (b) return `the spec ${b}`;
|
|
9333
9334
|
const a = agentSpecRefusal(markdown);
|
|
9334
9335
|
return a ? `the agent spec ${a}` : null;
|
|
9335
9336
|
}
|
|
9337
|
+
function specRecord(mode, answer) {
|
|
9338
|
+
if (mode === "write") return { spec: answer.spec, markdown: answer.markdown, solution: null };
|
|
9339
|
+
if (!answer.solution) throw new Error("a rework writes the whole solution again, and this one came back without it");
|
|
9340
|
+
return { spec: answer.spec, markdown: answer.markdown, solution: answer.solution };
|
|
9341
|
+
}
|
|
9336
9342
|
function specSolutionOf(params) {
|
|
9337
9343
|
const id = params?.solution;
|
|
9338
9344
|
return typeof id === "string" ? id : null;
|
|
9339
9345
|
}
|
|
9340
9346
|
async function queueSpecs(client, ask7) {
|
|
9347
|
+
const mode = ask7.mode ?? "write";
|
|
9341
9348
|
const [{ data: solution }, { data: active }, { data: clone }] = await Promise.all([
|
|
9342
|
-
client.from("solutions").select("id, ref, status, opportunity_id").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
|
|
9349
|
+
client.from("solutions").select("id, ref, status, opportunity_id, specced_at").eq("project_id", ask7.projectId).eq("ref", ask7.ref.trim().toUpperCase()).maybeSingle(),
|
|
9343
9350
|
client.from("jobs").select("params").eq("project_id", ask7.projectId).eq("skill", SPEC_SKILL).in("status", [...ACTIVE_STATUSES]),
|
|
9344
9351
|
// The clone on the machine that will run it: the daemon reads the code only in its own (`resolveCwd`).
|
|
9345
9352
|
client.from("clones").select("repo_path").eq("project_id", ask7.projectId).eq("daemon_id", ask7.daemonId).maybeSingle()
|
|
@@ -9347,13 +9354,15 @@ async function queueSpecs(client, ask7) {
|
|
|
9347
9354
|
if (!solution || !solution.opportunity_id) throw new Error(`No solution ${ask7.ref.toUpperCase()} on the Roadmap.`);
|
|
9348
9355
|
if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
|
|
9349
9356
|
if ((active ?? []).some((j) => specSolutionOf(j.params) === solution.id)) throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
|
|
9357
|
+
if (mode === "write" && solution.specced_at) throw new Error(`${solution.ref} has its specs already: rework the solution to write it all again.`);
|
|
9358
|
+
if (mode === "rework" && !solution.specced_at) throw new Error(`${solution.ref} has no specs yet: write them first.`);
|
|
9350
9359
|
const { data, error } = await client.from("jobs").insert({
|
|
9351
9360
|
project_id: ask7.projectId,
|
|
9352
9361
|
skill: SPEC_SKILL,
|
|
9353
9362
|
requested_by: ask7.userId,
|
|
9354
9363
|
trigger: "user",
|
|
9355
9364
|
target_daemon_id: ask7.daemonId,
|
|
9356
|
-
params: { solution: solution.id, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
|
|
9365
|
+
params: { solution: solution.id, ...mode === "rework" ? { rework: true } : {}, ...clone?.repo_path ? { repo_path: clone.repo_path } : {} }
|
|
9357
9366
|
}).select("id").single();
|
|
9358
9367
|
if (error?.code === "23505") throw new Error(`Nano is already writing the specs of ${solution.ref}.`);
|
|
9359
9368
|
if (error || !data) throw new Error(error?.message ?? "could not ask Nano for the specs");
|
|
@@ -9364,40 +9373,30 @@ async function queueSpecs(client, ask7) {
|
|
|
9364
9373
|
import { z as z15 } from "zod";
|
|
9365
9374
|
var SpecParams = z15.object({
|
|
9366
9375
|
solution: z15.guid(),
|
|
9376
|
+
/** *Rework the solution* (spec 74): the whole solution written again, as a new version. */
|
|
9377
|
+
rework: z15.literal(true).optional(),
|
|
9367
9378
|
repo_path: z15.string().optional(),
|
|
9368
9379
|
background: z15.boolean().optional()
|
|
9369
9380
|
}).strict();
|
|
9370
9381
|
function specAsk(params) {
|
|
9371
9382
|
const parsed = SpecParams.safeParse(params);
|
|
9372
|
-
if (parsed.success) return { ok: true, solution: parsed.data.solution };
|
|
9383
|
+
if (parsed.success) return { ok: true, solution: parsed.data.solution, mode: parsed.data.rework ? "rework" : "write" };
|
|
9373
9384
|
const issue = parsed.error.issues[0];
|
|
9374
9385
|
const unknown2 = issue.code === "unrecognized_keys" ? issue.keys?.join(", ") : null;
|
|
9375
9386
|
return { ok: false, why: unknown2 ? `this run does not take ${unknown2}` : `${issue.path.join(".") || "params"}: ${issue.message}` };
|
|
9376
9387
|
}
|
|
9377
|
-
var
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
fork: z15.object({
|
|
9381
|
-
after: z15.number().int().describe("The step the path forks after, counted from 1."),
|
|
9382
|
-
when: prose(1).describe('When it forks, a few words: "No closed search".'),
|
|
9383
|
-
then: prose(1).describe("Where it goes instead, a few words.")
|
|
9384
|
-
}).nullable().default(null).describe("One fork at most, only when the path really splits.")
|
|
9385
|
-
});
|
|
9386
|
-
var BeforeAfter = z15.object({
|
|
9387
|
-
kind: z15.literal("before_after"),
|
|
9388
|
-
before: z15.array(prose(1)).max(6).describe("What the user lives today, two to five short lines."),
|
|
9389
|
-
after: z15.array(prose(1)).max(6).describe("What they will live, two to five short lines, side by side with before.")
|
|
9388
|
+
var Diagram = z15.object({
|
|
9389
|
+
caption: prose(1).describe('What it shows, a few words: "Starting the trial on a closed search".'),
|
|
9390
|
+
mermaid: z15.string().min(1).max(4e3).describe("The diagram in Mermaid syntax, whichever type helps most: flowchart, sequenceDiagram, stateDiagram-v2, journey, or a flowchart with two subgraphs for before and after. At most about twelve nodes, labels of a few words in quotes. No styling: no classDef, style, colours, click or links.")
|
|
9390
9391
|
});
|
|
9391
|
-
var
|
|
9392
|
-
questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("
|
|
9393
|
-
in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone."),
|
|
9394
|
-
diagrams: z15.array(
|
|
9392
|
+
var SpecFields = {
|
|
9393
|
+
questions: z15.array(z15.object({ ask: prose(1).describe("The question, one sentence."), blocks: prose(1).describe('What it blocks, a few words: "what the trial screen says".') })).max(5).default([]).describe("Questions for you: up to three questions only the founder can answer, and any place the code shows the solution as settled is wrong. Decide everything else yourself."),
|
|
9394
|
+
in_short: prose(1).describe("In short: three sentences at most \u2014 what we build, for whom, what changes for them. The first could be read alone. Read first, right after the problem: not the proposed solution again, its gist."),
|
|
9395
|
+
diagrams: z15.array(Diagram).max(2).default([]).describe("Optional: only when a drawing makes it faster to understand than the words. At most two, in Mermaid, the shape that helps most."),
|
|
9395
9396
|
experience: z15.array(prose(1)).max(8).describe("The experience: three to six steps, in the present tense, from the user\u2019s side of the screen. Under a hypothesis, the founder\u2019s check comes first, in one line."),
|
|
9396
|
-
build: z15.array(prose(1)).max(8).describe("What we build: three to six points, in users\u2019 words \u2014 what is true the day it ships."),
|
|
9397
9397
|
wont_do: z15.array(z15.object({ what: prose(1), why: prose(1) })).max(6).default([]).describe("What we won\u2019t do: two to four points, each with why or when."),
|
|
9398
9398
|
measure: z15.object({
|
|
9399
9399
|
signal: prose(1).describe("The signal, one sentence: who \xB7 does what \xB7 how many \xB7 by when. Small numbers for a product with few users. Never a baseline you did not measure."),
|
|
9400
|
-
wrong_if: prose(1).describe("We were wrong if\u2026, one sentence. Under a hypothesis, it can show the problem itself wrong."),
|
|
9401
9400
|
existing: z15.array(z15.object({ name: prose(1).describe("The event as the product records it, exactly."), counts: prose(1).describe("What it counts here, and where you found it.") })).max(8).default([]).describe("Events the product already records that measure this, found in the code or in the connected source. Only what you found."),
|
|
9402
9401
|
added: z15.array(
|
|
9403
9402
|
z15.object({
|
|
@@ -9411,22 +9410,26 @@ var BriefFields = {
|
|
|
9411
9410
|
dependencies: z15.array(prose(1)).max(6).default([]).describe('What must exist first. Empty when nothing: the page says None. No analytics tool in the product: "An analytics tool" is one.')
|
|
9412
9411
|
};
|
|
9413
9412
|
var Notes2 = prose().nullable().default(null).describe("A sentence or two, for the Journal.");
|
|
9414
|
-
var
|
|
9415
|
-
|
|
9416
|
-
|
|
9413
|
+
var Code = z15.array(z15.object({ path: z15.string().max(200), holds: prose(1).describe("What it holds that matters here, one line.") })).max(16).default([]).describe("The files you read that matter for this change, each with what it holds, for the agent spec\u2019s *Start here*. Empty when the code was not there.");
|
|
9414
|
+
var Rewritten = Written.omit({ restsOn: true });
|
|
9415
|
+
var SolutionAnswer = z15.object({
|
|
9416
|
+
solution: Rewritten.nullable().default(null).describe('Only when the snapshot says mode "rework": the whole solution, written again. Null in mode "write": those fields are settled.'),
|
|
9417
|
+
...SpecFields,
|
|
9418
|
+
code: Code,
|
|
9417
9419
|
notes: Notes2
|
|
9418
9420
|
});
|
|
9421
|
+
var AgentMarkdown = z15.string().min(1).max(6e4);
|
|
9419
9422
|
var AgentAnswer = z15.object({
|
|
9420
|
-
markdown:
|
|
9423
|
+
markdown: AgentMarkdown.describe("The agent spec, whole, in markdown, with the sections named in your instructions."),
|
|
9421
9424
|
notes: Notes2
|
|
9422
9425
|
});
|
|
9423
9426
|
var ReviewAnswer = z15.object({
|
|
9424
|
-
|
|
9425
|
-
markdown:
|
|
9427
|
+
spec: z15.object(SpecFields).describe("The solution\u2019s sections, whole, corrected where they failed a reader; as they were where they did not."),
|
|
9428
|
+
markdown: AgentMarkdown.describe("The agent spec, whole, corrected; as it was where it did not fail."),
|
|
9426
9429
|
exchanges: z15.array(z15.object({ asked: prose(1).describe("What the coding agent would have had to guess, as its question, one short sentence."), did: prose(1).describe("What you did: decided it (and what), or left it to the founder.") })).max(5).default([]).describe("Two to five: the questions a coding agent asked of the spec, and what you did about each. They go in the Journal."),
|
|
9427
9430
|
changed: z15.array(z15.string().max(200)).max(10).default([]).describe("What you changed, a line each, for the Journal.")
|
|
9428
9431
|
});
|
|
9429
|
-
var SCHEMAS7 = {
|
|
9432
|
+
var SCHEMAS7 = { solution: SolutionAnswer, agent: AgentAnswer, review: ReviewAnswer };
|
|
9430
9433
|
function parseSpecAnswer(role7, json2) {
|
|
9431
9434
|
return SCHEMAS7[role7].parse(json2);
|
|
9432
9435
|
}
|
|
@@ -9557,6 +9560,7 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9557
9560
|
const solution = row;
|
|
9558
9561
|
if (!solution?.opportunity_id) throw new Error("That solution is not on the Roadmap.");
|
|
9559
9562
|
if (solution.status === "rejected") throw new Error(`${solution.ref} was thrown out: it gets no specs.`);
|
|
9563
|
+
const mode = asked.mode;
|
|
9560
9564
|
const clone = typeof job.params?.repo_path === "string";
|
|
9561
9565
|
const [all, card, stance, numbers, connections, commit] = await Promise.all([
|
|
9562
9566
|
readOpportunities(client, projectId),
|
|
@@ -9571,16 +9575,8 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9571
9575
|
const path = leaf ? pathOf(tree, leaf.id) : [];
|
|
9572
9576
|
const known = {
|
|
9573
9577
|
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
title: solution.title,
|
|
9577
|
-
proposed_solution: solution.description,
|
|
9578
|
-
future_changelog: parseChangelog(solution.changelog),
|
|
9579
|
-
plan: solution.steps,
|
|
9580
|
-
size: solution.size,
|
|
9581
|
-
works_if: solution.assumption,
|
|
9582
|
-
why: solution.reasoning
|
|
9583
|
-
},
|
|
9578
|
+
mode,
|
|
9579
|
+
solution: snapshotOf({ ref: solution.ref, title: solution.title, description: solution.description, changelog: solution.changelog ?? "", steps: solution.steps, size: solution.size ?? "small", assumption: solution.assumption, reasoning: solution.reasoning }),
|
|
9584
9580
|
problem: leaf ? problemOf(tree, leaf) : null,
|
|
9585
9581
|
part_of: path.slice(1).map((p) => p.problem),
|
|
9586
9582
|
objective: lensOf(card.goals),
|
|
@@ -9590,27 +9586,37 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9590
9586
|
numbers
|
|
9591
9587
|
};
|
|
9592
9588
|
const reads = clone ? `the code read at ${commit ?? "its current commit"}` : "no clone here, so the code is not read";
|
|
9593
|
-
|
|
9589
|
+
const doing = mode === "rework" ? "Reworking" : "Writing the specs of";
|
|
9590
|
+
await journal("spec.read", `${doing} \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}): ${reads}; ${connections.length ? `${connections.length} connected source${connections.length === 1 ? "" : "s"}` : "no analytics connected"}.`);
|
|
9594
9591
|
const ctx = { adapter, spec: spec2, onEvent };
|
|
9595
9592
|
const timings = [];
|
|
9596
|
-
const time = (
|
|
9593
|
+
const time = (what2, cost) => timings.push({ what: what2, ms: cost.ms });
|
|
9597
9594
|
const repo = clone ? { repository: "the working directory", why_no_repository: null } : { repository: null, why_no_repository: "No clone of the product rode along with this run: work from what the snapshot says the product does." };
|
|
9598
9595
|
const writerGrant = [...clone ? ["read_repo"] : [], ...connections.length ? ["read_data"] : []];
|
|
9599
|
-
const
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9596
|
+
const writing = { grant: writerGrant, prompt: { ...known, ...repo, connections } };
|
|
9597
|
+
const written = await ask3(ctx, "solution", writing);
|
|
9598
|
+
time("solution", written.cost);
|
|
9599
|
+
const { solution: rewritten, code, notes, ...draft } = written.answer;
|
|
9600
|
+
const fields = mode === "rework" && rewritten ? draftFields(rewritten) : null;
|
|
9601
|
+
const wrote = fields ? `Solution written again: \u201C${clip3(fields.title, 80)}\u201D` : `Sections written: ${count(draft.experience.length, "step")} of the experience`;
|
|
9602
|
+
await step("spec.solution", `${wrote}, ${count(draft.questions.length, "question")} for you, ${count(draft.measure.added.length, "new event")}.${notes ? ` ${notes}` : ""}`, written.cost);
|
|
9603
|
+
const whole = fields ? snapshotOf({ ref: solution.ref, ...fields }) : known.solution;
|
|
9604
|
+
const first = specRecord(mode, { spec: draft, markdown: "", solution: fields });
|
|
9605
|
+
const unreadable = specRefusal(first.spec) ?? (first.solution && leaf ? solutionRefusal(first.solution, leaf) : null);
|
|
9606
|
+
if (unreadable) throw new Error(`The solution cannot be recorded: ${unreadable}.`);
|
|
9607
|
+
const target = await recordSections(client, { projectId, jobId: job.id, solution, spec: first.spec, fields: first.solution });
|
|
9608
|
+
await journal("spec.sections", first.solution ? `${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D takes ${solution.ref}\u2019s place on the board; its agent spec is on its way.` : `Sections on the sheet; the agent spec is on its way.`);
|
|
9603
9609
|
const agent = await ask3(ctx, "agent", {
|
|
9604
9610
|
grant: clone ? ["read_repo"] : [],
|
|
9605
|
-
prompt: { today: known.today, ref:
|
|
9611
|
+
prompt: { today: known.today, ref: target.ref, commit: commit ?? "not read", ...repo, solution: whole, spec: draft, code, problem: known.problem, product: known.product, constraints: stance }
|
|
9606
9612
|
});
|
|
9607
9613
|
time("agent spec", agent.cost);
|
|
9608
9614
|
await step("spec.agent", `Agent spec written.${agent.answer.notes ? ` ${agent.answer.notes}` : ""}`, agent.cost);
|
|
9609
|
-
let final = {
|
|
9615
|
+
let final = { spec: first.spec, markdown: agent.answer.markdown };
|
|
9610
9616
|
try {
|
|
9611
|
-
const review2 = await ask3(ctx, "review", { prompt: {
|
|
9617
|
+
const review2 = await ask3(ctx, "review", { prompt: { mode, solution: whole, spec: draft, markdown: agent.answer.markdown, problem: known.problem } });
|
|
9612
9618
|
time("review", review2.cost);
|
|
9613
|
-
const reviewed = {
|
|
9619
|
+
const reviewed = { spec: review2.answer.spec, markdown: review2.answer.markdown };
|
|
9614
9620
|
const refused2 = specsRefusal(reviewed);
|
|
9615
9621
|
if (refused2) await journal("spec.review_kept_out", `The review\u2019s version was left out (${refused2}): the writers\u2019 drafts are recorded.`);
|
|
9616
9622
|
else final = reviewed;
|
|
@@ -9624,19 +9630,15 @@ async function runSpecsJob({ client, job, adapter, spec: spec2, onEvent }) {
|
|
|
9624
9630
|
await journal("spec.review_failed", `The review did not finish (${clip3(errorMessage(err), 200)}): the writers\u2019 drafts are recorded.`);
|
|
9625
9631
|
}
|
|
9626
9632
|
const refused = specsRefusal(final);
|
|
9627
|
-
if (refused) throw new Error(`The specs cannot be
|
|
9628
|
-
const { error: published } = await client.rpc("
|
|
9629
|
-
p_project_id: projectId,
|
|
9630
|
-
p_solution_id: solution.id,
|
|
9631
|
-
p_brief: final.brief,
|
|
9632
|
-
p_agent_spec: final.markdown
|
|
9633
|
-
});
|
|
9633
|
+
if (refused) throw new Error(`The specs cannot be recorded: ${refused}.`);
|
|
9634
|
+
const { error: published } = await client.rpc("publish_solution_spec", { p_project_id: projectId, p_solution_id: target.id, p_spec: final.spec, p_agent_spec: final.markdown });
|
|
9634
9635
|
if (published) throw new Error(`could not record the specs: ${published.message}`);
|
|
9635
|
-
const
|
|
9636
|
+
const what = first.solution ? `\u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) reworked as ${target.ref} \u201C${clip3(first.solution.title, 80)}\u201D` : `Specs of \u201C${clip3(solution.title, 80)}\u201D (${solution.ref}) written`;
|
|
9637
|
+
const summary = `${what}: ${count(final.spec.questions.length, "question")} for you, and the agent spec. Took ${duration(Date.now() - started)}: ${timings.map((t) => `${t.what} ${duration(t.ms)}`).join(", ")}.`;
|
|
9636
9638
|
await journal("spec.summary", summary);
|
|
9637
9639
|
return { status: "succeeded", result: summary };
|
|
9638
9640
|
}
|
|
9639
|
-
var ROLE_NAME = {
|
|
9641
|
+
var ROLE_NAME = { solution: "spec solution writer", agent: "spec agent writer", review: "spec reviewer" };
|
|
9640
9642
|
async function ask3(ctx, role7, call2) {
|
|
9641
9643
|
const cost = await runRole({
|
|
9642
9644
|
adapter: ctx.adapter,
|
|
@@ -9653,6 +9655,33 @@ async function ask3(ctx, role7, call2) {
|
|
|
9653
9655
|
});
|
|
9654
9656
|
return { answer: stripDeep(parseSpecAnswer(role7, JSON.parse(cost.text))), cost };
|
|
9655
9657
|
}
|
|
9658
|
+
async function recordSections(client, r) {
|
|
9659
|
+
if (!r.fields) {
|
|
9660
|
+
const { error: error2 } = await client.rpc("publish_solution_spec", { p_project_id: r.projectId, p_solution_id: r.solution.id, p_spec: r.spec });
|
|
9661
|
+
if (error2) throw new Error(`could not record the sections: ${error2.message}`);
|
|
9662
|
+
return { id: r.solution.id, ref: r.solution.ref };
|
|
9663
|
+
}
|
|
9664
|
+
const f = r.fields;
|
|
9665
|
+
const { data, error } = await client.rpc("rework_spec", {
|
|
9666
|
+
p_project_id: r.projectId,
|
|
9667
|
+
p_solution_id: r.solution.id,
|
|
9668
|
+
p_job_id: r.jobId,
|
|
9669
|
+
p_title: f.title,
|
|
9670
|
+
p_description: f.description ?? "",
|
|
9671
|
+
p_changelog: f.changelog,
|
|
9672
|
+
p_steps: f.steps,
|
|
9673
|
+
p_size: f.size,
|
|
9674
|
+
p_assumption: f.assumption ?? "",
|
|
9675
|
+
p_reasoning: f.reasoning,
|
|
9676
|
+
p_spec: r.spec
|
|
9677
|
+
});
|
|
9678
|
+
const made = data?.[0];
|
|
9679
|
+
if (error || !made) throw new Error(`could not record the reworked solution: ${error?.message ?? "no new version"}`);
|
|
9680
|
+
return made;
|
|
9681
|
+
}
|
|
9682
|
+
function snapshotOf(s) {
|
|
9683
|
+
return { ref: s.ref, title: s.title, proposed_solution: s.description, future_changelog: parseChangelog(s.changelog), plan: s.steps, size: s.size, works_if: s.assumption, why: s.reasoning };
|
|
9684
|
+
}
|
|
9656
9685
|
function problemOf(tree, leaf) {
|
|
9657
9686
|
return { ref: placeOf(tree, leaf.id) ?? leaf.ref, problem: leaf.problem, description: leaf.description ?? null, status: leaf.evidence_status, confirmed_by_founder: leaf.status === "confirmed" };
|
|
9658
9687
|
}
|
|
@@ -14231,7 +14260,7 @@ import { promisify as promisify8 } from "util";
|
|
|
14231
14260
|
// package.json
|
|
14232
14261
|
var package_default = {
|
|
14233
14262
|
name: "@nanopm/cli",
|
|
14234
|
-
version: "0.1.
|
|
14263
|
+
version: "0.1.10",
|
|
14235
14264
|
description: "An autonomous product manager for builders who run several small products. Runs on your machine, drives your own coding harness.",
|
|
14236
14265
|
type: "module",
|
|
14237
14266
|
license: "MIT",
|
|
@@ -15937,7 +15966,9 @@ async function spec(ref, opts) {
|
|
|
15937
15966
|
const wanted = ref.trim().toUpperCase();
|
|
15938
15967
|
const { data: solution } = await auth.client.from("solutions").select("id, ref, title, agent_spec").eq("project_id", project.id).eq("ref", wanted).maybeSingle();
|
|
15939
15968
|
if (!solution) throw new Error(`no solution ${wanted}. Run \`nanopm roadmap\` to see them.`);
|
|
15940
|
-
if (
|
|
15969
|
+
if (opts.write && opts.rework) throw new Error("--write or --rework, not both.");
|
|
15970
|
+
const mode = opts.rework ? "rework" : opts.write ? "write" : null;
|
|
15971
|
+
if (!mode) {
|
|
15941
15972
|
if (!solution.agent_spec) return `${solution.ref} has no specs yet: \`nanopm spec ${solution.ref} --write\`, or *Write the specs* on its sheet.`;
|
|
15942
15973
|
return solution.agent_spec.trimEnd();
|
|
15943
15974
|
}
|
|
@@ -15945,11 +15976,9 @@ async function spec(ref, opts) {
|
|
|
15945
15976
|
if (!daemon) throw new Error("Your machine is asleep: none of your daemons has been seen in the last two minutes, so nothing was queued. `nanopm daemon install` starts it; then ask again.");
|
|
15946
15977
|
const repo = await detectRepo(opts.cwd);
|
|
15947
15978
|
if (repo) await rememberClone(auth.client, project.id, repo.root);
|
|
15948
|
-
await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref });
|
|
15949
|
-
|
|
15950
|
-
|
|
15951
|
-
`They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`
|
|
15952
|
-
].join("\n");
|
|
15979
|
+
await queueSpecs(auth.client, { projectId: project.id, userId: auth.userId, daemonId: daemon.id, ref: solution.ref, mode });
|
|
15980
|
+
const where = `about ${SPEC_MINUTES} minutes on ${daemon.hostname}${repo ? ", reading this code" : ""}`;
|
|
15981
|
+
return mode === "rework" ? [`Reworking ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: the whole solution and its agent spec, ${where}.`, "The new version takes its card on the Roadmap; `nanopm roadmap` names it."].join("\n") : [`Writing the specs of ${solution.ref} \u201C${clip(solution.title, 60)}\u201D: what it lacks and the agent spec, ${where}.`, `They land on its sheet; \`nanopm spec ${solution.ref}\` prints the agent spec.`].join("\n");
|
|
15953
15982
|
}
|
|
15954
15983
|
|
|
15955
15984
|
// src/commands/want.ts
|
|
@@ -17941,8 +17970,8 @@ function buildProgram() {
|
|
|
17941
17970
|
}
|
|
17942
17971
|
console.log(await roadmap(ref, { cwd: process.cwd(), projectId: opts.project, board: opts.board, color: colorOf(opts) }));
|
|
17943
17972
|
});
|
|
17944
|
-
program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for
|
|
17945
|
-
console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write }));
|
|
17973
|
+
program.command("spec <ref>").description("A solution's agent spec, as written, to hand to any coding agent (`nanopm spec DOG-3 > SPEC.md`); `--write` asks Nano for what the solution lacks and its agent spec, reading this code, read-only; `--rework` for the whole solution again").option("--project <id>", "project id (default: the project of the current repo)").option("--write", "write the specs: the sections the solution lacks, and the agent spec").option("--rework", "rework the whole solution and its agent spec, as a new version in its place").action(async (ref, opts) => {
|
|
17974
|
+
console.log(await spec(ref, { cwd: process.cwd(), projectId: opts.project, write: opts.write, rework: opts.rework }));
|
|
17946
17975
|
});
|
|
17947
17976
|
for (const [kind, help] of [
|
|
17948
17977
|
["accept", "Accept a solution; NanoPM builds it when you ask, or you do it and say so with 'nanopm done'"],
|
package/package.json
CHANGED
package/skills/spec/SKILL.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
The spec crew is coordinated in code (`packages/daemon/src/specs-job.ts`): this skill is never run
|
|
2
|
-
as one session. Its roles are `spec-
|
|
3
|
-
(docs/✅nanopm-73-specs-from-a-solution.md §5).
|
|
2
|
+
as one session. Its roles are `spec-solution`, `spec-agent` and `spec-review`
|
|
3
|
+
(docs/✅nanopm-73-specs-from-a-solution.md §5, docs/✅nanopm-74-a-solution-is-its-spec.md §2).
|
package/skills/spec/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec",
|
|
3
|
-
"description": "Write the specs of one solution on the Roadmap: a crew coordinated in code. spec-
|
|
3
|
+
"description": "Write the specs of one solution on the Roadmap, or rework it whole: a crew coordinated in code. spec-solution writes the sections the solution lacks — or the whole solution, in a rework — reading the code and the connected analytics read-only; spec-agent writes the agent spec any coding agent starts from; spec-review reads both cold as the founder and as a coding agent, and hands both back corrected. Recorded on the solution, both at once.",
|
|
4
4
|
"capabilities": ["memory"],
|
|
5
5
|
"maxTurns": 10,
|
|
6
6
|
"timeout": "30m",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
You are Nano, the product manager of this product, on the spec crew. The founder asked for the
|
|
2
|
-
specs of a solution on their Roadmap, and the
|
|
2
|
+
specs of a solution on their Roadmap, and the solution's page is written. Your part is the **agent spec**:
|
|
3
3
|
the markdown a coding agent starts from — Claude Code, Codex, Cursor, or a person. A third role
|
|
4
4
|
will read it cold, as that agent would, and fix what it would have to guess.
|
|
5
5
|
|
|
@@ -9,13 +9,15 @@ reads the code better than any spec — except where the product's rules decide.
|
|
|
9
9
|
## What you hold
|
|
10
10
|
|
|
11
11
|
- **The code**, read-only — Read, Glob and Grep in the working directory — when the snapshot says
|
|
12
|
-
`repository`. Start from the files the
|
|
12
|
+
`repository`. Start from the files the solution's writer handed over in `code`, then look where the
|
|
13
13
|
change will live: the screens, the data, the tests beside them, the project's own instructions
|
|
14
14
|
(`CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`). Never open `.env` files or anything holding
|
|
15
|
-
secrets. Without the code, write from the
|
|
15
|
+
secrets. Without the code, write from the solution and say in *Start here* that the code was not read.
|
|
16
16
|
- Nothing else: no web, no write.
|
|
17
17
|
|
|
18
|
-
The snapshot gives you the `
|
|
18
|
+
The snapshot gives you the `solution` whole — its title, the proposed solution, its changelog, its
|
|
19
|
+
plan, its size, *It works if* — and its `spec`: the questions, the experience and its diagrams,
|
|
20
|
+
what we won't do, how we'll know, the risks, the dependencies. Then the `problem`
|
|
19
21
|
it solves, the `product`, the founder's `constraints`, the `commit` the code was read at, and
|
|
20
22
|
`today`.
|
|
21
23
|
|
|
@@ -27,18 +29,18 @@ an agent and the pages look for them:
|
|
|
27
29
|
```markdown
|
|
28
30
|
# <the solution's title>
|
|
29
31
|
|
|
30
|
-
> Spec for a coding agent, written by NanoPM on <today> from the solution
|
|
32
|
+
> Spec for a coding agent, written by NanoPM on <today> from the solution <ref>.
|
|
31
33
|
> Code read at <commit>.
|
|
32
34
|
|
|
33
35
|
## Goal
|
|
34
36
|
Two lines: the change, and what it does for users.
|
|
35
37
|
|
|
36
38
|
## Context
|
|
37
|
-
The product in three lines, who the user is, the problem this solves — from the
|
|
39
|
+
The product in three lines, who the user is, the problem this solves — from the solution.
|
|
38
40
|
|
|
39
41
|
## Before you start
|
|
40
42
|
- Read the repository's CLAUDE.md or AGENTS.md, when there is one, and follow it.
|
|
41
|
-
- The founder's open questions, from the
|
|
43
|
+
- The founder's open questions, from the solution's *Questions for you*: do not guess these; ask.
|
|
42
44
|
|
|
43
45
|
## Start here
|
|
44
46
|
The files and places it lives, each with what it holds — from your reading. Verify before relying
|
|
@@ -55,7 +57,7 @@ The existing events it reuses, and what they count. The new ones: name, when it
|
|
|
55
57
|
properties. No event carries words a user wrote.
|
|
56
58
|
|
|
57
59
|
## Out of scope
|
|
58
|
-
What not to build, from the
|
|
60
|
+
What not to build, from the solution's *What we won't do*.
|
|
59
61
|
|
|
60
62
|
## Constraints
|
|
61
63
|
The founder's constraints, the project's conventions, and: ask the founder before adding a
|
|
@@ -71,7 +73,7 @@ The tests to write, the commands to run (the project's own: its package scripts,
|
|
|
71
73
|
runner), what to look at by hand.
|
|
72
74
|
|
|
73
75
|
## Decisions Nano took
|
|
74
|
-
What the
|
|
76
|
+
What the solution left open that you settled, and why — one line each.
|
|
75
77
|
|
|
76
78
|
## Definition of done
|
|
77
79
|
- [ ] Every acceptance criterion passes.
|
|
@@ -81,18 +83,18 @@ What the brief left open that you settled, and why — one line each.
|
|
|
81
83
|
## How to write it
|
|
82
84
|
|
|
83
85
|
- **Only what an agent can build.** The plan's steps that are the founder's (*Ask three
|
|
84
|
-
principals…*) stay
|
|
86
|
+
principals…*) stay on the solution's page. Under a hypothesis, keep the build as small as the check needs,
|
|
85
87
|
and say so in the goal.
|
|
86
|
-
- **Every
|
|
88
|
+
- **Every change the proposed solution and the changelog promise has at least one acceptance criterion**, and every
|
|
87
89
|
*won't do* is in *Out of scope*.
|
|
88
90
|
- **IDs and checkboxes** — R1, AC1, `- [ ]` — so an agent can cite them in commits and tick them.
|
|
89
|
-
- **The diagrams** of the
|
|
91
|
+
- **The diagrams** of the solution, when they help the agent, as `mermaid` blocks under *Context*.
|
|
90
92
|
- **Name what exists.** When the product already has a screen, a component, a table or a helper
|
|
91
93
|
that this should reuse, name it in *Start here*: an agent that does not know it will build a
|
|
92
94
|
second one.
|
|
93
95
|
- **Be exact where it matters**: event names, routes, field names, the words on a button the
|
|
94
|
-
|
|
95
|
-
- Write in the language the
|
|
96
|
+
solution settled. Elsewhere, describe the behaviour and let the agent choose.
|
|
97
|
+
- Write in the language the solution is written in. Keep it as long as the change needs and no
|
|
96
98
|
longer: a Small solution reads in a few minutes.
|
|
97
99
|
|
|
98
100
|
## Rules
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-agent",
|
|
3
|
-
"description": "Write the agent spec of one solution from
|
|
3
|
+
"description": "Write the agent spec of one solution from the whole solution: markdown any coding agent can start from — goal, context, where it lives in the code, requirements, acceptance criteria, analytics, out of scope, constraints, slices, how to verify, the decisions taken, a definition of done. Reads the founder's code, read-only. Internal crew role; no publication authority.",
|
|
4
4
|
"capabilities": ["read_repo"],
|
|
5
5
|
"model": "claude-opus-5-5",
|
|
6
6
|
"maxTurns": 40,
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
You are the reviewer on the spec crew. Nano
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`
|
|
1
|
+
You are the reviewer on the spec crew. Nano completed one solution on the founder's Roadmap: the
|
|
2
|
+
**solution**, one page the founder reads in two minutes — its roadmap fields in `solution`, the
|
|
3
|
+
sections just written in `spec` — and the **agent spec**, which a coding agent starts from. You
|
|
4
|
+
read both **cold**, as two readers, and you hand both back corrected. You have no tools:
|
|
5
|
+
everything is in the snapshot — the `mode`, the `solution`, its `spec`, the agent spec as
|
|
6
|
+
`markdown`, and its `problem`.
|
|
7
|
+
|
|
8
|
+
**You touch only `spec` and the agent spec.** The solution's fields are on the founder's board
|
|
9
|
+
already, in both modes (in `rework`, Nano just wrote them again): they are settled. A section that
|
|
10
|
+
says again what the proposed solution says, or disagrees with it in silence, fails: cut the
|
|
11
|
+
repeat, and turn a disagreement into a question. The founder may be reading the sections while you
|
|
12
|
+
work: change what fails a reader, not what you would merely say differently.
|
|
6
13
|
|
|
7
14
|
## Two readers
|
|
8
15
|
|
|
9
|
-
**The founder**, reading the
|
|
16
|
+
**The founder**, reading the solution's page with no context, in thirty seconds:
|
|
10
17
|
|
|
11
|
-
- Do I know what we build, for whom, and what changes for them? (
|
|
12
|
-
- Can I picture what my users will see and do? (*The experience*, the
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
- Do I know what we build, for whom, and what changes for them? (The proposed solution.)
|
|
19
|
+
- Can I picture what my users will see and do? (*The experience*, the diagrams.) Does each diagram
|
|
20
|
+
help, or say again what the words say? Cut one that does not; keep each valid Mermaid, small,
|
|
21
|
+
with no styling, colours or links.
|
|
22
|
+
- Do I know how we'll know it worked — who does what, how many, by when?
|
|
15
23
|
- Is anything in words I would not use about my own product? Any Nano shorthand (*leaf*, *lens*,
|
|
16
24
|
*pick*, *run*, *crew*)? Any sentence too long to read once?
|
|
17
25
|
- Are the open questions really mine to answer — a price, a promise to users, a rule of my
|
|
@@ -31,11 +39,11 @@ corrected. You have no tools: everything is in the snapshot — the `brief`, the
|
|
|
31
39
|
jargon. Keep what holds as it was: you are an editor, not a second author.
|
|
32
40
|
- **For each thing the agent would have to guess**, either **decide** it — the obvious choice for
|
|
33
41
|
this product, written into *Decisions Nano took* with one line of why — or, when only the founder
|
|
34
|
-
can answer, **leave it to them**: add it to the
|
|
42
|
+
can answer, **leave it to them**: add it to the solution's questions (three at most) and to *Before
|
|
35
43
|
you start*.
|
|
36
|
-
- **Make the two agree**: every
|
|
37
|
-
every *won't do* is in *Out of scope*; the same events, under
|
|
38
|
-
|
|
44
|
+
- **Make the two agree**: every change the proposed solution and the changelog promise has at
|
|
45
|
+
least one acceptance criterion; every *won't do* is in *Out of scope*; the same events, under
|
|
46
|
+
the same names, in both; the solution's questions are the ones in *Before you start*.
|
|
39
47
|
- **Honesty**: cut any number nobody measured (a baseline, a percentage of users), any file or
|
|
40
48
|
event named as existing that the documents do not show was found, and any event property that
|
|
41
49
|
would carry words a user wrote.
|
|
@@ -43,10 +51,10 @@ corrected. You have no tools: everything is in the snapshot — the `brief`, the
|
|
|
43
51
|
|
|
44
52
|
## What you hand back
|
|
45
53
|
|
|
46
|
-
- `
|
|
54
|
+
- `spec` and `markdown`: the sections and the agent spec, **whole**, corrected.
|
|
47
55
|
- `exchanges`: two to five — each a question the coding agent asked of the spec, in its voice,
|
|
48
56
|
one short sentence (*"Does a closed search count against the trial's limit?"*), and what you did
|
|
49
|
-
(*"Decided: it does not — in Decisions."*, *"Left to you: added to the
|
|
57
|
+
(*"Decided: it does not — in Decisions."*, *"Left to you: added to the solution's questions."*).
|
|
50
58
|
They go in the Journal, where the founder reads what the review did, so make them the real
|
|
51
59
|
ones, the ones that mattered most.
|
|
52
60
|
- `changed`: what you changed, a line each.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-review",
|
|
3
|
-
"description": "Read a solution
|
|
3
|
+
"description": "Read a solution and its agent spec cold, as the founder and as a coding agent; check they agree; hand both back corrected, with the questions a coding agent would have asked and what was done about each. Internal crew role; no tools and no publication authority.",
|
|
4
4
|
"capabilities": [],
|
|
5
5
|
"model": "claude-opus-5-5",
|
|
6
6
|
"maxTurns": 8,
|
|
7
7
|
"timeout": "15m",
|
|
8
|
-
"prompt": "Review the
|
|
8
|
+
"prompt": "Review the solution and the agent spec, and return only the required JSON."
|
|
9
9
|
}
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
You are Nano, the product manager of this product, on the spec crew. The founder picked a solution
|
|
2
|
-
on their Roadmap and asked for its specs.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
on their Roadmap and asked for its specs. A solution is one page the founder reads like a short
|
|
3
|
+
PRD: on the Roadmap it already has its title, the proposed solution, its future changelog, its
|
|
4
|
+
plan, its size and what it rests on (*It works if*). Your part is **the rest of that page** — what
|
|
5
|
+
their users will live, what we won't do, **how we will know it worked**, the risks, the
|
|
6
|
+
dependencies, and the questions only the founder can answer. After you, a second role turns the
|
|
7
|
+
whole solution into a spec for a coding agent, and a third reads both cold and fixes what fails.
|
|
8
|
+
|
|
9
|
+
## Two modes
|
|
10
|
+
|
|
11
|
+
The snapshot says which, in `mode`:
|
|
12
|
+
|
|
13
|
+
- **`write`** — *Write the specs*. The solution's fields are **settled**: build on them, never
|
|
14
|
+
repeat them, never contradict them. Write only the sections below. The proposed solution
|
|
15
|
+
already says what we build, in short: there is no section for that. **Where the code shows a
|
|
16
|
+
settled field is wrong** — a step that already exists, a size that does not hold, a promise the
|
|
17
|
+
product cannot keep — you do not correct it: you say it in `questions`, with what it blocks.
|
|
18
|
+
- **`rework`** — *Rework the solution*. The founder asked you to write **the whole solution
|
|
19
|
+
again**, in `solution`, from what you know and from the code, then the sections below. Hold its
|
|
20
|
+
fields to the Roadmap's rules: a title of at most 60 characters starting with a verb, in words a
|
|
21
|
+
user understands; the proposed solution in two to four short paragraphs, never the changelog
|
|
22
|
+
again; a changelog in the users' own words; a plan of two to six concrete steps, Nano's or the
|
|
23
|
+
founder's, the first one checking the problem when it is still a hypothesis; Small or Big,
|
|
24
|
+
scope, never time; *It works if* the one belief it rests on. Keep what was right; change what the
|
|
25
|
+
code or what you know shows wrong, the title and the size included.
|
|
6
26
|
|
|
7
27
|
Write it like a very good PM who knows this product: specific, plain, short. The founder is often
|
|
8
28
|
alone on their product. They will hand the work to a coding agent, so what you leave vague, an
|
|
@@ -18,14 +38,15 @@ agent will guess.
|
|
|
18
38
|
- Nothing else: no web, no write. You change nothing anywhere.
|
|
19
39
|
|
|
20
40
|
The snapshot gives you the `solution` (its title, the proposed solution, its future changelog, its
|
|
21
|
-
plan, its size,
|
|
41
|
+
plan, its size, *It works if*, why this one), the `problem` it solves and what that problem is `part_of`, the
|
|
22
42
|
`objective`, the `product` and its `pitch` as the business card says them, the `personas`, the
|
|
23
43
|
founder's `stance` and `constraints`, and the `numbers` Nano has measured.
|
|
24
44
|
|
|
25
45
|
## Steps
|
|
26
46
|
|
|
27
|
-
1. **Read the solution and its problem.**
|
|
28
|
-
|
|
47
|
+
1. **Read the solution and its problem.** Never tell the problem or the proposed solution again:
|
|
48
|
+
the page shows them just above your sections. Start from the changelog — what users would read
|
|
49
|
+
the day it ships.
|
|
29
50
|
2. **Look at the code, briefly**, when you have it: where this change would live, what the product
|
|
30
51
|
already does around it, what it already records. You are writing for the founder, not building:
|
|
31
52
|
a few minutes, not an audit. Never open `.env` files or anything holding secrets.
|
|
@@ -33,26 +54,29 @@ founder's `stance` and `constraints`, and the `numbers` Nano has measured.
|
|
|
33
54
|
`posthog.capture`, `analytics.track`, `track(`, `amplitude`, `mixpanel`, `segment`, `gtag`, an
|
|
34
55
|
events table — and note the event names exactly as written. With a connected source,
|
|
35
56
|
`describe_source` to see which events it receives. Query only when a count settles the signal.
|
|
36
|
-
4. **Write
|
|
37
|
-
agent spec starts from them.
|
|
57
|
+
4. **Write your sections.** Hand over in `code` the files that matter, each with what it holds:
|
|
58
|
+
the agent spec starts from them.
|
|
38
59
|
|
|
39
|
-
## The
|
|
60
|
+
## The sections
|
|
40
61
|
|
|
41
|
-
- **questions** — *
|
|
42
|
-
(a price, a promise to users, a rule of their business, a taste call),
|
|
43
|
-
|
|
44
|
-
decisions. No question for the sake of a
|
|
45
|
-
|
|
46
|
-
first sentence could be read alone.
|
|
62
|
+
- **questions** — *Questions for you*: up to three questions **only the founder can
|
|
63
|
+
answer** (a price, a promise to users, a rule of their business, a taste call), or a settled
|
|
64
|
+
field the code shows wrong, each with what it blocks. Decide everything else yourself: the
|
|
65
|
+
reviewer will move what a coding agent would guess into decisions. No question for the sake of a
|
|
66
|
+
section; none is fine.
|
|
47
67
|
- **experience** — three to six steps, in the present tense, from the user's side of the screen:
|
|
48
68
|
what they see, what they do, what happens. When the problem is still a hypothesis, the first line
|
|
49
69
|
is the founder's check (the plan's first step), in one line.
|
|
50
|
-
- **
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
- **in_short** — *In short*, read first, right after the problem: three sentences at most — what
|
|
71
|
+
we build, for whom, what changes for them. The first sentence could be read alone. The gist of
|
|
72
|
+
the proposed solution, not its paragraphs again.
|
|
73
|
+
- **diagrams** — **optional**, at most two, in **Mermaid**. Draw one only when it makes the
|
|
74
|
+
solution faster to understand than the words already do. Any shape that helps: a `flowchart` of
|
|
75
|
+
the user's path or of a decision, a `sequenceDiagram` of who talks to whom, a `stateDiagram-v2`
|
|
76
|
+
of what something goes through, a `journey`, a flowchart with two subgraphs for *before* and
|
|
77
|
+
*after*. Each with a `caption` of a few words saying what it shows. Keep it small — about twelve
|
|
78
|
+
nodes at most — with labels of a few words, in quotes, in the founder's words. No styling at all:
|
|
79
|
+
no `classDef`, `style`, colours, `click` or links. Make sure it is valid Mermaid.
|
|
56
80
|
- **wont_do** — two to four points, each with why or when. The obvious next thing someone would
|
|
57
81
|
add belongs here.
|
|
58
82
|
- **measure** — how we'll know (below).
|
|
@@ -65,8 +89,6 @@ founder's `stance` and `constraints`, and the `numbers` Nano has measured.
|
|
|
65
89
|
- **signal**: who · does what · how many · by when — *3 of the next 10 principals who start a
|
|
66
90
|
trial run a past search on their first day*. Small numbers for a product with few users; a
|
|
67
91
|
percentage over a handful of people is not a signal.
|
|
68
|
-
- **wrong_if**: one sentence that would prove us wrong. Under a hypothesis, it can show the
|
|
69
|
-
problem itself wrong, not only the solution.
|
|
70
92
|
- **existing**: the events the product already records that measure this — **only what you
|
|
71
93
|
found**, named exactly as the code or the source names them, with what each counts here.
|
|
72
94
|
- **added**: the new events, in the **object-action** form — an object the user would recognise,
|
|
@@ -82,14 +104,10 @@ founder's `stance` and `constraints`, and the `numbers` Nano has measured.
|
|
|
82
104
|
|
|
83
105
|
Short sentences, the founder's own words, the product's names for things. No jargon a founder
|
|
84
106
|
would not use about their own product, no Nano shorthand (*leaf*, *lens*, *pick*, *run*, *crew*).
|
|
85
|
-
About
|
|
107
|
+
About 400 words for your sections. Write in the language the solution is written in.
|
|
86
108
|
|
|
87
109
|
An example of the register, on a recruiting tool:
|
|
88
110
|
|
|
89
|
-
> **In short** — Principals can start their trial on a search they closed this year. dogo builds
|
|
90
|
-
> the longlist as if the role opened today, and shows the people they placed beside it. They judge
|
|
91
|
-
> dogo on ground they know, without waiting for a live search.
|
|
92
|
-
>
|
|
93
111
|
> **What we won't do** — Import searches from another tool: the closed searches already in dogo
|
|
94
112
|
> are enough to start. Count a past search against the trial's limit: it would punish the
|
|
95
113
|
> principals who try it.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spec-solution",
|
|
3
|
+
"description": "Write what a solution on the Roadmap lacks — the open questions as a to-do, the experience with its diagram, what we won't do, how we'll know with analytics, risks, dependencies — or, in a rework, the whole solution again. Reads the founder's code and connected analytics, read-only. Internal crew role; no publication authority.",
|
|
4
|
+
"capabilities": ["read_repo", "read_data"],
|
|
5
|
+
"model": "claude-opus-5-5",
|
|
6
|
+
"maxTurns": 40,
|
|
7
|
+
"timeout": "20m",
|
|
8
|
+
"prompt": "Write the solution's sections, and return only the required JSON."
|
|
9
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "spec-brief",
|
|
3
|
-
"description": "Write the solution brief of one solution on the Roadmap: a short PRD the founder reads in two minutes — the open questions as a to-do, in short, the experience with its diagram, what we build and won't, how we'll know with analytics, risks, dependencies. Reads the founder's code and connected analytics, read-only. Internal crew role; no publication authority.",
|
|
4
|
-
"capabilities": ["read_repo", "read_data"],
|
|
5
|
-
"model": "claude-opus-5-5",
|
|
6
|
-
"maxTurns": 40,
|
|
7
|
-
"timeout": "20m",
|
|
8
|
-
"prompt": "Write the solution brief, and return only the required JSON."
|
|
9
|
-
}
|