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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .claude/rules/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,20 @@ Si un hook futuro necesita otra dep no-stdlib, abrir un commit propio que la jus
- Loader con suite propia: `hooks/tests/test_lib_policy.py` (57 casos — cache behavior, los 3 accessors con happy path + missing section + missing file, `derive_test_pair` por label, invalidación del cache via `reset_cache()`, precedencia del first-match-wins en conditional rules).
- Tests de los 3 hooks consumidores: fixture de repo escribe `policy.yaml` a disco + autouse `_reset_policy_cache` para isolation. Tests unitarios de los consumidores aceptan la dataclass tipada como argumento (`check_docs_sync(files, rules)`, `match_triggers(paths, trigger)`). Constantes hardcoded que eran mirror de `policy.yaml` (`TestIsEnforcedUnit`, `TestExpectedTestPairUnit`, `TestPolicyConstants`) eliminadas por redundantes con el loader test.

### Drift temporal meta-repo ↔ template (abierto tras D5b)
### Drift temporal meta-repo ↔ template — cerrado en `refactor/template-policy-d5b-migration`

Tras D5b, `policy.yaml` del meta-repo tiene el shape nuevo (`pre_write.enforced_patterns` + `docs_sync_conditional.hooks/**` con `excludes: ["hooks/tests/**"]`). **`templates/policy.yaml.hbs` + `generator/renderers/policy.ts` + snapshots NO fueron tocados** en esta rama. Proyectos generados con `pos` hoy emiten un `policy.yaml` con el shape anterior. Reconciliar (template + renderer + snapshots + `pyyaml` en requirements-dev de stacks Python generados) queda diferido a una rama propia post-D6.
Tras D5b el `policy.yaml` del meta-repo quedó con el shape nuevo, mientras que `templates/policy.yaml.hbs` siguió emitiendo el shape pre-D5b. El drift se cerró en la rama `refactor/template-policy-d5b-migration` migrando el template a un shape contractual con el loader (`hooks/_lib/policy.py`). Decisiones ratificadas:

**No leer esta rule como "el template ya consume el loader"** — explícitamente no lo hace. Ver [MASTER_PLAN.md § Rama D5b](../../MASTER_PLAN.md), [ROADMAP.md § refactor/d5-policy-loader](../../ROADMAP.md), [HANDOFF.md §11](../../HANDOFF.md), [docs/ARCHITECTURE.md § 7](../../docs/ARCHITECTURE.md#capa-1-hooks).
- `lifecycle.pre_write.enforced_patterns: []` — clave presente, lista vacía. Proyectos generados **no** heredan los enforcement patterns del meta-repo; el loader devuelve `PreWriteRules(())` (no `None`).
- `skills_allowed` — clave **omitida** en el template. Loader devuelve `None` (deferred), reflejando el estado del meta-repo hasta que se pueble la allowlist por proyecto.
- `lifecycle.pre_compact.persist` — los 3 items canónicos (`decisions_in_flight`, `phase_minus_one_state`, `unsaved_pattern_candidates`).
- `lifecycle.post_merge.skills_conditional[0].trigger` — globs genéricos conservadores (`src/**`, `lib/**`, `*.py`, `package.json`, `pyproject.toml`) + `skip_if_only` para docs + `min_files_changed: 2`. Suficiente para que `post_merge_trigger()` devuelva un dataclass tipado; los proyectos afinan cuando estabilicen convenciones.

Contrato lockdown: `bin/tests/test_template_loader_contract.py` corre los 5 accessors del loader real contra el output del generador real sobre los 3 profiles canónicos (cli-tool, nextjs-app, agent-sdk). Cualquier regresión del template que rompa el shape esperado se detecta ahí — no hay TS-side reimplementation del contrato.

Selftest cleanup: `bin/_selftest.py` removió los overlays de D4 y D5 (template ahora emite baseline matching). D3 y D6 mantienen overlays mínimos por diseño explícito (A1 emite `enforced_patterns: []` + A2 omite `skills_allowed`); el comentario en cada escenario documenta la razón.

Ver [MASTER_PLAN.md § Rama F3b](../../MASTER_PLAN.md), [ROADMAP.md fila refactor/template-policy-d5b-migration](../../ROADMAP.md), [HANDOFF.md §1](../../HANDOFF.md), [docs/ARCHITECTURE.md § 10 Selftest end-to-end](../../docs/ARCHITECTURE.md).

## Sexto hook entregado — `hooks/pre-compact.py` (Rama D6)

Expand Down
8 changes: 4 additions & 4 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
## 1. Snapshot

- Repo: `project-operating-system` (plugin `pos`).
- Rama actual: **F4 ✅ PR pendiente** (`feat/f4-marketplace-public-repo`, en revisión docs-sync — última rama de Fase F). Anterior: **F3 ✅ PR #27** (mergeada). Siguiente: **`refactor/template-policy-d5b-migration`** (rama propia post-F4 para cerrar drift template post-D5b) o cualquier rama Fase G — Fase F cerrada tras F4.
- Rama actual: **`refactor/template-policy-d5b-migration` ✅ PR pendiente** (sub-rama refactor post-F4 que cierra el drift `meta-repo ↔ template` documentado desde D5b y reforzado en F3). Anterior: **F4 ✅ PR pendiente** (`feat/f4-marketplace-public-repo`, en revisión docs-sync — última rama de Fase F propiamente dicha). Siguiente: **`feat/fx-knowledge-plane-plan`** (entry-point Fase G, opcional, docs-only) o cualquier carry-over restante post-F4 — Fase F + drift template cerrados.
- `refactor/template-policy-d5b-migration` entregó: `templates/policy.yaml.hbs` migrado al shape contractual con loader (A1 `pre_write.enforced_patterns: []` + A2 `skills_allowed` omitido + A3 `pre_compact.persist` 3 items canónicos + A4 `post_merge.skills_conditional[0].trigger` con globs genéricos conservadores) + 3 snapshots regenerados (cli-tool, nextjs-app, agent-sdk) + cleanup de overlays D4+D5 en `bin/_selftest.py` (D3+D6 mantienen overlays mínimos por diseño explícito). Contract test Python-side `bin/tests/test_template_loader_contract.py` corre los 5 accessors reales del loader sobre el output del generator real. Suite: 671 passed + 1 skipped (vs main baseline 644 + 1 skip; +27 contract tests, sin regresión). Vitest 515/515. Selftest 5/5 escenarios verdes sin overlays para D4/D5.
- F4 entregó: `.claude-plugin/marketplace.json` (manifest oficial Claude Code marketplace primitive: top-level `{name, owner, plugins, metadata}` + `owner.name="javiAI"` + `plugins[0].source.{source:github, repo:javiAI/project-operating-system, ref:v0.1.0}`) + `.github/workflows/release.yml` (5 jobs: version-match → selftest + build-bundle → publish-release → mirror-marketplace condicional via `vars.POS_MARKETPLACE_REPO`) + `docs/RELEASE.md` (runbook de versionado + bundle + flujo + recovery + activación de mirror) + bump `plugin.json.version` 0.0.1→0.1.0 (single source of truth: tag git = `v${version}`; `marketplace.json.source.ref` espeja). Bundle release curated plugin-only (excluye `generator/`, `templates/`, `questionnaire/`, `tools/`). Repo público `javiAI/pos-marketplace` **diferido** — creación manual cuando se decida ir live; mirror skippea silenciosamente si `POS_MARKETPLACE_REPO` está vacío. Suite: 665 passed + 1 skipped.
- F3 entregó: `bin/pos-selftest.sh` (wrapper bash mínimo) + `bin/_selftest.py` (orquestador stdlib Python) + `bin/tests/test_selftest_smoke.py` + `bin/tests/test_selftest_scenarios.py` (5 escenarios funcionales-críticos D1/D3/D4/D5/D6 sobre proyecto sintético generado real-time por `npx tsx generator/run.ts --profile cli-tool.yaml`). CI: nuevo job `selftest` (ubuntu × py 3.11) en `.github/workflows/ci.yml`. Sin Claude Code runtime, sin invocaciones reales de skills/agents.
- F2 entregó: `agents/pos-code-reviewer.md` + `agents/pos-architect.md` (plugin subagents primitive-correct con namespace `pos-*`); flips de `pre-commit-review` y `compound` a los nuevos consumidores; 26 contract tests parametrizados (`agents/tests/test_agent_frontmatter.py`). **No** toca `policy.yaml` (`agents_allowed` diferido). `auditor` diferido (sin consumer real, regla #7).
Expand Down Expand Up @@ -102,7 +103,7 @@ Ejecuta §2.1 Fase -1 completo. Espera aprobación explícita antes de `git chec
- El hook `pre-write-guard.py` **ya está vivo** desde D3: PreToolUse(Write) blocker. Bloquea con exit 2 la creación de archivos en paths enforced (`hooks/*.py` top-level + `generator/**/*.ts` excluyendo tests/fixtures) sin test pair co-located. Los writes sobre archivos existentes en esos paths enforced sí pasan, pero siguen logueándose (allow / audit trail del flujo de edición). El pass-through silencioso (sin log) aplica solo a `hooks/_lib/**`, tests/docs/templates/meta y paths fuera del repo. Bypass legítimo: crear primero `hooks/tests/test_<x>.py` o `<path>.test.ts` con un test que falle (RED), luego escribir la implementación.
- El hook `pre-pr-gate.py` **ya está vivo** desde D4: PreToolUse(Bash) blocker sobre `gh pr create` únicamente. Resuelve base con `git merge-base HEAD main` y calcula archivos tocados con `git diff --name-only <base> HEAD`. Bloquea con exit 2 cuando ese diff no contiene los docs exigidos (required `ROADMAP.md` + `HANDOFF.md`; conditional por prefijo: `generator/**` | `hooks/**` excl. `hooks/tests/` | `.claude/patterns/**` → `docs/ARCHITECTURE.md`; `skills/**` → `.claude/rules/skills-map.md`). Skip advisory (pass-through + log explícito en hook log, no en phase-gates) en `main` / `master` / HEAD detached / cwd no-git / main borrada localmente / `git diff` subprocess falla (`diff_files() is None`). Empty diff (`[]`) → deny dedicado con reason `empty PR`, separado textualmente del reason docs-sync. 3 entradas advisory `deferred` (skills_required / ci_dry_run_required / invariants_check) se loguean en cada decisión real como scaffold activable sin cambio de shape cuando sus ramas dedicadas aporten sustrato. Reglas hardcoded (mirror de `policy.yaml.lifecycle.pre_pr.docs_sync_required` + `docs_sync_conditional`); divergencia deliberada D4: la lista `hooks/**` de la policy es uniforme, el hook excluye `hooks/tests/` — convergencia diferida a la rama policy-loader. Migración a parser declarativo en esa misma rama (junto con los paths hardcoded de D3).
- **Loader declarativo `hooks/_lib/policy.py`** vivo desde D5b: los tres hooks D3/D4/D5 ya **no** hardcodean policy — leen via `docs_sync_rules()` / `post_merge_trigger()` / `pre_write_rules()`. Failure mode canónico (c.2): `policy.yaml` ausente o corrupto → loader devuelve `None` → hook degrada a pass-through advisory con `status: policy_unavailable` en su propio log. Nunca deny blind. Consumo único (stdlib + `pyyaml==6.0.2`, primera dependencia no-stdlib en `hooks/_lib/`, justificada en kickoff D5b). Ver `.claude/rules/hooks.md § Policy loader`.
- **Drift temporal meta-repo ↔ template** abierto tras D5b: `policy.yaml` (meta-repo) tiene el shape nuevo (`pre_write.enforced_patterns` + `docs_sync_conditional.hooks/**` con `excludes: ["hooks/tests/**"]`); `templates/policy.yaml.hbs` y el renderer `generator/renderers/policy.ts` **no** — un proyecto generado hoy con `pos` emite un `policy.yaml` con el shape previo. Reconciliación con stub abierto en `refactor/template-policy-d5b-migration` (ver [MASTER_PLAN.md § Rama F3b](MASTER_PLAN.md)) — no bloquea F4 ni Fase G; F3 lo evade vía overlays por escenario en `bin/_selftest.py`.
- **Drift temporal meta-repo ↔ template** ✅ **cerrado** en `refactor/template-policy-d5b-migration` (sub-rama post-F4): `templates/policy.yaml.hbs` migrado al shape contractual con loader (`pre_write.enforced_patterns: []` + `pre_pr.docs_sync_conditional: []` + `pre_compact.persist` con 3 items canónicos + `post_merge.skills_conditional[0].trigger` con globs genéricos conservadores; `skills_allowed` permanece omitido por diseño). Lockdown vía `bin/tests/test_template_loader_contract.py` que corre los 5 accessors reales del loader sobre output del generator real (cli-tool / nextjs-app / agent-sdk). Overlays F3 D4+D5 removidos; D3+D6 mantienen overlays mínimos por A1+A2.
- El hook `post-action.py` **ya está vivo** desde D5: PostToolUse(Bash) hook **non-blocking** (exit 0 siempre; no emite `permissionDecision`). Detección jerárquica 2 tiers — Tier 1 (`shlex.split`): matcher A `git merge <ref>` (excluye `--abort/--quit/--continue/--skip`), matcher C `git pull` (excluye `--rebase/-r`). Tier 2 (`git reflog HEAD -1 --format=%gs`): confirma `"merge "` (A) o `"pull:" | "pull "` y no `"pull --rebase"` (C). `gh pr merge` (matcher B) descartado en Fase -1 por ausencia de `tool_response.exit_code` garantizado. Con ambos tiers confirmados: `git diff --name-only HEAD@{1} HEAD` + `fnmatch` contra `TRIGGER_GLOBS` mirror de `policy.yaml.lifecycle.post_merge.skills_conditional[0]` (`generator/lib/**`, `generator/renderers/**`, `hooks/**`, `skills/**`, `templates/**/*.hbs`), respetando `SKIP_IF_ONLY_GLOBS` (`docs/**`, `*.md`, `.claude/patterns/**`) y `MIN_FILES_CHANGED = 2`. Si matchea, emite `hookSpecificOutput.additionalContext` sugiriendo `/pos:compound` (4 líneas, cap 3 paths + `(+N more)`). **Nunca dispatcha la skill** — advisory-only; D5 sólo sugiere, E3a la entrega. Double log: `post-action.jsonl` (4 status distinguidos: `tier2_unconfirmed`, `diff_unavailable`, `confirmed_no_triggers`, `confirmed_triggers_matched`) + `phase-gates.jsonl` (evento `post_merge`, sólo en los dos status confirmed — los advisory tier2/diff no cruzan la puerta del lifecycle). Pass-throughs (Tier 1 no matchea) NO loguean. Reuso `_lib/`: `append_jsonl` + `now_iso`. Hardcode mirror de `policy.yaml` (segunda repetición tras D4) — regla #7 CLAUDE.md **cumplida dos veces** para el parser declarativo, precondición ready para la rama policy-loader.
- El hook `pre-compact.py` **ya está vivo** desde D6: PreCompact **informative** (shape D2) — exit 0 siempre, nunca `permissionDecision`. Lee `pre_compact_rules(cwd).persist` del loader (D5b) y emite `additionalContext` con checklist de items a persistir antes del compact. Failure mode canónico (c.2): policy ausente o sección `lifecycle.pre_compact` ausente → loader devuelve `None` → hook emite contexto informativo mínimo que señala policy no disponible + log `status: policy_unavailable`. Safe-fail informative ante stdin corrupto: contexto degradado que señala el error de payload + log `status: payload_error`. Double log: `pre-compact.jsonl` siempre; `phase-gates.jsonl` evento `pre_compact` **solo** en happy path. (Wording exacto del contexto no es contrato — no se citan strings; ver `hooks/pre-compact.py` y sus tests si algún consumidor necesita inspeccionarlo.)
- El hook `stop-policy-check.py` **ya está vivo** desde D6 como **scaffold activable** — shape D1 blocker (safe-fail canónico deny en payload malformado), pero **sin enforcement en producción hoy**. Lee `skills_allowed_list(cwd)` como tri-estado: `None` → `status: deferred` + pass-through silencioso (sección `skills_allowed` ausente del `policy.yaml` del meta-repo hoy); `SKILLS_ALLOWED_INVALID` → `status: policy_misconfigured` + pass-through (clave presente pero mal formada — **observable, NO silenciosa**: un typo en la policy ya no apaga enforcement como si fuera deferred); `()` → explicit deny-all; `tuple[str, ...]` poblada → enforcement live. Las invocaciones se leen de `.claude/logs/skills.jsonl` **filtradas por el `session_id`** del payload Stop actual (entradas de sesiones anteriores o sin `session_id` se ignoran); sin `session_id` en el payload → deny safe-fail (no se puede scopiar enforcement). Double log solo en decisiones reales (`deferred`/`policy_misconfigured`/`policy_unavailable` van solo al hook log, no cruzan `phase-gates.jsonl`). Framing **anti-sobrerrepresentación**: hoy el hook NO protege nada en producción; la entrega D6 aporta el shape y la suite de tests que valida el contrato — el switch-on real llega cuando una skill poblada declare su allowlist.
Expand Down Expand Up @@ -135,9 +136,8 @@ Hasta F1 el plugin reusaba subagents built-in; desde F2 los críticos son propio

## 9. Próxima rama

Fase F **cerrada** tras F4. Carry-overs abiertos:
Fase F + drift template **cerrados** tras F4 + `refactor/template-policy-d5b-migration`. Carry-overs abiertos:

- **`refactor/template-policy-d5b-migration`** — rama propia para migrar `templates/policy.yaml.hbs` + renderer + snapshots al shape post-D5b (drift documentado desde D5b/F3; F3 lo evade vía overlays por escenario en `bin/_selftest.py`). No bloquea Fase G.
- **`feat/fx-knowledge-plane-plan`** (Fase G entry-point, opcional): docs-only — abre Fase G en MASTER_PLAN.md como capa knowledge plane opt-in. Sin fecha — el usuario decide cuándo arrancar.
- **Activación del marketplace público**: cuando se decida crear `javiAI/pos-marketplace`, seguir el runbook de [docs/RELEASE.md § Mirror al marketplace público](docs/RELEASE.md) (3 pasos: crear repo + `gh variable set POS_MARKETPLACE_REPO` + `gh secret set POS_MARKETPLACE_TOKEN`). El próximo release abre PR automático contra el repo público.
- **Skills `/pos:pr-description` + `/pos:release`**: diferidas por regla #7 CLAUDE.md (≥2 repeticiones documentadas). F4 entrega flow manual; cuando se observe el patrón ≥2 veces, extraer.
Expand Down
Loading
Loading