Baseline: distribution workspace before observability redesign

This commit is contained in:
agra
2026-06-02 16:08:17 +03:00
commit 055c8ced15
38 changed files with 6001 additions and 0 deletions

106
.agents/CHECKPOINT.md Normal file
View File

@@ -0,0 +1,106 @@
# Checkpoint
Last updated: 2026-06-02 15:49:58 EEST
## Current State
- Workspace: `/Users/agra/projects/distribution`
- Current phase: observability redesign implementation
- Active run id: `2026-06-02-observability-redesign`
- Active implementation branch: `opus/observability-redesign`
- Git status: not a git repository; user clarified git setup is Opus
responsibility for this implementation pass
- Opus connector: local `opus-runner` plugin created and runtime self-test
passed
- Orchestration mode: sequential agents, branches only, no worktrees
## Completed
- Created initial distribution platform plan in `PLAN.md`.
- Created a static website mock in `index.html`, `styles.css`, and `app.js`.
- User rejected the mock layout quality.
- Created `.agents/ORCHESTRATION.md`.
- Created local `opus-runner` plugin at `/Users/agra/plugins/opus-runner`.
- Verified Claude CLI is installed and reports Claude Code `2.1.158`.
- Added detailed subplans under `.agents/subplans/`.
- Rewrote `PLAN.md` as an overview and moved detailed execution guidance to
`.agents/subplans/`.
- Added agent liveness and restart policy to orchestration docs.
- Added `.agents/scripts/status.mjs` for run/agent status and progress tails.
- Added local observability dashboard at `.agents/observability/`.
- Started dashboard server at `http://127.0.0.1:4317`.
- Registered the current planning session as an active run for observability.
- Snarky completed plan review and saved `snarky-review.md`.
- User approved Opus read-only access to `/Users/agra/projects/distribution`
and `/Users/agra/projects/sx`.
- Opus completed read-only plan/sx consultation and saved `opus-review.md`.
- Snarky and Opus completed two additional discussion rounds before
implementation.
- Created an observability redesign run and brief for Opus.
- Opus completed the observability redesign proposal.
- Saved Opus implementation instructions based on the proposal.
- Requested Opus implementation; Opus is responsible for initializing the git
baseline and branch.
## Important Decisions
- CLI command name is `dist`.
- iOS mobile install must distinguish TestFlight, Enterprise/MDM, and
artifact-only flows.
- Standard library planning must include `HashMap`, `StringBuilder`, and
extended `List`.
- Unicode must be specified precisely, not described as "UTF-8-ish".
- `pub print :: core.print` style namespace member re-export is required.
- Opus owns layout/design decisions.
- Snarky owns product scope and final product acceptance.
- Opus is the only role allowed to write code during Opus implementation.
- Opus leases and CLI/tool timeouts must never be less than 30 minutes.
- Implementation uses git branches, not worktrees.
## Blockers
- None active. The missing git baseline is now assigned to Opus as part of the
implementation request.
## Next Safe Action
Wait for Opus to initialize the git baseline, switch to branch
`opus/observability-redesign`, and implement the observability redesign from the
proposal.
```txt
.agents/runs/2026-06-02-redesign-distribution-mock/
```
Then run the sequential flow:
1. Snarky writes `brief.md` using `.agents/subplans/06-admin-ui.md`.
2. Opus writes `opus-proposal.md`.
3. Snarky reviews and resolves product requirements.
4. Opus implements on branch `opus/redesign-distribution-mock`.
5. Manager validates.
6. Snarky approves or requests another pass.
## Last Validation
- `node --check /Users/agra/plugins/opus-runner/scripts/opus_runner.mjs`: passed
- `node /Users/agra/plugins/opus-runner/scripts/opus_runner.mjs --self-test`:
passed
- MCP handshake test for `opus-runner`: passed
- `.agents/checkpoint.json` JSON parse: passed
- `.agents/` and new subplan files ASCII check: passed
- `PLAN.md` overview ASCII check: passed
- Agent recovery protocol documented in `.agents/ORCHESTRATION.md` and
`.agents/subplans/08-orchestration-and-qa.md`.
- `node --check .agents/scripts/status.mjs`: passed
- `.agents/scripts/status.mjs --tail` fixture test: passed
- `node --check .agents/scripts/observe.mjs`: passed
- `node --check .agents/observability/app.js`: passed
- `curl http://127.0.0.1:4317/api/status?tail=5`: passed
- Current planning run registered in `.agents/runs/2026-06-02-orchestration-planning/`.
- Snarky review artifact saved.
- Opus review artifact saved.
- Discussion round artifacts saved.
- Opus implementation instructions saved.
- Plugin helper validation could not run because local Python environments lack
`yaml`.

149
.agents/ORCHESTRATION.md Normal file
View File

@@ -0,0 +1,149 @@
# Distribution Platform Agent Orchestration
This workspace uses a sequential, branch-based agent workflow.
## Roles
Codex manager:
- Owns orchestration, routing, validation, and reporting.
- Writes run metadata and decision records.
- Does not implement application code during Opus phases.
Snarky:
- Writes product proposals and acceptance criteria.
- Reviews whether Opus satisfies the product requirements.
- Has final say on product scope and required behavior.
- Challenges Opus concerns on their merits.
Opus:
- Owns layout and visual design decisions.
- Is consulted for technical problems until the manager can drive consensus.
- Is the only role allowed to write application code.
- Implements on git branches, not worktrees.
## Branch Rules
- No parallel implementation agents.
- No worktrees.
- Every implementation run uses a named branch, for example:
- `opus/redesign-distribution-mock`
- `opus/install-flow-detail`
- `opus/release-console-ui`
- The working tree must be clean before Opus implementation starts.
- Opus implementation receives an explicit `allowedPaths` list.
- Manager validates the final diff before Snarky approval.
## Run Directory Shape
Each task gets a durable run directory:
```txt
.agents/runs/<run-id>/
state.json
agents.json
brief.md
opus-proposal.md
concerns.json
snarky-review.md
decisions.json
implementation-instructions.md
progress.log
implementation-log.md
validation.md
final-approval.md
```
Planning and orchestration work also gets a run directory when it should be
visible in the dashboard. If the manager is only editing files without creating
`.agents/runs/<run-id>/state.json`, the dashboard cannot show that session.
## Agent Recovery
Agents are treated as disposable workers. The run directory is the durable
source of truth.
Each active run tracks:
- `state.json`: current phase, expected artifact, input hash, retry count, and
next action.
- `agents.json`: active role leases, heartbeat timestamps, process/thread ids
when available, and last known status.
Manager responsibilities:
- Start one agent phase at a time.
- Write the phase input before invoking the agent.
- Record the expected output path.
- Record a lease timeout for the active role.
- Poll or wait for completion.
- Mark the agent dead if the lease expires or the process/tool call fails.
- Restart the same role with the same input artifact.
- Refuse to advance phases until the expected output exists and validates.
Recovery rules:
- Snarky can be recreated from the run brief, decisions, and checkpoint files.
- Opus proposal/review calls can be retried because they do not edit files.
- Opus implementation can only be retried after checking branch and dirty state.
- If an Opus implementation dies with a dirty branch, manager must inspect the
diff before retrying. Do not blindly overwrite partial code.
- Retry count is capped. After repeated failure, manager records a blocker and
asks the user.
Recommended lease defaults:
- Snarky brief/review: 10 minutes.
- Opus proposal/review/technical consult: 30 minutes minimum.
- Opus implementation: 45 minutes minimum.
Opus CLI/tool calls must never use a timeout below 30 minutes, even for
read-only design proposals. If a shorter timeout is accidentally used, record
the attempt, update the lease, and retry with at least 30 minutes.
## Status Command
From the workspace root:
```sh
node .agents/scripts/status.mjs --tail 40
```
Browser dashboard:
```sh
node .agents/scripts/observe.mjs --port 4317
```
This prints every recorded run, every recorded agent, lease expiry state,
blockers, next action, and the tail of the active run progress file. The browser
dashboard shows the same state and refreshes automatically.
Managers should append progress events to `.agents/runs/<run-id>/progress.log`.
## Sequential Flow
1. Manager creates `.agents/runs/<run-id>/`.
2. Snarky writes `brief.md`.
3. Manager calls Opus for `opus-proposal.md`.
4. Snarky reviews product fit and challenges concerns.
5. Opus responds to accepted concern challenges.
6. Manager resolves consensus for technical issues.
7. Snarky writes `implementation-instructions.md`.
8. Manager calls Opus implementation on a named branch.
9. Manager validates the branch diff and checks.
10. Snarky approves or requests another Opus pass.
## Opus Runner Tools
The local `opus-runner` plugin exposes:
- `opus_branch_status`
- `opus_propose_design`
- `opus_review_concern`
- `opus_solve_technical_issue`
- `opus_implement_on_branch`
Only `opus_implement_on_branch` may enable file editing.

89
.agents/checkpoint.json Normal file
View File

@@ -0,0 +1,89 @@
{
"schema": 1,
"updated_at": "2026-06-02 15:49:58 EEST",
"workspace": "/Users/agra/projects/distribution",
"current_phase": "observability-redesign-implementation",
"active_run_id": "2026-06-02-observability-redesign",
"active_branch": "opus/observability-redesign",
"git": {
"is_repo": false,
"status": "not-a-git-repository; git baseline delegated to Opus for implementation"
},
"orchestration": {
"mode": "sequential",
"implementation_is_branch_based": true,
"uses_worktrees": false,
"code_writer": "opus",
"product_final_say": "snarky",
"layout_final_say": "opus"
},
"completed": [
"PLAN.md created",
"static website mock created",
"mock layout rejected by user",
".agents/ORCHESTRATION.md created",
"opus-runner plugin created",
"opus-runner runtime self-test passed",
"detailed subplans created",
"PLAN.md rewritten as overview",
"agent liveness and restart policy documented",
".agents/scripts/status.mjs added",
"observability dashboard added",
"observability dashboard started at http://127.0.0.1:4317",
"current planning run registered for observability",
"Snarky review artifact saved",
"Opus review artifact saved after user-approved repo access",
"Snarky and Opus completed two additional discussion rounds"
,
"observability redesign run created",
"Opus timeout minimum documented as 30 minutes",
"Opus observability redesign proposal saved",
"Opus observability implementation requested"
],
"blockers": [],
"next_action": "Wait for Opus to initialize the git baseline, switch to branch opus/observability-redesign, and implement the observability redesign from the proposal.",
"last_validation": [
"opus-runner node syntax passed",
"opus-runner Claude CLI self-test passed",
"opus-runner MCP handshake passed",
"checkpoint JSON parse passed",
"new agent planning files ASCII check passed",
"PLAN.md overview ASCII check passed",
"agent recovery protocol documented",
"status script syntax passed",
"status script fixture tail test passed",
"observability server syntax passed",
"observability app syntax passed",
"observability API check passed",
"current planning run registered",
"Snarky and Opus consultation artifacts saved",
"additional Snarky/Opus discussion artifacts saved",
"Opus implementation instructions saved"
],
"artifacts": {
"plan": "PLAN.md",
"orchestration": ".agents/ORCHESTRATION.md",
"checkpoint": ".agents/CHECKPOINT.md",
"checkpoint_json": ".agents/checkpoint.json",
"subplans": ".agents/subplans/",
"status_script": ".agents/scripts/status.mjs",
"observability_server": ".agents/scripts/observe.mjs",
"observability_frontend": ".agents/observability/",
"observability_url": "http://127.0.0.1:4317",
"snarky_review": ".agents/runs/2026-06-02-orchestration-planning/snarky-review.md",
"opus_review": ".agents/runs/2026-06-02-orchestration-planning/opus-review.md",
"discussion_round_1_snarky": ".agents/runs/2026-06-02-orchestration-planning/discussion-round-1-snarky.md",
"discussion_round_1_opus": ".agents/runs/2026-06-02-orchestration-planning/discussion-round-1-opus.md",
"discussion_round_2_snarky": ".agents/runs/2026-06-02-orchestration-planning/discussion-round-2-snarky.md",
"discussion_round_2_opus": ".agents/runs/2026-06-02-orchestration-planning/discussion-round-2-opus.md",
"observability_redesign_run": ".agents/runs/2026-06-02-observability-redesign/",
"observability_redesign_proposal": ".agents/runs/2026-06-02-observability-redesign/opus-proposal.md",
"observability_implementation_instructions": ".agents/runs/2026-06-02-observability-redesign/implementation-instructions.md",
"mock_files": [
"index.html",
"styles.css",
"app.js"
],
"opus_runner_plugin": "/Users/agra/plugins/opus-runner"
}
}

View File

