harness-everything 0.2.0 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ---
9
+ ## [0.2.1] - 2026-07-24
10
+
11
+ ### Fixed
12
+ - **Global skill installs landed one directory too deep**: `--global`/`-g` installs (all platforms share this one code path) copied skills to `~/.agents/harness-everything/skills/` instead of the documented `~/.agents/skills/` (see `bin/cli.js`'s own `-g, --global` help text, and the legacy-fallback scan `scripts/lib/skills.js` already expected at that path). Corrected in `scripts/installer.js` to match the same convention already used locally: `harness-everything/` holds manifest bookkeeping only, skill content lives in the native/shared `skills/` folder next to it. `~/.agents/harness-everything/manifest.json` is unchanged; uninstall's final global sweep was updated to match.
13
+ - **Duplicate lines could accumulate in `.gitignore`**: `ensureHarnessStateIgnored` (`hooks/scripts/lib/harness-state.js`) runs once per hook-invoked subprocess with no cross-process lock around its read-then-append, so two invocations firing close together could both decide the same ignore pattern was missing and both append it. It (and the installer's own `scripts/lib/gitignore.js` writer, which shares the identical shape) now collapses exact-duplicate non-comment/non-blank lines on every write, so a duplicate from a lost race self-heals on the next invocation instead of accumulating.
14
+
8
15
  ---
9
16
  ## [0.2.0] - 2026-07-23
10
17
 
package/README.md CHANGED
@@ -118,7 +118,7 @@ Harness operates through five core cognitive concepts:
118
118
 
119
119
  | AI Agent Tool | Integration Method | Local Target Location | Enforcement |
120
120
  |---|---|---|---|
121
- | **Claude Code** | Native Lifecycle Hooks (`PreToolUse`, `PostToolUse`, `SessionStart`) | `.claude/settings.json` (project) / `~/.claude/settings.json` (user) | **Hard** — hooks can block a tool call (`exit(2)`) |
121
+ | **Claude Code** | Native Lifecycle Hooks (`PreToolUse`, `PostToolUse`, `SessionStart`) | `.claude/settings.json` (project) / `~/.claude/settings.json` (user)<br>*.claude/skills/* (Project Skills) / *~/.claude/skills/* (Global Skills) | **Hard** — hooks can block a tool call (`exit(2)`) |
122
122
  | **Cursor** | Native Project Rules | `.cursorrules` | Advisory only |
123
123
  | **Copilot Chat** | Custom Instructions | `.github/copilot-instructions.md` | Advisory only |
124
124
  | **Codex** | Custom Instructions (`AGENTS.md`, not `.codex/config.toml` — that file controls CLI/sandbox behavior, not prompt content) | `AGENTS.md` | Advisory only |
@@ -60,10 +60,10 @@ Harness is designed to align with the unique capabilities of various AI IDEs and
60
60
 
61
61
  **Placement rule:** a platform's own native file/folder (`.claude/settings.json`, `.cursorrules`, `.cursor/skills/`, …) is never moved — it lives exactly where that platform expects it. Anything Harness itself needs that has no native home converges into one exclusively-owned subfolder per platform: `<platform-dir>/harness-everything/`. Nothing else ever creates a directory literally named `harness-everything`, which is what lets the installer add or remove it as a unit — including on uninstall — without touching anything else already in that platform's directory (another tool's files, or skills the user installed by hand).
62
62
 
63
- **Runtime state vs. installed skill content:** `<platform-dir>/harness-everything/` holds two kinds of content, kept apart by subfolder. `skills/` — Claude only, since Claude has no native project-skill directory to piggyback on — is the installer's local skill-copy target (see `scripts/lib/skills.js`'s `getInstalledSkills`, which is manifest-tracked, not a blind directory scan) — real files, not cache. Every other platform's skills stay at that platform's own established location (`.cursor/skills`, `.github/skills`, `.codex/skills`, `.continue/skills`) and are never moved. `state/` is pure runtime state: hook JSON, circuit-breaker counters, handoff/verification timestamps. It's scoped per Claude Code `session_id` under `sessions/<id>/` so two sessions open on the same repo never share (and stomp) each other's edit/verify timestamps or breaker counts — invocations with no `session_id` (manual runs, tests) fall into a fixed `sessions/default/` bucket. Both are gitignored and safe to delete; `state/` regenerates itself from nothing but in-session state (losing it mid-session just clears breaker/verification history), while deleting `skills/` requires re-running the installer to restore the copied skill files. A `manifest.json` sibling at the `harness-everything/` root records exactly what the installer put where, per platform, so uninstall never has to guess.
63
+ **Runtime state vs. installed skill content:** `<platform-dir>/harness-everything/` holds runtime state (pure runtime state: hook JSON, circuit-breaker counters, handoff/verification timestamps) kept apart in the `state/` subfolder. On the other hand, the actual installed skills go to each platform's native/expected skills directories (e.g., `.claude/skills/` for Claude, `.cursor/skills/` for Cursor, `.github/skills/` for Copilot, `.codex/skills/` for Codex, `.continue/skills/` for Continue, and `~/.agents/skills/` or `~/.claude/skills/` for Global scope) where they are dynamically discovered and processed. A `manifest.json` under `<platform-dir>/harness-everything/` (or global home) records exactly what the installer put where, per platform, so uninstall can match against the manifest files and perform precise cleanup without guessing. Both runtime state and skills directories are gitignored.
64
64
 
65
65
  ### 1. Claude Code (hook-enforced)
66
- Our installer configures native lifecycle hooks inside `.claude/settings.json`:
66
+ Our installer configures native lifecycle hooks inside `.claude/settings.json`, and places project-level skills inside the native `.claude/skills/` directory where Claude Code natively auto-discovers and registers them.
67
67
  * `SessionStart`: Runs `bootstrap.js` to restore previous handoffs, check environment variables, and initialize session state.
68
68
  * `PreToolUse`: Triggers `rule-of-3.js`, `boundary-guard.js`, `depth-guard.js`, `context-compact.js`, and `subagent-scope-guard.js` to intercept tool invocations before they run, and can actually block one (`exit(2)`) — e.g. the Rule of 3 circuit breaker.
69
69
  * `PostToolUse`: Records tool outcomes, updates the persistent transaction log (WAL) via `state-persist.js`, tracks repeat failures, and can only add advisory context back (the tool already ran; nothing here blocks it).
@@ -112,9 +112,35 @@ function ensureHarnessStateIgnored(rootPath) {
112
112
  }
113
113
  }
114
114
 
115
- if (patternsToAdd.length > 0) {
116
- const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
117
- fs.appendFileSync(gitignorePath, separator + patternsToAdd.join('\n') + '\n', 'utf8');
115
+ // This runs once per hook invocation - i.e. once per Claude Code
116
+ // subprocess - with no cross-process lock around the read-then-append
117
+ // below. Two hook invocations firing close together (e.g. two tool
118
+ // calls in the same turn) can each read the file before either has
119
+ // written, so both decide the same pattern is missing and both append
120
+ // it, leaving an exact duplicate line behind. Rather than add real
121
+ // cross-process locking for a housekeeping file that already fails
122
+ // silently by design, every call also collapses any exact-duplicate
123
+ // non-comment/non-blank line it finds - so a duplicate from a lost
124
+ // race self-heals on the very next invocation instead of accumulating.
125
+ const seen = new Set();
126
+ let sawDuplicate = false;
127
+ const dedupedLines = lines.filter(line => {
128
+ const trimmed = line.trim();
129
+ if (trimmed === '' || trimmed.startsWith('#')) return true;
130
+ if (seen.has(trimmed)) {
131
+ sawDuplicate = true;
132
+ return false;
133
+ }
134
+ seen.add(trimmed);
135
+ return true;
136
+ });
137
+
138
+ if (patternsToAdd.length > 0 || sawDuplicate) {
139
+ while (dedupedLines.length > 0 && dedupedLines[dedupedLines.length - 1] === '') {
140
+ dedupedLines.pop();
141
+ }
142
+ const finalLines = dedupedLines.concat(patternsToAdd);
143
+ fs.writeFileSync(gitignorePath, finalLines.join('\n') + '\n', 'utf8');
118
144
  }
119
145
  } catch (err) {
120
146
  // Fail silently to avoid breaking execution if .gitignore is write-locked
@@ -1,14 +1,8 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
 
4
- // Claude Code has no native project-skill directory this package can piggy
5
- // back on, so everything Harness adds locally - runtime state AND the
6
- // installed skill copies - lives under one self-contained subfolder of the
7
- // platform's own directory: .claude/harness-everything/. That keeps it out
8
- // of a self-invented top-level folder (the old .harness/) while still being
9
- // exclusively ours - nothing else creates a directory literally named
10
- // "harness-everything" inside .claude/, so it's trivial to add/remove
11
- // without touching anything else Claude Code or the user put in .claude/.
4
+ // Claude Code has native project-skill directory: .claude/skills/.
5
+ // Runtime state converges into .claude/harness-everything/.
12
6
  module.exports = {
13
7
  name: 'claude',
14
8
  label: 'Claude Code',
@@ -19,10 +13,14 @@ module.exports = {
19
13
  return path.join(this.getHarnessDir(workspaceRoot), 'state');
20
14
  },
21
15
  getSkillsDir(workspaceRoot) {
22
- return path.join(this.getHarnessDir(workspaceRoot), 'skills');
16
+ return path.join(workspaceRoot, '.claude', 'skills');
23
17
  },
24
18
  getIgnorePatterns(workspaceRoot) {
25
- return ['.claude/harness-everything/'];
19
+ const patterns = ['.claude/harness-everything/'];
20
+ if (fs.existsSync(path.join(workspaceRoot, '.claude', 'skills'))) {
21
+ patterns.push('.claude/skills/');
22
+ }
23
+ return patterns;
26
24
  },
27
25
  isMatch(pattern, trimmedLine) {
28
26
  if (pattern === '.claude/harness-everything/') {
@@ -38,5 +36,97 @@ module.exports = {
38
36
  return fs.existsSync(path.join(userHome, '.claude', 'settings.json'));
39
37
  }
40
38
  return fs.existsSync(path.join(workspaceRoot, '.claude', 'settings.json'));
39
+ },
40
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
41
+ if (isGlobal) {
42
+ const claudeDir = path.join(userHome, '.claude');
43
+ return {
44
+ path: path.join(claudeDir, 'skills'),
45
+ label: '~/.claude/skills/',
46
+ manifestPath: manifest.getManifestPath(claudeDir)
47
+ };
48
+ } else {
49
+ const claudeDir = path.join(workspaceRoot, '.claude');
50
+ return {
51
+ path: path.join(claudeDir, 'skills'),
52
+ label: '.claude/skills/',
53
+ manifestPath: manifest.getManifestPath(claudeDir)
54
+ };
55
+ }
56
+ },
57
+ install({ isGlobal, targetWorkspaceRoot, harnessSourceDir, packageVersion, getUserPromptsDir, advisory, claudeHooks, manifest }) {
58
+ const userHome = require('os').homedir();
59
+ const claudeDir = isGlobal ? path.join(userHome, '.claude') : path.join(targetWorkspaceRoot, '.claude');
60
+ if (!fs.existsSync(claudeDir)) {
61
+ fs.mkdirSync(claudeDir, { recursive: true });
62
+ console.log(` Created ${isGlobal ? '~/.claude' : '.claude'} directory`);
63
+ }
64
+ const claudeSettingsFile = path.join(claudeDir, 'settings.json');
65
+ let claudeConfig = {};
66
+ if (fs.existsSync(claudeSettingsFile)) {
67
+ try {
68
+ claudeConfig = JSON.parse(fs.readFileSync(claudeSettingsFile, 'utf8'));
69
+ } catch (e) {
70
+ console.warn(` ⚠️ Existing ${isGlobal ? '~' : ''}/.claude/settings.json is malformed, creating fresh one.`);
71
+ }
72
+ }
73
+
74
+ const sourceHooksFile = path.join(harnessSourceDir, 'hooks', 'hooks.json');
75
+ if (fs.existsSync(sourceHooksFile)) {
76
+ const sourceHooksObj = JSON.parse(fs.readFileSync(sourceHooksFile, 'utf8'));
77
+ const resolvedHooks = {};
78
+ for (const [hookType, hookList] of Object.entries(sourceHooksObj.hooks || {})) {
79
+ resolvedHooks[hookType] = hookList.map(hookItem => {
80
+ const cloned = JSON.parse(JSON.stringify(hookItem));
81
+ if (cloned.hooks) {
82
+ cloned.hooks = cloned.hooks.map(h => {
83
+ if (h.type === 'command' && h.command) {
84
+ h.command = h.command.replace(/^node\s+"?([^"\s]+)"?/, (m, scriptPath) => {
85
+ const abs = path.isAbsolute(scriptPath) ? scriptPath : path.join(harnessSourceDir, scriptPath);
86
+ return `node "${abs}"`;
87
+ });
88
+ }
89
+ return h;
90
+ });
91
+ }
92
+ cloned.harness = { package: manifest.PACKAGE_NAME, version: packageVersion, author: manifest.HARNESS_AUTHOR };
93
+ return cloned;
94
+ });
95
+ }
96
+ claudeHooks.mergeHarnessHooks(claudeConfig, resolvedHooks);
97
+ }
98
+
99
+ fs.writeFileSync(claudeSettingsFile, JSON.stringify(claudeConfig, null, 2), 'utf8');
100
+ console.log(` ✅ Configured Claude Code hooks safely in ${isGlobal ? '~' : ''}/.claude/settings.json`);
101
+
102
+ // DOWNWARD COMPATIBLE CLEANUP: Remove old wrong skill folder .claude/harness-everything/skills if it exists
103
+ const oldSkillsDir = path.join(claudeDir, 'harness-everything', 'skills');
104
+ if (fs.existsSync(oldSkillsDir)) {
105
+ try {
106
+ fs.rmSync(oldSkillsDir, { recursive: true, force: true });
107
+ console.log(` 🧹 Cleaned up legacy incorrect local skills folder at: .claude/harness-everything/skills/`);
108
+ } catch (e) {
109
+ console.warn(` ⚠️ Failed to remove legacy skills folder: ${e.message}`);
110
+ }
111
+ }
112
+ },
113
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome, getUserPromptsDir, advisory, claudeHooks, manifest, cleanEmptyDirs }) {
114
+ if (removeLocal) {
115
+ const localSettingsFile = path.join(workspaceRoot, '.claude', 'settings.json');
116
+ claudeHooks.removeHarnessHooks(localSettingsFile);
117
+
118
+ // Clear old wrong skill folder .claude/harness-everything/skills if any remain
119
+ const oldSkillsDir = path.join(workspaceRoot, '.claude', 'harness-everything', 'skills');
120
+ if (fs.existsSync(oldSkillsDir)) {
121
+ fs.rmSync(oldSkillsDir, { recursive: true, force: true });
122
+ console.log(` 🧹 Cleaned up legacy incorrect local skills folder at: .claude/harness-everything/skills/`);
123
+ }
124
+ }
125
+ if (removeGlobal) {
126
+ const globalSettingsFile = path.join(userHome, '.claude', 'settings.json');
127
+ claudeHooks.removeHarnessHooks(globalSettingsFile);
128
+ const globalClaudeDir = path.join(userHome, '.claude');
129
+ cleanEmptyDirs(globalClaudeDir, [userHome]);
130
+ }
41
131
  }
42
132
  };
@@ -42,5 +42,55 @@ module.exports = {
42
42
  return false;
43
43
  }
44
44
  return fs.existsSync(path.join(workspaceRoot, 'AGENTS.md'));
45
+ },
46
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
47
+ if (isGlobal) {
48
+ const globalAgentsDir = path.join(userHome, '.agents');
49
+ return {
50
+ path: path.join(globalAgentsDir, 'skills'),
51
+ label: '~/.agents/skills/',
52
+ manifestPath: manifest.getManifestPath(globalAgentsDir),
53
+ };
54
+ } else {
55
+ const codexDir = path.join(workspaceRoot, '.codex');
56
+ return {
57
+ path: path.join(codexDir, 'skills'),
58
+ label: '.codex/skills/',
59
+ manifestPath: manifest.getManifestPath(codexDir),
60
+ };
61
+ }
62
+ },
63
+ install({ isGlobal, targetWorkspaceRoot, getUserPromptsDir, advisory }) {
64
+ if (!isGlobal) {
65
+ const targetFile = path.join(targetWorkspaceRoot, 'AGENTS.md');
66
+ advisory.injectAdvisoryText(targetFile, '# AGENTS.md', 'AGENTS.md');
67
+ } else {
68
+ try {
69
+ const promptsDir = getUserPromptsDir();
70
+ if (!fs.existsSync(promptsDir)) {
71
+ fs.mkdirSync(promptsDir, { recursive: true });
72
+ }
73
+ const vscodeAgentFile = path.join(promptsDir, 'harness.agent.md');
74
+ fs.writeFileSync(vscodeAgentFile, advisory.buildCodexGlobalContent(), 'utf8');
75
+ console.log(` ✅ Installed global Codex agent to VS Code: ${vscodeAgentFile}`);
76
+ } catch (err) {
77
+ console.warn(` ⚠️ Failed to write VS Code user prompts folder: ${err.message}`);
78
+ }
79
+ }
80
+ },
81
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome, getUserPromptsDir, cleanEmptyDirs }) {
82
+ const advisory = require('../../../../scripts/lib/advisory-text');
83
+ if (removeLocal) {
84
+ advisory.removeAdvisoryText(path.join(workspaceRoot, 'AGENTS.md'));
85
+ }
86
+ if (removeGlobal) {
87
+ const promptsDir = getUserPromptsDir();
88
+ const vscodeAgentFile = path.join(promptsDir, 'harness.agent.md');
89
+ if (fs.existsSync(vscodeAgentFile)) {
90
+ fs.unlinkSync(vscodeAgentFile);
91
+ console.log(` ✅ Removed global Codex agent: ${vscodeAgentFile}`);
92
+ }
93
+ cleanEmptyDirs(promptsDir, [userHome]);
94
+ }
45
95
  }
46
96
  };
@@ -37,5 +37,37 @@ module.exports = {
37
37
  return fs.existsSync(path.join(userHome, '.continue', 'rules', 'harness.md'));
38
38
  }
39
39
  return fs.existsSync(path.join(workspaceRoot, '.continue', 'rules', 'harness.md'));
40
+ },
41
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
42
+ if (isGlobal) {
43
+ const globalAgentsDir = path.join(userHome, '.agents');
44
+ return {
45
+ path: path.join(globalAgentsDir, 'skills'),
46
+ label: '~/.agents/skills/',
47
+ manifestPath: manifest.getManifestPath(globalAgentsDir),
48
+ };
49
+ } else {
50
+ const continueDir = path.join(workspaceRoot, '.continue');
51
+ return {
52
+ path: path.join(continueDir, 'skills'),
53
+ label: '.continue/skills/',
54
+ manifestPath: manifest.getManifestPath(continueDir),
55
+ };
56
+ }
57
+ },
58
+ install({ isGlobal, targetWorkspaceRoot, userHome, advisory, workspaceRoot }) {
59
+ const targetFile = isGlobal ? path.join(userHome, '.continue', 'rules', 'harness.md') : path.join(targetWorkspaceRoot, '.continue', 'rules', 'harness.md');
60
+ advisory.installContinueRule(targetFile, isGlobal ? '~/.continue/rules/harness.md' : '.continue/rules/harness.md');
61
+ },
62
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome, cleanEmptyDirs }) {
63
+ const advisory = require('../../../../scripts/lib/advisory-text');
64
+ if (removeLocal) {
65
+ advisory.removeContinueRule(path.join(workspaceRoot, '.continue', 'rules', 'harness.md'));
66
+ cleanEmptyDirs(path.join(workspaceRoot, '.continue', 'rules'), [workspaceRoot, userHome]);
67
+ }
68
+ if (removeGlobal) {
69
+ advisory.removeContinueRule(path.join(userHome, '.continue', 'rules', 'harness.md'));
70
+ cleanEmptyDirs(path.join(userHome, '.continue', 'rules'), [userHome]);
71
+ }
40
72
  }
41
73
  };
@@ -37,5 +37,55 @@ module.exports = {
37
37
  return false; // Handled via global userPromptsDir check
38
38
  }
39
39
  return fs.existsSync(path.join(workspaceRoot, '.github', 'copilot-instructions.md'));
40
+ },
41
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
42
+ if (isGlobal) {
43
+ const globalAgentsDir = path.join(userHome, '.agents');
44
+ return {
45
+ path: path.join(globalAgentsDir, 'skills'),
46
+ label: '~/.agents/skills/',
47
+ manifestPath: manifest.getManifestPath(globalAgentsDir),
48
+ };
49
+ } else {
50
+ const githubDir = path.join(workspaceRoot, '.github');
51
+ return {
52
+ path: path.join(githubDir, 'skills'),
53
+ label: '.github/skills/',
54
+ manifestPath: manifest.getManifestPath(githubDir),
55
+ };
56
+ }
57
+ },
58
+ install({ isGlobal, targetWorkspaceRoot, getUserPromptsDir, advisory }) {
59
+ if (!isGlobal) {
60
+ const targetFile = path.join(targetWorkspaceRoot, '.github', 'copilot-instructions.md');
61
+ advisory.injectAdvisoryText(targetFile, '# Copilot Instructions', '.github/copilot-instructions.md');
62
+ } else {
63
+ try {
64
+ const promptsDir = getUserPromptsDir();
65
+ if (!fs.existsSync(promptsDir)) {
66
+ fs.mkdirSync(promptsDir, { recursive: true });
67
+ }
68
+ const vscodeInstFile = path.join(promptsDir, 'harness.instructions.md');
69
+ fs.writeFileSync(vscodeInstFile, advisory.buildCopilotGlobalContent(), 'utf8');
70
+ console.log(` ✅ Installed global Copilot instructions to VS Code: ${vscodeInstFile}`);
71
+ } catch (err) {
72
+ console.warn(` ⚠️ Failed to write VS Code user prompts folder: ${err.message}`);
73
+ }
74
+ }
75
+ },
76
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome, getUserPromptsDir, cleanEmptyDirs }) {
77
+ const advisory = require('../../../../scripts/lib/advisory-text');
78
+ if (removeLocal) {
79
+ advisory.removeAdvisoryText(path.join(workspaceRoot, '.github', 'copilot-instructions.md'));
80
+ }
81
+ if (removeGlobal) {
82
+ const promptsDir = getUserPromptsDir();
83
+ const vscodeInstFile = path.join(promptsDir, 'harness.instructions.md');
84
+ if (fs.existsSync(vscodeInstFile)) {
85
+ fs.unlinkSync(vscodeInstFile);
86
+ console.log(` ✅ Removed global Copilot instructions: ${vscodeInstFile}`);
87
+ }
88
+ cleanEmptyDirs(promptsDir, [userHome]);
89
+ }
40
90
  }
41
91
  };
@@ -37,5 +37,35 @@ module.exports = {
37
37
  return fs.existsSync(path.join(userHome, '.cursorrules'));
38
38
  }
39
39
  return fs.existsSync(path.join(workspaceRoot, '.cursorrules'));
40
+ },
41
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
42
+ if (isGlobal) {
43
+ const globalAgentsDir = path.join(userHome, '.agents');
44
+ return {
45
+ path: path.join(globalAgentsDir, 'skills'),
46
+ label: '~/.agents/skills/',
47
+ manifestPath: manifest.getManifestPath(globalAgentsDir),
48
+ };
49
+ } else {
50
+ const cursorDir = path.join(workspaceRoot, '.cursor');
51
+ return {
52
+ path: path.join(cursorDir, 'skills'),
53
+ label: '.cursor/skills/',
54
+ manifestPath: manifest.getManifestPath(cursorDir),
55
+ };
56
+ }
57
+ },
58
+ install({ isGlobal, targetWorkspaceRoot, userHome, advisory }) {
59
+ const targetFile = isGlobal ? path.join(userHome, '.cursorrules') : path.join(targetWorkspaceRoot, '.cursorrules');
60
+ advisory.injectAdvisoryText(targetFile, '# Cursor Project Rules', isGlobal ? '~/.cursorrules' : '.cursorrules');
61
+ },
62
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome }) {
63
+ const advisory = require('../../../../scripts/lib/advisory-text');
64
+ if (removeLocal) {
65
+ advisory.removeAdvisoryText(path.join(workspaceRoot, '.cursorrules'));
66
+ }
67
+ if (removeGlobal) {
68
+ advisory.removeAdvisoryText(path.join(userHome, '.cursorrules'));
69
+ }
40
70
  }
41
71
  };
@@ -28,5 +28,30 @@ module.exports = {
28
28
  return false;
29
29
  }
30
30
  return fs.existsSync(path.join(workspaceRoot, '.hermes.md'));
31
+ },
32
+ getSkillsTarget({ workspaceRoot, userHome, isGlobal, manifest }) {
33
+ if (isGlobal) {
34
+ const globalAgentsDir = path.join(userHome, '.agents');
35
+ return {
36
+ path: path.join(globalAgentsDir, 'skills'),
37
+ label: '~/.agents/skills/',
38
+ manifestPath: manifest.getManifestPath(globalAgentsDir),
39
+ };
40
+ }
41
+ return null;
42
+ },
43
+ install({ isGlobal, targetWorkspaceRoot, advisory }) {
44
+ if (!isGlobal) {
45
+ const targetFile = path.join(targetWorkspaceRoot, '.hermes.md');
46
+ advisory.injectAdvisoryText(targetFile, '# .hermes.md', '.hermes.md');
47
+ } else {
48
+ console.log(` ℹ️ Hermes Agent has no documented global instructions file (it reads .hermes.md from the current project directory only) - skipping global install for --hermes.`);
49
+ }
50
+ },
51
+ uninstall({ removeLocal, removeGlobal, workspaceRoot, userHome }) {
52
+ const advisory = require('../../../../scripts/lib/advisory-text');
53
+ if (removeLocal) {
54
+ advisory.removeAdvisoryText(path.join(workspaceRoot, '.hermes.md'));
55
+ }
31
56
  }
32
57
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {