harness-everything 0.16.3 → 0.16.5
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -0
- package/ci/mechanism-2am-openai-packaged-helper-runtime.test.js +82 -0
- package/ci/mechanism-33-issues-205-207.test.js +59 -0
- package/ci/skill-routing-check.js +1 -1
- package/commitlint.config.js +2 -2
- package/docker-verify.sh +18 -22
- package/docs/architecture.md +1 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/bootstrap.js +4 -3
- package/harness-everything/scripts/runtime-paths.js +37 -0
- package/harness-everything/scripts/self-heal.js +3 -2
- package/harness-everything/scripts/tier-router.js +2 -6
- package/hooks/scripts/lib/platforms/claude.js +3 -3
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/platforms/claude.js +3 -3
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/installer.js +629 -0
- package/plugins/harness-everything/scripts/lib/claude-hooks.js +84 -0
- package/plugins/harness-everything/scripts/lib/execution-contract.js +70 -0
- package/plugins/harness-everything/scripts/lib/gitignore.js +199 -0
- package/plugins/harness-everything/scripts/lib/manifest.js +142 -0
- package/plugins/harness-everything/scripts/lib/path-boundary.js +90 -0
- package/plugins/harness-everything/scripts/lib/prompts.js +241 -0
- package/plugins/harness-everything/scripts/lib/skills.js +314 -0
- package/plugins/harness-everything/scripts/lib/workspace.js +290 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/bootstrap.js +4 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/runtime-paths.js +37 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/self-heal.js +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/tier-router.js +2 -6
- package/scripts/installer.js +12 -4
- 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
|
+
};
|
|
@@ -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
|
-
} =
|
|
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 } =
|
|
166
|
+
const { isHarnessRepo } = requireWorkspace();
|
|
166
167
|
const auditingSelf = isHarnessRepo(workspaceRoot)
|
|
167
|
-
|| path.resolve(workspaceRoot) === path.resolve(
|
|
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
|
+
};
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { execSync } = require('child_process');
|
|
26
|
-
const {
|
|
26
|
+
const { getHarnessRoot, requireWorkspace } = require('./runtime-paths');
|
|
27
|
+
const { getWorkspaceRoot, isHarnessRepo } = requireWorkspace();
|
|
27
28
|
|
|
28
29
|
const MARKER = 'Harness OS Guidance (Advisory)';
|
|
29
30
|
const HOOK_ID = 'harness:pre:bootstrap';
|
|
@@ -194,7 +195,7 @@ function main() {
|
|
|
194
195
|
const forceSelf = process.argv.includes('--force-self');
|
|
195
196
|
|
|
196
197
|
const workspaceRoot = getWorkspaceRoot();
|
|
197
|
-
const harnessSourceDir =
|
|
198
|
+
const harnessSourceDir = getHarnessRoot();
|
|
198
199
|
const installerPath = path.join(harnessSourceDir, 'scripts', 'installer.js');
|
|
199
200
|
|
|
200
201
|
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 } =
|
|
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}`);
|
package/scripts/installer.js
CHANGED
|
@@ -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
|
|
45
|
-
const
|
|
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
|
-
|
|
69
|
-
|
|
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
|