@@ -0,0 +1,183 @@
const refreshButton = document.getElementById("refresh-button");
const refreshStatus = document.getElementById("refresh-status");
const tailSelect = document.getElementById("tail-select");
const fields = {
activeRun: document.getElementById("metric-active-run"),
phase: document.getElementById("metric-phase"),
branch: document.getElementById("metric-branch"),
blockers: document.getElementById("metric-blockers"),
runCount: document.getElementById("run-count"),
runsList: document.getElementById("runs-list"),
generatedAt: document.getElementById("generated-at"),
nextAction: document.getElementById("next-action"),
workspace: document.getElementById("workspace"),
blockersList: document.getElementById("blockers"),
progressPath: document.getElementById("progress-path"),
progressTail: document.getElementById("progress-tail"),
};
let refreshTimer = null;
refreshButton.addEventListener("click", () => loadStatus());
tailSelect.addEventListener("change", () => loadStatus());
loadStatus();
refreshTimer = setInterval(loadStatus, 5000);
window.addEventListener("beforeunload", () => clearInterval(refreshTimer));
async function loadStatus() {
setRefreshState("Loading", "");
try {
const response = await fetch(`/api/status?tail=${encodeURIComponent(tailSelect.value)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
render(data);
setRefreshState("Live", "status-ok");
} catch (error) {
setRefreshState("Error", "status-dead");
fields.progressTail.textContent = `Failed to load status: ${error.message}`;
}
}
function render(data) {
const summary = data.summary || {};
const checkpoint = data.checkpoint || {};
const blockers = Array.isArray(checkpoint.blockers) ? checkpoint.blockers : [];
fields.activeRun.textContent = summary.active_run_id || "none";
fields.phase.textContent = summary.current_phase || "unknown";
fields.branch.textContent = summary.active_branch || "none";
fields.blockers.textContent = String(blockers.length);
fields.runCount.textContent = `${data.runs.length} total`;
fields.generatedAt.textContent = formatTime(data.generated_at);
fields.nextAction.textContent = summary.next_action || "No next action recorded.";
fields.workspace.textContent = data.workspace || "-";
renderBlockers(blockers);
renderRuns(data.runs, summary.active_run_id);
renderProgress(data.active_progress);
}
function renderBlockers(blockers) {
fields.blockersList.innerHTML = "";
if (blockers.length === 0) {
fields.blockersList.innerHTML = '<p class="muted">No blockers recorded.</p>';
return;
}
for (const blocker of blockers) {
const item = document.createElement("div");
item.className = "blocker";
item.textContent = blocker;
fields.blockersList.appendChild(item);
}
}
function renderRuns(runs, activeRunId) {
fields.runsList.innerHTML = "";
if (runs.length === 0) {
fields.runsList.innerHTML =
'<div class="empty">No runs yet. Create .agents/runs/&lt;run-id&gt;/ with state.json and agents.json.</div>';
return;
}
for (const run of runs) {
fields.runsList.appendChild(renderRun(run, run.id === activeRunId));
}
}
function renderRun(run, active) {
const state = run.state || {};
const card = document.createElement("article");
card.className = "run-card";
const title = document.createElement("div");
title.className = "run-title";
title.innerHTML = `
<strong>${escapeHtml(run.id)}</strong>
<span class="chip ${active ? "status-ok" : ""}">${active ? "active" : "recorded"}</span>
`;
card.appendChild(title);
const meta = document.createElement("div");
meta.className = "run-meta";
meta.innerHTML = `
<span>Phase: ${escapeHtml(state.current_phase || state.phase || "unknown")}</span>
<span>Branch: ${escapeHtml(state.current_branch || state.branch || "none")}</span>
<span>Expected: ${escapeHtml(state.expected_output_artifact || state.expected_output || "none")}</span>
<span>Retries: ${escapeHtml(String(state.retry_count ?? 0))}</span>
`;
card.appendChild(meta);
const agents = document.createElement("div");
agents.className = "agent-list";
if (!run.agents || run.agents.length === 0) {
agents.innerHTML = '<small class="muted">No agents recorded.</small>';
} else {
for (const agent of run.agents) {
agents.appendChild(renderAgent(agent));
}
}
card.appendChild(agents);
return card;
}
function renderAgent(agent) {
const row = document.createElement("div");
row.className = "agent-row";
const statusClass = agent.lease_expired
? "status-dead"
: agent.status === "completed"
? "status-ok"
: "status-warn";
const id = agent.thread_id || agent.process_id || agent.tool_call_id || "none";
row.innerHTML = `
<div>
<strong>${escapeHtml(agent.role || "unknown-role")}</strong>
<small>heartbeat ${escapeHtml(agent.heartbeat_at || "none")} / id ${escapeHtml(String(id))}</small>
${agent.last_error ? `<small>${escapeHtml(agent.last_error)}</small>` : ""}
</div>
<span class="chip ${statusClass}">${escapeHtml(agent.lease_expired ? "expired" : agent.status || "unknown")}</span>
`;
return row;
}
function renderProgress(progress) {
if (!progress || !progress.path) {
fields.progressPath.textContent = "no progress file";
fields.progressTail.textContent = "No progress file found for the active run.";
return;
}
fields.progressPath.textContent = progress.path;
fields.progressTail.textContent = progress.lines.length > 0
? progress.lines.join("\n")
: "Progress file is empty.";
}
function setRefreshState(label, extraClass) {
refreshStatus.className = `status-pill ${extraClass}`;
refreshStatus.textContent = label;
}
function formatTime(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleTimeString();
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}

View File

@@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Agent Observability</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div>
<p class="eyebrow">Distribution orchestration</p>
<h1>Agent Observability</h1>
</div>
<div class="toolbar">
<label>
Tail
<select id="tail-select">
<option value="40">40 lines</option>
<option value="80" selected>80 lines</option>
<option value="160">160 lines</option>
</select>
</label>
<button id="refresh-button">Refresh</button>
<span id="refresh-status" class="status-pill">Loading</span>
</div>
</header>
<main>
<section class="summary-grid" aria-label="Summary">
<article class="metric">
<span>Active run</span>
<strong id="metric-active-run">-</strong>
</article>
<article class="metric">
<span>Phase</span>
<strong id="metric-phase">-</strong>
</article>
<article class="metric">
<span>Branch</span>
<strong id="metric-branch">-</strong>
</article>
<article class="metric">
<span>Blockers</span>
<strong id="metric-blockers">-</strong>
</article>
</section>
<section class="layout">
<section class="panel runs-panel">
<div class="panel-head">
<h2>Runs</h2>
<span id="run-count" class="muted">0</span>
</div>
<div id="runs-list" class="runs-list"></div>
</section>
<section class="panel detail-panel">
<div class="panel-head">
<h2>Checkpoint</h2>
<span id="generated-at" class="muted">-</span>
</div>
<div class="checkpoint-grid">
<div>
<span>Next action</span>
<p id="next-action">-</p>
</div>
<div>
<span>Workspace</span>
<p id="workspace">-</p>
</div>
</div>
<div id="blockers" class="blockers"></div>
<div class="panel-head lower">
<h2>Progress Tail</h2>
<span id="progress-path" class="muted">-</span>
</div>
<pre id="progress-tail" class="progress-tail">No progress loaded.</pre>
</section>
</section>
</main>
</div>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,363 @@
:root {
--bg: #f5f4ef;
--panel: #ffffff;
--ink: #202428;
--muted: #68707a;
--line: #d9d4c8;
--soft: #faf8f2;
--blue: #235f8f;
--green: #237457;
--amber: #9a6818;
--red: #a43d34;
--shadow: 0 14px 34px rgba(35, 32, 25, 0.08);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
min-height: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
}
button,
select {
font: inherit;
}
button {
min-height: 36px;
border: 1px solid #1d4f76;
border-radius: 8px;
background: var(--blue);
color: #ffffff;
font-weight: 800;
padding: 0 14px;
}
select {
min-height: 36px;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
color: var(--ink);
padding: 0 10px;
}
.shell {
width: min(1440px, 100%);
margin: 0 auto;
padding: 22px;
}
.topbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
h1,
h2,
p {
margin: 0;
}
h1 {
font-size: 28px;
letter-spacing: 0;
}
h2 {
font-size: 16px;
letter-spacing: 0;
}
.toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.toolbar label {
display: flex;
align-items: center;
gap: 7px;
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.status-pill,
.chip {
display: inline-flex;
align-items: center;
min-height: 26px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
padding: 0 10px;
}
.status-pill {
border: 1px solid var(--line);
background: #ffffff;
color: var(--muted);
}
.status-ok {
background: #e5f2eb;
color: var(--green);
}
.status-warn {
background: #f7ead1;
color: var(--amber);
}
.status-dead {
background: #f5dddd;
color: var(--red);
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.metric,
.panel {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
box-shadow: var(--shadow);
}
.metric {
display: grid;
gap: 7px;
min-height: 94px;
padding: 14px;
}
.metric span,
.checkpoint-grid span {
color: var(--muted);
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
}
.metric strong {
overflow: hidden;
color: var(--ink);
font-size: 23px;
text-overflow: ellipsis;
white-space: nowrap;
}
.layout {
display: grid;
grid-template-columns: minmax(320px, 430px) minmax(0, 1fr);
gap: 14px;
}
.panel {
min-width: 0;
padding: 16px;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.panel-head.lower {
margin-top: 18px;
}
.muted {
overflow: hidden;
color: var(--muted);
font-size: 12px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.runs-list {
display: grid;
gap: 10px;
}
.empty {
border: 1px dashed var(--line);
border-radius: 8px;
background: var(--soft);
color: var(--muted);
padding: 16px;
}
.run-card {
display: grid;
gap: 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
padding: 12px;
}
.run-title {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
}
.run-title strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.run-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
color: var(--muted);
font-size: 12px;
}
.agent-list {
display: grid;
gap: 7px;
}
.agent-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
border-top: 1px solid var(--line);
padding-top: 8px;
}
.agent-row small {
display: block;
overflow: hidden;
color: var(--muted);
text-overflow: ellipsis;
white-space: nowrap;
}
.checkpoint-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.checkpoint-grid div,
.blockers {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
padding: 12px;
}
.checkpoint-grid p {
overflow-wrap: anywhere;
margin-top: 5px;
color: var(--ink);
}
.blockers {
display: grid;
gap: 8px;
}
.blocker {
color: var(--red);
font-size: 14px;
font-weight: 750;
}
.progress-tail {
overflow: auto;
min-height: 360px;
max-height: 620px;
margin: 0;
border: 1px solid #2d3339;
border-radius: 8px;
background: #202428;
color: #f6f3ea;
padding: 14px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 13px;
line-height: 1.55;
white-space: pre-wrap;
}
@media (max-width: 980px) {
.summary-grid,
.layout {
grid-template-columns: 1fr;
}
.topbar {
display: grid;
}
.toolbar {
justify-content: flex-start;
}
}
@media (max-width: 560px) {
.shell {
padding: 14px;
}
h1 {
font-size: 24px;
}
.toolbar,
.toolbar label,
button,
select {
width: 100%;
}
.checkpoint-grid,
.run-meta {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,23 @@
{
"schema": 1,
"agents": [
{
"role": "codex-manager",
"status": "running",
"started_at": "2026-06-02T12:05:10Z",
"heartbeat_at": "2026-06-02T12:49:58Z",
"lease_expires_at": "2026-06-02T13:04:58Z",
"thread_id": "current-codex-thread",
"last_error": null
},
{
"role": "opus",
"status": "running",
"started_at": "2026-06-02T12:49:58Z",
"heartbeat_at": "2026-06-02T12:49:58Z",
"lease_expires_at": "2026-06-02T13:19:58Z",
"tool_call_id": "claude-cli-observability-implementation-1",
"last_error": null
}
]
}

View File

@@ -0,0 +1,75 @@
# Observability Redesign Brief
## Request
Ask Opus to redesign the local agent observability dashboard before any
implementation starts.
## Context
The dashboard lives under `.agents/observability/` and is served by
`.agents/scripts/observe.mjs` at `http://127.0.0.1:4317`.
Current files:
- `.agents/observability/index.html`
- `.agents/observability/styles.css`
- `.agents/observability/app.js`
- `.agents/scripts/observe.mjs`
The dashboard currently shows:
- Summary cards for active run, phase, branch, and blockers.
- A runs list with run metadata and agent rows.
- Checkpoint next action, workspace, blockers, and active progress tail.
- Tail length selector, refresh button, and live/error status.
Known design problem:
- The current layout feels generic and visually weak.
- It does not make agent liveness, leases, progress, blockers, or next action
feel operationally obvious enough.
- It should feel like a focused orchestration cockpit, not a landing page.
## Product Requirements
- First viewport should immediately answer:
- What is active?
- Is anything dead or blocked?
- Who is working?
- What happened most recently?
- What should the manager do next?
- Preserve the data source and static app constraint.
- Keep the UI dense, calm, and operational.
- Avoid marketing layout, decorative cards inside cards, and hero-style copy.
- Make liveness and lease expiry visually obvious.
- Make progress tail readable without dominating the whole page.
- Support desktop and mobile layouts without text overlap.
- Prefer familiar controls and icons if implementation later adds an icon
library or inline symbols.
- Keep the design implementable in plain HTML, CSS, and vanilla JS.
## Current Constraints
- This is a read-only Opus design proposal request.
- Do not edit application files yet.
- The distribution workspace is not a git repository yet, so branch-based Opus
implementation is still blocked.
- If implementation is later approved, the likely write scope is:
- `.agents/observability/index.html`
- `.agents/observability/styles.css`
- `.agents/observability/app.js`
- possibly `.agents/scripts/observe.mjs` if Opus needs small API fields for
better status grouping.
## Opus Output Requested
Write `opus-proposal.md` with:
1. The proposed information architecture.
2. The exact visual layout direction.
3. The key interactions and states.
4. Any small API/data-shape changes requested from `observe.mjs`.
5. Mobile behavior.
6. Implementation notes for the eventual Opus coding pass.
7. Concerns or blockers, if any.

View File

@@ -0,0 +1,135 @@
# Observability Redesign Implementation Instructions
## Role
You are Opus in the distribution platform orchestration flow. Codex is the
manager. Snarky owns product acceptance. You own layout and visual design
decisions and are the only role allowed to edit code during this phase.
## User Direction
The user clarified that the git setup required for the Opus implementation
branch is Opus responsibility.
If `/Users/agra/projects/distribution` is not yet a git repository, initialize a
local git repository, create a clean baseline commit from the current workspace,
and then create or switch to branch:
```txt
opus/observability-redesign
```
Use branches only. Do not create a worktree.
## Primary Task
Implement the observability dashboard redesign from:
```txt
.agents/runs/2026-06-02-observability-redesign/opus-proposal.md
```
The dashboard lives at:
```txt
.agents/observability/
```
It is served by:
```txt
.agents/scripts/observe.mjs
```
## Allowed Edit Scope
Application/dashboard edits are limited to:
- `.agents/observability/index.html`
- `.agents/observability/styles.css`
- `.agents/observability/app.js`
- `.agents/scripts/observe.mjs`
- `.agents/runs/2026-06-02-observability-redesign/implementation-log.md`
Creating `.git/` and committing/switching branches is allowed as setup for this
phase. Do not edit root mock files such as `index.html`, `styles.css`, or
`app.js`.
## Required Product Shape
Implement the proposal's operational cockpit direction:
- Replace the four metric cards with a compact sticky status bar and a "Now"
command band.
- Make the first viewport answer what is active, whether anything is blocked or
stalled, who is working, what happened recently, and what the manager should do
next.
- Pin active-agent liveness with status, lease countdown, heartbeat age, and
clear warning/dead states.
- Keep progress readable but bounded; it must not dominate the whole page.
- Keep the UI dense, calm, and operational. Avoid marketing layout, hero copy,
decorative nesting, and generic dashboard fluff.
- Support desktop and mobile without text overlap.
## Required Interactions
- Keep the existing `/api/status` polling model and static vanilla app.
- Add a one-second local ticker for freshness, heartbeat age, and lease
countdown text.
- Add pause/resume for auto-refresh.
- Keep the last good render on fetch error; do not replace the progress log with
an error message.
- Add progress expand/collapse.
- Preserve progress bottom-stick behavior and show a jump control when new lines
arrive while the user is scrolled up.
- Make run rows selectable. If the API supports `?run=`, use it to refocus
progress for the selected run; otherwise degrade gracefully.
- Add click-to-copy for run id, branch, and progress path with a short copied
state.
## Required Server/API Additions
Make only additive, optional changes:
- Per-agent liveness fields:
- `lease_remaining_ms`
- `heartbeat_age_ms`
- `lease_expired`
- `is_active`
- Summary fields:
- `active_role`
- `health`
- Active progress fields:
- `modified_at`
- `total_lines`
- `truncated`
- Support `/api/status?tail=N&run=<id>` so selected runs can show their own
progress.
- In progress selection, prefer the newest candidate progress artifact by mtime.
The frontend must remain missing-field safe.
## Validation Expectations
Before finishing, run:
```sh
node --check .agents/scripts/observe.mjs
node --check .agents/observability/app.js
node .agents/scripts/status.mjs --tail 20
```
If possible, start or reuse the local server and verify:
```sh
curl http://127.0.0.1:4317/api/status?tail=5
```
Write a short final implementation report to:
```txt
.agents/runs/2026-06-02-observability-redesign/implementation-log.md
```
Include changed files, validation commands/results, unresolved risks, and the
current git branch/status.

View File

@@ -0,0 +1,46 @@
# Role
You are Opus in the distribution platform orchestration flow.
Codex is the manager. Snarky owns product acceptance. You own layout and visual
design decisions, and you are consulted for technical problems until consensus.
This is a read-only design proposal. Do not write code. Do not assume
implementation has started.
# Repositories
You have read-only context for:
- `/Users/agra/projects/distribution`
- `/Users/agra/projects/sx`
For this request, focus on the distribution repo only.
# Task
Redesign the local agent observability dashboard.
Read these files:
- `/Users/agra/projects/distribution/.agents/runs/2026-06-02-observability-redesign/brief.md`
- `/Users/agra/projects/distribution/.agents/observability/index.html`
- `/Users/agra/projects/distribution/.agents/observability/styles.css`
- `/Users/agra/projects/distribution/.agents/observability/app.js`
- `/Users/agra/projects/distribution/.agents/scripts/observe.mjs`
- `/Users/agra/projects/distribution/.agents/ORCHESTRATION.md`
Return a proposal in Markdown with these sections:
1. Verdict
2. Information Architecture
3. Visual Layout Direction
4. Interactions And States
5. API Or Data Shape Changes
6. Mobile Behavior
7. Implementation Notes
8. Concerns
Be specific enough that an implementation pass can follow it without another
layout brainstorm. Keep the design operational, dense, and calm. Avoid a hero
page, marketing copy, decorative nesting, or generic SaaS fluff.

View File

@@ -0,0 +1,309 @@
# Opus Proposal - Local Agent Observability Dashboard Redesign
## 1. Verdict
Redesign is warranted, and the problem is structural, not cosmetic. The current
page is a symmetric four-metric grid feeding two equal-weight panels. That
layout treats every fact as equally important, which is wrong for a sequential,
single-active orchestration model.
Keep the hard constraints: static HTML/CSS/vanilla JS, single `/api/status`
poll, no build step, and no dependencies. The redesign needs no breaking API
changes, only small additive, server-computed fields to make liveness and lease
expiry honest rather than guessed client-side.
The reorientation:
- Replace the equal metric grid with a "Now" command band that answers active
run, phase, branch, and next action in one strip.
- Anchor the band with one system-health token.
- Make agent liveness a live ticking signal: lease countdown plus heartbeat age.
- Demote the progress tail from page-dominator to bounded, auto-sticking log.
- Remove landing-page tells: heavy shadows, shouting weights, eyebrow plus large
heading, and cards-inside-cards.
Implementation is blocked until the workspace is a git repo. This proposal is
approvable now and implementable once `git init` and a baseline commit exist.
## 2. Information Architecture
Three tiers, in strict priority order. Tiers 0 through 2 should sit above the
fold on desktop.
Tier 0 - Chrome/status bar:
- Workspace basename.
- Connection state.
- Freshness, ticking as "updated 3s ago".
- Tail selector.
- Refresh.
- Auto-refresh pause/play.
Tier 1 - Now band:
- System token: `WORKING`, `BLOCKED`, `STALLED`, `STALE`, or `IDLE`.
- Active run: run id, phase stepper, branch chip.
- Next action: prominent prose label.
Tier 2 - Operational split:
- Left rail around 340px:
- Active agent pinned at the top with status dot, role, lease countdown, and
heartbeat age.
- Agents for the active run.
- Compact runs list with active run highlighted.
- Right column:
- Conditional blockers strip at the top when blockers exist.
- Progress tail with path, "last N of M", and last-modified age.
Remove the standalone workspace metric and the four-up summary grid. Workspace
moves to the status bar; the grid is absorbed into the Now band and health
token.
## 3. Visual Layout Direction
Desktop layout:
```txt
+--------------------------------------------------------------------+
| distribution updated 3s ago Tail [80] refresh pause |
+--------------------------------------------------------------------+
| WORKING | run 2026-06-02-obs... 4/10 Opus proposal | NEXT ACTION |
| | branch none Wait on ... |
+----------------------------+---------------------------------------+
| ACTIVE AGENT | BLOCKERS, if any |
| opus running +---------------------------------------+
| lease 28:14 heartbeat 4s | PROGRESS runs/.../progress.log |
| | +-----------------------------------+ |
| AGENTS | | 14:02:11 proposal requested | |
| codex running | | 14:02:48 reading brief.md | |
| snarky completed | | ... | |
| | +-----------------------------------+ |
| RUNS | expand new |
| active run | |
+----------------------------+---------------------------------------+
```
Grid and sizing:
- Shell: `width: min(1440px, 100%)`, padding `20px`.
- Status bar: sticky, top `0`, about `44px`, hairline bottom border.
- Now band: single panel with
`grid-template-columns: 150px minmax(0, 1fr) minmax(300px, 1fr)`.
- Main split: `grid-template-columns: minmax(300px, 340px) minmax(0, 1fr)`.
- Progress body: `max-height: clamp(320px, 52vh, 720px); overflow: auto`.
- Expanded progress body: about `80vh`.
Visual language:
- Use one elevation level. Prefer hairline borders over heavy shadows.
- Delete nested bordered boxes; internal sections use dividers and spacing.
- Reduce blanket `font-weight: 800`. Labels around 600, values 400, system
token 700.
- Use monospace for machine facts: run ids, branches, paths, timestamps, counts,
lease values, heartbeat values.
- Keep prose in the sans stack.
- Keep the warm-paper base and the current semantic colors, but assign strict
meaning:
- green: healthy, live, completed
- blue: working or active focus
- amber: warning, expiring lease, aging heartbeat, stale data
- red: blocked, expired, dead, fetch error
- muted grey: idle, recorded, none, absent
- Use 8px status dots for rows. Reserve pills for the system token and active
or recorded run status.
- Keep the progress log dark as the one large terminal-like surface.
Phase stepper:
- Render the ten canonical sequential-flow steps as a segmented bar.
- Filled segments are done, current segment is ringed, future segments are
hollow.
- Show `N / 10 - <label>`.
- If phase text is unknown, degrade to label-only text.
## 4. Interactions And States
System health token priority:
```txt
BLOCKED red blocker_count > 0
STALLED red active agent lease_expired and not blocked
STALE amber data age is stale or active heartbeat is stale
WORKING blue active-run agent is running with healthy unexpired lease
IDLE grey no active run or no leased agent
```
Use `aria-live="polite"` for health transitions.
Liveness:
- Add a 1-second local ticker independent from the 5-second poll.
- Lease countdown displays `mm:ss` remaining.
- Lease at or below two minutes becomes amber.
- Expired lease becomes red and displays elapsed expiry age.
- Heartbeat age displays as `heartbeat 4s`.
- Heartbeat over 60 seconds becomes amber.
- Heartbeat over 180 seconds becomes red.
- Data freshness in the status bar ticks locally.
- The ticker only updates timestamp text nodes and never re-renders lists.
Polling and errors:
- Keep the last good render on fetch error.
- Flip the status bar to red error/stale state.
- Do not replace the progress tail with an error string.
- Pause timers while the tab is hidden and resume with an immediate refetch on
focus.
Selection and affordances:
- Run rows should be selectable.
- Selecting a run refocuses progress and agent details if the API supports
`?run=`.
- Progress auto-sticks to bottom only if the user was already at bottom.
- If new content arrives while scrolled up, show a "new" jump button.
- Add expand/collapse for the progress log.
- Add click-to-copy for run id, branch, and progress path with a brief copied
state.
Empty and degraded states:
- No runs.
- No active run.
- No progress file.
- No lease recorded: show `lease -`, muted.
- No heartbeat recorded: show `heartbeat -`, muted.
- Unknown phase: label-only stepper.
- Checkpoint parse error: surface it in the blockers strip.
## 5. API Or Data Shape Changes
All requests are additive and optional. The dashboard must render correctly if
any are absent.
Per agent:
```json
{
"lease_remaining_ms": 1694000,
"heartbeat_age_ms": 4000,
"lease_expired": true,
"is_active": true
}
```
Summary:
```json
{
"active_role": "opus",
"health": "working"
}
```
Active progress:
```json
{
"modified_at": "2026-06-02T14:05:03Z",
"total_lines": 342,
"truncated": true
}
```
New optional query parameter:
```txt
/api/status?tail=N&run=<id>
```
When `run` is supplied, `active_progress` reflects that run instead of the
active one. Default behavior remains unchanged.
Optional manager-written fields in `state.json`:
```json
{
"phase_index": 4,
"phase_total": 10,
"updated_at": "2026-06-02T14:05:03Z"
}
```
Suggested server tweak: in `readProgress`, pick the newest candidate file by
mtime rather than the first file in a fixed list.
## 6. Mobile Behavior
At widths up to 980px, switch to one column.
Stacking order:
1. Sticky status bar, compressed to workspace basename.
2. Now band, collapsed to rows: token, run plus stepper, next action.
3. Blockers, if any.
4. Active agent.
5. Progress tail, collapsed by default to about `42vh`.
6. Runs accordion, collapsed.
At widths up to 560px:
- Toolbar becomes a compact wrapping row: tail selector, refresh, pause.
- Tap targets stay at least 44px.
- The segmented phase bar degrades to text.
- Mono ids and paths ellipsize.
- Prose wraps.
- No text overlap.
## 7. Implementation Notes
Likely write scope:
- `.agents/observability/index.html`
- `.agents/observability/styles.css`
- `.agents/observability/app.js`
- `.agents/scripts/observe.mjs` for additive API fields
Implementation guidance:
- No frameworks and no build step.
- Use a small inline SVG sprite or simple text/icon fallbacks.
- Keep two timers:
- `setInterval(poll, 5000)` rebuilds data.
- `setInterval(tick, 1000)` updates timestamp/countdown text only.
- Centralize constants:
- `POLL_MS`
- `DATA_STALE_MS`
- `LEASE_WARN_MS`
- `HEARTBEAT_WARN_MS`
- `HEARTBEAT_DEAD_MS`
- Add one pure `computeHealth(data)` helper.
- Add `formatAge(ms)` and `formatCountdown(ms)` helpers.
- Prefer `document.createElement` and `textContent` for rows instead of
template-literal `innerHTML`.
- Preserve progress scroll position and bottom-stick behavior across renders.
- Remember selected run id.
- Treat every optional API field as missing-safe.
- Add `role="status"` to freshness and `aria-live="polite"` to health and
blocker changes.
- Gate any pulse animation behind `prefers-reduced-motion`.
- Keep the CSS token approach and add a mono font token.
## 8. Concerns
1. Implementation is currently blocked by process. The workspace is not a git
repository, and Opus implementation must happen on a named branch from a
clean baseline.
2. Liveness is only honest if the manager writes `lease_expires_at` and
`heartbeat_at`. Missing values must render as unknown, not healthy.
3. Heartbeat cadence is unspecified. Proposed defaults are warning at 60 seconds
and dead at 180 seconds, but the manager should pick a real cadence.
4. Phase stepper depends on canonical phase strings. Unknown phases must degrade
gracefully.
5. Server-computed millisecond deltas are preferred to avoid client clock drift.
6. Polling cannot show sub-5-second events, which is acceptable for
human-paced orchestration.
7. Avoid adding timelines, charts, or history graphs in the first pass.
8. Non-active run progress depends on the optional `?run=` parameter. Without
it, run selection degrades to metadata-only.

View File

@@ -0,0 +1,10 @@
2026-06-02 15:05:10 EEST - User asked Codex manager to ask Opus to redesign the observability dashboard.
2026-06-02 15:05:10 EEST - Created observability redesign run directory and brief.
2026-06-02 15:06:20 EEST - Opus design proposal requested through Claude CLI.
2026-06-02 15:08:55 EEST - First Opus CLI invocation produced no output and was stopped; retrying with explicit read-only tools.
2026-06-02 15:10:50 EEST - User clarified Opus timeout must never be less than 30 minutes.
2026-06-02 15:10:50 EEST - Retry 1 exited with empty output; retrying with a 30-minute Opus timeout and lease.
2026-06-02 15:17:03 EEST - Opus returned observability redesign proposal; saved to opus-proposal.md.
2026-06-02 15:48:02 EEST - Manager attempted to initialize git baseline; sandbox blocked .git creation and escalation was rejected.
2026-06-02 15:49:58 EEST - User clarified git setup is Opus responsibility.
2026-06-02 15:49:58 EEST - Saved implementation instructions and requested Opus implementation on branch opus/observability-redesign.

View File

@@ -0,0 +1,12 @@
{
"schema": 1,
"run_id": "2026-06-02-observability-redesign",
"current_phase": "opus-implementation-requested",
"current_branch": "opus/observability-redesign",
"input_artifact": ".agents/runs/2026-06-02-observability-redesign/implementation-instructions.md",
"input_hash": "manual-observability-implementation-2026-06-02",
"expected_output_artifact": ".agents/runs/2026-06-02-observability-redesign/implementation-log.md",
"retry_count": 0,
"next_action": "Wait for Opus to initialize the git baseline and implement the observability redesign on branch opus/observability-redesign.",
"blocker": null
}

View File

@@ -0,0 +1,32 @@
{
"schema": 1,
"agents": [
{
"role": "codex-manager",
"status": "running",
"started_at": "2026-06-02T11:28:30Z",
"heartbeat_at": "2026-06-02T11:50:10Z",
"lease_expires_at": "2026-06-02T12:05:10Z",
"thread_id": "current-codex-thread",
"last_error": null
},
{
"role": "snarky",
"status": "completed",
"started_at": "2026-06-02T11:33:00Z",
"heartbeat_at": "2026-06-02T11:57:20Z",
"lease_expires_at": "2026-06-02T12:06:58Z",
"thread_id": "019e8831-eab0-7f20-9536-fb10f0f906f0",
"last_error": null
},
{
"role": "opus",
"status": "completed",
"started_at": "2026-06-02T11:33:00Z",
"heartbeat_at": "2026-06-02T12:02:05Z",
"lease_expires_at": "2026-06-02T12:21:27Z",
"tool_call_id": "claude-cli-session-54314",
"last_error": null
}
]
}

View File

@@ -0,0 +1,120 @@
# Discussion Round 1 - Opus
## Agreements
Opus accepts Snarky's eight decisions:
- Ship the NAS release console first.
- Drop `pub`/alias/namespace re-export from the critical path.
- Replace Milestone 1 with a walking skeleton.
- Make Linux/NAS the first runtime target.
- Allow FFI as a thin, named, replaceable boundary.
- Keep orchestration sequential and artifact-driven.
- Remove `Release.channel`, add release state, and use promotion history for
rollback.
- Use the v1 access model with app visibility, roles, scoped tokens, and signed
install links.
Opus also accepts Snarky's challenges: temporary boundaries must be named, M1
can be small but the product model cannot be vague, FFI must be Linux-first, and
the mock redesign must preserve iOS install truth.
## Remaining Technical Objections
1. **The cross-repo sx governance stop is still unresolved.**
FFI reduces compiler-bug surface but does not remove it. `#foreign` and
comptime are themselves active sx risk areas. If an M1 slice trips an sx bug,
`sx/CLAUDE.md` requires filing an issue, marking blocked, ending the session,
and not working around it. The distribution orchestration needs an explicit
`sx-blocked` state and handoff.
2. **M1 std signatures must pin to landed sx error handling.**
Do not implement against planned error semantics. Pin to the current landed
shape and record the checkpoint/commit.
3. **M1 APK inspection must not become an AXML decoder.**
For M1, artifact inspection means opening the zip central directory and
confirming `AndroidManifest.xml` exists with size/offset. Decoding binary
Android XML is M2.
4. **Enterprise install readiness depends on proxy HTTPS.**
`distd` cannot self-satisfy the Enterprise HTTPS requirement if TLS is behind
a reverse proxy. `dist doctor` can validate config/reachability, not TLS
termination by `distd`.
5. **Public visibility needs an iOS artifact-only carve-out.**
An iOS artifact-only IPA should never be anonymously downloadable, even on a
public app.
## Refinements To Snarky
- M1 JSON output should be the same shape later used by HTTP and remote
`dist ci publish`.
- M1 artifact inspection is zip-entry presence/size only.
- FFI boundaries should be named:
- miniz/libzip or zlib for zip reads
- libsqlite3 deferred to M2
- HMAC for signed links in M2
- SHA-256 remains a compiler-risk choice, not a portability mandate
- `rejected` is terminal.
- M1 releases can go `draft -> published` with `validation=not_run`.
- Signed install links are HMAC over `(artifact_id, expiry)` with a server key.
- Download auth should be a table across actor/auth mode, visibility, and
platform.
## Minimal Plan Patch Order
1. `git init` and baseline commit.
2. Patch Subplan 01:
- remove dangling references
- park `pub` as language-track work
- pin error model to landed sx state
- mark FFI-first/Linux boundaries
3. Patch `PLAN.md`:
- replace the first milestone with the walking skeleton
- demote HTTP/SQLite/UI/Docker to later milestones
4. Patch Subplan 02:
- remove `Release.channel`
- add `Release.state`
- add validation outcome
- add `ChannelPromotion`
5. Patch Subplan 03:
- local publish first
- add `dist.json` schema and example
- add idempotency key and rerun behavior
6. Patch Subplan 05:
- label validation checks as `required`, `warning`, `informational`, or
`not_supported`
- scope M1 APK to zip-entry presence
7. Patch Subplan 04:
- split Enterprise and MDM
- add TLS-by-proxy caveat
- define concrete routes and JSON shapes
8. Add access-policy section:
- roles
- visibility
- token scopes
- signed-link HMAC
- download authorization table
- iOS artifact-only never public
9. Patch orchestration:
- add `sx-blocked` state
- split Opus read-only consultation approval from write implementation
approval
## What Opus Needs Before Any Code
1. Answer the sx governance question: on an M1 compiler bug, does the
distribution run hard-stop and file in `sx/issues`, or is there a sanctioned
narrow-workaround budget?
2. Pin the current landed sx error-handling shape.
3. Approve the `dist.json` schema and idempotency key.
4. Approve the download authorization table.
5. Confirm libsqlite3 is M2 and M1 is DB-free.
6. For the mock branch, provide a Snarky `brief.md` with seed data and required
UI states.

View File

@@ -0,0 +1,138 @@
# Discussion Round 1 - Snarky
## Decisions
1. **Accept: ship the NAS release console first.**
The goal is to ship a working distribution platform in `sx`, not to mature
`sx` for its own sake. `sx` work is justified only when it directly unblocks
`dist`.
2. **Accept with narrowing: drop `pub` from the critical path.**
`pub`, alias imports, and `pub print :: core.print` remain desirable
language work, but they are no longer Phase 0 blockers for the product. They
move to a parallel/future language track unless a concrete implementation
blocker appears.
3. **Accept: replace Milestone 1 with a walking skeleton.**
M1 should be:
- `dist ci publish --local-store .dist`
- file-backed or JSON-backed domain model
- content-addressed artifact storage
- manifest parsing
- stable JSON output
- at least one simple artifact inspection path
No HTTP, SQLite, admin UI, Docker, or full IPA/APK validation in M1.
4. **Accept: Linux/NAS-first runtime.**
`distd` targets Linux Docker on UGREEN NAS first. macOS support is useful for
dev, but Linux correctness wins for server primitives.
5. **Accept: FFI is allowed as a thin boundary.**
For v1, FFI/system libs are acceptable for SQLite, hashing, archive
inspection, TLS boundary, and platform APIs. The product still lives in `sx`;
the wrappers must be explicit and replaceable.
6. **Accept: sequential orchestration stays.**
We are not doing parallel agents. Codex manages one visible workflow, Snarky
and Opus communicate through run artifacts, and Opus implements only on
branches.
7. **Decide release/channel model.**
`Release.channel` is removed. Releases have state. Channels point to
releases. Rollback uses promotion history, not magic memory.
Minimum states:
- `draft`
- `validating`
- `published`
- `rejected`
- `withdrawn`
`superseded` is derived from channel history, not a release state.
8. **Decide v1 access model.**
V1 supports:
- app visibility: `private`, `link`, `public`
- human roles: `admin`, `release_manager`, `viewer`
- CI tokens scoped by app and allowed actions
- downloads allowed by public visibility, signed install link,
authenticated user, or scoped token
## Challenges To Opus
1. "Build against sx as-is" is acceptable only if it does not create a junk
drawer of one-off shims. Opus needs to name every temporary boundary clearly.
2. "APK zip entry only" is fine for the walking skeleton, but the product plan
must still model iOS modes, access policy, release states, and validation
outcomes. M1 can be small; the product model cannot be vague.
3. FFI-first must be Linux-first. CommonCrypto-only, macOS-only sockets, or
host-specific assumptions are not acceptable for `distd`.
4. The UI mock redesign can proceed independently from the sx backend, but Opus
must preserve product truth: especially iOS TestFlight vs Enterprise vs MDM
vs artifact-only.
## Required Plan Patches
1. Fix Subplan 01 dangling references to removed `PLAN.md` sections.
2. Re-sequence language work:
- move `pub` and namespace re-export out of the blocker path
- keep actual required std primitives tied to product slices
3. Rewrite Milestone 1 as the walking skeleton.
4. Patch domain model:
- remove `Release.channel`
- add `Release.state`
- add `ChannelPromotion` / channel history
- define rollback semantics
5. Patch access policy:
- roles
- app visibility
- token scopes
- signed install links
- download authorization rules
6. Patch CI contract:
- concrete `dist.json` schema
- example manifest
- idempotency key
- rerun behavior
- duplicate version/build behavior
7. Patch HTTP API with concrete routes and JSON response shapes before HTTP
implementation.
8. Patch validation policy so every check is marked:
- `required`
- `warning`
- `informational`
- `not_supported`
9. Split iOS Enterprise and MDM. They are related, not identical.
10. Patch orchestration docs to make read-only Opus consultation and
write-capable Opus implementation separate approval modes.
## Implementation Gate
No implementation until:
- distribution repo has a clean git baseline
- plan patches above are applied
- active run has `brief.md`, `state.json`, and `agents.json`
- Opus has produced a design/technical proposal for the relevant slice
- Snarky has accepted product coverage
- allowed write paths are explicit
- branch is clean before Opus writes
For the website mock specifically: once those gates are satisfied, Opus may
redesign only the allowed UI files, and Snarky reviews product coverage
afterward.

View File

@@ -0,0 +1,93 @@
# Discussion Round 2 - Opus
## Final Opus Position
Opus accepts all six of Snarky's final decisions and accepted refinements:
- `sx-blocked` is a hard governance state.
- Error model is pinned to landed sx with real examples.
- M1 APK inspection is ZIP container plus `AndroidManifest.xml` presence only.
- Enterprise, MDM, proxy HTTPS readiness, and artifact-only are distinct states.
- Public visibility does not imply public IPA download.
- M1 is DB-free and SQLite moves to M2.
No open contest remains on product scope or sequencing.
## Any Remaining Blockers
1. **Cross-repo sx-authoring ownership is undefined.**
`sx-blocked` handles bugs, but not normal sx work. The DB-free skeleton still
requires new sx modules such as JSON, CLI, hash, and archive wrappers. Decide
whether distribution M1 consumes sx strictly as-is, or whether there is a
sanctioned sx-repo authoring lane under `sx/CLAUDE.md`.
2. **Repo/module-resolution boundary is undefined.**
Decide where `dist`/`distd` product code lives and how it imports sx stdlib
across the two repos.
3. **Subagent machinery versus single-thread rule is still unresolved.**
Either explicitly waive the no-fan-out rule for this project or drop the
machinery and keep only run-dir artifacts.
## Exact First Patch Set To Apply
1. `git init` and baseline commit in `/Users/agra/projects/distribution`.
2. Patch `PLAN.md` to:
- replace First Milestone with DB-free walking skeleton
- demote HTTP, SQLite, admin UI, Docker to M2+
- move `pub`/alias/re-export off the critical path
3. Patch orchestration docs:
- add `sx-blocked`
- add cross-repo handoff
- split Opus read-only approval from write approval
- record subagent/single-thread decision
4. Patch Subplan 01:
- remove dead `PLAN.md` references
- park `pub`
- pin landed sx commit and error examples
- mark FFI-first/Linux-first boundaries
5. Patch Subplan 02:
- remove `Release.channel`
- add `Release.state`
- add validation outcome
- add `ChannelPromotion`
- mark SQLite as M2
6. Patch Subplan 03:
- local publish becomes M1
- add concrete `dist.json` schema and example
- add idempotency and rerun semantics
- clarify `dist doctor` TLS/proxy checks
7. Patch Subplan 05:
- classify every validation check
- scope M1 APK to zip-entry metadata
- move full IPA/APK checks post-M1
8. Patch Subplan 04:
- split Enterprise, MDM, artifact-only
- add TLS caveat
- add concrete routes and JSON shapes
- mark HTTP as M2+
9. Add access policy section:
- roles
- visibility
- token scopes
- signed-link HMAC
- download authorization table
- iOS artifact-only never public
10. Author the real implementation run directory for the chosen first slice.
## Whether Implementation Can Start After Those Patches
Product/sx track:
- No, not after plan patches alone.
- Still needs sx authoring lane, module resolution boundary, pinned sx commit
and error examples, accepted Opus proposal, clean branch, and write-paths.
Mock-redesign track:
- Yes, after git baseline plus a Snarky `brief.md`.
- It has no sx/backend dependency and can run independently under the UI
allowed paths.

View File

@@ -0,0 +1,82 @@
# Discussion Round 2 - Snarky
## Final Decisions
1. **`sx-blocked` is a hard governance state.**
If an M1 slice hits an `sx` compiler/runtime/FFI bug, that slice stops. File
or link the `sx` issue, record it in the distribution run, mark
`sx-blocked`, and do not invent local workarounds unless Snarky explicitly
approves a narrow boundary change.
2. **Error model is pinned to landed `sx`, not imagined `sx`.**
No `!T` folklore, no planned syntax, no aspirational std signatures. Before
std work starts, pin the exact `sx` commit and include tiny real examples of
current error handling.
3. **M1 APK inspection is deliberately tiny.**
M1 only proves ZIP container access and confirms `AndroidManifest.xml` exists
with basic entry metadata. No AXML decoder, no package/version parsing, no
signature validation.
4. **Enterprise, MDM, and proxy readiness must be separate.**
Enterprise install, MDM install, and artifact-only IPA access are distinct
product states. `distd` behind a reverse proxy may validate configured HTTPS
reachability, but it must not claim to terminate or own TLS.
5. **Public visibility has iOS carve-outs.**
Public app visibility does not mean public IPA download. iOS artifact-only
IPAs require authenticated access, scoped token access, or signed install
link access.
6. **M1 is DB-free.**
M1 uses file-backed or JSON-backed state only. SQLite is M2.
## Accepted Opus Refinements
- M1 JSON output should become the shape later reused by HTTP and remote CI.
- `rejected` is terminal.
- M1 release path may be `draft -> published` with `validation=not_run`.
- Signed install links are HMAC over artifact/link identity plus expiry, with a
server secret.
- Download authorization must be a table, not prose soup.
- FFI is acceptable when named, Linux-first, thin, and replaceable.
## Rejected Or Deferred Items
- `pub`, alias imports, and namespace re-export are deferred from the product
critical path.
- Pure-`sx` crypto/archive/SQLite is deferred.
- HTTP, SQLite, admin UI, Docker/NAS deployment, full APK validation, and IPA
validation are not M1.
- Any layout redesign before product coverage is pinned is rejected.
## Plan Patch Backlog
1. Add `sx-blocked` state and cross-repo handoff rules.
2. Patch Subplan 01 to remove dangling `PLAN.md` references and pin landed
error handling.
3. Rewrite M1 as the DB-free walking skeleton.
4. Patch domain model: release states, validation outcome, channel promotion
history.
5. Patch CI manifest schema, example, idempotency key, rerun behavior.
6. Patch access policy with roles, visibility, token scopes, signed links, and
download table.
7. Patch validation policy with `required`, `warning`, `informational`,
`not_supported`.
8. Split Enterprise, MDM, proxy HTTPS readiness, and iOS artifact-only behavior.
## Implementation Still Blocked Until
- Distribution repo has a clean git baseline.
- Plan patches above are applied.
- Active run has `brief.md`, `state.json`, and `agents.json`.
- Current `sx` commit and error model are pinned.
- Opus produces a proposal for the exact slice.
- Snarky accepts product coverage.
- Allowed write paths and branch are explicit and clean.

View File

@@ -0,0 +1,267 @@
# Opus Consultation - Distribution Platform Plan Review
## Context
Read-only review of the distribution platform plan package (`PLAN.md`,
`.agents/ORCHESTRATION.md`, `.agents/subplans/*.md`, checkpoints) against the
real state of the `sx` repository, from four lenses: design, technical
sequencing, implementation feasibility, and platform risk.
Verdict in one line: **the plan is well-written and the product model is mostly
right, but it is sequenced as if `sx` were a mature systems language. It is
not.** `sx` is self-described "highly experimental" with ~37 open compiler
issues and five active fix-streams, and its own governance forbids working
around compiler bugs. The plan's Phase 0-1 (build a full HTTP/TLS/SQLite/
crypto/JSON/archive stdlib in `sx` first) is the single largest risk and is
underestimated by roughly an order of magnitude.
This file is a consultation report, not an implementation plan. Findings are
severity-ordered with file references. The last sections are where Opus would
overrule/refine/ask, and the layout read on the rejected mock (Opus's authority
domain).
---
## Findings (severity-ordered)
### P0 - The two repos have incompatible governance; nobody owns the sx changes
- Distribution `ORCHESTRATION.md:24` makes Opus the *only* code writer, on
named git branches, under lease timeouts. But the work in subplan 01 is
**`sx` compiler + stdlib work**, and `sx/CLAUDE.md:5-65` ("IMPASSIBLE RULES")
mandates: the moment you hit a compiler bug, **STOP, file an issue, end the
session, wait for a fix in another session - do not work around it.**
- Building HTTP/TLS/SQLite/zip/crypto on an experimental compiler with 37 open
issues *will* hit unimplemented/buggy paths repeatedly (FFI itself is an
active, buggy stream: see `sx/issues/0043,0052,0057`). Under sx's own rules,
the distribution "Opus implementation" phase will keep hard-stopping.
- The two checkpoint systems (distribution `.agents/runs/*` vs sx
`current/CHECKPOINT-*.md`) have **no defined handoff**. There is no answer to
"who advances sx, in which repo's process, under whose lease."
- **This is the gating decision for the whole program** and it is not addressed
anywhere in the plan.
### P0 - Distribution workspace is not a git repo (already flagged, still true)
- `subplan 08:194-198`, `CHECKPOINT.md:48-53`, `checkpoint.json:8-11`. The
entire orchestration is branch-based; it cannot start. Snarky P0 is correct.
Trivial to fix (`git init` + baseline commit) but it blocks everything, so
it should be the literal first action.
### P1 - Phase 0 "language prerequisites" are largely gold-plating, not prerequisites
- `PLAN.md:159-190`, `subplan 01:23-52` require `pub` exports, alias imports,
and `pub print :: core.print` namespace re-export **before product code**.
- `pub`/module-member visibility **does not exist** in `sx` (specs.md:550 only
has import-scoped `impl` visibility) and is **on no active sx workstream**.
So the plan's first "blocking" slice is net-new parser/AST/resolver/
module-graph compiler work that nobody is scheduled to do.
- But `sx` already has namespaced imports (`math :: #import "..."`) and
import-scoped impl visibility. **The product can be built without `pub`.**
Treating it as a prerequisite front-loads hard compiler work and delays any
product signal. (See "Opus would overrule," below.)
### P1 - The server substrate that Phase 1 assumes does not exist, and the one primitive that does is wrong-platform
- Confirmed absent in `library/modules/`: HTTP, TLS, SQLite, JSON, SHA-256/
crypto, base64/hex, RFC3339/time, CLI parser, config, zip/tar/gzip. Subplan
01 Slices 6-8 (`subplan 01:104-145`) describe all of these as if they are
small deliverables. Each is a real subsystem.
- `library/modules/socket.sx:1-2,23-29` is the only networking code: raw POSIX,
**macOS-only** (`sin_len`, macOS `SO_REUSEADDR=0x4`), blocking `read`/`write`,
no event loop. The deployment target is **Linux Docker on UGREEN NAS**
(`PLAN.md:147-158`), where `sockaddr_in` has no `sin_len` and constants
differ. The HTTP foundation must be (re)written Linux-correct before `distd`
can run where it ships.
- Implication: every Phase-1 "slice" is actually a from-scratch systems library
on an unstable compiler. This is the bulk of the project, mislabeled as
foundation.
### P1 - `IPA`/`APK` validation needs a ZIP reader that does not exist
- `subplan 05:19-48` requires reading the zip structure of IPA (Info.plist) and
APK (AndroidManifest) to validate bundle id / version / signature. IPA and
APK are ZIP containers. There is no `std.archive`/zip reader (`subplan
01:137`), and `AndroidManifest.xml` inside an APK is *binary* XML (AXML), not
text - a non-trivial parser on its own. This is materially harder than the
one-line "manifest package id" bullet implies.
### P1 - Release/Channel model is internally contradictory and cannot support rollback as specified
- Snarky already flagged the redundancy (`Release.channel` at `subplan
02:43-54` vs `Channel.current_release_id` at `subplan 02:56-61`). Concur.
- Beyond that: `Channel` stores only `current_release_id` with **no promotion
history**. `dist release rollback` "moves a channel pointer to the previous
valid release" (`subplan 03:45-47`) - but "previous" is unknowable from a
single pointer. Rollback requires a promotion/channel-history table (or
deriving it from audit events). The data model as drawn cannot do what the
CLI promises.
- No release state machine is defined (`draft/validating/published/rejected/
superseded`). Snarky P1 #3 is correct and should block domain implementation.
### P1 - The multi-agent orchestration conflicts with the operator's own standing rules
- `ORCHESTRATION.md` + `subplan 08` are built on multiple named subagents
(Codex/Snarky/Opus) invoked through an `opus-runner` MCP plugin
(`ORCHESTRATION.md:136-146`), which is **unbootstrapped** ("may need Codex
reload before MCP tools are available," `CHECKPOINT.md:52-53`).
- The operator's global rules forbid subagent fan-out and require work in a
single visible thread. The orchestration design is therefore in direct
tension with how the operator actually wants work done. Either the global
rule is waived for this project, or the multi-agent machinery should be
dropped in favor of sequential single-thread phases with the same run-dir
artifacts. This needs an explicit decision before more tooling is built.
### P2 - Milestone 1 is the whole project, not a milestone
- `PLAN.md:259-272` bundles: sx language work + full stdlib + server + SQLite +
APK *and* IPA validators + channels + install pages + admin UI + Docker/NAS.
On an experimental language. Snarky P2 #8 is correct; I'd escalate it to P1
for scheduling purposes. Replace with a "walking skeleton" (below).
### P2 - Validation policy mixes requirements with wishful tooling
- `subplan 05` is full of "if tool support exists," "when provided," "malware
scan placeholder" (`05:55-89`). Concur with Snarky P2 #7: for v1 mark each
check `required | warning | informational | not-supported` and delete the
rest. Notarization/authenticode/malware-scan are out of reach for v1 and
should be `not-supported`, not aspirational statuses.
### P2 - iOS Enterprise vs MDM are conflated
- `subplan 04:50-52` / `05:24-28` treat "Enterprise/MDM" as one mode serving "a
signed HTTPS manifest plist for enrolled devices." Those are two different
mechanisms: `itms-services://` in-house (Apple Enterprise Program, cert-trust
on device) vs MDM-managed `InstallApplication`. The plist/host requirements
differ. The *product intent* (don't imply a normal iPhone can sideload) is
correct and is the strongest part of the plan - just split the two modes.
- Also: `distd` cannot self-satisfy the HTTPS requirement (no TLS); it relies on
the reverse proxy. `dist doctor`'s "HTTPS base URL" check (`subplan 03:14-16`)
can therefore only validate config/reachability, not terminate TLS itself.
State that explicitly so Enterprise mode isn't marked "ready" when it isn't.
### P2 - Subplan 01 still points implementers at deleted PLAN sections
- `subplan 01:18-21` tells implementers to read `Standard Library API Surface`
and `Detailed Std Struct And Method Sketches`, removed from `PLAN.md`. Snarky
P1 #4 confirmed; fix or the first sx session starts by chasing ghosts.
### P3 - CI manifest schema / idempotency undefined
- `subplan 03:48-63` lists fields but no JSON schema, example, idempotency key,
or rerun semantics. "CI is the primary writer" and "releases are immutable"
(`PLAN.md:38-42`) make rerun-of-same-build behavior a v1 correctness concern,
not a later detail. Define an idempotency key (app+version+build digest) now.
---
## Where Opus would overrule, refine, or ask Snarky
**Overrule - drop Phase 0 language work from the critical path.**
`pub`/alias-imports/namespace-re-export (`PLAN.md:194-196`, `subplan 01:23-52`)
are ergonomics, not blockers. sx already has namespaced imports and impl
visibility. Building net-new compiler features before the product compiles once
is exactly backwards on an unstable compiler. Opus would build the product
against the language as-is and only add `pub` if a concrete name-leak actually
bites. This reorders the whole plan and Opus would hold this line against a
"do it properly first" push.
**Overrule - Milestone 1 is replaced by a walking skeleton.** Opus's M1:
`dist ci publish --local-store` writes a content-addressed artifact to disk +
an in-memory/JSON-file domain model + one platform's metadata read (APK zip
entry only) + a JSON output contract. No HTTP, no SQLite, no TLS, no UI. This
exercises the real sx pain (fs, zip, hashing, JSON, CLI) on the smallest
surface and produces a runnable artifact in days, not a quarter. SQLite/HTTP/
admin-UI become M2+, each gated on the prior actually running on Linux.
**Refine - make the SHA-256 and zip decisions explicitly FFI-first.** Pure-sx
streaming SHA-256 and inflate are real projects and will trip compiler bugs.
Bind libsqlite3, a crypto lib (CommonCrypto on mac / OpenSSL on Linux), and
miniz/libzip via `#foreign` first; only reimplement in sx if FFI proves worse.
This trades "pure sx" purity for actually shipping, and keeps bug-surface on the
linker, not the comptime evaluator.
**Refine - model promotion history, not just a channel pointer.** Add a
`channel_release` history (or derive rollback from audit). Without it, rollback
is undefined.
**Ask Snarky to decide before any code:**
1. Is the goal to *ship a NAS release console* or to *mature sx*? They imply
opposite sequencing. If shipping is the goal, the sx-first framing is wrong.
2. Is the multi-agent/opus-runner machinery actually required, given the
operator's single-thread rule? If not, delete it and keep only run-dir
artifacts.
3. Linux-first or macOS-first for `distd` runtime? The only socket code is
macOS-only; the deploy target is Linux. Pick one and make the socket layer
match it before "server skeleton."
4. For v1, is FFI-to-system-libs acceptable, or is "pure sx stdlib" a hard
product constraint? This single answer changes the size of the project by
months.
---
## Layout read on the rejected mock (Opus authority domain)
The mock (`index.html`, `styles.css`) is **not** weak at the system level: warm
off-white token palette, semantic green/amber/red with soft fills, Inter, a real
shadow token, a clean CSS-grid shell (224px sidebar + 64px topbar,
`styles.css:49-69`). The information architecture (apps list + detail, release,
install, tokens, audit, settings) matches the brief. So the rejection is almost
certainly about *finish and tone*, not structure:
1. **Placeholder iconography reads as a wireframe.** Single letters as icons
everywhere: brand "d," nav glyphs `A/R/I/T/L/S` (`index.html:41-64`), top
actions literally rendering "R" for sync and "!" for notifications
(`index.html:26-31`), 2-letter app monograms, and a QR faked as empty
`<span>`s (`index.html:439-446`). Nothing signals "unfinished" harder than
letter-in-a-box icons. A minimal real stroke-icon set would lift perceived
quality more than any layout change.
2. **Tone fights the brief.** The brief wants "operational SaaS density, quiet,
no oversized marketing hero" (`subplan 06:64-72`). The mock leans airy/
card-y with big metric numbers and a phone-mockup hero on the Install tab
(`index.html:362-382`) - that's the marketing-flavored hero the brief warns
against. A release console likely wants a denser, cooler, more devtools/
terminal register than warm paper.
3. **Six fake views via class-toggle** inflate surface area that all has to look
finished at once; it spreads the polish too thin.
Opus's design direction for `opus/redesign-distribution-mock`: keep the 3-zone
shell and IA; (a) replace every letter-glyph with a real minimal icon set; (b)
shift to a cooler, denser operator palette and a tighter type/spacing scale; (c)
demote the Install phone-hero to an inline preview, not a centerpiece; (d) make
the first viewport land on real working content (apps + latest release), per
`subplan 06:94-99`; (e) keep the iOS TestFlight/Enterprise/Artifact distinction
loud - it's the one thing the current mock gets exactly right
(`index.html:384-437`).
---
## Recommended re-sequencing (concise)
1. `git init` the distribution workspace; baseline commit. (Unblocks
everything.)
2. Snarky decides the four questions above (ship-vs-mature, agents-yes/no,
Linux-vs-mac, FFI-vs-pure-sx). These are gating.
3. Drop Phase 0 language work from the critical path.
4. Build the walking-skeleton M1 (`dist ci publish --local-store`, content-
addressed fs store, JSON contract, APK zip-entry read) against sx as-is,
FFI-first for hash/zip.
5. Only then add SQLite (FFI), then the HTTP server (Linux-correct sockets),
then install pages, then admin UI - each gated on the prior running on the
actual Linux/NAS target.
6. In parallel and independently: the mock redesign (no backend dependency),
under Opus design direction above.
## Verification (how to sanity-check the re-scope, read-only)
- Confirm `pub` truly absent: `sx ir` / compile a probe with `pub x :: 1` and
observe parser rejection (don't land it - just confirm scope of work).
- Confirm Linux socket gap: attempt `sx build --target linux` of a trivial
`socket.sx` consumer and observe `sin_len` mismatch.
- Confirm error model present (it is): `sx/CHECKPOINT-ERR.md:15`, plus the
`examples/10xx-errors-*` suite passing (341 tests, `CHECKPOINT-ERR.md:42-43`).
- For the redesign: build the static mock and screenshot the first viewport on
desktop + mobile widths to validate the "working content first, no hero"
acceptance (`subplan 06:94-99`).
</content>
</invoke>

View File

@@ -0,0 +1,20 @@
2026-06-02 14:28:30 EEST - Created active planning run after noticing the observability dashboard only showed durable run records.
2026-06-02 14:28:30 EEST - Registered Codex manager as the active planning agent.
2026-06-02 14:28:30 EEST - Next action: explain that planning sessions must create .agents/runs records to be visible.
2026-06-02 14:30:39 EEST - Reviewing PLAN.md and .agents/subplans for gaps.
2026-06-02 14:31:38 EEST - User asked to introduce Snarky and Opus to review the plan gaps.
2026-06-02 14:33:00 EEST - Snarky and Opus marked running for plan-gap consultation.
2026-06-02 14:33:00 EEST - Snarky spawned as sub-agent Locke / 019e881c-6aa7-7901-8069-22411d955e06.
2026-06-02 14:34:22 EEST - Opus CLI consultation blocked pending explicit approval for external plan-file disclosure.
2026-06-02 14:34:22 EEST - Snarky completed plan review; saved to snarky-review.md.
2026-06-02 14:36:22 EEST - User asked how to give Opus access; waiting for explicit approval scope.
2026-06-02 14:37:39 EEST - User approved Opus access to the entire distribution and sx repos for consultation.
2026-06-02 14:39:33 EEST - Opus Claude CLI review running with read-only tools and access to distribution plus sx repos.
2026-06-02 14:45:10 EEST - Opus completed review; saved to opus-review.md.
2026-06-02 14:50:05 EEST - User requested a few more Snarky/Opus discussion rounds before implementation.
2026-06-02 14:50:05 EEST - Snarky round 1 spawned as sub-agent Linnaeus / 019e882b-f0a8-7ed1-ab9d-fa32caf8d5ad.
2026-06-02 14:50:05 EEST - Snarky round 1 completed and saved to discussion-round-1-snarky.md.
2026-06-02 14:56:10 EEST - Opus round 1 completed and saved to discussion-round-1-opus.md.
2026-06-02 14:56:58 EEST - Snarky round 2 spawned as sub-agent Fermat / 019e8831-eab0-7f20-9536-fb10f0f906f0.
2026-06-02 14:57:20 EEST - Snarky round 2 completed and saved to discussion-round-2-snarky.md.
2026-06-02 15:02:05 EEST - Opus round 2 completed and saved to discussion-round-2-opus.md.

View File

@@ -0,0 +1,63 @@
# Snarky Review
Findings first, because vibes are not a roadmap.
1. **P0: Opus cannot implement yet because the workspace has no git baseline.**
Branch-based implementation is mandatory, but the plan admits this workspace
is not a git repo. Snarky will not approve an Opus implementation phase until
git is initialized or attached and the current baseline is committed.
2. **P1: Access policy is hand-waved, and this product lives or dies there.**
The plan names actors and says access policy decides install/download, but
does not define auth modes, human sessions, public links, private apps, app
ownership, RBAC, or scoped install access. "Authenticated and public modes"
is not a model.
3. **P1: Release lifecycle is internally muddy.**
Releases are supposed to be immutable and channels mutable. But `Release`
still has `channel`, while `Channel` separately points to
`current_release_id`. Also the CLI creates a draft release, validates,
publishes, and maybe promotes, but no state machine is defined. Define
`draft`, `validating`, `published`, `rejected`, `superseded`, etc.
4. **P1: The std subplan references sections that no longer exist.**
Subplan 01 tells implementers to read `Standard Library API Surface` and
`Detailed Std Struct And Method Sketches`. Those were removed from
`PLAN.md`.
5. **P1: CI manifest and idempotency are underspecified.**
The CLI plan gives required fields, but not a schema, examples, retry
semantics, duplicate release behavior, idempotency keys, or what happens
when CI reruns the same build.
6. **P1: HTTP API is too abstract to build against.**
The API plan lists slices but no concrete routes, request bodies, response
schemas, pagination/filtering, auth errors, upload protocol, or download
authorization rules.
7. **P2: Validation policy mixes product requirements with wishful tooling.**
"If tool support exists," "provided or checked," and "malware scan
placeholder" are not acceptance criteria. For v1, mark each check as
`required`, `warning`, `informational`, or `not supported`.
8. **P2: Milestone 1 is too large to be a milestone.**
It includes sx primitives, local server, APK and IPA publishing, SQLite,
channels, install pages, admin UI, and NAS Docker deployment.
## Required Before Opus Implements
- Create a clean git baseline.
- Write a Snarky `brief.md` for the redesign run with hard acceptance criteria.
- Resolve the minimum product model for the mock: release states, channel
behavior, access modes, token scopes, and iOS install modes.
- Provide seed data and required UI states Opus must represent.
- Let Opus produce a design proposal first. No code until Snarky accepts the
product coverage.

View File

@@ -0,0 +1,12 @@
{
"schema": 1,
"run_id": "2026-06-02-orchestration-planning",
"current_phase": "snarky-opus-discussion",
"current_branch": null,
"input_artifact": "user request: why is the current planning session not visible in observability",
"input_hash": "manual-current-turn",
"expected_output_artifact": ".agents/runs/2026-06-02-orchestration-planning/progress.log",
"retry_count": 0,
"next_action": "Apply consensus plan patches, then create a git baseline before implementation.",
"blocker": null
}

219
.agents/scripts/observe.mjs Normal file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env node
import { createServer } from "node:http";
import {
existsSync,
readdirSync,
readFileSync,
statSync,
} from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const workspace = path.resolve(scriptDir, "../..");
const agentsDir = path.join(workspace, ".agents");
const runsDir = path.join(agentsDir, "runs");
const checkpointPath = path.join(agentsDir, "checkpoint.json");
const staticDir = path.join(agentsDir, "observability");
const port = parsePort(process.argv) || 4317;
const server = createServer((req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname === "/api/status") {
const tail = parseTail(url.searchParams.get("tail"));
sendJson(res, buildStatus(tail));
return;
}
serveStatic(res, url.pathname);
});
server.listen(port, "127.0.0.1", () => {
console.log(`Agent observability dashboard: http://127.0.0.1:${port}`);
});
server.on("error", (error) => {
console.error(`Could not start observability dashboard on 127.0.0.1:${port}`);
console.error(error.message);
process.exit(1);
});
function parsePort(argv) {
const index = argv.findIndex((arg) => arg === "--port" || arg === "-p");
if (index === -1) return null;
const value = Number(argv[index + 1]);
if (!Number.isInteger(value) || value < 1 || value > 65535) return null;
return value;
}
function parseTail(value) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) return 80;
return Math.min(parsed, 500);
}
function buildStatus(tailLines) {
const checkpoint = readJsonIfExists(checkpointPath);
const runs = listRuns();
const activeRunId = checkpoint?.active_run_id || findLatestRunId(runs);
const activeRun = runs.find((run) => run.id === activeRunId) || null;
return {
generated_at: new Date().toISOString(),
workspace,
checkpoint,
summary: {
run_count: runs.length,
active_run_id: activeRunId,
current_phase: checkpoint?.current_phase || null,
active_branch: checkpoint?.active_branch || null,
blocker_count: Array.isArray(checkpoint?.blockers) ? checkpoint.blockers.length : 0,
next_action: checkpoint?.next_action || null,
},
runs,
active_progress: activeRun ? readProgress(activeRun, tailLines) : null,
};
}
function listRuns() {
if (!existsSync(runsDir)) return [];
return readdirSync(runsDir)
.map((name) => path.join(runsDir, name))
.filter((runPath) => statSync(runPath).isDirectory())
.map((runPath) => {
const id = path.basename(runPath);
const state = readJsonIfExists(path.join(runPath, "state.json"));
const agents = normalizeAgents(readJsonIfExists(path.join(runPath, "agents.json")));
return {
id,
path: path.relative(workspace, runPath),
state,
agents: agents.map((agent) => ({
...agent,
lease_expired: isExpired(agent.lease_expires_at),
})),
mtime_ms: statSync(runPath).mtimeMs,
};
})
.sort((a, b) => b.mtime_ms - a.mtime_ms);
}
function normalizeAgents(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
if (Array.isArray(value.agents)) return value.agents;
return Object.entries(value).map(([role, agent]) => ({
role,
...(typeof agent === "object" && agent ? agent : { status: String(agent) }),
}));
}
function findLatestRunId(runs) {
if (runs.length === 0) return null;
return runs.reduce((latest, run) => (run.mtime_ms > latest.mtime_ms ? run : latest)).id;
}
function readProgress(run, tailLines) {
const runPath = path.join(workspace, run.path);
const candidateNames = [
"progress.log",
"implementation-log.md",
"validation.md",
"opus-proposal.md",
"snarky-review.md",
];
const filePath = candidateNames
.map((name) => path.join(runPath, name))
.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
if (!filePath) {
return {
run_id: run.id,
path: null,
lines: [],
};
}
const lines = readFileSync(filePath, "utf8").split(/\r?\n/);
if (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop();
}
return {
run_id: run.id,
path: path.relative(workspace, filePath),
lines: lines.slice(Math.max(0, lines.length - tailLines)),
};
}
function isExpired(value) {
if (!value) return false;
const time = Date.parse(value);
if (!Number.isFinite(time)) return false;
return time < Date.now();
}
function readJsonIfExists(filePath) {
if (!existsSync(filePath)) return null;
try {
return JSON.parse(readFileSync(filePath, "utf8"));
} catch (error) {
return {
parse_error: error.message,
};
}
}
function sendJson(res, value) {
const body = JSON.stringify(value);
res.writeHead(200, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
});
res.end(body);
}
function serveStatic(res, pathname) {
const relative = pathname === "/" ? "index.html" : pathname.slice(1);
const filePath = path.resolve(staticDir, relative);
if (filePath !== staticDir && !filePath.startsWith(`${staticDir}${path.sep}`)) {
sendNotFound(res);
return;
}
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
sendNotFound(res);
return;
}
const body = readFileSync(filePath);
res.writeHead(200, {
"Content-Type": contentType(filePath),
"Content-Length": body.length,
});
res.end(body);
}
function sendNotFound(res) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
}
function contentType(filePath) {
const extension = path.extname(filePath);
switch (extension) {
case ".html":
return "text/html; charset=utf-8";
case ".css":
return "text/css; charset=utf-8";
case ".js":
return "text/javascript; charset=utf-8";
case ".json":
return "application/json";
default:
return "application/octet-stream";
}
}

192
.agents/scripts/status.mjs Normal file
View File

@@ -0,0 +1,192 @@
#!/usr/bin/env node
import {
existsSync,
readdirSync,
readFileSync,
statSync,
} from "node:fs";
import path from "node:path";
const workspace = process.cwd();
const agentsDir = path.join(workspace, ".agents");
const runsDir = path.join(agentsDir, "runs");
const checkpointPath = path.join(agentsDir, "checkpoint.json");
const tailLines = parseTailLines(process.argv);
main();
function main() {
const checkpoint = readJsonIfExists(checkpointPath);
const runs = listRuns();
const activeRunId = checkpoint?.active_run_id || findLatestRunId(runs);
printHeader(checkpoint, runs);
if (runs.length === 0) {
console.log("No runs found under .agents/runs.");
console.log("Next: create .agents/runs/<run-id>/ with state.json and agents.json.");
return;
}
for (const run of runs) {
printRun(run, run.id === activeRunId);
}
const activeRun = runs.find((run) => run.id === activeRunId);
if (activeRun) {
printProgressTail(activeRun);
}
}
function parseTailLines(argv) {
const tailIndex = argv.findIndex((arg) => arg === "--tail" || arg === "-n");
if (tailIndex === -1) return 30;
const value = Number(argv[tailIndex + 1]);
if (!Number.isFinite(value) || value < 0) return 30;
return Math.floor(value);
}
function printHeader(checkpoint, runs) {
console.log("Agent Orchestration Status");
console.log("==========================");
console.log(`Workspace: ${workspace}`);
console.log(`Runs: ${runs.length}`);
console.log(`Active run: ${checkpoint?.active_run_id || "(none)"}`);
console.log(`Current phase: ${checkpoint?.current_phase || "(unknown)"}`);
console.log(`Active branch: ${checkpoint?.active_branch || "(none)"}`);
console.log(`Next action: ${checkpoint?.next_action || "(none recorded)"}`);
const blockers = Array.isArray(checkpoint?.blockers) ? checkpoint.blockers : [];
if (blockers.length > 0) {
console.log("Blockers:");
for (const blocker of blockers) {
console.log(`- ${blocker}`);
}
}
console.log("");
}
function listRuns() {
if (!existsSync(runsDir)) return [];
return readdirSync(runsDir)
.map((name) => path.join(runsDir, name))
.filter((runPath) => statSync(runPath).isDirectory())
.map((runPath) => {
const id = path.basename(runPath);
return {
id,
path: runPath,
state: readJsonIfExists(path.join(runPath, "state.json")),
agents: normalizeAgents(readJsonIfExists(path.join(runPath, "agents.json"))),
mtimeMs: statSync(runPath).mtimeMs,
};
})
.sort((a, b) => a.id.localeCompare(b.id));
}
function normalizeAgents(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
if (Array.isArray(value.agents)) return value.agents;
return Object.entries(value).map(([role, agent]) => ({
role,
...(typeof agent === "object" && agent ? agent : { status: String(agent) }),
}));
}
function findLatestRunId(runs) {
if (runs.length === 0) return null;
return runs.reduce((latest, run) => (run.mtimeMs > latest.mtimeMs ? run : latest)).id;
}
function printRun(run, isActive) {
const state = run.state || {};
const marker = isActive ? "*" : "-";
console.log(`${marker} Run: ${run.id}`);
console.log(` Phase: ${state.current_phase || state.phase || "(unknown)"}`);
console.log(` Branch: ${state.current_branch || state.branch || "(none)"}`);
console.log(` Expected output: ${state.expected_output_artifact || state.expected_output || "(none)"}`);
console.log(` Retry count: ${state.retry_count ?? 0}`);
console.log(` Next action: ${state.next_action || "(none recorded)"}`);
if (state.blocker) console.log(` Blocker: ${state.blocker}`);
if (run.agents.length === 0) {
console.log(" Agents: none recorded");
} else {
console.log(" Agents:");
for (const agent of run.agents) {
printAgent(agent);
}
}
console.log("");
}
function printAgent(agent) {
const role = agent.role || "(unknown-role)";
const status = agent.status || "(unknown)";
const heartbeat = agent.heartbeat_at || "(none)";
const lease = agent.lease_expires_at || "(none)";
const expired = isExpired(agent.lease_expires_at);
const thread = agent.thread_id || agent.process_id || agent.tool_call_id || "(none)";
console.log(` - ${role}: ${status}${expired ? " [lease expired]" : ""}`);
console.log(` heartbeat: ${heartbeat}`);
console.log(` lease: ${lease}`);
console.log(` id: ${thread}`);
if (agent.last_error) {
console.log(` last_error: ${agent.last_error}`);
}
}
function printProgressTail(run) {
if (tailLines === 0) return;
const candidateNames = [
"progress.log",
"implementation-log.md",
"validation.md",
"opus-proposal.md",
"snarky-review.md",
];
const progressPath = candidateNames
.map((name) => path.join(run.path, name))
.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
console.log("Progress Tail");
console.log("=============");
if (!progressPath) {
console.log(`No progress file found for run ${run.id}.`);
console.log("Expected one of: progress.log, implementation-log.md, validation.md.");
return;
}
console.log(`${path.relative(workspace, progressPath)} last ${tailLines} lines:`);
const lines = readFileSync(progressPath, "utf8").split(/\r?\n/);
if (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop();
}
const tail = lines.slice(Math.max(0, lines.length - tailLines));
for (const line of tail) {
console.log(line);
}
}
function isExpired(value) {
if (!value) return false;
const time = Date.parse(value);
if (!Number.isFinite(time)) return false;
return time < Date.now();
}
function readJsonIfExists(filePath) {
if (!existsSync(filePath)) return null;
try {
return JSON.parse(readFileSync(filePath, "utf8"));
} catch (error) {
return {
parse_error: error.message,
};
}
}

