diff --git a/STATUS.md b/STATUS.md
new file mode 100644
index 0000000..25e24f1
--- /dev/null
+++ b/STATUS.md
@@ -0,0 +1,143 @@
+# Project status
+
+> Generated from [project/status.json](project/status.json) on 2026-07-19. Do not edit this file directly.
+
+**Stage:** pre-alpha
+**Current milestone:** M2 — GitHub collaboration adapter
+**Roadmap:** 15/28 actionable items complete; 0 blocked; 3 deferred.
+
+| Milestone | Status | Complete | Blocked | Deferred |
+|---|---:|---:|---:|---:|
+| M0 — Protocol kernel | complete | 3/3 | 0 | 0 |
+| M1 — Local vertical slice | complete | 6/6 | 0 | 0 |
+| M2 — GitHub collaboration adapter | planned | 2/6 | 0 | 0 |
+| M3 — Research and assessment profiles | planned | 1/6 | 0 | 1 |
+| M4 — Public console and integrations | planned | 3/7 | 0 | 0 |
+| M5 — Optional managed service | deferred | 0/0 | 0 | 2 |
+
+## M0 — Protocol kernel
+
+**Status:** complete
+
+Exit criteria:
+- The provider-neutral protocol kernel, local read-only MCP surface, fixtures, and public boundary are implemented.
+
+- [x] **M0-PROTOCOL: Canonical schemas, hashes, and event-chain verification** — done
+ - Evidence: [Foundation commit](https://github.com/brendohxd/Project-Relay/commit/99f8c9c)
+- [x] **M0-MCP: Read-only local MCP server** — done
+ - Evidence: [M0 implementation](https://github.com/brendohxd/Project-Relay/commit/99f8c9c)
+- [x] **M0-PUBLIC: Public-boundary scanner and repository checks** — done
+ - Evidence: [M0 implementation](https://github.com/brendohxd/Project-Relay/commit/99f8c9c)
+
+## M1 — Local vertical slice
+
+**Status:** complete
+
+Exit criteria:
+- The complete local remediation loop passes Windows and Linux verification and receives human review.
+
+- [x] **M1-LIFECYCLE: Policy transition engine and remediation fixture** — done
+ - Evidence: [M1 implementation commit](https://github.com/brendohxd/Project-Relay/commit/d899218)
+- [x] **M1-EVIDENCE: Evidence CLI, independent review, reproduction, and human gates** — done
+ - Evidence: [M1 review fixes](https://github.com/brendohxd/Project-Relay/commit/786648e)
+- [x] **M1-LOCAL-VERIFY: Windows local verification with 18 tests** — done
+ - Evidence: [Draft PR verification record](https://github.com/brendohxd/Project-Relay/pull/1#issuecomment-5014591903)
+- [x] **M1-HUMAN-REVIEW: Human review of draft PR #1** — done
+ - Evidence: [Approved and merged M1 pull request](https://github.com/brendohxd/Project-Relay/pull/1)
+- [x] **M1-LINUX-VERIFY: Linux conformance verification** — done
+ - Evidence: [Passing Windows and Linux M1 matrix](https://github.com/brendohxd/Project-Relay/actions/runs/29680192255)
+- [x] **M1-MERGE: Merge the reviewed M1 candidate** — done
+ - Evidence: [M1 merge commit](https://github.com/brendohxd/Project-Relay/commit/65a7d36ea2d9d29271b970ba883baa9c5a5a2b34)
+
+## M2 — GitHub collaboration adapter
+
+**Status:** planned
+
+Exit criteria:
+- An approved Relay proposal can be translated into an idempotent, least-privilege GitHub change with conflict and failure records.
+
+- [x] **M2-CAPABILITIES: Declared actor capabilities** — done
+ - Evidence: [Capability foundation](https://github.com/brendohxd/Project-Relay/commit/11dc90a)
+- [x] **M2-CAUSAL-LINKS: Typed causal provenance links** — done
+ - Evidence: [Causal-link foundation](https://github.com/brendohxd/Project-Relay/commit/11dc90a)
+- [ ] **M2-PROPOSAL: Deterministic network-free GitHub mutation proposal** — planned
+ - Next: Define proposal input and output contracts before authentication or writes.
+- [ ] **M2-CONTRACT: Fake-GitHub contract, conflict, retry, and idempotency tests** — planned
+ - Next: Test the adapter without live repository mutations.
+- [ ] **M2-AUTH: Repository-scoped least-privilege authentication** — planned
+ - Next: Add dedicated fine-grained credentials only after proposal behavior is reviewed.
+- [ ] **M2-WRITE-PILOT: Explicitly approved live write pilot** — planned
+ - Next: Require fresh user approval and a clean threat-model review.
+
+## M3 — Research and assessment profiles
+
+**Status:** planned
+
+Exit criteria:
+- Synthetic high-risk profiles prove blind reproduction, independence, disagreement, supersession, and human authority before real domain use.
+
+- [x] **M3-REPRODUCTION: Independent high-risk reproduction core** — done
+ - Evidence: [Independent reproduction gates](https://github.com/brendohxd/Project-Relay/commit/786648e)
+- [ ] **M3-BLIND-PACKET: Blind reproduction packet contract** — planned
+ - Next: Design and test against synthetic claims before any ITSM packet.
+- [ ] **M3-DISAGREEMENT: Adversarial review and disagreement ledger** — planned
+ - Next: Preserve incompatible findings without forced consensus.
+- [ ] **M3-SUPERSESSION: Remediation and supersession graph** — planned
+ - Next: Build on typed causal links and immutable prior records.
+- [ ] **M3-ITSM-PROFILE: ITSM operational policy profile** — planned
+ - Next: Implement only after the synthetic research profile passes.
+- [ ] **M3-EDUCATION-PROFILE: Synthetic education marking and moderation profile** — planned
+ - Next: Model rubric assessment, moderation, appeals, and named human authority without real student data.
+- [ ] **M3-HIRING-PROFILE: Synthetic hiring-assistance profile** — deferred
+ - Rationale: Hiring requires dedicated privacy, fairness, discrimination, explainability, and legal threat models; Relay must remain decision support with human authority.
+
+## M4 — Public console and integrations
+
+**Status:** planned
+
+Exit criteria:
+- Users can inspect project and task status through credential-free projections and supported client guides.
+
+- [x] **M4-STATUS: Canonical project status and generated GitHub view** — done
+ - Evidence: [Canonical project status](project/status.json), [Generated GitHub status](STATUS.md)
+- [x] **M4-CONSOLE: Visual milestone, blocker, and pilot dashboard** — done
+ - Evidence: [Read-only console](apps/console/index.html)
+- [x] **M4-PAGES-READY: GitHub Pages-ready static bundle** — done
+ - Evidence: [Pages entry point](docs/index.html)
+- [ ] **M4-PAGES-PUBLISH: Publish the visual console on GitHub Pages** — planned
+ - Next: After merge, configure Pages from main /docs and record the resulting public URL or blocker.
+- [ ] **M4-CLIENT-GUIDES: Codex, Claude, Gemini, and compatible MCP guides** — planned
+ - Next: Document read-only status and task access before any write integrations.
+- [ ] **M4-MANUAL-BRIDGE: Manual packet bridge** — planned
+ - Next: Support clients without GitHub or MCP while preserving the same evidence contract.
+- [ ] **M4-NOTIFICATIONS: Notifications and human approval inbox** — planned
+ - Next: Begin only after authenticated M2 write boundaries are proven.
+
+## M5 — Optional managed service
+
+**Status:** deferred
+
+Exit criteria:
+- Security, funding, licensing, product demand, and service responsibilities are explicitly approved before hosted development.
+
+- [ ] **M5-HOSTED: Hosted authentication, secrets, tenancy, and audit service** — deferred
+ - Rationale: No hosted service is authorised before security, funding, licensing, and demand gates are resolved.
+- [ ] **M5-BILLING: Usage controls, billing, support, and service objectives** — deferred
+ - Rationale: No paid infrastructure or billing work is required for the current local-first roadmap.
+
+# Proving grounds
+
+## ITSM proving ground
+
+**Visibility:** operational-summary-only
+**Boundary:** Relay may show workflow readiness and gate progress only. ITSM equations, claims, datasets, results, manuscripts, and scientific acceptance remain in the separate ITSM repository and knowledge base.
+
+- [x] **Relay and ITSM separation rule documented** — done
+ - Evidence: [ITSM pilot operating model](https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84)
+- [x] **Operational owner, review, reproduction, audit, and authority roles documented** — done
+ - Evidence: [ITSM pilot operating model](https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84)
+- [ ] **Synthetic research profile passes before ITSM use** — planned
+ - Next: Complete the M3 synthetic research-gate fixture.
+- [ ] **First real ITSM Relay packet** — blocked
+ - Blocker (dependency): A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task.
+ - Next: Select the pilot only after the synthetic profile passes.
diff --git a/apps/console/app.js b/apps/console/app.js
index d601cc5..6456080 100644
--- a/apps/console/app.js
+++ b/apps/console/app.js
@@ -1,4 +1,12 @@
const elements = {
+ projectStage: document.querySelector("#project-stage"),
+ currentMilestone: document.querySelector("#current-milestone"),
+ roadmapComplete: document.querySelector("#roadmap-complete"),
+ roadmapBlocked: document.querySelector("#roadmap-blocked"),
+ roadmapDeferred: document.querySelector("#roadmap-deferred"),
+ milestoneRows: document.querySelector("#milestone-rows"),
+ pilotRows: document.querySelector("#pilot-rows"),
+ projectUpdated: document.querySelector("#project-updated"),
tasks: document.querySelector("#metric-tasks"),
active: document.querySelector("#metric-active"),
evidence: document.querySelector("#metric-evidence"),
@@ -17,11 +25,65 @@ function cell(className, text) {
return node;
}
+function stateLabel(state) {
+ return state.replaceAll("_", " ");
+}
+
+function renderMilestone(milestone) {
+ const card = document.createElement("article");
+ card.className = `milestone-card milestone-${milestone.summary.state}`;
+ const heading = document.createElement("div");
+ heading.className = "milestone-heading";
+ const name = document.createElement("div");
+ const id = cell("milestone-id", milestone.id);
+ const title = document.createElement("h3");
+ title.textContent = milestone.title;
+ name.append(id, title);
+ heading.append(name, cell(`badge badge-${milestone.summary.state}`, stateLabel(milestone.summary.state)));
+ const progress = document.createElement("p");
+ progress.className = "milestone-progress";
+ progress.textContent = `${milestone.summary.completed}/${milestone.summary.actionable} actionable items complete`;
+ const list = document.createElement("ul");
+ list.className = "roadmap-items";
+ for (const item of milestone.items) {
+ const row = document.createElement("li");
+ row.className = `roadmap-item item-${item.state}`;
+ row.append(cell("item-marker", item.state === "done" ? "✓" : item.state === "blocked" ? "!" : "·"));
+ const detail = document.createElement("span");
+ const itemTitle = document.createElement("strong");
+ itemTitle.textContent = item.title;
+ detail.append(itemTitle, cell("item-state", stateLabel(item.state)));
+ if (item.blocker) detail.append(cell("item-note", item.blocker.summary));
+ else if (item.next_action && item.state !== "done") detail.append(cell("item-note", item.next_action));
+ row.append(detail);
+ list.append(row);
+ }
+ card.append(heading, progress, list);
+ return card;
+}
+
+function renderPilot(pilot) {
+ const card = document.createElement("article");
+ card.className = "pilot-card";
+ const heading = document.createElement("div");
+ heading.className = "pilot-heading";
+ const title = document.createElement("h3");
+ title.textContent = pilot.name;
+ heading.append(title, cell("badge badge-boundary", pilot.visibility));
+ const boundary = document.createElement("p");
+ boundary.className = "pilot-boundary";
+ boundary.textContent = pilot.boundary;
+ const progress = document.createElement("p");
+ progress.className = "pilot-progress";
+ progress.textContent = `${pilot.summary.completed}/${pilot.summary.actionable} operational gates complete · ${pilot.summary.counts.blocked} blocked`;
+ card.append(heading, boundary, progress);
+ return card;
+}
+
function renderTask(task) {
const row = document.createElement("article");
row.className = "task-row";
row.setAttribute("role", "row");
-
const title = document.createElement("span");
title.className = "task-title";
const strong = document.createElement("strong");
@@ -29,27 +91,17 @@ function renderTask(task) {
const small = document.createElement("small");
small.textContent = `${task.id} · ${task.history.length} transitions · policy ${task.policy_valid ? "valid" : "failed"}`;
title.append(strong, small);
-
row.append(
title,
- cell(
- `badge badge-state ${task.policy_valid ? "" : "badge-policy-failed"}`,
- task.derived_state.replaceAll("_", " ")
- ),
+ cell(`badge badge-state ${task.policy_valid ? "" : "badge-policy-failed"}`, stateLabel(task.derived_state)),
cell(`badge badge-risk-${task.risk}`, task.risk),
cell("", String(task.evidence_count)),
cell("", String(task.review_count))
);
-
const gates = document.createElement("div");
gates.className = "task-gates";
for (const [name, satisfied] of Object.entries(task.gates)) {
- gates.append(
- cell(
- `gate ${satisfied ? "gate-pass" : "gate-fail"}`,
- `${satisfied ? "✓" : "×"} ${name.replaceAll("_", " ")}`
- )
- );
+ gates.append(cell(`gate ${satisfied ? "gate-pass" : "gate-fail"}`, `${satisfied ? "✓" : "×"} ${stateLabel(name)}`));
}
row.append(gates);
return row;
@@ -59,6 +111,16 @@ try {
const response = await fetch("./state/index.json", { cache: "no-store" });
if (!response.ok) throw new Error(`State request failed with ${response.status}`);
const state = await response.json();
+ const project = state.project;
+
+ elements.projectStage.textContent = project.project.stage;
+ elements.currentMilestone.textContent = project.summary.current_milestone ?? "complete";
+ elements.roadmapComplete.textContent = `${project.summary.completed}/${project.summary.actionable}`;
+ elements.roadmapBlocked.textContent = project.summary.counts.blocked;
+ elements.roadmapDeferred.textContent = project.summary.counts.deferred;
+ elements.projectUpdated.textContent = `updated · ${project.updated_at}`;
+ elements.milestoneRows.replaceChildren(...project.milestones.map(renderMilestone));
+ elements.pilotRows.replaceChildren(...project.pilots.map(renderPilot));
elements.tasks.textContent = state.summary.tasks;
elements.active.textContent = state.summary.active;
diff --git a/apps/console/index.html b/apps/console/index.html
index 13761df..a79b4fb 100644
--- a/apps/console/index.html
+++ b/apps/console/index.html
@@ -29,8 +29,8 @@
One record for work across many minds.
reproducible evidence, explicit disagreements, and accountable decisions.
@@ -38,6 +38,33 @@ One record for work across many minds.
Tasks —
Active —
Evidence —
+
+
+
+
Project control plane
+
Roadmap and blockers
+
+
Loading status?
+
+
+
Stage ?
+
Current ?
+
Complete ?
+
Blocked ?
+
Deferred ?
+
+
+
+
+
+
+
+
Boundary-safe proving grounds
+
Pilot readiness
+
+
+
+
Reviews —
Decisions —
diff --git a/apps/console/state/index.json b/apps/console/state/index.json
index 0f7f62f..a7b1d37 100644
--- a/apps/console/state/index.json
+++ b/apps/console/state/index.json
@@ -1,6 +1,529 @@
{
"protocol_version": "0.1.0",
"generated_from": "examples/m1",
+ "project": {
+ "schema_version": "0.1.0",
+ "project": {
+ "id": "project-relay",
+ "name": "Project Relay",
+ "stage": "pre-alpha",
+ "repository": "https://github.com/brendohxd/Project-Relay"
+ },
+ "updated_at": "2026-07-19",
+ "milestones": [
+ {
+ "id": "M0",
+ "title": "Protocol kernel",
+ "exit_criteria": [
+ "The provider-neutral protocol kernel, local read-only MCP surface, fixtures, and public boundary are implemented."
+ ],
+ "items": [
+ {
+ "id": "M0-PROTOCOL",
+ "title": "Canonical schemas, hashes, and event-chain verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Foundation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-MCP",
+ "title": "Read-only local MCP server",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-PUBLIC",
+ "title": "Public-boundary scanner and repository checks",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "state": "complete",
+ "counts": {
+ "done": 3,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 0
+ },
+ "total": 3,
+ "actionable": 3,
+ "completed": 3,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M1",
+ "title": "Local vertical slice",
+ "exit_criteria": [
+ "The complete local remediation loop passes Windows and Linux verification and receives human review."
+ ],
+ "items": [
+ {
+ "id": "M1-LIFECYCLE",
+ "title": "Policy transition engine and remediation fixture",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 implementation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/d899218"
+ }
+ ]
+ },
+ {
+ "id": "M1-EVIDENCE",
+ "title": "Evidence CLI, independent review, reproduction, and human gates",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 review fixes",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M1-LOCAL-VERIFY",
+ "title": "Windows local verification with 18 tests",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Draft PR verification record",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1#issuecomment-5014591903"
+ }
+ ]
+ },
+ {
+ "id": "M1-HUMAN-REVIEW",
+ "title": "Human review of draft PR #1",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Approved and merged M1 pull request",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1"
+ }
+ ]
+ },
+ {
+ "id": "M1-LINUX-VERIFY",
+ "title": "Linux conformance verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Passing Windows and Linux M1 matrix",
+ "uri": "https://github.com/brendohxd/Project-Relay/actions/runs/29680192255"
+ }
+ ]
+ },
+ {
+ "id": "M1-MERGE",
+ "title": "Merge the reviewed M1 candidate",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 merge commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/65a7d36ea2d9d29271b970ba883baa9c5a5a2b34"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "state": "complete",
+ "counts": {
+ "done": 6,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 0
+ },
+ "total": 6,
+ "actionable": 6,
+ "completed": 6,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M2",
+ "title": "GitHub collaboration adapter",
+ "exit_criteria": [
+ "An approved Relay proposal can be translated into an idempotent, least-privilege GitHub change with conflict and failure records."
+ ],
+ "items": [
+ {
+ "id": "M2-CAPABILITIES",
+ "title": "Declared actor capabilities",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Capability foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-CAUSAL-LINKS",
+ "title": "Typed causal provenance links",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Causal-link foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-PROPOSAL",
+ "title": "Deterministic network-free GitHub mutation proposal",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Define proposal input and output contracts before authentication or writes."
+ },
+ {
+ "id": "M2-CONTRACT",
+ "title": "Fake-GitHub contract, conflict, retry, and idempotency tests",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Test the adapter without live repository mutations."
+ },
+ {
+ "id": "M2-AUTH",
+ "title": "Repository-scoped least-privilege authentication",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Add dedicated fine-grained credentials only after proposal behavior is reviewed."
+ },
+ {
+ "id": "M2-WRITE-PILOT",
+ "title": "Explicitly approved live write pilot",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Require fresh user approval and a clean threat-model review."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 2,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 4,
+ "deferred": 0
+ },
+ "total": 6,
+ "actionable": 6,
+ "completed": 2,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M3",
+ "title": "Research and assessment profiles",
+ "exit_criteria": [
+ "Synthetic high-risk profiles prove blind reproduction, independence, disagreement, supersession, and human authority before real domain use."
+ ],
+ "items": [
+ {
+ "id": "M3-REPRODUCTION",
+ "title": "Independent high-risk reproduction core",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Independent reproduction gates",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M3-BLIND-PACKET",
+ "title": "Blind reproduction packet contract",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Design and test against synthetic claims before any ITSM packet."
+ },
+ {
+ "id": "M3-DISAGREEMENT",
+ "title": "Adversarial review and disagreement ledger",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Preserve incompatible findings without forced consensus."
+ },
+ {
+ "id": "M3-SUPERSESSION",
+ "title": "Remediation and supersession graph",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Build on typed causal links and immutable prior records."
+ },
+ {
+ "id": "M3-ITSM-PROFILE",
+ "title": "ITSM operational policy profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Implement only after the synthetic research profile passes."
+ },
+ {
+ "id": "M3-EDUCATION-PROFILE",
+ "title": "Synthetic education marking and moderation profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Model rubric assessment, moderation, appeals, and named human authority without real student data."
+ },
+ {
+ "id": "M3-HIRING-PROFILE",
+ "title": "Synthetic hiring-assistance profile",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "Hiring requires dedicated privacy, fairness, discrimination, explainability, and legal threat models; Relay must remain decision support with human authority."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 1,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 5,
+ "deferred": 1
+ },
+ "total": 7,
+ "actionable": 6,
+ "completed": 1,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M4",
+ "title": "Public console and integrations",
+ "exit_criteria": [
+ "Users can inspect project and task status through credential-free projections and supported client guides."
+ ],
+ "items": [
+ {
+ "id": "M4-STATUS",
+ "title": "Canonical project status and generated GitHub view",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Canonical project status",
+ "uri": "project/status.json"
+ },
+ {
+ "label": "Generated GitHub status",
+ "uri": "STATUS.md"
+ }
+ ]
+ },
+ {
+ "id": "M4-CONSOLE",
+ "title": "Visual milestone, blocker, and pilot dashboard",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Read-only console",
+ "uri": "apps/console/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-READY",
+ "title": "GitHub Pages-ready static bundle",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Pages entry point",
+ "uri": "docs/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-PUBLISH",
+ "title": "Publish the visual console on GitHub Pages",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "After merge, configure Pages from main /docs and record the resulting public URL or blocker."
+ },
+ {
+ "id": "M4-CLIENT-GUIDES",
+ "title": "Codex, Claude, Gemini, and compatible MCP guides",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Document read-only status and task access before any write integrations."
+ },
+ {
+ "id": "M4-MANUAL-BRIDGE",
+ "title": "Manual packet bridge",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Support clients without GitHub or MCP while preserving the same evidence contract."
+ },
+ {
+ "id": "M4-NOTIFICATIONS",
+ "title": "Notifications and human approval inbox",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Begin only after authenticated M2 write boundaries are proven."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 3,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 4,
+ "deferred": 0
+ },
+ "total": 7,
+ "actionable": 7,
+ "completed": 3,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M5",
+ "title": "Optional managed service",
+ "exit_criteria": [
+ "Security, funding, licensing, product demand, and service responsibilities are explicitly approved before hosted development."
+ ],
+ "items": [
+ {
+ "id": "M5-HOSTED",
+ "title": "Hosted authentication, secrets, tenancy, and audit service",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No hosted service is authorised before security, funding, licensing, and demand gates are resolved."
+ },
+ {
+ "id": "M5-BILLING",
+ "title": "Usage controls, billing, support, and service objectives",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No paid infrastructure or billing work is required for the current local-first roadmap."
+ }
+ ],
+ "summary": {
+ "state": "deferred",
+ "counts": {
+ "done": 0,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 2
+ },
+ "total": 2,
+ "actionable": 0,
+ "completed": 0,
+ "blockers": []
+ }
+ }
+ ],
+ "pilots": [
+ {
+ "id": "itsm",
+ "name": "ITSM proving ground",
+ "visibility": "operational-summary-only",
+ "boundary": "Relay may show workflow readiness and gate progress only. ITSM equations, claims, datasets, results, manuscripts, and scientific acceptance remain in the separate ITSM repository and knowledge base.",
+ "items": [
+ {
+ "id": "ITSM-BOUNDARY",
+ "title": "Relay and ITSM separation rule documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-ROLES",
+ "title": "Operational owner, review, reproduction, audit, and authority roles documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-SYNTHETIC",
+ "title": "Synthetic research profile passes before ITSM use",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Complete the M3 synthetic research-gate fixture."
+ },
+ {
+ "id": "ITSM-REAL-PILOT",
+ "title": "First real ITSM Relay packet",
+ "state": "blocked",
+ "evidence": [],
+ "blocker": {
+ "kind": "dependency",
+ "summary": "A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task."
+ },
+ "next_action": "Select the pilot only after the synthetic profile passes."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 2,
+ "in_progress": 0,
+ "blocked": 1,
+ "planned": 1,
+ "deferred": 0
+ },
+ "total": 4,
+ "actionable": 4,
+ "completed": 2,
+ "blockers": [
+ {
+ "id": "ITSM-REAL-PILOT",
+ "title": "First real ITSM Relay packet",
+ "state": "blocked",
+ "evidence": [],
+ "blocker": {
+ "kind": "dependency",
+ "summary": "A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task."
+ },
+ "next_action": "Select the pilot only after the synthetic profile passes."
+ }
+ ]
+ }
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 15,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 13,
+ "deferred": 3
+ },
+ "total": 31,
+ "actionable": 28,
+ "completed": 15,
+ "blockers": [],
+ "current_milestone": "M2",
+ "current_milestone_title": "GitHub collaboration adapter"
+ }
+ },
"summary": {
"tasks": 1,
"active": 0,
diff --git a/apps/console/styles.css b/apps/console/styles.css
index 9520fcc..782a7a2 100644
--- a/apps/console/styles.css
+++ b/apps/console/styles.css
@@ -106,3 +106,39 @@ footer { border-top: 1px solid var(--line); color: var(--muted); display: flex;
.flow li { min-height: 150px; }
.flow li > span { margin-top: 36px; }
}
+
+.project-status { padding: 88px 0 24px; }
+.pilot-section { padding: 72px 0 24px; }
+.project-metrics { border: 1px solid var(--line); display: grid; grid-template-columns: repeat(5, 1fr); margin-bottom: 22px; }
+.project-metrics article { border-right: 1px solid var(--line); padding: 18px; }
+.project-metrics article:last-child { border-right: 0; }
+.project-metrics span, .item-state { color: var(--muted); display: block; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
+.project-metrics strong { display: block; font-size: 24px; margin-top: 7px; }
+.milestone-grid { display: grid; gap: 12px; grid-template-columns: repeat(2, 1fr); }
+.milestone-card, .pilot-card { background: var(--panel); border: 1px solid var(--line); padding: 22px; }
+.milestone-complete { border-top-color: rgba(185, 247, 124, .55); }
+.milestone-blocked { border-top-color: rgba(255, 107, 53, .65); }
+.milestone-heading, .pilot-heading { align-items: start; display: flex; gap: 16px; justify-content: space-between; }
+.milestone-id { color: var(--orange); font-family: ui-monospace, monospace; font-size: 11px; }
+h3 { font-size: 19px; margin: 5px 0 0; }
+.milestone-progress, .pilot-progress { color: var(--muted); font-size: 12px; }
+.badge-complete { border-color: rgba(185, 247, 124, .4); color: var(--lime); }
+.badge-blocked { border-color: rgba(255, 107, 53, .5); color: var(--orange); }
+.badge-deferred { color: var(--muted); }
+.badge-boundary { border-color: rgba(121, 168, 255, .4); color: var(--blue); }
+.roadmap-items { list-style: none; margin: 20px 0 0; padding: 0; }
+.roadmap-item { align-items: start; border-top: 1px solid var(--line); display: grid; gap: 10px; grid-template-columns: 18px 1fr; padding: 12px 0; }
+.roadmap-item strong { display: block; font-size: 12px; }
+.item-marker { color: var(--muted); font-family: ui-monospace, monospace; }
+.item-done .item-marker { color: var(--lime); }
+.item-blocked .item-marker { color: var(--orange); }
+.item-state { margin-top: 4px; }
+.item-note { color: var(--muted); display: block; font-size: 11px; line-height: 1.4; margin-top: 6px; }
+.pilot-grid { display: grid; gap: 12px; }
+.pilot-boundary { border-left: 2px solid var(--blue); color: var(--muted); font-size: 13px; line-height: 1.55; padding-left: 14px; }
+
+@media (max-width: 800px) {
+ .project-metrics { grid-template-columns: repeat(2, 1fr); }
+ .project-metrics article { border-bottom: 1px solid var(--line); }
+ .milestone-grid { grid-template-columns: 1fr; }
+ .milestone-heading, .pilot-heading { align-items: start; flex-direction: column; }
diff --git a/apps/mcp-server/src/index.js b/apps/mcp-server/src/index.js
index 81654c7..f861884 100644
--- a/apps/mcp-server/src/index.js
+++ b/apps/mcp-server/src/index.js
@@ -13,10 +13,13 @@ import {
validateWorkspace
} from "@project-relay/protocol";
import { z } from "zod";
+import { loadProjectStatus } from "../../../scripts/project-status-lib.mjs";
const VERSION = "0.1.0";
const workspace = path.resolve(process.env.RELAY_WORKSPACE ?? process.cwd());
const taskIdPattern = /^[A-Z][A-Z0-9_-]{1,31}-[0-9]{4,}$/;
+const projectStatusFile = path.resolve(process.env.RELAY_PROJECT_STATUS ?? "project/status.json");
+const projectStatusSchema = path.resolve(process.env.RELAY_PROJECT_STATUS_SCHEMA ?? "project/status.schema.json");
const eventTypes = [
"task.created",
"task.ready",
@@ -241,6 +244,44 @@ server.registerTool(
return result({ event, validation }, !validation.valid);
}
);
+server.registerTool(
+ "relay_get_project_status",
+ {
+ title: "Get Project Relay status",
+ description: "Read the validated roadmap, blockers, and operational-only pilot summaries.",
+ inputSchema: {}
+ },
+ async () => {
+ try {
+ return result(await loadProjectStatus(projectStatusFile, projectStatusSchema));
+ } catch (error) {
+ return result({ error: error.message, issues: error.issues ?? [] }, true);
+ }
+ }
+);
+
+server.registerResource(
+ "relay-project-status",
+ "relay://project/status",
+ {
+ title: "Project Relay status",
+ description: "Validated project roadmap and operational-only proving-ground status.",
+ mimeType: "application/json"
+ },
+ async (uri) => {
+ const status = await loadProjectStatus(projectStatusFile, projectStatusSchema);
+ return {
+ contents: [
+ {
+ uri: uri.href,
+ mimeType: "application/json",
+ text: JSON.stringify(status, null, 2)
+ }
+ ]
+ };
+ }
+);
+
const transport = new StdioServerTransport();
await server.connect(transport);
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/docs/app.js b/docs/app.js
new file mode 100644
index 0000000..6456080
--- /dev/null
+++ b/docs/app.js
@@ -0,0 +1,137 @@
+const elements = {
+ projectStage: document.querySelector("#project-stage"),
+ currentMilestone: document.querySelector("#current-milestone"),
+ roadmapComplete: document.querySelector("#roadmap-complete"),
+ roadmapBlocked: document.querySelector("#roadmap-blocked"),
+ roadmapDeferred: document.querySelector("#roadmap-deferred"),
+ milestoneRows: document.querySelector("#milestone-rows"),
+ pilotRows: document.querySelector("#pilot-rows"),
+ projectUpdated: document.querySelector("#project-updated"),
+ tasks: document.querySelector("#metric-tasks"),
+ active: document.querySelector("#metric-active"),
+ evidence: document.querySelector("#metric-evidence"),
+ reviews: document.querySelector("#metric-reviews"),
+ decisions: document.querySelector("#metric-decisions"),
+ rows: document.querySelector("#task-rows"),
+ source: document.querySelector("#source-note"),
+ error: document.querySelector("#load-error"),
+ version: document.querySelector("#protocol-version")
+};
+
+function cell(className, text) {
+ const node = document.createElement("span");
+ if (className) node.className = className;
+ node.textContent = text;
+ return node;
+}
+
+function stateLabel(state) {
+ return state.replaceAll("_", " ");
+}
+
+function renderMilestone(milestone) {
+ const card = document.createElement("article");
+ card.className = `milestone-card milestone-${milestone.summary.state}`;
+ const heading = document.createElement("div");
+ heading.className = "milestone-heading";
+ const name = document.createElement("div");
+ const id = cell("milestone-id", milestone.id);
+ const title = document.createElement("h3");
+ title.textContent = milestone.title;
+ name.append(id, title);
+ heading.append(name, cell(`badge badge-${milestone.summary.state}`, stateLabel(milestone.summary.state)));
+ const progress = document.createElement("p");
+ progress.className = "milestone-progress";
+ progress.textContent = `${milestone.summary.completed}/${milestone.summary.actionable} actionable items complete`;
+ const list = document.createElement("ul");
+ list.className = "roadmap-items";
+ for (const item of milestone.items) {
+ const row = document.createElement("li");
+ row.className = `roadmap-item item-${item.state}`;
+ row.append(cell("item-marker", item.state === "done" ? "✓" : item.state === "blocked" ? "!" : "·"));
+ const detail = document.createElement("span");
+ const itemTitle = document.createElement("strong");
+ itemTitle.textContent = item.title;
+ detail.append(itemTitle, cell("item-state", stateLabel(item.state)));
+ if (item.blocker) detail.append(cell("item-note", item.blocker.summary));
+ else if (item.next_action && item.state !== "done") detail.append(cell("item-note", item.next_action));
+ row.append(detail);
+ list.append(row);
+ }
+ card.append(heading, progress, list);
+ return card;
+}
+
+function renderPilot(pilot) {
+ const card = document.createElement("article");
+ card.className = "pilot-card";
+ const heading = document.createElement("div");
+ heading.className = "pilot-heading";
+ const title = document.createElement("h3");
+ title.textContent = pilot.name;
+ heading.append(title, cell("badge badge-boundary", pilot.visibility));
+ const boundary = document.createElement("p");
+ boundary.className = "pilot-boundary";
+ boundary.textContent = pilot.boundary;
+ const progress = document.createElement("p");
+ progress.className = "pilot-progress";
+ progress.textContent = `${pilot.summary.completed}/${pilot.summary.actionable} operational gates complete · ${pilot.summary.counts.blocked} blocked`;
+ card.append(heading, boundary, progress);
+ return card;
+}
+
+function renderTask(task) {
+ const row = document.createElement("article");
+ row.className = "task-row";
+ row.setAttribute("role", "row");
+ const title = document.createElement("span");
+ title.className = "task-title";
+ const strong = document.createElement("strong");
+ strong.textContent = task.title;
+ const small = document.createElement("small");
+ small.textContent = `${task.id} · ${task.history.length} transitions · policy ${task.policy_valid ? "valid" : "failed"}`;
+ title.append(strong, small);
+ row.append(
+ title,
+ cell(`badge badge-state ${task.policy_valid ? "" : "badge-policy-failed"}`, stateLabel(task.derived_state)),
+ cell(`badge badge-risk-${task.risk}`, task.risk),
+ cell("", String(task.evidence_count)),
+ cell("", String(task.review_count))
+ );
+ const gates = document.createElement("div");
+ gates.className = "task-gates";
+ for (const [name, satisfied] of Object.entries(task.gates)) {
+ gates.append(cell(`gate ${satisfied ? "gate-pass" : "gate-fail"}`, `${satisfied ? "✓" : "×"} ${stateLabel(name)}`));
+ }
+ row.append(gates);
+ return row;
+}
+
+try {
+ const response = await fetch("./state/index.json", { cache: "no-store" });
+ if (!response.ok) throw new Error(`State request failed with ${response.status}`);
+ const state = await response.json();
+ const project = state.project;
+
+ elements.projectStage.textContent = project.project.stage;
+ elements.currentMilestone.textContent = project.summary.current_milestone ?? "complete";
+ elements.roadmapComplete.textContent = `${project.summary.completed}/${project.summary.actionable}`;
+ elements.roadmapBlocked.textContent = project.summary.counts.blocked;
+ elements.roadmapDeferred.textContent = project.summary.counts.deferred;
+ elements.projectUpdated.textContent = `updated · ${project.updated_at}`;
+ elements.milestoneRows.replaceChildren(...project.milestones.map(renderMilestone));
+ elements.pilotRows.replaceChildren(...project.pilots.map(renderPilot));
+
+ elements.tasks.textContent = state.summary.tasks;
+ elements.active.textContent = state.summary.active;
+ elements.evidence.textContent = state.summary.evidence_bundles;
+ elements.reviews.textContent = state.summary.reviews;
+ elements.decisions.textContent = state.summary.decisions;
+ elements.source.textContent = `source · ${state.generated_from}`;
+ elements.version.textContent = `Protocol ${state.protocol_version}`;
+ elements.rows.replaceChildren(...state.tasks.map(renderTask));
+} catch (error) {
+ elements.source.textContent = "state unavailable";
+ elements.error.hidden = false;
+ elements.error.textContent = `${error.message}. Serve this directory over HTTP rather than opening index.html directly.`;
+}
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..a79b4fb
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+ Project Relay
+
+
+
+
+
+
+
+
+ Evidence before consensus
+ One record for work across many minds.
+
+ Relay turns disconnected AI sessions and human reviews into bounded tasks,
+ reproducible evidence, explicit disagreements, and accountable decisions.
+
+
+
+
+
+ Tasks —
+ Active —
+ Evidence —
+
+
+
+
Project control plane
+
Roadmap and blockers
+
+
Loading status?
+
+
+
Stage ?
+
Current ?
+
Complete ?
+
Blocked ?
+
Deferred ?
+
+
+
+
+
+
+
+
Boundary-safe proving grounds
+
Pilot readiness
+
+
+
+
+ Reviews —
+ Decisions —
+
+
+
+
+
+
Canonical projection
+
Current tasks
+
+
Loading state…
+
+
+
+
+
+
+
+
+
+
Default evidence gate
+
From question to decision
+
+
+
+ 01 Bound the task One question, inputs, constraints, and acceptance criteria.
+ 02 Submit evidence Methods, commands, environment, artifacts, failures, and hashes.
+ 03 Review independently Reproduce where required and preserve material disagreement.
+ 04 Record authority A named human accepts, remediates, rejects, or defers.
+
+
+
+
+
+
+
+ Project Relay · working title
+ Protocol —
+
+
+
+
+
diff --git a/docs/state/index.json b/docs/state/index.json
new file mode 100644
index 0000000..a7b1d37
--- /dev/null
+++ b/docs/state/index.json
@@ -0,0 +1,645 @@
+{
+ "protocol_version": "0.1.0",
+ "generated_from": "examples/m1",
+ "project": {
+ "schema_version": "0.1.0",
+ "project": {
+ "id": "project-relay",
+ "name": "Project Relay",
+ "stage": "pre-alpha",
+ "repository": "https://github.com/brendohxd/Project-Relay"
+ },
+ "updated_at": "2026-07-19",
+ "milestones": [
+ {
+ "id": "M0",
+ "title": "Protocol kernel",
+ "exit_criteria": [
+ "The provider-neutral protocol kernel, local read-only MCP surface, fixtures, and public boundary are implemented."
+ ],
+ "items": [
+ {
+ "id": "M0-PROTOCOL",
+ "title": "Canonical schemas, hashes, and event-chain verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Foundation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-MCP",
+ "title": "Read-only local MCP server",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-PUBLIC",
+ "title": "Public-boundary scanner and repository checks",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "state": "complete",
+ "counts": {
+ "done": 3,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 0
+ },
+ "total": 3,
+ "actionable": 3,
+ "completed": 3,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M1",
+ "title": "Local vertical slice",
+ "exit_criteria": [
+ "The complete local remediation loop passes Windows and Linux verification and receives human review."
+ ],
+ "items": [
+ {
+ "id": "M1-LIFECYCLE",
+ "title": "Policy transition engine and remediation fixture",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 implementation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/d899218"
+ }
+ ]
+ },
+ {
+ "id": "M1-EVIDENCE",
+ "title": "Evidence CLI, independent review, reproduction, and human gates",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 review fixes",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M1-LOCAL-VERIFY",
+ "title": "Windows local verification with 18 tests",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Draft PR verification record",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1#issuecomment-5014591903"
+ }
+ ]
+ },
+ {
+ "id": "M1-HUMAN-REVIEW",
+ "title": "Human review of draft PR #1",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Approved and merged M1 pull request",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1"
+ }
+ ]
+ },
+ {
+ "id": "M1-LINUX-VERIFY",
+ "title": "Linux conformance verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Passing Windows and Linux M1 matrix",
+ "uri": "https://github.com/brendohxd/Project-Relay/actions/runs/29680192255"
+ }
+ ]
+ },
+ {
+ "id": "M1-MERGE",
+ "title": "Merge the reviewed M1 candidate",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 merge commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/65a7d36ea2d9d29271b970ba883baa9c5a5a2b34"
+ }
+ ]
+ }
+ ],
+ "summary": {
+ "state": "complete",
+ "counts": {
+ "done": 6,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 0
+ },
+ "total": 6,
+ "actionable": 6,
+ "completed": 6,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M2",
+ "title": "GitHub collaboration adapter",
+ "exit_criteria": [
+ "An approved Relay proposal can be translated into an idempotent, least-privilege GitHub change with conflict and failure records."
+ ],
+ "items": [
+ {
+ "id": "M2-CAPABILITIES",
+ "title": "Declared actor capabilities",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Capability foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-CAUSAL-LINKS",
+ "title": "Typed causal provenance links",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Causal-link foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-PROPOSAL",
+ "title": "Deterministic network-free GitHub mutation proposal",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Define proposal input and output contracts before authentication or writes."
+ },
+ {
+ "id": "M2-CONTRACT",
+ "title": "Fake-GitHub contract, conflict, retry, and idempotency tests",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Test the adapter without live repository mutations."
+ },
+ {
+ "id": "M2-AUTH",
+ "title": "Repository-scoped least-privilege authentication",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Add dedicated fine-grained credentials only after proposal behavior is reviewed."
+ },
+ {
+ "id": "M2-WRITE-PILOT",
+ "title": "Explicitly approved live write pilot",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Require fresh user approval and a clean threat-model review."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 2,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 4,
+ "deferred": 0
+ },
+ "total": 6,
+ "actionable": 6,
+ "completed": 2,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M3",
+ "title": "Research and assessment profiles",
+ "exit_criteria": [
+ "Synthetic high-risk profiles prove blind reproduction, independence, disagreement, supersession, and human authority before real domain use."
+ ],
+ "items": [
+ {
+ "id": "M3-REPRODUCTION",
+ "title": "Independent high-risk reproduction core",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Independent reproduction gates",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M3-BLIND-PACKET",
+ "title": "Blind reproduction packet contract",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Design and test against synthetic claims before any ITSM packet."
+ },
+ {
+ "id": "M3-DISAGREEMENT",
+ "title": "Adversarial review and disagreement ledger",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Preserve incompatible findings without forced consensus."
+ },
+ {
+ "id": "M3-SUPERSESSION",
+ "title": "Remediation and supersession graph",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Build on typed causal links and immutable prior records."
+ },
+ {
+ "id": "M3-ITSM-PROFILE",
+ "title": "ITSM operational policy profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Implement only after the synthetic research profile passes."
+ },
+ {
+ "id": "M3-EDUCATION-PROFILE",
+ "title": "Synthetic education marking and moderation profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Model rubric assessment, moderation, appeals, and named human authority without real student data."
+ },
+ {
+ "id": "M3-HIRING-PROFILE",
+ "title": "Synthetic hiring-assistance profile",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "Hiring requires dedicated privacy, fairness, discrimination, explainability, and legal threat models; Relay must remain decision support with human authority."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 1,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 5,
+ "deferred": 1
+ },
+ "total": 7,
+ "actionable": 6,
+ "completed": 1,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M4",
+ "title": "Public console and integrations",
+ "exit_criteria": [
+ "Users can inspect project and task status through credential-free projections and supported client guides."
+ ],
+ "items": [
+ {
+ "id": "M4-STATUS",
+ "title": "Canonical project status and generated GitHub view",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Canonical project status",
+ "uri": "project/status.json"
+ },
+ {
+ "label": "Generated GitHub status",
+ "uri": "STATUS.md"
+ }
+ ]
+ },
+ {
+ "id": "M4-CONSOLE",
+ "title": "Visual milestone, blocker, and pilot dashboard",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Read-only console",
+ "uri": "apps/console/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-READY",
+ "title": "GitHub Pages-ready static bundle",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Pages entry point",
+ "uri": "docs/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-PUBLISH",
+ "title": "Publish the visual console on GitHub Pages",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "After merge, configure Pages from main /docs and record the resulting public URL or blocker."
+ },
+ {
+ "id": "M4-CLIENT-GUIDES",
+ "title": "Codex, Claude, Gemini, and compatible MCP guides",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Document read-only status and task access before any write integrations."
+ },
+ {
+ "id": "M4-MANUAL-BRIDGE",
+ "title": "Manual packet bridge",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Support clients without GitHub or MCP while preserving the same evidence contract."
+ },
+ {
+ "id": "M4-NOTIFICATIONS",
+ "title": "Notifications and human approval inbox",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Begin only after authenticated M2 write boundaries are proven."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 3,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 4,
+ "deferred": 0
+ },
+ "total": 7,
+ "actionable": 7,
+ "completed": 3,
+ "blockers": []
+ }
+ },
+ {
+ "id": "M5",
+ "title": "Optional managed service",
+ "exit_criteria": [
+ "Security, funding, licensing, product demand, and service responsibilities are explicitly approved before hosted development."
+ ],
+ "items": [
+ {
+ "id": "M5-HOSTED",
+ "title": "Hosted authentication, secrets, tenancy, and audit service",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No hosted service is authorised before security, funding, licensing, and demand gates are resolved."
+ },
+ {
+ "id": "M5-BILLING",
+ "title": "Usage controls, billing, support, and service objectives",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No paid infrastructure or billing work is required for the current local-first roadmap."
+ }
+ ],
+ "summary": {
+ "state": "deferred",
+ "counts": {
+ "done": 0,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 0,
+ "deferred": 2
+ },
+ "total": 2,
+ "actionable": 0,
+ "completed": 0,
+ "blockers": []
+ }
+ }
+ ],
+ "pilots": [
+ {
+ "id": "itsm",
+ "name": "ITSM proving ground",
+ "visibility": "operational-summary-only",
+ "boundary": "Relay may show workflow readiness and gate progress only. ITSM equations, claims, datasets, results, manuscripts, and scientific acceptance remain in the separate ITSM repository and knowledge base.",
+ "items": [
+ {
+ "id": "ITSM-BOUNDARY",
+ "title": "Relay and ITSM separation rule documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-ROLES",
+ "title": "Operational owner, review, reproduction, audit, and authority roles documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-SYNTHETIC",
+ "title": "Synthetic research profile passes before ITSM use",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Complete the M3 synthetic research-gate fixture."
+ },
+ {
+ "id": "ITSM-REAL-PILOT",
+ "title": "First real ITSM Relay packet",
+ "state": "blocked",
+ "evidence": [],
+ "blocker": {
+ "kind": "dependency",
+ "summary": "A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task."
+ },
+ "next_action": "Select the pilot only after the synthetic profile passes."
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 2,
+ "in_progress": 0,
+ "blocked": 1,
+ "planned": 1,
+ "deferred": 0
+ },
+ "total": 4,
+ "actionable": 4,
+ "completed": 2,
+ "blockers": [
+ {
+ "id": "ITSM-REAL-PILOT",
+ "title": "First real ITSM Relay packet",
+ "state": "blocked",
+ "evidence": [],
+ "blocker": {
+ "kind": "dependency",
+ "summary": "A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task."
+ },
+ "next_action": "Select the pilot only after the synthetic profile passes."
+ }
+ ]
+ }
+ }
+ ],
+ "summary": {
+ "state": "planned",
+ "counts": {
+ "done": 15,
+ "in_progress": 0,
+ "blocked": 0,
+ "planned": 13,
+ "deferred": 3
+ },
+ "total": 31,
+ "actionable": 28,
+ "completed": 15,
+ "blockers": [],
+ "current_milestone": "M2",
+ "current_milestone_title": "GitHub collaboration adapter"
+ }
+ },
+ "summary": {
+ "tasks": 1,
+ "active": 0,
+ "evidence_bundles": 2,
+ "reviews": 2,
+ "decisions": 1,
+ "failed_gates": 0
+ },
+ "tasks": [
+ {
+ "id": "RELAY-1001",
+ "title": "Exercise the default remediation policy",
+ "question": "Can Relay preserve a failed independent review, require remediation, and record human-authorised acceptance?",
+ "state": "accepted",
+ "derived_state": "accepted",
+ "policy_valid": true,
+ "gates": {
+ "evidence_present": true,
+ "independent_review": true,
+ "independent_reproduction": true,
+ "remediation_recorded": true,
+ "human_decision": true,
+ "final_state_consistent": true
+ },
+ "history": [
+ {
+ "sequence": 1,
+ "event_id": "EVT-m1event0001",
+ "type": "task.created",
+ "from_state": null,
+ "to_state": "draft",
+ "occurred_at": "2026-07-19T01:00:00Z"
+ },
+ {
+ "sequence": 2,
+ "event_id": "EVT-m1event0002",
+ "type": "task.ready",
+ "from_state": "draft",
+ "to_state": "ready",
+ "occurred_at": "2026-07-19T01:01:00Z"
+ },
+ {
+ "sequence": 3,
+ "event_id": "EVT-m1event0003",
+ "type": "task.claimed",
+ "from_state": "ready",
+ "to_state": "claimed",
+ "occurred_at": "2026-07-19T01:02:00Z"
+ },
+ {
+ "sequence": 4,
+ "event_id": "EVT-m1event0004",
+ "type": "task.started",
+ "from_state": "claimed",
+ "to_state": "in_progress",
+ "occurred_at": "2026-07-19T01:03:00Z"
+ },
+ {
+ "sequence": 5,
+ "event_id": "EVT-m1event0005",
+ "type": "task.submitted",
+ "from_state": "in_progress",
+ "to_state": "submitted",
+ "occurred_at": "2026-07-19T01:05:00Z"
+ },
+ {
+ "sequence": 6,
+ "event_id": "EVT-m1event0006",
+ "type": "review.requested",
+ "from_state": "submitted",
+ "to_state": "under_review",
+ "occurred_at": "2026-07-19T01:06:00Z"
+ },
+ {
+ "sequence": 8,
+ "event_id": "EVT-m1event0008",
+ "type": "remediation.requested",
+ "from_state": "under_review",
+ "to_state": "remediation",
+ "occurred_at": "2026-07-19T01:08:00Z"
+ },
+ {
+ "sequence": 9,
+ "event_id": "EVT-m1event0009",
+ "type": "task.submitted",
+ "from_state": "remediation",
+ "to_state": "submitted",
+ "occurred_at": "2026-07-19T01:10:00Z"
+ },
+ {
+ "sequence": 10,
+ "event_id": "EVT-m1event0010",
+ "type": "review.requested",
+ "from_state": "submitted",
+ "to_state": "under_review",
+ "occurred_at": "2026-07-19T01:11:00Z"
+ },
+ {
+ "sequence": 12,
+ "event_id": "EVT-m1event0012",
+ "type": "decision.recorded",
+ "from_state": "under_review",
+ "to_state": "accepted",
+ "occurred_at": "2026-07-19T01:13:00Z"
+ }
+ ],
+ "risk": "moderate",
+ "owner": {
+ "id": "model:builder",
+ "type": "model",
+ "role": "implementation"
+ },
+ "reviewer_count": 1,
+ "evidence_count": 2,
+ "review_count": 2,
+ "decision_count": 1
+ }
+ ]
+}
diff --git a/docs/styles.css b/docs/styles.css
new file mode 100644
index 0000000..782a7a2
--- /dev/null
+++ b/docs/styles.css
@@ -0,0 +1,144 @@
+:root {
+ color-scheme: dark;
+ --ink: #f3f1eb;
+ --muted: #a7a49a;
+ --line: rgba(255, 255, 255, 0.12);
+ --panel: rgba(255, 255, 255, 0.035);
+ --orange: #ff6b35;
+ --lime: #b9f77c;
+ --blue: #79a8ff;
+ --page: #0b0d0e;
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0;
+ background:
+ radial-gradient(circle at 83% 4%, rgba(121, 168, 255, 0.12), transparent 25rem),
+ radial-gradient(circle at 15% 18%, rgba(255, 107, 53, 0.08), transparent 28rem),
+ var(--page);
+ color: var(--ink);
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ min-height: 100vh;
+}
+
+a { color: inherit; }
+.page-shell { width: min(1180px, calc(100% - 40px)); margin: 0 auto; }
+.topbar {
+ align-items: center;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ justify-content: space-between;
+ padding: 24px 0;
+}
+.brand { align-items: center; display: flex; font-size: 15px; font-weight: 700; gap: 12px; text-decoration: none; }
+.brand-mark { display: flex; gap: 3px; height: 22px; transform: skewX(-14deg); }
+.brand-mark i { background: var(--orange); display: block; width: 5px; }
+.brand-mark i:nth-child(2) { background: var(--lime); }
+.brand-mark i:nth-child(3) { background: var(--blue); }
+.status-pill {
+ align-items: center;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ color: var(--muted);
+ display: flex;
+ font-size: 12px;
+ gap: 8px;
+ padding: 8px 12px;
+}
+.status-pill span { background: var(--lime); border-radius: 50%; box-shadow: 0 0 12px var(--lime); height: 7px; width: 7px; }
+.hero { padding: 108px 0 88px; }
+.eyebrow { color: var(--orange); font-family: ui-monospace, monospace; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; }
+h1 { font-size: clamp(48px, 8.3vw, 104px); letter-spacing: -.065em; line-height: .9; margin: 23px 0 30px; max-width: 1000px; }
+.hero p { color: var(--muted); font-size: clamp(17px, 2vw, 21px); line-height: 1.55; margin: 0; max-width: 750px; }
+.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 38px; }
+.hero-actions a { border-radius: 3px; font-size: 13px; font-weight: 700; padding: 13px 18px; text-decoration: none; }
+.primary-action { background: var(--ink); color: #111; }
+.secondary-action { border: 1px solid var(--line); }
+.metrics { border-bottom: 1px solid var(--line); border-top: 1px solid var(--line); display: grid; grid-template-columns: repeat(5, 1fr); }
+.metrics article { border-right: 1px solid var(--line); padding: 24px 18px; }
+.metrics article:first-child { padding-left: 0; }
+.metrics article:last-child { border-right: 0; }
+.metrics span { color: var(--muted); display: block; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
+.metrics strong { display: block; font-size: 31px; margin-top: 8px; }
+.workspace, .flow-section { padding: 90px 0 16px; }
+.section-heading { align-items: end; display: flex; justify-content: space-between; margin-bottom: 28px; }
+h2 { font-size: clamp(28px, 4vw, 44px); letter-spacing: -.035em; margin: 8px 0 0; }
+.source-note { color: var(--muted); font-family: ui-monospace, monospace; font-size: 11px; }
+.task-table { border-bottom: 1px solid var(--line); border-top: 1px solid var(--line); }
+.task-row { align-items: center; display: grid; gap: 16px; grid-template-columns: minmax(280px, 1fr) 120px 90px 80px 80px; min-height: 72px; padding: 0 14px; }
+.task-row + .task-row { border-top: 1px solid var(--line); }
+.task-header { color: var(--muted); font-size: 10px; letter-spacing: .12em; min-height: 42px; text-transform: uppercase; }
+.task-title strong, .task-title small { display: block; }
+.task-title strong { font-size: 14px; }
+.task-title small { color: var(--muted); font-family: ui-monospace, monospace; margin-top: 5px; }
+.badge { border: 1px solid var(--line); border-radius: 999px; display: inline-flex; font-family: ui-monospace, monospace; font-size: 10px; padding: 6px 9px; text-transform: uppercase; width: fit-content; }
+.badge-state { border-color: rgba(121, 168, 255, .4); color: var(--blue); }
+.badge-policy-failed { border-color: rgba(255, 107, 53, .5); color: var(--orange); }
+.badge-risk-high, .badge-risk-critical { border-color: rgba(255, 107, 53, .5); color: var(--orange); }
+.task-gates { display: flex; flex-wrap: wrap; gap: 7px; grid-column: 1 / -1; padding-bottom: 14px; }
+.gate { border: 1px solid var(--line); border-radius: 999px; font-family: ui-monospace, monospace; font-size: 9px; letter-spacing: .04em; padding: 5px 8px; text-transform: uppercase; }
+.gate-pass { border-color: rgba(185, 247, 124, .32); color: var(--lime); }
+.gate-fail { border-color: rgba(255, 107, 53, .5); color: var(--orange); }
+.load-error { background: rgba(255, 107, 53, .09); border: 1px solid rgba(255, 107, 53, .35); color: #ffc2ac; padding: 16px; }
+.flow { counter-reset: relay; display: grid; gap: 1px; grid-template-columns: repeat(4, 1fr); list-style: none; margin: 0; padding: 0; }
+.flow li { background: var(--panel); min-height: 210px; padding: 22px; }
+.flow b { color: var(--orange); font-family: ui-monospace, monospace; font-size: 11px; }
+.flow li > span { color: var(--muted); display: block; font-size: 13px; line-height: 1.55; margin-top: 72px; }
+.flow strong { color: var(--ink); display: block; font-size: 15px; margin-bottom: 7px; }
+.principle { align-items: start; border-left: 3px solid var(--lime); display: grid; gap: 25px; grid-template-columns: 60px 1fr; margin: 90px 0; padding: 4px 0 4px 24px; }
+.principle-index { color: var(--lime); font-family: ui-monospace, monospace; font-size: 11px; }
+.principle p { font-size: clamp(22px, 4vw, 40px); letter-spacing: -.035em; line-height: 1.13; margin: 0; max-width: 900px; }
+footer { border-top: 1px solid var(--line); color: var(--muted); display: flex; font-size: 11px; justify-content: space-between; padding: 26px 0 42px; }
+
+@media (max-width: 800px) {
+ .page-shell { width: min(100% - 28px, 1180px); }
+ .hero { padding: 80px 0 64px; }
+ .metrics { grid-template-columns: repeat(2, 1fr); }
+ .metrics article { border-bottom: 1px solid var(--line); }
+ .metrics article:nth-child(2n) { border-right: 0; }
+ .metrics article:first-child { padding-left: 18px; }
+ .task-header { display: none; }
+ .task-row { gap: 12px; grid-template-columns: 1fr 1fr; padding: 18px 6px; }
+ .task-title { grid-column: 1 / -1; }
+ .flow { grid-template-columns: 1fr; }
+ .flow li { min-height: 150px; }
+ .flow li > span { margin-top: 36px; }
+}
+
+.project-status { padding: 88px 0 24px; }
+.pilot-section { padding: 72px 0 24px; }
+.project-metrics { border: 1px solid var(--line); display: grid; grid-template-columns: repeat(5, 1fr); margin-bottom: 22px; }
+.project-metrics article { border-right: 1px solid var(--line); padding: 18px; }
+.project-metrics article:last-child { border-right: 0; }
+.project-metrics span, .item-state { color: var(--muted); display: block; font-size: 10px; letter-spacing: .08em; text-transform: uppercase; }
+.project-metrics strong { display: block; font-size: 24px; margin-top: 7px; }
+.milestone-grid { display: grid; gap: 12px; grid-template-columns: repeat(2, 1fr); }
+.milestone-card, .pilot-card { background: var(--panel); border: 1px solid var(--line); padding: 22px; }
+.milestone-complete { border-top-color: rgba(185, 247, 124, .55); }
+.milestone-blocked { border-top-color: rgba(255, 107, 53, .65); }
+.milestone-heading, .pilot-heading { align-items: start; display: flex; gap: 16px; justify-content: space-between; }
+.milestone-id { color: var(--orange); font-family: ui-monospace, monospace; font-size: 11px; }
+h3 { font-size: 19px; margin: 5px 0 0; }
+.milestone-progress, .pilot-progress { color: var(--muted); font-size: 12px; }
+.badge-complete { border-color: rgba(185, 247, 124, .4); color: var(--lime); }
+.badge-blocked { border-color: rgba(255, 107, 53, .5); color: var(--orange); }
+.badge-deferred { color: var(--muted); }
+.badge-boundary { border-color: rgba(121, 168, 255, .4); color: var(--blue); }
+.roadmap-items { list-style: none; margin: 20px 0 0; padding: 0; }
+.roadmap-item { align-items: start; border-top: 1px solid var(--line); display: grid; gap: 10px; grid-template-columns: 18px 1fr; padding: 12px 0; }
+.roadmap-item strong { display: block; font-size: 12px; }
+.item-marker { color: var(--muted); font-family: ui-monospace, monospace; }
+.item-done .item-marker { color: var(--lime); }
+.item-blocked .item-marker { color: var(--orange); }
+.item-state { margin-top: 4px; }
+.item-note { color: var(--muted); display: block; font-size: 11px; line-height: 1.4; margin-top: 6px; }
+.pilot-grid { display: grid; gap: 12px; }
+.pilot-boundary { border-left: 2px solid var(--blue); color: var(--muted); font-size: 13px; line-height: 1.55; padding-left: 14px; }
+
+@media (max-width: 800px) {
+ .project-metrics { grid-template-columns: repeat(2, 1fr); }
+ .project-metrics article { border-bottom: 1px solid var(--line); }
+ .milestone-grid { grid-template-columns: 1fr; }
+ .milestone-heading, .pilot-heading { align-items: start; flex-direction: column; }
diff --git a/package.json b/package.json
index 9938c66..02a15a8 100644
--- a/package.json
+++ b/package.json
@@ -14,15 +14,20 @@
],
"scripts": {
"build:m1-events": "node scripts/build-m1-example-events.mjs",
+ "build:status": "node scripts/build-project-status.mjs",
"build:state": "node scripts/build-console-state.mjs examples/m1 apps/console/state/index.json",
"build:state:check": "node scripts/check-console-state.mjs",
- "check": "npm run public:check && npm run validate && npm test && npm run build:state:check",
+ "check": "npm run public:check && npm run validate && npm test && npm run status:check && npm run build:state:check && npm run pages:check",
"console": "node scripts/serve-console.mjs",
"doctor": "node scripts/project-doctor.mjs",
"evidence:create": "node scripts/create-evidence-bundle.mjs",
"mcp": "npm --workspace @project-relay/mcp-server start",
+ "pages:build": "node scripts/build-pages.mjs",
+ "pages:check": "node scripts/check-pages.mjs",
"public:check": "node scripts/check-public-boundary.mjs",
"test": "node --test",
+ "status:build": "npm run build:status && npm run build:state && npm run pages:build",
+ "status:check": "node scripts/check-project-status.mjs",
"validate": "node scripts/validate-repository.mjs examples/minimal examples/m1",
"versions:check": "node scripts/version-doctor.mjs check",
"versions:update": "node scripts/version-doctor.mjs update"
diff --git a/project/status.json b/project/status.json
new file mode 100644
index 0000000..58a6542
--- /dev/null
+++ b/project/status.json
@@ -0,0 +1,397 @@
+{
+ "schema_version": "0.1.0",
+ "project": {
+ "id": "project-relay",
+ "name": "Project Relay",
+ "stage": "pre-alpha",
+ "repository": "https://github.com/brendohxd/Project-Relay"
+ },
+ "updated_at": "2026-07-19",
+ "milestones": [
+ {
+ "id": "M0",
+ "title": "Protocol kernel",
+ "exit_criteria": [
+ "The provider-neutral protocol kernel, local read-only MCP surface, fixtures, and public boundary are implemented."
+ ],
+ "items": [
+ {
+ "id": "M0-PROTOCOL",
+ "title": "Canonical schemas, hashes, and event-chain verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Foundation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-MCP",
+ "title": "Read-only local MCP server",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ },
+ {
+ "id": "M0-PUBLIC",
+ "title": "Public-boundary scanner and repository checks",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M0 implementation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/99f8c9c"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "M1",
+ "title": "Local vertical slice",
+ "exit_criteria": [
+ "The complete local remediation loop passes Windows and Linux verification and receives human review."
+ ],
+ "items": [
+ {
+ "id": "M1-LIFECYCLE",
+ "title": "Policy transition engine and remediation fixture",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 implementation commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/d899218"
+ }
+ ]
+ },
+ {
+ "id": "M1-EVIDENCE",
+ "title": "Evidence CLI, independent review, reproduction, and human gates",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 review fixes",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M1-LOCAL-VERIFY",
+ "title": "Windows local verification with 18 tests",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Draft PR verification record",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1#issuecomment-5014591903"
+ }
+ ]
+ },
+ {
+ "id": "M1-HUMAN-REVIEW",
+ "title": "Human review of draft PR #1",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Approved and merged M1 pull request",
+ "uri": "https://github.com/brendohxd/Project-Relay/pull/1"
+ }
+ ]
+ },
+ {
+ "id": "M1-LINUX-VERIFY",
+ "title": "Linux conformance verification",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Passing Windows and Linux M1 matrix",
+ "uri": "https://github.com/brendohxd/Project-Relay/actions/runs/29680192255"
+ }
+ ]
+ },
+ {
+ "id": "M1-MERGE",
+ "title": "Merge the reviewed M1 candidate",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "M1 merge commit",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/65a7d36ea2d9d29271b970ba883baa9c5a5a2b34"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "M2",
+ "title": "GitHub collaboration adapter",
+ "exit_criteria": [
+ "An approved Relay proposal can be translated into an idempotent, least-privilege GitHub change with conflict and failure records."
+ ],
+ "items": [
+ {
+ "id": "M2-CAPABILITIES",
+ "title": "Declared actor capabilities",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Capability foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-CAUSAL-LINKS",
+ "title": "Typed causal provenance links",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Causal-link foundation",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/11dc90a"
+ }
+ ]
+ },
+ {
+ "id": "M2-PROPOSAL",
+ "title": "Deterministic network-free GitHub mutation proposal",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Define proposal input and output contracts before authentication or writes."
+ },
+ {
+ "id": "M2-CONTRACT",
+ "title": "Fake-GitHub contract, conflict, retry, and idempotency tests",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Test the adapter without live repository mutations."
+ },
+ {
+ "id": "M2-AUTH",
+ "title": "Repository-scoped least-privilege authentication",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Add dedicated fine-grained credentials only after proposal behavior is reviewed."
+ },
+ {
+ "id": "M2-WRITE-PILOT",
+ "title": "Explicitly approved live write pilot",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Require fresh user approval and a clean threat-model review."
+ }
+ ]
+ },
+ {
+ "id": "M3",
+ "title": "Research and assessment profiles",
+ "exit_criteria": [
+ "Synthetic high-risk profiles prove blind reproduction, independence, disagreement, supersession, and human authority before real domain use."
+ ],
+ "items": [
+ {
+ "id": "M3-REPRODUCTION",
+ "title": "Independent high-risk reproduction core",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Independent reproduction gates",
+ "uri": "https://github.com/brendohxd/Project-Relay/commit/786648e"
+ }
+ ]
+ },
+ {
+ "id": "M3-BLIND-PACKET",
+ "title": "Blind reproduction packet contract",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Design and test against synthetic claims before any ITSM packet."
+ },
+ {
+ "id": "M3-DISAGREEMENT",
+ "title": "Adversarial review and disagreement ledger",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Preserve incompatible findings without forced consensus."
+ },
+ {
+ "id": "M3-SUPERSESSION",
+ "title": "Remediation and supersession graph",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Build on typed causal links and immutable prior records."
+ },
+ {
+ "id": "M3-ITSM-PROFILE",
+ "title": "ITSM operational policy profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Implement only after the synthetic research profile passes."
+ },
+ {
+ "id": "M3-EDUCATION-PROFILE",
+ "title": "Synthetic education marking and moderation profile",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Model rubric assessment, moderation, appeals, and named human authority without real student data."
+ },
+ {
+ "id": "M3-HIRING-PROFILE",
+ "title": "Synthetic hiring-assistance profile",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "Hiring requires dedicated privacy, fairness, discrimination, explainability, and legal threat models; Relay must remain decision support with human authority."
+ }
+ ]
+ },
+ {
+ "id": "M4",
+ "title": "Public console and integrations",
+ "exit_criteria": [
+ "Users can inspect project and task status through credential-free projections and supported client guides."
+ ],
+ "items": [
+ {
+ "id": "M4-STATUS",
+ "title": "Canonical project status and generated GitHub view",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Canonical project status",
+ "uri": "project/status.json"
+ },
+ {
+ "label": "Generated GitHub status",
+ "uri": "STATUS.md"
+ }
+ ]
+ },
+ {
+ "id": "M4-CONSOLE",
+ "title": "Visual milestone, blocker, and pilot dashboard",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Read-only console",
+ "uri": "apps/console/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-READY",
+ "title": "GitHub Pages-ready static bundle",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "Pages entry point",
+ "uri": "docs/index.html"
+ }
+ ]
+ },
+ {
+ "id": "M4-PAGES-PUBLISH",
+ "title": "Publish the visual console on GitHub Pages",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "After merge, configure Pages from main /docs and record the resulting public URL or blocker."
+ },
+ {
+ "id": "M4-CLIENT-GUIDES",
+ "title": "Codex, Claude, Gemini, and compatible MCP guides",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Document read-only status and task access before any write integrations."
+ },
+ {
+ "id": "M4-MANUAL-BRIDGE",
+ "title": "Manual packet bridge",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Support clients without GitHub or MCP while preserving the same evidence contract."
+ },
+ {
+ "id": "M4-NOTIFICATIONS",
+ "title": "Notifications and human approval inbox",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Begin only after authenticated M2 write boundaries are proven."
+ }
+ ]
+ },
+ {
+ "id": "M5",
+ "title": "Optional managed service",
+ "exit_criteria": [
+ "Security, funding, licensing, product demand, and service responsibilities are explicitly approved before hosted development."
+ ],
+ "items": [
+ {
+ "id": "M5-HOSTED",
+ "title": "Hosted authentication, secrets, tenancy, and audit service",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No hosted service is authorised before security, funding, licensing, and demand gates are resolved."
+ },
+ {
+ "id": "M5-BILLING",
+ "title": "Usage controls, billing, support, and service objectives",
+ "state": "deferred",
+ "evidence": [],
+ "rationale": "No paid infrastructure or billing work is required for the current local-first roadmap."
+ }
+ ]
+ }
+ ],
+ "pilots": [
+ {
+ "id": "itsm",
+ "name": "ITSM proving ground",
+ "visibility": "operational-summary-only",
+ "boundary": "Relay may show workflow readiness and gate progress only. ITSM equations, claims, datasets, results, manuscripts, and scientific acceptance remain in the separate ITSM repository and knowledge base.",
+ "items": [
+ {
+ "id": "ITSM-BOUNDARY",
+ "title": "Relay and ITSM separation rule documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-ROLES",
+ "title": "Operational owner, review, reproduction, audit, and authority roles documented",
+ "state": "done",
+ "evidence": [
+ {
+ "label": "ITSM pilot operating model",
+ "uri": "https://app.notion.com/p/3a236fb4bd1d810fb409f87153bc3e84"
+ }
+ ]
+ },
+ {
+ "id": "ITSM-SYNTHETIC",
+ "title": "Synthetic research profile passes before ITSM use",
+ "state": "planned",
+ "evidence": [],
+ "next_action": "Complete the M3 synthetic research-gate fixture."
+ },
+ {
+ "id": "ITSM-REAL-PILOT",
+ "title": "First real ITSM Relay packet",
+ "state": "blocked",
+ "evidence": [],
+ "blocker": {
+ "kind": "dependency",
+ "summary": "A real ITSM packet must wait for the synthetic M3 profile and explicit selection of a boundary-safe pilot task."
+ },
+ "next_action": "Select the pilot only after the synthetic profile passes."
+ }
+ ]
+ }
+ ]
+}
diff --git a/project/status.schema.json b/project/status.schema.json
new file mode 100644
index 0000000..0395cfd
--- /dev/null
+++ b/project/status.schema.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://project-relay.dev/project/status.schema.json",
+ "title": "Project Relay Status",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["schema_version", "project", "updated_at", "milestones", "pilots"],
+ "properties": {
+ "schema_version": { "const": "0.1.0" },
+ "project": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "name", "stage", "repository"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{2,63}$" },
+ "name": { "type": "string", "minLength": 3, "maxLength": 100 },
+ "stage": { "enum": ["pre-alpha", "alpha", "beta", "stable"] },
+ "repository": { "type": "string", "format": "uri" }
+ }
+ },
+ "updated_at": { "type": "string", "format": "date" },
+ "milestones": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/$defs/milestone" }
+ },
+ "pilots": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/pilot" }
+ }
+ },
+ "$defs": {
+ "evidence_link": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["label", "uri"],
+ "properties": {
+ "label": { "type": "string", "minLength": 1, "maxLength": 160 },
+ "uri": { "type": "string", "minLength": 1, "maxLength": 2000 }
+ }
+ },
+ "blocker": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "summary"],
+ "properties": {
+ "kind": { "enum": ["external", "human-action", "dependency", "decision"] },
+ "summary": { "type": "string", "minLength": 5, "maxLength": 1000 }
+ }
+ },
+ "work_item": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "title", "state", "evidence"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[A-Z][A-Z0-9-]{2,63}$" },
+ "title": { "type": "string", "minLength": 3, "maxLength": 200 },
+ "state": { "enum": ["done", "in_progress", "blocked", "planned", "deferred"] },
+ "evidence": {
+ "type": "array",
+ "items": { "$ref": "#/$defs/evidence_link" }
+ },
+ "blocker": { "$ref": "#/$defs/blocker" },
+ "rationale": { "type": "string", "minLength": 5, "maxLength": 2000 },
+ "next_action": { "type": "string", "minLength": 5, "maxLength": 1000 }
+ }
+ },
+ "milestone": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "title", "exit_criteria", "items"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^M[0-9]+$" },
+ "title": { "type": "string", "minLength": 3, "maxLength": 160 },
+ "exit_criteria": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "minLength": 5, "maxLength": 1000 }
+ },
+ "items": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/$defs/work_item" }
+ }
+ }
+ },
+ "pilot": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "name", "visibility", "boundary", "items"],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{2,63}$" },
+ "name": { "type": "string", "minLength": 2, "maxLength": 100 },
+ "visibility": { "const": "operational-summary-only" },
+ "boundary": { "type": "string", "minLength": 20, "maxLength": 2000 },
+ "items": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "$ref": "#/$defs/work_item" }
+ }
+ }
+ }
+ }
+}
diff --git a/scripts/build-console-state.mjs b/scripts/build-console-state.mjs
index 82df3ba..29295c2 100644
--- a/scripts/build-console-state.mjs
+++ b/scripts/build-console-state.mjs
@@ -2,11 +2,14 @@ import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { readDocuments, readTaskPacket, validateWorkspace } from "@project-relay/protocol";
+import { loadProjectStatus } from "./project-status-lib.mjs";
const workspaceArgument = process.argv[2] ?? "examples/minimal";
const outputArgument = process.argv[3] ?? "apps/console/state/index.json";
const workspace = path.resolve(workspaceArgument);
const output = path.resolve(outputArgument);
+const projectStatusFile = path.resolve(process.argv[4] ?? "project/status.json");
+const projectStatusSchema = path.resolve("project/status.schema.json");
const verification = await validateWorkspace(workspace);
if (!verification.valid) {
@@ -26,11 +29,12 @@ async function load(kind) {
);
}
-const [tasks, evidence, reviews, decisions] = await Promise.all([
+const [tasks, evidence, reviews, decisions, project] = await Promise.all([
load("task"),
load("evidence"),
load("review"),
- load("decision")
+ load("decision"),
+ loadProjectStatus(projectStatusFile, projectStatusSchema)
]);
const packets = new Map(
(
@@ -44,6 +48,7 @@ const countFor = (records, taskId) => records.filter((record) => record.task_id
const snapshot = {
protocol_version: "0.1.0",
generated_from: workspaceArgument.replaceAll("\\", "/"),
+ project,
summary: {
tasks: tasks.length,
active: tasks.filter((task) => !["accepted", "rejected", "cancelled"].includes(task.state)).length,
diff --git a/scripts/build-pages.mjs b/scripts/build-pages.mjs
new file mode 100644
index 0000000..9891d97
--- /dev/null
+++ b/scripts/build-pages.mjs
@@ -0,0 +1,11 @@
+import { copyFile, mkdir, writeFile } from "node:fs/promises";
+
+await mkdir("docs/state", { recursive: true });
+await Promise.all([
+ copyFile("apps/console/index.html", "docs/index.html"),
+ copyFile("apps/console/app.js", "docs/app.js"),
+ copyFile("apps/console/styles.css", "docs/styles.css"),
+ copyFile("apps/console/state/index.json", "docs/state/index.json"),
+ writeFile("docs/.nojekyll", "", "utf8")
+]);
+console.log("Wrote GitHub Pages bundle to docs/.");
diff --git a/scripts/build-project-status.mjs b/scripts/build-project-status.mjs
new file mode 100644
index 0000000..fb9a3c6
--- /dev/null
+++ b/scripts/build-project-status.mjs
@@ -0,0 +1,7 @@
+import { writeFile } from "node:fs/promises";
+
+import { loadProjectStatus, renderProjectStatus } from "./project-status-lib.mjs";
+
+const status = await loadProjectStatus();
+await writeFile("STATUS.md", renderProjectStatus(status), "utf8");
+console.log("Wrote STATUS.md");
diff --git a/scripts/check-pages.mjs b/scripts/check-pages.mjs
new file mode 100644
index 0000000..db003d1
--- /dev/null
+++ b/scripts/check-pages.mjs
@@ -0,0 +1,19 @@
+import { readFile } from "node:fs/promises";
+
+const pairs = [
+ ["apps/console/index.html", "docs/index.html"],
+ ["apps/console/app.js", "docs/app.js"],
+ ["apps/console/styles.css", "docs/styles.css"],
+ ["apps/console/state/index.json", "docs/state/index.json"]
+];
+const stale = [];
+for (const [source, generated] of pairs) {
+ const [left, right] = await Promise.all([readFile(source), readFile(generated)]);
+ if (!left.equals(right)) stale.push(generated);
+}
+if ((await readFile("docs/.nojekyll", "utf8")) !== "") stale.push("docs/.nojekyll");
+if (stale.length > 0) {
+ console.error(`Pages bundle is stale: ${stale.join(", ")}. Run npm run pages:build.`);
+ process.exit(1);
+}
+console.log("GitHub Pages bundle is current.");
diff --git a/scripts/check-project-status.mjs b/scripts/check-project-status.mjs
new file mode 100644
index 0000000..bca0f96
--- /dev/null
+++ b/scripts/check-project-status.mjs
@@ -0,0 +1,18 @@
+import { readFile } from "node:fs/promises";
+
+import { loadProjectStatus, renderProjectStatus } from "./project-status-lib.mjs";
+
+try {
+ const status = await loadProjectStatus();
+ const actual = await readFile("STATUS.md", "utf8");
+ const expected = renderProjectStatus(status);
+ if (actual !== expected) {
+ console.error("STATUS.md is stale. Run npm run build:status.");
+ process.exit(1);
+ }
+ console.log("Project status is valid and STATUS.md is current.");
+} catch (error) {
+ console.error(error.message);
+ if (error.issues) console.error(JSON.stringify(error.issues, null, 2));
+ process.exit(1);
+}
diff --git a/scripts/project-status-lib.mjs b/scripts/project-status-lib.mjs
new file mode 100644
index 0000000..466d974
--- /dev/null
+++ b/scripts/project-status-lib.mjs
@@ -0,0 +1,164 @@
+import { readFile } from "node:fs/promises";
+
+import Ajv2020 from "ajv/dist/2020.js";
+import addFormats from "ajv-formats";
+
+const STATE_ORDER = ["done", "in_progress", "blocked", "planned", "deferred"];
+
+function issue(path, message) {
+ return { path, message };
+}
+
+export function deriveCollection(items) {
+ const counts = Object.fromEntries(STATE_ORDER.map((state) => [state, 0]));
+ for (const item of items) counts[item.state] += 1;
+ let state = "planned";
+ if (counts.done === items.length) state = "complete";
+ else if (counts.in_progress > 0) state = "in_progress";
+ else if (counts.deferred === items.length) state = "deferred";
+ else if (counts.blocked > 0 && counts.blocked + counts.done + counts.deferred === items.length) {
+ state = "blocked";
+ }
+ return {
+ state,
+ counts,
+ total: items.length,
+ actionable: items.length - counts.deferred,
+ completed: counts.done,
+ blockers: items.filter((item) => item.state === "blocked")
+ };
+}
+
+export function validateStatusSemantics(status) {
+ const issues = [];
+ const identifiers = new Map();
+ const recordId = (id, path) => {
+ if (identifiers.has(id)) issues.push(issue(path, `duplicate id ${id}; first used at ${identifiers.get(id)}`));
+ else identifiers.set(id, path);
+ };
+ const inspectItems = (items, base) => {
+ items.forEach((item, index) => {
+ const path = `${base}/${index}`;
+ recordId(item.id, `${path}/id`);
+ if (item.state === "done" && item.evidence.length === 0) {
+ issues.push(issue(`${path}/evidence`, "done items require at least one evidence link"));
+ }
+ if (item.state === "blocked" && !item.blocker) {
+ issues.push(issue(`${path}/blocker`, "blocked items require a blocker"));
+ }
+ if (item.state !== "blocked" && item.blocker) {
+ issues.push(issue(`${path}/blocker`, "only blocked items may carry a blocker"));
+ }
+ if (item.state === "deferred" && !item.rationale) {
+ issues.push(issue(`${path}/rationale`, "deferred items require a rationale"));
+ }
+ });
+ };
+ status.milestones.forEach((milestone, index) => {
+ recordId(milestone.id, `/milestones/${index}/id`);
+ inspectItems(milestone.items, `/milestones/${index}/items`);
+ });
+ status.pilots.forEach((pilot, index) => {
+ recordId(pilot.id, `/pilots/${index}/id`);
+ inspectItems(pilot.items, `/pilots/${index}/items`);
+ });
+ return issues;
+}
+
+export function deriveProjectStatus(status) {
+ const milestones = status.milestones.map((milestone) => ({
+ ...milestone,
+ summary: deriveCollection(milestone.items)
+ }));
+ const pilots = status.pilots.map((pilot) => ({ ...pilot, summary: deriveCollection(pilot.items) }));
+ const allItems = milestones.flatMap((milestone) => milestone.items);
+ const summary = deriveCollection(allItems);
+ const current = milestones.find((milestone) => !["complete", "deferred"].includes(milestone.summary.state));
+ return {
+ ...status,
+ summary: {
+ ...summary,
+ current_milestone: current?.id ?? null,
+ current_milestone_title: current?.title ?? null
+ },
+ milestones,
+ pilots
+ };
+}
+
+export async function loadProjectStatus(
+ statusFile = "project/status.json",
+ schemaFile = "project/status.schema.json"
+) {
+ const [status, schema] = await Promise.all([
+ readFile(statusFile, "utf8").then(JSON.parse),
+ readFile(schemaFile, "utf8").then(JSON.parse)
+ ]);
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
+ addFormats(ajv);
+ const validate = ajv.compile(schema);
+ const issues = [];
+ if (!validate(status)) {
+ issues.push(...validate.errors.map((error) => issue(error.instancePath || "/", error.message)));
+ } else {
+ issues.push(...validateStatusSemantics(status));
+ }
+ if (issues.length > 0) {
+ const error = new Error(`Project status validation failed with ${issues.length} issue(s)`);
+ error.issues = issues;
+ throw error;
+ }
+ return deriveProjectStatus(status);
+}
+
+function evidenceMarkdown(evidence) {
+ return evidence.map(({ label, uri }) => `[${label}](${uri})`).join(", ");
+}
+
+function stateLabel(state) {
+ return state.replaceAll("_", " ");
+}
+
+export function renderProjectStatus(status) {
+ const derived = status.summary ? status : deriveProjectStatus(status);
+ const lines = [
+ "# Project status",
+ "",
+ `> Generated from [project/status.json](project/status.json) on ${derived.updated_at}. Do not edit this file directly.`,
+ "",
+ `**Stage:** ${derived.project.stage} `,
+ `**Current milestone:** ${derived.summary.current_milestone ?? "None"} — ${derived.summary.current_milestone_title ?? "All planned milestones complete or deferred"} `,
+ `**Roadmap:** ${derived.summary.completed}/${derived.summary.actionable} actionable items complete; ${derived.summary.counts.blocked} blocked; ${derived.summary.counts.deferred} deferred.`,
+ "",
+ "| Milestone | Status | Complete | Blocked | Deferred |",
+ "|---|---:|---:|---:|---:|"
+ ];
+ for (const milestone of derived.milestones) {
+ lines.push(`| ${milestone.id} — ${milestone.title} | ${stateLabel(milestone.summary.state)} | ${milestone.summary.completed}/${milestone.summary.actionable} | ${milestone.summary.counts.blocked} | ${milestone.summary.counts.deferred} |`);
+ }
+ for (const milestone of derived.milestones) {
+ lines.push("", `## ${milestone.id} — ${milestone.title}`, "", `**Status:** ${stateLabel(milestone.summary.state)}`, "", "Exit criteria:");
+ for (const criterion of milestone.exit_criteria) lines.push(`- ${criterion}`);
+ lines.push("");
+ for (const item of milestone.items) {
+ const checked = item.state === "done" ? "x" : " ";
+ lines.push(`- [${checked}] **${item.id}: ${item.title}** — ${stateLabel(item.state)}`);
+ if (item.blocker) lines.push(` - Blocker (${item.blocker.kind}): ${item.blocker.summary}`);
+ if (item.rationale) lines.push(` - Rationale: ${item.rationale}`);
+ if (item.next_action) lines.push(` - Next: ${item.next_action}`);
+ if (item.evidence.length > 0) lines.push(` - Evidence: ${evidenceMarkdown(item.evidence)}`);
+ }
+ }
+ if (derived.pilots.length > 0) lines.push("", "# Proving grounds");
+ for (const pilot of derived.pilots) {
+ lines.push("", `## ${pilot.name}`, "", `**Visibility:** ${pilot.visibility} `, `**Boundary:** ${pilot.boundary}`, "");
+ for (const item of pilot.items) {
+ const checked = item.state === "done" ? "x" : " ";
+ lines.push(`- [${checked}] **${item.title}** — ${stateLabel(item.state)}`);
+ if (item.blocker) lines.push(` - Blocker (${item.blocker.kind}): ${item.blocker.summary}`);
+ if (item.next_action) lines.push(` - Next: ${item.next_action}`);
+ if (item.evidence.length > 0) lines.push(` - Evidence: ${evidenceMarkdown(item.evidence)}`);
+ }
+ }
+ return `${lines.join("\n")}\n`;
+}
diff --git a/test/mcp.test.js b/test/mcp.test.js
index f9eef43..6f0bafa 100644
--- a/test/mcp.test.js
+++ b/test/mcp.test.js
@@ -17,6 +17,16 @@ test("MCP exposes the M1 task resource and bounded review prompt", async () => {
try {
await client.connect(transport);
+ const resources = await client.listResources();
+ assert.ok(resources.resources.some((item) => item.uri === "relay://project/status"));
+ const projectResource = await client.readResource({ uri: "relay://project/status" });
+ const projectStatus = JSON.parse(projectResource.contents[0].text);
+ assert.equal(projectStatus.summary.current_milestone, "M2");
+ assert.equal(projectStatus.pilots[0].visibility, "operational-summary-only");
+
+ const statusTool = await client.callTool({ name: "relay_get_project_status", arguments: {} });
+ assert.equal(statusTool.isError, false);
+ assert.equal(statusTool.structuredContent.summary.current_milestone, "M2");
const templates = await client.listResourceTemplates();
assert.ok(
templates.resourceTemplates.some(
diff --git a/test/project-status.test.js b/test/project-status.test.js
new file mode 100644
index 0000000..7608c57
--- /dev/null
+++ b/test/project-status.test.js
@@ -0,0 +1,38 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+
+import {
+ deriveProjectStatus,
+ loadProjectStatus,
+ renderProjectStatus,
+ validateStatusSemantics
+} from "../scripts/project-status-lib.mjs";
+
+const source = JSON.parse(await readFile("project/status.json", "utf8"));
+
+test("canonical project status validates and identifies the current milestone", async () => {
+ const status = await loadProjectStatus();
+ assert.equal(status.milestones[0].summary.state, "complete");
+ assert.equal(status.summary.current_milestone, "M2");
+ assert.equal(status.summary.counts.blocked, 0);
+});
+
+test("generated status preserves the ITSM publication boundary", () => {
+ const markdown = renderProjectStatus(deriveProjectStatus(source));
+ assert.match(markdown, /operational-summary-only/);
+ assert.match(markdown, /ITSM equations, claims, datasets, results, manuscripts/);
+});
+
+test("semantic validation rejects unsupported state claims and duplicate ids", () => {
+ const invalid = structuredClone(source);
+ invalid.milestones[0].items[0].evidence = [];
+ invalid.pilots[0].items[3].blocker = undefined;
+ invalid.milestones[2].items[2].id = invalid.milestones[0].items[1].id;
+ invalid.milestones[5].items[0].rationale = undefined;
+ const messages = validateStatusSemantics(invalid).map(({ message }) => message);
+ assert.ok(messages.some((message) => message.includes("done items require")));
+ assert.ok(messages.some((message) => message.includes("blocked items require")));
+ assert.ok(messages.some((message) => message.includes("duplicate id")));
+ assert.ok(messages.some((message) => message.includes("deferred items require")));
+});