diff --git a/apps/website/qa/agents-spec-contract.mjs b/apps/website/qa/agents-spec-contract.mjs
index 41411423ed..35fde93405 100644
--- a/apps/website/qa/agents-spec-contract.mjs
+++ b/apps/website/qa/agents-spec-contract.mjs
@@ -21,7 +21,7 @@ import { join } from 'node:path'
import { generate, SKILLS_OUT, CRONS_OUT, AGENTS_OUT, FUNCTIONS_OUT, TOOLS_OUT, FUNCTIONS_MANIFEST, EXPERIENCE_PATH, AGENT_COUNT, AGENT_LAYERS, HOSTS, CONTROLS, ON_FAILURE, FN_TRIGGERS, FN_ON_FAILURE, FN_SIDE_EFFECTS, FN_PROBE_RESULTS, FN_CONTROLS, functionDifferences, I18N_SPEC_DIR, I18N_FIELD_SOURCE, REPO_ROOT } from '../scripts/agents-from-specs.mjs'
import { canonicalSpecEditUrl, vendoredSpecUrl, specSlug } from '../src/lib/agentSpecs.ts'
import { MODULES } from '../src/lib/queenModules.ts'
-import { HUD_VIEWS, HUD_KEYS, RAIL_VIEWS, SPEC_LAYERS, BOARD_VIEWS, railViewOf } from '../src/components/queenHud.ts'
+import { HUD_VIEWS, HUD_KEYS, RAIL_VIEWS, SPEC_LAYERS, BOARD_VIEWS, PROJECT_VIEWS, railViewOf } from '../src/components/queenHud.ts'
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
const skills = JSON.parse(readFileSync(SKILLS_OUT, 'utf8'))
@@ -397,15 +397,18 @@ for (const tab of shown) {
assert.ok(tab !== 'comb' && tab !== 'specs', `'${tab}' has a block of its own; showcasing it would render it twice`)
}
assert.match(app, //, 'the homepage does not mount the index, so every module the showcase leaves out is unreachable')
-// The index draws three groups -- the rail's buttons, the ladder inside SPECS,
-// the board inside KANBAN -- and they have to stay a partition of MODULES or the
-// homepage silently drops a module. The predicates are asked of the shell's own
+// The index draws four groups -- the rail's buttons, the ladder inside SPECS,
+// the board inside KANBAN, the record inside PROJECT -- and they have to stay a
+// partition of MODULES or the homepage silently drops a module. The fourth came
+// with PASSPORT's move inside PROJECT (2026-09-21): until the index learned it,
+// PASSPORT was a module with no card anywhere. The predicates are asked of the shell's own
// lists, so this is checkable here rather than only readable there.
const modulesBlock = src('components/ModulesBlock.tsx')
assert.match(modulesBlock, /RAIL_VIEWS as readonly string\[\]\)\.includes\(tab\)/, 'the index no longer reads the rail from RAIL_VIEWS, so its groups can drift from the buttons')
assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isRailModule\(m\.tab\)\)/, 'the rail group is no longer MODULES on the rail, so a module can fall off the homepage')
assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isLadderRung\(m\.tab\)\)/, 'the ladder group is no longer the spec layers the rail does not draw')
assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isBoardReading\(m\.tab\)\)/, 'the board group is no longer the board views the rail does not draw')
+assert.match(modulesBlock, /MODULES\.filter\(\(m\) => isProjectRecord\(m\.tab\)\)/, 'the project group is no longer the project views the rail does not draw')
for (const m of MODULES) {
const group = RAIL_VIEWS.includes(m.tab)
? 'rail'
@@ -413,8 +416,10 @@ for (const m of MODULES) {
? 'ladder'
: BOARD_VIEWS.includes(m.tab)
? 'board'
- : null
- assert.ok(group, `module '${m.tab}' is in none of the index's three groups, so its card is drawn nowhere`)
+ : PROJECT_VIEWS.includes(m.tab)
+ ? 'project'
+ : null
+ assert.ok(group, `module '${m.tab}' is in none of the index's four groups, so its card is drawn nowhere`)
}
assert.match(modulesBlock, /modules\.map\(\(module\)/, 'the index no longer draws a card for every module in a group')
// lib/queenModules opens by calling itself "the only place their identity is
@@ -520,9 +525,11 @@ assert.equal(HUD_KEYS.slice(0, HUD_VIEWS.length).join(''), '1234567890tprbwmlx',
// member on both its family's list and the rail is drawn twice.
assert.ok(SPEC_LAYERS.every((layer) => HUD_VIEWS.includes(layer)), 'a ladder layer is not a view, so it has no address and no key')
assert.ok(BOARD_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'a board view is not a view, so it has no address and no key')
+assert.ok(PROJECT_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'a project view is not a view, so it has no address and no key')
assert.ok(RAIL_VIEWS.every((view) => HUD_VIEWS.includes(view)), 'the rail draws a button for something that is not a view')
assert.equal(SPEC_LAYERS[0], 'specs', 'SPECS is the first rung: it is the module the rail opens and the ladder stands in')
assert.equal(BOARD_VIEWS[0], 'kanban', 'KANBAN is the board the rail opens and the row stands in')
+assert.equal(PROJECT_VIEWS[0], 'project', 'PROJECT is the module the rail opens and PASSPORT stands inside (2026-09-21)')
assert.deepEqual(
[...SPEC_LAYERS],
['specs', 'skills', 'crons', 'agents', 'tools', 'functions'],
@@ -534,9 +541,9 @@ assert.deepEqual(
'the board is kanban, mission map, factory, tech tree -- the order of their keys, 3/4/5/6',
)
assert.deepEqual(
- [...new Set([...RAIL_VIEWS, ...SPEC_LAYERS, ...BOARD_VIEWS])].sort(),
+ [...new Set([...RAIL_VIEWS, ...SPEC_LAYERS, ...BOARD_VIEWS, ...PROJECT_VIEWS])].sort(),
[...HUD_VIEWS].sort(),
- 'rail plus ladder plus board is no longer every view: something is unreachable',
+ 'rail plus ladder plus board plus project is no longer every view: something is unreachable',
)
assert.deepEqual(
RAIL_VIEWS.filter((view) => SPEC_LAYERS.includes(view)),
@@ -548,10 +555,15 @@ assert.deepEqual(
['kanban'],
'only KANBAN may be on the rail and in the board row; another reading on both is drawn twice',
)
+assert.deepEqual(
+ RAIL_VIEWS.filter((view) => PROJECT_VIEWS.includes(view)),
+ ['project'],
+ 'only PROJECT may be on the rail and in the project family; PASSPORT on both is drawn twice',
+)
assert.equal(
RAIL_VIEWS.length,
- HUD_VIEWS.length - (SPEC_LAYERS.length - 1) - (BOARD_VIEWS.length - 1),
- 'the rail is the views less the five layers folded into SPECS and the two readings folded into KANBAN',
+ HUD_VIEWS.length - (SPEC_LAYERS.length - 1) - (BOARD_VIEWS.length - 1) - (PROJECT_VIEWS.length - 1),
+ 'the rail is the views less the layers folded into SPECS, the readings folded into KANBAN and the record folded into PROJECT',
)
// Every view still answers railViewOf, and answers with a button the rail draws
// -- that is what lights the rail while a folded member is open.
diff --git a/apps/website/qa/docs-spec-contract.mjs b/apps/website/qa/docs-spec-contract.mjs
index e61d3c0fad..9bea432925 100644
--- a/apps/website/qa/docs-spec-contract.mjs
+++ b/apps/website/qa/docs-spec-contract.mjs
@@ -170,8 +170,11 @@ if (witnessTable) {
const constitution = readFileSync(join(CORPUS, 'docs/T27-CONSTITUTION.md'), 'utf8')
const alphabet = readFileSync(join(CORPUS, 'docs/agents/AGENTS_ALPHABET.md'), 'utf8')
const laws = parseLawTable(constitution), phases = parsePhases(alphabet)
-assert.equal(laws.rows.length, 7, 'the constitution defines L1-L7')
-assert.deepEqual(laws.rows.map((l) => l.law), ['L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7'])
+// L0 PURPOSE (adopted 2026-09-23) opens the table and sits outside the L1-L7
+// ordering by the constitution's own words; L1-L7 follow in order, unchanged.
+const lawIds = laws.rows.map((l) => l.law)
+const orderedLaws = lawIds[0] === 'L0' ? lawIds.slice(1) : lawIds
+assert.deepEqual(orderedLaws, ['L1', 'L2', 'L3', 'L4', 'L5', 'L6', 'L7'], 'the constitution defines L1-L7, optionally preceded by L0 PURPOSE')
if (lawsTable) assert.deepEqual(lawsTable.rows, laws.rows)
assert.deepEqual(docs.figures['law-hierarchy'].laws.map((l) => l.law), laws.rows.map((l) => l.law))
assert.equal(phases.length, 7, 'the alphabet draws six phases plus GIT WORKFLOW')
diff --git a/apps/website/src/components/ModulesBlock.tsx b/apps/website/src/components/ModulesBlock.tsx
index 7c1c71b23c..99b4cd07b1 100644
--- a/apps/website/src/components/ModulesBlock.tsx
+++ b/apps/website/src/components/ModulesBlock.tsx
@@ -1,6 +1,6 @@
import { useI18n } from '../i18n/context'
import { MODULES } from '../lib/queenModules'
-import { BOARD_VIEWS, RAIL_VIEWS, SPEC_LAYERS, isBoardView, isSpecLayer } from './queenHud'
+import { BOARD_VIEWS, PROJECT_VIEWS, RAIL_VIEWS, SPEC_LAYERS, isBoardView, isProjectView, isSpecLayer } from './queenHud'
import './ModulesBlock.css'
// A compact index of the modules: one card each, linking to its tab. Not on the
@@ -13,21 +13,23 @@ import './ModulesBlock.css'
// FUNCTIONS — are rungs of the ladder inside SPECS, because each is a catalogue
// generated from .t27 specs and each names the one below it. Two — MISSION MAP
// and FACTORY — sit under KANBAN, because the three are three readings of the
-// one board. A reader who scrolled past fourteen equal cards and then opened a
-// rail of seven was reading a map of a shell that no longer existed.
+// one board. PASSPORT sits inside PROJECT (2026-09-21), the record the project
+// keeps beside itself. A reader who scrolled past fourteen equal cards and then
+// opened the rail was reading a map of a shell that no longer existed.
//
// Which group a module is in is asked of the shell's own lists rather than
// restated here: RAIL_VIEWS already is HUD_VIEWS less whatever has been folded,
-// so folding a view under another moves its card by itself. The three groups
+// so folding a view under another moves its card by itself. The four groups
// are `MODULES.filter` of predicates that partition by construction -- every
// module is in exactly one, and a new one lands in its group with no second
// list to remember.
/** On the rail: the shell's own list, so this cannot drift from the buttons. */
const isRailModule = (tab: string) => (RAIL_VIEWS as readonly string[]).includes(tab)
-/** Folded into SPECS, and folded into KANBAN: the two families less their heads. */
+/** Folded into SPECS, KANBAN and PROJECT: the three families less their heads. */
const isLadderRung = (tab: string) => isSpecLayer(tab) && !isRailModule(tab)
const isBoardReading = (tab: string) => isBoardView(tab) && !isRailModule(tab)
+const isProjectRecord = (tab: string) => isProjectView(tab) && !isRailModule(tab)
const RAIL_MODULES = MODULES.filter((m) => isRailModule(m.tab))
// Sorted into each family's own order, which is not the order of their keys:
@@ -35,8 +37,10 @@ const RAIL_MODULES = MODULES.filter((m) => isRailModule(m.tab))
const order = (list: readonly string[]) => (tab: string) => list.indexOf(tab)
const rung = order(SPEC_LAYERS)
const seat = order(BOARD_VIEWS)
+const shelf = order(PROJECT_VIEWS)
const LADDER_MODULES = MODULES.filter((m) => isLadderRung(m.tab)).sort((a, b) => rung(a.tab) - rung(b.tab))
const BOARD_MODULES = MODULES.filter((m) => isBoardReading(m.tab)).sort((a, b) => seat(a.tab) - seat(b.tab))
+const PROJECT_MODULES = MODULES.filter((m) => isProjectRecord(m.tab)).sort((a, b) => shelf(a.tab) - shelf(b.tab))
// Every count is read, never a word typed here: the eyebrow once said "EIGHT"
// while the title said "ten" and the rail showed ten.
@@ -44,11 +48,12 @@ const N = MODULES.length
const RAIL_N = RAIL_MODULES.length
const LADDER_N = LADDER_MODULES.length
const BOARD_N = BOARD_MODULES.length
+const PROJECT_N = PROJECT_MODULES.length
const COPY = {
en: {
eyebrow: `QUEEN / ${N} MODULES`,
title: `One screen, ${N} ways to read it`,
- lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; the last ${BOARD_N} are the board's other two readings, under KANBAN.`,
+ lede: `The Queen is a single-page module: the 3D map is the ground and every panel floats on it. These ${N} are what that ground can be read as — each one opens on the same map, in the same shell, from the key beside it. ${RAIL_N} are buttons down the left edge; ${LADDER_N} are rungs of one ladder inside SPECS, where every catalogue generated from a spec lives together; ${BOARD_N} are the board's other two readings, under KANBAN; and ${PROJECT_N} is the record kept beside the project, inside PROJECT.`,
railHead: `On the rail · ${RAIL_N}`,
ladderHead: `Inside SPECS, on the ladder · ${LADDER_N}`,
ladderNote:
@@ -56,13 +61,16 @@ const COPY = {
boardHead: `Inside KANBAN, on the board · ${BOARD_N}`,
boardNote:
'The kanban, the mission map and the factory read one board: the same issues as columns, as a map, and as what the swarm is building from them. Same keys, one step inside KANBAN.',
+ projectHead: `Inside PROJECT, beside it · ${PROJECT_N}`,
+ projectNote:
+ 'The passport is part of how the project describes itself, not an instrument of its own: the record every result has to carry. Same key, one step inside PROJECT.',
open: 'Open',
all: 'Open the shell →',
},
ru: {
eyebrow: `КОРОЛЕВА / ${N} МОДУЛЕЙ`,
title: `Один экран и ${N} способов его прочитать`,
- lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, а последние ${BOARD_N} — два других прочтения доски, внутри КАНБАНА.`,
+ lede: `Королева — одностраничный модуль: 3D-карта здесь основание, а каждая панель плавает над ним. Эти ${N} — то, чем это основание можно прочитать; любой открывается на той же карте, в том же шелле, по клавише рядом с ним. ${RAIL_N} — кнопки вдоль левого края, ${LADDER_N} — ступени одной лестницы внутри СПЕК, где лежат вместе все каталоги, порождённые из спек, ${BOARD_N} — два других прочтения доски, внутри КАНБАНА, и ${PROJECT_N} — запись, которую проект держит рядом с собой, внутри ПРОЕКТА.`,
railHead: `На панели · ${RAIL_N}`,
ladderHead: `Внутри СПЕК, на лестнице · ${LADDER_N}`,
ladderNote:
@@ -70,6 +78,9 @@ const COPY = {
boardHead: `Внутри КАНБАНА, на доске · ${BOARD_N}`,
boardNote:
'Канбан, карта миссий и фабрика читают одну доску: те же задачи — колонками, картой и тем, что рой из них строит. Клавиши прежние, просто на шаг внутрь КАНБАНА.',
+ projectHead: `Внутри ПРОЕКТА, рядом с ним · ${PROJECT_N}`,
+ projectNote:
+ 'Паспорт — часть того, как проект описывает себя, а не отдельный инструмент: запись, которую должен нести каждый результат. Клавиша прежняя, просто на шаг внутрь ПРОЕКТА.',
open: 'Открыть',
all: 'Открыть шелл →',
},
@@ -130,6 +141,12 @@ export default function ModulesBlock() {