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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/circuit-briefs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Circuit brief integrity

on:
pull_request:
paths:
- "data/map/circuits.json"
- "data/map/orgs.v3.geojson"
- "data/map/stats.v3.json"
- "tools/build-circuit-briefs.py"
- "briefs/**"
- "sitemap.xml"
push:
branches: [master]
paths:
- "data/map/circuits.json"
- "data/map/orgs.v3.geojson"
- "data/map/stats.v3.json"
- "tools/build-circuit-briefs.py"
- "briefs/**"
- "sitemap.xml"
workflow_dispatch:

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Verify generated briefs
run: python tools/build-circuit-briefs.py --check
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ node_modules/
**/node_modules/
npm-debug.log*

# Python
__pycache__/
*.py[cod]

# Local civic-identity/neighborhood-os runtime artifacts
civic-identity.db
neighborhood-os.db
Expand All @@ -54,4 +58,4 @@ fed-a.db*
fed-b.db*

# Per-node operator config (the example file is in the repo; real one is local)
node.config.json
node.config.json
16 changes: 16 additions & 0 deletions AGENT-TASKS.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,22 @@
"Both link to canonical site URLs (commonweave.earth or simonlpaige.com/commonweave/) where applicable",
"Both are kept short enough to be useful (no whole-file dumps)"
]
},
{
"id": "cw-circuit-brief-audit-001",
"title": "Audit the first four circuit briefs before outreach",
"type": "data",
"agent_ready": true,
"difficulty": "medium",
"goal": "Audit the Hartford, Syracuse, Tulsa, and Durham circuit briefs against current public sources before any brief is delivered through a local channel.",
"files": ["briefs/", "data/map/circuits.json", "OUTREACH-ATTRACTION-PLAN.md"],
"acceptance_criteria": [
"All listed organizations still have a current public presence",
"Each proposed role is supported by a cited public source or the brief is corrected",
"No listed organization is made more visible than its own public communications support",
"The review records whether the brief should be delivered, revised, or withdrawn",
"No outreach is sent as part of the audit"
]
}
]
}
136 changes: 136 additions & 0 deletions OUTREACH-ATTRACTION-PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Attraction Plan: how organizations find Commonweave and want to stay

*2026-07-03, written as part of the red-team rebuild. Companion to OUTREACH.md
(which governs how we contact people). This document is about the opposite
direction: making the directory useful enough that orgs, networks, and
organizers come to it, claim their place in it, and tell others.*

The May 2026 automated outreach experiment taught the lesson the hard way:
push doesn't work for a project whose entire value is trust. A cold email
from an unknown directory is spam; the same directory handing you something
you actually need is infrastructure. Everything below is a variation on one
move: **give a specific, verifiable, locally-relevant gift, and make the
next step obvious and voluntary.**

The asset that makes this possible now exists: `data/map/circuits.json`
identifies groups of nearby organizations that jointly fill the roles of a
documented solution circuit (land + build labor + finance = housing pipeline;
grower + distributor + food access = local food loop; and eight more). That
is information almost no local org has about itself.

---

## Mechanism 1: Circuit introduction briefs (the gift that's also the thesis)

**What.** A one-page brief per circuit candidate: "Within 12 km of each
other: {Land steward} + {Build labor} + {Community finance}. Together these
three could run a labor-for-housing pipeline. Here's the documented pattern
(LABOR-FOR-HOUSING-GUIDE.md), here's what each role contributes, here's what
usually goes wrong." Rendered from circuits.json with a stable URL per
circuit group (map deep-link + printable page).

**Why it attracts.** It's the only artifact in this space that tells three
specific orgs something actionable about *each other*. Nobody has to join
anything: the brief is complete without us. If it's wrong, the correction
flow teaches us; if it's right, we're the project that introduced them.

**How it ships with zero budget.**
- Generator: `tools/build-circuit-briefs.py` renders a static `briefs/` index,
printable page per group, machine-readable manifest, and sitemap entries.
As of 2026-07-22, the source declares 76 candidates but contains 13 groups;
the manifest records that mismatch, and public copy must not claim 76 until
the export is repaired.
- Distribution is NOT bulk email. Route each brief through the most
credible LOCAL channel: the city's co-op development center, a CLT
network's newsletter, one warm human. One brief, hand-delivered, per week
beats 500 sends.
- Every brief ends with: "Are we wrong about one of these three orgs?
Correct us" → prefilled GitHub issue (already wired in map.html).

