Files
stack/packages/project
rocko 4bdc7e0e86 processing: inspectable per-run stop seam for lifecycle fire
Unify halt and fire cancellation by verified run identity with durable first-owner receipts and shared deadlines. Integrate the instance control with the current host, library and tester-socket paths. Focused nine-file evidence is 62 passing tests; broad suites, mutants and review remain pending.
2026-10-21 22:42:12 -05:00
..

Project record repository — layout 1

A project's records live in a separate local git repository. This is distinct from its code checkout (business.projects[project].root) and the working bare repository used for code review. No remote is created or contacted.

Set an optional normalized absolute repo in the business project entry:

{"projects":{"stack":{"root":"/work/code","repo":"/work/project-records"}}}

Without repo, records live at <dataRoot>/projects/<business>/<project>/. The records path must not overlap any declared project's code root, including another project's root (in either direction). Record paths and their existing ancestors must not be symlinks.

mosaic project init acme stack
mosaic project check acme stack

Init failure removes its own files and Git directory, allowing retry. A target that existed empty is preserved; newly created empty parent directories are removed. Unrelated files at the root or inside a layout directory are preserved, so a concurrent human save can leave a non-empty target that needs inspection before retry. Changed ownership of an initializer path refuses project-init-cleanup and preserves that path for reconciliation. The private .git created by this initializer is removed as a unit; external tools must not modify Git metadata during initialization.

Both commands use the existing system and business config loaders. Init requires an outside-agent CLI environment, a declared project and an empty directory. It creates main and the first commit as the business's human, with a local <human>@mosaic.invalid email. Check is read-only; it exits 2 and lists all problems found. An otherwise valid layout with no readable HEAD commit refuses project-no-commit (CLI exit 2), rather than reporting success. Usage exits 4; configuration problems exit 3. No bus is needed.

Layout

Path Contents
project.json layoutVersion: 1, business, project, name, createdAt
spec/north-star.md Human-editable Markdown north star
docs/ Markdown documentation; check accepts nested docs
requirements/ID.json One requirement per file
decisions/ID.json One decision per file
templates/ project.json, requirement.json, decision.json, document.md, north-star.md

Empty requirements and decisions directories contain an empty .gitkeep, so the layout survives git checkout. That marker is checked separately from records. Init makes no example requirements or fictional decisions. The initial project name is its project ID; the user can edit the display name in project.json.

A requirement has recordVersion: 1, id, title, text, and status (draft, accepted, complete). A decision has recordVersion: 1, id, question, options (2–9 distinct {key,text} entries), recommendation, status (open, accepted, rejected) and choice (null while open, an option key when closed). These are local reference records, not bus authorization or approval. Decision spec citations and settlement policy belong to row 138.

Schemas are fixed in packages/project/schemas/, never loaded from the project. Every JSON template validates against its record schema. Editing templates cannot weaken validation; adding a project-local schema is refused. The schema validator implements a closed subset of JSON Schema 2020-12 matching these schemas and refuses any unsupported schema keyword. Dates use canonical millisecond UTC. Files are bounded to 1 MiB, valid UTF-8 without NULs, regular, and not hard-linked. JSON record names must equal their IDs. Unknown top-level paths, missing required paths, unexpected templates and non-Markdown docs refuse. The git directory is inspected separately, without reading project data as git configuration or running hooks.

Shared APIs

Import from packages/project/src/index.mjs:

const context = { business, project: 'stack', dataRoot };
projectRepo(context);                    // absolute repo path; no writes
await initProject(context);              // {repo, commit}
await checkProject(context);             // {ok, repo, problems}
validateRecord('requirement', value);     // array of problem strings
await writeRecord({
  ...context,
  path: 'requirements/REQ-PROJECT-1.json',
  record: value,
  author: { definition: 'coder', instance: 'builder' },
});                                      // {repo, path, commit}

writeRecord is the single agent record writer. It accepts docs/NAME.md, requirements/ID.json and decisions/ID.json, validates first, and makes a local commit authored and committed as <definition>.<instance> with a local .invalid email. Actor metadata is trusted caller input whose shape is validated here; row 138's broker must derive it from the bound role. This package neither authenticates an agent nor grants it authority.

