diff --git a/docs/TOOLS.md b/docs/TOOLS.md index fa1edff42..6ac0c7399 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -594,3 +594,21 @@ Mapping JSON contains `models`, `families` and optional per-role `instances` selections. Jason checks and installs the output in Mos's window with the PM run idle; re-pin in that same window. Never migrate the live file from an agent session. + + +### Local project records (`mosaic project`) + +`mosaic project init ` creates the declared project's +separate local record repository, with layout version 1, package-validated JSON +templates, Markdown spec/docs and its first commit authored as the business's +human. Init refuses an agent environment, an undeclared project or a non-empty +directory. No bus or remote is involved. + +`mosaic project check ` validates project metadata, every +record, filename/ID agreement and the fixed layout, naming each problem. It +exits 0 when valid, 2 for project/record problems (including an undeclared +project), 3 for configuration loading problems and 4 for usage. The optional business project `repo` is a normalized absolute +record-repository path; the default is +`/projects///`. `root` remains the code checkout. +The package README describes schemas and the shared agent record writer; agent +writes never enter `spec/`, never absorb pending edits and refuse remotes. diff --git a/packages/business/src/business.mjs b/packages/business/src/business.mjs index 21a7f5a65..630486662 100644 --- a/packages/business/src/business.mjs +++ b/packages/business/src/business.mjs @@ -176,11 +176,15 @@ export function validateBusinessDocument(document, file, { rolesDir }) { for (const [id, entry] of Object.entries(document.projects)) { requireId(id, `${file} project id`); requireObject(entry, `${file} projects.${id}`); - rejectUnknownKeys(entry, ["root"], `${file} projects.${id}`); + rejectUnknownKeys(entry, ["root", "repo"], `${file} projects.${id}`); if (typeof entry.root !== "string" || !isAbsolute(entry.root) || normalize(entry.root) !== entry.root || entry.root.includes("\0")) { refuse(`${file} projects.${id}.root must be a normalized absolute path`); } - projects[id] = { root: entry.root }; + if (Object.hasOwn(entry, "repo") && (typeof entry.repo !== "string" || !isAbsolute(entry.repo) || + normalize(entry.repo) !== entry.repo || entry.repo.includes("\0"))) { + refuse(`${file} projects.${id}.repo must be a normalized absolute path`); + } + projects[id] = { root: entry.root, ...(Object.hasOwn(entry, "repo") ? { repo: entry.repo } : {}) }; } const tracker = checkTracker(document.tracker, file); diff --git a/packages/cli/src/cli.mjs b/packages/cli/src/cli.mjs index e341a0b0e..26cddd24f 100644 --- a/packages/cli/src/cli.mjs +++ b/packages/cli/src/cli.mjs @@ -13,6 +13,7 @@ import { join } from "node:path"; import { createInterface } from "node:readline/promises"; import { fileURLToPath } from "node:url"; import { BINDING_NAME } from "../../discord/src/binding.mjs"; +import { projectCommand, PROJECT_USAGE } from "./project.mjs"; import { limitsCommand, LIMITS_USAGE } from "./limits.mjs"; import { pluginsCommand, PLUGINS_USAGE } from "./plugins.mjs"; import { CliError } from "./errors.mjs"; @@ -30,6 +31,7 @@ import { SeatError } from "../../seat/src/seat.mjs"; export const USAGE = `usage: ${LIMITS_USAGE} ${PLUGINS_USAGE} + ${PROJECT_USAGE} mosaic inbox [--business ] [--json] [--tester] mosaic decide