**Success metric.** Replies/corrections per brief delivered (target: any
response on 1 in 4); one documented introduction that led to a real meeting
within 90 days.

## Mechanism 2: Serve federations, not individual orgs

**What.** Every federation/network in the directory (ICA, RIPESS, Grounded
Solutions, Transition Network, mutual aid hubs...) maintains a member
directory, and every one of those directories is partly stale. We generate
**member-directory health reports**: dead websites, moved orgs, missing
geocodes, members that appear in other networks, members that show up in
circuit candidates. CC0, no strings.

**Why it attracts.** One conversation with a network reaches hundreds of
orgs with borrowed trust. Networks have a real, unglamorous maintenance
problem we can partially automate. The report is useful even if they ignore
us forever — which is exactly why it works.

**How it ships.** `tools/federation-health-report.py` reading the DB +
link-checker (a weekend). Deliver v1 to the two networks already curated in
`data/federations.yaml` (ICA, and pick one of RIPESS/Grounded Solutions).
Ask nothing except "is this useful? what did we get wrong?"

**Success metric.** 1 network integrates or replies substantively per
quarter; their members start appearing in the corrections queue (that means
they're looking).

## Mechanism 3: Claim your listing (make the directory self-improving)

**What.** A visible, low-friction path for an org to verify and own its
entry: "This is us" → confirm/fix details → entry gets the reviewed marker
(Tier A path) and a `claimed` badge on the map + a small embeddable badge
("On the Commonweave map — 29,378 organizations weaving healthier
communities") for their site, which links back to their map deep-link.

**Why it attracts.** The badge is a tiny status good and a backlink engine;
each claimed listing is a unit of Tier A data we didn't have to research;
each embed is organic distribution to exactly the right audience.

**How it ships.** Phase 1 needs no backend: "Claim this listing" button in
the map detail panel → prefilled GitHub issue with a `claim` label + an
email fallback (hello@commonweave.earth). A maintainer verifies (reply from
an org-domain address or the org's listed site referencing the claim),
flips `review_status`, done. Badge = one static SVG endpoint per org id.

**Success metric.** Claims per month (target: 5/mo by day 90); % of claims
that arrive via another org's badge or brief (the compounding signal).

---

## Grassroots sequencing (30 / 60 / 90)

**Days 1–30 — prove the gift works.** Ship brief generator; hand-deliver 4
circuit briefs (one per week) through one warm channel each, chosen from the
strongest US candidates in circuits.json (Hartford, Syracuse, Tulsa, Durham
are already promising). Add the claim button. Effort: ~2 evenings/week.
Gate: if 0 of 4 briefs get any response, the brief format is wrong — revise
before scaling, don't send more.

**Days 31–60 — borrow a network's trust.** Federation health report v1 to
ICA + one other. Post ONE show-don't-tell artifact where practitioners
already talk (co-op listservs, a CLT network call, r/cooperatives): the
city-level circuit map for one metro, framed as a question — "we think
these three orgs could build housing together; what are we missing?" —
not an announcement.

**Days 61–90 — make it a habit.** Monthly "weave report": what got added,
what got corrected, which circuits got stronger, one introduction that
happened. Same post, three places (site, one listserv, one fediverse
account). Invite one guest correction-sprint with a partner network's
members.

**What we deliberately do NOT do:** bulk email campaigns, GitHub issue
outreach, engagement-bait threads, press releases, "launch" theater. The
directory is infrastructure; infrastructure earns trust by being repeatedly
useful in small ways.

---

## Honesty rails (applies to every mechanism)

- Every number quoted anywhere comes from `data/map/stats.v3.json` with its
build date. No exceptions (OUTREACH.md rule 6).
- Circuit briefs say "derived, unverified — treat as an introduction worth
making, not an existing partnership" in the body, not a footnote.
- Every artifact carries its correction path. The gift includes the ability
to tell us we're wrong; that's what makes it credible.
73 changes: 73 additions & 0 deletions briefs/briefs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* Generated support stylesheet for Commonweave circuit briefs. */
.brief-shell { max-width: 1120px; margin: 0 auto; padding: clamp(42px, 7vw, 92px) clamp(20px, 5vw, 56px) var(--s-9); }
.brief-hero { display: grid; grid-template-columns: minmax(0, 1fr) 210px; gap: var(--s-7); align-items: start; padding-bottom: var(--s-7); border-bottom: var(--rule); }
.brief-hero h1 { font-size: clamp(52px, 8vw, 104px); line-height: .92; font-style: italic; max-width: 13ch; margin: var(--s-4) 0 var(--s-5); text-wrap: balance; }
.brief-hero .lede { color: var(--ink-soft); max-width: 37ch; }
.status-stamp { border: 2px solid var(--clay); color: var(--clay); padding: 18px 14px; font-family: var(--f-mono); font-size: 11px; line-height: 1.45; letter-spacing: .13em; text-transform: uppercase; transform: rotate(2deg); text-align: center; background: color-mix(in oklab, var(--paper) 88%, transparent); }
.status-stamp strong { display: block; font-size: 15px; letter-spacing: .08em; margin-bottom: 6px; }
.brief-meta { display: flex; gap: var(--s-4); flex-wrap: wrap; margin-top: var(--s-5); font-family: var(--f-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-muted); }
.brief-section { padding: var(--s-8) 0; border-bottom: var(--rule); }
.brief-section h2 { font-size: clamp(38px, 5vw, 66px); max-width: 15ch; margin-bottom: var(--s-6); }
.section-intro { color: var(--ink-soft); font-size: 19px; max-width: 62ch; }
.circuit-loom { position: relative; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-5); margin-top: var(--s-7); }
.circuit-loom::before { content: ""; position: absolute; left: 10%; right: 10%; top: 31px; height: 2px; background: var(--thread); z-index: 0; }
.role-card { position: relative; z-index: 1; border: var(--rule); background: var(--paper); padding: 58px var(--s-5) var(--s-5); min-height: 245px; }
.role-card::before { content: ""; position: absolute; top: 23px; left: 50%; width: 16px; height: 16px; margin-left: -9px; border-radius: 50%; background: var(--clay); border: 2px solid var(--paper); box-shadow: 0 0 0 1px var(--thread); }
.role-label { font-family: var(--f-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--clay); }
.role-card h3 { font-size: clamp(24px, 2.4vw, 33px); margin: var(--s-3) 0; }
.role-card p { color: var(--ink-muted); font-size: 15px; }
.role-links { display: flex; gap: var(--s-3); flex-wrap: wrap; margin-top: var(--s-5); font-family: var(--f-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
.role-links a { text-decoration: none; border-bottom: 1px solid currentColor; }
.finding { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(260px, .6fr); gap: var(--s-7); align-items: start; }
.finding blockquote { margin: 0; font-family: var(--f-display); font-style: italic; font-size: clamp(27px, 3.2vw, 44px); line-height: 1.16; color: var(--moss-deep); }
.field-note { border-left: 3px solid var(--thread); padding-left: var(--s-5); color: var(--ink-soft); }
.unknowns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--ink); border: var(--rule); margin-top: var(--s-6); }
.unknown { background: var(--paper); padding: var(--s-5); }
.unknown b { display: block; font-family: var(--f-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--clay); margin-bottom: var(--s-3); }
.brief-actions { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; margin-top: var(--s-6); }
.brief-button { appearance: none; border: 1px solid var(--ink); background: transparent; color: var(--ink); padding: 10px 16px; font: 500 10px/1 var(--f-mono); letter-spacing: .12em; text-transform: uppercase; text-decoration: none; cursor: pointer; }
.brief-button.primary { background: var(--moss); border-color: var(--moss); color: var(--paper); }
.brief-button:hover { background: var(--paper-2); color: var(--moss-deep); }
.brief-button.primary:hover { background: var(--moss-deep); color: var(--paper); }
.source-note { color: var(--ink-muted); font-size: 14px; overflow-wrap: anywhere; }
.source-note code { font-family: var(--f-mono); font-size: 12px; }
.brief-index-hero { padding-bottom: var(--s-7); border-bottom: var(--rule); }
.brief-index-hero h1 { font-size: clamp(56px, 9vw, 112px); line-height: .91; font-style: italic; max-width: 12ch; margin: var(--s-4) 0 var(--s-5); }
.filter-bar { display: grid; grid-template-columns: minmax(220px, 1fr) 220px; gap: var(--s-3); margin: var(--s-7) 0; }
.filter-bar label { display: grid; gap: 6px; font: 500 10px/1.4 var(--f-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-muted); }
.filter-bar input, .filter-bar select { width: 100%; border: 1px solid var(--ink); border-radius: 0; background: var(--paper); color: var(--ink); padding: 12px 13px; font: 16px/1.3 var(--f-body); }
.brief-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-5); }
.brief-card { border: var(--rule); padding: var(--s-5); background: color-mix(in oklab, var(--paper) 82%, var(--paper-2)); display: flex; flex-direction: column; }
.brief-card .card-kicker { display: flex; justify-content: space-between; gap: var(--s-3); font-family: var(--f-mono); font-size: 9px; letter-spacing: .11em; text-transform: uppercase; color: var(--ink-muted); }
.brief-card h2 { font-size: clamp(30px, 3vw, 44px); margin: var(--s-4) 0; }
.brief-card ul { list-style: none; margin: 0 0 var(--s-5); padding: 0; color: var(--ink-soft); }
.brief-card li { padding: 7px 0; border-top: var(--rule-soft); }
.brief-card > a { margin-top: auto; align-self: flex-start; font-family: var(--f-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; }
.empty-result { display: none; padding: var(--s-7); border: var(--rule); color: var(--ink-soft); }
.empty-result.visible { display: block; }
body.dark .role-card, body.dark .unknown, body.dark .filter-bar input, body.dark .filter-bar select { background: var(--paper); }
body.dark .status-stamp { background: var(--paper); }
@media (max-width: 820px) {
.brief-hero { grid-template-columns: 1fr; }
.status-stamp { max-width: 210px; transform: none; }
.circuit-loom { grid-template-columns: 1fr; }
.circuit-loom::before { top: 6%; bottom: 6%; left: 31px; right: auto; width: 2px; height: auto; }
.role-card { padding: var(--s-5) var(--s-5) var(--s-5) 62px; min-height: 0; }
.role-card::before { left: 23px; top: 50%; margin: -9px 0 0; }
.finding, .unknowns, .brief-grid { grid-template-columns: 1fr; }
.filter-bar { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
.nav-links a:not(.nav-cta) { display: none; }
.brief-shell { padding-left: 18px; padding-right: 18px; }
}
@media print {
.site-nav, .site-foot, .brief-actions, .filter-bar { display: none !important; }
body { background: #fff !important; color: #111 !important; font-size: 12pt; }
.brief-shell { max-width: none; padding: 0; }
.brief-hero { grid-template-columns: 1fr 150px; }
.brief-hero h1 { font-size: 52pt; }
.brief-section { padding: 28pt 0; }
.role-card, .unknown { break-inside: avoid; background: #fff !important; }
a { color: #111 !important; }
}
38 changes: 38 additions & 0 deletions briefs/briefs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
(function () {
"use strict";
var search = document.getElementById("brief-search");
var country = document.getElementById("brief-country");
var cards = Array.prototype.slice.call(document.querySelectorAll("[data-brief-card]"));
var count = document.getElementById("brief-visible-count");
var empty = document.getElementById("brief-empty");

function applyFilters() {
var q = search ? search.value.trim().toLowerCase() : "";
var cc = country ? country.value : "";
var visible = 0;
cards.forEach(function (card) {
var matchesText = !q || (card.getAttribute("data-search") || "").indexOf(q) !== -1;
var matchesCountry = !cc || card.getAttribute("data-country") === cc;
var show = matchesText && matchesCountry;
card.hidden = !show;
if (show) visible += 1;
});
if (count) count.textContent = String(visible);
if (empty) empty.classList.toggle("visible", visible === 0);
}

if (search) search.addEventListener("input", applyFilters);
if (country) country.addEventListener("change", applyFilters);

document.querySelectorAll("[data-copy-url]").forEach(function (button) {
button.addEventListener("click", function () {
var url = button.getAttribute("data-copy-url") || location.href;
if (!navigator.clipboard) return;
navigator.clipboard.writeText(url).then(function () {
var previous = button.textContent;
button.textContent = "Link copied";
window.setTimeout(function () { button.textContent = previous; }, 1600);
});
});
});
}());
Loading
Loading