@damiangilgonzalez/harness 1.3.10 → 1.3.11

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.10",
2
+ "version": "1.3.11",
3
3
  "counts": {
4
4
  "skills": 291,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@damiangilgonzalez/harness",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
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": {
package/targets/claude.js CHANGED
@@ -4,6 +4,9 @@ import { fileURLToPath } from 'node:url';
4
4
  import { linkOrCopy } from './index.js';
5
5
 
6
6
  const HERE = dirname(fileURLToPath(import.meta.url));
7
+ // Shell tools the PreToolUse guards listen on. On Windows the primary shell is the PowerShell tool,
8
+ // so a `Bash`-only matcher let `git push --force` or a commit without gitmoji through unasked.
9
+ const SHELL_TOOLS = 'Bash|PowerShell';
7
10
 
8
11
  export function writeSkill(id, fromDir, toPath) {
9
12
  // Migrate away from the legacy nested layout (.claude/skills/rsc/<id>) that
@@ -142,7 +145,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
142
145
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
143
146
  (e) => !hookWiringOf(e).includes('.rsc/ship-guard.'),
144
147
  );
145
- settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: sgCmd }] });
148
+ settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: sgCmd }] });
146
149
 
147
150
  // Danger guard: a PreToolUse(Bash) hook that DENIES irreversible foot-gun commands
148
151
  // (rm -rf, git push --force, DROP/TRUNCATE, DELETE/UPDATE without WHERE, dd to /dev,
@@ -155,7 +158,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
155
158
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
156
159
  (e) => !hookWiringOf(e).includes('.rsc/danger-guard.'),
157
160
  );
158
- settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: dgCmd }] });
161
+ settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: dgCmd }] });
159
162
 
160
163
  // Gitmoji guard: a PreToolUse(Bash) hook that DENIES a `git commit` whose message
161
164
  // carries no gitmoji (gitmoji.dev) in front of the Conventional Commits header. The
@@ -170,7 +173,7 @@ export function wireHook(paths, sourceMd, policy = {}) {
170
173
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
171
174
  (e) => !hookWiringOf(e).includes('.rsc/gitmoji-guard.'),
172
175
  );
173
- settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: gmCmd }] });
176
+ settings.hooks.PreToolUse.push({ matcher: SHELL_TOOLS, hooks: [{ type: 'command', command: gmCmd }] });
174
177
 
175
178
  // New-feature gate: a UserPromptSubmit hook that re-injects the SDD new-feature gate as
176
179
  // the most-recent instruction on every user turn. SessionStart injects the full gate (the
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // rsc Danger guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash)
2
+ // rsc Danger guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash|PowerShell)
3
3
  // as `node ...` so it runs on every platform including Windows.
4
4
  // argv[2] = absolute project root stdin = PreToolUse hook JSON
5
5
  //
@@ -46,7 +46,7 @@ if (technicalLevel() === 'technical') allow();
46
46
  // Only Bash commands can be dangerous here.
47
47
  let input = {};
