disensor 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (27) hide show
  1. {disensor-0.2.0/src/disensor.egg-info → disensor-0.3.0}/PKG-INFO +8 -5
  2. {disensor-0.2.0 → disensor-0.3.0}/README.md +7 -4
  3. {disensor-0.2.0 → disensor-0.3.0}/pyproject.toml +2 -2
  4. disensor-0.3.0/src/disensor/GUIDE.md +108 -0
  5. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/__init__.py +1 -1
  6. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/cli.py +14 -3
  7. disensor-0.3.0/src/disensor/guide.py +32 -0
  8. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/init.py +46 -28
  9. {disensor-0.2.0 → disensor-0.3.0/src/disensor.egg-info}/PKG-INFO +8 -5
  10. {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/SOURCES.txt +3 -0
  11. disensor-0.3.0/tests/test_guide.py +36 -0
  12. {disensor-0.2.0 → disensor-0.3.0}/tests/test_init.py +20 -10
  13. {disensor-0.2.0 → disensor-0.3.0}/LICENSE +0 -0
  14. {disensor-0.2.0 → disensor-0.3.0}/setup.cfg +0 -0
  15. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/__main__.py +0 -0
  16. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/gate.py +0 -0
  17. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/render.py +0 -0
  18. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/residue.schema.json +0 -0
  19. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/rules.py +0 -0
  20. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/template.py +0 -0
  21. {disensor-0.2.0 → disensor-0.3.0}/src/disensor/vectors.py +0 -0
  22. {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/dependency_links.txt +0 -0
  23. {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/entry_points.txt +0 -0
  24. {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/requires.txt +0 -0
  25. {disensor-0.2.0 → disensor-0.3.0}/src/disensor.egg-info/top_level.txt +0 -0
  26. {disensor-0.2.0 → disensor-0.3.0}/tests/test_rules.py +0 -0
  27. {disensor-0.2.0 → disensor-0.3.0}/tests/test_vectors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disensor
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema).
5
5
  Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
6
6
  License: MIT
@@ -28,7 +28,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
28
28
 
29
29
  - `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
30
30
  - `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
31
- - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
31
+ - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
32
32
  - `action.yml`: GitHub Action compuesta, lista para usar.
33
33
  - `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
34
34
 
@@ -39,15 +39,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
39
39
  ```bash
40
40
  pip install disensor # o pipx install disensor, recomendado para CLIs
41
41
 
42
- disensor init # en la raíz del repo: config, sección de CLAUDE.md y workflow de CI
42
+ disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
43
43
  disensor new --gate diff --level B # plantilla prellenada en .residue/
44
44
  disensor validate .residue/<id>.json # schema + reglas R0 a R10
45
45
  disensor gate --no-comment # lo que va a correr CI, en local
46
+
47
+ disensor guide # la guía de llenado, para cualquier agente o humano
48
+ disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
46
49
  ```
47
50
 
48
51
  Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
49
52
 
50
- `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md` y el workflow del gate; lo que ya existe se respeta y se informa. Config resultante:
53
+ `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
51
54
 
52
55
  ```json
53
56
  {
@@ -120,7 +123,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
120
123
 
121
124
  ## Estado
122
125
 
123
- v0.2, borrador en uso. Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
126
+ v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
124
127
 
125
128
  ## Licencia
126
129
 
@@ -14,7 +14,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
14
14
 
15
15
  - `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
16
16
  - `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
17
- - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
17
+ - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
18
18
  - `action.yml`: GitHub Action compuesta, lista para usar.
19
19
  - `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
20
20
 
@@ -25,15 +25,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
25
25
  ```bash
26
26
  pip install disensor # o pipx install disensor, recomendado para CLIs
27
27
 
28
- disensor init # en la raíz del repo: config, sección de CLAUDE.md y workflow de CI
28
+ disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
29
29
  disensor new --gate diff --level B # plantilla prellenada en .residue/
30
30
  disensor validate .residue/<id>.json # schema + reglas R0 a R10
31
31
  disensor gate --no-comment # lo que va a correr CI, en local
32
+
33
+ disensor guide # la guía de llenado, para cualquier agente o humano
34
+ disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
32
35
  ```
33
36
 
34
37
  Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
35
38
 
36
- `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md` y el workflow del gate; lo que ya existe se respeta y se informa. Config resultante:
39
+ `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
37
40
 
38
41
  ```json
39
42
  {
@@ -106,7 +109,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
106
109
 
107
110
  ## Estado
108
111
 
109
- v0.2, borrador en uso. Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
112
+ v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
110
113
 
111
114
  ## Licencia
112
115
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "disensor"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -22,4 +22,4 @@ disensor = "disensor.cli:main"
22
22
  where = ["src"]
23
23
 
24
24
  [tool.setuptools.package-data]
25
- disensor = ["residue.schema.json"]
25
+ disensor = ["residue.schema.json", "GUIDE.md"]
@@ -0,0 +1,108 @@
1
+ # How to fill a residue declaration (residue/v0.2)
2
+
3
+ This guide is the single source of truth for filling the artifact that
4
+ `disensor new` creates under `.residue/`. It ships inside the package:
5
+ `disensor guide` prints it for any coding agent, and `disensor init` installs
6
+ it as a Claude Code skill. The validator (`disensor validate`) and the CI gate
7
+ enforce everything described here; filling the artifact correctly the first
8
+ time is cheaper than iterating against their errors.
9
+
10
+ ## The flow at the close of a review round
11
+
12
+ 1. `disensor new --gate <plan|diff> --level <A|B|C>` creates the template,
13
+ prefilled with what git knows (repository, commits, timestamp, uuid).
14
+ 2. Fill in every `FILL_IN` marker and the findings of the round. The template
15
+ does not validate while markers remain: that is intentional.
16
+ 3. `disensor validate .residue/<id>.json`. Fix until it prints VALID.
17
+ 4. Commit the artifact alone: `docs(residue): declare event <short-id>`.
18
+ Never mixed with code changes.
19
+
20
+ Declare what happened, not what should have happened. An event without
21
+ findings and with an express declaration of absence is valid data, not a
22
+ failure.
23
+
24
+ ## Actors
25
+
26
+ - `generator`: the assistant that produced the plan or diff. `family` is its
27
+ model family (anthropic, openai, google, meta, mistral, other).
28
+ - `reviewers[]`: the attacking assistants. Each needs `reviewer_id` (r1,
29
+ r2...), `family`, `model`, and `confinement`. Rule R4 rejects any reviewer
30
+ whose family equals the generator's: decorrelation is the point of the
31
+ method, not an option.
32
+ - `reviewers[].prompt_hash`: hash of the adversarial brief given to the
33
+ reviewer. Compute it with `disensor hash <brief-file>`; paste the full
34
+ `sha256:...` output.
35
+ - `confinement.mode`: how it was guaranteed that the reviewer only reads
36
+ (permissions, sandbox, read_only_by_instruction, no_confinement). Declare
37
+ the real mode; the gate makes gaps visible instead of hiding them.
38
+ - `confinement.verified`: true ONLY if you ran `git status` after the
39
+ reviewer's run and the tree was clean. Otherwise leave false.
40
+ - `human_arbiter.present`: must be true; an event without a human arbiter
41
+ does not comply with the protocol (R0).
42
+
43
+ ## Findings
44
+
45
+ One entry per point the reviewer raised. Fields: `id` (h1, h2...), `origin`
46
+ (the reviewer_id that produced it), `severity` (critical, major, minor,
47
+ info), `title`, `description`, `location` (full profile only), and:
48
+
49
+ - `verification.against`: what the generator checked the finding against
50
+ before accepting or refuting it: `repository` (code, config, contracts),
51
+ `execution` (running tests or the program), or `none`. Do not take the
52
+ reviewer's word: verify, then decide.
53
+ - `final_state`, the terminal outcome. Decision table:
54
+ - `incorporated`: the finding changed the plan or the code. In the diff
55
+ gate you MUST add `fix_verification` with type `diff_gate` or
56
+ `specific_test` (R7); `pending_in_diff_gate` is only legal in the plan
57
+ gate. If the reviewer's remedy was wrong and you fixed it, record
58
+ `remedy_adjustment`.
59
+ - `debt_recorded`: valid, deferred; requires `debt_id` (schema).
60
+ - `owner_decision`: valid, the owner changed scope, behavior or accepted
61
+ risk; requires `risk_record` (schema).
62
+ - `refuted_verifiable`: false positive with proof; requires `evidence`
63
+ (text quote, link, or hash).
64
+ - `refuted_interpretive`: false positive by judgment; it MUST also appear
65
+ as a residue item (R1) with `requires_human_attention: true` (R8).
66
+ - `escalated_open`: no decision yet; it MUST also appear as a residue
67
+ item (R1).
68
+
69
+ ## Residue
70
+
71
+ The heart of the declaration: what the cycle could not close by itself.
72
+ Either `items` or the express absence, never an empty field.
73
+
74
+ - `items[]`: `id` (r1, r2...), `class`, `finding_ref` when it comes from a
75
+ finding, `requires_human_attention`.
76
+ - `escalation_without_decision`: from every `escalated_open` finding.
77
+ - `principal_refutation`: from every refuted finding; add
78
+ `refutation_type` (`verifiable` or `interpretive`; interpretive forces
79
+ `requires_human_attention: true`).
80
+ - `execution_gap`: behavior execution could not arbitrate; add
81
+ `gap_reason`. In Level A an execution gap blocks the merge until a
82
+ technical lead accepts it in writing (`lead_acceptance`, R5).
83
+ - Absence: `"declared_absence": true` plus `declaration`, minimum 30
84
+ characters of concrete text. Generic markers (none, n/a, all resolved,
85
+ ninguno, todo resuelto...) are rejected by R2 in any language.
86
+
87
+ ## Metrics
88
+
89
+ `counts` must add up exactly against the findings list (R6): each
90
+ `valid.*` and `false_positives.*` bucket equals the number of findings in
91
+ that state, `escalated_open` likewise, `total_findings` equals the list
92
+ length. Count, do not estimate.
93
+
94
+ ## Minimized profile
95
+
96
+ No free text anywhere (R9): no titles, descriptions or locations in
97
+ findings; no descriptions in items; evidence only as `hash`; `repository`
98
+ as a hash or opaque identifier, never a URL.
99
+
100
+ ## Quick map of validator labels
101
+
102
+ R0 human arbiter absent; R1 residue/finding coherence; R2 generic or
103
+ template markers; R3 abbreviated path over protected cases; R4 reviewer
104
+ shares the generator's family; R5 Level A execution gap without lead
105
+ acceptance; R6 counts that do not add up; R7 incorporated without verified
106
+ fix in the diff gate; R8 interpretive refutation without human attention;
107
+ R9 text leaks in the minimized profile; R10 full profile without findings;
108
+ `schema` shape errors (missing required fields, wrong enums, bad patterns).
@@ -1,2 +1,2 @@
1
1
  """disensor: residue declaration of adversarial review (controlled disagreement)."""
2
- __version__ = "0.2.0"
2
+ __version__ = "0.3.0"
@@ -12,6 +12,7 @@ import sys
12
12
  from pathlib import Path
13
13
 
14
14
  from .gate import main_gate
15
+ from .guide import main_guide, main_hash
15
16
  from .init import main_init
16
17
  from .rules import load_schema, validate_artifact
17
18
  from .template import main_new
@@ -38,11 +39,12 @@ def build_parser() -> argparse.ArgumentParser:
38
39
  )
39
40
  sub = p.add_subparsers(dest="command", required=True)
40
41
 
41
- init = sub.add_parser("init", help="Scaffold a repository: config, CLAUDE.md section and CI workflow.")
42
+ init = sub.add_parser("init", help="Scaffold a repository: config, CLAUDE.md section, filling skill and CI workflow.")
42
43
  init.add_argument("--level", "--nivel", choices=["A", "B", "C"], default="B")
43
- init.add_argument("--no-claude", action="store_true", help="Do not touch CLAUDE.md.")
44
+ init.add_argument("--no-claude", action="store_true", help="Do not touch CLAUDE.md nor the skill.")
45
+ init.add_argument("--no-skill", action="store_true", help="Write the CLAUDE.md section but not the skill.")
44
46
  init.add_argument("--claude-global", action="store_true",
45
- help="Write the Claude Code section to ~/.claude/CLAUDE.md instead of the repo.")
47
+ help="Write the Claude Code section and skill to ~/.claude instead of the repo.")
46
48
  init.add_argument("--no-workflow", action="store_true", help="Do not write the CI workflow.")
47
49
  init.set_defaults(func=main_init)
48
50
 
@@ -67,6 +69,15 @@ def build_parser() -> argparse.ArgumentParser:
67
69
  gate.add_argument("--no-comment", "--sin-comentario", action="store_true",
68
70
  help="Do not post a comment on the PR.")
69
71
  gate.set_defaults(func=main_gate)
72
+
73
+ guide = sub.add_parser("guide", help="Print the artifact filling guide (for any coding agent or human).")
74
+ guide.set_defaults(func=main_guide)
75
+
76
+ hash_ = sub.add_parser("hash", help="Compute the sha256:<hex> value for prompt_hash from a file or text.")
77
+ src = hash_.add_mutually_exclusive_group(required=True)
78
+ src.add_argument("file", nargs="?", help="File to hash (e.g. the adversarial brief).")
79
+ src.add_argument("--text", help="Hash this literal text instead of a file.")
80
+ hash_.set_defaults(func=main_hash)
70
81
  return p
71
82
 
72
83
 
@@ -0,0 +1,32 @@
1
+ """Packaged filling guide and hash helper: `disensor guide` and `disensor hash`.
2
+
3
+ The guide (GUIDE.md, shipped inside the package) is the single source of
4
+ truth on how to fill a residue declaration. `disensor init` installs it as a
5
+ Claude Code skill; `disensor guide` prints it for any other coding agent or
6
+ for a human. `disensor hash` computes the `sha256:...` value the schema
7
+ expects in `prompt_hash`, so nobody hashes the adversarial brief by hand.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ from importlib import resources
13
+ from pathlib import Path
14
+
15
+
16
+ def guide_text() -> str:
17
+ """The packaged guide, verbatim."""
18
+ return resources.files("disensor").joinpath("GUIDE.md").read_text(encoding="utf-8")
19
+
20
+
21
+ def main_guide(args) -> int:
22
+ print(guide_text(), end="")
23
+ return 0
24
+
25
+
26
+ def main_hash(args) -> int:
27
+ if args.text is not None:
28
+ data = args.text.encode("utf-8")
29
+ else:
30
+ data = Path(args.file).read_bytes()
31
+ print("sha256:" + hashlib.sha256(data).hexdigest())
32
+ return 0
@@ -5,10 +5,16 @@ initialized once with this command. Idempotent by design: running it again
5
5
  respects what already exists and reports what it did, so nothing is ever
6
6
  silently overwritten.
7
7
 
8
- It writes three pieces, each optional by flag:
8
+ It writes four pieces, each optional by flag:
9
9
  1. disensor.config.json: the criticality level, versioned with the code.
10
- 2. A CLAUDE.md section: the event-close instructions for Claude Code.
11
- 3. .github/workflows/disensor.yml: the CI gate, pinned to this version.
10
+ 2. A CLAUDE.md section: the event-close trigger for Claude Code.
11
+ 3. A Claude Code skill with the full filling guide, loaded on demand
12
+ (the same text `disensor guide` prints for any other agent).
13
+ 4. .github/workflows/disensor.yml: the CI gate, pinned to this version.
14
+
15
+ After `pip install disensor` and `disensor init`, the user should not have
16
+ to touch anything by hand: Claude knows when (CLAUDE.md) and how (skill),
17
+ and CI enforces the result.
12
18
  """
13
19
  from __future__ import annotations
14
20
 
@@ -17,6 +23,7 @@ import subprocess
17
23
  from pathlib import Path
18
24
 
19
25
  from . import __version__
26
+ from .guide import guide_text
20
27
 
21
28
  CLAUDE_HEADING = "## disensor: residue declaration at event close"
22
29
 
@@ -25,29 +32,16 @@ CLAUDE_SECTION = f"""{CLAUDE_HEADING}
25
32
  At the end of each adversarial review round (plan or diff), BEFORE closing
26
33
  the event:
27
34
 
28
- 1. Run `disensor new --gate <plan|diff> --level <A|B|C>` and fill in the
29
- template it creates under `.residue/` with what happened in the round:
30
- - One finding per point raised by the reviewer (an assistant of another
31
- model family), with its terminal state: incorporated (with
32
- `remedy_adjustment` if you fixed the proposed remedy), debt_recorded
33
- (with id), owner_decision (with the risk record), refuted_verifiable
34
- (with evidence), refuted_interpretive, or escalated_open.
35
- - The verification of each finding (`against`: repository or execution).
36
- - In the diff gate, the fix verification of each incorporated finding
37
- (diff_gate or specific_test). Never "pending".
38
- - The residue: escalations without a decision, refutations of the
39
- principal, and execution gaps. If nothing remained, the express
40
- declaration of absence (concrete text, not "no residue").
41
- - The sha256 hash of the adversarial brief used, in `prompt_hash`.
42
- - `confinement.verified: true` ONLY if you ran `git status` after the
43
- round and it was clean.
44
- 2. Run `disensor validate` on the file. If it fails, fix it: the CI gate
35
+ 1. `disensor new --gate <plan|diff> --level <A|B|C>` creates the template
36
+ under `.residue/`.
37
+ 2. Fill it following the disensor skill (`.claude/skills/disensor/SKILL.md`;
38
+ the same guide is available as `disensor guide`). Do not invent findings
39
+ or states: the artifact declares what happened, not what should have
40
+ happened.
41
+ 3. Run `disensor validate` on the file until it prints VALID; the CI gate
45
42
  rejects exactly the same.
46
- 3. The artifact goes in its own commit (`docs(residue): declare event
47
- <short-id>`). Never mixed with code.
48
- 4. Do not invent findings or states: the artifact declares what happened,
49
- not what should have happened. An event without findings and with an
50
- express declaration of absence is valid pilot data, not a failure.
43
+ 4. The artifact goes in its own commit (`docs(residue): declare event
44
+ <short-id>`), never mixed with code.
51
45
  """
52
46
 
53
47
  GLOBAL_GUARD = (
@@ -55,6 +49,13 @@ GLOBAL_GUARD = (
55
49
  "`disensor.config.json`:\n\n"
56
50
  )
57
51
 
52
+ SKILL_FRONTMATTER = """---
53
+ name: disensor
54
+ description: Fill and validate a disensor residue declaration (.residue/*.json) at the close of an adversarial review round. Use when closing a review event, filling the template created by `disensor new`, or fixing errors reported by `disensor validate`.
55
+ ---
56
+
57
+ """
58
+
58
59
  WORKFLOW = f"""# Generated by disensor init. The gate validates the .residue/ artifacts of each PR.
59
60
  name: disensor
60
61
  on:
@@ -119,6 +120,16 @@ def _write_claude(path: Path, section: str, label: str, report: list[str]) -> No
119
120
  report.append(f"created {label}")
120
121
 
121
122
 
123
+ def _write_skill(base: Path, label: str, report: list[str]) -> None:
124
+ path = base / ".claude" / "skills" / "disensor" / "SKILL.md"
125
+ if path.exists():
126
+ report.append(f"kept {label} (already exists)")
127
+ return
128
+ path.parent.mkdir(parents=True, exist_ok=True)
129
+ path.write_text(SKILL_FRONTMATTER + guide_text(), encoding="utf-8")
130
+ report.append(f"created {label}")
131
+
132
+
122
133
  def _write_workflow(root: Path, report: list[str]) -> None:
123
134
  path = root / ".github" / "workflows" / "disensor.yml"
124
135
  if path.exists():
@@ -139,16 +150,21 @@ def main_init(args) -> int:
139
150
  _write_config(root, args.level, report)
140
151
 
141
152
  if args.claude_global:
153
+ home = Path.home()
142
154
  _write_claude(
143
- Path.home() / ".claude" / "CLAUDE.md",
155
+ home / ".claude" / "CLAUDE.md",
144
156
  GLOBAL_GUARD + CLAUDE_SECTION,
145
157
  "~/.claude/CLAUDE.md (global)",
146
158
  report,
147
159
  )
160
+ if not args.no_skill:
161
+ _write_skill(home, "~/.claude/skills/disensor/SKILL.md (global)", report)
148
162
  elif not args.no_claude:
149
163
  _write_claude(root / "CLAUDE.md", CLAUDE_SECTION, "CLAUDE.md", report)
164
+ if not args.no_skill:
165
+ _write_skill(root, ".claude/skills/disensor/SKILL.md", report)
150
166
  else:
151
- report.append("skipped CLAUDE.md (--no-claude)")
167
+ report.append("skipped CLAUDE.md and skill (--no-claude)")
152
168
 
153
169
  if args.no_workflow:
154
170
  report.append("skipped .github/workflows/disensor.yml (--no-workflow)")
@@ -160,6 +176,8 @@ def main_init(args) -> int:
160
176
  print(
161
177
  "\nNext: the adversarial loop needs a reviewer from another model family "
162
178
  "(rule R4). Close each review round with `disensor new`, fill in the "
163
- "artifact, and `disensor validate` it before committing."
179
+ "artifact (the skill or `disensor guide` explains every field, and "
180
+ "`disensor hash` computes prompt_hash), and `disensor validate` it "
181
+ "before committing."
164
182
  )
165
183
  return 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disensor
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Adversarial plan & code review with a declared residue. Emits, validates and CI-enforces residue declarations (residue/v0.2 schema).
5
5
  Author-email: Nicolas Rocchia <nicolasrocchia@gmail.com>
6
6
  License: MIT
@@ -28,7 +28,7 @@ Paper del método: Rocchia, N. (2026), *Desacuerdo controlado: revisión adversa
28
28
 
29
29
  - `spec/residue.schema.json`: el esquema del artefacto (JSON Schema 2020-12), versión residue/v0.2.
30
30
  - `spec/examples/`: tres artefactos de ejemplo, incluido un evento real anonimizado y el perfil minimizado sin texto libre.
31
- - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`).
31
+ - `src/disensor/`: paquete Python con el validador (reglas R0 a R10), el gate de CI (chequeos G1 a G5), el render del comentario de PR, el scaffolding de artefactos y el de repositorios (`init`), y la guía de llenado empaquetada (`GUIDE.md`).
32
32
  - `action.yml`: GitHub Action compuesta, lista para usar.
33
33
  - `docs/integracion-claude-code.md`: cómo el flujo real (Claude Code más un revisor de otra familia) emite el artefacto al cierre de cada evento.
34
34
 
@@ -39,15 +39,18 @@ El paquete se instala una vez (global); cada repositorio se inicializa una vez:
39
39
  ```bash
40
40
  pip install disensor # o pipx install disensor, recomendado para CLIs
41
41
 
42
- disensor init # en la raíz del repo: config, sección de CLAUDE.md y workflow de CI
42
+ disensor init # en la raíz del repo: config, CLAUDE.md, skill de llenado y workflow de CI
43
43
  disensor new --gate diff --level B # plantilla prellenada en .residue/
44
44
  disensor validate .residue/<id>.json # schema + reglas R0 a R10
45
45
  disensor gate --no-comment # lo que va a correr CI, en local
46
+
47
+ disensor guide # la guía de llenado, para cualquier agente o humano
48
+ disensor hash consigna.md # el sha256: que pide prompt_hash, sin calcularlo a mano
46
49
  ```
47
50
 
48
51
  Los subcomandos y flags de la v0.1 en español (`nuevo`, `validar`, `--compuerta`, `--nivel`, `--directorio`, `--sin-comentario`) siguen funcionando como alias.
49
52
 
50
- `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md` y el workflow del gate; lo que ya existe se respeta y se informa. Config resultante:
53
+ `disensor init` escribe, en forma idempotente, el `disensor.config.json` (el nivel viaja con el código, en un archivo versionado), la sección de cierre de evento en `CLAUDE.md`, la skill de Claude Code con la guía completa de llenado (`.claude/skills/disensor/SKILL.md`, cargada a demanda al cerrar cada ronda) y el workflow del gate; lo que ya existe se respeta y se informa. El principio es que después de `pip install disensor` y `disensor init` el usuario no toque nada a mano: Claude sabe cuándo (CLAUDE.md) y cómo (la skill), cualquier otro agente recibe lo mismo con `disensor guide`, y el CI hace cumplir el resultado. Config resultante:
51
54
 
52
55
  ```json
53
56
  {
@@ -120,7 +123,7 @@ Migración desde v0.1: renombrar `.residuo/` a `.residue/`, las claves del confi
120
123
 
121
124
  ## Estado
122
125
 
123
- v0.2, borrador en uso. Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
126
+ v0.3, borrador en uso. El esquema sigue en residue/v0.2 (v0.3 no lo toca: agrega la skill de llenado, `disensor guide` y `disensor hash`). Decisión cerrada en v0.2: claves del esquema y CLI en inglés (el español queda como alias en la CLI y como idioma de la documentación). El esquema puede cambiar hasta v1.0; los cambios se declaran en el propio esquema. Decisión abierta antes de v1.0: licencia definitiva (hoy MIT; Apache-2.0 está en consideración por la concesión de patentes antes del release público).
124
127
 
125
128
  ## Licencia
126
129
 
@@ -1,10 +1,12 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ src/disensor/GUIDE.md
4
5
  src/disensor/__init__.py
5
6
  src/disensor/__main__.py
6
7
  src/disensor/cli.py
7
8
  src/disensor/gate.py
9
+ src/disensor/guide.py
8
10
  src/disensor/init.py
9
11
  src/disensor/render.py
10
12
  src/disensor/residue.schema.json
@@ -17,6 +19,7 @@ src/disensor.egg-info/dependency_links.txt
17
19
  src/disensor.egg-info/entry_points.txt
18
20
  src/disensor.egg-info/requires.txt
19
21
  src/disensor.egg-info/top_level.txt
22
+ tests/test_guide.py
20
23
  tests/test_init.py
21
24
  tests/test_rules.py
22
25
  tests/test_vectors.py
@@ -0,0 +1,36 @@
1
+ """Tests of `disensor guide` and `disensor hash`: the no-hands helpers."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import re
6
+
7
+ from disensor.cli import build_parser
8
+ from disensor.guide import guide_text
9
+
10
+ PROMPT_HASH_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
11
+
12
+
13
+ def run(capsys, *argv: str) -> str:
14
+ args = build_parser().parse_args(list(argv))
15
+ assert args.func(args) == 0
16
+ return capsys.readouterr().out
17
+
18
+
19
+ def test_guide_prints_packaged_text(capsys):
20
+ out = run(capsys, "guide")
21
+ assert out == guide_text()
22
+ assert "residue/v0.2" in out and "R4" in out and "disensor hash" in out
23
+
24
+
25
+ def test_hash_of_file_matches_hashlib_and_schema_pattern(tmp_path, capsys):
26
+ f = tmp_path / "consigna.md"
27
+ f.write_bytes(b'{"prueba":"recibo"}')
28
+ out = run(capsys, "hash", str(f)).strip()
29
+ assert out == "sha256:" + hashlib.sha256(b'{"prueba":"recibo"}').hexdigest()
30
+ assert PROMPT_HASH_PATTERN.match(out)
31
+
32
+
33
+ def test_hash_of_text(capsys):
34
+ out = run(capsys, "hash", "--text", "consigna adversarial v3").strip()
35
+ expected = hashlib.sha256("consigna adversarial v3".encode("utf-8")).hexdigest()
36
+ assert out == f"sha256:{expected}"
@@ -30,24 +30,25 @@ def test_init_scaffolds_everything(repo, monkeypatch):
30
30
  assert config == {"criticality_level": "B", "level_A_enabled": False}
31
31
  claude = (repo / "CLAUDE.md").read_text(encoding="utf-8")
32
32
  assert CLAUDE_HEADING in claude and "disensor validate" in claude
33
+ skill = (repo / ".claude" / "skills" / "disensor" / "SKILL.md").read_text(encoding="utf-8")
34
+ assert skill.startswith("---\nname: disensor\n")
35
+ assert "final_state" in skill and "R4" in skill # the full guide travels in the skill
33
36
  workflow = (repo / ".github" / "workflows" / "disensor.yml").read_text(encoding="utf-8")
34
37
  assert f"NicolasRocchia/disensor@v{__version__}" in workflow
35
38
  assert "fetch-depth: 0" in workflow
36
39
 
37
40
 
38
41
  def test_init_is_idempotent(repo, monkeypatch):
42
+ pieces = [
43
+ repo / "disensor.config.json",
44
+ repo / "CLAUDE.md",
45
+ repo / ".claude" / "skills" / "disensor" / "SKILL.md",
46
+ repo / ".github" / "workflows" / "disensor.yml",
47
+ ]
39
48
  run_init(repo, monkeypatch)
40
- before = {
41
- p.name: p.read_text(encoding="utf-8")
42
- for p in [repo / "disensor.config.json", repo / "CLAUDE.md",
43
- repo / ".github" / "workflows" / "disensor.yml"]
44
- }
49
+ before = {str(p): p.read_text(encoding="utf-8") for p in pieces}
45
50
  run_init(repo, monkeypatch)
46
- after = {
47
- p.name: p.read_text(encoding="utf-8")
48
- for p in [repo / "disensor.config.json", repo / "CLAUDE.md",
49
- repo / ".github" / "workflows" / "disensor.yml"]
50
- }
51
+ after = {str(p): p.read_text(encoding="utf-8") for p in pieces}
51
52
  assert before == after
52
53
 
53
54
 
@@ -69,9 +70,16 @@ def test_init_flags_skip_pieces(repo, monkeypatch):
69
70
  assert json.loads((repo / "disensor.config.json").read_text(encoding="utf-8"))[
70
71
  "criticality_level"] == "C"
71
72
  assert not (repo / "CLAUDE.md").exists()
73
+ assert not (repo / ".claude").exists() # --no-claude also skips the skill
72
74
  assert not (repo / ".github").exists()
73
75
 
74
76
 
77
+ def test_init_no_skill_keeps_claude_section(repo, monkeypatch):
78
+ run_init(repo, monkeypatch, "--no-skill")
79
+ assert CLAUDE_HEADING in (repo / "CLAUDE.md").read_text(encoding="utf-8")
80
+ assert not (repo / ".claude").exists()
81
+
82
+
75
83
  def test_init_claude_global_is_guarded(repo, monkeypatch, tmp_path_factory):
76
84
  home = tmp_path_factory.mktemp("home")
77
85
  monkeypatch.setenv("HOME", str(home))
@@ -81,7 +89,9 @@ def test_init_claude_global_is_guarded(repo, monkeypatch, tmp_path_factory):
81
89
  content = global_md.read_text(encoding="utf-8")
82
90
  assert "disensor.config.json" in content.splitlines()[0] or "ONLY inside repositories" in content
83
91
  assert CLAUDE_HEADING in content
92
+ assert (home / ".claude" / "skills" / "disensor" / "SKILL.md").exists()
84
93
  assert not (repo / "CLAUDE.md").exists()
94
+ assert not (repo / ".claude").exists()
85
95
 
86
96
 
87
97
  def test_init_warns_on_v01_config(repo, monkeypatch, capsys):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes