@damiangilgonzalez/harness 1.3.15 → 1.3.17

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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.3.15",
2
+ "version": "1.3.17",
3
3
  "counts": {
4
4
  "skills": 294,
5
5
  "agents": 34,
@@ -3805,7 +3805,7 @@
3805
3805
  },
3806
3806
  {
3807
3807
  "id": "pr",
3808
- "description": "Use when writing a PR body: a visual summary, before/after evidence, and the merge danger (one-way or two-way door, blast radius).",
3808
+ "description": "Use when writing a PR body or opening a PR: a visual summary, before/after evidence, the merge danger (one-way or two-way door, blast radius), and a title checked against the repo's CI title rule before `gh pr create`.",
3809
3809
  "tags": [
3810
3810
  "pull-request",
3811
3811
  "review",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damiangilgonzalez/harness",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -354,6 +354,11 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
354
354
  if (target) {
355
355
  const paths = targetPaths(target, undefined, cwd);
356
356
  const rel = (abs) => relative(cwd, abs).split(sep).join('/');
357
+ // `.claude/worktrees/` is where Claude Code puts the checkout of each isolated branch. Nothing
358
+ // ignored it, so every file of every worktree showed up as untracked: noise in `git status`, a
359
+ // `git add .` away from being committed, and the memory hook fingerprinted them all on each turn
360
+ // (54 s on a project with 964 of them, against a 5 s timeout).
361
+ if (target === 'claude') wanted.push(CLAUDE_WORKTREES);
357
362
  for (const id of Object.keys(readState(paths.stateFile).skills || {})) wanted.push(rel(paths.skillDir(id)));
358
363
  // The per-file ledger is derived AND platform-shaped (symlink here, real copy on
359
364
  // Windows), so committing it puts two incompatible inventories of one thing in one
@@ -530,7 +535,7 @@ export async function uninstall({ skillIds = [], agentIds = [], target, home, cw
530
535
  export function removeTargetInstall({ target, home, cwd = process.cwd() }) {
531
536
  const paths = targetPaths(target, home, cwd);
532
537
  const state = readState(paths.stateFile);
533
- const ignored = [paths.stateFile];
538
+ const ignored = [paths.stateFile, ...(target === 'claude' ? [join(cwd, CLAUDE_WORKTREES)] : [])];
534
539
  const ownedIgnoreEntries = new Set(state.ignoreEntriesAdded || []);
535
540
  const owned = [
536
541
  ...Object.values(state.skills || {}).flatMap((entry) => entry.files || []),
@@ -659,6 +664,7 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
659
664
  // own knowledge — kept unless `withDocs` is set. Returns the paths touched.
660
665
  // Note: backups live under `.rsc/backups/`, which this removes — so purge does not
661
666
  // snapshot (a pre-purge backup would delete itself). It is the deliberate escape hatch.
667
+ const CLAUDE_WORKTREES = '.claude/worktrees/';
662
668
  const RSC_IGNORE_HEADER = '# rsc local state (hooks, seals, logs) and managed skill links — machine-local';
663
669
 
664
670
  // Merge consecutive rsc blocks into the first: drop a repeated header and the blank seam before it
@@ -38,6 +38,8 @@ Busca el spec de origen, en este orden:
38
38
 
39
39
  Cualquier cosa en el repo que documente cómo debe escribirse el código, como `CODING_STANDARDS.md` o `CONTRIBUTING.md`. (En este proyecto: `CLAUDE.md` y `docs/CARTA-MAGNA.md`.)
40
40
 
41
+ Si no encuentras ninguna, **no lo calles**: el eje de Estándares solo tendrá el smell baseline, y el reporte final debe decirlo en su primera línea ("Estándares: el repo no documenta estándares; solo se revisó el smell baseline"). Ofrece generar un `CODING_STANDARDS.md` inicial a partir de las convenciones que se observan en el repo (nombres, estructura, manejo de errores, tests). No lo escribas sin un sí del usuario.
42
+
41
43
  Además de lo que el repo documente, el eje de Estándares siempre lleva el **smell baseline** de abajo — un conjunto fijo de code smells de Fowler (_Refactoring_, cap. 3) que aplica incluso cuando el repo no documenta nada. Dos reglas lo condicionan:
42
44
 
43
45
  - **El repo manda.** Un estándar documentado del repo siempre gana; donde el repo avale algo que el baseline marcaría, suprime el smell.
@@ -74,7 +76,7 @@ Envía un único mensaje con dos llamadas a la herramienta `Agent`. Usa el subag
74
76
  - La ruta o el contenido recuperado del spec.
75
77
  - El encargo: "Reporta: (a) requisitos que el spec pedía y que faltan o están parciales; (b) comportamiento en el diff que no se pidió (scope creep); (c) requisitos que parecen implementados pero cuya implementación parece incorrecta. Cita la línea del spec por cada hallazgo. Menos de 400 palabras."
76
78
 
77
- Si falta el spec, sáltate el sub-agente de Spec y anótalo en el reporte final.
79
+ Si falta el spec, sáltate el sub-agente de Spec y anótalo en el reporte final. Si faltan las fuentes de estándares, lanza igual el de Estándares con el baseline, y anótalo también junto con la oferta del `CODING_STANDARDS.md`.
78
80
 
79
81
  ### 5. Verificar los graves antes de presentarlos
80
82
 
@@ -80,6 +80,13 @@ de las skills de diseno. Las mecanicas del framework son de `angular`, `react` o
80
80
  | Un flujo pierde usuarios a mitad | `leyes-de-retencion` -> `mejor-redaccion` -> `mejor-accesibilidad` |
81
81
  | Solo pulir lo que ya hay | `revision-interfaz` |
82
82
 
83
+ ## Cuando cuenta como hecho
84
+
85
+ Un cambio visual no esta hecho sin una **captura del entorno donde el usuario lo va a probar**
86
+ (su navegador, su URL, su viewport; no un test ni un render aislado si el usuario no prueba ahi).
87
+ Que compile y pasen los tests no dice nada de como se ve. Si no puedes abrirlo, dilo y no lo
88
+ declares terminado: "no lo he mirado en un navegador" es un trabajo sin acabar, no un matiz.
89
+
83
90
  ## Donde encaja en la metodologia
84
91
 
85
92
  Esta cadena vive **dentro** del paso de construir, no lo sustituye. El trabajo sigue llegando con
@@ -14,6 +14,8 @@ Usa TDD (superpowers:test-driven-development) donde sea posible, en las costuras
14
14
 
15
15
  Ejecuta el typechecking con regularidad, archivos de test sueltos con regularidad, y la suite de tests completa una vez al final.
16
16
 
17
+ Si el trabajo cambia algo visible, no está hecho hasta que lo hayas mirado: adjunta una captura del entorno donde el usuario lo va a probar. Si no puedes abrirlo, dilo y no lo des por terminado.
18
+
17
19
  Al terminar, usa /code-review para revisar el trabajo.
18
20
 
19
21
  Commitea tu trabajo en la rama actual.
@@ -30,9 +30,11 @@ Lanza los **subagentes implementadores** en segundo plano siempre que se pueda,
30
30
  4. Usa **subagentes implementadores** para implementar cada ticket, cada uno en su propio worktree y en su propia rama (`superpowers:using-git-worktrees`). Cada subagente implementador:
31
31
  - confirma que su worktree parte de la rama de integración antes de empezar, y se reinicia sobre ella si no;
32
32
  - llama a la herramienta Skill con `superpowers:test-driven-development` para construir el ticket;
33
- - fusiona la punta de la rama de integración en su propia rama antes de decir que ha terminado.
33
+ - fusiona la punta de la rama de integración en su propia rama antes de decir que ha terminado;
34
+ - si el ticket cambia algo visible, adjunta una captura del entorno donde el usuario lo va a probar;
35
+ - entrega un informe final de **150 palabras como máximo**: qué cambió, ficheros, resultado de los tests y bloqueos. El detalle va al commit o al ticket, no a la respuesta. Pon este tope literal en el prompt de cada subagente: cada entrega entra entera en tu contexto, y decenas de entregas largas te hacen olvidar lo que acaba de decir el usuario.
34
36
 
35
- 5. Cuando un **subagente implementador** termina, fusiona su trabajo en la rama de integración con un **subagente fusionador**.
37
+ 5. Cuando un **subagente implementador** termina, revisa su entrega antes de fusionar. Si toca algo visible y no trae captura, o dice que no lo ha mirado en un navegador, **no está hecho**: devuélvesela para que lo mire. Si la entrega es buena, fusiona su trabajo en la rama de integración con un **subagente fusionador**.
36
38
 
37
39
  6. Si eso cambia la **frontera** de tickets disponibles, lanza más **subagentes implementadores** para los tickets nuevos. Así se mantiene la máxima concurrencia.
38
40
 
@@ -63,6 +63,9 @@ The dial is spoken, not configured: when the user says "explícame más / menos"
63
63
  tanto" or "enséñame", update `accompaniment_level` in the profile, confirm in one line, and apply
64
64
  the new depth from this turn on.
65
65
 
66
+ When a front closes (PR merged or opened, ticket done), `➡️` offers `handoff` and a fresh session
67
+ before more work in this one; see the contract.
68
+
66
69
  At a real fork, ask. Deciding alone is faster and wrong — the fork is the moment the person's
67
70
  judgment is worth more than yours.
68
71
 
@@ -28,6 +28,15 @@ Read `docs/wiki/harness/user-profile.md`. Two fields combine: `accompaniment_lev
28
28
 
29
29
  `technical_level` is orthogonal (plain language for `non-technical` even at L0; skip 101s for `technical` even at L3). Missing profile → assume `non-technical` + **L3** and offer to set the dial.
30
30
 
31
+ ## When a front closes
32
+
33
+ A front ("frente") closes when its PR is merged or opened, or its ticket is done. That turn's `➡️`
34
+ proposes `handoff` and a new session as the first option, instead of starting the next front here.
35
+ A session that runs for days through several `/compact` loses what the user just said: each
36
+ compaction and each subagent hand-back pushes the latest instructions further out. A handoff
37
+ document carries what matters; the new session starts with room for it. If the user prefers to
38
+ continue, continue — offer once per front, not every turn.
39
+
31
40
  ## Division of labor
32
41
 
33
42
  - `orient` guides the **person** (this contract).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pr
3
- description: "Use when writing a PR body: a visual summary, before/after evidence, and the merge danger (one-way or two-way door, blast radius)."
3
+ description: "Use when writing a PR body or opening a PR: a visual summary, before/after evidence, the merge danger (one-way or two-way door, blast radius), and a title checked against the repo's CI title rule before `gh pr create`."
4
4
  tags: ["pull-request", "review", "evidence", "risk"]
5
5
  profiles: [core, ui, full]
6
6
  origin: damiangil
@@ -171,3 +171,17 @@ La evidencia basada en ejecución va justo detrás: resultados de tests, salida
171
171
  Di si es una puerta de un sentido o de dos sentidos. Por una puerta de dos sentidos se puede volver atrás; por una de un sentido, no. Una PR que es barata de revertir tiene menos riesgo. Los cambios que implican acciones destructivas o decisiones difíciles de revertir son puertas de un sentido.
172
172
 
173
173
  El radio de impacto es el alcance posible de los efectos que introduce la PR. Considera todas las posibilidades. Ejemplos: saltos de layout, roturas para quien consume el código, el comportamiento en móvil, etc.
174
+
175
+ ## Título
176
+
177
+ Antes de `gh pr create`, comprueba que el título pasa el check de CI del repo. Un título mal formado tumba la PR aunque el código esté bien.
178
+
179
+ 1. Busca la regla: `grep -rnE "semantic-pull-request|pull_request\.title|commitlint" .github/workflows/` y, si aparece commitlint, su configuración (`commitlint.config.*`, `.commitlintrc*`, `package.json`). En `amannn/action-semantic-pull-request` mira `types`, `scopes`, `requireScope` y `subjectPattern`.
180
+ 2. Saca la regex o los tipos permitidos y prueba el título:
181
+
182
+ ```bash
183
+ node -e 'const re=/^(feat|fix|docs|refactor|test|chore)(\([\w-]+\))?!?: .+/u; const t=process.argv[1]; console.log(re.test(t)?"OK":"FALLA: "+t)' "fix(pr): valida el título"
184
+ ```
185
+
186
+ Si el repo antepone un gitmoji u otro prefijo, inclúyelo en la regex tal como lo pida la regla.
187
+ 3. Si falla, corrige el título antes de crear la PR. Si no hay ningún check de título, sigue la convención de los commits recientes (`git log --oneline -10`).
@@ -11,6 +11,8 @@ should_trigger:
11
11
  why: "Disparador directo de pr: peligro de fusión y radio de impacto."
12
12
  - prompt: "¿Qué pongo en la PR para que se revise rápido?"
13
13
  why: "Disparador directo de pr."
14
+ - prompt: "Abre la PR, pero que el título no vuelva a tumbar el CI."
15
+ why: "Disparador directo de pr: el título se valida contra el check del repo."
14
16
 
15
17
  should_not_trigger:
16
18
  - prompt: "Revisa el código de esta PR."
@@ -31,3 +33,7 @@ capability:
31
33
  must_include:
32
34
  - "Peligro de fusión"
33
35
  - "Radio de impacto"
36
+ - scenario: "El repo tiene un check de título de PR en sus workflows."
37
+ must_include:
38
+ - ".github/workflows"
39
+ - "gh pr create"
package/targets/claude.js CHANGED
@@ -9,6 +9,15 @@ const HERE = dirname(fileURLToPath(import.meta.url));
9
9
  // so a `Bash`-only matcher let `git push --force` or a commit without gitmoji through unasked.
10
10
  const SHELL_TOOLS = 'Bash|PowerShell';
11
11
 
12
+ // Mods de la terminal de Claude Code que el harness deja activados en cada proyecto.
13
+ // `cache-tax` mantiene caliente la caché del prompt en las pausas, y eso gasta tokens.
14
+ export const CLAUDE_CODE_MODS = Object.freeze([
15
+ { id: 'skins@hellosverre-mods', marketplace: 'hellosverre-mods', repo: 'hellosverre/claude-skins' },
16
+ { id: 'savvy-progress@claude-kit', marketplace: 'claude-kit', repo: 'johnnyvizz/claude-kit' },
17
+ { id: 'filetree@claude-code-filetree', marketplace: 'claude-code-filetree', repo: 'data-goblin/claude-code-filetree' },
18
+ { id: 'cache-tax@claude-code-mods', marketplace: 'claude-code-mods', repo: 'karanb192/claude-code-mods' },
19
+ ]);
20
+
12
21
  export function writeSkill(id, fromDir, toPath) {
13
22
  // Migrate away from the legacy nested layout (.claude/skills/rsc/<id>) that
14
23
  // Claude Code never discovered — it only reads .claude/skills/<name>/SKILL.md.
@@ -235,6 +244,16 @@ export function wireHook(paths, sourceMd, policy = {}) {
235
244
  for (const id of policy.plugins) settings.enabledPlugins[id] = true;
236
245
  }
237
246
 
247
+ // Los mods de Claude Code van en cada install y update, se use el flujo o no. Con el marketplace
248
+ // en `extraKnownMarketplaces`, Claude Code ofrece instalarlos al abrir el proyecto. Un `false`
249
+ // que alguien puso a mano es una decisión suya y se respeta.
250
+ settings.extraKnownMarketplaces ||= {};
251
+ settings.enabledPlugins ||= {};
252
+ for (const { id, marketplace, repo } of CLAUDE_CODE_MODS) {
253
+ settings.extraKnownMarketplaces[marketplace] ||= { source: { source: 'github', repo } };
254
+ if (settings.enabledPlugins[id] !== false) settings.enabledPlugins[id] = true;
255
+ }
256
+
238
257
  // Danger guard: its own decision, not part of the code-hook block above (#273). `init` promises it
239
258
  // to non-technical and mixed users whatever the project is, and a plan that predates the field is
240
259
  // read as "yes": a non-technical operations install gets the guard it was missing on its next sync,
@@ -46,9 +46,10 @@ function cleanId(value, prefix = 'session') {
46
46
  return `${prefix}-${hash}`;
47
47
  }
48
48
 
49
- function git(cwd, args) {
49
+ function git(cwd, args, input) {
50
50
  try {
51
- return { ok: true, out: execFileSync('git', args, { windowsHide: true, cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 2 * 1024 * 1024 }).trim() };
51
+ const stdin = input === undefined ? 'ignore' : 'pipe';
52
+ return { ok: true, out: execFileSync('git', args, { windowsHide: true, cwd, input, encoding: 'utf8', stdio: [stdin, 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024 }).trim() };
52
53
  } catch {
53
54
  return { ok: false, out: '' };
54
55
  }
@@ -162,7 +163,15 @@ function parseStatus(output) {
162
163
  return files;
163
164
  }
164
165
 
166
+ // One `git` for every file, not one per file. Spawning a process costs ~25 ms on Windows, and this
167
+ // runs on every hook event: a few hundred dirty files were enough to blow the 5 s hook timeout.
168
+ // `--stdin-paths` stops at the first path it cannot read (a deleted file), so on failure each file
169
+ // is hashed on its own, which keeps the old per-file `null` for the unreadable ones.
165
170
  function fingerprintFiles(worktree, files) {
171
+ if (!files.length) return {};
172
+ const batch = git(worktree, ['hash-object', '--no-filters', '--stdin-paths'], `${files.join('\n')}\n`);
173
+ const hashes = batch.ok ? batch.out.split('\n') : [];
174
+ if (hashes.length === files.length) return Object.fromEntries(files.map((path, i) => [path, hashes[i]]));
166
175
  return Object.fromEntries(files.map((path) => {
167
176
  const result = git(worktree, ['hash-object', '--no-filters', '--', path]);
168
177
  return [path, result.ok ? result.out : null];
@@ -305,7 +314,8 @@ export function capture(input = {}) {
305
314
  const recordPath = join(store.root, 'sessions', `${storageId}.json`);
306
315
  let anchor = readJson(anchorPath);
307
316
  let existing = readJson(recordPath);
308
- const firstSnapshot = snapshot(here, null);
317
+ // The first snapshot only seeds a new anchor; an existing one already has its baseline.
318
+ const firstSnapshot = anchor ? null : snapshot(here, null);
309
319
  if (!anchor) {
310
320
  anchor = {
311
321
  sessionId,
@@ -318,7 +328,11 @@ export function capture(input = {}) {
318
328
  }
319
329
  if (input.event === 'start' && !existing) return { record: null, path: null, notice: null, compactionHint: false };
320
330
 
321
- const repo = snapshot(here, anchor.baselineHead);
331
+ // Against its own HEAD as baseline there is nothing committed to diff, so a fresh anchor's
332
+ // snapshot already is the repo state: reuse it instead of walking the tree a second time.
333
+ const repo = firstSnapshot && anchor.baselineHead === firstSnapshot.head
334
+ ? firstSnapshot
335
+ : snapshot(here, anchor.baselineHead);
322
336
  // Count what CHANGED, not which tool changed it. `editDelta` is +1 per Edit/Write tool event, and
323
337
  // on its own it was blind to everything done through the shell — a heredoc, `sed -i`, a script, a
324
338
  // formatter — which is how automated sessions work. The session that found this had touched dozens