View File

@@ -0,0 +1,153 @@
# Subplan 01 - sx Language And Standard Library
## Goal
Build the language and std primitives required before the distribution platform
can be implemented cleanly in sx.
## Work Location
- Primary repo: `/Users/agra/projects/sx`
- Planning repo: `/Users/agra/projects/distribution`
## Prerequisites
- Read `/Users/agra/projects/sx/readme.md`
- Read `/Users/agra/projects/sx/specs.md`
- Read `/Users/agra/projects/sx/docs/error-handling.md`
- Re-read `PLAN.md` sections:
- `Phase 0 - sx Language and Module Prerequisites`
- `Standard Library API Surface`
- `Detailed Std Struct And Method Sketches`
## Slice 1 - Public Exports And Namespace Aliases
Deliver:
- Add `pub` declarations for public module members.
- Add public namespace member re-export aliases:
- `pub print :: core.print`
- `pub format :: core.format`
- Support alias imports without forcing every caller to know the source module.
- Add parser, AST, resolver, and compiler tests.
- Add examples for module barrels such as `std.sx`.
Acceptance:
- Private module members are not importable outside their namespace.
- Re-exported members keep correct identity for diagnostics and docs.
- Cycles and duplicate public names produce clear errors.
## Slice 2 - Error Handling Alignment
Deliver:
- Audit all proposed std signatures against sx error handling.
- Replace any incorrect `!T` assumptions with the actual sx shape.
- Add tests that show success and error return forms.
Acceptance:
- New std APIs follow the actual sx error model.
- Distribution platform examples compile with the same error style.
## Slice 3 - Unicode And String Model
Deliver:
- Define `String` as validated UTF-8 bytes.
- Define explicit terms:
- byte length
- Unicode scalar value
- grapheme cluster
- code point index is not a random-access string index
- Add `std.unicode` primitives for validation, scalar iteration,
normalization policy, case mapping policy, and ASCII fast paths.
- Add `StringBuilder` for efficient construction.
Acceptance:
- Invalid UTF-8 cannot silently become `String`.
- APIs that operate on bytes, scalars, and display width are named distinctly.
- Platform identifiers and paths do not accidentally use display width logic.
## Slice 4 - Collections
Deliver:
- Extend `List` with reserve, resize, insert, remove, pop, clear, sort, find,
contains, map/filter style helpers where idiomatic.
- Add `HashMap` with deterministic iteration option or clear documentation that
order is unspecified.
- Add hash/equality hooks for custom key types.
- Add tests for resize, collisions, deletion, and iteration.
Acceptance:
- Manifest parsing, route tables, and in-memory product stores can be written
without ad hoc arrays.
## Slice 5 - Bytes, Paths, FS, And Process
Deliver:
- `std.bytes` for slices, builders, compare, copy, starts_with, ends_with.
- `std.path` with platform-aware join, normalize, basename, dirname, extension.
- `std.fs` with open/read/write/stat/mkdir/rename/remove/temp_dir/walk.
- `std.process` with args, env, cwd, exit, spawn, wait, stdout/stderr capture.
Acceptance:
- A CLI can read a manifest, inspect files, spawn validation helpers, and write
artifacts safely.
## Slice 6 - Time, Random, Hashing, Encoding
Deliver:
- `std.time` timestamps, durations, monotonic clock, RFC3339 parse/format.
- `std.random` secure bytes and deterministic PRNG for tests.
- `std.hash` SHA-256 streaming.
- `std.encoding` hex, base64url, percent encoding.
Acceptance:
- Release ids, token hashes, artifact digests, and audit timestamps can be
implemented without platform-specific shims.
## Slice 7 - JSON, Config, CLI, And Logging
Deliver:
- `std.json` parser/writer with useful typed access.
- `std.config` for env/file/CLI config layering.
- `std.cli` parser with subcommands, flags, help, exit codes.
- `std.log` structured events and levels.
Acceptance:
- `dist ci publish --manifest dist.json --json` can parse args and emit stable
JSON output.
## Slice 8 - HTTP, TLS, SQLite, Archive, Testing
Deliver:
- `std.http` server/client basics.
- `std.tls` client/server wiring or explicit temporary boundary.
- `std.db.sqlite` minimal prepared statements and transactions.
- `std.archive` zip/tar/gzip reading needed for artifacts.
- `std.testing` assertions, fixtures, temp directories, golden files.
Acceptance:
- `distd` can expose endpoints, persist releases, inspect artifacts, and run
repeatable tests.
## Checkpoint Notes
After each slice, update:
- `.agents/CHECKPOINT.md`
- `.agents/checkpoint.json`
- Any changed API signatures in `PLAN.md`

