Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ buddy/
workspace/*
!workspace/.gitkeep

# Task worktree tohoto repa patří vedle něj do personalspace/.worktrees/<task>.
# Kdyby přece vznikl uvnitř, Git ho netrackuje.
.worktrees/

# Editor state
.idea/
.cursor/
Expand Down
21 changes: 18 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,24 @@ Název adresáře musí odpovídat `owner.github_username + "_GEN3"` a

## Osobní moduly

Privátní nested repozitáře vytvářej pod `workspace/` a deklaruj je
v `modules.manifest.json`. Defaultní přístup je private a moduly se nesmějí
objevit v Organization discovery.
Osobní modul je soukromý repozitář `<login>/<slug>` na GitHub účtu vlastníka,
checkout v `workspace/<slug>` a slot v `modules.manifest.json` (Lazurio
decision 0171, dodatek z 2026-10-13). Nezakládej ho ručně:

1. `lazurio module create personalspace/<slug> --stack <stack> --dry-run`
ukáže plán (repozitář, soubory, port) a nic nezapíše. Ukaž ho vlastníkovi.
2. Teprve po jeho potvrzení týž příkaz s `--confirm <plan>` založí soukromý
repozitář, pushne první commit, umístí checkout do `workspace/<slug>`
a zapíše slot do `modules.manifest.json` task worktree.
3. Slot jde pull requestem do tohoto repa; merge je Publikace vlastníka.

Task worktree tohoto repa patří vedle něj do `personalspace/.worktrees/<task>`,
nikdy jako druhý adresář v `personalspace/` (Lazurio by pak nevědělo, který
Personalspace je vlastníkův). Lease osobních modulů leží v `module_port_pool`
tohoto manifestu, ve vyhrazeném rozsahu 23000–23099, který pooly Organizací
nepoužívají; Personalspace bez poolu ho dostane v pull requestu se slotem
prvního modulu. Defaultní přístup je private, collaboratory se nepřidávají
a moduly se nesmějí objevit v Organization discovery.

## Agent skills

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ owner a platform-operator kroků a viditelnými `BLOCKED` gaty.
- `personal.gen3.json` — vlastník, privacy hranice a repo kontrakty; výchozí
template nemá blok `buddy`.
- `personalspace.template.json` — veřejný marker původu a verze bootstrapu.
- `modules.manifest.json` — owner-scoped sloty privátních osobních modulů.
- `modules.manifest.json` — owner-scoped sloty privátních osobních modulů
a jejich `module_port_pool` (vyhrazený rozsah 23000–23099; osobní modul
zakládá `lazurio module create personalspace/<slug>`).
- `workspace/` — gitignored/nested privátní repozitáře osobních aplikací.
- `gbrain/` — celý gitignored mount samostatného private Markdown data repa.
- `manual/` — bootstrap, migrace a recovery.
Expand Down
4 changes: 4 additions & 0 deletions modules.manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"personal_generation": "gen3",
"owner": "owner-github-username",
"module_port_pool": {
"start": 23000,
"end": 23099
},
"module_slots": [
{
"path": "workspace/personal-notes",
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-instance-static.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
expectedIdentity,
gitlinkPaths,
isValidGitHubLogin,
modulePortPoolIssues,
reposEqual,
trackedPathPrivacyIssues,
trackedTextPrivacyIssues,
Expand Down Expand Up @@ -71,6 +72,7 @@ expect(personal.privacy?.shared_outputs === "metadata-only", "privacy.shared_out
expect(Array.isArray(personal.shared_spaces) && personal.shared_spaces.length === 0, "shared_spaces musí zůstat prázdné");
expect(personal.secrets?.path === "secrets" && personal.secrets?.git === "ignored", "secrets custody musí zůstat gitignored");
for (const issue of doctorDeclarationIssues(personal)) failures.push(issue);
for (const issue of modulePortPoolIssues(modules)) failures.push(issue);

if (personal.buddy !== undefined) {
expect(personal.buddy?.repository?.visibility === "private", "Buddy profile repo declaration musí být private");
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-template.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PERSONALSPACE_TEMPLATE_REPO,
PERSONALSPACE_TEMPLATE_VERSION,
gitlinkPaths,
modulePortPoolIssues,
trackedPathPrivacyIssues,
trackedTextPrivacyIssues,
} from "./personalspace-lib.mjs";
Expand Down Expand Up @@ -51,6 +52,7 @@ expect(personal.doctor?.scope_type === DOCTOR_SCOPE_TYPE, "doctor.scope_type mus
expect(existsSync(join(root, personal.doctor?.command?.[1] ?? "")), "deklarovaný doctor entrypoint musí existovat");
expect(manifest.owner === OWNER_PLACEHOLDER, "modules manifest owner placeholder driftuje");
expect(!(manifest.module_slots ?? []).some((slot) => slot.required_roles?.includes("buddy")), "slot bez Buddyho nesmí vyžadovat roli buddy");
for (const issue of modulePortPoolIssues(manifest)) expect(false, issue);
expect(ignore.split(/\r?\n/).includes("gbrain/"), "super-repo musí celé gbrain/ ignorovat");
expect(ignore.split(/\r?\n/).includes("buddy/"), "super-repo musí volitelný Buddy checkout ignorovat");
expect(ignore.split(/\r?\n/).includes("workspace/*"), "super-repo musí osobní module checkouty ignorovat");
Expand Down
42 changes: 42 additions & 0 deletions scripts/doctor-personalspace.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,45 @@ test("bootstrap odmítne běh, ze kterého žádný report nevznikl", () => {
expect(outcome.accepted).toBe(false);
expect(outcome.reason).toContain("report");
});

test("osobní modul tak, jak ho deklaruje lazurio module create: slot s repem a pool projdou privacy i materializací", async () => {
const { cwd } = await fixture();
await writeFile(join(cwd, "modules.manifest.json"), JSON.stringify({
...modulesManifest,
module_port_pool: { start: 23000, end: 23099 },
module_slots: [{
path: "workspace/notes",
slug: "notes",
space: "workspace",
category: "application",
default_access: "private",
required_roles: ["owner"],
source_of_truth: "git-native",
status: "active",
notes: "Notes: osobní Modul založený příkazem lazurio module create (stack vite-react).",
git: { url: `git@github.com:${LOGIN}/notes.git`, branch: "main" },
}],
}, null, 2));
await mkdir(join(cwd, "workspace", "notes", ".git"), { recursive: true });
const result = await runPersonalspaceDoctor({
cwd,
commandRunner: stubWithModules({ "workspace/notes": { visibility: "PRIVATE" } }),
});
expect(byId(result, "modules.repo_private").status).toBe("ok");
expect(byId(result, "personalspace.profile_materialization").status).toBe("ok");
expect(result.checks.some((check) => check.id === "modules.repo_observability")).toBe(false);
});

test("pool osobních modulů mimo vyhrazený rozsah je vada materializace", async () => {
const { cwd } = await fixture();
await writeFile(join(cwd, "modules.manifest.json"), JSON.stringify({
...modulesManifest,
module_port_pool: { start: 24000, end: 24099 },
}, null, 2));
const result = await runPersonalspaceDoctor({ cwd, commandRunner: stubRunCommand() });
const materialization = byId(result, "personalspace.profile_materialization");
expect(materialization.status).toBe("fail");
expect(materialization.details).toContain(
"modules.manifest.json module_port_pool musí být {\"start\", \"end\"} uvnitř vyhrazeného rozsahu 23000-23099",
);
});
36 changes: 36 additions & 0 deletions scripts/personalspace-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export const NESTED_REPO_STRATEGY = "doctor-managed-nested-repo";
export const DOCTOR_DECLARATION_SCHEMA_VERSION = "humanandmachines.doctor.declaration.v1";
export const DOCTOR_SCOPE_TYPE = "personalspace";
export const DOCTOR_COMMAND = Object.freeze(["bun", "scripts/doctor-personalspace.mjs", "--json"]);
/**
* Vyhrazený rozsah portů osobních modulů (Lazurio decision 0171, dodatek
* z 2026-10-13; LazurioPlatform F52). Personalspace ho deklaruje jako
* `module_port_pool` v `modules.manifest.json` a pooly Organizací ho nikdy
* nepoužívají. `lazurio module create personalspace/<slug>` z něj bere lease
* nového osobního modulu a Personalspace bez poolu ho dostane v pull requestu
* se slotem prvního modulu.
*/
export const PERSONAL_MODULE_PORT_POOL = Object.freeze({ start: 23000, end: 23099 });

const githubLoginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
const repoNamePattern = /^[A-Za-z0-9_.-]+$/;
Expand Down Expand Up @@ -202,6 +211,32 @@ export function buildModulesManifest(template, login) {
return output;
}

/**
* `module_port_pool` osobního prostoru: smí chybět (Personalspace z doby před
* vyhrazeným rozsahem ho dostane se slotem prvního modulu), a když ho manifest
* deklaruje, leží celý ve vyhrazeném rozsahu.
*/
export function modulePortPoolIssues(manifest) {
if (!manifest || typeof manifest !== "object" || !Object.hasOwn(manifest, "module_port_pool")) {
return [];
}
const pool = manifest.module_port_pool;
const keys = pool && typeof pool === "object" && !Array.isArray(pool) ? Object.keys(pool) : null;
const { start, end } = PERSONAL_MODULE_PORT_POOL;
if (
keys === null
|| keys.some((key) => key !== "start" && key !== "end")
|| !Number.isInteger(pool.start)
|| !Number.isInteger(pool.end)
|| pool.start > pool.end
|| pool.start < start
|| pool.end > end
) {
return [`modules.manifest.json module_port_pool musí být {"start", "end"} uvnitř vyhrazeného rozsahu ${start}-${end}`];
}
return [];
}

export function validatePersonalState(config, manifest, {
directoryName = null,
ownerRemote = null,
Expand Down Expand Up @@ -317,6 +352,7 @@ export function validatePersonalState(config, manifest, {
if (!Array.isArray(manifest?.module_slots)) {
failures.push("modules.manifest.json module_slots musí být pole");
}
failures.push(...modulePortPoolIssues(manifest));
if (
config.buddy === undefined
&& Array.isArray(manifest?.module_slots)
Expand Down
76 changes: 76 additions & 0 deletions scripts/personalspace-lib.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
buildPersonalConfig,
doctorDeclarationIssues,
expectedIdentity,
PERSONAL_MODULE_PORT_POOL,
gitlinkPaths,
inferMountLayout,
modulePortPoolIssues,
parseGitHubRemote,
parseFsckObjectFindings,
parseLsTreeLongEntries,
Expand Down Expand Up @@ -474,3 +476,77 @@ test("chybějící nebo pokažená deklarace doctora je vada, ne ticho", () => {
expect(validatePersonalState(personal, buildModulesManifest({ module_slots: [] }, "example")))
.toContain("personal.gen3.json musí deklarovat blok doctor (decision 0118)");
});

test("module_port_pool osobního prostoru smí chybět, jinak leží ve vyhrazeném rozsahu", () => {
expect(PERSONAL_MODULE_PORT_POOL).toEqual({ start: 23000, end: 23099 });
// Personalspace z doby před vyhrazeným rozsahem: pool dostane se slotem
// prvního modulu (lazurio module create personalspace/<slug>).
expect(modulePortPoolIssues({ module_slots: [] })).toEqual([]);
expect(modulePortPoolIssues({
module_port_pool: { start: 23000, end: 23099 },
module_slots: [],
})).toEqual([]);
expect(modulePortPoolIssues({ module_port_pool: { start: 23010, end: 23019 } })).toEqual([]);
for (const pool of [
{ start: 24000, end: 24099 },
{ start: 22999, end: 23050 },
{ start: 23050, end: 23100 },
{ start: 23050, end: 23040 },
{ start: 23000 },
{ start: 23000, end: 23099, extra: 1 },
{ start: "23000", end: 23099 },
null,
[],
"23000-23099",
]) {
expect(modulePortPoolIssues({ module_port_pool: pool }), JSON.stringify(pool)).toHaveLength(1);
}
});

test("bootstrap zachová pool šablony a materializace hlídá jeho rozsah", () => {
const personal = buildPersonalConfig(personalTemplate, {
login: "example",
displayName: "Example Owner",
});
const template = {
personal_generation: "gen3",
owner: "owner-github-username",
module_port_pool: { start: 23000, end: 23099 },
module_slots: [],
};
const manifest = buildModulesManifest(template, "example");
expect(manifest.module_port_pool).toEqual({ start: 23000, end: 23099 });
expect(validatePersonalState(personal, manifest)).toEqual([]);
expect(validatePersonalState(personal, {
...manifest,
module_port_pool: { start: 24900, end: 24999 },
})).toEqual([
"modules.manifest.json module_port_pool musí být {\"start\", \"end\"} uvnitř vyhrazeného rozsahu 23000-23099",
]);
});

test("slot osobního modulu tak, jak ho deklaruje lazurio module create, projde materializací", () => {
const personal = buildPersonalConfig(personalTemplate, {
login: "example",
displayName: "Example Owner",
});
const manifest = buildModulesManifest({
personal_generation: "gen3",
owner: "owner-github-username",
module_port_pool: { start: 23000, end: 23099 },
module_slots: [{
path: "workspace/notes",
slug: "notes",
space: "workspace",
category: "application",
default_access: "private",
required_roles: ["owner"],
source_of_truth: "git-native",
status: "active",
notes: "Notes: osobní Modul založený příkazem lazurio module create (stack vite-react).",
git: { url: "git@github.com:example/notes.git", branch: "main" },
}],
}, "example");
expect(manifest.module_slots[0].required_roles).toEqual(["owner"]);
expect(validatePersonalState(personal, manifest)).toEqual([]);
});
Loading