create-venture 0.1.1 → 0.3.0

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.
Files changed (2) hide show
  1. package/index.mjs +209 -30
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -1,15 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // create-venture: Naive's generator, the way create-next-app is Next.js's (I-1, I-3, I-7). It asks for a
3
- // name, offers the recommended defaults or lets you customize them, creates the venture in a new or
4
- // empty directory, installs the framework with your package manager, and initializes git.
2
+ // create-venture: Naive's generator, the way create-next-app is Next.js's (I-1, I-3, I-7, EE-082). Before
3
+ // anything is scaffolded it asks what you are building (described in a few words and classified by a
4
+ // model, or picked from the list) and what role you hold, then who else is founding it, the phase, and
5
+ // which functions (hats) each person owns. The venture type decides the packages and first decisions:
6
+ // a VC fund starts with a thesis and terms, a software startup with its product questions.
5
7
  //
6
- // npx create-venture [dir] [--yes] [--phase <phase>] [--layers a,b] [--legal-form <form>] [--dump <file>]
8
+ // npx create-venture [dir] [--yes] [--type <type> | --describe "<a few words>"] [--role <role>]
9
+ // [--name "<your name>"] [--team "<name>, <name>"] [--phase <phase>]
10
+ // [--source <function>=<system>[:<address>],... ...] [--rho-fixture]
11
+ // [--layers a,b] [--legal-form <form>] [--dump <file>]
7
12
  // [--use-npm | --use-pnpm | --use-yarn | --use-bun] [--skip-install] [--no-git]
8
- // [--framework <spec>] [--reset-preferences]
13
+ // [--framework <spec>] [--reset-preferences] [--venture-name "<name>"] [--no-workspace]
9
14
  //
10
- // --yes takes the recommended defaults: the discovery phase, the phase's packages, checks on, and
11
- // AGENTS.md. Answers to the setup questions are kept
12
- // in ~/.config/naive/create-venture.json, so the next venture can reuse them.
15
+ // --source says where a function keeps its truth (schema/systems-catalog.json), recorded as pointers in
16
+ // knowledge/sources.json: --source products=local-repo:../app,github:acme/app,vercel:acme/app
17
+ // --source finance=rho. Rho is synced at setup: its sandbox, the founder's own with RHO_API_TOKEN, or a
18
+ // saved answer with --rho-fixture (offline; recorded as a fixture).
19
+ //
20
+ // --yes takes the recommended defaults: a software startup, you as its solo founder wearing every hat,
21
+ // the discovery phase, the type's packages, checks on, and AGENTS.md. --describe classifies only when
22
+ // TYPESAFE_API_KEY is set; otherwise pick with --type. Answers are kept in
23
+ // ~/.config/naive/create-venture.json, so the next venture can reuse them.
13
24
 
14
25
  import { spawnSync } from "node:child_process";
15
26
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -30,11 +41,13 @@ const fw = JSON.parse(readFileSync(join(framework, "package.json"), "utf8"));
30
41
  const { layers: PHASE_LAYERS } = JSON.parse(readFileSync(join(framework, "schema/scaffold/defaults.json"), "utf8"));
31
42
  const PHASES = Object.keys(PHASE_LAYERS);
32
43
  const LAYERS = ["purpose", "knowledge", "operations", "evidence", "governance", "interface"];
44
+ // The legal forms venture.json accepts, from its schema, so the question can only offer valid answers.
45
+ const FORMS = JSON.parse(readFileSync(join(framework, "schema/venture.schema.json"), "utf8")).properties.form.properties.legal_form.enum;
33
46
 
34
47
  const argv = process.argv.slice(2);
35
48
  const opt = (n) => { const i = argv.indexOf(`--${n}`); return i >= 0 ? argv[i + 1] : undefined; };
36
49
  const flag = (n) => argv.includes(`--${n}`);
37
- const valued = new Set(["phase", "layers", "legal-form", "dump", "framework"]);
50
+ const valued = new Set(["phase", "layers", "legal-form", "dump", "framework", "type", "describe", "role", "name", "team", "layout", "venture-name"]);
38
51
  const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && valued.has(argv[i - 1].slice(2))));