48
48
  try { input = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { allow(); }
49
- if ((input.tool_name || input.toolName) !== 'Bash') allow();
49
+ if (!['Bash', 'PowerShell'].includes(input.tool_name || input.toolName)) allow();
50
50
  const cmd = input.tool_input?.command || input.toolInput?.command || '';
51
51
  if (typeof cmd !== 'string' || !cmd) allow();
52
52
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // rsc Gitmoji guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash)
2
+ // rsc Gitmoji guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash|PowerShell)
3
3
  // as `node ...` so it runs on every platform including Windows.
4
4
  // argv[2] = absolute project root stdin = PreToolUse hook JSON
5
5
  //
@@ -230,7 +230,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
230
230
 
231
231
  let input = {};
232
232
  try { input = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { allow(); }
233
- if ((input.tool_name || input.toolName) !== 'Bash') allow();
233
+ if (!['Bash', 'PowerShell'].includes(input.tool_name || input.toolName)) allow();
234
234
  const command = input.tool_input?.command || input.toolInput?.command || '';
235
235
  if (typeof command !== 'string' || !command) allow();
236
236
 
@@ -8,6 +8,7 @@
8
8
  import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
9
9
  import { join, resolve } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
+ import { spawnSync } from 'node:child_process';
11
12
  import { claimOnce, readHookInput } from './hook-once.mjs';
12
13
 
13
14
  const STALE_AUDIT_DAYS = 14; // periodic skill-audit cadence (kept in sync with scripts/audit.js)
@@ -27,6 +28,19 @@ const emitBody = hook.session_id
27
28
 
28
29
  const has = (...p) => existsSync(join(root, ...p));
29
30
 
31
+ // The npm lookup (up to 1.5 s) starts now and runs while the local checks below do their work;
32
+ // the update block at the end only awaits it.
33
+ const latestVersion = process.env.RSC_NO_UPDATE_CHECK ? Promise.resolve(null)
34
+ : process.env.RSC_LATEST ? Promise.resolve(process.env.RSC_LATEST)
35
+ : (async () => {
36
+ const ctrl = new AbortController();
37
+ const timer = setTimeout(() => ctrl.abort(), 1500);
38
+ try {
39
+ const res = await fetch('https://registry.npmjs.org/@damiangilgonzalez%2fharness/latest', { signal: ctrl.signal });
40
+ return (await res.json()).version;
41
+ } catch { return null; } finally { clearTimeout(timer); }
42
+ })();
43
+
30
44
  // Is a Context7 MCP server configured for this project (.mcp.json → mcpServers.context7)?
31
45
  function hasContext7() {
32
46
  try {
@@ -221,10 +235,24 @@ if (has('.git')) {
221
235
  // off, so a second check could never fail and would only look like protection (P2). Test 36 holds
222
236
  // the behaviour end to end, through this hook.
223
237
  const W = await import('./worktree-reaper.mjs');
224
- {
238
+ // Classifying spawns several git processes per worktree (1.2 s with a dozen of them, on every
239
+ // session start). A verdict only moves when a worktree HEAD, the trunk or the opt-out moves, so
240
+ // those make the fingerprint and an unchanged one reuses the last notice. Safe because `reap`
241
+ // re-classifies before it removes anything.
242
+ const trunk = W.resolveTrunk(root);
243
+ const tip = trunk ? spawnSync('git', ['-C', root, 'rev-parse', trunk], { encoding: 'utf8' }).stdout : '';
244
+ const list = spawnSync('git', ['-C', root, 'worktree', 'list', '--porcelain'], { encoding: 'utf8' }).stdout;
245
+ const fingerprint = `${W.isCleanupEnabled(root)}\n${trunk}\n${tip}\n${list}`;
246
+ const cacheFile = join(root, '.rsc', 'worktree-sweep.json');
247
+ let cached = null;
248
+ try { cached = JSON.parse(readFileSync(cacheFile, 'utf8')); } catch { /* first run */ }
249
+ if (cached && cached.fingerprint === fingerprint && typeof cached.text === 'string') {
250
+ process.stdout.write(cached.text);
251
+ } else {
252
+ let text = '';
225
253
  const candidates = W.classifyWorktrees(root).filter((c) => c.verdict !== 'skip');
226
254
  if (candidates.length) {
227
- process.stdout.write(`
255
+ text = `
228
256
  ===== rsc worktree cleanup =====
229
257
  ${candidates.length} worktree(s) hold work that is already in the trunk:
230
258
  ${candidates.map((c) => W.summarize(c, root)).join('\n')}
@@ -234,8 +262,13 @@ ACTION: offer to retire them in ONE line and wait for a yes. On a yes run:
234
262
  A yes in bulk covers only the safe ones; anything marked \`ask\` is confirmed on its own.
235
263
  A no holds for this session. Permanent off: .rsc/.no-worktree-cleanup
236
264
  ================================
237
- `);
265
+ `;
238
266
  }
267
+ process.stdout.write(text);
268
+ try {
269
+ mkdirSync(join(root, '.rsc'), { recursive: true });
270
+ writeFileSync(cacheFile, JSON.stringify({ fingerprint, text }) + '\n');
271
+ } catch { /* unwritable → scan again next session */ }
239
272
  }
240
273
  } catch { /* reaper missing or git unhappy → say nothing; this is never worth breaking startup for */ }
241
274
  }
@@ -257,14 +290,7 @@ function isNewer(a, b) {
257
290
  if (!process.env.RSC_NO_UPDATE_CHECK) {
258
291
  try {
259
292
  const installed = readFileSync(join(root, '.rsc', '.version'), 'utf8').trim();
260
- let latest = process.env.RSC_LATEST;
261
- if (!latest) {
262
- const ctrl = new AbortController();
263
- const timer = setTimeout(() => ctrl.abort(), 1500);
264
- const res = await fetch('https://registry.npmjs.org/@damiangilgonzalez%2fharness/latest', { signal: ctrl.signal });
265
- clearTimeout(timer);
266
- latest = (await res.json()).version;
267
- }
293
+ const latest = await latestVersion;
268
294
  if (installed && latest && isNewer(latest, installed)) {
269
295
  process.stdout.write(`
270
296
  ===== rsc update available =====
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // rsc Ship guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash)
2
+ // rsc Ship guard (claude). Wired by targets/claude.js onto PreToolUse (matcher Bash|PowerShell)
3
3
  // as `node ...` so it runs on every platform including Windows.
4
4
  // argv[2] = absolute project root stdin = PreToolUse hook JSON
5
5
  //
@@ -39,7 +39,7 @@ if (!existsSync(join(root, '.git'))) allow();
39
39
  // Read the tool call. Only Bash commands can move branches.
40
40
  let input = {};
41
41
  try { input = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { allow(); }
42
- if ((input.tool_name || input.toolName) !== 'Bash') allow();
42
+ if (!['Bash', 'PowerShell'].includes(input.tool_name || input.toolName)) allow();
43
43
  const command = input.tool_input?.command || input.toolInput?.command || '';
44
44
  if (typeof command !== 'string' || !command) allow();
45
45
 
@@ -13,6 +13,7 @@
13
13
  // Fail-open (never blocks a turn). Opt out per project with .rsc/.no-feature-gate.
14
14
  import { existsSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
+ import { homedir } from 'node:os';
16
17
  import { claimOnce, readHookInput, WORKFLOW_GATE_TEXT } from './hook-once.mjs';
17
18
 
18
19
  const root = process.argv[2] || process.cwd();
@@ -28,4 +29,8 @@ const hook = readHookInput();
28
29
  const turnKey = hook.prompt_id || (hook.prompt ? `p${hook.prompt.length}:${hook.prompt.slice(0, 64)}` : null);
29
30
  if (hook.session_id && turnKey && !claimOnce(`up:${hook.session_id}:${turnKey}`)) process.exit(0);
30
31
 
31
- process.stdout.write(WORKFLOW_GATE_TEXT);
32
+ // The gate routes screen work through `flujo-interfaz`, but that skill is optional. Naming a skill
33
+ // that is not installed sends the agent looking for something that does not exist, so the line only
34
+ // ships when the skill is present in this project or in the user scope.
35
+ const hasFlujo = [root, homedir()].some((r) => existsSync(join(r, '.claude', 'skills', 'flujo-interfaz', 'SKILL.md')));
36
+ process.stdout.write(hasFlujo ? WORKFLOW_GATE_TEXT : WORKFLOW_GATE_TEXT.split('\n').filter((l) => !l.includes('flujo-interfaz')).join('\n'));