harness-everything 0.16.4 → 0.16.6

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 (36) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/ci/mechanism-2am-openai-packaged-helper-runtime.test.js +110 -0
  5. package/ci/mechanism-2an-environment-detection-readonly.test.js +113 -0
  6. package/ci/skill-routing-check.js +1 -1
  7. package/docs/workflows/environment-detection.md +15 -7
  8. package/environment-detection/SKILL.md +15 -16
  9. package/harness-everything/SKILL.md +1 -1
  10. package/harness-everything/scripts/bootstrap.js +4 -3
  11. package/harness-everything/scripts/runtime-paths.js +37 -0
  12. package/harness-everything/scripts/self-heal.js +31 -8
  13. package/harness-everything/scripts/tier-router.js +2 -6
  14. package/hooks/scripts/lib/platforms/claude.js +3 -3
  15. package/opencode-plugin/plugin.json +1 -1
  16. package/package.json +1 -1
  17. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  18. package/plugins/harness-everything/hooks/scripts/lib/platforms/claude.js +3 -3
  19. package/plugins/harness-everything/plugin.json +1 -1
  20. package/plugins/harness-everything/scripts/installer.js +629 -0
  21. package/plugins/harness-everything/scripts/lib/claude-hooks.js +84 -0
  22. package/plugins/harness-everything/scripts/lib/execution-contract.js +70 -0
  23. package/plugins/harness-everything/scripts/lib/gitignore.js +199 -0
  24. package/plugins/harness-everything/scripts/lib/manifest.js +142 -0
  25. package/plugins/harness-everything/scripts/lib/path-boundary.js +90 -0
  26. package/plugins/harness-everything/scripts/lib/prompts.js +241 -0
  27. package/plugins/harness-everything/scripts/lib/skills.js +314 -0
  28. package/plugins/harness-everything/scripts/lib/workspace.js +290 -0
  29. package/plugins/harness-everything/skills/environment-detection/SKILL.md +15 -16
  30. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  31. package/plugins/harness-everything/skills/harness-everything/scripts/bootstrap.js +4 -3
  32. package/plugins/harness-everything/skills/harness-everything/scripts/runtime-paths.js +37 -0
  33. package/plugins/harness-everything/skills/harness-everything/scripts/self-heal.js +31 -8
  34. package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +2 -6
  35. package/scripts/installer.js +12 -4
  36. package/scripts/sync-openai-plugin.js +5 -2