39
52
  const bold = (s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[22m` : s);
40
53
  const cyan = (s) => (process.stdout.isTTY ? `\x1b[36m${s}\x1b[39m` : s);
@@ -62,31 +75,137 @@ const choose = async (q, options, fallback) => {
62
75
  return a;
63
76
  };
64
77
 
65
- // 1. The name and the directory.
78
+ // 1. The name and the directory. The defaults are "My first venture" in my-first-venture/, and either can
79
+ // be renamed later; a directory given on the command line names the venture unless --venture-name does.
80
+ const FIRST = "My first venture";
81
+ const WORKSPACE_FILE = "naive-workspace.json";
66
82
  let target = positional[0];
83
+ let ventureName = opt("venture-name");
67
84
  if (!target) {
68
- if (!interactive) fail("usage: create-venture <dir> [--yes]: name the directory for the new venture");
69
- target = await ask("What is your venture named?", "my-venture");
85
+ ventureName ??= interactive ? await ask("What is your venture named?", FIRST) : FIRST;
86
+ target = slug(ventureName);
70
87
  }
71
88
  const dir = resolve(target);
89
+ ventureName ??= basename(dir);
72
90
  const id = slug(basename(dir));
73
91
  if (!id) fail(`${basename(dir)} has no letters or digits to make a venture id from`);
74
92
  if (existsSync(dir) && readdirSync(dir).length) fail(`${dir} already exists and is not empty. create-venture only creates new ventures; to bring an existing project in, create one here and run naive port or naive migrate.`);
75
93
 
76
- // 2. Defaults, the last venture's answers, or your own (I-3, I-7).
94
+ // The framework's registries: venture types, roles and functions (EE-082).
95
+ const { types: TYPES, roles: ROLES, classify, canClassify } = await import(join(framework, "schema/venture-type.mjs")).then((m) => ({ types: m.types(), roles: m.roles(), classify: m.classify, canClassify: m.canClassify }));
96
+ const DEPARTMENTS = JSON.parse(readFileSync(join(framework, "operations/departments.json"), "utf8")).departments;
97
+ const deptLabel = (d) => DEPARTMENTS.find((x) => x.id === d)?.label ?? d;
98
+ const roleOf = (r) => ROLES.find((x) => x.id === r);
99
+ const gitName = () => spawnSync("git", ["config", "user.name"], { encoding: "utf8" }).stdout?.trim() || null;
100
+
101
+ // 2. What you are building: in a few words for the model, or from the list. Nothing is scaffolded until it is set.
102
+ let vtype = opt("type") ? TYPES.find((t) => t.id === opt("type")) : null;
103
+ if (opt("type") && !vtype) fail(`unknown venture type ${opt("type")}: one of ${TYPES.map((t) => t.id).join(", ")}`);
104
+ const pickFromList = async () => {
105
+ TYPES.forEach((t, i) => console.log(` ${String(i + 1).padStart(2)}. ${t.label}: ${t.description}`));
106
+ const a = await ask("Which kind of venture is it? (number or id)", "1");
107
+ const t = TYPES[Number(a) - 1] ?? TYPES.find((x) => x.id === a);
108
+ if (!t) { console.log(" choose a number from the list"); if (piped && !piped.length) fail("no answer left to choose with"); return pickFromList(); }
109
+ return t;
110
+ };
111
+ const classified = async (words) => {
112
+ if (!canClassify()) { console.log(" (no TYPESAFE_API_KEY, so pick from the list)"); return null; }
113
+ try {
114
+ const c = classify(words);
115
+ if (c.uncertain) { console.log(` not sure what kind of venture that is (best guess ${c.ranked[0].label}, ${Math.round(c.ranked[0].p * 100)}%), so pick from the list`); return null; }
116
+ return TYPES.find((t) => t.id === c.best.id);
117
+ } catch (e) { console.log(` ${e.message}; pick from the list`); return null; }
118
+ };
119
+ if (!vtype && opt("describe")) {
120
+ vtype = await classified(opt("describe"));
121
+ if (!vtype && !interactive) fail(`could not classify "${opt("describe")}": pass --type, one of ${TYPES.map((t) => t.id).join(", ")}`);
122
+ if (vtype) console.log(` ${opt("describe")}: a ${vtype.label.toLowerCase()}`);
123
+ }
124
+ if (!vtype && interactive) {
125
+ const words = await ask("What are you building? Describe it in a few words, or press Enter to pick from the list", "");
126
+ if (words) {
127
+ const t = await classified(words);
128
+ if (t && (await choose(`That sounds like a ${t.label.toLowerCase()}. Is that right?`, ["yes", "no"], "yes")) === "yes") vtype = t;
129
+ }
130
+ if (!vtype) vtype = await pickFromList();
131
+ }
132
+ // --yes with no type: the recommended default, a software startup.
133
+ if (!vtype) vtype = TYPES.find((t) => t.id === "software-startup");
134
+
135
+ // 3. Your role in it, from the roles this type has. Investors are a role too: a general partner runs a fund.
136
+ let role = opt("role") ?? (interactive ? await choose("What is your role in it?", vtype.roles, vtype.default_role) : vtype.default_role);
137
+ if (!vtype.roles.includes(role)) fail(`a ${vtype.label.toLowerCase()} has no ${role} role: one of ${vtype.roles.join(", ")}`);
138
+
139
+ // 4. You, and whether you are building it solo or with a team.
140
+ const me = opt("name") ?? (interactive ? await ask("What is your name?", gitName() ?? "Founder") : gitName() ?? "Founder");
141
+ let others = opt("team") ? opt("team").split(",").map((x) => x.trim()).filter(Boolean) : [];
142
+ if (!opt("team") && interactive && (await choose("Are you building it solo or with a team?", ["solo", "team"], prefs?.mode ?? "solo")) === "team") {
143
+ others = (await ask("Who else is founding it? (names, comma separated)", "")).split(",").map((x) => x.trim()).filter(Boolean);
144
+ if (!others.length) console.log(" no other names, so you are building it solo");
145
+ }
146
+ const mode = others.length ? "team" : "solo";
147
+ const coRole = vtype.id === "vc-fund" ? "general-partner" : "co-founder";
148
+ const people = [{ id: slug(me) || "founder", name: me, role }, ...others.map((n) => ({ id: slug(n), name: n, role: coRole }))];
149
+ if (new Set(people.map((p) => p.id)).size !== people.length) fail("two people would have the same id; give each a distinct name");
150
+
151
+ // 5. The phase, the hats, and the rest of the defaults (I-3, I-7).
77
152
  const recommended = { phase: "discovery", layers: null, legalForm: null, dump: null };
78
153
  let answers = { ...recommended };
79
154
  const given = opt("phase") || opt("layers") || opt("legal-form") || opt("dump");
80
155
  if (given) answers = { phase: opt("phase") ?? "discovery", layers: opt("layers") ?? null, legalForm: opt("legal-form") ?? null, dump: opt("dump") ?? null };
81
- else if (interactive) {
156
+ else if (interactive) answers.phase = await choose("Which phase is the venture in?", PHASES, prefs?.phase ?? "discovery");
157
+ // The layout (EE-086): structured (knowledge/, meta/, the org chart in venture.json) unless --layout flat.
158
+ const layout = opt("layout") ?? "structured";
159
+ if (!["flat", "structured"].includes(layout)) fail(`unknown layout ${layout}: structured or flat`);
160
+ const structured = layout === "structured";
161
+ // A solo founder owns every function; on a team, each function is asked once. In the structured layout the
162
+ // functions are the org chart's (one per package, flat) and ownership goes in venture.json; in the flat one,
163
+ // the catalog's departments, worn as hats in people/.
164
+ const fnIds = structured ? vtype.packages : vtype.departments;
165
+ const fnLabel = (f) => (structured ? f : deptLabel(f));
166
+ const owners = people.filter((p) => roleOf(p.role)?.owns_when_solo);
167
+ const ownerOf = {};
168
+ for (const p of people) p.hats = [];
169
+ if (owners.length === 1 || !interactive) for (const f of fnIds) { ownerOf[f] = owners[0]?.id ?? null; owners[0]?.hats.push(f); }
170
+ else for (const f of fnIds) {
171
+ const who = await choose(`Who owns ${fnLabel(f)}?`, owners.map((p) => p.id), owners[0].id);
172
+ ownerOf[f] = who;
173
+ owners.find((p) => p.id === who).hats.push(f);
174
+ }
175
+ // 6. Where each function keeps its truth (EE-083): pointers to its systems of record, in the founder's words when none fits.
176
+ const CATALOG = JSON.parse(readFileSync(join(framework, "schema/systems-catalog.json"), "utf8"));
177
+ const asked = [...vtype.departments.filter((d) => CATALOG.functions[d]), "legal"];
178
+ const parseSystems = (text) => {
179
+ const items = text.split(",").map((x) => x.trim()).filter(Boolean);
180
+ const systems = items.map((x) => { const i = x.indexOf(":"); const sys = i < 0 ? x : x.slice(0, i); return CATALOG.systems[sys] ? { system: sys, ...(i < 0 ? {} : { address: x.slice(i + 1) }) } : null; });
181
+ return systems.every(Boolean) ? { systems } : { systems: [], note: text.trim() };
182
+ };
183
+ const functions = {};
184
+ const sourceFlags = argv.flatMap((a, i) => (a === "--source" ? [argv[i + 1]] : []));
185
+ for (const f of sourceFlags) {
186
+ const [fn, ...rest] = f.split("=");
187
+ if (!CATALOG.functions[fn]) fail(`--source ${f}: ${fn} is not a function systems are kept for (${Object.keys(CATALOG.functions).join(", ")})`);
188
+ functions[fn] = parseSystems(rest.join("="));
189
+ for (const s of functions[fn].systems) if (!CATALOG.functions[fn].includes(s.system)) fail(`--source ${f}: ${s.system} is not offered for ${fn} (${CATALOG.functions[fn].join(", ")})`);
190
+ }
191
+ if (interactive && !sourceFlags.length) {
192
+ console.log("\nNow point Naive at where each function keeps its truth. Name systems (comma separated, add :address if you know it), type none, or say it in your own words.");
193
+ for (const fn of asked) {
194
+ const options = CATALOG.functions[fn];
195
+ const a = await ask(`Where is the source of truth for ${fn === "legal" ? "legal documents" : deptLabel(fn)}? ${options.join(" / ")} / none`, "none");
196
+ if (a.toLowerCase() === "none") continue;
197
+ functions[fn] = parseSystems(a);
198
+ }
199
+ }
200
+
201
+ if (interactive && !given) {
82
202
  const modes = prefs ? ["recommended", "previous", "customize"] : ["recommended", "customize"];
83
- const mode = await choose("Would you like to use the recommended Naive defaults?", modes, "recommended");
84
- if (mode === "previous") answers = { ...recommended, ...prefs };
85
- if (mode === "customize") {
86
- answers.phase = await choose("Which phase is the venture in?", PHASES, "discovery");
87
- const l = await ask(`Which layers should be on? (comma separated, from ${LAYERS.join(", ")}; empty for the phase's defaults: ${PHASE_LAYERS[answers.phase].join(", ")})`, "");
203
+ const pick = await choose("Would you like to use the recommended Naive defaults for the rest?", modes, "recommended");
204
+ if (pick === "previous") answers = { ...answers, layers: prefs.layers ?? null, legalForm: prefs.legalForm ?? null };
205
+ if (pick === "customize") {
206
+ const l = await ask(`Which layers should be on? (comma separated, from ${LAYERS.join(", ")}; empty for the defaults)`, "");
88
207
  answers.layers = l || null;
89
- const f = await choose("Is the venture a legal entity yet?", ["none", "llc", "corporation", "partnership", "nonprofit"], "none");
208
+ const f = await choose("Is the venture a legal entity yet?", FORMS, "none");
90
209
  answers.legalForm = f === "none" ? null : f;
91
210
  const d = await ask("Paste the path of an idea dump to start from (optional)", "");
92
211
  answers.dump = d || null;
@@ -94,6 +213,7 @@ else if (interactive) {
94
213
  }
95
214
  if (!PHASES.includes(answers.phase)) fail(`unknown phase ${answers.phase}: one of ${PHASES.join(", ")}`);
96
215
  if (answers.dump && !existsSync(resolve(answers.dump))) fail(`no idea dump at ${answers.dump}`);
216
+ if (answers.legalForm && !FORMS.includes(answers.legalForm)) fail(`unknown legal form ${answers.legalForm}: one of ${FORMS.join(", ")}`);
97
217
 
98
218
  // 3. The package manager: the flag, else whichever ran this (npm_config_user_agent), else npm.
99
219
  const pm = ["npm", "pnpm", "yarn", "bun"].find((p) => flag(`use-${p}`)) ?? (process.env.npm_config_user_agent ?? "").split("/")[0].replace(/^npx$/, "npm") ?? "npm";
@@ -101,20 +221,59 @@ const manager = ["npm", "pnpm", "yarn", "bun"].includes(pm) ? pm : "npm";
101
221
  rl?.close();
102
222
 
103
223
  // 4. The venture itself, by the framework's own scaffold.
104
- console.log(`\nCreating a new Naive venture in ${bold(dir)}.\n`);
105
- const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase,
224
+ console.log(`\nCreating a new ${vtype.label.toLowerCase()} in ${bold(dir)}, with ${mode === "solo" ? `${me} as its ${role}, wearing every hat` : `${people.length} founders`}.\n`);
225
+ const init = spawnSync("node", [join(framework, "schema/init-venture.mjs"), dir, id, "--phase", answers.phase, "--type", vtype.id, "--layout", layout,
106
226
  ...(answers.layers ? ["--layers", answers.layers] : []), ...(answers.legalForm ? ["--legal-form", answers.legalForm] : [])], { encoding: "utf8" });
107
227
  if (init.status !== 0) fail((init.stderr || init.stdout).trim());
108
228
  const vfile = join(dir, "venture.json");
109
229
  const venture = JSON.parse(readFileSync(vfile, "utf8"));
110
- venture.name = basename(dir);
230
+ venture.name = ventureName;
231
+ if (mode === "team") venture.members = people.slice(1).map((p) => ({ role: p.role === "general-partner" ? "partner" : p.role, kind: "person" }));
232
+ // The org chart (EE-086): each function's owner. Hats are read from here, never stored twice.
233
+ if (structured) for (const f of Object.keys(venture.functions ?? {})) venture.functions[f].owner = ownerOf[f] ?? null;
111
234
  writeFileSync(vfile, JSON.stringify(venture, null, 2) + "\n");
235
+ // Where each logical file sits in this venture's layout (schema/venture-paths.mjs).
236
+ const { ventureFile: vf, physical } = await import(join(framework, "schema/venture-paths.mjs"));
112
237
 
238
+ // people/: the roster and one file per founder, the venture.json of human capital (EE-082).
113
239
  const { agentFromEnv, record } = await import(join(framework, "interface/provenance.mjs"));
240
+ const today = new Date().toISOString().slice(0, 10);
241
+ mkdirSync(vf(dir, "people/founders"), { recursive: true });
242
+ const asPerson = agentFromEnv("person", "cli", { id: people[0].id });
243
+ for (const p of people.filter((x) => roleOf(x.role)?.founder)) {
244
+ p.founder_file = `people/founders/${p.id}.json`;
245
+ writeFileSync(vf(dir, p.founder_file), JSON.stringify({ id: p.id, name: p.name, role: p.role, ...(structured ? {} : { hats: p.hats }), joined: today }, null, 2) + "\n");
246
+ record(dir, { entity: p.founder_file, route: `people/founders/${p.id}`, activity: "write", agent: asPerson, note: `${p.name}, ${p.role}, as answered at create-venture` });
247
+ }
248
+ writeFileSync(vf(dir, "people/people.json"), JSON.stringify({ venture: id, mode, people: people.map(({ id: pid, name, role: r, hats, founder_file }) => ({ id: pid, name, role: r, ...(structured ? {} : { hats }), ...(founder_file ? { founder_file } : {}) })), updated: today }, null, 2) + "\n");
249
+ record(dir, { entity: "people/people.json", route: "people", activity: "write", agent: asPerson, note: `${mode}: ${people.map((p) => `${p.name} (${p.role})`).join(", ")}` });
114
250
  if (answers.dump) {
115
- mkdirSync(join(dir, "raw"), { recursive: true });
116
- copyFileSync(resolve(answers.dump), join(dir, "raw/dump.md"));
117
- record(dir, { entity: "raw/dump.md", route: "raw/dump", activity: "create", agent: agentFromEnv("person", "create-venture"), note: `the founder's idea dump, verbatim, from ${resolve(answers.dump)}` });
251
+ mkdirSync(vf(dir, "raw"), { recursive: true });
252
+ copyFileSync(resolve(answers.dump), vf(dir, "raw/dump.md"));
253
+ record(dir, { entity: "raw/dump.md", route: "raw/dump", activity: "write", agent: agentFromEnv("person", "cli"), note: `the founder's idea dump, verbatim, from ${resolve(answers.dump)}` });
254
+ }
255
+
256
+ // knowledge/sources.json: each function's systems of record, as pointers (EE-083). Rho is read by code, so it is synced now.
257
+ const rhoScript = join(framework, "platform/integrations/rho.mjs");
258
+ let rhoNote = null;
259
+ for (const f of Object.values(functions)) for (const s of f.systems) {
260
+ s.status = "pointed";
261
+ if (s.system !== "rho") continue;
262
+ if (!existsSync(rhoScript)) { rhoNote = "Rho: recorded as pointed; this copy of Naive has no Rho integration"; continue; }
263
+ const fixture = flag("rho-fixture") ? join(framework, "platform/integrations/fixtures/rho-sandbox.json") : null;
264
+ const live = !fixture && process.env.RHO_API_TOKEN;
265
+ const r = spawnSync("node", [rhoScript, "sync", "--venture", dir, ...(fixture ? ["--fixture", fixture] : live ? ["--live"] : [])], { encoding: "utf8", env: process.env });
266
+ if (r.status === 0) {
267
+ Object.assign(s, { status: "connected", synced: new Date().toISOString().replace(/\.\d+Z$/, "Z"), ...(fixture ? { fixture: true } : {}), address: s.address ?? (fixture ? "saved sandbox answer" : live ? "live" : "sandbox") });
268
+ rhoNote = `Rho: synced ${fixture ? "from a saved sandbox answer (a fixture, not a live account)" : live ? "from your Rho account" : "from Rho's sandbox (a fictional company)"} into money/`;
269
+ } else rhoNote = `Rho: could not sync (${(r.stderr || r.stdout).trim().split("\n").at(-1)}); recorded as pointed`;
270
+ }
271
+ if (Object.keys(functions).length) {
272
+ const map = vf(dir, "knowledge/sources.json");
273
+ mkdirSync(dirname(map), { recursive: true });
274
+ writeFileSync(map, JSON.stringify({ sources: [], records: {}, truth: "local", functions }, null, 2) + "\n");
275
+ record(dir, { entity: "knowledge/sources.json", route: "knowledge/knowledge-sources", activity: "write", agent: asPerson,
276
+ note: `systems of record: ${Object.entries(functions).map(([fn, f]) => `${fn} ${f.systems.map((s) => `${s.system} (${s.status})`).join(", ") || "in the founder's words"}`).join("; ")}` });
118
277
  }
119
278
 
120
279
  // The venture's package.json: the framework as a dev dependency and the four scripts (I-17).
@@ -126,12 +285,12 @@ writeFileSync(join(dir, "package.json"), JSON.stringify({
126
285
  scripts: { dev: "naive dev", build: "naive build", start: "naive start", check: "naive check" },
127
286
  devDependencies: { [fw.name]: spec },
128
287
  }, null, 2) + "\n");
129
- writeFileSync(join(dir, ".gitignore"), "node_modules/\n.naive/\n.DS_Store\n# Founder-supplied originals stay out of git (EE-070).\n**/raw/**/original.*\n");
288
+ writeFileSync(join(dir, ".gitignore"), "node_modules/\n.naive/\n.DS_Store\n# Founder-supplied originals stay out of git (EE-070).\n**/raw/**/original.*\n# A data stream's activity (bank transactions, CRM events) stays out of git; only its stream.json and summary.md are versioned.\n/*/_streams/*/*\n!/*/_streams/*/stream.json\n!/*/_streams/*/summary.md\n/knowledge/*/_streams/*/*\n!/knowledge/*/_streams/*/stream.json\n!/knowledge/*/_streams/*/summary.md\n");
130
289
 
131
290
  // Editor completion for records (I-26): every record file is mapped to its JSON Schema in the installed framework.
132
291
  const { routes } = await import(join(framework, "interface/router.mjs"));
133
292
  const schemas = new Map();
134
- for (const r of routes()) if (r.kind === "record" && r.schema && r.form !== "md") for (const f of r.files ?? []) if (f.endsWith(".json")) schemas.set(r.schema, [...(schemas.get(r.schema) ?? []), `/${f}`]);
293
+ for (const r of routes()) if (r.kind === "record" && r.schema && r.form !== "md") for (const f of r.files ?? []) if (f.endsWith(".json")) schemas.set(r.schema, [...(schemas.get(r.schema) ?? []), `/${physical(dir, f)}`]);
135
294
  mkdirSync(join(dir, ".vscode"), { recursive: true });
136
295
  writeFileSync(join(dir, ".vscode/settings.json"), JSON.stringify({
137
296
  "json.schemas": [...schemas].map(([schema, fileMatch]) => ({ fileMatch: [...new Set(fileMatch)], url: `./node_modules/${fw.name}/${schema}` })),
@@ -151,13 +310,33 @@ if (!flag("no-git") && spawnSync("git", ["--version"]).status === 0 && spawnSync
151
310
  git = c.status === 0 ? "initialized, with a first commit" : "initialized; the first commit was skipped because git has no user.name and user.email";
152
311
  } else if (!flag("no-git")) git = "skipped (already inside a git repository)";
153
312
 
154
- try { mkdirSync(dirname(prefsFile), { recursive: true }); writeFileSync(prefsFile, JSON.stringify({ phase: answers.phase, layers: answers.layers, legalForm: answers.legalForm }, null, 2) + "\n"); } catch {}
313
+ // The workspace (the folder holding the founder's ventures, like a monorepo root): the nearest
314
+ // naive-workspace.json above the venture lists it; with none, the venture's parent folder becomes the
315
+ // workspace, named "Naive workspace" until the founder renames it, with an AGENTS.md for rules that hold
316
+ // across every venture. --no-workspace leaves the parent folder as it is.
317
+ let workspace = null;
318
+ if (!flag("no-workspace")) {
319
+ let up = dirname(dir);
320
+ while (!existsSync(join(up, WORKSPACE_FILE)) && dirname(up) !== up) up = dirname(up);
321
+ const root = existsSync(join(up, WORKSPACE_FILE)) ? up : dirname(dir);
322
+ const wf = join(root, WORKSPACE_FILE);
323
+ const ws = existsSync(wf) ? JSON.parse(readFileSync(wf, "utf8")) : { schema: "everything-ecosystem", schema_version: readFileSync(join(framework, "schema/VERSION"), "utf8").trim(), name: "Naive workspace", ventures: [] };
324
+ const rel = relative(root, dir);
325
+ if (!ws.ventures.includes(rel)) ws.ventures.push(rel);
326
+ writeFileSync(wf, JSON.stringify(ws, null, 2) + "\n");
327
+ if (!existsSync(join(root, "AGENTS.md"))) writeFileSync(join(root, "AGENTS.md"), `# ${ws.name}\n\nRules for agents across every venture in this workspace: who you are, how you work, what is off limits. A venture's own AGENTS.md adds to these, and the nearer file wins when two say the same thing.\n`);
328
+ workspace = { root, name: ws.name };
329
+ }
330
+
331
+ try { mkdirSync(dirname(prefsFile), { recursive: true }); writeFileSync(prefsFile, JSON.stringify({ type: vtype.id, role, mode, phase: answers.phase, layers: answers.layers, legalForm: answers.legalForm }, null, 2) + "\n"); } catch {}
155
332
 
156
333
  const runScript = (s) => (manager === "npm" ? `npm run ${s}` : `${manager} ${s}`);
157
334
  const cd = relative(process.cwd(), dir) || ".";
335
+ const legal = functions.legal?.systems.length ? null : "Legal documents: none connected yet. Point Naive at them later with --source legal=google-drive:<folder> (or docusign, local-folder), and draft the founder agreement with the founding-agreement kit.";
158
336
  console.log(`
159
337
  ${bold("Success!")} Created ${basename(dir)} at ${dir}
160
- Git: ${git}
338
+ Workspace: ${workspace ? `${workspace.name} at ${workspace.root} (${WORKSPACE_FILE})` : "none (--no-workspace)"}
339
+ Git: ${git}${rhoNote ? `\n${rhoNote}` : ""}${legal ? `\n${legal}` : ""}
161
340
 
162
341
  Inside that directory, you can run:
163
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-venture",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Create a new Naive venture: npx create-venture my-venture",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,6 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "naive-framework": "^0.1.0"
23
+ "naive-framework": "^0.3.0"
24
24
  }
25
25
  }