View File

@@ -0,0 +1,151 @@
# Subplan 02 - Product Domain And Storage
## Goal
Define the distribution platform's core data model and persistence layer once
the required std primitives exist.
## Core Structs
App:
- id
- slug
- display_name
- bundle identifiers by platform
- owner
- visibility
- created_at
- updated_at
Platform:
- ios
- android_apk
- macos
- linux
- windows
Artifact:
- id
- app_id
- release_id
- platform
- filename
- content_type
- size_bytes
- sha256
- storage_key
- metadata
- validation_status
Release:
- id
- app_id
- version
- build
- channel
- notes
- created_by
- created_at
- published_at
Channel:
- app_id
- name
- current_release_id
- policy
- rollout_percent
Token:
- id
- name
- token_hash
- scopes
- created_at
- expires_at
- last_used_at
- revoked_at
AuditEvent:
- id
- actor
- action
- target_type
- target_id
- metadata
- created_at
## Slice 1 - In-Memory Model
Deliver:
- In-memory repository interfaces.
- Basic create/list/get/update operations.
- Domain validation for slugs, versions, channels, and platform ids.
Acceptance:
- CLI and HTTP tests can run without SQLite.
- Invalid app/release/artifact state is rejected at the domain boundary.
## Slice 2 - SQLite Schema
Deliver:
- Schema migrations.
- Tables for apps, releases, artifacts, channels, tokens, audit events.
- Indexes for slug lookup, app releases, artifact digest lookup, token hash.
- Transaction wrapper for publish flows.
Acceptance:
- A release and all artifacts can be created atomically.
- Channel promotion can be rolled back if a validation gate fails.
## Slice 3 - Artifact Storage
Deliver:
- Content-addressed storage by SHA-256.
- Staging directory for uploads.
- Atomic move from staging to final storage.
- Metadata sidecar or DB rows for content type, size, and platform metadata.
Acceptance:
- Duplicate artifact bytes are not stored twice unless policy requires it.
- Interrupted uploads do not create published artifacts.
## Slice 4 - Retention And Cleanup
Deliver:
- Retention policy per app/channel.
- Cleanup of unreferenced staged files.
- Audit events for deletion.
Acceptance:
- Stable releases can be retained longer than beta/internal builds.
- Cleanup never deletes the release currently pointed at by a channel.
## Slice 5 - Token Security
Deliver:
- Token generation.
- Token hashing at rest.
- Scope checks.
- Expiration and revocation.
- Last-used audit updates.
Acceptance:
- Raw tokens are shown only once.
- CI publish tokens can be scoped to one app and one channel.