@@ -0,0 +1,290 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+
6
+ const WORKSPACE_CONTEXT_KEYS = [
7
+ 'workspace_root', 'workspaceRoot', 'project_root', 'projectRoot',
8
+ 'working_directory', 'workingDirectory', 'cwd', 'root'
9
+ ];
10
+ const SESSION_CONTEXT_KEYS = ['session_id', 'sessionId'];
11
+ const HOST_CONTEXT_KEYS = [
12
+ 'host_id', 'hostId', 'agent_id', 'agentId', 'client_id', 'clientId',
13
+ 'machine_id', 'machineId', 'runtime_id', 'runtimeId'
14
+ ];
15
+ const SESSION_REGISTRY_DIR = 'session-workspaces';
16
+ const SESSION_REGISTRY_VERSION = 1;
17
+ const UNBOUND_WORKSPACE_KEY = 'unbound-workspace';
18
+
19
+ function findWorkspaceRoot(startPath, allowNonGit = false) {
20
+ if (typeof startPath !== 'string' || !startPath.trim()) return null;
21
+ let dir = path.resolve(startPath);
22
+ while (true) {
23
+ if (fs.existsSync(path.join(dir, '.git'))) return canonicalPath(dir);
24
+ const parent = path.dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ return allowNonGit ? canonicalPath(startPath) : null;
29
+ }
30
+
31
+ function canonicalPath(value) {
32
+ if (typeof value !== 'string' || !value.trim()) return null;
33
+ const resolved = path.resolve(value);
34
+ try { return fs.realpathSync(resolved); } catch (err) { return resolved; }
35
+ }
36
+
37
+ function getSessionId(context) {
38
+ if (!context || typeof context !== 'object') return null;
39
+ for (const key of SESSION_CONTEXT_KEYS) {
40
+ if (typeof context[key] === 'string' && context[key].trim()) {
41
+ return context[key].trim();
42
+ }
43
+ }
44
+ for (const key of ['session', 'metadata', 'context']) {
45
+ if (context[key] && typeof context[key] === 'object') {
46
+ const nested = getSessionId(context[key]);
47
+ if (nested) return nested;
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function getHostId(context) {
54
+ if (!context || typeof context !== 'object') return null;
55
+ for (const key of HOST_CONTEXT_KEYS) {
56
+ if (typeof context[key] === 'string' && context[key].trim()) return context[key].trim();
57
+ }
58
+ if (typeof context.host === 'string' && context.host.trim()) return context.host.trim();
59
+ if (context.host && typeof context.host === 'object') return getHostId(context.host);
60
+ for (const key of ['agent', 'client', 'machine', 'runtime', 'metadata', 'context']) {
61
+ if (context[key] && typeof context[key] === 'object') {
62
+ const nested = getHostId(context[key]);
63
+ if (nested) return nested;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function contextPath(context, depth = 0) {
70
+ if (typeof context === 'string' && context.trim()) return context.trim();
71
+ if (!context || typeof context !== 'object' || depth > 2) return null;
72
+
73
+ for (const key of WORKSPACE_CONTEXT_KEYS) {
74
+ const value = context[key];
75
+ if (typeof value === 'string' && value.trim()) return value.trim();
76
+ if (value && typeof value === 'object') {
77
+ const nested = contextPath(value, depth + 1);
78
+ if (nested) return nested;
79
+ }
80
+ }
81
+
82
+ for (const key of ['workspace', 'project', 'host', 'context']) {
83
+ const nested = contextPath(context[key], depth + 1);
84
+ if (nested) return nested;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function sessionRegistryPath(sessionId, hostId) {
90
+ const namespace = hostId ? `${hostId}\0${sessionId}` : String(sessionId);
91
+ const hash = crypto.createHash('sha256').update(namespace).digest('hex');
92
+ return path.join(getStateHome(), SESSION_REGISTRY_DIR, `${hash}.json`);
93
+ }
94
+
95
+ function readRegistryRecord(filePath) {
96
+ try {
97
+ const record = JSON.parse(fs.readFileSync(filePath, 'utf8'));
98
+ if (record.version !== SESSION_REGISTRY_VERSION || typeof record.workspaceRoot !== 'string') return null;
99
+ return record;
100
+ } catch (err) {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function findSessionRecords(sessionId) {
106
+ const registryDir = path.join(getStateHome(), SESSION_REGISTRY_DIR);
107
+ try {
108
+ return fs.readdirSync(registryDir, { withFileTypes: true })
109
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
110
+ .map(entry => readRegistryRecord(path.join(registryDir, entry.name)))
111
+ .filter(record => record && record.sessionId === sessionId);
112
+ } catch (err) {
113
+ return [];
114
+ }
115
+ }
116
+
117
+ function readWorkspaceSession(sessionId, context) {
118
+ if (!sessionId) return null;
119
+ const hostId = getHostId(context);
120
+ const records = [];
121
+ const preferred = readRegistryRecord(sessionRegistryPath(sessionId, hostId));
122
+ if (preferred && (!hostId || !preferred.hostId || preferred.hostId === hostId)) records.push(preferred);
123
+ if (hostId) {
124
+ // A session may have been first observed by a caller that did not expose
125
+ // host metadata. Reuse that unscoped binding rather than forking state.
126
+ const unscoped = readRegistryRecord(sessionRegistryPath(sessionId, null));
127
+ if (unscoped && !unscoped.hostId) records.push(unscoped);
128
+ } else {
129
+ records.push(...findSessionRecords(sessionId));
130
+ }
131
+
132
+ const unique = [...new Map(records.map(record => [
133
+ `${record.hostId || ''}\0${record.workspaceRoot}`,
134
+ record
135
+ ])).values()];
136
+ if (unique.length === 1) return unique[0];
137
+ if (unique.length > 1) {
138
+ // Hostless payloads are safe only when all known host bindings agree on
139
+ // the same workspace. Otherwise select nothing and require context.
140
+ const roots = new Set(unique.map(record => canonicalPath(record.workspaceRoot)));
141
+ if (roots.size === 1) return unique[0];
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function bindWorkspaceSession(sessionId, workspaceRoot, context, force = false) {
147
+ if (!sessionId) return null;
148
+ const root = findWorkspaceRoot(workspaceRoot, true);
149
+ if (!root) return null;
150
+ const hostId = getHostId(context);
151
+ const target = sessionRegistryPath(sessionId, hostId);
152
+ const record = {
153
+ version: SESSION_REGISTRY_VERSION,
154
+ sessionId,
155
+ hostId: hostId || undefined,
156
+ workspaceRoot: root,
157
+ workspaceKey: getWorkspaceKey(root),
158
+ updatedAt: new Date().toISOString()
159
+ };
160
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
161
+ try {
162
+ fs.mkdirSync(path.dirname(target), { recursive: true });
163
+ fs.writeFileSync(temporary, JSON.stringify(record, null, 2), 'utf8');
164
+ // Keep the first successful binding when two hooks initialize together.
165
+ if (fs.existsSync(target)) {
166
+ if (readRegistryRecord(target) && !force) fs.unlinkSync(temporary);
167
+ else {
168
+ fs.unlinkSync(target);
169
+ fs.renameSync(temporary, target);
170
+ }
171
+ } else fs.renameSync(temporary, target);
172
+ } catch (err) {
173
+ // Identity persistence is best effort. The caller still receives the
174
+ // resolved root and the next invocation can retry the binding.
175
+ try { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } catch (cleanupErr) { /* ignore */ }
176
+ }
177
+ return root;
178
+ }
179
+
180
+ function isIdentityReset(context) {
181
+ if (!context || typeof context !== 'object') return false;
182
+ return context.workspace_identity_reset === true ||
183
+ context.workspaceIdentityReset === true ||
184
+ context.new_session === true ||
185
+ context.newSession === true;
186
+ }
187
+
188
+ function getWorkspaceRoot(context) {
189
+ const sessionId = getSessionId(context);
190
+ const existing = readWorkspaceSession(sessionId, context);
191
+ const explicit = contextPath(context);
192
+
193
+ // SessionStart is the host's authoritative opportunity to establish (or
194
+ // re-establish) the project identity. All later hook invocations keep that
195
+ // binding even if the agent changes cwd or reports a nested repository.
196
+ if (existing && !isIdentityReset(context)) return existing.workspaceRoot;
197
+
198
+ const hostRoot = explicit || process.env.HARNESS_WORKSPACE_ROOT ||
199
+ process.env.CLAUDE_PROJECT_DIR || process.env.CODEX_PROJECT_DIR;
200
+ const root = findWorkspaceRoot(hostRoot || process.cwd(), Boolean(hostRoot));
201
+ if (sessionId) bindWorkspaceSession(sessionId, root, context, isIdentityReset(context));
202
+ return root;
203
+ }
204
+
205
+ // Single global root for ALL Harness runtime state (session counters,
206
+ // circuit-breaker trips, audit logs, handoff timestamps, ...). Never
207
+ // cwd-derived, so a script invoked from a fixture dir, worktree, submodule,
208
+ // or any other nested non-git directory can never scatter files there
209
+ // (issue #42). `$HARNESS_STATE_HOME` matches the env var the opencode
210
+ // plugin already used for its own (differently-rooted) global state.
211
+ function getStateHome() {
212
+ return process.env.HARNESS_STATE_HOME || path.join(os.homedir(), '.agents', 'harness-everything');
213
+ }
214
+
215
+ // Keys runtime state per real workspace instead of per invocation cwd, so
216
+ // two different repos (or a repo and a stray subdirectory someone `cd`ed
217
+ // into) never collide or fork the same state stream. Uses the resolved real
218
+ // path (symlinks collapsed) hashed short, prefixed with a readable slug
219
+ // purely so `~/.agents/harness-everything/workspaces/` stays eyeballable.
220
+ function getWorkspaceKey(root) {
221
+ const workspaceRoot = typeof root === 'string' && root.trim() ? root : getWorkspaceRoot(root);
222
+ if (!workspaceRoot) return UNBOUND_WORKSPACE_KEY;
223
+ const resolved = path.resolve(workspaceRoot);
224
+ let real = resolved;
225
+ try { real = fs.realpathSync(resolved); } catch (err) { /* path may not exist yet (tests) */ }
226
+ const slug = path.basename(real).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
227
+ const hashInput = process.platform === 'win32' ? real.toLowerCase() : real;
228
+ const hash = crypto.createHash('sha1').update(hashInput).digest('hex').slice(0, 12);
229
+ return `${slug}-${hash}`;
230
+ }
231
+
232
+ function getWorkspaceStateDir(root) {
233
+ return path.join(getStateHome(), 'workspaces', getWorkspaceKey(root));
234
+ }
235
+
236
+ function resolveWorkspaceIdentity(context) {
237
+ const workspaceRoot = getWorkspaceRoot(context);
238
+ return {
239
+ workspaceRoot,
240
+ workspaceKey: getWorkspaceKey(workspaceRoot),
241
+ stateDir: getWorkspaceStateDir(workspaceRoot),
242
+ sessionId: getSessionId(context)
243
+ };
244
+ }
245
+
246
+ function getUserPromptsDir() {
247
+ if (process.env.VSCODE_USER_PROMPTS_FOLDER) {
248
+ return process.env.VSCODE_USER_PROMPTS_FOLDER;
249
+ }
250
+ const home = os.homedir();
251
+ if (process.platform === 'win32') {
252
+ return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User', 'prompts');
253
+ } else if (process.platform === 'darwin') {
254
+ return path.join(home, 'Library', 'Application Support', 'Code', 'User', 'prompts');
255
+ } else {
256
+ return path.join(home, '.config', 'Code', 'User', 'prompts');
257
+ }
258
+ }
259
+
260
+ // The harness repo must never be "repaired" into carrying its own generated
261
+ // advisory files. Identify it by repo identity, not by comparing the caller's
262
+ // __dirname: when harness runs from an npx/global install against its own
263
+ // source checkout, the installed path and the workspace path differ and a
264
+ // path-based guard silently fails open (issue #40).
265
+ const HARNESS_PACKAGE_NAME = 'harness-everything';
266
+
267
+ function isHarnessRepo(workspaceRoot) {
268
+ try {
269
+ const pkgPath = path.join(workspaceRoot, 'package.json');
270
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
271
+ return pkg.name === HARNESS_PACKAGE_NAME;
272
+ } catch (err) {
273
+ return false;
274
+ }
275
+ }
276
+
277
+ module.exports = {
278
+ getWorkspaceRoot,
279
+ resolveWorkspaceIdentity,
280
+ bindWorkspaceSession,
281
+ readWorkspaceSession,
282
+ getSessionId,
283
+ getHostId,
284
+ getStateHome,
285
+ getWorkspaceKey,
286
+ getWorkspaceStateDir,
287
+ getUserPromptsDir,
288
+ isHarnessRepo,
289
+ HARNESS_PACKAGE_NAME,
290
+ };
@@ -4,38 +4,37 @@ description: Use at session start (Discover phase) to detect and align with the
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.4.0
7
+ version: 0.16.6
8
8
  ---
9
9
 
10
10
  # Environment Detection & Shell Alignment
11
11
 
12
- Think > Try > Summarize > Record: detect before executing.
13
-
14
12
  ## 📋 Skill Contract
15
13
 
16
14
  | Component | Specification |
17
15
  | :--- | :--- |
18
16
  | **Trigger / Input** | Session start or complex terminal sequences. |
19
- | **Expected Output** | Detected OS, shell, PATH via script or heuristics. |
20
- | **State Mutations** | Session context adopts path/env-var syntax. |
21
- | **Enforcement Gate** | Preflight; fall back to `<environment_info>` inspection and probing. |
17
+ | **Expected Output** | OS, shell, PATH, available CLIs. |
18
+ | **State Mutations** | Read-only by default; explicit repair may update Harness files. |
19
+ | **Enforcement Gate** | Preflight; fall back to `<environment_info>` and minimal probes. |
22
20
 
23
- Boundary: operate solely within `process.cwd()`; ignore other workspaces, history, or temp paths surfaced by the IDE — never run commands outside the current project root.
21
+ Boundary: stay inside `process.cwd()`. Detection never writes integrations or searches parent/cache paths for another runtime.
24
22
 
25
23
  ## Workflow
26
24
 
27
- 1. Preflight from this skill's own directory; parse OS, shell, available CLIs: `node "<this-skill-dir>/scripts/preflight.js"`
28
- 2. Self-heal harness touchpoints (idempotent): `node "<skills-repo-root>/harness-everything/scripts/self-heal.js"` — audits `<workspace>/.claude/settings.json`, `<workspace>/.cursorrules`, `<workspace>/.github/copilot-instructions.md`, `<workspace>/AGENTS.md`.
29
- 3. Adopt syntax: Git Bash → forward `/`, `$VAR`, Unix cmds (`ls`, `rm -rf`), never `dir`/`del`/PowerShell · PowerShell → `$env:VAR`, cmdlets · CMD → `\`, `%VAR%`, `dir`/`del`/`copy`, never Unix.
30
- 4. On failure don't blindly retry — suspect wrong-shell syntax. Three failures → trigger `zoom-out`.
25
+ 1. Run `node "<this-skill-dir>/scripts/preflight.js"`; detect OS, shell, CLIs.
26
+ 2. Where supported, audit read-only: `node "<this-skill-dir>/../harness-everything/scripts/self-heal.js" --check`. If unavailable, report and continue.
27
+ 3. Only explicit install/setup/repair intent may omit `--check`. Repair covers Claude, Codex, Copilot, Cursor, Continue, Hermes. OpenCode has its own plugin runtime; skills-only surfaces may lack repair.
28
+ 4. Adopt shell syntax: Git Bash → `/`, `$VAR`, Unix commands; PowerShell → `$env:VAR`, cmdlets; CMD → `\`, `%VAR%`, Windows commands. Three repeated failures → `zoom-out`.
31
29
 
32
30
  Deep dive: <this-skill-dir>/references/shell-syntax-rules.md
33
31
 
34
32
  ## USE FOR:
35
- - session-start OS/shell/toolchain detection
36
- - per-terminal command/path/env-var alignment
37
- - repairing missing harness touchpoints
33
+ - session-start environment detection
34
+ - shell/path/toolchain alignment
35
+ - read-only audit or explicit repair
38
36
 
39
37
  ## DO NOT USE FOR:
40
- - operating outside the current workspace
41
- - installing toolchains from scratch
38
+ - implicit repair during detection
39
+ - six-adapter repair on OpenCode/skills-only surfaces
40
+ - work outside the workspace or toolchain installation
@@ -4,7 +4,7 @@ description: "Route software/project work through the Harness kernel: classify T
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.16.3
7
+ version: 0.16.6
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const { getHarnessRoot, requireHarnessState, requireWorkspace } = require('./runtime-paths');
4
5
  const {
5
6
  getWorkspaceRoot,
6
7
  getSessionId,
7
8
  getSessionDir,
8
9
  writeCurrentSession,
9
10
  pruneStaleSessions,
10
- } = require('../../hooks/scripts/lib/harness-state');
11
+ } = requireHarnessState();
11
12
 
12
13
  console.log("Bootstrapping Harness Skills OS...");
13
14
 
@@ -162,9 +163,9 @@ try {
162
163
  // npx/global install auditing the harness checkout itself compares two
163
164
  // different paths and wrongly advises a repair that self-heal then skips
164
165
  // (issue #40). Required lazily so a missing lib can never break session start.
165
- const { isHarnessRepo } = require('../../scripts/lib/workspace');
166
+ const { isHarnessRepo } = requireWorkspace();
166
167
  const auditingSelf = isHarnessRepo(workspaceRoot)
167
- || path.resolve(workspaceRoot) === path.resolve(__dirname, '..', '..');
168
+ || path.resolve(workspaceRoot) === path.resolve(getHarnessRoot());
168
169
 
169
170
  if (missing.length > 0 && !auditingSelf) {
170
171
  console.log(`\n[Self-Heal] Missing integration touchpoints: ${missing.join(', ')}`);
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Helpers live in both the source checkout and the packaged plugin's skill
7
+ // tree. Resolve the owning runtime root instead of assuming either layout.
8
+ function candidateRoots() {
9
+ return [
10
+ path.resolve(__dirname, '..', '..'),
11
+ path.resolve(__dirname, '..', '..', '..'),
12
+ ];
13
+ }
14
+
15
+ function getHarnessRoot() {
16
+ const root = candidateRoots().find(candidate =>
17
+ fs.existsSync(path.join(candidate, 'scripts', 'lib', 'workspace.js'))
18
+ );
19
+ if (!root) {
20
+ throw new Error(`Harness runtime root not found from ${__dirname}`);
21
+ }
22
+ return root;
23
+ }
24
+
25
+ function requireWorkspace() {
26
+ return require(path.join(getHarnessRoot(), 'scripts', 'lib', 'workspace.js'));
27
+ }
28
+
29
+ function requireHarnessState() {
30
+ return require(path.join(getHarnessRoot(), 'hooks', 'scripts', 'lib', 'harness-state.js'));
31
+ }
32
+
33
+ module.exports = {
34
+ getHarnessRoot,
35
+ requireWorkspace,
36
+ requireHarnessState,
37
+ };
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * Self-healing installation audit for Harness OS.
4
4
  *
5
- * Audits the six platform integration touchpoints in the current workspace
6
- * and, when any are missing, re-runs the idempotent installer to repair them.
7
- * This makes the harness portable across editors: install once via Claude
8
- * Code, then open the same repo in Copilot/Cursor/Codex/Continue/Hermes and
9
- * the missing advisory instructions are backfilled automatically the first time
10
- * harness-everything (or the environment-detection Discover phase) runs.
5
+ * Audits the six platform integration touchpoints in the current workspace.
6
+ * Read-only callers use --check. Repair mode is explicit: only a direct run
7
+ * without --check may delegate to the idempotent installer.
8
+ *
9
+ * environment-detection uses the read-only path; ordinary Discover/preflight
10
+ * must never backfill hooks or advisory files as a side effect.
11
11
  *
12
12
  * Usage:
13
13
  * node self-heal.js Audit and repair anything missing.
@@ -23,7 +23,27 @@
23
23
  const fs = require('fs');
24
24
  const path = require('path');
25
25
  const { execSync } = require('child_process');
26
- const { getWorkspaceRoot, isHarnessRepo } = require('../../scripts/lib/workspace');
26
+ const { getHarnessRoot, requireWorkspace } = require('./runtime-paths');
27
+
28
+ function resolveLocalRuntime(checkOnly) {
29
+ try {
30
+ const { getWorkspaceRoot, isHarnessRepo } = requireWorkspace();
31
+ return { getWorkspaceRoot, isHarnessRepo, harnessSourceDir: getHarnessRoot() };
32
+ } catch (err) {
33
+ const message = err && err.message ? err.message : String(err);
34
+ if (!/Harness runtime root not found/.test(message)) throw err;
35
+
36
+ if (checkOnly) {
37
+ console.log('[Harness Self-Heal Audit]');
38
+ console.log(' SKIPPED Local repair runtime unavailable on this distribution surface.');
39
+ console.log('\nRead-only audit skipped; no workspace files were changed.');
40
+ return null;
41
+ }
42
+
43
+ console.error('Cannot repair: local Harness installer/runtime is unavailable on this distribution surface.');
44
+ process.exit(1);
45
+ }
46
+ }
27
47
 
28
48
  const MARKER = 'Harness OS Guidance (Advisory)';
29
49
  const HOOK_ID = 'harness:pre:bootstrap';
@@ -193,8 +213,11 @@ function main() {
193
213
  const checkOnly = process.argv.includes('--check');
194
214
  const forceSelf = process.argv.includes('--force-self');
195
215
 
216
+ const runtime = resolveLocalRuntime(checkOnly);
217
+ if (!runtime) return;
218
+
219
+ const { getWorkspaceRoot, isHarnessRepo, harnessSourceDir } = runtime;
196
220
  const workspaceRoot = getWorkspaceRoot();
197
- const harnessSourceDir = path.resolve(__dirname, '..', '..');
198
221
  const installerPath = path.join(harnessSourceDir, 'scripts', 'installer.js');
199
222
 
200
223
  console.log(`Workspace: ${workspaceRoot}`);
@@ -5,6 +5,7 @@ const {
5
5
  buildRouterContract,
6
6
  writeRouterContract,
7
7
  } = require('./router-contract');
8
+ const { requireWorkspace } = require('./runtime-paths');
8
9
 
9
10
  const DEFAULT_ROUTING_CONFIG = {
10
11
  tiers: { tier3: [], tier2: [] },
@@ -163,7 +164,7 @@ function plannerInputsFromContext(context, promptLower) {
163
164
  function emitDynamicSkills(promptLower, context, recommendedGuides) {
164
165
  try {
165
166
  const userHome = process.env.HOME || process.env.USERPROFILE || '';
166
- const { getWorkspaceRoot } = require('../../scripts/lib/workspace');
167
+ const { getWorkspaceRoot } = requireWorkspace();
167
168
  const workspaceRoot = getWorkspaceRoot(context);
168
169
 
169
170
  const manifestPaths = [
@@ -315,11 +316,6 @@ function run(userPrompt, context) {
315
316
  console.log(`\n=> RECOMMENDED TIER: ${recommendedTier}`);
316
317
  console.log(`=> RATIONALE: ${rationale}`);
317
318
 
318
- if (recommendedTier.startsWith('Tier 2') || recommendedTier.startsWith('Tier 3')) {
319
- console.log(`\n=> BASE EXECUTION LOOP: Load 'todo-driven-workflow' and initialize its checklist (3-7 verifiable sub-tasks) BEFORE editing any file.`);
320
- console.log(` Track exactly ONE item in-progress at a time; verify with real evidence before marking completed.`);
321
- }
322
-
323
319
  const requestedFableModel = detectFableModel(promptLower);
324
320
  if (requestedFableModel) {
325
321
  console.log(`\n=> REQUESTED FABLE MODEL MODE: ${requestedFableModel}`);
@@ -41,8 +41,15 @@ const detectedWorkspaceRoot = getWorkspaceRoot();
41
41
  // fallback private to global mode; main() rejects non-global work from a
42
42
  // directory whose workspace identity cannot be resolved.
43
43
  const workspaceRoot = detectedWorkspaceRoot || path.resolve(process.cwd());
44
- const harnessSourceDir = path.resolve(__dirname, '..');
45
- const packageVersion = require(path.join(harnessSourceDir, 'package.json')).version;
44
+ const harnessPackageRoot = path.resolve(__dirname, '..');
45
+ const harnessSourceDir = fs.existsSync(path.join(harnessPackageRoot, 'plugin.json')) &&
46
+ fs.existsSync(path.join(harnessPackageRoot, 'skills'))
47
+ ? path.join(harnessPackageRoot, 'skills')
48
+ : harnessPackageRoot;
49
+ const packageManifestPath = fs.existsSync(path.join(harnessPackageRoot, 'package.json'))
50
+ ? path.join(harnessPackageRoot, 'package.json')
51
+ : path.join(harnessPackageRoot, 'plugin.json');
52
+ const packageVersion = require(packageManifestPath).version;
46
53
 
47
54
  let platformModules = [];
48
55
  try {
@@ -80,11 +87,11 @@ async function main() {
80
87
  // auto-detection explicitly - --symlink surfaces a link failure as an
81
88
  // error instead of silently falling back, since the user asked for it.
82
89
  const linkMode = args.includes('--copy') ? 'copy' : args.includes('--symlink') ? 'symlink' : 'auto';
83
-
90
+
84
91
  let requestedSkills = ['add', 'skill', 'skills'].includes(command)
85
92
  ? args.slice(3).filter(arg => !arg.startsWith('-'))
86
93
  : [];
87
-
94
+
88
95
  const skillFlagIdx = args.indexOf('--skill') !== -1 ? args.indexOf('--skill') : args.indexOf('--skills');
89
96
  if (skillFlagIdx !== -1 && args[skillFlagIdx + 1] && !args[skillFlagIdx + 1].startsWith('-')) {
90
97
  requestedSkills.push(args[skillFlagIdx + 1]);
@@ -246,6 +253,7 @@ async function main() {
246
253
  isGlobal,
247
254
  targetWorkspaceRoot,
248
255
  harnessSourceDir,
256
+ harnessRuntimeRoot: harnessPackageRoot,
249
257
  packageVersion,
250
258
  getUserPromptsDir,
251
259
  advisory,
@@ -65,8 +65,11 @@ syncTree(sourceHooksScriptsRoot, packageHookScriptsRoot);
65
65
  const packageWorkspace = path.join(packageHookScriptsRoot, 'lib', 'workspace.js');
66
66
  syncTree(path.join(ROOT, 'scripts', 'lib', 'workspace.js'), packageWorkspace);
67
67
 
68
- const packageAdvisoryText = path.join(pluginRoot, 'scripts', 'lib', 'advisory-text.js');
69
- syncTree(path.join(ROOT, 'scripts', 'lib', 'advisory-text.js'), packageAdvisoryText);
68
+ // Self-heal delegates to the installer. Package the installer and all of its
69
+ // small local libraries under the plugin root so a published plugin does not
70
+ // reach back into the source checkout.
71
+ syncTree(path.join(ROOT, 'scripts', 'installer.js'), path.join(pluginRoot, 'scripts', 'installer.js'));
72
+ syncTree(path.join(ROOT, 'scripts', 'lib'), path.join(pluginRoot, 'scripts', 'lib'));
70
73
 
71
74
  // Contract-integrity is shared runtime, not a directly-routed 27th skill.
72
75
  // Package it under the plugin root and rewrite its one canonical TDD import