diff --git a/README.md b/README.md
index 75c0a18..03b7e0c 100644
--- a/README.md
+++ b/README.md
@@ -129,8 +129,9 @@ deleting `reference-transaction`.
Read each skill before use. Delegation and review target T3 Code's orchestration
MCP tools and its configured provider runtimes. Other workflows may need goal
-state tools, GitHub CLI, optional Tailscale sharing, or Apple development tools. Linking skills
-does not install provider executables, authenticate accounts, or enable models.
+state tools, GitHub CLI, Claude artifact tools, optional Tailscale sharing, or
+Apple development tools. Linking skills does not install provider executables,
+authenticate accounts, or enable models.
### Claude Code
@@ -272,13 +273,15 @@ to you, with judgment calls ahead of quick picks. Each outcome has its own
**Land**, **Try**, or PR finish line: an owner lands its accepted outcome once
the chief releases it, the chief lands only sets that span owners, and each
**Try** waits for your merge, changes, or scrap choice. Its command center shows
-Needs you first and Work second, rendered from a checked snapshot of the program
-record: each outcome with its finish line, results to open, layers, provisional
+Needs you first and Work second, rendered from the program record's last
+commit: each outcome with its finish line, results to open, layers, provisional
state, and what each decision blocks. Answers go back through chat. Existing
-trackers stay authoritative. A hosting guide can serve the page at a stable
-private URL through Show Me and Tailscale Serve. Open pages pick up each new
-snapshot on their own and keep your place; publishing one in the background
-needs explicitly requested host scheduling.
+trackers stay authoritative. The page is a private Claude artifact, so the chief
+runs in Claude and republishes it after each commit that changes it. On reload,
+the page restores your tab, open sections, selected decision, and scroll
+position; local browser tests cover this. In Claude's web viewer, an open page
+moved to a newly published version without a reload, kept its selected tab, and
+loaded its icons. The iPhone app is unchecked.
[Swarm](skills/swarm/SKILL.md) is a bounded tool any session can use. For example,
“Swarm these three possible causes and return evidence” partitions the question
diff --git a/SOURCES.md b/SOURCES.md
index 6aae717..99a4fbf 100644
--- a/SOURCES.md
+++ b/SOURCES.md
@@ -450,8 +450,13 @@ base and provisional bases, and linearizes fork-and-join inputs into reviewable
stacks. Existing trackers stay authoritative; there is no mandatory wiki,
Linear, Slack, cloud runtime, or recurring schedule.
-The original command center renders a checked JSON snapshot of the program
-record (`scripts/render_dashboard.py`, `assets/command-center.html`) with
+The program record is one file, `program.json`, organized by topic: program
+facts, `decisions` (each decision from arrival to answer or withdrawal), `work`
+(each row with its scope, sources, and owner with that owner's plumbing),
+tradeoffs, discoveries, landings, and waits. The renderer picks what the page
+shows. Owners write only their own report files. The program folder is a git
+repo; its last commit is the record and its log the history. The original command center
+(`scripts/render_dashboard.py`, `assets/command-center.html`) renders it with
Needs you before Work, collapsible sections, per-row observation times, copyable
decision prompts that name one recorded option, judgment calls ahead of quick
picks with a batch prompt, reopenable chief-resolved decisions, and accepted
@@ -459,14 +464,16 @@ tradeoffs. Each work row is one outcome, follow-up, or support task, counted onc
with its finish line, results to open, provisional condition, owner, and PR
layers; each decision names the outcomes it blocks and the phase each waits at.
Below 900px it stays one column; wider, Needs you is a dual-pane inbox and Work a
-board of stage lanes. It replaces the supplied archive's Claude artifact database
-and integrations; no archive code or prose is copied. Validation rejects
+board of stage lanes. It replaces the supplied archive's artifact database and
+integrations; no archive code or prose is copied. Validation rejects
provisional landings, PRs on a Try, stacks on another owner's unmerged layer
-without a provisional mark, and terminal rows with active blockers. Delivery is
-hosted only; `references/hosting.md` serves the page through Show Me's web
-sharing, including Tailscale Serve. A hosted page polls its own URL and swaps in
-a newer published digest without a server push, keeping the viewer's place per
-URL in session storage; the chief stays the only publisher.
+without a provisional mark, terminal rows with active blockers, entries that
+name missing work, and fields the schema doesn't name. The command center is a private Claude artifact per program,
+so only a Claude session can be chief. `render_dashboard.py --page` renders the
+last commit beside the last published page, says whether it changed, and stamps
+the commit time that changed it; the chief republishes on a change and on every
+resume and chief move. The page keeps the viewer's place in session storage;
+the chief stays the only writer.
## implement
diff --git a/skills/chief-of-staff/SKILL.md b/skills/chief-of-staff/SKILL.md
index 48e035b..bd78e87 100644
--- a/skills/chief-of-staff/SKILL.md
+++ b/skills/chief-of-staff/SKILL.md
@@ -25,6 +25,8 @@ comparison through [Swarm](../swarm/SKILL.md) or [Arena](../arena/SKILL.md).
The user can enter an owner's session directly; reconcile their new direction
before you dispatch work that depends on it.
+Only a Claude session with the artifact tools can be chief, since the chief
+publishes the command center; elsewhere, ask the user to start the chief in one.
Only the record's current chief acts on program items and writes the record.
Check it before acting on a request, direction, report, event, or pasted answer;
a thread that is not the chief forwards the item under
@@ -72,9 +74,9 @@ unmerged work: update the record and owners, then reconcile pending landings
under [withdraw a release](references/landing.md#withdraw-a-release).
Set up the [program record](references/program-state.md) before you start or
-adopt owners, so another chief could resume from it. Reuse the project's
-tracker; without one, keep a task-owned file record. A coordination request
-needs no new tracker. When a small choice will recur, propose a
+adopt owners, so another chief could resume from it. Work items stay in the
+project's tracker, and a coordination request needs no new one. When a small
+choice will recur, propose a
[standing approval](references/decisions.md#standing-approvals) with the plan;
owners start without waiting for the answer.
@@ -101,8 +103,8 @@ top-level sessions, as in a plain Claude Code or Codex session, start a
delegated owner in its own host-managed worktree instead. Use
[Orchestration](../orchestration/SKILL.md) for model choice and concurrency.
Give each branch one writer, and serialize stack operations and shared
-resources. Record each owner with its start mode, route, and report path. Only
-you start program owners.
+resources. Record each owner with its start mode and route. Only you start
+program owners.
Each owner checks its access and proof before work runs unattended, under
Implement's [prepare](../implement/SKILL.md#2-state-the-finish-line-and-prepare).
@@ -212,13 +214,12 @@ user, its requested PR state, or a repair with its owner.
## 5. Show and continue
-Keep the [command center](references/program-state.md#command-center) current
-and host it through [dashboard hosting](references/hosting.md), never inline. It
-shows every outcome once, with its finish line, results and how to open them,
-provisional condition, owner, layers, and what each Need blocks. Results come
-from owners' Shown reports: link what they showed, say what opening it needs,
-and name the gaps. Refresh on meaningful results and decisions, and show
-freshness honestly.
+[Publish the command center](references/hosting.md) after each record write and
+give the user its link, never an inline render. It shows every outcome once,
+with its finish line, results and how to open them, provisional condition,
+owner, layers, and what each Need blocks. Results come from owners' Shown reports: link what they
+showed, say what opening it needs, and name the gaps. Keep each row's
+observation time honest.
Answers arrive through chat or the host's input tools. Record each under
[record an answer](references/decisions.md#record-an-answer), then update the
@@ -228,8 +229,8 @@ Choose user updates from the owner reports: what the user can now use, try,
decide, or unblock. Keep routine checks, recovery, and unchanged milestones in
the record, and answer direct questions in the detail asked.
-Done when the hosted view matches the record and shows every result and open
-Need.
+Done when the published page matches the last commit and shows every result
+and open Need.
## 6. Settle or resume
@@ -246,9 +247,10 @@ usable outcomes and their links. Name the decisions you resolved so the user can
reopen them. Clean up through [Repo Cleanup](../repo-cleanup/SKILL.md) once
retained work and active dependents no longer need the resources.
-Before pausing or moving, bring the record current. On resume, reconcile it with
-current files, threads, and forge state before dispatching. To move the program
-to another thread or take over from a chief that cannot act, follow
-[chief move](references/chief-move.md). This skill supplies no background
-wakeups: native goal tracking, recurring refreshes, and other automation need an
+Before pausing or moving, bring the record current. On resume, start from the
+record's `next`, reconcile the record with current files, threads, and forge
+state, and [publish the page](references/hosting.md), before dispatching. To
+move the program to another thread or take over from a chief that cannot act,
+follow [chief move](references/chief-move.md). This skill supplies no background
+wakeups: native goal tracking, recurring checks, and other automation need an
explicit user request and host support.
diff --git a/skills/chief-of-staff/agents/openai.yaml b/skills/chief-of-staff/agents/openai.yaml
index cef10e6..6a8ce45 100644
--- a/skills/chief-of-staff/agents/openai.yaml
+++ b/skills/chief-of-staff/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
display_name: "Chief of Staff"
- short_description: "Carry several outcomes through Implement owners"
- default_prompt: "Use $chief-of-staff to carry these outcomes to their finish lines through Implement owners and keep the command center current."
+ short_description: "Carry several outcomes through Implement owners; the chief runs in Claude"
+ default_prompt: "Use $chief-of-staff to carry these outcomes to their finish lines through Implement owners."
diff --git a/skills/chief-of-staff/assets/command-center.html b/skills/chief-of-staff/assets/command-center.html
index 9d5bf9a..bcb6ddd 100644
--- a/skills/chief-of-staff/assets/command-center.html
+++ b/skills/chief-of-staff/assets/command-center.html
@@ -1,5 +1,5 @@
-
-
+
+
@@ -257,7 +257,6 @@
-
diff --git a/skills/chief-of-staff/assets/example-state.json b/skills/chief-of-staff/assets/example-state.json
index 67717e1..e82ef5f 100644
--- a/skills/chief-of-staff/assets/example-state.json
+++ b/skills/chief-of-staff/assets/example-state.json
@@ -2,12 +2,64 @@
"title": "Export reliability",
"goal": "Make exports dependable and let people see and regenerate their export history.",
"finishLine": "Each outcome has its own finish line. Land outcomes merge on the chief's release; Try outcomes wait for your choice.",
- "updatedAt": "2026-11-02T17:00:00Z",
"demo": true,
- "needs": [
+ "chief": "Chief thread (example)",
+ "hosting": "https://claude.ai/artifact/ExampleCommandCenter",
+ "next": "Release W1's landing once PR 44 is ready.",
+ "request": {
+ "text": "Make exports dependable, then let people see and regenerate their export history.",
+ "sources": [
+ "EXP-100",
+ "EXP-101",
+ "EXP-102",
+ "EXP-103",
+ "Chat message 2026-11-01"
+ ]
+ },
+ "corrections": [
+ {
+ "date": "2026-11-01",
+ "source": "Chat message 2026-11-01",
+ "text": "Export history is a **Try**: show it before anything merges."
+ }
+ ],
+ "requirements": [
+ {
+ "date": "2026-11-01",
+ "source": "EXP-101",
+ "text": "A retried export never emails the same file twice."
+ }
+ ],
+ "approvals": [
+ {
+ "id": "SA-1",
+ "date": "2026-11-01",
+ "source": "Chat message 2026-11-01",
+ "text": "Pick the existing component when a new view could reuse one.",
+ "covers": "Component reuse for new views",
+ "picks": "The existing component"
+ }
+ ],
+ "directions": [
+ {
+ "date": "2026-11-01",
+ "source": "Chat message 2026-11-01",
+ "text": "Hold W6's landing until the support team reviews link expiry.",
+ "until": "Support confirms the expiry copy"
+ }
+ ],
+ "landingAuthority": {
+ "date": "2026-11-01",
+ "source": "Chat message 2026-11-01",
+ "text": "Land covers W0, W1, W2, and W6 in example/exports. Required checks and review stay the gate."
+ },
+ "decisions": [
{
"id": "W6-D1",
- "ask": "Should expired export links remain visible?",
+ "question": "Should expired export links remain visible?",
+ "date": "2026-11-02",
+ "source": "Expiry owner report",
+ "status": "open",
"context": [
"Expiry needs a settled retention rule before it can be accepted.",
"Existing tests cover downloads, but do not specify expired-link behavior."
@@ -29,12 +81,14 @@
"id": "W6",
"phase": "acceptance"
}
- ],
- "status": "open"
+ ]
},
{
"id": "W5-T",
- "ask": "Keep, change, or scrap the export history view?",
+ "question": "Keep, change, or scrap the export history view?",
+ "date": "2026-11-02T16:40:00Z",
+ "source": "History owner's Shown report",
+ "status": "open",
"context": [
"Built, proved, and shown. Nothing else depends on it."
],
@@ -63,12 +117,14 @@
"label": "Open the preview",
"url": "https://example.com/previews/export-history"
}
- ],
- "status": "open"
+ ]
},
{
"id": "W1-D2",
- "ask": "How many retries should an interrupted export get?",
+ "question": "How many retries should an interrupted export get?",
+ "date": "2026-11-02",
+ "source": "Retry owner report",
+ "status": "open",
"weight": "quick",
"context": [
"Other worker jobs use three attempts."
@@ -83,12 +139,14 @@
"label": "Five attempts",
"effect": "Rides out longer outages but reports failures later."
}
- ],
- "status": "open"
+ ]
},
{
"id": "W3-D1",
- "ask": "When should the simulator fixture reset?",
+ "question": "When should the simulator fixture reset?",
+ "date": "2026-11-02",
+ "source": "Support worker report",
+ "status": "open",
"weight": "quick",
"options": [
{
@@ -100,12 +158,14 @@
"recommended": true,
"effect": "Adds about a minute and stops state leaking."
}
- ],
- "status": "open"
+ ]
},
{
"id": "W5-D2",
- "ask": "Which component should render export history?",
+ "question": "Which component should render export history?",
+ "date": "2026-11-02",
+ "source": "History owner report",
+ "status": "resolved",
"context": [
"The history view needs a sortable list."
],
@@ -115,13 +175,15 @@
"phase": "acceptance"
}
],
- "status": "resolved",
"answer": "Use the shared data table.",
"basis": "Project rule: history views reuse the shared data table. Swapping it later needs no data change."
},
{
"id": "W0-D1",
- "ask": "Should a retried export keep its original job ID?",
+ "question": "Should a retried export keep its original job ID?",
+ "date": "2026-11-01",
+ "source": "Idempotency owner report",
+ "status": "answered",
"context": [
"Keeping the ID lets users follow one export through retries."
],
@@ -131,8 +193,22 @@
"phase": "landing"
}
],
- "status": "answered",
- "answer": "Keep the original job ID."
+ "answer": "Keep the original job ID.",
+ "answerSource": "Chat message 2026-11-02"
+ },
+ {
+ "id": "W2-D1",
+ "question": "Paginate history by cursor or by page?",
+ "date": "2026-11-02",
+ "source": "API owner report",
+ "status": "withdrawn",
+ "blocks": [
+ {
+ "id": "W2",
+ "phase": "proof"
+ }
+ ],
+ "rationale": "The endpoint returns at most 50 rows, so it needs no pagination."
}
],
"work": [
@@ -142,16 +218,25 @@
"repo": "example/exports",
"kind": "outcome",
"description": "Retry interrupted exports without creating duplicate jobs or losing cancellation requests.",
- "finishLine": "land",
- "state": "in-review",
- "owner": "Retry owner",
- "updatedAt": "2026-11-02T16:55:00Z",
+ "scope": "Covers `jobs/retry.py` and its queue wiring. Proof: an interrupted export resumes once. Leaves export history to W5.",
+ "sources": [
+ "Chat message 2026-11-01"
+ ],
"tickets": [
{
"label": "EXP-101",
"url": "https://example.com/tracker/EXP-101"
}
],
+ "finishLine": "land",
+ "finishLineReason": "Land: a contained repair the user asked to ship.",
+ "state": "in-review",
+ "owner": {
+ "name": "Retry owner",
+ "start": "top-level",
+ "route": "`t3_thread_send` to the chief",
+ "workspace": "Worktree on `retry-export`"
+ },
"layers": [
{
"id": "W1-L1",
@@ -176,7 +261,9 @@
"label": "Retry fixture run",
"url": "https://example.com/exports/actions/runs/7"
}
- ]
+ ],
+ "acceptance": "Retry proof passed at the current head; burst timing untested.",
+ "checkedAt": "2026-11-02T16:55:00Z"
},
{
"id": "W2",
@@ -184,17 +271,19 @@
"repo": "example/exports",
"kind": "outcome",
"description": "Let API clients see how many attempts an export has made and whether it will retry.",
- "finishLine": "land",
- "effectiveFinishLine": "try",
- "state": "building",
- "owner": "API owner",
- "updatedAt": "2026-11-02T16:50:00Z",
"tickets": [
{
"label": "EXP-103",
"url": "https://example.com/tracker/EXP-103"
}
],
+ "finishLine": "land",
+ "state": "building",
+ "owner": {
+ "name": "API owner",
+ "start": "top-level",
+ "route": "`t3_thread_send` to the chief"
+ },
"provisional": {
"basis": "W1-L2, the Retry owner's unmerged cancellation layer.",
"clears": "W1 lands and this outcome's proof is refreshed on the new base.",
@@ -209,7 +298,8 @@
"status": "local",
"stackedOn": "W1-L2"
}
- ]
+ ],
+ "checkedAt": "2026-11-02T16:50:00Z"
},
{
"id": "W5",
@@ -217,16 +307,29 @@
"repo": "example/exports",
"kind": "outcome",
"description": "Add an export history view with download and regeneration controls.",
- "finishLine": "try",
- "state": "shown",
- "owner": "History owner",
- "updatedAt": "2026-11-02T16:40:00Z",
"tickets": [
{
"label": "EXP-102",
"url": "https://example.com/tracker/EXP-102"
}
],
+ "finishLine": "try",
+ "state": "shown",
+ "owner": {
+ "name": "History owner",
+ "start": "top-level",
+ "route": "`t3_thread_send` to the chief",
+ "resources": "History preview server on port 8105; stop with `kill` on its recorded PID."
+ },
+ "layers": [
+ {
+ "id": "W5-L1",
+ "branch": "export-history",
+ "status": "local",
+ "head": "c03e5d8",
+ "base": "main"
+ }
+ ],
"results": [
{
"label": "Preview build",
@@ -238,15 +341,7 @@
"url": "https://example.com/previews/export-history/compare"
}
],
- "layers": [
- {
- "id": "W5-L1",
- "branch": "export-history",
- "status": "local",
- "head": "c03e5d8",
- "base": "main"
- }
- ]
+ "checkedAt": "2026-11-02T16:40:00Z"
},
{
"id": "W6",
@@ -255,10 +350,12 @@
"kind": "outcome",
"description": "Expire download links after seven days and offer regeneration.",
"finishLine": "land",
- "effectiveFinishLine": "try",
"state": "building",
- "owner": "Expiry owner",
- "updatedAt": "2026-11-02T16:52:00Z",
+ "owner": {
+ "name": "Expiry owner",
+ "start": "top-level",
+ "route": "`t3_thread_send` to the chief"
+ },
"provisional": {
"basis": "W6-D1's recommended option: expired entries stay visible.",
"clears": "W6-D1 is answered and the affected proof is refreshed."
@@ -269,7 +366,8 @@
"branch": "export-expiry",
"status": "local"
}
- ]
+ ],
+ "checkedAt": "2026-11-02T16:52:00Z"
},
{
"id": "W3",
@@ -278,8 +376,12 @@
"kind": "support",
"description": "Restore the shared simulator fixture so export UI checks can run reliably.",
"state": "building",
- "owner": "Support worker",
- "updatedAt": "2026-11-02T16:58:00Z"
+ "owner": {
+ "name": "Support worker",
+ "start": "delegated",
+ "route": "End its turn with the report"
+ },
+ "checkedAt": "2026-11-02T16:58:00Z"
},
{
"id": "W4",
@@ -291,7 +393,7 @@
"state": "planned",
"required": false,
"origin": "Found while building W1; separate from retry correctness.",
- "updatedAt": "2026-11-02T16:45:00Z"
+ "checkedAt": "2026-11-02T16:45:00Z"
},
{
"id": "W0",
@@ -299,16 +401,19 @@
"repo": "example/exports",
"kind": "outcome",
"description": "Give each export a stable key so a retry cannot start a second job.",
- "finishLine": "land",
- "state": "landed",
- "owner": "Idempotency owner",
- "updatedAt": "2026-11-02T15:30:00Z",
"tickets": [
{
"label": "EXP-100",
"url": "https://example.com/tracker/EXP-100"
}
],
+ "finishLine": "land",
+ "state": "landed",
+ "owner": {
+ "name": "Idempotency owner",
+ "start": "adopted",
+ "route": "`t3_thread_send` to the chief"
+ },
"layers": [
{
"id": "W0-L1",
@@ -317,13 +422,14 @@
"url": "https://example.com/exports/pull/39"
}
],
- "origin": "Split from W1 so retries could land on a stable job identity.",
"evidence": [
{
"label": "Duplicate-job fixture run",
"url": "https://example.com/exports/actions/runs/5"
}
- ]
+ ],
+ "origin": "Split from W1 so retries could land on a stable job identity.",
+ "checkedAt": "2026-11-02T15:30:00Z"
}
],
"revisits": [
@@ -341,5 +447,41 @@
"revisit": "If support needs per-attempt logs, add an attempt number.",
"source": "W0-D1"
}
+ ],
+ "discoveries": [
+ {
+ "date": "2026-11-02",
+ "origin": "W2",
+ "text": "Export filenames repeat across time zones.",
+ "route": "Saved as snip EX-7"
+ }
+ ],
+ "landings": [
+ {
+ "id": "L1",
+ "date": "2026-11-02",
+ "chief": "Chief thread (example)",
+ "lander": "Idempotency owner",
+ "target": "main",
+ "work": [
+ "W0"
+ ],
+ "prs": [
+ {
+ "url": "https://example.com/exports/pull/39",
+ "head": "4f2a91c",
+ "base": "9be03d1"
+ }
+ ],
+ "state": "landed"
+ }
+ ],
+ "waits": [
+ {
+ "date": "2026-11-02T16:50:00Z",
+ "work": "W1",
+ "on": "PR 44 checks",
+ "event": "Required checks finish"
+ }
]
}
diff --git a/skills/chief-of-staff/assets/owner-kickoff.md b/skills/chief-of-staff/assets/owner-kickoff.md
index fdbb5df..edf26c2 100644
--- a/skills/chief-of-staff/assets/owner-kickoff.md
+++ b/skills/chief-of-staff/assets/owner-kickoff.md
@@ -1,7 +1,7 @@
/implement
-Program: . Program record (read-only except your report file): .
-Original request and accepted corrections: .
+Program: . Program record (read-only): `git -C show HEAD:program.json`.
+Original request, accepted corrections, and preserved requirements: its `request`, `corrections`, and `requirements`.
Outcome : . Sources: . Prefix decision request IDs with .
Scope: .
@@ -13,11 +13,11 @@ Inputs available at that base: . Uncommitted inputs: . Your own planned layers are not provisional. Provisional work never merges and never takes destructive or outward-facing steps on its own; the user's scrap allows its scoped cleanup per Implement. Land becomes Try, and a requested draft or review-ready PR state can still be reached. Publish such a PR with a Provisional section in its body, per PR's body guide; that section holds its status until the condition clears and proof is refreshed.
Finish line: , picked because .
-Landing authority: . Land within that scope is the merge request Merge needs, and it takes effect only on the current chief's recorded landing release for your PR or stack.
-Landing: report readiness and end your turn. Land through Merge only on that release. Immediately before submitting, re-read the record: confirm the current chief, your active release, you as its one named lander, and no affecting hold or decision; match the forge's head/base for every PR in the set to the release. Report a mismatch instead of landing. Sets that span several owners are landed by the chief.
+Landing authority: `landingAuthority` and this outcome's finish line. Land within that scope is the merge request Merge needs, and it takes effect only on the current chief's recorded landing release for your PR or stack.
+Landing: report readiness and end your turn. Land through Merge only on that release. Immediately before submitting, re-read the record: confirm `chief`, your active release in `landings`, you as its one named lander, and no affecting hold or decision; match the forge's head/base for every PR in the set to the release. Report a mismatch instead of landing. Sets that span several owners are landed by the chief.
Holds and user gates: .
Delegation allowance: you may start the bounded helpers your workflow needs as your own subagents: implementation, verification, and review helpers, and Swarm workers that fit. . Start no new program owners; send work that needs one to the chief.
-Owner report: write it to /reports/.md. Send a partial report as soon as you hit a missing prerequisite, a human decision, or a pending PR, naming what continues and the event that resumes the rest.
+Owner report: write it to /reports/.md. Send a partial report as soon as you hit a missing prerequisite, a human decision, or a pending PR, naming what continues and the event that resumes the rest.
Route: with t3_thread_send; you watch your PR | delegated: end your turn with it; the chief thread watches your PR>.
Decision requests and shared blockers use the same route as they arise. In each decision request, say what reversing each option would take and cite any recorded source that settles it.
diff --git a/skills/chief-of-staff/references/chief-move.md b/skills/chief-of-staff/references/chief-move.md
index 464282e..6b984d6 100644
--- a/skills/chief-of-staff/references/chief-move.md
+++ b/skills/chief-of-staff/references/chief-move.md
@@ -6,30 +6,37 @@ from acting on stale state or writing over the record.
## Check the chief
Before acting on a request, direction, event, report, or pasted answer, and
-before each record write, read the record's current chief. A thread that is not
-the chief reads the chief's thread, forwards the item with `t3_thread_send`,
-linking the user's message for requests, directions, and answers, and stops. It
-unwatches its PRs when the chief asks. A user item that arrives through
-`t3_thread_send` counts under [record an answer](decisions.md#record-an-answer).
+before each record write, read `chief` from
+`git -C /absolute/path show HEAD:program.json`. A thread that is not the chief
+reads the chief's thread, forwards the item with `t3_thread_send`, linking the
+user's message for requests, directions, and answers, and stops. It unwatches
+its PRs when the chief asks. A user item that arrives through `t3_thread_send`
+counts under [record an answer](decisions.md#record-an-answer).
## Move
-Only the chief moves the program to a new thread. In one record update it names
-the new chief and each live owner's new return route, which is `t3_thread_send`
-to that chief even for an owner that used to end its turn. It then tells the new
-thread with `t3_thread_send`. Owners keep running.
+Only the chief moves the program to a new thread. In one
+[record write](program-state.md#write-the-record) it names the new chief in
+`chief` and each live owner's new return route in its `owner.route`, which is
+`t3_thread_send` to that chief even for an owner that used to end its turn.
+Right after the commit, and without publishing, it tells the new thread with
+`t3_thread_send`. Owners keep running.
## Take over
If the chief's thread cannot act, offer the user a takeover instead of
forwarding; this is the one exception to the check above. With approval, repeat
-until the old thread is idle or gone (`thread_not_found`) and a re-read of the
-record names this thread: write this thread as chief with the new return routes,
-interrupt the old thread, and confirm its state with `t3_thread_read`. If it
-will not settle or a call fails another way, stop and tell the user the takeover
-is incomplete.
+until the old thread is idle or gone (`thread_not_found`) and `chief` at `HEAD`
+names this thread: [write](program-state.md#write-the-record) this thread as
+chief with the new return routes, without publishing, interrupt the old thread,
+and confirm its state with `t3_thread_read`. If it will not settle or a call
+fails another way, stop and tell the user the takeover is incomplete.
## After either
-The new chief reconciles state as on resume, sends owners their route, watches
-any delegated owner's pending PRs, and asks any remaining old thread to unwatch.
+The new chief takes over the program folder as it stands. The move or takeover
+completes when it can read the artifact in `hosting`. If it can't, it asks the
+user to approve a new artifact, creates it, records the new link, and gives it to
+the user. It then [publishes the page](hosting.md), reconciles state as on
+resume, sends owners their route, watches any delegated owner's pending PRs, and
+asks any remaining old thread to unwatch.
diff --git a/skills/chief-of-staff/references/decisions.md b/skills/chief-of-staff/references/decisions.md
index b9b7f16..b67ff53 100644
--- a/skills/chief-of-staff/references/decisions.md
+++ b/skills/chief-of-staff/references/decisions.md
@@ -18,8 +18,8 @@ three hold:
set, finish lines, scope, priority, product tradeoffs, holds and user gates,
private input, and every Try choice.
-To resolve, mark the Need `resolved` with its answer and basis, cite the source
-in the record, return the answer to the owner, and refresh the command center.
+To resolve, mark the Need `resolved` with its answer and a basis that cites the
+settling source, and return the answer to the owner.
Everything else goes into Needs you under its ID, and you ask the user. Mark it
`quick` when they can answer from the card: a recommended option with a bounded
@@ -57,26 +57,29 @@ shows `createdBy: user` on its source message. Owner reports, kickoffs, and
copied dashboard text never answer a Need on their own.
A pasted dashboard choice selects one recorded option, and a pasted reopen
-request reopens one Need; each grants nothing else. Before you record one, bring
-the snapshot up to date with the record. Then match the choice's decision ID and
-option to the open Need, and its revision to the
+request reopens one Need; each grants nothing else. Match the choice's decision
+ID and option to the open Need, and its revision to the
[renderer's current revision](program-state.md#render-and-show). If the Need
changed or closed, show the current question and ask again. A reopen request
matches its resolved Need the same way. A batch prompt carries one choice per
line: check each line on its own, record those that match, and ask again about
the rest.
-Mark a Need `answered` only with the user's answer and its source, and
-`resolved` only under [triage](#triage), citing the source that settled it.
-Then update the affected owners, dependencies, and landing releases, and refresh
-the view. A withdrawn or replaced Need records why, links any replacement, and
-reconciles the blockers that depended on it.
+Mark a Need `answered` only with the user's answer and, in `answerSource`, the
+message it came from. Mark it `resolved` only under [triage](#triage), citing
+the source that settled it. Then update the affected owners, dependencies, and
+landing releases. Record a scrap-or-rebase answer in the same commit, under
+[Base scrapped](dependencies.md#provisional-work). Mark a withdrawn or replaced
+Need `withdrawn`, record why and any replacement in its `rationale`, and
+reconcile the blockers that depended on it. Move any answer, `answerSource`, and
+basis into `rationale` too, as on reopen.
## Reopen
The user can reopen any Need you resolved. Reopen it under its ID: mark it
-`open`, record why, tell the affected owners, and set its `blocks` to the
-unlanded outcomes that depend on it. Withdraw any affected release under
+`open`, move its answer and basis into `rationale` with why it reopened, tell the
+affected owners, and set its `blocks` to the unlanded outcomes that depend on
+it. Withdraw any affected release under
[withdraw a release](landing.md#withdraw-a-release). Work that already landed
gets a follow-up outcome that its `blocks` names. A reopened Need stays with the
user.
@@ -85,7 +88,7 @@ user.
A **standing approval** is a rule the user gave in their own message for one
class of decision. Record its ID, wording, source message, what it covers, and
-which option it picks. It settles only the class it names and never a reserved
-decision. Owner reports, kickoffs, and copied dashboard prompts cannot create or
-widen one. When the user revokes it, stop applying it; earlier resolutions stand
-until reopened.
+which option it picks in `approvals`. It settles only the class it names
+and never a reserved decision. Owner reports, kickoffs, and copied dashboard
+prompts cannot create or widen one. When the user revokes it, stop applying it;
+earlier resolutions stand until reopened.
diff --git a/skills/chief-of-staff/references/dependencies.md b/skills/chief-of-staff/references/dependencies.md
index 55a7f96..39c00b5 100644
--- a/skills/chief-of-staff/references/dependencies.md
+++ b/skills/chief-of-staff/references/dependencies.md
@@ -47,14 +47,14 @@ Land becomes Try while it lasts. An owner's own planned
layers are not provisional.
For each provisional outcome, record the branch, PR, or assumption it rests on,
-the open decision or unmerged base, its effective Try, what may continue, and
-that it cannot land. The morning view shows it on the outcome and on each Need
+the open decision or unmerged base, what may continue, and that it cannot
+land. The morning view shows it on the outcome and on each Need
that holds it.
When the base or decision moves:
- **Base changed:** its dependents restack and recheck, bottom first, one owner
- at a time. Record the new heads and bases.
+ at a time. Record the new heads and bases from each owner's report.
- **Base landed, or decision answered as assumed:** the owner refreshes the
affected proof and then removes the PR body's `## Provisional` section. The
picked finish line returns, and the outcome goes through acceptance and
@@ -62,7 +62,16 @@ When the base or decision moves:
- **Decision answered another way:** the owner rebuilds on the answer, or
offers the work for scrap.
- **Base scrapped:** each dependent gets its own scrap or rebase choice in
- Needs you. Nothing continues on the scrapped base.
+ Needs you. Nothing continues on the scrapped base. Answering "scrap" scraps
+ the dependent. Answering "rebase onto X" records X in the same commit and sets
+ the dependent's `base`:
+ - X is another outcome's unmerged layer: retarget `stackedOn` to that layer, and
+ retarget `provisional` from the scrapped work to X's outcome.
+ - X is the target branch: drop `stackedOn`, and remove the scrapped work from
+ `provisional.on`. If nothing remains, drop `provisional`, and the owner then
+ follows **Base landed**.
+
+ Its owner then restacks, and the report updates `head`.
When two options are close and building both costs less than waiting, the
outcome's owner builds each on its own branch, as a Swarm race. The answer keeps
diff --git a/skills/chief-of-staff/references/hosting.md b/skills/chief-of-staff/references/hosting.md
index 3a5dc14..2b9160b 100644
--- a/skills/chief-of-staff/references/hosting.md
+++ b/skills/chief-of-staff/references/hosting.md
@@ -1,37 +1,25 @@
-# Host the command center
+# Publish the command center
-The command center is hosted only: give the user a link to return to, not an
-inline render in the chat. The page is a read-only snapshot. The chief owns its data; hosting serves the
-file and adds no coordinator, database, or wakeups. A static file server is enough.
+The command center is one private Claude artifact per program, and `hosting`
+holds its link.
-1. Choose a task-owned directory that survives worktree cleanup. Keep the
- program record, snapshot JSON, logs, and hosting details outside a dedicated
- public folder, and publish only the rendered page there.
-2. Render each snapshot to a staging file. When it validates, move it over the
- public `index.html` on the same filesystem, so the last valid page stays up
- and no partial file is served.
-3. Use Show Me's [web sharing](../../show-me/references/web-sharing.md) to choose
- hosting. For Tailscale, follow its procedure: a
- loopback-only origin on an unused port, existing Serve mappings preserved,
- verification, lifetime, and teardown. A small Python static file server is
- enough when the project has no server to reuse; serve only the public folder.
-4. Return the actual HTTPS URL after checking its normal hostname and page
- content, ideally from the user's phone or another tailnet client. A check
- with an explicit IP or DNS override proves only that path; label the rest
- unverified.
+After each commit under [write the record](program-state.md#write-the-record),
+render `HEAD` from this skill's folder:
-Record the URL, public path, origin process and start time, Serve mapping,
-exact stop commands, and access limits in the program record. The hosting
-machine must stay awake and connected, and viewers need tailnet access.
+```bash
+python3 scripts/render_dashboard.py --state /absolute/path/program.json --page /absolute/path/command-center.html
+```
-On each meaningful result or decision, replace the page at the same path.
-An open page fetches that URL every 20 seconds while visible and when the user
-returns to it, then shows a new snapshot in place with its tab, selection,
-sections, and scroll position kept. Each browser tab keeps that view for the
-URL, so a manual refresh returns to it too; a new tab starts at Needs you.
-Only the chief's next publish changes what any page shows, and answers still go
-through chat.
+It writes `command-center.next.html` beside the page file and prints `changed`
+when that differs from `command-center.html`, the last page you published. On
+`changed`, publish the `.next` file to the artifact in `hosting`, then move it
+over the page file:
-For teardown, follow the
-[web sharing](../../show-me/references/web-sharing.md#take-it-down)'s take-down
-rules. Keep the state and any page the chief still needs.
+```bash
+mv /absolute/path/command-center.next.html /absolute/path/command-center.html
+```
+
+A failed or skipped publish leaves the page file as it was, so the next render
+still prints `changed`. Create the artifact only when `hosting` has none, then
+record its link. On every resume and chief move, publish the page file even when
+unchanged, so skill updates reach it.
diff --git a/skills/chief-of-staff/references/landing.md b/skills/chief-of-staff/references/landing.md
index 28decbc..4f2a118 100644
--- a/skills/chief-of-staff/references/landing.md
+++ b/skills/chief-of-staff/references/landing.md
@@ -22,13 +22,14 @@ set at those heads.
Record exactly one lander for each merge set.
- **The owner,** for a set within its own outcome. Record a **landing release**
- from the current chief naming the owner, each PR with its accepted head and
- base, and any resources dependents still need, then resume the owner with it.
- The record is authoritative. Immediately before submitting a merge, the owner
- re-reads it to confirm the current chief, its active release, itself as the
- one lander, and no affecting decision or hold, and matches the forge's head
- and base to the release. A mismatch returns to you for reconciliation. If the
- owner cannot act, replace it under Orchestration's
+ in `landings` from the current chief, naming the owner, each PR with
+ its accepted head and base, and any resources dependents still need, then
+ resume the owner with it.
+ The record's last commit is authoritative. Immediately before submitting a
+ merge, the owner re-reads it to confirm the current chief, its active
+ release, itself as the one lander, and no affecting decision or hold, and
+ matches the forge's head and base to the release. A mismatch returns to you
+ for reconciliation. If the owner cannot act, replace it under Orchestration's
[replace an owner](../../orchestration/references/reuse-and-recovery.md#replace-an-owner)
first.
- **You,** for a set that spans owners, such as outcomes that must ship
diff --git a/skills/chief-of-staff/references/program-state.md b/skills/chief-of-staff/references/program-state.md
index d9c15c0..f7b10a3 100644
--- a/skills/chief-of-staff/references/program-state.md
+++ b/skills/chief-of-staff/references/program-state.md
@@ -1,166 +1,106 @@
# Program record and command center
Specs, tickets, snips, PRs, and user decisions stay where the project already
-keeps them. The **program record** links those sources and adds coordination
-state. The **command center** is a rendered snapshot of that record, not a second
-tracker.
+keeps them. The **program record** is one file, `program.json`, that links those
+sources and holds the program's coordination state, organized by topic. The
+**command center** renders it; the renderer picks what the page shows. Each fact
+has one home in the file.
## Program record
-Reuse an established record, or choose a task-owned directory that the chief and
-its owners can read and that survives worktree cleanup. Put its absolute path in
-the chief's handoff and every owner kickoff. It needs no wiki or committed state
-file.
+Keep `program.json` and a `reports/` folder in a task-owned directory that the
+chief and its owners can read and that survives worktree cleanup. Adopt an
+existing program's folder rather than starting another. Put the folder's
+absolute path in the chief's handoff and every owner kickoff.
+
+The program folder is a git repository. Its last commit is the record: the
+command center and every reader use `HEAD`, and the log is its history. The
+working `program.json` is only the chief's edit buffer. A new program starts
+from a new `program.json`: run `git -C /absolute/path init`, then add and
+commit `program.json` once it passes the check.
**One writer:** owners write only their own `reports/.md`. The
-current chief alone writes the rest of the record and the snapshot. Before each
-write, confirm the record still names this thread as chief; otherwise follow
-[chief move](chief-move.md).
-
-Keep, for the program:
-
-- The goal, original request and message sources, accepted corrections,
- preserved requirements, the agreed scope that Land's authority covers, and
- holds with their source and release condition.
-- Standing approvals, accepted tradeoffs with what would prompt revisiting them,
- and decision requests with their dispositions.
-- Discoveries with origin and destination, and support work with the owners it
- affects.
-- The current chief thread, the snapshot state and HTML paths, hosting details,
- and the next useful action.
-
-Keep, for each outcome under its stable ID:
-
-- **Sources:** the ticket, snip, PR, or message IDs it came from.
-- **Scope:** the result, the code it covers, its proof, and what it leaves to
- other outcomes.
-- **Finish line:** the picked line (Land, Try, review-ready PR, or draft PR) and
- why, and the effective line while it is provisional.
-- **Owner:** identity, thread or task IDs and links, start mode (top-level,
- delegated, or adopted), return route, report path, model, and workspace. An
- outcome can be planned without one.
-- **Inputs:** logical dependencies, branch-order constraints, and any blocks
- with what is missing, who can supply it, and what it blocks.
-- **Provisional:** the branch, PR, or assumption it rests on, the decision or
- landing that clears it, and what may continue meanwhile.
-- **Layers:** each PR, branch, or local artifact, with its link, head, base,
- and stack parent.
-- **Results:** what the owner showed and how to open it, with what that needs.
-- **Waits:** pending PRs, live helpers and watchers, and the events they await.
-- **Landing:** the merge set, its one lander, target branch, pending submission
- or queue state, and the landing release (issuing chief, lander, accepted
- head/base per PR, resources dependents still need, active or withdrawn).
-- **Disposition:** acceptance evidence, the delivery state, and any stop for the
- user.
-
-Date decisions and cite their sources. Code claims name the repository and
-revision they describe. Keep superseded reasoning when it explains a tradeoff,
-and recheck current facts at their source before acting on them.
-
-Each decision request keeps its ID, originating owner and message, exact
-question, affected outcomes with the phase each one blocks, and disposition
-under Orchestration's [decision requests](../../orchestration/SKILL.md#return).
-[Decisions](decisions.md) say when a Need is answered, resolved, reopened, or
-withdrawn.
+current chief alone writes `program.json`, after it
+[checks the chief](chief-move.md#check-the-chief).
-### Owner reports
+### Write the record
-Read each report against Implement's [report](../../implement/SKILL.md#report)
-fields; ask no other format. Record, from each:
-
-| Report field | Record it under |
-| --- | --- |
-| Outcome and finish line | The outcome's finish line and delivery state |
-| Layers | Layers, after checking each head and base at the forge |
-| Proof | Acceptance evidence, with every blocked or untested assertion kept visible |
-| Shown | Results, with access limits, servers, and stop actions |
-| Waits | Waits, and which pending PRs this thread watches |
-| Decisions | Needs, with the phase each blocks, and provisional assumptions |
-| Follow-ups | Discoveries, each with its disposition |
-| Sources | Handled follow-up messages, so none is answered twice |
+Record each owner report, answer, direction, and decision in one write, before
+you reply or move on:
-Also record task IDs an owner returns when it resumes.
+1. Look for an earlier uncommitted change:
-## Command center
+ ```bash
+ git -C /absolute/path status --porcelain -- program.json
+ ```
-Before each refresh, read the relevant owner reports, user direction, tickets,
-and live forge state. Each work row carries the time its state was last
-observed, separate from the snapshot's assembly time. Keep an unchecked row's
-old observation time; use `unknown` with `unknownReason` when its state cannot be
-read.
+ Any output means a failed commit or an outside edit. Show the user
+ `git -C /absolute/path diff HEAD -- program.json` and settle it with them
+ before you build on it. If they keep it, check and commit it; otherwise run
+ `git -C /absolute/path checkout HEAD -- program.json`.
+2. Edit `program.json` in place, changing only what the event changed. Never
+ regenerate the whole file; that silently drops history.
+3. Check it from this skill's folder, and fix every error it reports. When only
+ the layout is wrong, run the command it prints.
-A row is an outcome, a follow-up (an outcome that came from another one, with
-its own finish line), or a support task. Count an outcome once whatever its
-layers. Outcome states:
+ ```bash
+ python3 scripts/render_dashboard.py --state /absolute/path/program.json --check
+ ```
-- `planned`: not started. `building`: building and proving.
-- `shown`: a Try result built, proved, and shown, waiting for the user's merge,
- changes, or scrap choice. It needs `results`.
-- `in-review`: a PR is open while review or CI continues.
-- `ready`: every layer at readiness for its finish line: ready or merged for
- Land or a review-ready PR, a draft or merged for a draft PR. Provisional work
- is never ready, on any finish line: one that reaches its requested PR state
- stays `in-review` with its Provisional badge. Shown under In review with a
- readiness badge.
-- `landed`: every layer confirmed merged. `scrapped`: the user scrapped it, or it
- was declined or made obsolete; the record says which.
-- `done`: completed support work. An outcome never uses `done`.
-
-`blockedBy` lists logical inputs not yet satisfied under the
-[dependency rule](dependencies.md#dependencies); drop an edge once it is. A
-layer's `stackedOn` names the layer below it. When that layer belongs to another
-owner's outcome and is unmerged, the dependent is provisional on that outcome.
-An outcome that was published before it became provisional, through a later
-decision or adoption onto another owner's PR, keeps its PR layers; only a Try
-finish line forbids them.
+4. Read `git -C /absolute/path diff -- program.json`. A removed or changed line
+ the event didn't call for is a mistake. Restore it and check again. Mark a
+ change to a history entry in place instead of deleting it.
+5. Commit with a message naming the event:
-A base is the outcome that work rests on through `provisional.on` or a live
-layer's `stackedOn`. A base is **dead** when it was scrapped, or when it has not
-landed and every one of its layers is closed; one closed layer beside a live or
-merged one does not count. Active work on a dead base, whether provisional or
-the same owner's stack, needs an open Need, such as its scrap-or-rebase choice,
-that halts it at `start`, `access`, or `proof`. The card shows the base once, as
-a Scrapped base or Closed base blocker. A live layer stacked on a closed layer of
-a base that is still alive needs the same Need, or a restack.
-Before marking a row terminal, clear its active blockers and open Needs; history
-stays in the record. Unresolved work stays visible even after its owner failed
-or its PR was abandoned.
+ ```bash
+ git -C /absolute/path commit -m "R2 report: PR 12 open" program.json
+ ```
-Each Need's `blocks` names the outcomes it holds and the earliest phase each
-waits at: `start`, `access`, `proof`, `acceptance`, or `landing`. The first three
-halt that outcome's work. `acceptance` and `landing` let building and proof
-continue, so a shown Try waiting for its choice or a provisional build carries
-an open Need without being blocked. Put what continues meanwhile in
-`meanwhile`.
+6. [Publish the command center](hosting.md), except after a
+ [chief move](chief-move.md), where the new chief publishes.
-Every snapshot carries all open and resolved Needs, so each resolved one stays
-reopenable; answered ones may remain. Omit a withdrawn or superseded Need only
-after recording its disposition. If work lands while a Need about it is open,
-keep the landed row and point the Need's `blocks` at a follow-up row for any
-change the answer requires.
+A write is done when its commit lands and its page is published. On a git or
+publish error, stop and report it to the user.
-### Snapshot format
+### File format
-The renderer reads one JSON object. Fields without `?` are required. Times are
-ISO timestamps with a timezone, such as `2026-11-02T17:00:00Z`. Omit absent
-optional fields rather than sending empty strings. IDs are unique within each
-collection, and layer IDs are unique across the program.
+`program.json` is one JSON object. Fields without `?` are required, and
+`--check` rejects fields the sketch doesn't name. Times are ISO timestamps with
+a timezone, such as `2026-11-02T17:00:00Z`; dates are `YYYY-MM-DD` or
+timestamps. Prose is markdown. Omit absent optional fields rather than sending
+empty strings. IDs are unique within each collection, and layer IDs
+are unique across the program. The [example file](../assets/example-state.json)
+uses fictional work.
```ts
-type Link = { label: string; url: string };
-type Snapshot = {
- title: string;
- goal: string;
+type Program = {
+ title: string; goal: string;
finishLine?: string; // a program-wide note, such as who decides merges
- updatedAt: string; // snapshot assembly time
- demo?: boolean; // true only for an illustrative example
- needs: Need[];
+ demo?: boolean; // true only for an illustrative example
+ chief: string; // the current chief thread
+ hosting?: string; // the command center artifact's link
+ next: string; // the next useful action
+ request: { text: string; sources?: string[] }; // the original request and its message sources
+ corrections?: Entry[]; // accepted corrections
+ requirements?: Entry[]; // preserved requirements
+ approvals?: (Entry & { id: string; covers: string; picks: string })[]; // standing approvals
+ directions?: (Entry & { until?: string })[]; // standing directions; until: what releases a hold
+ landingAuthority?: Entry; // the agreed scope Land covers, and its gates
+ decisions: Decision[];
work: Work[];
revisits?: Revisit[];
+ discoveries?: { date: string; origin: string; text: string; route: string }[];
+ landings?: Landing[];
+ waits?: { date: string; on: string; event: string; work?: string }[];
};
+type Entry = { date: string; source: string; text: string };
+type Link = { label: string; url: string };
type Phase = "start" | "access" | "proof" | "acceptance" | "landing";
-type Need = {
- id: string; ask: string;
+
+type Decision = {
+ id: string; question: string;
+ date: string; source: string; // when it arrived, and the report or message that raised it
+ status: "open" | "answered" | "resolved" | "withdrawn"; // resolved: the chief settled it
context?: string[];
meanwhile?: string; // what continues while it is open
recommendation?: string; // rationale for the recommended option, or a suggestion when there are no options
@@ -168,30 +108,42 @@ type Need = {
weight?: "quick" | "judgment"; // default judgment; quick needs two or more options and a recommended one
blocks?: { id: string; phase: Phase }[]; // one entry per outcome, at the earliest phase it waits
links?: Link[];
- status: "open" | "answered" | "resolved"; // resolved: the chief settled it
- answer?: string; // required unless open
+ answer?: string; // required when answered or resolved
+ answerSource?: string; // required when answered: the user's message that answered it
basis?: string; // required when resolved: what settled it and why it is reversible
+ rationale?: string; // dated reasoning the page omits, such as why it reopened; when withdrawn, why and any replacement
};
+
type Work = {
id: string; title: string; repo: string;
- description?: string; // one or two sentences of scope
kind: "outcome" | "follow-up" | "support";
state: "planned" | "building" | "shown" | "in-review" | "ready"
| "landed" | "done" | "scrapped" | "unknown";
+ description?: string; // the result it delivers, in one or two sentences
+ scope?: string; // the code it covers, its proof, and what it leaves to other outcomes
+ sources?: string[]; // snip and message sources
+ tickets?: Link[];
finishLine?: "land" | "try" | "pr-review" | "pr-draft"; // required unless support
- effectiveFinishLine?: "try"; // required when provisional work's finish line is land; it keeps any PR it already has
- owner?: string; sessionUrl?: string; // owner required while building, shown, in review, or ready
+ finishLineReason?: string;
+ owner?: Owner; // required while building, shown, in review, or ready
provisional?: { basis: string; clears: string; on?: string[] }; // on: outcome IDs it rests on
layers?: Layer[]; // bottom first; not on support
results?: (Link & { access?: string })[]; // what the owner showed, and what opening it needs
- tickets?: Link[];
- blockedBy?: string[];
- blocked?: string; // another active blocker, such as an external dependency
+ evidence?: Link[]; // acceptance evidence
+ acceptance?: string; // blocked or untested assertions in its proof
+ blockedBy?: string[]; // logical inputs not yet satisfied
+ blocked?: string; // another active blocker, with what is missing and who can supply it
unknownReason?: string; // why the state could not be read; unknown rows only
- updatedAt: string; // last observation supporting this row
- evidence?: Link[];
+ checkedAt: string; // when its state was last observed
required?: boolean; // false marks proposed work outside committed scope
- origin?: string;
+ origin?: string; // the outcome or discovery it came from
+};
+type Owner = {
+ name: string; session?: string; // session: the owner's thread link
+ start: "top-level" | "delegated" | "adopted"; route: string;
+ tasks?: string[]; workspace?: string;
+ resources?: string; // servers, ports, and databases, with their stop actions
+ handled?: string[]; // follow-up messages already handled
};
type Layer = {
id: string; branch: string;
@@ -200,20 +152,113 @@ type Layer = {
head?: string; base?: string;
stackedOn?: string; // the layer below, in this or another outcome
};
-type Revisit = {
+type Revisit = { // an accepted tradeoff
id: string; title: string; tradeoff: string; revisit: string;
source?: string; // decision ID or source reference
};
+type Landing = {
+ id: string; date: string; chief: string; lander: string; target: string;
+ work: string[]; prs: { url: string; head: string; base: string }[]; // accepted head and base per PR
+ state: "released" | "submitted" | "landed" | "withdrawn";
+ keep?: string; // resources dependents still need
+ note?: string; // such as the queue state, or why it was withdrawn
+};
```
-The renderer rejects a snapshot that claims more than the record supports:
-provisional work that merged, keeps Land, or claims ready on any finish line; a
-Try finish line with a published PR; a shown Try with nothing to open; a ready
-outcome with a layer short of its finish line; a landed outcome with an unmerged
-layer; active work with no owner; a layer on another owner's unmerged layer
-without a provisional mark; active work on a dead base, or a live layer on a
-closed one, without a halting Need; a `start` Need on work that started; and
-dangling or cyclic edges, including work blocked by itself.
+Each decision request is a **Need**: one `decisions` entry from arrival to
+answer, kept after it settles so it stays reopenable. Some facts have a less
+obvious home:
+
+| Fact | Home |
+| --- | --- |
+| A stop for the user, including a Try choice | An open Need |
+| A chief call on the plan with no question for the user | A `resolved` decision |
+| A discovery that became work | That row's `origin` and `description` |
+| Support work, and the outcomes it unblocks | A `support` row, named in their `blockedBy` or `blocked` |
+| Branch order | Each layer's `stackedOn`, apart from `blockedBy` |
+| What provisional work continues meanwhile | `provisional`, or the Need's `meanwhile` |
+
+Date entries and cite their sources. Code claims name the repository and
+revision they describe. Recheck current facts at their source before acting on
+them. [Decisions](decisions.md) say when a Need is answered, resolved, reopened,
+or withdrawn.
+
+### Owner reports
+
+Read each report against Implement's [report](../../implement/SKILL.md#report)
+fields; ask no other format. Record, from each:
+
+| Report field | Record it under |
+| --- | --- |
+| Outcome and finish line | `finishLine` and `state` |
+| Layers | `layers`, after checking each head and base at the forge |
+| Proof | `evidence`, and every blocked or untested assertion in `acceptance` |
+| Shown | `results` with access limits; servers and stop actions in `owner.resources` |
+| Waits | `waits`, with the work each one holds |
+| Decisions | `decisions` with the phase each blocks, and `provisional` assumptions |
+| Follow-ups | A work row or `discoveries`, each with its disposition |
+| Sources | `owner.handled`, so no message is answered twice |
+
+Also record task IDs an owner returns when it resumes in `owner.tasks`.
+
+## Command center
+
+Before each write, read the relevant owner reports, user direction, tickets,
+and live forge state. Set a row's `checkedAt` when you observe its state. Keep an
+unchecked row's old time; use `unknown` with `unknownReason` when its state
+cannot be read.
+
+A row is an outcome, a follow-up (an outcome that came from another one, with
+its own finish line), or a support task. Count an outcome once whatever its
+layers. Outcome states:
+
+- `planned`: not started. `building`: building and proving.
+- `shown`: a Try result built, proved, and shown, waiting for the user's merge,
+ changes, or scrap choice. It needs `results`.
+- `in-review`: a PR is open while review or CI continues.
+- `ready`: every layer at readiness for its finish line: ready or merged for
+ Land or a review-ready PR, a draft or merged for a draft PR. Provisional work
+ is never ready, on any finish line: one that reaches its requested PR state
+ stays `in-review` with its Provisional badge. Shown under In review with a
+ readiness badge.
+- `landed`: every layer confirmed merged. `scrapped`: the user scrapped it, or it
+ was declined or made obsolete.
+- `done`: completed support work. An outcome never uses `done`.
+
+`blockedBy` lists logical inputs not yet satisfied under the
+[dependency rule](dependencies.md#dependencies); drop an edge once it is. A
+layer's `stackedOn` names the layer below it. When that layer belongs to another
+owner's outcome and is unmerged, the dependent is provisional on that outcome.
+An outcome that was published before it became provisional, through a later
+decision or adoption onto another owner's PR, keeps its PR layers; only a Try
+finish line forbids them.
+
+A base is the outcome that work rests on through `provisional.on` or a live
+layer's `stackedOn`. A base is **dead** when it was scrapped, or when it has not
+landed and every one of its layers is closed; one closed layer beside a live or
+merged one does not count. Active work on a dead base, whether provisional or
+the same owner's stack, needs an open Need that halts it at `start`, `access`,
+or `proof`, such as its scrap-or-rebase choice. Its answer moves the work off the
+dead base in the same commit, under [Base scrapped](dependencies.md#provisional-work).
+The card shows the base once, as a Scrapped base or Closed base blocker. A live
+layer stacked on a closed layer of a base that is still alive needs the same
+Need, or a restack.
+Before marking a row terminal, clear its active blockers and open Needs; history
+stays in the record. Unresolved work stays visible even after its owner failed
+or its PR was abandoned.
+
+Each Need's `blocks` names the outcomes it holds and the earliest phase each
+waits at: `start`, `access`, `proof`, `acceptance`, or `landing`. The first three
+halt that outcome's work. `acceptance` and `landing` let building and proof
+continue, so a shown Try waiting for its choice or a provisional build carries
+an open Need without being blocked. Put what continues meanwhile in
+`meanwhile`. If work lands while a Need about it is open, keep the landed row and
+point the Need's `blocks` at a follow-up row for any change the answer requires.
+
+The page shows open, answered, and resolved Needs, work, and tradeoffs, and
+leaves out withdrawn Needs, decision history, owner plumbing, and program facts
+other than the title, goal, and finish line. Its Updated time is the commit that
+last changed it.
Write each `description` from the outcome's scope, not its title or the owner's
next step. Store returned T3 thread links verbatim and use only real ticket, PR,
@@ -221,24 +266,13 @@ result, and artifact links.
### Render and show
-From this skill's folder, with Python 3 and no third-party packages:
-
-```bash
-python3 scripts/render_dashboard.py --state /absolute/path/program.json --output /absolute/path/command-center.html
-```
-
-The renderer validates the snapshot, escapes content, and writes standalone
-HTML stamped with a digest of the page, which open pages compare to spot a new
-publication. It makes no tracker, forge, or session changes. The
-[example snapshot](../assets/example-state.json) uses fictional work.
+The scripts need Python 3 and no third-party packages. They make no tracker,
+forge, or session changes.
Each copied choice or reopen request names its decision's revision, a digest of
-the whole Need. Two or more open quick picks add a batch prompt that takes every
-recommendation. Print a Need's current revision with:
+what the page shows for that Need. Two or more open quick picks add a batch
+prompt that takes every recommendation. Print a Need's revision at `HEAD` with:
```bash
python3 scripts/render_dashboard.py --state /absolute/path/program.json --revision
```
-
-Deliver the page through [dashboard hosting](hosting.md). Do not render it
-inline in T3 or another chat surface.
diff --git a/skills/chief-of-staff/scripts/render_dashboard.py b/skills/chief-of-staff/scripts/render_dashboard.py
index 52e8431..d3bd9f1 100644
--- a/skills/chief-of-staff/scripts/render_dashboard.py
+++ b/skills/chief-of-staff/scripts/render_dashboard.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Render a checked program snapshot as a command center."""
+"""Check a program state file and render its command center."""
import argparse
from datetime import datetime
@@ -8,6 +8,8 @@
import json
from pathlib import Path
import re
+import shlex
+import subprocess
from urllib.parse import urlsplit
@@ -28,6 +30,23 @@
}
PHASES = ("start", "access", "proof", "acceptance", "landing")
HALTING = {"start", "access", "proof"} # The others let building continue.
+DAY = re.compile(r"\d{4}-\d{2}-\d{2}")
+ENTRY = ("date", "source", "text")
+START_MODES = ("top-level", "delegated", "adopted")
+LANDING_STATES = ("released", "submitted", "landed", "withdrawn")
+DECISION_STATES = ("open", "answered", "resolved", "withdrawn")
+HISTORY = {"date", "source", "answerSource", "rationale"} # Decision fields the page never shows.
+PROGRAM_KEYS = ("title", "goal", "finishLine", "demo", "chief", "hosting", "next", "request", "corrections",
+ "requirements", "approvals", "directions", "landingAuthority", "decisions", "work", "revisits",
+ "discoveries", "landings", "waits")
+DECISION_KEYS = ("id", "question", "date", "source", "status", "context", "meanwhile", "recommendation", "options",
+ "weight", "blocks", "links", "answer", "answerSource", "basis", "rationale")
+WORK_KEYS = ("id", "title", "repo", "kind", "state", "description", "scope", "sources", "tickets", "finishLine",
+ "finishLineReason", "owner", "provisional", "layers", "results", "evidence", "acceptance", "blockedBy",
+ "blocked", "unknownReason", "checkedAt", "required", "origin")
+OWNER_KEYS = ("name", "session", "start", "route", "tasks", "workspace", "resources", "handled")
+LAYER_KEYS = ("id", "branch", "status", "url", "head", "base", "stackedOn")
+TEMPLATE = Path(__file__).resolve().parents[1] / "assets" / "command-center.html"
ISO_TIME = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)")
@@ -47,6 +66,17 @@ def timestamp(value, path):
raise ValueError(f"{path} must be an ISO timestamp with a timezone, such as 2026-11-02T17:00:00Z") from None
+def date(value, path):
+ if isinstance(value, str) and DAY.fullmatch(value):
+ try:
+ datetime.strptime(value, "%Y-%m-%d")
+ except ValueError:
+ raise ValueError(f"{path} must be a date, such as 2026-11-02") from None
+ else:
+ timestamp(value, path)
+ return value
+
+
def url(value, path):
text(value, path)
candidate = value[1:-1] if value.startswith("<") and value.endswith(">") else value
@@ -74,10 +104,36 @@ def strings(value, path):
def links(value, path):
for index, item in enumerate(records(value, path)):
+ known_keys(item, f"{path}[{index}]", ("label", "url"))
text(item.get("label"), f"{path}[{index}].label")
url(item.get("url"), f"{path}[{index}].url")
+def obj(value, path):
+ if not isinstance(value, dict):
+ raise ValueError(f"{path} must be an object")
+ return value
+
+
+def known_keys(row, path, keys):
+ for key in row:
+ if key not in keys:
+ raise ValueError(f"{path}.{key} is not a known field" if path else f"{key} is not a known field")
+ return row
+
+
+def fields(row, path, required=(), optional=()):
+ for key in (*required, *(key for key in optional if key in row)):
+ (date if key == "date" else text)(row.get(key), f"{path}.{key}" if path else key)
+ return row
+
+
+def unique_ids(rows, path):
+ ids = [text(row.get("id"), f"{path}[{index}].id") for index, row in enumerate(rows)]
+ if len(ids) != len(set(ids)):
+ raise ValueError(f"{path} contains duplicate IDs")
+
+
def one_of(value, choices, path):
text(value, path)
if value not in choices:
@@ -86,17 +142,23 @@ def one_of(value, choices, path):
def effective_finish(row):
- return row.get("effectiveFinishLine", row.get("finishLine"))
+ """Provisional work never merges, so its Land becomes Try."""
+ return "try" if "provisional" in row and row.get("finishLine") == "land" else row.get("finishLine")
+
+
+def owner_name(row):
+ return row.get("owner", {}).get("name")
def owned_by_another(row, other):
- return not (row.get("owner") and row.get("owner") == other.get("owner"))
+ return not (owner_name(row) and owner_name(row) == owner_name(other))
def validate_layers(row, path, layers):
for index, layer in enumerate(records(row.get("layers", []), f"{path}.layers")):
identity = text(layer.get("id"), f"{path}.layers[{index}].id")
where = f"{path}.layers[{identity}]"
+ known_keys(layer, where, LAYER_KEYS)
if identity in layers:
raise ValueError(f"layer {identity} appears more than once; layer IDs are unique across the program")
layers[identity] = (layer, row)
@@ -113,38 +175,47 @@ def validate_layers(row, path, layers):
raise ValueError(f"{where}: provisional work never merges; clear the condition before recording a merge")
+def validate_owner(owner, path):
+ known_keys(obj(owner, path), path, OWNER_KEYS)
+ fields(owner, path, ("name", "route"), ("workspace", "resources"))
+ one_of(owner.get("start"), START_MODES, f"{path}.start")
+ if "session" in owner:
+ url(owner["session"], f"{path}.session")
+ for key in ("tasks", "handled"):
+ strings(owner.get(key, []), f"{path}.{key}")
+
+
def validate_work(row, layers):
path = f"work[{row['id']}]"
+ known_keys(row, path, WORK_KEYS)
for key in ("title", "repo"):
text(row.get(key), f"{path}.{key}")
state = one_of(row.get("state"), STATES, f"{path}.state")
kind = one_of(row.get("kind"), ("outcome", "follow-up", "support"), f"{path}.kind")
- timestamp(row.get("updatedAt"), f"{path}.updatedAt")
- for key in ("description", "owner", "blocked", "origin", "unknownReason"):
- if key in row:
- text(row[key], f"{path}.{key}")
- for key in ("url", "stackedOn"):
- if key in row:
- raise ValueError(f"{path}.{key}: record each PR and its stack parent on the outcome's layers")
+ timestamp(row.get("checkedAt"), f"{path}.checkedAt")
+ fields(row, path, (), ("description", "scope", "finishLineReason", "acceptance", "blocked", "origin", "unknownReason"))
+ strings(row.get("sources", []), f"{path}.sources")
+ if "owner" in row:
+ validate_owner(row["owner"], f"{path}.owner")
if "unknownReason" in row and state != "unknown":
raise ValueError(f"{path}.unknownReason applies only to unknown state")
- if "sessionUrl" in row:
- url(row["sessionUrl"], f"{path}.sessionUrl")
for key in ("tickets", "evidence"):
links(row.get(key, []), f"{path}.{key}")
for index, result in enumerate(records(row.get("results", []), f"{path}.results")):
- links([result], f"{path}.results[{index}]")
+ known_keys(result, f"{path}.results[{index}]", ("label", "url", "access"))
+ text(result.get("label"), f"{path}.results[{index}].label")
+ url(result.get("url"), f"{path}.results[{index}].url")
if "access" in result:
text(result["access"], f"{path}.results[{index}].access")
if "required" in row and not isinstance(row["required"], bool):
raise ValueError(f"{path}.required must be a boolean")
if state in TERMINAL and (row.get("blockedBy") or row.get("blocked")):
- raise ValueError(f"{path}: terminal work cannot retain active blockers; keep history in the program record")
+ raise ValueError(f"{path}: terminal work cannot retain active blockers; keep their history in decisions or discoveries")
if state in ACTIVE and "owner" not in row:
raise ValueError(f"{path}: {state} work needs its owner")
if kind == "support":
- for key in ("finishLine", "effectiveFinishLine", "layers", "provisional"):
+ for key in ("finishLine", "layers", "provisional"):
if key in row:
raise ValueError(f"{path}.{key}: support work has no finish line or layers; give a repository change its own outcome")
if state not in {"planned", "building", "done", "scrapped", "unknown"}:
@@ -154,17 +225,10 @@ def validate_work(row, layers):
if state == "done":
raise ValueError(f"{path}: an outcome ends landed, at its shown Try, or at its PR state, not done")
if "provisional" in row:
- provisional = row["provisional"]
- if not isinstance(provisional, dict):
- raise ValueError(f"{path}.provisional must be an object")
+ provisional = known_keys(obj(row["provisional"], f"{path}.provisional"), f"{path}.provisional", ("basis", "clears", "on"))
for key in ("basis", "clears"):
text(provisional.get(key), f"{path}.provisional.{key}")
strings(provisional.get("on", []), f"{path}.provisional.on")
- if finish == "land" and row.get("effectiveFinishLine") != "try":
- raise ValueError(f"{path}: provisional work never merges; set effectiveFinishLine to try")
- if "effectiveFinishLine" in row and not (
- "provisional" in row and finish == "land" and row["effectiveFinishLine"] == "try"):
- raise ValueError(f"{path}.effectiveFinishLine is only Try, for provisional work whose finish line is Land")
validate_layers(row, path, layers)
effective = effective_finish(row)
published = [layer for layer in row.get("layers", []) if "url" in layer]
@@ -185,10 +249,10 @@ def validate_work(row, layers):
raise ValueError(f"{path}: landed needs a Land finish line and every layer merged")
-def halted_by_need(needs):
- """IDs of work an open Need holds at a phase that stops its work."""
- return {entry["id"] for need in needs if need["status"] == "open"
- for entry in need.get("blocks", []) if entry["phase"] in HALTING}
+def held(decisions):
+ """IDs of work an open decision holds at a phase that stops its work."""
+ return {entry["id"] for decision in decisions if decision["status"] == "open"
+ for entry in decision.get("blocks", []) if entry["phase"] in HALTING}
LIVE_LAYERS = {"local", "draft", "open", "ready"}
@@ -223,16 +287,22 @@ def validate_edges(work, by_id, layers, halted):
for identity in row.get("provisional", {}).get("on", []):
if identity == row["id"] or identity not in by_id:
raise ValueError(f"{path}.provisional.on must name other work; a decision names what it blocks")
+ branches = {}
+ for identity, (layer, row) in layers.items():
+ branches.setdefault((row["repo"], layer["branch"]), []).append(identity)
for identity, (layer, row) in layers.items():
- if "stackedOn" not in layer:
- continue
where = f"work[{row['id']}].layers[{identity}].stackedOn"
- if layer["stackedOn"] == identity or layer["stackedOn"] not in layers:
+ if "stackedOn" in layer and (layer["stackedOn"] == identity or layer["stackedOn"] not in layers):
raise ValueError(f"{where} must name another layer")
+ below = [other for other in branches.get((row["repo"], layer.get("base")), []) if other != identity]
+ if layer["status"] in LIVE_LAYERS and below and layer.get("stackedOn") not in below:
+ raise ValueError(f"work[{row['id']}].layers[{identity}].base is {below[0]}'s branch; name {below[0]} in stackedOn")
+ if "stackedOn" not in layer:
+ continue
parent, parent_row = layers[layer["stackedOn"]]
if (layer["status"] in LIVE_LAYERS and parent["status"] == "closed" and not dead_base(parent_row)
and row["id"] not in halted):
- raise ValueError(f"{where} names closed {parent['id']}; record the restack or halt it with its Need")
+ raise ValueError(f"{where} names closed {parent['id']}; record the restack or halt it with its decision")
if (parent_row is not row and parent["status"] != "merged" and row["state"] not in TERMINAL
and owned_by_another(row, parent_row)
and parent_row["id"] not in row.get("provisional", {}).get("on", [])):
@@ -241,7 +311,7 @@ def validate_edges(work, by_id, layers, halted):
for identity in bases(row, layers):
reason = dead_base(by_id[identity])
if reason and row["id"] not in halted:
- raise ValueError(f"work[{row['id']}] rests on {reason} {identity}; halt it with its scrap-or-rebase Need or record its new base")
+ raise ValueError(f"work[{row['id']}] rests on {reason} {identity}; halt it with its scrap-or-rebase decision or record its new base")
def depends_on(row):
found = list(row.get("blockedBy", []))
@@ -271,24 +341,26 @@ def visit(identity):
visit(identity)
-def validate_need(row, by_id):
- path = f"needs[{row['id']}]"
- text(row.get("ask"), f"{path}.ask")
- status = one_of(row.get("status"), ("open", "answered", "resolved"), f"{path}.status")
- if status != "open":
+def validate_decision(row, by_id):
+ path = f"decisions[{row['id']}]"
+ known_keys(row, path, DECISION_KEYS)
+ fields(row, path, ("question", "date", "source"), ("recommendation", "meanwhile", "answer", "rationale"))
+ status = one_of(row.get("status"), DECISION_STATES, f"{path}.status")
+ if status in {"answered", "resolved"}:
text(row.get("answer"), f"{path}.answer")
- if status == "resolved":
- text(row.get("basis"), f"{path}.basis")
- elif "basis" in row:
- raise ValueError(f"{path}.basis applies only to resolved decisions")
- for key in ("recommendation", "answer", "meanwhile"):
- if key in row:
- text(row[key], f"{path}.{key}")
+ for key, owner in (("answerSource", "answered"), ("basis", "resolved")):
+ if status == owner:
+ text(row.get(key), f"{path}.{key}")
+ elif key in row:
+ raise ValueError(f"{path}.{key} applies only to {owner} decisions")
+ if status == "withdrawn":
+ text(row.get("rationale"), f"{path}.rationale")
if row.get("weight", "judgment") not in {"quick", "judgment"}:
raise ValueError(f"{path}.weight must be quick or judgment")
strings(row.get("context", []), f"{path}.context")
blocked = set()
for index, entry in enumerate(records(row.get("blocks", []), f"{path}.blocks")):
+ known_keys(entry, f"{path}.blocks[{index}]", ("id", "phase"))
identity = text(entry.get("id"), f"{path}.blocks[{index}].id")
phase = one_of(entry.get("phase"), PHASES, f"{path}.blocks[{index}].phase")
if identity not in by_id:
@@ -300,7 +372,8 @@ def validate_need(row, by_id):
raise ValueError(f"{path}: an open decision cannot block terminal work")
if status == "open" and phase == "start" and by_id[identity]["state"] != "planned":
raise ValueError(f"{path}: {identity} has started; record the phase this decision blocks now")
- for option in records(row.get("options", []), f"{path}.options"):
+ for index, option in enumerate(records(row.get("options", []), f"{path}.options")):
+ known_keys(option, f"{path}.options[{index}]", ("label", "effect", "recommended"))
for key in ("label", "effect"):
text(option.get(key), f"{path}.options.{key}")
if "recommended" in option and not isinstance(option["recommended"], bool):
@@ -315,35 +388,65 @@ def validate_need(row, by_id):
links(row.get("links", []), f"{path}.links")
+def validate_landing(row, by_id):
+ path = f"landings[{row['id']}]"
+ known_keys(row, path, ("id", "date", "chief", "lander", "target", "work", "prs", "state", "keep", "note"))
+ fields(row, path, ("date", "chief", "lander", "target"), ("keep", "note"))
+ one_of(row.get("state"), LANDING_STATES, f"{path}.state")
+ if not row.get("work") or not row.get("prs"):
+ raise ValueError(f"{path} needs its work and PRs")
+ for identity in strings(row["work"], f"{path}.work"):
+ known(identity, f"{path}.work", by_id)
+ for index, pr in enumerate(records(row["prs"], f"{path}.prs")):
+ known_keys(pr, f"{path}.prs[{index}]", ("url", "head", "base"))
+ url(pr.get("url"), f"{path}.prs[{index}].url")
+ fields(pr, f"{path}.prs[{index}]", ("head", "base"))
+
+
+def known(identity, path, by_id):
+ if identity not in by_id:
+ raise ValueError(f"{path} names missing work {identity}")
+
+
def validate(state):
- if not isinstance(state, dict):
- raise ValueError("state must be an object")
- for key in ("title", "goal"):
- text(state.get(key), key)
- timestamp(state.get("updatedAt"), "updatedAt")
- if "finishLine" in state:
- text(state["finishLine"], "finishLine")
+ known_keys(obj(state, "program"), "", PROGRAM_KEYS)
+ fields(state, "", ("title", "goal", "chief", "next"), ("finishLine",))
if "demo" in state and not isinstance(state["demo"], bool):
raise ValueError("demo must be a boolean")
+ if "hosting" in state:
+ url(state["hosting"], "hosting")
+ request = fields(known_keys(obj(state.get("request"), "request"), "request", ("text", "sources")), "request", ("text",))
+ strings(request.get("sources", []), "request.sources")
+ if "landingAuthority" in state:
+ fields(known_keys(obj(state["landingAuthority"], "landingAuthority"), "landingAuthority", ENTRY), "landingAuthority", ENTRY)
+
work = records(state.get("work"), "work")
- needs = records(state.get("needs"), "needs")
- revisits = records(state.get("revisits", []), "revisits")
- for name, rows in (("work", work), ("needs", needs), ("revisits", revisits)):
- ids = [text(row.get("id"), f"{name}[{i}].id") for i, row in enumerate(rows)]
- if len(ids) != len(set(ids)):
- raise ValueError(f"{name} contains duplicate IDs")
+ decisions = records(state.get("decisions"), "decisions")
+ for name in ("work", "decisions", "revisits", "approvals", "landings"):
+ unique_ids(records(state.get(name, []), name), name)
by_id = {row["id"]: row for row in work}
layers = {}
for row in work:
validate_work(row, layers)
- for row in needs:
- validate_need(row, by_id)
- validate_edges(work, by_id, layers, halted_by_need(needs))
- for row in revisits:
- for key in ("title", "tradeoff", "revisit"):
- text(row.get(key), f"revisits[{row['id']}].{key}")
- if "source" in row:
- text(row["source"], f"revisits[{row['id']}].source")
+ for row in decisions:
+ validate_decision(row, by_id)
+ validate_edges(work, by_id, layers, held(decisions))
+ for row in state.get("revisits", []):
+ known_keys(row, f"revisits[{row['id']}]", ("id", "title", "tradeoff", "revisit", "source"))
+ fields(row, f"revisits[{row['id']}]", ("title", "tradeoff", "revisit"), ("source",))
+ for name, required, optional in (
+ ("corrections", ENTRY, ()), ("requirements", ENTRY, ()),
+ ("approvals", (*ENTRY, "id", "covers", "picks"), ()), ("directions", ENTRY, ("until",)),
+ ("waits", ("date", "on", "event"), ("work",)),
+ ("discoveries", ("date", "origin", "text", "route"), ()),
+ ):
+ for index, row in enumerate(records(state.get(name, []), name)):
+ known_keys(row, f"{name}[{index}]", (*required, *optional))
+ fields(row, f"{name}[{index}]", required, optional)
+ if "work" in row:
+ known(row["work"], f"{name}[{index}].work", by_id)
+ for row in state.get("landings", []):
+ validate_landing(row, by_id)
return state
@@ -354,7 +457,7 @@ def validate(state):
"scrapped": "ban", "unknown": "circle-question-mark", "provisional": "git-branch",
"blocked": "triangle-alert", "waits": "hourglass", "copy": "copy", "copied": "check",
"decision": "message-circle-question-mark", "quick": "zap", "resolved": "badge-check", "reopen": "rotate-ccw",
- "recommended": "star", "pr": "git-pull-request", "finish": "flag", "snapshot": "clock", "tradeoff": "scale",
+ "recommended": "star", "pr": "git-pull-request", "finish": "flag", "updated": "clock", "tradeoff": "scale",
"result": "external-link",
}
@@ -385,17 +488,24 @@ def hexed(identity):
return identity.encode().hex()
-def when(value, age=False):
- return f''
+def when(value):
+ return f''
+
+
+UPDATED = re.compile(r'')
+
+
+def updated_time(value):
+ return f''
def quoted(value):
return json.dumps(" ".join(value.split()), ensure_ascii=False)
-def revision(need):
+def revision(decision):
"""Digest of the decision content a copied choice answers."""
- return digest(json.dumps(need, sort_keys=True))[:10]
+ return digest(json.dumps({key: value for key, value in decision.items() if key not in HISTORY}, sort_keys=True))[:10]
def display_stage(row):
@@ -415,7 +525,7 @@ def collapsible(heading_id, title, count, body, *, level=2, state="", mark="", a
def decision_prompt(row, state, action):
return (
f'Decision {json.dumps(row["id"], ensure_ascii=False)} (revision {revision(row)}) in {quoted(state["title"])}\n'
- f'Question: {quoted(row["ask"])}\n'
+ f'Question: {quoted(row["question"])}\n'
f'{action}'
)
@@ -489,7 +599,7 @@ def tradeoff_label(row):
def need_card(row, state, selected=False):
classes = "card need" + (" quick" if row["status"] == "open" and row.get("weight") == "quick" else "") + (" selected" if selected else "")
- content = f'
{decision_label(row)}
{e(row["ask"])}
'
+ content = f'
{decision_label(row)}
{e(row["question"])}
'
if row.get("context"):
content += '
' + "".join(f"
{e(item)}
" for item in row["context"]) + "
"
if row["status"] == "open" and row.get("meanwhile"):
@@ -527,7 +637,7 @@ def inbox_group(heading_id, title, tone, rows):
def needs_view(state, judgment, quick, resolved, answered):
"""Decision cards in reading order, with the list that shows one card at a time in the dual-pane inbox."""
- empty = '
Nothing needs you in this snapshot.
'
+ empty = '
Nothing needs you right now.
'
revisits = state.get("revisits", [])
open_needs = judgment + quick
if not (open_needs or resolved or revisits or answered):
@@ -541,15 +651,15 @@ def needs_view(state, judgment, quick, resolved, answered):
groups = ""
for heading_id, title, rows in (("judgment-list", "Judgment calls", judgment), ("quick-list", "Quick picks", quick)):
if rows:
- groups += inbox_group(heading_id, title, "open", [inbox_row(card_id("decision", row), row["id"], row["ask"], selected=row is selected) for row in rows])
+ groups += inbox_group(heading_id, title, "open", [inbox_row(card_id("decision", row), row["id"], row["question"], selected=row is selected) for row in rows])
if resolved:
- groups += inbox_group("resolved-list", "Chief decided", "answered", [inbox_row(card_id("decision", row), row["id"], row["ask"]) for row in resolved])
+ groups += inbox_group("resolved-list", "Chief decided", "answered", [inbox_row(card_id("decision", row), row["id"], row["question"]) for row in resolved])
cards += collapsible("resolved-heading", "Chief decided", len(resolved), f'
{"".join(need_card(row, state) for row in resolved)}
')
if revisits:
groups += inbox_group("revisits-list", "Worth a second look", "revisit", [inbox_row(card_id("tradeoff", row), row["id"], row["title"]) for row in revisits])
cards += collapsible("revisits-heading", "Worth a second look", len(revisits), f'
{"".join(revisit_card(row) for row in revisits)}
')
if answered:
- groups += inbox_group("answered-list", "Answered decisions", "answered", [inbox_row(card_id("decision", row), row["id"], row["ask"]) for row in answered])
+ groups += inbox_group("answered-list", "Answered decisions", "answered", [inbox_row(card_id("decision", row), row["id"], row["question"]) for row in answered])
cards += collapsible("answered-heading", "Answered decisions", len(answered), f'
{"".join(need_card(row, state) for row in answered)}
')
return f'
{groups}
{cards}
'
@@ -558,7 +668,7 @@ class Program:
"""Lookups the work board needs across outcomes, layers, and decisions."""
def __init__(self, state):
- self.needs = state["needs"]
+ self.decisions = state["decisions"]
self.by_id = {row["id"]: row for row in state["work"]}
self.layers = {layer["id"]: (layer, row) for row in state["work"] for layer in row.get("layers", [])}
@@ -572,10 +682,10 @@ def foreign_parents(self, row):
def blockers(self, row):
"""(halts, kind, identity, text) for each open blocker; waits let building continue."""
items = []
- for need in self.needs:
- entry = next((entry for entry in need.get("blocks", []) if entry["id"] == row["id"]), None)
- if need["status"] == "open" and entry:
- items.append((entry["phase"] in HALTING, f'Decision · blocks {entry["phase"]}', need["id"], need["ask"]))
+ for decision in self.decisions:
+ entry = next((entry for entry in decision.get("blocks", []) if entry["id"] == row["id"]), None)
+ if decision["status"] == "open" and entry:
+ items.append((entry["phase"] in HALTING, f'Decision · blocks {entry["phase"]}', decision["id"], decision["question"]))
items += [(True, STATES[self.by_id[identity]["state"]], identity, self.by_id[identity]["title"]) for identity in row.get("blockedBy", [])]
if row.get("blocked"):
items.append((True, "Other", "", row["blocked"]))
@@ -612,8 +722,8 @@ def finish_line(row):
if "finishLine" not in row:
return ""
value = FINISH_LINES[row["finishLine"]]
- if "effectiveFinishLine" in row:
- value += f' → {FINISH_LINES[row["effectiveFinishLine"]]} while provisional'
+ if effective_finish(row) != row["finishLine"]:
+ value += f' → {FINISH_LINES[effective_finish(row)]} while provisional'
return f'
{icon("finish")}Finish line {e(value)}
'
@@ -696,12 +806,10 @@ def work_card(row, program):
content += link_list(row.get("evidence", []))
content += ""
content += ''
return content + ""
@@ -725,13 +833,15 @@ def overview(work):
)
-def render(state):
+def render(state, updated=None):
+ """Validate the program and render its page, stamped with the time its content last changed."""
validate(state)
- open_needs = [row for row in state["needs"] if row["status"] == "open"]
+ decisions = state["decisions"]
+ open_needs = [row for row in decisions if row["status"] == "open"]
judgment = [row for row in open_needs if row.get("weight") != "quick"]
quick = [row for row in open_needs if row.get("weight") == "quick"]
- resolved = [row for row in state["needs"] if row["status"] == "resolved"]
- answered = [row for row in state["needs"] if row["status"] == "answered"]
+ resolved = [row for row in decisions if row["status"] == "resolved"]
+ answered = [row for row in decisions if row["status"] == "answered"]
program = Program(state)
needs_html = needs_view(state, judgment, quick, resolved, answered)
@@ -744,12 +854,13 @@ def render(state):
alert = f', {icon("blocked")}{blocked} blocked' if blocked else ""
cards = "".join(work_card(row, program) for row in rows)
stages += collapsible(f"stage-{key}", STATES[key], len(rows), f'