@profile-as-code/cli 0.3.6 → 0.4.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.
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: github-repo-meta
3
+ description: >
4
+ Superficie de presencia `github-repo-meta` (D23, #262): la metadata del
5
+ artefacto de código — descripción del repo, topics, social preview, release
6
+ notes y la página npm derivada. Convierte un release en presencia visible
7
+ sobre el repo, sin inventar hechos: la descripción y las notas salen del
8
+ release plan (`pasco release plan`) y del SSOT, nunca de la memoria del agente.
9
+ model: mecánico (la copy es corta y acotada; el juicio de posicionamiento vive en
10
+ la skill de la README del perfil)
11
+ applies_to: github-repo-meta
12
+ projection: agent-agnostic (AionUi, OpenHands, Claude Code, Cursor, pi…)
13
+ version: 0.1.0
14
+ status: draft (D23, #262)
15
+ sources: "platforms/registry.yaml (kind: repo-meta) · docs/SPEC.md §22"
16
+ ---
17
+
18
+ # GitHub repo metadata — skill de superficie repo-meta (D23)
19
+
20
+ ## Misión
21
+
22
+ Que un release exista como **presencia**: el repo se encuentra, se entiende y se
23
+ comparte. El motor no renderiza esta superficie (no hay renderer: es HTML/API de
24
+ GitHub); lo que el motor SÍ da es el **plan**: `pasco release plan <id>` nombra la
25
+ superficie, su estado derivado del ledger y la **espina factual** del release. Esta
26
+ skill es la capa de juicio que convierte esa espina en metadata publicable.
27
+
28
+ ## Regla de oro
29
+
30
+ > La metadata del repo describe el **artefacto**, no a la persona. El perfil
31
+ > (github-readme) es la voz; `github-repo-meta` es la ficha: qué es esto, para
32
+ > quién, y qué cambió.
33
+
34
+ - La descripción NO repite el positioning personal: dice qué hace el repo.
35
+ - Los facts (versión, fecha, link) salen del release row y del SSOT; si un dato
36
+ no está, no se escribe.
37
+ - Un release = una pasada. No optimices metadata sin release: sin evento, la
38
+ superficie queda `never` en el plan (y eso es correcto, D23).
39
+
40
+ ## Flujo
41
+
42
+ 1. **Plan**: `pasco release plan <release-id> --json` → la fila
43
+ `github-repo-meta` (estado) + `baseText[kind=repo-meta]` (la espina factual).
44
+ Sin `releases[]` en el workspace, el comando falla con la razón: agregá el
45
+ release primero (schema v1.15).
46
+ 2. **Composición** (juicio, esta skill):
47
+ - `description` ≤350 chars, la promesa del artefacto + una señal de para quién.
48
+ - `topics` ≤20, minúsculas, guiones; genéricos primero, específicos después.
49
+ - release notes: `baseText` como primer párrafo + `changedUnits` como bullets
50
+ verificables; nada de métricas sin fuente.
51
+ - social preview 1280×640 (el preview se sube a mano: no hay API).
52
+ 3. **Deploy** (API de GitHub, D9):
53
+ ```sh
54
+ gh repo edit --description "<description>" --add-topic <topic> …
55
+ gh release edit <tag> --notes-file <notas.md>
56
+ ```
57
+ 4. **Verificación** (playbook.md, sección "Verificación post-deploy"): la
58
+ lectura de vuelta con `gh repo view --json description,repositoryTopics` y la
59
+ inspección visual del social preview.
60
+ 5. **Cierre del loop**: en v1 el ledger solo direcciona las superficies del enum
61
+ de perfiles; `github-repo-meta` no tiene record y el plan la lee `never`. Es
62
+ un límite declarado (D23): el plan nombra lo que falta tocar; extender la
63
+ clave de superficie del ledger es un cambio de contrato aparte.
64
+
65
+ ## Límites y trampas
66
+
67
+ - **Descripción duplicada**: si el repo y la página npm dicen lo mismo, es señal
68
+ de que el release no cambió nada relevante; no reescribas por inercia.
69
+ - **Topics como hashtags**: no son keywords de marketing; son el índice por el
70
+ que un desconocido encuentra el artefacto.
71
+ - **Notas = changelog completo**: el release note es la parte que le importa a
72
+ quien evalúa adoptar; el detalle vive en CHANGELOG.
73
+ - **Social preview desactualizado**: es la única pieza sin API; verificala a
74
+ mano o no cuenta como publicada.
75
+
76
+ ## Checks
77
+
78
+ `checks.yaml` declara los criterios con la misma forma que el resto de las skills
79
+ (`rule` del catálogo, `platform: github-repo-meta`). En v1 `pasco check` no
80
+ ejecuta una superficie fuera del enum de perfiles: el catálogo queda declarado
81
+ para cuando la superficie tenga renderer, y hoy la verificación es la del
82
+ playbook.
@@ -0,0 +1,50 @@
1
+ # checks.yaml — criterios de la superficie `github-repo-meta` (D23, #262)
2
+ # Misma forma que el catálogo de `pasco check` (rule + value + note). v1: el
3
+ # engine no renderiza esta superficie y `pasco check` no la ejecuta (no está en
4
+ # el enum de perfiles); el catálogo queda declarado y la verificación es la del
5
+ # playbook. Los caps citados son límites oficiales de GitHub.
6
+
7
+ - id: repo-description-max
8
+ platform: github-repo-meta
9
+ rule: max-chars
10
+ value: 350
11
+ surface: bio
12
+ note: "La descripción del repositorio se trunca a 350 caracteres en la UI. [OFICIAL] docs.github.com (customizing-your-repository), leído 2026-10-29."
13
+
14
+ - id: repo-description-min
15
+ platform: github-repo-meta
16
+ rule: min-chars
17
+ value: 40
18
+ surface: bio
19
+ note: "Una descripción de menos de 40 chars no dice qué hace el artefacto; el piso es juicio de esta skill, no un límite de GitHub."
20
+
21
+ - id: repo-topic-max-chars
22
+ platform: github-repo-meta
23
+ rule: max-chars
24
+ value: 50
25
+ surface: headline
26
+ note: "Cada topic tiene un máximo de 50 caracteres (regla de largo, no de conteo). GitHub permite además hasta 20 topics por repositorio: ese tope es de CANTIDAD y el catálogo no tiene una regla de conteo — lo verifica el playbook (paso 5), no esta fila. [OFICIAL] docs.github.com (customizing-your-repository), leído 2026-10-29."
27
+
28
+ - id: repo-facts-ssot
29
+ platform: github-repo-meta
30
+ rule: fact-present
31
+ value: true
32
+ note: "La versión, la fecha y el link de la metadata salen del release row (`pasco release plan`) y del SSOT; un dato que la superficie agrega y el SSOT no tiene es un FAIL de revisión (no lo ejecuta pasco check en v1)."
33
+
34
+ - id: repo-links-identity
35
+ platform: github-repo-meta
36
+ rule: link-in-identity
37
+ value: true
38
+ note: "Toda URL escrita en la descripción o las notas vive en `identity.links` (regla drift §15.4)."
39
+
40
+ - id: repo-geo-consistent
41
+ platform: github-repo-meta
42
+ rule: geo-consistent
43
+ value: true
44
+ note: "Los hechos de la metadata coinciden con el resto de superficies (README, npm): misma versión, mismo nombre, mismo link canónico."
45
+
46
+ - id: repo-lang-en
47
+ platform: github-repo-meta
48
+ rule: lang-present
49
+ value: en
50
+ note: "La metadata del artefacto va en EN (D8/§11) aunque las notas del release puedan traducirse; el repo es la superficie internacional."
@@ -0,0 +1,45 @@
1
+ # GitHub repo metadata — deploy playbook
2
+
3
+ Deploy = **API de GitHub** (D9): la metadata se escribe con `gh` (REST/GraphQL),
4
+ no se pega a mano en la UI. El social preview es la única pieza sin API — se
5
+ sube a mano y se verifica visualmente.
6
+
7
+ Superficie: `github-repo-meta` (`kind: repo-meta`, D23/#262). Estado esperado en
8
+ el plan: `never` mientras el ledger no pueda direccionar la superficie (límite
9
+ declarado v1).
10
+
11
+ ## Flujo
12
+
13
+ 1. **Plan**: `pasco release plan <release-id> --json` → `surfaces[]` trae la fila
14
+ `github-repo-meta` y `baseText` la espina factual del release.
15
+ 2. **Componer** (ver SKILL.md): `description` ≤350 chars · `topics` ≤20 ·
16
+ release notes desde `changedUnits`.
17
+ 3. **Deploy**:
18
+ ```sh
19
+ gh repo edit --description "<description>"
20
+ gh repo edit --add-topic <topic> # repetir por topic nuevo
21
+ gh release edit <tag> --notes-file <notas.md>
22
+ ```
23
+ 4. **Social preview**: subir 1280×640 (Settings → Social preview). Sin API.
24
+ 5. **Verificar** (ver abajo) y reportar el resultado en el PR/issue del release.
25
+
26
+ ## Verificación post-deploy
27
+
28
+ - [ ] `gh repo view --json description --jq .description` devuelve la descripción
29
+ publicada, y no pasa de 350 chars.
30
+ - [ ] `gh repo view --json repositoryTopics --jq '.repositoryTopics[].name'`
31
+ devuelve exactamente los topics declarados (≤20), sin duplicados.
32
+ - [ ] `gh release view <tag> --json body --jq .body` devuelve las notas y el
33
+ primer párrafo cita la versión + fecha del release row.
34
+ - [ ] El social preview se ve en la página del repo (1280×640, sin texto
35
+ recortado) — inspección visual, es la excepción sin API.
36
+ - [ ] La descripción describe el artefacto, no repite el positioning personal de
37
+ `github-readme`.
38
+ - [ ] Ningún número de las notas carece de fuente (regla de evidencia).
39
+
40
+ ## Rollback
41
+
42
+ `gh repo edit` es aditivo y versionable en git no: guardá la descripción y los
43
+ topics anteriores antes de escribir (`gh repo view --json description,repositoryTopics`).
44
+ Revertir = repetir el paso 3 con los valores previos; las release notes se
45
+ revierte con `gh release edit <tag> --notes-file <previas.md>`.
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: hashnode-post
3
+ description: >
4
+ Long-form post skill for Hashnode. Says what a Hashnode article is for and how
5
+ the agent writes it; the machine contract is
6
+ skills/hashnode/longform.yaml (required/optional fields, editorial word band,
7
+ export: api). The judgment layer — shape, voice, GEO, i18n — lives in post.md.
8
+ The agent writes N variants, the operator picks one, and the choice is recorded
9
+ with `pasco post choose`.
10
+ model: juicio (frontier) — the technical claim and its example are judgment; the
11
+ mechanical parts (markdown, front matter) are not
12
+ applies_to: hashnode
13
+ projection: agent-agnostic (any harness that reads Markdown and can run the CLI)
14
+ version: 0.1.0
15
+ status: draft (#361)
16
+ sources: skills/hashnode/longform.yaml
17
+ ---
18
+
19
+ # Hashnode long-form — skill
20
+
21
+ ## Mission
22
+
23
+ Turn one canonical seed into a Hashnode article a developer can build from: the
24
+ problem, a runnable example, the reasoning, the caveats. The skill is the
25
+ judgment layer (D5); the contract plus `pasco post check` are the deterministic
26
+ part.
27
+
28
+ ## When it runs
29
+
30
+ `pasco post task <seed> --format longform --channel hashnode` emits a task bundle
31
+ that points at the contract (`longform.yaml`) and the judgment file (`post.md`);
32
+ it does **not** carry this `SKILL.md`. This file is the channel's strategy
33
+ reference for whoever opens it directly: read the contract for the fields and the
34
+ band, then `post.md` for how to write.
35
+
36
+ Do **not** run `pasco refine hashnode` for this channel. Refine is the profile
37
+ command: with a `SKILL.md` now present it resolves this file instead of answering
38
+ "no skill for hashnode yet", and emits a **profile** task ("write N variants of
39
+ the hashnode profile") that saves to `profiles/hashnode/variants/<id>.md` — not a
40
+ post workflow. Telling a profile skill from a post skill is an engine decision,
41
+ not something this skill can fix.
42
+
43
+ ## Flow
44
+
45
+ 1. Read `longform.yaml` + `post.md`; do not re-derive the limits.
46
+ 2. Write N variants as YAML front matter + markdown body, at
47
+ `posts/<seed>/hashnode/hashnode-<n>.md`.
48
+ 3. The operator picks one; record it with `pasco post choose`.
49
+ 4. `pasco post check` — fix the findings.
50
+ 5. `pasco post export <seed> --target postiz` for the API draft payload.
51
+ 6. Publish manually, then verify the live page (title, tags, canonical).
52
+
53
+ ## Contract pointers (single source each)
54
+
55
+ | Question | Home |
56
+ |---|---|
57
+ | required / optional fields | `longform.yaml` `fields` |
58
+ | editorial word band | `longform.yaml` `limits.words` (not a platform cap) |
59
+ | api vs paste | `longform.yaml` `export` |
60
+ | link policy | `longform.yaml` `links.policy` (`identity-ssot-only`) |
61
+ | dated platform sources | `longform.yaml` `sources[]` |
62
+ | how to write the piece | `post.md` |
63
+
64
+ ## GEO and i18n
65
+
66
+ Public, indexed article page; tags are the arrival paths; the citation unit is
67
+ the snippet plus the claim it proves. Write `en` by default, and treat a Spanish
68
+ article as a separate seed for that audience — never a duplicate of the English
69
+ one under a shared canonical. The crawler stance is the repo's global `ai.policy`
70
+ (`platforms/registry.yaml`, v1, 2026-10-27).
71
+
72
+ ## Verification
73
+
74
+ - `pasco post check` exit 0 (no `missing-field`, `word-count` or
75
+ `url-not-in-ssot` errors).
76
+ - Every code block runs as printed; every version is stated.
77
+ - Facts byte-identical to the SSOT; every URL from the identity SSOT.
78
+ - Report any contract gap as a finding — the engine is not extended from a
79
+ skill (#361).
@@ -0,0 +1,33 @@
1
+ # hashnode long-form rules — machine-readable (#266)
2
+ #
3
+ # Read by `pasco post task|check|export` when the seed is `format: longform`.
4
+ # The word range is an EDITORIAL default (the operator's house style), NOT a
5
+ # platform limit: cite a platform source before encoding a hard cap here.
6
+
7
+ platform: hashnode
8
+ format: longform
9
+ export: api
10
+ settings:
11
+ __type: hashnode
12
+ fields:
13
+ required:
14
+ - title
15
+ - dek
16
+ optional:
17
+ - tags
18
+ - canonical
19
+ - publication
20
+ limits:
21
+ words:
22
+ min: 600
23
+ max: 3000
24
+ links:
25
+ policy: identity-ssot-only
26
+ structure:
27
+ - title
28
+ - dek
29
+ - sections
30
+ sources:
31
+ - url: https://apidocs.hashnode.com/
32
+ date: 2026-10-29
33
+ note: "Hashnode's GraphQL API publishes posts with a title, tags and markdown content."
@@ -0,0 +1,90 @@
1
+ # Hashnode long-form post — skill (MVP)
2
+
3
+ > Long-form post skill. The contract is `longform.yaml`: required/optional
4
+ > fields, the editorial word band and `export` live there and are **not**
5
+ > restated here — this file is the judgment layer the agent reads before
6
+ > writing. The contract's `sources[]` carries the dated URL for every platform
7
+ > claim; re-verify before making a hard one, and cite a source (date + tier)
8
+ > before calling anything a limit.
9
+
10
+ ## What a Hashnode piece is for
11
+
12
+ A technical article published on a developer blogging network — the network feed
13
+ plus, optionally, a publication. It is the "write it once, let it be found by
14
+ developers" surface: an idea, a working example and the reasoning between them.
15
+ It is not a copy of the newsletter issue with a different header.
16
+
17
+ ## Shape
18
+
19
+ 1. **Title** — what the reader can build or decide after reading. No curiosity
20
+ gap that the body does not close.
21
+ 2. **Dek** — one line of scope: the problem, the approach, the result. The
22
+ contract requires it as a field and the engine delivers it as front matter,
23
+ never as the body's first lines. No platform mapping is declared; confirm on
24
+ the first publish how Hashnode renders it [?].
25
+ 3. **Sections** — an opening that states the problem, then one H2 per step of
26
+ the solution, then the caveats. Code is the evidence: show it, state the
27
+ version, and keep it runnable.
28
+
29
+ ## Rules
30
+
31
+ - **Front matter is the field contract.** `longform.yaml` declares the fields this
32
+ channel consumes, required and optional alike; the engine exports only those,
33
+ and an undeclared field is dropped (`pasco post check` warns `unknown-field`) —
34
+ never smuggle one into the body instead.
35
+ - **Length:** the band in `limits.words` is the operator's editorial default (the
36
+ contract says so in its header), not a platform cap. It is a guardrail for a
37
+ piece read in one sitting.
38
+ - **Links:** `links.policy` is `identity-ssot-only`. `canonical` points at the
39
+ canonical home from the SSOT when the piece is syndicated; a network URL is
40
+ never the canonical for another channel's copy unless the SSOT says so.
41
+ - **Facts:** `{{fact:<pointer>}}` for every number, date, version and URL.
42
+ - **Voice:** first person technical, second person for the reader ("you"); no
43
+ hype, no "revolutionary".
44
+
45
+ ## Export: `api`
46
+
47
+ The contract declares `export: api`: the body is markdown published through
48
+ Hashnode's GraphQL API, with the title, tags and markdown content in the mutation
49
+ [OFICIAL — the contract's source, `sources[]` in `longform.yaml`, dated
50
+ 2026-10-29]. Consequences for the writing:
51
+
52
+ - Markdown only; no raw HTML or editor-only embeds.
53
+ - `publication` is a payload field (which blog the post belongs to) — it is not
54
+ a body line and it is not the author profile.
55
+ - The export produces a Postiz draft payload; publishing stays manual (no
56
+ scheduler, no OAuth — `docs/post-mvp.md`).
57
+
58
+ ## GEO (how this channel is read and cited)
59
+
60
+ Hashnode article pages are public and indexed, and the tags are the paths readers
61
+ arrive from. The quotable unit is the code block plus the sentence that says what
62
+ it proves — so keep the claim adjacent to the snippet. The crawler stance is the
63
+ repo's global `ai.policy` (`platforms/registry.yaml`, v1, 2026-10-27); no
64
+ per-channel rule is invented here. Consistency check: version numbers, dates and
65
+ claims match the SSOT byte for byte, same as the profile.
66
+
67
+ ## i18n
68
+
69
+ The network's audience and tag vocabulary are English-first: write `en` by
70
+ default. A Spanish article is its own seed for that audience, not a translation
71
+ of the English one, and it must not share a canonical with it.
72
+
73
+ ## Anti-patterns
74
+
75
+ - A tutorial whose code cannot run as printed.
76
+ - A "getting started" post that is only a link to someone else's docs.
77
+ - Tag stuffing to reach feeds the piece does not belong to.
78
+ - Cross-posting without `canonical`.
79
+ - Numbers with no method, date or source.
80
+
81
+ ## Output contract
82
+
83
+ Save each variant as YAML front matter + a markdown body at
84
+ `posts/<seed>/hashnode/hashnode-<n>.md`, then record the choice and check it:
85
+
86
+ ```
87
+ pasco post choose <seed> --channel hashnode --variant hashnode-1
88
+ pasco post check <seed> # missing-field · word-count · url-not-in-ssot · unknown-field
89
+ pasco post export <seed> --target postiz
90
+ ```
@@ -1,5 +1,8 @@
1
1
  # checks.yaml — criterios machine-readable para `pasco check hotmart`
2
- # (unión con content/checks.yaml del workspace; misma rule catalog que el schema).
2
+ # Misma rule catalog que el schema; la unión con content/checks.yaml decide qué
3
+ # premisas corren (fact-present) y los caps de caracteres salen del catálogo del
4
+ # workspace cuando declara checks — entero, sin merge por fila — o de los defaults
5
+ # del paquete; nunca de estas filas (SPEC §7, pasco check).
3
6
  #
4
7
  # Fuente de los valores: research/SOTA-2026.md (todas las fuentes fechadas
5
8
  # 2026-10-05). Hotmart NO documenta límite para el cuerpo de perfil [?] — no
@@ -16,7 +19,7 @@
16
19
  platform: hotmart
17
20
  rule: fact-present
18
21
  value: true
19
- note: "Hechos del cuerpo byte-idénticos al SSOT (D4); el cuerpo es el cierre de confianza del storefront (SOTA-2026 §1)."
22
+ note: "El cuerpo que escribió `pasco build` se compara contra el render actual del SSOT en los dos sentidos — URL, fecha o número agregado u omitido, y texto sin token que lo explique (WARN, D12; `pasco audit` lo gradúa); el cuerpo es el cierre de confianza del storefront (SOTA-2026 §1). `link-in-identity` es una premisa aparte."
20
23
 
21
24
  - id: h-links-identity
22
25
  platform: hotmart
@@ -1,5 +1,8 @@
1
1
  # checks.yaml — criterios machine-readable para `pasco check kofi`
2
- # (unión con content/checks.yaml del workspace; misma rule catalog que el schema).
2
+ # Misma rule catalog que el schema; la unión con content/checks.yaml decide qué
3
+ # premisas corren (fact-present) y los caps de caracteres salen del catálogo del
4
+ # workspace cuando declara checks — entero, sin merge por fila — o de los defaults
5
+ # del paquete; nunca de estas filas (SPEC §7, pasco check).
3
6
  #
4
7
  # Fuente de los valores: research/SOTA-2026.md (todas las fuentes fechadas
5
8
  # 2026-10-05). Ko-fi NO documenta límite para el About [?] — el max-chars es
@@ -15,7 +18,7 @@
15
18
  platform: kofi
16
19
  rule: fact-present
17
20
  value: true
18
- note: "Hechos del About byte-idénticos al SSOT (D4); el About es el cuerpo del perfil (SOTA-2026 §5)."
21
+ note: "El About que escribió `pasco build` se compara contra el render actual del SSOT en los dos sentidos — URL, fecha o número agregado u omitido, y texto sin token que lo explique (WARN, D12; `pasco audit` lo gradúa); el About es el cuerpo del perfil (SOTA-2026 §5). `link-in-identity` es una premisa aparte."
19
22
 
20
23
  - id: k-links-identity
21
24
  platform: kofi
@@ -39,4 +42,4 @@
39
42
  platform: kofi
40
43
  rule: lang-present
41
44
  value: en
42
- note: "EN requerido (D8/§11); compile no hace fallback por fila de bios — artefacto vacío = gap visible."
45
+ note: "EN requerido (D8/§11); compile no hace fallback por fila de bios — artefacto vacío = gap visible."
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: listmonk-post
3
+ description: >
4
+ Long-form post skill for listmonk — a self-hosted email campaign to named
5
+ lists, not a feed. Says what the issue is for and how the agent writes it; the
6
+ machine contract is skills/listmonk/longform.yaml (required/optional fields,
7
+ the tightest editorial word band of the long-form channels, export: api). The
8
+ judgment layer — subject line, shape, GEO, i18n — lives in post.md. The agent
9
+ writes N variants, the operator picks one and the list, and the choice is
10
+ recorded with `pasco post choose`.
11
+ model: juicio (frontier) — the subject line and the single action are judgment;
12
+ the mechanical parts (markdown, front matter) are not
13
+ applies_to: listmonk
14
+ projection: agent-agnostic (any harness that reads Markdown and can run the CLI)
15
+ version: 0.1.0
16
+ status: draft (#361)
17
+ sources: skills/listmonk/longform.yaml
18
+ ---
19
+
20
+ # listmonk long-form — skill
21
+
22
+ ## Mission
23
+
24
+ Turn one canonical seed into one email issue that a subscriber reads to the end
25
+ and acts on once. The reader opted in, so the piece owes them a letter: a
26
+ subject that states the value, a body that delivers it, one action at the close.
27
+ The skill is the judgment layer (D5); the contract plus `pasco post check` are
28
+ the deterministic part.
29
+
30
+ ## When it runs
31
+
32
+ `pasco post task <seed> --format longform --channel listmonk` emits a task bundle
33
+ that points at the contract (`longform.yaml`) and the judgment file (`post.md`);
34
+ it does **not** carry this `SKILL.md`. This file is the channel's strategy
35
+ reference for whoever opens it directly: read the contract for the fields and the
36
+ band, then `post.md` for how to write.
37
+
38
+ Do **not** run `pasco refine listmonk` for this channel. Refine is the profile
39
+ command: with a `SKILL.md` now present it resolves this file instead of answering
40
+ "no skill for listmonk yet", and emits a **profile** task ("write N variants of
41
+ the listmonk profile") that saves to `profiles/listmonk/variants/<id>.md` — not a
42
+ post workflow. Telling a profile skill from a post skill is an engine decision,
43
+ not something this skill can fix.
44
+
45
+ ## Flow
46
+
47
+ 1. Read `longform.yaml` + `post.md`; the band is narrower here, and the contract
48
+ — not this file — is where its numbers live.
49
+ 2. Write N variants as YAML front matter + markdown body, at
50
+ `posts/<seed>/listmonk/listmonk-<n>.md`.
51
+ 3. The operator picks one and the target `list`; record the choice with
52
+ `pasco post choose`.
53
+ 4. `pasco post check` — fix the findings.
54
+ 5. `pasco post export <seed> --target postiz` for the campaign draft payload.
55
+ 6. Send manually; the operator owns consent, unsubscribes and deliverability.
56
+
57
+ ## Contract pointers (single source each)
58
+
59
+ | Question | Home |
60
+ |---|---|
61
+ | required / optional fields | `longform.yaml` `fields` (`list` selects the audience) |
62
+ | editorial word band | `longform.yaml` `limits.words` (not a platform cap) |
63
+ | api vs paste | `longform.yaml` `export` |
64
+ | link policy | `longform.yaml` `links.policy` (`identity-ssot-only`) |
65
+ | dated platform sources | `longform.yaml` `sources[]` |
66
+ | how to write the issue | `post.md` |
67
+
68
+ ## GEO and i18n
69
+
70
+ Email is not crawlable: citation depends on the same text existing on a public
71
+ surface, with facts byte-identical to this issue. One list per language, one
72
+ language per issue; `en` by default. The crawler stance is the repo's global
73
+ `ai.policy` (`platforms/registry.yaml`, v1, 2026-10-27).
74
+
75
+ ## Verification
76
+
77
+ - `pasco post check` exit 0 (no `missing-field`, `word-count` or
78
+ `url-not-in-ssot` errors).
79
+ - Subject (title), dek and preview are three different sentences doing three
80
+ different jobs.
81
+ - Exactly one call to action; every URL from the identity SSOT.
82
+ - Report any contract gap as a finding — the engine is not extended from a
83
+ skill (#361).
@@ -0,0 +1,33 @@
1
+ # listmonk long-form rules — machine-readable (#266)
2
+ #
3
+ # Read by `pasco post task|check|export` when the seed is `format: longform`.
4
+ # The word range is an EDITORIAL default (the operator's house style), NOT a
5
+ # platform limit: cite a platform source before encoding a hard cap here.
6
+
7
+ platform: listmonk
8
+ format: longform
9
+ export: api
10
+ settings:
11
+ __type: listmonk
12
+ fields:
13
+ required:
14
+ - title
15
+ - dek
16
+ optional:
17
+ - preview
18
+ - publication
19
+ - list
20
+ limits:
21
+ words:
22
+ min: 400
23
+ max: 1500
24
+ links:
25
+ policy: identity-ssot-only
26
+ structure:
27
+ - title
28
+ - dek
29
+ - sections
30
+ sources:
31
+ - url: https://listmonk.app/docs/apis/apis/
32
+ date: 2026-10-29
33
+ note: "listmonk's API creates a campaign with a body and one or more list ids; the audience is the list, not a channel."
@@ -0,0 +1,93 @@
1
+ # listmonk long-form post — skill (MVP)
2
+
3
+ > Long-form post skill. The contract is `longform.yaml`: required/optional
4
+ > fields, the editorial word band and `export` live there and are **not**
5
+ > restated here — this file is the judgment layer the agent reads before
6
+ > writing. The contract's `sources[]` carries the dated URL for every platform
7
+ > claim; re-verify before making a hard one, and cite a source (date + tier)
8
+ > before calling anything a limit.
9
+
10
+ ## What a listmonk piece is for
11
+
12
+ One email campaign to one or more lists the reader already subscribed to. The
13
+ audience is the list, not a feed: there is no discovery, no search surface and no
14
+ second chance — the subject line decides whether the issue is read at all. The
15
+ piece is a letter to a named group, not a post that hopes to be found.
16
+
17
+ ## Shape
18
+
19
+ 1. **Title** — the subject line. It is what the reader sees first and the only
20
+ line that competes with an inbox; state the value of opening, not the topic.
21
+ 2. **Dek** — the line that sets up the issue: why this, why now. Where the
22
+ channel renders a preview line, the optional `preview` field carries it — the
23
+ contract declares both, and they must not say the same thing.
24
+ 3. **Sections** — an email is read top to bottom in one sitting: lead with the
25
+ point, then the evidence, then one action. Short paragraphs, no nested
26
+ headings, no "read more" link as the only payload.
27
+
28
+ ## Rules
29
+
30
+ - **Front matter is the field contract.** `longform.yaml` declares the fields this
31
+ channel consumes, required and optional alike; the engine exports only those,
32
+ and an undeclared field is dropped (`pasco post check` warns `unknown-field`).
33
+ The field that selects the audience is a targeting decision, not prose: choose
34
+ it deliberately, and never send one issue to a list it was not written for.
35
+ - **Length:** the band in `limits.words` is the operator's editorial default (the
36
+ contract says so) — and it is the tightest of the long-form channels, because
37
+ the channel is read in one sitting, not skimmed. Do not pad to reach it and do
38
+ not assume the ceiling is the goal.
39
+ - **Links:** `links.policy` is `identity-ssot-only`. An email has no canonical
40
+ field in the contract, so the SSOT is the only URL source and the archive copy
41
+ (if any) is the only place a URL can be cited from.
42
+ - **Facts:** `{{fact:<pointer>}}` for every number, date and URL.
43
+ - **Voice:** direct, personal, no marketing cadence. Match `brandVoice`; a
44
+ newsletter that sounds like a press release is an unsubscribe.
45
+
46
+ ## Export: `api`
47
+
48
+ The contract declares `export: api`: listmonk's API creates a campaign with a
49
+ body and one or more list ids [OFICIAL — the contract's source, `sources[]` in
50
+ `longform.yaml`, dated 2026-10-29]. Consequences:
51
+
52
+ - The body is markdown/HTML the campaign carries; keep it paste-safe and
53
+ email-safe (no scripts, no layout that only a browser renders).
54
+ - The audience is a `list` id, not a channel: the send is a targeting decision
55
+ the operator makes, and this skill never picks it for them.
56
+ - The export produces a draft payload; sending stays manual.
57
+ - Consent, unsubscribe mechanics and deliverability are the operator's
58
+ responsibility and out of scope here.
59
+
60
+ ## GEO (how this channel is read and cited)
61
+
62
+ Email is not a crawlable surface: an issue that exists only in an inbox is
63
+ invisible to search and to citation. Citability therefore depends on the same
64
+ text also existing where it can be linked and quoted — if it matters, mirror it
65
+ to a public surface whose URL the SSOT already carries, and keep the two copies
66
+ byte-identical in their facts. The crawler stance is the repo's global
67
+ `ai.policy` (`platforms/registry.yaml`, v1, 2026-10-27); no per-channel rule is
68
+ invented here.
69
+
70
+ ## i18n
71
+
72
+ The list is the segmentation: one list per language, and one language per issue.
73
+ Write `en` by default; a Spanish issue goes to the Spanish list, not to every
74
+ list. Do not machine-translate an issue and send both to the same audience.
75
+
76
+ ## Anti-patterns
77
+
78
+ - A subject line that is the article title ("On X") with no reason to open.
79
+ - A dek and a preview that repeat each other.
80
+ - An issue with five calls to action.
81
+ - Marketing cadence in a letter ("Don't miss out!").
82
+ - Sending a piece written for one audience to a list it does not fit.
83
+
84
+ ## Output contract
85
+
86
+ Save each variant as YAML front matter + a markdown body at
87
+ `posts/<seed>/listmonk/listmonk-<n>.md`, then record the choice and check it:
88
+
89
+ ```
90
+ pasco post choose <seed> --channel listmonk --variant listmonk-1
91
+ pasco post check <seed> # missing-field · word-count · url-not-in-ssot · unknown-field
92
+ pasco post export <seed> --target postiz
93
+ ```