View File

@@ -0,0 +1,125 @@
# Subplan 03 - CLI And CI Integration
## Goal
Make `dist` useful from CI before the admin UI exists.
## Commands
`dist server`:
- starts `distd`
- accepts config file and env overrides
`dist doctor`:
- checks config, database, storage directory, HTTPS base URL, and platform tools
`dist app create`:
- creates an app
- accepts slug, display name, owner, platform ids
`dist token create`:
- creates scoped CI tokens
- prints raw token once
`dist ci publish`:
- validates manifest
- creates/fetches app
- creates draft release
- uploads artifacts
- validates artifacts
- publishes release
- optionally promotes a channel
- prints JSON output
`dist release promote`:
- promotes a release to a channel
- checks policy gates
`dist release rollback`:
- moves a channel pointer to the previous valid release
## Manifest Shape
Required fields:
- app slug
- version
- channel
- artifacts list
Artifact fields:
- platform
- path
- filename override
- content type override
- metadata
## Slice 1 - Parser And Help
Deliver:
- Top-level parser.
- Subcommand parser.
- Help text.
- Exit code contract.
Acceptance:
- Unknown commands produce readable errors.
- `--json` never emits human-only text on stdout.
## Slice 2 - Local Publish
Deliver:
- `dist ci publish --manifest dist.json --local-store .dist`
- In-memory or file-backed local mode for early testing.
Acceptance:
- CI flow can be dogfooded without a running HTTP server.
## Slice 3 - Remote Publish
Deliver:
- HTTP client integration.
- Token auth.
- Streaming uploads with SHA-256.
- Retry policy for safe requests.
Acceptance:
- Upload output includes release id, artifact ids, digests, and URLs.
## Slice 4 - CI Templates
Deliver:
- GitHub Actions example.
- GitLab CI example.
- Generic shell example.
Acceptance:
- Users can copy the command and only set server, token, and manifest path.
## Slice 5 - Observability
Deliver:
- Structured logs.
- `--verbose`.
- Machine-readable errors under `--json`.
Acceptance:
- CI logs explain what failed without leaking tokens.

View File

@@ -0,0 +1,104 @@
# Subplan 04 - HTTP API And Install Experience
## Goal
Expose release management and install/download flows through `distd`.
## API Slices
Slice 1 - Server Skeleton:
- config loading
- routing
- JSON request/response helpers
- error response shape
- health endpoint
Slice 2 - Auth:
- bearer token parsing
- scope checks
- audit actor resolution
Slice 3 - Apps And Releases:
- create/list/get apps
- create/list/get releases
- publish release
- promote/rollback channel
Slice 4 - Uploads And Downloads:
- streaming upload endpoint
- digest validation
- resumable upload optional for later
- download endpoint with content length and SHA-256 headers
Slice 5 - Public Install Pages:
- app page by slug/channel
- platform detection
- QR/deep link support
- authenticated and public modes
## iOS Install Rules
Normal iOS devices cannot install arbitrary IPA files from a web page.
Supported modes:
- TestFlight link: open Apple's TestFlight flow.
- Enterprise/MDM: serve signed HTTPS manifest plist for enrolled devices.
- Artifact only: allow authenticated IPA download without claiming mobile
install support.
Acceptance:
- The UI labels iOS install mode accurately.
- Enterprise install requires HTTPS and a valid manifest.
- Artifact-only mode does not display a misleading "Install on iPhone" action.
## Android APK Install Rules
Supported modes:
- Direct APK download.
- Optional install instructions shown only when relevant.
- SHA-256 visible in download metadata.
Acceptance:
- APK download is access-controlled according to app/channel policy.
## Desktop Install Rules
macOS:
- notarization status displayed when available
- download zip/dmg/pkg
Linux:
- tar/appimage/deb/rpm metadata
Windows:
- installer zip/exe/msi metadata
- signature status displayed when available
## API Contract
All JSON endpoints should return:
- stable status code
- error code
- message
- request id
- optional details
Downloads should include:
- `Content-Length`
- `Content-Type`
- `X-Artifact-SHA256`
- cache policy by channel

View File

@@ -0,0 +1,105 @@
# Subplan 05 - Artifact Validation
## Goal
Validate uploaded platform artifacts before publication and channel promotion.
## Common Validation
All artifacts:
- file exists
- size matches upload metadata
- SHA-256 matches
- content type is allowed
- extension matches platform policy
- archive is readable when applicable
## iOS IPA
Validate:
- zip structure
- `.app` bundle exists
- Info.plist can be read
- bundle id matches app policy
- version/build metadata matches release
- provisioning profile mode is classified
- install mode is classified as TestFlight, Enterprise/MDM, or Artifact only
Acceptance:
- The system never implies browser IPA install unless enterprise/MDM manifest
requirements are met.
## Android APK
Validate:
- APK structure
- manifest package id
- version code/name
- min sdk
- signature presence
- basic metadata extraction
Acceptance:
- APK validator can reject package id mismatch before publication.
## macOS
Validate:
- archive opens
- app bundle or installer package exists
- Info.plist metadata when relevant
- code signing status if tool support exists
- notarization state when provided or checked
Acceptance:
- Notarization can be pending for beta but must satisfy stable policy if
configured.
## Linux
Validate:
- archive/package opens
- architecture metadata when available
- executable presence
- optional package metadata for deb/rpm/appimage
Acceptance:
- Platform and architecture metadata can drive download labels.
## Windows
Validate:
- exe/msi/zip exists
- authenticode status if supported
- malware scan placeholder status
- architecture metadata when available
Acceptance:
- Stable promotion can require completed scan policy.
## Validation Pipeline
Statuses:
- pending
- passed
- failed
- warning
- skipped
Policy:
- beta/internal may allow warnings.
- stable requires all configured required checks.
- every validation result is auditable.

View File

@@ -0,0 +1,100 @@
# Subplan 06 - Admin UI
## Goal
Build a dense, operational admin UI for managing apps, releases, artifacts,
channels, tokens, install pages, and audit logs.
## Authority
- Snarky defines product requirements and acceptance criteria.
- Opus owns layout, visual hierarchy, and interaction design.
- Opus is the only role that writes UI code during implementation.
## Screens
Apps:
- app list
- platform coverage
- channel status
- latest release summary
App detail:
- metadata
- releases table
- channels
- install page visibility
Release detail:
- artifact list
- validation status
- timeline
- promote/rollback actions
Install preview:
- public install page preview
- platform detection states
- iOS mode selector
- QR/deep link preview
Tokens:
- token list
- create token
- scopes
- revoke
Audit:
- filter by actor, action, app, release
- event details
Settings:
- server URL
- storage
- policy gates
- NAS deployment values
## UX Requirements
- First screen is the working product, not a landing page.
- Operational SaaS density: quiet, scan-friendly, no oversized marketing hero.
- Layout works on desktop and mobile without text overlap.
- Tables and panels should have stable dimensions.
- Repeated items can be cards; avoid nested cards.
- The UI must distinguish iOS TestFlight, Enterprise/MDM, and Artifact-only
modes clearly.
## Mock Redesign Slice
Current static mock files:
- `index.html`
- `styles.css`
- `app.js`
The current layout was rejected by the user.
Next redesign branch:
- `opus/redesign-distribution-mock`
Allowed paths for Opus:
- `index.html`
- `styles.css`
- `app.js`
- `assets/**`
Acceptance:
- App console is usable in the first viewport.
- Navigation is not visually heavy.
- Release workflow and install workflow are immediately legible.
- Mobile layout prioritizes content before secondary chrome.
- No misleading iOS install claim.