An author with definition human refuses project-author; this agent writer provides no human record write path. Every agent path under spec/ refuses with project-spec-human-only. Agents cannot write project.json, templates or git metadata through this API. A valid but unchanged record refuses project-record-unchanged without an empty commit.

Stricter safeguards agreed with Sage (sage-0400): a remote refuses project-remote; pending tracked, staged, untracked or ignored files and hidden index flags refuse project-dirty. The writer never folds human edits into its commit. Concurrent writers refuse project-busy through an exclusive local lock; no stale lock is automatically removed. Links, indirect git stores and unsupported local git config refuse. Git uses no inherited global/system configuration, no hooks, fsmonitor, filters or transports, and runs in bounded process groups using the existing seat git runner.

Records publish with a temporary file, fsync and rename, then a commit of only that path. On git failure with HEAD unchanged, only the writer's file/index change is restored. If HEAD changed while an error occurred, the writer refuses project-commit-uncertain and preserves evidence for reconciliation; it never rewinds history. Locking serializes this writer, not arbitrary human editors: callers must coordinate simultaneous human saves. No destructive repair, push, remote creation, role grant, spec-confirmation mechanism or live action ships.

Tests: node --test packages/project/tests/*.test.mjs. Fixtures only.

Committed file reads (row 138)

readProjectFile({business,project,dataRoot,path,commit?}) reads only spec/north-star.md or a named docs/NAME.md; readSpec({business,project,dataRoot,commit?}) fixes the spec path. Both are async and read-only, share repository/path checks, refuse remotes and unsafe Git metadata, and validate committed project.json identity. Working-tree edits do not replace committed content. A supplied full SHA-1 must be a commit reachable from a captured HEAD.

The result is {repo,path,head,revision,commit,content,author,log,truncated}. commit is the last commit changing the file at revision, not necessarily HEAD; content preserves exact valid UTF-8 including trailing newlines, up to 1 MiB with no NUL. Log has at most 100 entries, newest first, each {commit,author:{name,email},at,subject}; at is canonical UTC. truncated says more entries exist. Reads reject nonregular Git blobs, missing paths and invalid content. History output is separately bounded to 256 KiB.

readSpec adds confirmed=true exactly when the file commit author.name equals business.human. An agent-authored latest change stays unconfirmed. This is local Git provenance, not a cryptographic identity proof. Broker citation validation compares a fresh current snapshot and never uses an older read as authority. Read errors are ProjectError: project-read-path, project-read-commit, project-read-missing, project-read-content, project-invalid, plus existing project-id/path/not-declared/repository/remote/git codes. These reads write nothing; the human writer is the next section.

The human editor writer (row 148)

listProjectFiles({business,project,dataRoot}) returns {head, files:[{path}]}: spec/north-star.md and the Markdown files directly under docs/ (name [A-Za-z0-9][A-Za-z0-9._-]{0,127}.md), as committed at HEAD and in path order. An uncommitted file, a subdirectory, a non-Markdown file and a link are not listed. documentPath(path) is the one definition of those paths, shared by the reads and the writer.

writeHumanFile({business,project,dataRoot,path,content,base}) is the Console editor's save. It replaces the whole text of an existing document with content and makes one local commit, Edit <path>, authored by business.human. Only the human bus verb project.file.save calls it; the agent writer (writeRecord) is unchanged and still refuses spec/. It never creates a file, never pushes and creates no remote.

base is the full commit the editor opened: the file's last commit at HEAD. If the file's last commit is any other, the writer refuses stale-base and writes nothing, whoever made the newer commit. The check runs under the same exclusive lock and the same clean-tree rules as the agent writer, so a tree with pending changes refuses project-dirty and a concurrent writer project-busy. Other refusals: project-record-path (a path outside the two), project-record-invalid (empty content, a NUL, text that is not well-formed UTF-8, over 1 MiB, or a base that is not a full commit id) and project-read-missing (no such document).

Tests: packages/project/tests/human-file.test.mjs.