cohorte 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +10 -0
- package/core/commands/build.md +10 -3
- package/core/commands/doctor.md +11 -2
- package/core/commands/fix.md +1 -1
- package/core/commands/review.md +2 -2
- package/core/commands/smoke.md +2 -1
- package/core/commands/update-pipeline.md +4 -1
- package/core/templates/steps/init-pipeline/02-interview-gaps.md +10 -0
- package/install.ps1 +1 -0
- package/install.sh +2 -1
- package/package.json +2 -1
- package/profile/SCHEMA.md +41 -3
- package/profile/cohorte.config.template.yaml +14 -0
- package/scripts/telemetry-send.sh +52 -0
- package/telemetry/README.md +49 -0
- package/telemetry/collector.mjs +79 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
Entries are shown by `/update-pipeline` ("What's new") after a core refresh. Keep them short,
|
|
4
4
|
user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` section per release.
|
|
5
5
|
|
|
6
|
+
## 1.2.0 — 2026-07-29
|
|
7
|
+
|
|
8
|
+
> **Opt-in anonymous telemetry, GDPR-first.** Nothing is sent unless you explicitly say yes.
|
|
9
|
+
|
|
10
|
+
- `/init-pipeline` (and `/update-pipeline` on existing installs) ask ONE consent question, once per
|
|
11
|
+
machine, default **No** — both answers are recorded in `~/.claude/cohorte.config.yaml` §`telemetry`
|
|
12
|
+
so you're never re-asked.
|
|
13
|
+
- When enabled, each pipeline phase fires a ~200-byte ping (fire-and-forget, 2s timeout, never
|
|
14
|
+
blocks): core version, OS, phase, duration, per-surface result counts, and a **hash** of the
|
|
15
|
+
feature id. Never sent: repo names, paths, code, spec content, IPs.
|
|
16
|
+
- Withdraw anytime (`telemetry.enabled: false`); erase your history anytime (`/doctor` prints your
|
|
17
|
+
`install_id`; `DELETE /v1/install/<id>` on the collector drops it). Full spec: SCHEMA.md
|
|
18
|
+
§Telemetry; privacy summary in the README.
|
|
19
|
+
- Ships a zero-dependency reference collector (`telemetry/collector.mjs` — NDJSON storage, strict
|
|
20
|
+
field allowlist, erasure endpoint, stores no IPs) to self-host.
|
|
21
|
+
- `/doctor` reports telemetry consent state and flags incoherent configs (enabled without a
|
|
22
|
+
recorded consent).
|
|
23
|
+
- Note: the shipped default `endpoint` is empty — telemetry stays dormant even for consenting
|
|
24
|
+
installs until a collector URL ships in the config template.
|
|
25
|
+
|
|
26
|
+
## 1.1.1 — 2026-07-29
|
|
27
|
+
|
|
28
|
+
- **Fix: pipeline metrics survive worktree teardown.** With `isolation.enabled` the lead session
|
|
29
|
+
runs inside the feature worktree, so metrics lines landed in the worktree's `.claude/` and were
|
|
30
|
+
deleted with it — defeating their purpose (cross-feature evidence for surface splits, dashboard
|
|
31
|
+
history). All phases now append to the **main checkout's** `.claude/pipeline-metrics.jsonl`,
|
|
32
|
+
resolved from anywhere via `git rev-parse --git-common-dir`; `/doctor` flags a stray metrics file
|
|
33
|
+
inside a worktree as a stale-core sign.
|
|
34
|
+
|
|
6
35
|
## 1.1.0 — 2026-07-29
|
|
7
36
|
|
|
8
37
|
> **The token-economy release.** A full audit of the core (40 verified fixes) cuts the pipeline's
|
package/README.md
CHANGED
|
@@ -269,6 +269,16 @@ Rules that make it safe:
|
|
|
269
269
|
their eventual reviews diff against reality.
|
|
270
270
|
- `/doctor` check 6 shows the live slot table (feature ↔ worktree ↔ ports) when you lose track.
|
|
271
271
|
|
|
272
|
+
## Privacy — opt-in telemetry
|
|
273
|
+
|
|
274
|
+
Cohorte can send **anonymous** usage pings (core version, OS, phase name, duration, per-surface
|
|
275
|
+
result counts, and a *hash* of the feature id — never repo names, paths, code, or IPs). It is
|
|
276
|
+
**strictly opt-in**: `/init-pipeline` asks once per machine, the default is No, and both answers are
|
|
277
|
+
recorded so you're never re-asked. Withdraw anytime (`telemetry.enabled: false` in
|
|
278
|
+
`~/.claude/cohorte.config.yaml`); erase your history anytime (`/doctor` prints your `install_id`,
|
|
279
|
+
the collector's `DELETE /v1/install/<id>` drops it). Full spec + GDPR details:
|
|
280
|
+
`profile/SCHEMA.md` §Telemetry; reference collector in `telemetry/`.
|
|
281
|
+
|
|
272
282
|
## License
|
|
273
283
|
|
|
274
284
|
[AGPL-3.0](LICENSE). Free to use, including commercially — but if you modify it and distribute it
|
package/core/commands/build.md
CHANGED
|
@@ -93,11 +93,18 @@ tree. For each surface in `surfaces`:
|
|
|
93
93
|
|
|
94
94
|
When all return, flag any contract mismatch or failing test from the handoffs; otherwise print one
|
|
95
95
|
status line per surface (`<key> · tests pass/fail · <n> TODOs`) — do not restate handoff content.
|
|
96
|
-
Append **ONE line for the batch** to `.claude/pipeline-metrics.jsonl`
|
|
97
|
-
|
|
96
|
+
Append **ONE line for the batch** to the **main checkout's** `.claude/pipeline-metrics.jsonl` —
|
|
97
|
+
NOT the worktree's, which dies at teardown while metrics must accumulate across features. Resolve
|
|
98
|
+
it from anywhere: `$(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl`
|
|
99
|
+
(in the main checkout this resolves to itself). Create it if absent; it must be gitignored.
|
|
100
|
+
Compute the elapsed time in the same Bash call
|
|
98
101
|
(`echo "{...\"seconds\":$(($(date +%s)-<start epoch from §2>)),...}" >> …`):
|
|
99
102
|
`{"ts":"<ISO date>","feature":"$ARGUMENTS","phase":"build","seconds":<wall-clock>,"surfaces":{"<key>":"ok|error",…}}`
|
|
100
|
-
— this is the evidence SCHEMA.md §Specialization asks for before splitting a surface.
|
|
103
|
+
— this is the evidence SCHEMA.md §Specialization asks for before splitting a surface. In the same
|
|
104
|
+
Bash call, chain the opt-in usage ping:
|
|
105
|
+
`<core>/pipeline/scripts/telemetry-send.sh build "$ARGUMENTS" <seconds> "<ok,ok|error,…>" || true`
|
|
106
|
+
(`<core>` = `~/.claude` global / `.claude` bundled) — a silent no-op unless the human explicitly
|
|
107
|
+
consented (SCHEMA.md §Telemetry); never ask about consent here.
|
|
101
108
|
Then tell the human: run `/smoke $ARGUMENTS` to exercise the feature end-to-end (or test by hand),
|
|
102
109
|
then `/review $ARGUMENTS`. Do not run the app or migrations yourself here — `/smoke` is the
|
|
103
110
|
sanctioned path for that. **Recommend a `/clear` now** — the spec, contract and diff are all on
|
package/core/commands/doctor.md
CHANGED
|
@@ -53,9 +53,18 @@ fix only with the human's go-ahead (or hand them the command).
|
|
|
53
53
|
When ≥2 slots are live, print the parallel-feature table (feature · worktree · ports · db ·
|
|
54
54
|
branch behind main by N commits) — a worktree far behind main means its next review will diff
|
|
55
55
|
against stale code ⇒ suggest rebasing it.
|
|
56
|
-
7. **
|
|
56
|
+
7. **Telemetry** (consent hygiene — read `~/.claude/cohorte.config.yaml` §`telemetry`). Report the
|
|
57
|
+
status in one line: `disabled` / `enabled since <consent_date> · install_id <id> · endpoint <url>`
|
|
58
|
+
(the install_id is the human's GDPR erasure key — see SCHEMA.md §Telemetry). Flag ❌ any
|
|
59
|
+
incoherent state: `enabled: true` with no `install_id` or no `consent_date` (sending without
|
|
60
|
+
recorded consent — fix: set `enabled: false` until the consent question is re-run), or a
|
|
61
|
+
`telemetry:` block missing entirely on a current core (top up via `/update-pipeline`).
|
|
62
|
+
8. **Specs & metrics.** Every `specs/*.md` front-matter `status` is a valid stage; `shipped` specs
|
|
57
63
|
with a live worktree flagged (see 6). `.claude/pipeline-metrics.jsonl` and `specs/reports/` (the
|
|
58
|
-
`/review`·`/smoke` report buffer that lets a `/fix` survive a `/clear`) are gitignored.
|
|
64
|
+
`/review`·`/smoke` report buffer that lets a `/fix` survive a `/clear`) are gitignored. Metrics
|
|
65
|
+
belong to the **main checkout** — a `pipeline-metrics.jsonl` inside a live feature worktree is a
|
|
66
|
+
stale-core sign (its lines die at teardown) ⇒ suggest appending its lines to the main checkout's
|
|
67
|
+
file and deleting the stray.
|
|
59
68
|
|
|
60
69
|
## Report
|
|
61
70
|
|
package/core/commands/fix.md
CHANGED
|
@@ -60,7 +60,7 @@ When the agents return:
|
|
|
60
60
|
re-reads each loop. Keep any round with ≥1 still-open `- [ ]` item fully expanded (§2's skip logic
|
|
61
61
|
needs those checkboxes).
|
|
62
62
|
- Print one status line per surface (`<key> · items fixed <n>/<m> · tests pass/fail`) — do not restate
|
|
63
|
-
handoff content — and append ONE metrics line for the batch to
|
|
63
|
+
handoff content — and append ONE metrics line for the batch to `pipeline-metrics.jsonl`
|
|
64
64
|
(see `/build` §4, `phase: "fix"`).
|
|
65
65
|
- Tell the human: re-run `/smoke` if the failures were runtime ones, and `/review $ARGUMENTS` for the
|
|
66
66
|
re-verdict — the re-review is what *verifies* the ticked items actually hold (a regression simply
|
package/core/commands/review.md
CHANGED
|
@@ -56,8 +56,8 @@ metrics line needs it.
|
|
|
56
56
|
|
|
57
57
|
Merge the returned reports into **one** REVIEW REPORT (same template): findings concatenated and
|
|
58
58
|
re-ordered by severity, counts summed, duplicates collapsed, verdict = the worst returned
|
|
59
|
-
(`BLOCK` > `REVISE` > `SHIP`). Append ONE metrics line for the batch to
|
|
60
|
-
(
|
|
59
|
+
(`BLOCK` > `REVISE` > `SHIP`). Append ONE metrics line for the batch to `pipeline-metrics.jsonl`
|
|
60
|
+
(main-checkout path + rules in `/build` §4): `{"ts":"<ISO>","feature":"$ARGUMENTS","phase":"review","seconds":<wall-clock>,"surfaces":{"<key>":"<verdict>:<finding count>",…}}`.
|
|
61
61
|
**Stage the full report to `specs/reports/$ARGUMENTS.md`** (overwrite) — a gitignored buffer so a
|
|
62
62
|
`/fix` after a `/clear` can still read the findings; the `specs/reports/` subfolder is skipped by the
|
|
63
63
|
non-recursive `specs/*.md` glob, so it's never mistaken for a spec (no phantom card, no bogus stage).
|
package/core/commands/smoke.md
CHANGED
|
@@ -33,7 +33,8 @@ human; that's expected.
|
|
|
33
33
|
## 3. Relay the verdict
|
|
34
34
|
|
|
35
35
|
- Print the agent's return as-is (verdict + ❌ lines + report path) — it is already minimal.
|
|
36
|
-
- Append ONE metrics line to
|
|
36
|
+
- Append ONE metrics line to `pipeline-metrics.jsonl` (main-checkout path + rules in `/build` §4,
|
|
37
|
+
`phase: "smoke"`).
|
|
37
38
|
- **PASS** → tell the human to run `/review $ARGUMENTS`. **FAIL** → the failures are findings: feed
|
|
38
39
|
them to `/fix $ARGUMENTS`, re-run `/smoke` after. Either way the report is on disk —
|
|
39
40
|
**recommend a `/clear`** before the next command.
|
|
@@ -75,7 +75,10 @@ Two of the §Reconcile steps matter specifically here:
|
|
|
75
75
|
|
|
76
76
|
- **Global config seed** (§Reconcile step 5): if `~/.claude/cohorte.config.yaml` is absent, seed it
|
|
77
77
|
from the template so the kanban + shared-vault config has a home. Never clobber an existing filled
|
|
78
|
-
file. Report what was seeded.
|
|
78
|
+
file. Report what was seeded. If the existing file has NO `telemetry:` block with a `consent_date`
|
|
79
|
+
(pre-telemetry install), top up the block from the template and ask the ONE opt-in consent
|
|
80
|
+
question defined in `templates/steps/init-pipeline/02-interview-gaps.md` §Telemetry — record the
|
|
81
|
+
answer either way so it is never re-asked. Consent is strictly opt-in; "No" is the default.
|
|
79
82
|
- **Kanban sync** (§Reconcile step 6): resolve this project's board from `kanban.boards[<PIPELINE
|
|
80
83
|
name>]`. **Not linked** → offer to link/create a board (confirm the vault + `<folder>/Tasks.md`,
|
|
81
84
|
write the `boards` entry, create the board file per §Kanban). **Linked** → verify the board file
|
|
@@ -39,3 +39,13 @@ Prefer sensible defaults from Phase 1 as the first (Recommended) option in each
|
|
|
39
39
|
|
|
40
40
|
> The **kanban** link is user-scoped (it points at a personal vault, so it never goes in the committed
|
|
41
41
|
> `PIPELINE.md`), but IS wired here because it is per-project — see Phase 4.
|
|
42
|
+
|
|
43
|
+
- **Telemetry** (optional, machine-scoped — SKIP entirely if `~/.claude/cohorte.config.yaml` already
|
|
44
|
+
has a `telemetry:` block with a `consent_date`, i.e. the human already answered on this machine).
|
|
45
|
+
Ask ONE opt-in question, stating exactly: _"Share anonymous usage stats with the cohorte project?
|
|
46
|
+
Sent per pipeline phase: core version, OS, phase name, duration, per-surface result counts, and a
|
|
47
|
+
hash of the feature id — never repo names, paths, code, or IPs. Off by default; withdraw anytime
|
|
48
|
+
(`telemetry.enabled: false`); erase your history anytime (SCHEMA.md §Telemetry). Default: No."_
|
|
49
|
+
On **yes**: in the global config set `telemetry.enabled: true`, mint `install_id` (`uuidgen`,
|
|
50
|
+
lowercase), set `consent_date` (ISO date). On **no**: set `enabled: false` + `consent_date` (so
|
|
51
|
+
no future command re-asks). Never touch `endpoint` — it ships with the template.
|
package/install.ps1
CHANGED
|
@@ -144,6 +144,7 @@ try {
|
|
|
144
144
|
Copy-Item (Join-Path $src 'profile\cohorte.config.template.yaml') (Join-Path $dest 'pipeline') -Force
|
|
145
145
|
Copy-Item (Join-Path $src 'scripts\*.template') (Join-Path $dest 'pipeline\scripts') -Force
|
|
146
146
|
Copy-Item (Join-Path $src 'scripts\kanban-move.sh') (Join-Path $dest 'pipeline\scripts') -Force
|
|
147
|
+
Copy-Item (Join-Path $src 'scripts\telemetry-send.sh') (Join-Path $dest 'pipeline\scripts') -Force
|
|
147
148
|
Copy-Item (Join-Path $src 'core\agents\implementer.template.md') (Join-Path $dest 'pipeline') -Force
|
|
148
149
|
if (Test-Path (Join-Path $src 'CHANGELOG.md')) { Copy-Item (Join-Path $src 'CHANGELOG.md') (Join-Path $dest 'pipeline') -Force }
|
|
149
150
|
[System.IO.File]::WriteAllText((Join-Path $dest 'pipeline\VERSION'), "$ver`n", [System.Text.UTF8Encoding]::new($false))
|
package/install.sh
CHANGED
|
@@ -86,7 +86,8 @@ copy_core() {
|
|
|
86
86
|
cp "$src/profile/cohorte.config.template.yaml" "$dest/pipeline/"
|
|
87
87
|
cp "$src"/scripts/*.template "$dest/pipeline/scripts/"
|
|
88
88
|
cp "$src/scripts/kanban-move.sh" "$dest/pipeline/scripts/"
|
|
89
|
-
|
|
89
|
+
cp "$src/scripts/telemetry-send.sh" "$dest/pipeline/scripts/"
|
|
90
|
+
chmod +x "$dest/pipeline/scripts/kanban-move.sh" "$dest/pipeline/scripts/telemetry-send.sh" 2>/dev/null || true
|
|
90
91
|
cp "$src/core/agents/implementer.template.md" "$dest/pipeline/"
|
|
91
92
|
[ -f "$src/CHANGELOG.md" ] && cp "$src/CHANGELOG.md" "$dest/pipeline/"
|
|
92
93
|
printf '%s\n' "$ver" > "$dest/pipeline/VERSION"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code — install the core, run /init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dashboard/server",
|
|
17
17
|
"dashboard/dist",
|
|
18
18
|
"dashboard/README.md",
|
|
19
|
+
"telemetry",
|
|
19
20
|
"install.sh",
|
|
20
21
|
"install.ps1",
|
|
21
22
|
"CHANGELOG.md"
|
package/profile/SCHEMA.md
CHANGED
|
@@ -155,10 +155,12 @@ the frozen contract as the only cross-surface channel**. So specialization means
|
|
|
155
155
|
|
|
156
156
|
Coarse first, specialize on evidence: start with one `frontend` / `backend` surface each; split only a
|
|
157
157
|
surface that's proven slow and cleanly separable. The evidence lives in
|
|
158
|
-
`.claude/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
|
|
158
|
+
the **main checkout's** `.claude/pipeline-metrics.jsonl` (gitignored) — one JSONL line per phase batch
|
|
159
159
|
(`ts`/`feature`/`phase`/`seconds`/`surfaces:{key: result}`), appended by `/build`, `/review`, `/fix`
|
|
160
|
-
and `/smoke`.
|
|
161
|
-
|
|
160
|
+
and `/smoke`. Always the main checkout, never the feature worktree (which dies at teardown while
|
|
161
|
+
metrics must accumulate across features) — resolve from anywhere with
|
|
162
|
+
`$(dirname "$(git rev-parse --git-common-dir)")/.claude/pipeline-metrics.jsonl`. Read it before
|
|
163
|
+
proposing a split: split the surface that actually dominates wall-clock, not the one that feels big.
|
|
162
164
|
|
|
163
165
|
## Measuring cost — what's slow vs what's expensive
|
|
164
166
|
|
|
@@ -316,3 +318,39 @@ added vs. moved vs. already-correct.
|
|
|
316
318
|
`<obsidian.vault_path>/<folder>/Tasks.md` with the `kanban-plugin: board` front-matter, one `## <heading>`
|
|
317
319
|
per configured column in pipeline order, and the closing `%% kanban:settings %%` block
|
|
318
320
|
(`{"kanban-plugin":"board","list-collapse":[false,…]}` with one `false` per column).
|
|
321
|
+
|
|
322
|
+
## Telemetry — anonymous usage stats, strictly opt-in (GDPR-first)
|
|
323
|
+
|
|
324
|
+
Cohorte can send the maintainers anonymous usage pings so the pipeline improves where it's actually
|
|
325
|
+
slow. **Nothing is ever sent without explicit consent**: `/init-pipeline` (and `/update-pipeline` on
|
|
326
|
+
pre-telemetry installs) ask ONE question, once per machine, default **No**, and record the answer in
|
|
327
|
+
`~/.claude/cohorte.config.yaml` §`telemetry` (`enabled`, `install_id`, `consent_date`). The sender —
|
|
328
|
+
`pipeline/scripts/telemetry-send.sh`, chained by each phase after its metrics line — is a silent
|
|
329
|
+
no-op unless `enabled: true` AND `install_id` AND `endpoint` are all set, times out at 2s, and never
|
|
330
|
+
fails the pipeline.
|
|
331
|
+
|
|
332
|
+
**What one event contains** (strict allowlist, ~200 bytes):
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{"v":1,"install_id":"<random uuid>","ts":"<ISO>","core_version":"1.2.0","os":"Darwin",
|
|
336
|
+
"event":"phase","phase":"build","feature_hash":"<sha256[..12] of the feature id>",
|
|
337
|
+
"seconds":412,"results":"ok,ok"}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**What is NEVER sent:** repo/project names, file paths, code, spec content, prompts, emails,
|
|
341
|
+
usernames, IP handling client-side. The feature id is hashed (12 hex chars) so cross-feature counts
|
|
342
|
+
work without revealing what is being built.
|
|
343
|
+
|
|
344
|
+
**GDPR rights, concretely:**
|
|
345
|
+
|
|
346
|
+
- **Consent** — opt-in only, recorded with a date; "No" is also recorded so nothing re-asks.
|
|
347
|
+
- **Withdrawal** — set `telemetry.enabled: false` in `~/.claude/cohorte.config.yaml`; effective on
|
|
348
|
+
the next phase, no restart.
|
|
349
|
+
- **Erasure** — `/doctor` prints your `install_id`; send
|
|
350
|
+
`curl -X DELETE <endpoint-origin>/v1/install/<install_id>` and the collector drops every event
|
|
351
|
+
for that id (the reference collector in `telemetry/` implements this and stores no IPs).
|
|
352
|
+
- **Access/portability** — events are keyed by your `install_id`; ask the operator for an export.
|
|
353
|
+
|
|
354
|
+
**Collector contract** (implement your own, or deploy `telemetry/collector.mjs`):
|
|
355
|
+
`POST /v1/events` (one JSON event, allowlisted fields) · `DELETE /v1/install/<id>` (erasure) ·
|
|
356
|
+
`GET /healthz`. Operators must not retain IP-bearing access logs for the ingest vhost.
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
|
|
11
11
|
enabled: true # cfg:enabled — master switch; false disables every capability below
|
|
12
12
|
|
|
13
|
+
# ── telemetry (anonymous usage stats — OPT-IN, GDPR-first) ──────────────────
|
|
14
|
+
# Strictly opt-in: nothing is ever sent unless YOU said yes to the consent question
|
|
15
|
+
# (/init-pipeline or /update-pipeline ask once per machine; they record your answer here).
|
|
16
|
+
# What is sent when enabled: core version, OS, phase name, wall-clock seconds, per-surface
|
|
17
|
+
# result counts, and a SHA-256 HASH of the feature id — never repo names, paths, code, spec
|
|
18
|
+
# content, emails, or IPs. Sent fire-and-forget (2s timeout, silent on failure) by
|
|
19
|
+
# pipeline/scripts/telemetry-send.sh. Withdraw anytime: set enabled: false. Erase your history:
|
|
20
|
+
# see SCHEMA.md §Telemetry (DELETE by install_id).
|
|
21
|
+
telemetry:
|
|
22
|
+
enabled: false # cfg:telemetry_enabled — true ONLY via explicit consent
|
|
23
|
+
install_id: "" # cfg:telemetry_install_id — random UUID minted at consent; the erasure key
|
|
24
|
+
consent_date: "" # cfg:telemetry_consent_date — ISO date the human said yes
|
|
25
|
+
endpoint: "" # cfg:telemetry_endpoint — collector URL (maintainer default; empty = off even if enabled)
|
|
26
|
+
|
|
13
27
|
# ── obsidian ──────────────────────────────────────────────────────────────────
|
|
14
28
|
# Local vault used by the kanban mirror — defined here ONCE so no capability ever duplicates
|
|
15
29
|
# the path. Absolute path to the folder you open in Obsidian.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# telemetry-send.sh — fire-and-forget anonymous usage ping (SCHEMA.md §Telemetry).
|
|
3
|
+
#
|
|
4
|
+
# telemetry-send.sh <phase> <feature_id> <seconds> [results]
|
|
5
|
+
# phase build|review|fix|smoke
|
|
6
|
+
# feature the feature id — NEVER sent raw; SHA-256-hashed to 12 hex chars
|
|
7
|
+
# seconds batch wall-clock
|
|
8
|
+
# results optional compact summary, e.g. "ok,ok" or "REVISE:3"
|
|
9
|
+
#
|
|
10
|
+
# GDPR posture (documented in SCHEMA.md §Telemetry):
|
|
11
|
+
# - STRICTLY opt-in: exits silently unless ~/.claude/cohorte.config.yaml has
|
|
12
|
+
# telemetry.enabled: true AND a non-empty endpoint AND an install_id
|
|
13
|
+
# (all three written only by the explicit consent flow).
|
|
14
|
+
# - Data minimization: no repo names, no paths, no code, no IP handling client-side.
|
|
15
|
+
# - Never blocks or fails the pipeline: 2s timeout, all errors swallowed, exit 0 always.
|
|
16
|
+
set -u
|
|
17
|
+
|
|
18
|
+
cfg="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/cohorte.config.yaml"
|
|
19
|
+
[ -f "$cfg" ] || exit 0
|
|
20
|
+
|
|
21
|
+
# read keys scoped to the `telemetry:` block only
|
|
22
|
+
tval() {
|
|
23
|
+
awk -v key="$1" '
|
|
24
|
+
/^telemetry:/ { in_t=1; next }
|
|
25
|
+
/^[a-zA-Z]/ { in_t=0 }
|
|
26
|
+
in_t && $1 == key":" {
|
|
27
|
+
v=$2; gsub(/^"|"$|#.*/,"",v); gsub(/"/,"",v); print v; exit
|
|
28
|
+
}' "$cfg"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[ "$(tval enabled)" = "true" ] || exit 0
|
|
32
|
+
endpoint="$(tval endpoint)"; [ -n "$endpoint" ] || exit 0
|
|
33
|
+
install_id="$(tval install_id)"; [ -n "$install_id" ] || exit 0
|
|
34
|
+
|
|
35
|
+
phase="${1:-}"; feature="${2:-}"; seconds="${3:-0}"; results="${4:-}"
|
|
36
|
+
[ -n "$phase" ] || exit 0
|
|
37
|
+
|
|
38
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
39
|
+
fhash=$(printf '%s' "$feature" | shasum -a 256 | cut -c1-12)
|
|
40
|
+
else
|
|
41
|
+
fhash=$(printf '%s' "$feature" | sha256sum | cut -c1-12)
|
|
42
|
+
fi
|
|
43
|
+
ver=$(cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/pipeline/VERSION" 2>/dev/null | head -1)
|
|
44
|
+
os=$(uname -s 2>/dev/null || echo unknown)
|
|
45
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
46
|
+
|
|
47
|
+
payload=$(printf '{"v":1,"install_id":"%s","ts":"%s","core_version":"%s","os":"%s","event":"phase","phase":"%s","feature_hash":"%s","seconds":%s,"results":"%s"}' \
|
|
48
|
+
"$install_id" "$ts" "$ver" "$os" "$phase" "$fhash" "${seconds:-0}" "$results")
|
|
49
|
+
|
|
50
|
+
curl -s -m 2 -X POST -H 'content-type: application/json' \
|
|
51
|
+
-d "$payload" "$endpoint" >/dev/null 2>&1 || true
|
|
52
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# cohorte telemetry collector (reference)
|
|
2
|
+
|
|
3
|
+
Zero-dependency ingest endpoint for cohorte's **opt-in** anonymous usage pings
|
|
4
|
+
(see the repo README §Privacy and `profile/SCHEMA.md` §Telemetry for what is —
|
|
5
|
+
and is not — collected).
|
|
6
|
+
|
|
7
|
+
## Deploy (any VPS, Node ≥ 18)
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
TELEMETRY_DATA=/var/lib/cohorte-telemetry/events.ndjson \
|
|
11
|
+
TELEMETRY_PORT=8787 node collector.mjs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Put it behind HTTPS (Caddy example):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
telemetry.yourdomain.tld {
|
|
18
|
+
reverse_proxy 127.0.0.1:8787
|
|
19
|
+
log { output discard } # GDPR: do NOT keep access logs with IPs for this vhost
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then set the public URL as the `endpoint:` default in
|
|
24
|
+
`profile/cohorte.config.template.yaml` (anchor `cfg:telemetry_endpoint`) and release —
|
|
25
|
+
consenting installs start sending to it.
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
| Method | Path | Purpose |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `POST` | `/v1/events` | Ingest one event (strict field allowlist, 4 KB max) |
|
|
32
|
+
| `DELETE` | `/v1/install/<install_id>` | **Right to erasure** — drops every event for that id |
|
|
33
|
+
| `GET` | `/healthz` | Liveness |
|
|
34
|
+
|
|
35
|
+
Storage is an append-only NDJSON file — point your dashboard (or a cron that loads
|
|
36
|
+
into SQLite/Postgres) at it. One JSON object per line:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{"v":1,"install_id":"…","ts":"…","core_version":"1.2.0","os":"Darwin","event":"phase","phase":"build","feature_hash":"a1b2c3d4e5f6","seconds":412,"results":"ok,ok","received_at":"…"}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## GDPR checklist (operator side)
|
|
43
|
+
|
|
44
|
+
- [x] Opt-in only — the client never sends without recorded consent
|
|
45
|
+
- [x] No IPs stored (collector ignores them; disable proxy access logs)
|
|
46
|
+
- [x] Data minimization — hashed feature ids, no repo names/paths/code
|
|
47
|
+
- [x] Erasure — `DELETE /v1/install/<id>`; the user finds their id via `/doctor`
|
|
48
|
+
- [x] Withdrawal — `telemetry.enabled: false` in `~/.claude/cohorte.config.yaml`
|
|
49
|
+
- [ ] Your privacy notice — link the repo README §Privacy from wherever you present cohorte
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reference telemetry collector for cohorte — zero-dependency, GDPR-first.
|
|
3
|
+
// Deploy on any VPS behind HTTPS (Caddy/nginx). Storage: NDJSON append-only file,
|
|
4
|
+
// one event per line — read it from your own dashboard however you like.
|
|
5
|
+
//
|
|
6
|
+
// TELEMETRY_DATA=/var/lib/cohorte-telemetry/events.ndjson \
|
|
7
|
+
// TELEMETRY_PORT=8787 node collector.mjs
|
|
8
|
+
//
|
|
9
|
+
// GDPR notes:
|
|
10
|
+
// - IPs are NEVER stored (this process doesn't read them; make sure your reverse
|
|
11
|
+
// proxy's access log is disabled or anonymized for this vhost).
|
|
12
|
+
// - DELETE /v1/install/<install_id> erases every event for that id (right to erasure —
|
|
13
|
+
// the install_id printed by /doctor is the user's erasure key).
|
|
14
|
+
// - Events are validated against a strict allowlist of fields; anything else is dropped.
|
|
15
|
+
import { createServer } from "node:http";
|
|
16
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
|
|
19
|
+
const DATA = process.env.TELEMETRY_DATA || "./events.ndjson";
|
|
20
|
+
const PORT = Number(process.env.TELEMETRY_PORT || 8787);
|
|
21
|
+
const MAX_BODY = 4096;
|
|
22
|
+
|
|
23
|
+
mkdirSync(dirname(DATA), { recursive: true });
|
|
24
|
+
|
|
25
|
+
const FIELDS = ["v", "install_id", "ts", "core_version", "os", "event",
|
|
26
|
+
"phase", "feature_hash", "seconds", "results"];
|
|
27
|
+
const ID_RE = /^[A-Za-z0-9-]{8,64}$/;
|
|
28
|
+
|
|
29
|
+
function sanitize(raw) {
|
|
30
|
+
const e = {};
|
|
31
|
+
for (const k of FIELDS) if (k in raw) e[k] = raw[k];
|
|
32
|
+
if (e.v !== 1) return null;
|
|
33
|
+
if (typeof e.install_id !== "string" || !ID_RE.test(e.install_id)) return null;
|
|
34
|
+
if (typeof e.event !== "string" || e.event.length > 32) return null;
|
|
35
|
+
for (const k of ["ts", "core_version", "os", "phase", "feature_hash", "results"])
|
|
36
|
+
if (k in e && (typeof e[k] !== "string" || e[k].length > 64)) return null;
|
|
37
|
+
if ("seconds" in e && typeof e.seconds !== "number") return null;
|
|
38
|
+
e.received_at = new Date().toISOString();
|
|
39
|
+
return e;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const server = createServer((req, res) => {
|
|
43
|
+
const done = (code, body) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(body)); };
|
|
44
|
+
|
|
45
|
+
if (req.method === "GET" && req.url === "/healthz") return done(200, { ok: true });
|
|
46
|
+
|
|
47
|
+
if (req.method === "POST" && req.url === "/v1/events") {
|
|
48
|
+
let body = "";
|
|
49
|
+
req.on("data", (c) => { body += c; if (body.length > MAX_BODY) req.destroy(); });
|
|
50
|
+
req.on("end", () => {
|
|
51
|
+
try {
|
|
52
|
+
const e = sanitize(JSON.parse(body));
|
|
53
|
+
if (!e) return done(400, { error: "invalid event" });
|
|
54
|
+
appendFileSync(DATA, JSON.stringify(e) + "\n");
|
|
55
|
+
done(204, {});
|
|
56
|
+
} catch { done(400, { error: "bad json" }); }
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Right to erasure: drop every event carrying this install_id.
|
|
62
|
+
const del = req.url?.match(/^\/v1\/install\/([A-Za-z0-9-]{8,64})$/);
|
|
63
|
+
if (req.method === "DELETE" && del) {
|
|
64
|
+
const id = del[1];
|
|
65
|
+
let kept = [], dropped = 0;
|
|
66
|
+
if (existsSync(DATA))
|
|
67
|
+
for (const line of readFileSync(DATA, "utf8").split("\n")) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
if (line.includes(`"install_id":"${id}"`)) { dropped++; continue; }
|
|
70
|
+
kept.push(line);
|
|
71
|
+
}
|
|
72
|
+
writeFileSync(DATA, kept.length ? kept.join("\n") + "\n" : "");
|
|
73
|
+
return done(200, { erased: dropped });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
done(404, { error: "not found" });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
server.listen(PORT, () => console.log(`cohorte telemetry collector on :${PORT} → ${DATA}`));
|