View File

@@ -0,0 +1,84 @@
# Subplan 07 - Packaging, Docker, And UGREEN NAS
## Goal
Package the distribution platform so it can run on a UGREEN NAS through Docker.
## Runtime Shape
Container:
- `distd` server binary
- static admin UI assets
- config file support
- healthcheck
Volumes:
- database
- artifact storage
- logs
- optional TLS certs
Environment:
- `DIST_BASE_URL`
- `DIST_DATA_DIR`
- `DIST_DB_PATH`
- `DIST_STORAGE_DIR`
- `DIST_ADMIN_BIND`
- `DIST_PUBLIC_BIND`
- `DIST_LOG_LEVEL`
## Slice 1 - Local Container
Deliver:
- Dockerfile
- docker compose file
- healthcheck endpoint
- local volume layout
Acceptance:
- `docker compose up` starts `distd`.
- The admin UI loads.
- Data survives container restart.
## Slice 2 - NAS Deployment
Deliver:
- UGREEN NAS deployment notes.
- Recommended volume paths.
- Reverse proxy/TLS guidance.
- Backup/restore guidance.
Acceptance:
- A user can deploy with Docker UI or compose.
- Required external ports and volumes are clear.
## Slice 3 - Updates And Rollback
Deliver:
- image tag policy
- migration policy
- backup before migration
- rollback instructions
Acceptance:
- Updating the platform does not risk artifact loss without warning.
## Slice 4 - Dogfood
Deliver:
- Publish the distribution platform's own release through `dist`.
- Document the self-hosted release flow.
Acceptance:
- The platform can distribute its own updates.

View File

@@ -0,0 +1,200 @@
# Subplan 08 - Orchestration, Checkpoints, And QA
## Goal
Keep agent work resumable, auditable, and constrained.
## Required Files
- `.agents/ORCHESTRATION.md`
- `.agents/CHECKPOINT.md`
- `.agents/checkpoint.json`
- `.agents/subplans/README.md`
- `.agents/runs/<run-id>/...`
## Run Creation
For each substantial task:
1. Create `.agents/runs/<run-id>/`.
2. Copy relevant acceptance criteria into `brief.md`.
3. Record the active branch.
4. Record allowed write paths.
5. Update checkpoint before invoking Opus.
Manager planning sessions count as substantial tasks when the user expects
observability. Create a run record for planning work too, with Codex manager as
the active agent.
## Agent Liveness
Each active run should include:
```txt
.agents/runs/<run-id>/
state.json
agents.json
```
`state.json` records:
- run id
- current phase
- current branch
- input artifact
- input hash
- expected output artifact
- retry count
- next action
- blocker, if any
`agents.json` records:
- role
- status: queued, running, completed, failed, dead, restarted
- started_at
- heartbeat_at
- lease_expires_at
- thread id, process id, or tool call id when available
- last_error
## Status And Progress Tail
Use the local status command from the workspace root:
```sh
node .agents/scripts/status.mjs --tail 40
```
For a browser dashboard:
```sh
node .agents/scripts/observe.mjs --port 4317
```
Then open `http://127.0.0.1:4317`.
The command reads:
- `.agents/checkpoint.json`
- every `.agents/runs/<run-id>/state.json`
- every `.agents/runs/<run-id>/agents.json`
It prints:
- all known runs
- current phase and branch
- all recorded agents and their lease status
- expired leases
- blockers
- the next action
- the tail of the active run's progress file
Progress files are checked in this order:
- `progress.log`
- `implementation-log.md`
- `validation.md`
- `opus-proposal.md`
- `snarky-review.md`
Managers should append progress events to `progress.log` whenever possible.
Human-readable phase artifacts still stay in their named markdown files.
## Agent Restart Policy
If an agent dies, the manager restarts the role from durable files, not memory.
Snarky restart:
- Read `PLAN.md`, `.agents/ORCHESTRATION.md`, checkpoint files, and active run
artifacts.
- Re-run the current Snarky phase using the same input artifact.
- Replace only the expected Snarky output for that phase.
Opus proposal/review restart:
- Re-run the same `opus-runner` planning tool with the same input artifact.
- Keep previous failed output, if any, as diagnostic context.
- Do not advance until the expected output validates.
- Use a lease and CLI/tool timeout of at least 30 minutes.
Opus implementation restart:
- Check current branch.
- Check dirty state.
- If the branch is clean, retry the same implementation instruction.
- If the branch is dirty, manager must inspect the diff and decide whether to
continue, ask Opus to repair, or ask the user.
- Never auto-reset or discard partial Opus edits.
- Use a lease and CLI/tool timeout of at least 30 minutes.
Retry limits:
- Retry a dead planning phase up to 2 times.
- Retry an implementation phase up to 1 time without user input.
- After the retry cap, record a blocker in checkpoint files.
## Checkpoint Policy
Update checkpoints:
- at the start of a run
- after Snarky brief
- after Opus proposal
- after concern resolution
- before Opus implementation
- after implementation
- after validation
- before ending the turn
Checkpoint must include:
- timestamp
- current phase
- current branch
- active run id
- completed artifacts
- next action
- blockers
- commands/checks already run
## Validation Layers
Manager validation:
- git branch and diff check
- out-of-scope file check
- syntax checks
- unit/integration tests when available
- browser/screenshot checks for UI work when available
Snarky validation:
- product requirements
- acceptance criteria
- install flow accuracy
- scope discipline
Opus validation:
- layout/design quality
- interaction clarity
- technical design concerns
## Resume Procedure
After power loss or interruption:
1. Read `.agents/CHECKPOINT.md`.
2. Read `.agents/checkpoint.json`.
3. Check git branch and dirty state.
4. Read the active run directory if present.
5. Continue from `next_action`.
6. Do not assume an Opus implementation completed unless validation is recorded.
## Current Known Setup Issue
The distribution workspace is not currently a git repository. Branch-based Opus
implementation requires initializing git or moving these files into a repo with
a clean baseline commit.

View File

@@ -0,0 +1,44 @@
# Detailed Subplan Index
These subplans break `PLAN.md` into implementation slices that can be handed to
Snarky and Opus without reloading the whole plan every time.
## Reading Order
1. `01-language-and-stdlib.md`
2. `02-domain-and-storage.md`
3. `03-cli-and-ci.md`
4. `04-http-api-and-install.md`
5. `05-artifact-validation.md`
6. `06-admin-ui.md`
7. `07-packaging-nas.md`
8. `08-orchestration-and-qa.md`
## Session Contract
- Read `PLAN.md`, `.agents/ORCHESTRATION.md`, `.agents/CHECKPOINT.md`, and
the active subplan before doing work.
- Keep work sequential. Do not use parallel implementation agents.
- Use git branches for implementation. Do not use worktrees.
- For product or UX scope, Snarky writes acceptance criteria and has final say.
- For layout and visual design, Opus has final say.
- For technical problems, consult Opus and resolve by consensus.
- Opus is the only role that writes application code during Opus phases.
- Update `.agents/CHECKPOINT.md` and `.agents/checkpoint.json` after every
completed slice, before any risky handoff, and after validation.
## Slice Exit Criteria
Every slice should finish with:
- Changed files listed.
- Tests/checks run listed.
- Known risks listed.
- Next slice named.
- Checkpoint updated.
## Current Priority
The first blocking priority is `01-language-and-stdlib.md`. Product code should
wait until the required `sx` primitives exist or an explicit temporary shim is
approved.

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

304
PLAN.md Normal file
View File

@@ -0,0 +1,304 @@
# Distribution Platform in sx
## Goal
Build an App Store-like distribution platform in `sx`, but not limited to iOS
apps. It should distribute mobile, desktop, and server/client artifacts through
a CI-friendly release workflow.
Supported targets for the first product direction:
- iOS: IPA metadata, TestFlight links, Enterprise/MDM manifests, artifact-only
downloads
- Android: APK first, AAB later
- macOS: app archives, dmg/pkg metadata, signing and notarization status
- Linux: tarballs first, then deb/rpm/AppImage metadata
- Windows: zip/installer first, then MSIX/signing metadata
The main commands are:
- `distd`: the HTTP API, install page, and artifact server
- `dist`: the CI/admin CLI
The release workflow must be easy to integrate into CI. A CI job should be able
to upload artifacts, create a release, attach platform variants, promote a
channel, and receive machine-readable JSON output.
Product code should live in `sx`. C or system APIs are acceptable only as thin
platform backends for capabilities that `sx` cannot express yet.
## Product Shape
This is an operational distribution tool first, not a public marketplace. The
initial product should feel like a release console: quiet, dense, fast to scan,
and optimized for repeated internal/private release work.
Core principles:
- CI is the primary writer.
- Humans inspect, promote, revoke, download, and audit.
- Releases are immutable.
- Channels are mutable pointers to releases.
- Artifacts are content-addressed and never silently replaced.
- Every upload, publish, promotion, token change, and deletion writes an audit
event.
- The first version should be understandable from the filesystem and SQLite
database during development.
## Users And Actors
- CI runner: publishes releases with `dist ci publish`.
- Release manager: promotes channels, checks validation, and reads audit logs.
- Developer/tester: downloads the current artifact for a platform/channel.
- Admin: creates apps, manages tokens, and configures identity constraints.
- `distd`: server/API/artifact store.
- `dist`: CLI for CI and local admin work.
## Core Concepts
App:
- Product being distributed.
- Owns identity rules, allowed platforms, releases, channels, and tokens.
Release:
- Immutable version/build record for one app.
- Contains one or more platform artifacts.
Artifact:
- Uploaded file for a platform.
- Stored by digest and linked to release metadata.
Channel:
- Mutable pointer such as stable, beta, internal, nightly.
- Promotion changes the pointer, not the release.
Token:
- Scoped credential for CI and automation.
- Stored hashed, expires or can be revoked.
Audit event:
- Immutable record of important actions.
## Main Workflows
CI publish:
1. CI builds artifacts.
2. CI writes or updates `dist.json`.
3. CI runs `dist ci publish --server "$DIST_SERVER" --token "$DIST_TOKEN"
--manifest dist.json --json`.
4. `dist` validates the manifest and streams artifact uploads.
5. `distd` validates metadata and stores artifacts by digest.
6. The release is published.
7. A requested channel is promoted if policy allows it.
8. CI receives JSON with release ids, artifact ids, digests, and URLs.
Human release management:
1. Release manager opens the admin UI.
2. They inspect release status, artifact validation, and audit history.
3. They promote, rollback, revoke tokens, or download artifacts.
Install/download:
1. User opens an app/channel install page.
2. Platform detection chooses the most relevant artifact or install path.
3. The page shows accurate platform-specific actions and metadata.
4. Access policy determines whether the user can download or install.
## iOS Install Policy
iOS needs special handling. The platform must not imply that a normal iPhone can
install an arbitrary IPA from a web page.
Supported iOS modes:
- TestFlight: store a TestFlight link and open Apple's install flow.
- Enterprise/MDM: serve a valid HTTPS manifest plist for enrolled devices.
- Artifact only: allow authenticated IPA download, without presenting it as a
normal mobile install action.
This distinction must be visible in both the API model and admin/install UI.
## Architecture Overview
The intended stack is:
```text
sx language and std primitives
-> infra libraries: http, json, hashing, storage, db, auth, archive
-> distribution domain: app, artifact, release, channel, token, audit
-> interfaces: CLI, HTTP API, install pages, admin UI
-> deployment: Docker image, NAS-friendly config, persistent data volume
```
Storage direction:
- SQLite first.
- Local filesystem artifact storage first.
- Object storage adapter later.
Deployment direction:
- Docker/OCI image.
- One persistent `/data` volume for database, artifacts, uploads, config, and
logs.
- One HTTP port.
- `/healthz` endpoint.
- Runs behind a reverse proxy for TLS.
- Runs as a non-root user.
- UGREEN NAS deployment through Docker/Container Manager is a first-version
requirement.
## sx Foundation Work
Before the product can be implemented well, `sx` needs a stronger language and
standard library foundation.
Language/module needs:
- `pub` exports so std modules do not leak private helpers.
- Alias imports and curated namespace barrels.
- Namespace member re-export syntax such as `pub print :: core.print`.
- Error handling that follows the real sx model. `!` is an error channel, not a
generic result wrapper.
Standard library needs, at overview level:
- Collections: extended `List`, `HashMap`
- Strings: validated UTF-8 `String`, `StringBuilder`, explicit Unicode model
- Bytes and paths
- Filesystem and process APIs
- Time, random, hashing, and encoding
- JSON, URL, MIME, config, CLI, and logging
- HTTP server/client and TLS boundary
- SQLite
- Archive inspection
- Testing helpers
Unicode must be specified precisely:
- `String` is validated UTF-8 bytes.
- Byte length, scalar values, grapheme clusters, and display width are distinct.
- APIs must clearly say which unit they operate on.
- Invalid UTF-8 must not silently become a `String`.
## Implementation Phases
Phase 0 - sx language/module prerequisites:
- Add `pub` support, alias imports, and namespace member re-exports.
Phase 1 - standard library foundation:
- Build the std primitives needed for CLI, HTTP, storage, validation, and tests.
Phase 2 - product domain:
- Define apps, releases, artifacts, channels, tokens, policies, and audit
events.
Phase 3 - storage:
- Add SQLite persistence and filesystem artifact storage.
Phase 4 - CLI first:
- Make `dist ci publish --manifest dist.json --json` work before the admin UI.
Phase 5 - HTTP API:
- Expose app, release, upload, download, channel, token, and audit endpoints.
Phase 6 - artifact validation:
- Validate APK and IPA first, then desktop artifact metadata.
Phase 7 - channels and install pages:
- Add promotion, rollback, download URLs, and platform-specific install pages.
Phase 8 - admin UI:
- Build a dense operational UI for apps, releases, artifacts, tokens, settings,
install pages, and audit logs.
Phase 9 - packaging:
- Package `distd` and the admin UI into a Docker image suitable for UGREEN NAS.
## CI Contract
The primary CI command should be:
```sh
dist ci publish \
--server "$DIST_SERVER" \
--token "$DIST_TOKEN" \
--manifest dist.json \
--json
```
The command should:
1. Validate the manifest.
2. Create or find the app.
3. Create a draft release.
4. Upload all artifacts with streaming SHA-256.
5. Validate platform metadata.
6. Publish the release.
7. Promote the requested channel if specified.
8. Print JSON containing release id, artifact ids, digests, and download URLs.
## First Milestone
Milestone 1 is complete when:
- Required `sx` language/std primitives exist or have explicit temporary
boundaries.
- `distd` can run locally.
- `dist ci publish` can publish a release with at least APK and IPA artifacts.
- Artifacts are stored by digest.
- SQLite stores apps, releases, artifacts, channels, tokens, and audit events.
- A channel can be promoted and rolled back.
- Install/download pages accurately handle iOS, Android, and desktop artifacts.
- The admin UI can inspect apps, releases, validations, tokens, and audit logs.
- A Docker image can run on a UGREEN NAS with a persistent data volume.
## Non-goals For Version 1
- Public marketplace payments.
- Reviews and ratings.
- Organization billing.
- Sophisticated staged rollout targeting.
- CDN integration.
- Full signing/notarization automation.
- Object storage.
- Postgres.
## Detailed Execution
`PLAN.md` is the overview. Detailed implementation breakdowns live in
`.agents/subplans/`.
Before starting or resuming work, read:
- `.agents/ORCHESTRATION.md`
- `.agents/CHECKPOINT.md`
- `.agents/checkpoint.json`
- the active `.agents/subplans/*.md` file
The workflow is sequential and branch-based:
- Codex manages orchestration and validation.
- Snarky owns product briefs and final product acceptance.
- Opus owns layout/design decisions.
- Opus is the only role that writes code during Opus implementation phases.
- Implementation uses git branches, not worktrees.
- Checkpoints are updated after every completed slice and before stopping work.

104
app.js Normal file
View File

@@ -0,0 +1,104 @@
const views = Array.from(document.querySelectorAll(".view"));
const navItems = Array.from(document.querySelectorAll(".nav-item"));
const viewLinks = Array.from(document.querySelectorAll("[data-view-link]"));
function showView(name) {
views.forEach((view) => {
view.classList.toggle("is-active", view.id === `view-${name}`);
});
navItems.forEach((item) => {
item.classList.toggle("is-active", item.dataset.view === name);
});
}
navItems.forEach((item) => {
item.addEventListener("click", () => showView(item.dataset.view));
});
viewLinks.forEach((link) => {
link.addEventListener("click", () => showView(link.dataset.viewLink));
});
const appData = {
atlas: {
name: "Atlas Notes",
icon: "AN",
iconClass: "app-icon-atlas",
subtitle: "com.example.atlas - owner mobile-platform",
stable: "2.8.4",
downloads: "14,283",
platforms: "4",
},
forge: {
name: "Forge Build",
icon: "FB",
iconClass: "app-icon-forge",
subtitle: "io.example.forge - owner release-engineering",
stable: "1.12.0",
downloads: "7,421",
platforms: "3",
},
field: {
name: "FieldKit",
icon: "FK",
iconClass: "app-icon-field",
subtitle: "com.example.fieldkit - owner field-ops",
stable: "0.9.8",
downloads: "2,906",
platforms: "2",
},
ledger: {
name: "LedgerWorks",
icon: "LW",
iconClass: "app-icon-ledger",
subtitle: "net.example.ledger - owner finance-tools",
stable: "5.4.1",
downloads: "4,108",
platforms: "2",
},
};
const appRows = Array.from(document.querySelectorAll(".app-row"));
const detailIcon = document.getElementById("detail-icon");
const detailName = document.getElementById("detail-name");
const detailSubtitle = document.getElementById("detail-subtitle");
const metricStable = document.getElementById("metric-stable");
const metricDownloads = document.getElementById("metric-downloads");
const metricPlatforms = document.getElementById("metric-platforms");
function selectApp(key) {
const data = appData[key];
if (!data) return;
appRows.forEach((row) => {
row.classList.toggle("is-selected", row.dataset.app === key);
});
detailIcon.className = `app-icon app-icon-large ${data.iconClass}`;
detailIcon.textContent = data.icon;
detailName.textContent = data.name;
detailSubtitle.textContent = data.subtitle;
metricStable.textContent = data.stable;
metricDownloads.textContent = data.downloads;
metricPlatforms.textContent = data.platforms;
}
appRows.forEach((row) => {
row.addEventListener("click", () => selectApp(row.dataset.app));
});
const methodButtons = Array.from(document.querySelectorAll("#ios-methods button"));
const methodDetails = Array.from(document.querySelectorAll(".method-detail"));
methodButtons.forEach((button) => {
button.addEventListener("click", () => {
methodButtons.forEach((item) => {
item.classList.toggle("is-selected", item === button);
});
methodDetails.forEach((detail) => {
detail.classList.toggle("is-hidden", detail.id !== `method-${button.dataset.method}`);
});
});
});

596
index.html Normal file
View File

@@ -0,0 +1,596 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>dist release console mock</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand-block">
<div class="brand-mark">d</div>
<div>
<div class="brand-name">dist</div>
<div class="brand-meta">UGREEN NAS - local.distro.test</div>
</div>
</div>
<label class="search">
<span>/</span>
<input type="search" placeholder="Search apps, releases, artifacts" />
</label>
<div class="top-actions">
<button class="icon-button" aria-label="Sync">
<span aria-hidden="true">R</span>
</button>
<button class="icon-button" aria-label="Notifications">
<span aria-hidden="true">!</span>
</button>
<div class="server-pill">
<span class="dot dot-ok"></span>
distd healthy
</div>
</div>
</header>
<aside class="sidebar">
<nav class="nav-list" aria-label="Primary">
<button class="nav-item is-active" data-view="apps">
<span class="nav-glyph">A</span>
Apps
</button>
<button class="nav-item" data-view="release">
<span class="nav-glyph">R</span>
Release
</button>
<button class="nav-item" data-view="install">
<span class="nav-glyph">I</span>
Install
</button>
<button class="nav-item" data-view="tokens">
<span class="nav-glyph">T</span>
Tokens
</button>
<button class="nav-item" data-view="audit">
<span class="nav-glyph">L</span>
Audit
</button>
<button class="nav-item" data-view="settings">
<span class="nav-glyph">S</span>
Settings
</button>
</nav>
<div class="sidebar-status">
<div class="side-label">Storage</div>
<div class="meter" aria-label="Storage used">
<span style="width: 58%"></span>
</div>
<div class="side-value">824 GB of 1.4 TB</div>
</div>
</aside>
<main class="workspace">
<section class="view is-active" id="view-apps">
<div class="page-head">
<div>
<h1>Applications</h1>
<p>5 apps - 31 active release artifacts - 3 pending checks</p>
</div>
<div class="head-actions">
<button class="secondary-button">Import manifest</button>
<button class="primary-button">New app</button>
</div>
</div>
<div class="apps-layout">
<section class="panel app-list-panel" aria-label="Application list">
<div class="panel-toolbar">
<div class="segmented">
<button class="is-selected">All</button>
<button>Mobile</button>
<button>Desktop</button>
</div>
<button class="small-button">Filter</button>
</div>
<div class="app-list">
<button class="app-row is-selected" data-app="atlas">
<span class="app-icon app-icon-atlas">AN</span>
<span class="app-main">
<strong>Atlas Notes</strong>
<small>ios - apk - macos - windows</small>
</span>
<span class="status-badge status-live">Stable</span>
</button>
<button class="app-row" data-app="forge">
<span class="app-icon app-icon-forge">FB</span>
<span class="app-main">
<strong>Forge Build</strong>
<small>linux - windows - macos</small>
</span>
<span class="status-badge status-beta">Beta</span>
</button>
<button class="app-row" data-app="field">
<span class="app-icon app-icon-field">FK</span>
<span class="app-main">
<strong>FieldKit</strong>
<small>ios - apk</small>
</span>
<span class="status-badge status-hold">Hold</span>
</button>
<button class="app-row" data-app="ledger">
<span class="app-icon app-icon-ledger">LW</span>
<span class="app-main">
<strong>LedgerWorks</strong>
<small>windows - linux</small>
</span>
<span class="status-badge status-live">Stable</span>
</button>
</div>
</section>
<section class="panel app-detail-panel" aria-label="Application detail">
<div class="detail-top">
<div class="app-title">
<span class="app-icon app-icon-atlas app-icon-large" id="detail-icon">AN</span>
<div>
<h2 id="detail-name">Atlas Notes</h2>
<p id="detail-subtitle">com.example.atlas - owner mobile-platform</p>
</div>
</div>
<div class="detail-actions">
<button class="secondary-button">Edit</button>
<button class="primary-button">Publish</button>
</div>
</div>
<div class="metric-grid">
<div class="metric">
<span>Latest stable</span>
<strong id="metric-stable">2.8.4</strong>
<small>promoted 18 min ago</small>
</div>
<div class="metric">
<span>Downloads</span>
<strong id="metric-downloads">14,283</strong>
<small>last 30 days</small>
</div>
<div class="metric">
<span>Platforms</span>
<strong id="metric-platforms">4</strong>
<small>ios, apk, macos, windows</small>
</div>
</div>
<div class="section-row">
<h3>Channels</h3>
<button class="small-button">Manage</button>
</div>
<div class="channel-grid">
<div class="channel-box">
<span class="dot dot-ok"></span>
<strong>stable</strong>
<small>2.8.4 - signed - public</small>
</div>
<div class="channel-box">
<span class="dot dot-warn"></span>
<strong>beta</strong>
<small>2.9.0-rc.3 - notarizing</small>
</div>
<div class="channel-box">
<span class="dot dot-cold"></span>
<strong>internal</strong>
<small>main.6814 - CI only</small>
</div>
</div>
<div class="section-row">
<h3>Recent Releases</h3>
<button class="small-button" data-view-link="release">Open latest</button>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Version</th>
<th>Channel</th>
<th>Artifacts</th>
<th>Checks</th>
<th>Published</th>
</tr>
</thead>
<tbody>
<tr>
<td>2.9.0-rc.3</td>
<td><span class="status-badge status-beta">beta</span></td>
<td>5</td>
<td><span class="check-text check-warn">2 pending</span></td>
<td>9 min ago</td>
</tr>
<tr>
<td>2.8.4</td>
<td><span class="status-badge status-live">stable</span></td>
<td>4</td>
<td><span class="check-text check-ok">passed</span></td>
<td>18 min ago</td>
</tr>
<tr>
<td>2.8.3</td>
<td><span class="status-badge status-live">stable</span></td>
<td>4</td>
<td><span class="check-text check-ok">passed</span></td>
<td>May 31</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</section>
<section class="view" id="view-release">
<div class="page-head">
<div>
<h1>Atlas Notes 2.9.0-rc.3</h1>
<p>Release candidate - build 6814 - uploaded by ci-github</p>
</div>
<div class="head-actions">
<button class="secondary-button">Rollback</button>
<button class="primary-button">Promote to stable</button>
</div>
</div>
<div class="release-layout">
<section class="panel">
<div class="section-row">
<h2>Artifacts</h2>
<button class="small-button">Upload</button>
</div>
<div class="artifact-list">
<div class="artifact-row">
<span class="platform-badge ios">iOS</span>
<div>
<strong>AtlasNotes.ipa</strong>
<small>sha256 verified - 92.4 MB - enterprise profile</small>
</div>
<span class="check-text check-ok">signed</span>
</div>
<div class="artifact-row">
<span class="platform-badge apk">APK</span>
<div>
<strong>atlas-notes.apk</strong>
<small>v2 signature - 78.1 MB - min sdk 26</small>
</div>
<span class="check-text check-ok">signed</span>
</div>
<div class="artifact-row">
<span class="platform-badge mac">mac</span>
<div>
<strong>Atlas Notes.app.zip</strong>
<small>notarization submitted - 134 MB</small>
</div>
<span class="check-text check-warn">pending</span>
</div>
<div class="artifact-row">
<span class="platform-badge linux">linux</span>
<div>
<strong>atlas-notes-x86_64.tar.gz</strong>
<small>ELF scan complete - 119 MB</small>
</div>
<span class="check-text check-ok">clean</span>
</div>
<div class="artifact-row">
<span class="platform-badge win">win</span>
<div>
<strong>AtlasNotesSetup.exe</strong>
<small>authenticode verified - 146 MB</small>
</div>
<span class="check-text check-warn">scan</span>
</div>
</div>
</section>
<aside class="panel release-side">
<h2>CI Publish</h2>
<div class="code-card">
<code>dist ci publish --manifest dist.json --channel beta --json</code>
</div>
<dl class="definition-list">
<div>
<dt>Token</dt>
<dd>ci-github-atlas</dd>
</div>
<div>
<dt>Policy</dt>
<dd>publish:atlas, promote:beta</dd>
</div>
<div>
<dt>Retention</dt>
<dd>keep 20 stable, 10 beta</dd>
</div>
</dl>
</aside>
</div>
<section class="panel timeline-panel">
<h2>Release Timeline</h2>
<ol class="timeline">
<li>
<span class="dot dot-ok"></span>
<div>
<strong>Manifest accepted</strong>
<small>All artifact targets matched app policy</small>
</div>
<time>09:21</time>
</li>
<li>
<span class="dot dot-ok"></span>
<div>
<strong>Mobile signatures verified</strong>
<small>iOS enterprise profile and APK v2 signature passed</small>
</div>
<time>09:23</time>
</li>
<li>
<span class="dot dot-warn"></span>
<div>
<strong>Desktop checks running</strong>
<small>macOS notarization and Windows malware scan pending</small>
</div>
<time>09:29</time>
</li>
</ol>
</section>
</section>
<section class="view" id="view-install">
<div class="page-head">
<div>
<h1>Install Experience</h1>
<p>Public web install page, mobile-safe flows, and platform fallbacks</p>
</div>
<div class="head-actions">
<button class="secondary-button">Preview</button>
<button class="primary-button">Publish page</button>
</div>
</div>
<div class="install-layout">
<section class="panel install-phone-panel">
<div class="phone-frame" aria-label="Mobile install preview">
<div class="phone-speaker"></div>
<div class="mobile-page">
<span class="app-icon app-icon-atlas app-icon-large">AN</span>
<h2>Atlas Notes</h2>
<p>Version 2.8.4 - stable</p>
<button class="mobile-primary">Install</button>
<div class="mobile-platforms">
<span>iOS</span>
<span>Android</span>
<span>macOS</span>
<span>Windows</span>
</div>
<div class="mobile-note">
iOS opens the configured TestFlight, MDM, or enterprise install path.
</div>
</div>
</div>
</section>
<section class="panel install-config">
<div class="section-row">
<h2>iOS Path</h2>
<div class="segmented" id="ios-methods">
<button class="is-selected" data-method="testflight">TestFlight</button>
<button data-method="enterprise">Enterprise</button>
<button data-method="artifact">Artifact</button>
</div>
</div>
<div class="method-detail" id="method-testflight">
<h3>TestFlight</h3>
<p>Install action opens Apple's TestFlight URL. The page still tracks release, channel, and access policy in dist.</p>
<div class="form-grid">
<label>
Public link
<input value="https://testflight.apple.com/join/atlasnotes" />
</label>
<label>
Fallback
<input value="/download/atlas/ios/2.8.4" />
</label>
</div>
</div>
<div class="method-detail is-hidden" id="method-enterprise">
<h3>Enterprise / MDM</h3>
<p>Install action serves a signed manifest plist over HTTPS for enrolled devices and keeps the IPA behind policy.</p>
<div class="form-grid">
<label>
Manifest path
<input value="/ios/atlas/2.8.4/install.plist" />
</label>
<label>
Allowed teams
<input value="mobile-platform, field-ops" />
</label>
</div>
</div>
<div class="method-detail is-hidden" id="method-artifact">
<h3>Artifact Only</h3>
<p>Install action becomes download-only. The page warns that sideload installation is not available on normal iOS devices.</p>
<div class="form-grid">
<label>
Artifact
<input value="AtlasNotes.ipa" />
</label>
<label>
Visibility
<input value="authenticated users" />
</label>
</div>
</div>
<div class="qr-row">
<div class="qr-code" aria-label="QR code preview">
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span>
</div>
<div>
<strong>install.local/atlas/stable</strong>
<small>QR and deep links resolve to the same access-controlled install page.</small>
</div>
</div>
</section>
</div>
</section>
<section class="view" id="view-tokens">
<div class="page-head">
<div>
<h1>Access Tokens</h1>
<p>CI-friendly scoped credentials with short lived publish permissions</p>
</div>
<button class="primary-button">Create token</button>
</div>
<section class="panel">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Scope</th>
<th>Last used</th>
<th>Expires</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>ci-github-atlas</td>
<td>publish:atlas, promote:beta</td>
<td>9 min ago</td>
<td>30 days</td>
<td><span class="status-badge status-live">active</span></td>
</tr>
<tr>
<td>ci-gitlab-forge</td>
<td>publish:forge</td>
<td>3 hours ago</td>
<td>12 days</td>
<td><span class="status-badge status-live">active</span></td>
</tr>
<tr>
<td>old-jenkins-mobile</td>
<td>publish:fieldkit</td>
<td>May 12</td>
<td>expired</td>
<td><span class="status-badge status-hold">revoked</span></td>
</tr>
</tbody>
</table>
</div>
</section>
</section>
<section class="view" id="view-audit">
<div class="page-head">
<div>
<h1>Audit Log</h1>
<p>Release, token, and policy events across the distribution server</p>
</div>
<button class="secondary-button">Export</button>
</div>
<section class="panel">
<div class="audit-list">
<div class="audit-row">
<span class="audit-kind">publish</span>
<div>
<strong>ci-github-atlas uploaded Atlas Notes 2.9.0-rc.3</strong>
<small>5 artifacts - manifest dist.json - request 0c8f</small>
</div>
<time>09:21</time>
</div>
<div class="audit-row">
<span class="audit-kind">promote</span>
<div>
<strong>mira promoted Atlas Notes 2.8.4 to stable</strong>
<small>approval policy satisfied - public install page updated</small>
</div>
<time>08:52</time>
</div>
<div class="audit-row">
<span class="audit-kind">token</span>
<div>
<strong>kai rotated ci-gitlab-forge</strong>
<small>new token expires in 30 days - old token revoked</small>
</div>
<time>Yesterday</time>
</div>
<div class="audit-row">
<span class="audit-kind">policy</span>
<div>
<strong>Windows malware scan policy changed</strong>
<small>stable channel now requires completed scan before promotion</small>
</div>
<time>May 30</time>
</div>
</div>
</section>
</section>
<section class="view" id="view-settings">
<div class="page-head">
<div>
<h1>Server Settings</h1>
<p>NAS deployment, storage, HTTPS, and namespace policy</p>
</div>
<button class="primary-button">Save changes</button>
</div>
<div class="settings-grid">
<section class="panel">
<h2>Deployment</h2>
<div class="settings-list">
<label>
Docker image
<input value="ghcr.io/example/distd:0.1.0" />
</label>
<label>
Data directory
<input value="/volume1/docker/dist/data" />
</label>
<label>
Public base URL
<input value="https://install.example.test" />
</label>
</div>
</section>
<section class="panel">
<h2>Policy</h2>
<div class="check-list">
<label><input type="checkbox" checked /> Require checks before stable promotion</label>
<label><input type="checkbox" checked /> Require scoped tokens for CI publishing</label>
<label><input type="checkbox" /> Allow anonymous public downloads</label>
<label><input type="checkbox" checked /> Keep audit log immutable</label>
</div>
</section>
</div>
</section>
</main>
</div>
<script src="app.js"></script>
</body>
</html>

1104
styles.css Normal file

File diff suppressed because it is too large Load Diff