create-harness-vibe-coding 0.8.17 → 0.8.19
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 +22 -0
- package/README-CN.md +2 -0
- package/README.md +3 -0
- package/package.json +5 -2
- package/src/generator.js +613 -97
- package/src/index.js +556 -309
- package/src/prompts.js +18 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +5 -0
- package/templates/common/.claude/commands/wf-task-archive.md +26 -0
- package/templates/common/.claude/commands/wf-task-list.md +26 -0
- package/templates/common/.claude/commands/wf-task-record.md +24 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
- package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +105 -47
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +5 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
- package/templates/common/.opencode/commands/wf-task-list.md +29 -0
- package/templates/common/.opencode/commands/wf-task-record.md +27 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +8 -6
- package/templates/common/Harness/MEMORY.md +11 -0
- package/templates/common/Harness/README.md +21 -38
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +142 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +949 -26
- package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
- package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- package/templates/common/Harness/specs/guides/SETUP.md +8 -0
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
- package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
- package/templates/common/Harness/specs/runtime/subagents.md +6 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
- package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
- package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/STATE.json +6 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
package/src/generator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
@@ -70,12 +71,45 @@ const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates', 'common');
|
|
|
70
71
|
const OPTIONAL_DIR = path.resolve(__dirname, '..', 'templates', 'optional');
|
|
71
72
|
const OPTIONAL_CATALOG = path.join(OPTIONAL_DIR, 'catalog.json');
|
|
72
73
|
const VALID_CONFLICT_POLICIES = new Set(['fail', 'skip', 'backup', 'overwrite']);
|
|
74
|
+
const VALID_INSTALL_SCOPES = new Set(['project', 'global']);
|
|
73
75
|
const RETIRED_OPTION_MESSAGES = new Map([
|
|
74
76
|
['browser-e2e', 'Optional workflow "browser-e2e" has been retired; wf-browser is built in by default. Use /wf-browser, $wf-browser, or /skills wf-browser instead.'],
|
|
75
77
|
]);
|
|
76
78
|
const EMPTY_DIRS = [
|
|
77
79
|
'tests',
|
|
78
80
|
];
|
|
81
|
+
const GLOBAL_RUNTIME_EXCLUDE = [
|
|
82
|
+
/^Harness\/PROGRESS\.md$/,
|
|
83
|
+
/^Harness\/tasks\//,
|
|
84
|
+
/^Harness\/research\//,
|
|
85
|
+
/^Harness\/project\//,
|
|
86
|
+
];
|
|
87
|
+
const GLOBAL_PROJECT_STATE_INCLUDE = [
|
|
88
|
+
/^Harness\/\.harness-version$/,
|
|
89
|
+
/^Harness\/PROGRESS\.md$/,
|
|
90
|
+
/^Harness\/settings\.json$/,
|
|
91
|
+
/^Harness\/tasks\//,
|
|
92
|
+
/^Harness\/memory\//,
|
|
93
|
+
/^Harness\/research\//,
|
|
94
|
+
/^Harness\/project\//,
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const GLOBAL_PROJECT_TEMPLATE_DESTS = [
|
|
98
|
+
'AGENTS.md',
|
|
99
|
+
'CLAUDE.md',
|
|
100
|
+
'Harness/README.md',
|
|
101
|
+
'Harness/MEMORY.md',
|
|
102
|
+
'Harness/settings.json',
|
|
103
|
+
'Harness/specs/guides/SETUP.md',
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const PROJECT_USER_STATE_PATTERNS = [
|
|
107
|
+
'Harness/PROGRESS.md',
|
|
108
|
+
'Harness/tasks/**',
|
|
109
|
+
'Harness/memory/**',
|
|
110
|
+
'Harness/research/**',
|
|
111
|
+
'Harness/project/**',
|
|
112
|
+
];
|
|
79
113
|
|
|
80
114
|
export function harnessDest(file) {
|
|
81
115
|
if (file === '.harness-version') return 'Harness/.harness-version';
|
|
@@ -123,6 +157,262 @@ function normalizePath(filePath) {
|
|
|
123
157
|
return filePath.replace(/\\/g, '/');
|
|
124
158
|
}
|
|
125
159
|
|
|
160
|
+
function comparablePath(filePath) {
|
|
161
|
+
const resolved = path.resolve(filePath);
|
|
162
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function containsPath(parent, child) {
|
|
166
|
+
const rel = path.relative(comparablePath(parent), comparablePath(child));
|
|
167
|
+
return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pathsOverlap(a, b) {
|
|
171
|
+
return containsPath(a, b) || containsPath(b, a);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function defaultGlobalDir() {
|
|
175
|
+
return process.env.HARNESS_GLOBAL_HOME
|
|
176
|
+
|| path.join(os.homedir(), '.harness', 'create-harness-vibe-coding');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function defaultHostGlobalRoots(hostGlobalDir = undefined) {
|
|
180
|
+
if (hostGlobalDir || process.env.HARNESS_HOST_GLOBAL_HOME) {
|
|
181
|
+
const base = path.resolve(process.cwd(), hostGlobalDir || process.env.HARNESS_HOST_GLOBAL_HOME);
|
|
182
|
+
return {
|
|
183
|
+
claude: path.join(base, 'claude'),
|
|
184
|
+
codex: path.join(base, 'codex'),
|
|
185
|
+
opencode: path.join(base, 'opencode'),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
claude: process.env.CLAUDE_HOME || path.join(os.homedir(), '.claude'),
|
|
191
|
+
codex: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
|
|
192
|
+
opencode: process.env.OPENCODE_CONFIG_HOME || path.join(os.homedir(), '.config', 'opencode'),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isGlobalRuntimeDest(dest) {
|
|
197
|
+
return !GLOBAL_RUNTIME_EXCLUDE.some(pattern => pattern.test(dest));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isGlobalProjectStateDest(dest) {
|
|
201
|
+
return GLOBAL_PROJECT_STATE_INCLUDE.some(pattern => pattern.test(dest));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderInlineTemplate(content, vars) {
|
|
205
|
+
let rendered = content;
|
|
206
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
207
|
+
rendered = rendered.replaceAll(`{{${key}}}`, value);
|
|
208
|
+
}
|
|
209
|
+
return rendered;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function virtualSpec(dest, content) {
|
|
213
|
+
return {
|
|
214
|
+
dest,
|
|
215
|
+
type: 'virtual',
|
|
216
|
+
content,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function mapHostSpec(spec, fromPrefix, toPrefix = '') {
|
|
221
|
+
return {
|
|
222
|
+
...spec,
|
|
223
|
+
dest: `${toPrefix}${spec.dest.slice(fromPrefix.length)}`,
|
|
224
|
+
projectDest: spec.dest,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function mapExactHostSpec(spec, dest) {
|
|
229
|
+
return {
|
|
230
|
+
...spec,
|
|
231
|
+
dest,
|
|
232
|
+
projectDest: spec.dest,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function createHostGlobalFileSpecs(allFileSpecs) {
|
|
237
|
+
const byDest = new Map(allFileSpecs.map(spec => [spec.dest, spec]));
|
|
238
|
+
const claude = [];
|
|
239
|
+
const codex = [];
|
|
240
|
+
const opencode = [];
|
|
241
|
+
|
|
242
|
+
for (const spec of allFileSpecs) {
|
|
243
|
+
if (spec.dest.startsWith('.claude/commands/')) claude.push(mapHostSpec(spec, '.claude/'));
|
|
244
|
+
if (spec.dest.startsWith('.claude/skills/')) claude.push(mapHostSpec(spec, '.claude/'));
|
|
245
|
+
if (spec.dest.startsWith('.claude/agents/')) claude.push(mapHostSpec(spec, '.claude/'));
|
|
246
|
+
if (spec.dest.startsWith('.claude/rules/')) claude.push(mapHostSpec(spec, '.claude/'));
|
|
247
|
+
if (spec.dest.startsWith('.agents/skills/')) codex.push(mapHostSpec(spec, '.agents/'));
|
|
248
|
+
if (spec.dest.startsWith('.opencode/commands/')) opencode.push(mapHostSpec(spec, '.opencode/'));
|
|
249
|
+
if (spec.dest.startsWith('.opencode/agents/')) opencode.push(mapHostSpec(spec, '.opencode/'));
|
|
250
|
+
if (spec.dest.startsWith('.opencode/plugins/')) opencode.push(mapHostSpec(spec, '.opencode/'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const claudeSettings = byDest.get('.claude/settings.json');
|
|
254
|
+
if (claudeSettings) claude.push(mapExactHostSpec(claudeSettings, 'settings.json'));
|
|
255
|
+
|
|
256
|
+
const codexConfig = byDest.get('.codex/config.toml');
|
|
257
|
+
if (codexConfig) codex.push(mapExactHostSpec(codexConfig, 'config.toml'));
|
|
258
|
+
const codexHooks = byDest.get('.codex/hooks.json');
|
|
259
|
+
if (codexHooks) codex.push(mapExactHostSpec(codexHooks, 'hooks.json'));
|
|
260
|
+
|
|
261
|
+
const opencodeConfig = byDest.get('opencode.json');
|
|
262
|
+
if (opencodeConfig) opencode.push(mapExactHostSpec(opencodeConfig, 'opencode.json'));
|
|
263
|
+
|
|
264
|
+
return { claude, codex, opencode };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function createHostGlobalPlans(allFileSpecs, hostGlobalDir = undefined) {
|
|
268
|
+
const roots = defaultHostGlobalRoots(hostGlobalDir);
|
|
269
|
+
const specs = createHostGlobalFileSpecs(allFileSpecs);
|
|
270
|
+
return Object.entries(specs).map(([host, fileSpecs]) => ({
|
|
271
|
+
host,
|
|
272
|
+
root: roots[host],
|
|
273
|
+
fileSpecs,
|
|
274
|
+
specsByDest: new Map(fileSpecs.map(spec => [spec.dest, spec])),
|
|
275
|
+
plan: createPlan(roots[host], fileSpecs),
|
|
276
|
+
candidateSet: new Set(fileSpecs.map(spec => spec.dest)),
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createOwnershipClasses({ resolvedGlobalDir, hostPlans }) {
|
|
281
|
+
const hostGlobalTemplates = {};
|
|
282
|
+
for (const hostPlan of hostPlans || []) {
|
|
283
|
+
hostGlobalTemplates[hostPlan.host] = {
|
|
284
|
+
root: normalizePath(hostPlan.root),
|
|
285
|
+
paths: hostPlan.fileSpecs.map(spec => spec.dest).sort(),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
projectUserState: [...PROJECT_USER_STATE_PATTERNS],
|
|
291
|
+
projectTemplateBridge: [...GLOBAL_PROJECT_TEMPLATE_DESTS],
|
|
292
|
+
globalRuntimeTemplate: resolvedGlobalDir ? normalizePath(resolvedGlobalDir) : null,
|
|
293
|
+
hostGlobalTemplates,
|
|
294
|
+
userOwnedProtection: 'Existing non-Harness files at Harness-interest paths are preserved or surfaced as conflicts; generated project template files follow the selected conflict policy.',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createGlobalProjectBridgeSpecs({ globalDir }) {
|
|
299
|
+
const globalPath = normalizePath(globalDir);
|
|
300
|
+
return [
|
|
301
|
+
virtualSpec('CLAUDE.md', `# CLAUDE.md
|
|
302
|
+
|
|
303
|
+
This project uses a global Harness runtime at \`${globalPath}\`.
|
|
304
|
+
|
|
305
|
+
Project-local state is authoritative:
|
|
306
|
+
|
|
307
|
+
- Tasks: \`Harness/tasks/\`
|
|
308
|
+
- Active pointer: \`Harness/PROGRESS.md\`
|
|
309
|
+
- Project memory: \`Harness/MEMORY.md\` and \`Harness/memory/\`
|
|
310
|
+
- Project settings: \`Harness/settings.json\`
|
|
311
|
+
|
|
312
|
+
Load global workflow docs, command surfaces, agents, and scripts from \`${globalPath}\`.
|
|
313
|
+
Project settings override global settings. Do not store project task state in the global runtime. Do not store secrets or raw transcripts in project or global memory/settings.
|
|
314
|
+
`),
|
|
315
|
+
virtualSpec('AGENTS.md', `# AGENTS.md
|
|
316
|
+
|
|
317
|
+
Read \`CLAUDE.md\` first. This project uses the global Harness runtime at \`${globalPath}\`.
|
|
318
|
+
|
|
319
|
+
Keep task capsules, project memory, and project settings under local \`Harness/\`; use the global runtime only for shared framework docs, commands, skills, agents, settings defaults, and scripts.
|
|
320
|
+
`),
|
|
321
|
+
virtualSpec('Harness/README.md', `# {{projectName}} - Project Harness State
|
|
322
|
+
|
|
323
|
+
This project is bound to a global Harness runtime at \`${globalPath}\`.
|
|
324
|
+
|
|
325
|
+
Project-local files:
|
|
326
|
+
|
|
327
|
+
- \`Harness/PROGRESS.md\` - active task pointer and task index.
|
|
328
|
+
- \`Harness/tasks/\` - task capsules; always project-local.
|
|
329
|
+
- \`Harness/MEMORY.md\` and \`Harness/memory/\` - project memory.
|
|
330
|
+
- \`Harness/settings.json\` - project settings and overrides.
|
|
331
|
+
- \`Harness/research/\` and \`Harness/project/\` - project facts.
|
|
332
|
+
|
|
333
|
+
Global runtime files:
|
|
334
|
+
|
|
335
|
+
- \`${globalPath}/Harness/README.md\` - shared Harness router.
|
|
336
|
+
- \`${globalPath}/Harness/MEMORY.md\` and \`${globalPath}/Harness/memory/\` - global memory and resource index.
|
|
337
|
+
- \`${globalPath}/Harness/settings.json\` - global settings defaults.
|
|
338
|
+
- \`${globalPath}/.claude/\`, \`${globalPath}/.agents/\`, \`${globalPath}/.opencode/\` - shared runtime command, skill, and agent surfaces.
|
|
339
|
+
|
|
340
|
+
Never discover task context from the global runtime. Resume work from local \`Harness/PROGRESS.md\` and local task \`STATE.json\`.
|
|
341
|
+
Project settings override global settings. Existing user-authored files are preserved unless the user explicitly chooses an overwrite-capable conflict policy.
|
|
342
|
+
`),
|
|
343
|
+
virtualSpec('Harness/MEMORY.md', `# MEMORY.md - {{projectName}} Project Memory
|
|
344
|
+
|
|
345
|
+
Project memory lives here and under \`Harness/memory/\`.
|
|
346
|
+
|
|
347
|
+
Global memory lives under \`${globalPath}/Harness/memory/\` and is only for cross-project lessons and preferences.
|
|
348
|
+
|
|
349
|
+
Project settings live in \`Harness/settings.json\`; global settings defaults live in \`${globalPath}/Harness/settings.json\`. Project settings win.
|
|
350
|
+
|
|
351
|
+
Rules:
|
|
352
|
+
|
|
353
|
+
- Keep project-specific decisions, task evidence, and repo facts in this project.
|
|
354
|
+
- Keep reusable cross-project lessons in global memory only when safe and non-secret.
|
|
355
|
+
- Never store project task state, secrets, raw transcripts, or private project facts in global memory/settings.
|
|
356
|
+
`),
|
|
357
|
+
virtualSpec('Harness/specs/guides/SETUP.md', `# SETUP.md - {{projectName}} Global Harness Bridge
|
|
358
|
+
|
|
359
|
+
This project uses a global Harness runtime at \`${globalPath}\`.
|
|
360
|
+
|
|
361
|
+
Read the full setup guide from \`${globalPath}/Harness/specs/guides/SETUP.md\`.
|
|
362
|
+
|
|
363
|
+
Project-local state remains authoritative:
|
|
364
|
+
|
|
365
|
+
- \`Harness/PROGRESS.md\`
|
|
366
|
+
- \`Harness/tasks/\`
|
|
367
|
+
- \`Harness/memory/\`
|
|
368
|
+
- \`Harness/settings.json\`
|
|
369
|
+
- \`Harness/research/\`
|
|
370
|
+
- \`Harness/project/\`
|
|
371
|
+
|
|
372
|
+
Run validators and shared scripts from the global runtime unless this project later installs a full project-local Harness runtime.
|
|
373
|
+
Project settings override global settings. Existing user-authored files at Harness-interest paths are preserved or surfaced for review; generated project template files follow the selected conflict policy.
|
|
374
|
+
`),
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function installMetadata({ installScope, resolvedGlobalDir, hostPlans = [] }) {
|
|
379
|
+
if (installScope !== 'global') return { installScope: 'project' };
|
|
380
|
+
return {
|
|
381
|
+
installScope: 'global',
|
|
382
|
+
globalDir: normalizePath(resolvedGlobalDir),
|
|
383
|
+
copyMode: 'copy',
|
|
384
|
+
projectState: {
|
|
385
|
+
tasks: 'Harness/tasks/',
|
|
386
|
+
progress: 'Harness/PROGRESS.md',
|
|
387
|
+
memory: 'Harness/memory/',
|
|
388
|
+
settings: 'Harness/settings.json',
|
|
389
|
+
research: 'Harness/research/',
|
|
390
|
+
project: 'Harness/project/',
|
|
391
|
+
},
|
|
392
|
+
memoryScopes: {
|
|
393
|
+
project: 'Harness/memory/',
|
|
394
|
+
global: `${normalizePath(resolvedGlobalDir)}/Harness/memory/`,
|
|
395
|
+
},
|
|
396
|
+
settingsScopes: {
|
|
397
|
+
precedence: ['project', 'global'],
|
|
398
|
+
project: 'Harness/settings.json',
|
|
399
|
+
global: `${normalizePath(resolvedGlobalDir)}/Harness/settings.json`,
|
|
400
|
+
secretsPolicy: 'never store secrets in project/global settings or memory',
|
|
401
|
+
},
|
|
402
|
+
hostGlobal: {
|
|
403
|
+
copyMode: 'copy',
|
|
404
|
+
targets: Object.fromEntries(hostPlans.map(hostPlan => [
|
|
405
|
+
hostPlan.host,
|
|
406
|
+
{
|
|
407
|
+
root: normalizePath(hostPlan.root),
|
|
408
|
+
files: hostPlan.fileSpecs.map(spec => spec.dest).sort(),
|
|
409
|
+
},
|
|
410
|
+
])),
|
|
411
|
+
},
|
|
412
|
+
ownershipClasses: createOwnershipClasses({ resolvedGlobalDir, hostPlans }),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
126
416
|
export function getOptionalCatalog() {
|
|
127
417
|
if (!fs.existsSync(OPTIONAL_CATALOG)) {
|
|
128
418
|
return { skills: [], presets: {}, externalRecommendations: [] };
|
|
@@ -287,7 +577,7 @@ function symlinkInChain(rootDir, absPath) {
|
|
|
287
577
|
return null;
|
|
288
578
|
}
|
|
289
579
|
|
|
290
|
-
function createPlan(resolvedDir, fileSpecs) {
|
|
580
|
+
function createPlan(resolvedDir, fileSpecs, { includeEmptyDirs = true } = {}) {
|
|
291
581
|
const plan = {
|
|
292
582
|
create: [],
|
|
293
583
|
skip: [],
|
|
@@ -308,8 +598,10 @@ function createPlan(resolvedDir, fileSpecs) {
|
|
|
308
598
|
dir = path.posix.dirname(dir);
|
|
309
599
|
}
|
|
310
600
|
}
|
|
311
|
-
|
|
312
|
-
|
|
601
|
+
if (includeEmptyDirs) {
|
|
602
|
+
for (const dir of EMPTY_DIRS) {
|
|
603
|
+
dirSet.add(dir);
|
|
604
|
+
}
|
|
313
605
|
}
|
|
314
606
|
|
|
315
607
|
for (const dir of [...dirSet].sort()) {
|
|
@@ -414,20 +706,33 @@ export function isHarnessOwnedContent(content) {
|
|
|
414
706
|
/**
|
|
415
707
|
* Returns true if an existing on-disk file is a Harness-interest candidate path
|
|
416
708
|
* (per the manifest-first candidate set) that was NOT authored by this scaffold
|
|
417
|
-
* (candidate path + no ownership marker
|
|
418
|
-
* (skip) regardless of the conflict policy,
|
|
709
|
+
* (candidate path + no ownership marker + no matching template content).
|
|
710
|
+
* Such user-authored files are preserved (skip) regardless of the conflict policy,
|
|
711
|
+
* mirroring the updater safety rule.
|
|
712
|
+
*
|
|
713
|
+
* Ownership is determined by content marker first, then by exact template content
|
|
714
|
+
* match (for JSON/TOML configs without markers). Manifest declaration alone does
|
|
715
|
+
* NOT grant ownership — a user file at a manifest path without marker is still
|
|
716
|
+
* user-owned.
|
|
419
717
|
*/
|
|
420
|
-
function existingCandidateIsUserOwned(dest, destPath, candidateSet) {
|
|
718
|
+
function existingCandidateIsUserOwned(dest, destPath, candidateSet, srcPath = null) {
|
|
421
719
|
if (!candidateSet.has(dest)) return false;
|
|
422
720
|
try {
|
|
423
721
|
const content = fs.readFileSync(destPath, 'utf-8');
|
|
424
|
-
|
|
722
|
+
// Harness content marker present → Harness-owned
|
|
723
|
+
if (isHarnessOwnedContent(content)) return false;
|
|
724
|
+
// Same content as template source → Harness-generated → Harness-owned
|
|
725
|
+
if (srcPath && fs.existsSync(srcPath)) {
|
|
726
|
+
const templateContent = fs.readFileSync(srcPath, 'utf-8');
|
|
727
|
+
if (content === templateContent) return false;
|
|
728
|
+
}
|
|
729
|
+
return true; // user-owned: no marker, not template content
|
|
425
730
|
} catch {
|
|
426
731
|
return false;
|
|
427
732
|
}
|
|
428
733
|
}
|
|
429
734
|
|
|
430
|
-
function addFileActions(plan, fileSpecs, resolvedDir, onConflict, candidateSet) {
|
|
735
|
+
function addFileActions(plan, fileSpecs, resolvedDir, onConflict, candidateSet, specsByDest = null) {
|
|
431
736
|
for (const { dest: file } of fileSpecs) {
|
|
432
737
|
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
433
738
|
|
|
@@ -445,15 +750,20 @@ function addFileActions(plan, fileSpecs, resolvedDir, onConflict, candidateSet)
|
|
|
445
750
|
continue;
|
|
446
751
|
}
|
|
447
752
|
|
|
753
|
+
// Look up the template source path for content comparison
|
|
754
|
+
const spec = specsByDest ? specsByDest.get(file) : null;
|
|
755
|
+
const srcPath = spec && spec.type !== 'empty' && spec.type !== 'virtual' ? spec.src : null;
|
|
756
|
+
|
|
448
757
|
// G4 guard: a non-Harness same-name candidate (any Harness-interest path
|
|
449
758
|
// — agent/command/skill/optional-skill/script per the manifest-first
|
|
450
759
|
// candidate set — without an ownership marker) must NEVER be overwritten.
|
|
451
760
|
// Under overwrite/backup/skip it is preserved via plan.skip; under fail it
|
|
452
761
|
// is blocked via plan.conflict so the collision surfaces (matching the fail
|
|
453
|
-
// branch below). Only Harness-owned files (marker present
|
|
454
|
-
// policy switch. Mirrors the
|
|
455
|
-
// existingCandidateIsUserOwned /
|
|
456
|
-
|
|
762
|
+
// branch below). Only Harness-owned files (marker present or template
|
|
763
|
+
// content match) reach the normal policy switch. Mirrors the
|
|
764
|
+
// wf-update-check.mjs safety rule; see existingCandidateIsUserOwned /
|
|
765
|
+
// isHarnessOwnedContent above.
|
|
766
|
+
if (existingCandidateIsUserOwned(file, destPath, candidateSet, srcPath)) {
|
|
457
767
|
if (onConflict === 'fail') {
|
|
458
768
|
plan.conflict.push(file);
|
|
459
769
|
} else {
|
|
@@ -486,6 +796,14 @@ function createSummary(plan, { noWrites = false } = {}) {
|
|
|
486
796
|
};
|
|
487
797
|
}
|
|
488
798
|
|
|
799
|
+
function createHostSummary(hostPlans, options = {}) {
|
|
800
|
+
return hostPlans.map(hostPlan => ({
|
|
801
|
+
host: hostPlan.host,
|
|
802
|
+
root: normalizePath(hostPlan.root),
|
|
803
|
+
summary: createSummary(hostPlan.plan, options),
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
|
|
489
807
|
function nextBackupPath(destPath) {
|
|
490
808
|
const first = `${destPath}.harness-backup`;
|
|
491
809
|
if (!fs.existsSync(first)) {
|
|
@@ -562,6 +880,103 @@ function registrationWarnings(plan, selectedSkills) {
|
|
|
562
880
|
return warnings;
|
|
563
881
|
}
|
|
564
882
|
|
|
883
|
+
function writeGeneratedPlan({
|
|
884
|
+
plan,
|
|
885
|
+
fileSpecs,
|
|
886
|
+
specsByDest,
|
|
887
|
+
resolvedDir,
|
|
888
|
+
vars,
|
|
889
|
+
optional,
|
|
890
|
+
metadata,
|
|
891
|
+
createdPrefix = '',
|
|
892
|
+
}) {
|
|
893
|
+
const created = [];
|
|
894
|
+
const generatedFiles = [];
|
|
895
|
+
|
|
896
|
+
for (const dir of plan.mkdir) {
|
|
897
|
+
const dirPath = dir === './'
|
|
898
|
+
? resolvedDir
|
|
899
|
+
: path.join(resolvedDir, ...dir.slice(0, -1).split('/'));
|
|
900
|
+
const __slMkdir = symlinkInChain(resolvedDir, dirPath);
|
|
901
|
+
if (__slMkdir) throw new Error(`Cannot create directory ${createdPrefix}${dir}: symlinked directory in path (${__slMkdir})`);
|
|
902
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
903
|
+
created.push(`${createdPrefix}${dir === './' ? './ (directory)' : `${dir} (directory)`}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
for (const file of plan.backup) {
|
|
907
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
908
|
+
const __slBackup = symlinkInChain(resolvedDir, destPath);
|
|
909
|
+
if (__slBackup) throw new Error(`Cannot backup ${createdPrefix}${file}: symlinked directory in path (${__slBackup})`);
|
|
910
|
+
if (fs.existsSync(destPath)) {
|
|
911
|
+
try { if (fs.lstatSync(destPath).isSymbolicLink()) throw new Error('Symlink rejected'); } catch (e) { throw new Error(`Cannot backup ${createdPrefix}${file}: ${e.message}`); }
|
|
912
|
+
}
|
|
913
|
+
fs.renameSync(destPath, nextBackupPath(destPath));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const writableFiles = [
|
|
917
|
+
...plan.create,
|
|
918
|
+
...plan.overwrite,
|
|
919
|
+
...plan.backup,
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
const HARNESS_VERSION_DEST = 'Harness/.harness-version';
|
|
923
|
+
let harnessVersionSpec = null;
|
|
924
|
+
|
|
925
|
+
for (const file of writableFiles) {
|
|
926
|
+
if (file === HARNESS_VERSION_DEST) {
|
|
927
|
+
harnessVersionSpec = specsByDest.get(file);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const spec = specsByDest.get(file);
|
|
931
|
+
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
932
|
+
const __slWrite = symlinkInChain(resolvedDir, destPath);
|
|
933
|
+
if (__slWrite) throw new Error(`Cannot create/overwrite ${createdPrefix}${file}: symlinked directory in path (${__slWrite})`);
|
|
934
|
+
if (fs.existsSync(destPath)) {
|
|
935
|
+
try { if (fs.lstatSync(destPath).isSymbolicLink()) throw new Error('Symlink rejected'); } catch (e) { throw new Error(`Cannot create/overwrite ${createdPrefix}${file}: ${e.message}`); }
|
|
936
|
+
}
|
|
937
|
+
let content = spec.type === 'empty'
|
|
938
|
+
? ''
|
|
939
|
+
: spec.type === 'virtual'
|
|
940
|
+
? renderInlineTemplate(spec.content, vars)
|
|
941
|
+
: renderTemplate(spec.src, vars);
|
|
942
|
+
content = registerOptionalContent(file, content, optional.selectedSkills);
|
|
943
|
+
content = registerExternalRecommendations(file, content, optional.selectedRecommendations);
|
|
944
|
+
|
|
945
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
946
|
+
fs.writeFileSync(destPath, content, 'utf-8');
|
|
947
|
+
created.push(`${createdPrefix}${file}`);
|
|
948
|
+
generatedFiles.push({ dest: file, content });
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (harnessVersionSpec) {
|
|
952
|
+
const rendered = renderTemplate(harnessVersionSpec.src, vars);
|
|
953
|
+
let parsed;
|
|
954
|
+
try {
|
|
955
|
+
parsed = JSON.parse(rendered);
|
|
956
|
+
} catch {
|
|
957
|
+
parsed = {};
|
|
958
|
+
}
|
|
959
|
+
parsed.generator = vars.generatorVersion;
|
|
960
|
+
parsed.generated = vars.generatedTimestamp;
|
|
961
|
+
parsed.options = optional.selectedSkills.map(s => s.id);
|
|
962
|
+
parsed.externalRecommendations = optional.selectedRecommendations.map(item => item.id);
|
|
963
|
+
parsed.autoCheck = true;
|
|
964
|
+
Object.assign(parsed, metadata);
|
|
965
|
+
parsed.checksums = computeChecksums(generatedFiles);
|
|
966
|
+
const allWritableSpecs = writableFiles
|
|
967
|
+
.map(f => specsByDest.get(f))
|
|
968
|
+
.filter(Boolean);
|
|
969
|
+
parsed.sources = computeSources(allWritableSpecs);
|
|
970
|
+
const hvContent = JSON.stringify(parsed, null, 2) + '\n';
|
|
971
|
+
const hvDestPath = path.join(resolvedDir, ...HARNESS_VERSION_DEST.split('/'));
|
|
972
|
+
fs.mkdirSync(path.dirname(hvDestPath), { recursive: true });
|
|
973
|
+
fs.writeFileSync(hvDestPath, hvContent, 'utf-8');
|
|
974
|
+
created.push(`${createdPrefix}${HARNESS_VERSION_DEST}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return { created, generatedFiles };
|
|
978
|
+
}
|
|
979
|
+
|
|
565
980
|
export function generate({
|
|
566
981
|
projectName,
|
|
567
982
|
targetDir,
|
|
@@ -571,6 +986,9 @@ export function generate({
|
|
|
571
986
|
withoutOptions = [],
|
|
572
987
|
preset = undefined,
|
|
573
988
|
externalOptions = [],
|
|
989
|
+
installScope = 'project',
|
|
990
|
+
globalDir = undefined,
|
|
991
|
+
hostGlobalDir = undefined,
|
|
574
992
|
}) {
|
|
575
993
|
const created = [];
|
|
576
994
|
const errors = [];
|
|
@@ -578,6 +996,10 @@ export function generate({
|
|
|
578
996
|
|
|
579
997
|
// Resolve targetDir relative to cwd
|
|
580
998
|
const resolvedDir = path.resolve(process.cwd(), targetDir);
|
|
999
|
+
const normalizedInstallScope = String(installScope || 'project').trim().toLowerCase();
|
|
1000
|
+
const resolvedGlobalDir = normalizedInstallScope === 'global'
|
|
1001
|
+
? path.resolve(process.cwd(), globalDir || defaultGlobalDir())
|
|
1002
|
+
: undefined;
|
|
581
1003
|
const vars = {
|
|
582
1004
|
projectName,
|
|
583
1005
|
generatorVersion: pkg.version,
|
|
@@ -586,19 +1008,96 @@ export function generate({
|
|
|
586
1008
|
|
|
587
1009
|
const optional = resolveOptionalSelection({ withOptions, withoutOptions, preset, externalOptions });
|
|
588
1010
|
warnings.push(...optional.warnings);
|
|
589
|
-
const
|
|
1011
|
+
const allFileSpecs = [
|
|
590
1012
|
...createCoreFileSpecs(),
|
|
591
1013
|
...createOptionalFileSpecs(optional.selectedSkills),
|
|
592
1014
|
];
|
|
1015
|
+
const fileSpecs = normalizedInstallScope === 'global'
|
|
1016
|
+
? [
|
|
1017
|
+
...createGlobalProjectBridgeSpecs({ globalDir: resolvedGlobalDir }),
|
|
1018
|
+
...allFileSpecs.filter(spec => isGlobalProjectStateDest(spec.dest)),
|
|
1019
|
+
]
|
|
1020
|
+
: allFileSpecs;
|
|
593
1021
|
const specsByDest = new Map(fileSpecs.map(spec => [spec.dest, spec]));
|
|
594
|
-
const plan = createPlan(resolvedDir, fileSpecs);
|
|
1022
|
+
const plan = createPlan(resolvedDir, fileSpecs, { includeEmptyDirs: normalizedInstallScope !== 'global' });
|
|
1023
|
+
const globalFileSpecs = normalizedInstallScope === 'global'
|
|
1024
|
+
? allFileSpecs.filter(spec => isGlobalRuntimeDest(spec.dest))
|
|
1025
|
+
: [];
|
|
1026
|
+
const globalSpecsByDest = new Map(globalFileSpecs.map(spec => [spec.dest, spec]));
|
|
1027
|
+
const globalPlan = normalizedInstallScope === 'global'
|
|
1028
|
+
? createPlan(resolvedGlobalDir, globalFileSpecs)
|
|
1029
|
+
: undefined;
|
|
1030
|
+
const hostPlans = normalizedInstallScope === 'global'
|
|
1031
|
+
? createHostGlobalPlans(allFileSpecs, hostGlobalDir)
|
|
1032
|
+
: [];
|
|
595
1033
|
const duplicateDestinations = duplicateDests(fileSpecs);
|
|
1034
|
+
const duplicateHostDestinations = hostPlans
|
|
1035
|
+
.flatMap(hostPlan => duplicateDests(hostPlan.fileSpecs).map(dest => `${hostPlan.host}:${dest}`));
|
|
596
1036
|
const candidateSet = buildHarnessInterestCandidateSet(
|
|
597
1037
|
readOwnershipManifest(),
|
|
598
|
-
|
|
1038
|
+
allFileSpecs,
|
|
599
1039
|
optional.selectedSkills,
|
|
600
1040
|
);
|
|
601
1041
|
|
|
1042
|
+
if (!VALID_INSTALL_SCOPES.has(normalizedInstallScope)) {
|
|
1043
|
+
errors.push(`Unknown install scope "${installScope}". Use project or global.`);
|
|
1044
|
+
return {
|
|
1045
|
+
success: false,
|
|
1046
|
+
created,
|
|
1047
|
+
errors,
|
|
1048
|
+
plan,
|
|
1049
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1050
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
1051
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
1052
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan, { noWrites: true }) } : {}),
|
|
1053
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans, { noWrites: true }) } : {}),
|
|
1054
|
+
warnings,
|
|
1055
|
+
installScope: normalizedInstallScope,
|
|
1056
|
+
...(resolvedGlobalDir ? { globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (resolvedGlobalDir && pathsOverlap(resolvedDir, resolvedGlobalDir)) {
|
|
1061
|
+
errors.push('Global install directory must be outside the target project and must not contain the target project.');
|
|
1062
|
+
return {
|
|
1063
|
+
success: false,
|
|
1064
|
+
created,
|
|
1065
|
+
errors,
|
|
1066
|
+
plan,
|
|
1067
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1068
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
1069
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
1070
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan, { noWrites: true }), globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1071
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans, { noWrites: true }) } : {}),
|
|
1072
|
+
warnings,
|
|
1073
|
+
installScope: normalizedInstallScope,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
for (const hostPlan of hostPlans) {
|
|
1078
|
+
if (pathsOverlap(resolvedDir, hostPlan.root)) {
|
|
1079
|
+
errors.push(`Host-global ${hostPlan.host} directory must be outside the target project.`);
|
|
1080
|
+
}
|
|
1081
|
+
if (resolvedGlobalDir && pathsOverlap(resolvedGlobalDir, hostPlan.root)) {
|
|
1082
|
+
errors.push(`Host-global ${hostPlan.host} directory must be outside the global runtime directory.`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (errors.length > 0) {
|
|
1086
|
+
return {
|
|
1087
|
+
success: false,
|
|
1088
|
+
created,
|
|
1089
|
+
errors,
|
|
1090
|
+
plan,
|
|
1091
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1092
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
1093
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
1094
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan, { noWrites: true }), globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1095
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans, { noWrites: true }) } : {}),
|
|
1096
|
+
warnings,
|
|
1097
|
+
installScope: normalizedInstallScope,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
602
1101
|
if (!VALID_CONFLICT_POLICIES.has(onConflict)) {
|
|
603
1102
|
errors.push(`Unknown conflict policy "${onConflict}". Use fail, skip, backup, or overwrite.`);
|
|
604
1103
|
return {
|
|
@@ -634,9 +1133,34 @@ export function generate({
|
|
|
634
1133
|
warnings,
|
|
635
1134
|
};
|
|
636
1135
|
}
|
|
1136
|
+
if (duplicateHostDestinations.length > 0) {
|
|
1137
|
+
errors.push(`Generation stopped because duplicate host-global destination(s) were found: ${duplicateHostDestinations.join(', ')}`);
|
|
1138
|
+
return {
|
|
1139
|
+
success: false,
|
|
1140
|
+
created,
|
|
1141
|
+
errors,
|
|
1142
|
+
plan,
|
|
1143
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1144
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
1145
|
+
summary: createSummary(plan, { noWrites: true }),
|
|
1146
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan, { noWrites: true }) } : {}),
|
|
1147
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans, { noWrites: true }) } : {}),
|
|
1148
|
+
warnings,
|
|
1149
|
+
installScope: normalizedInstallScope,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
637
1152
|
|
|
638
|
-
addFileActions(plan, fileSpecs, resolvedDir, onConflict, candidateSet);
|
|
1153
|
+
addFileActions(plan, fileSpecs, resolvedDir, onConflict, candidateSet, specsByDest);
|
|
1154
|
+
if (globalPlan) {
|
|
1155
|
+
addFileActions(globalPlan, globalFileSpecs, resolvedGlobalDir, onConflict, candidateSet, globalSpecsByDest);
|
|
1156
|
+
}
|
|
1157
|
+
for (const hostPlan of hostPlans) {
|
|
1158
|
+
addFileActions(hostPlan.plan, hostPlan.fileSpecs, hostPlan.root, onConflict, hostPlan.candidateSet, hostPlan.specsByDest);
|
|
1159
|
+
}
|
|
639
1160
|
warnings.push(...registrationWarnings(plan, optional.selectedSkills));
|
|
1161
|
+
if (globalPlan) {
|
|
1162
|
+
warnings.push('Global install scope selected: shared runtime assets are written to the global directory and copied into Claude Code, Codex, and OpenCode host-global surfaces; Harness/tasks and Harness/PROGRESS.md remain project-local.');
|
|
1163
|
+
}
|
|
640
1164
|
|
|
641
1165
|
if (dryRun) {
|
|
642
1166
|
return {
|
|
@@ -644,14 +1168,30 @@ export function generate({
|
|
|
644
1168
|
created,
|
|
645
1169
|
errors,
|
|
646
1170
|
plan,
|
|
1171
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1172
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
647
1173
|
summary: createSummary(plan),
|
|
1174
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan), globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1175
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans) } : {}),
|
|
1176
|
+
...(globalPlan ? { ownershipClasses: createOwnershipClasses({ resolvedGlobalDir, hostPlans }) } : {}),
|
|
648
1177
|
warnings,
|
|
649
1178
|
dryRun: true,
|
|
1179
|
+
installScope: normalizedInstallScope,
|
|
650
1180
|
};
|
|
651
1181
|
}
|
|
652
1182
|
|
|
653
|
-
|
|
654
|
-
|
|
1183
|
+
const globalConflictCount = globalPlan?.conflict.length || 0;
|
|
1184
|
+
const hostConflicts = hostPlans.flatMap(hostPlan => hostPlan.plan.conflict.map(file => `${hostPlan.host}:${file}`));
|
|
1185
|
+
if (plan.conflict.length > 0 || globalConflictCount > 0 || hostConflicts.length > 0) {
|
|
1186
|
+
if (plan.conflict.length > 0) {
|
|
1187
|
+
errors.push(`Generation stopped because ${plan.conflict.length} file conflict(s) were found: ${plan.conflict.join(', ')}`);
|
|
1188
|
+
}
|
|
1189
|
+
if (globalConflictCount > 0) {
|
|
1190
|
+
errors.push(`Generation stopped because ${globalConflictCount} global file conflict(s) were found: ${globalPlan.conflict.join(', ')}`);
|
|
1191
|
+
}
|
|
1192
|
+
if (hostConflicts.length > 0) {
|
|
1193
|
+
errors.push(`Generation stopped because ${hostConflicts.length} host-global file conflict(s) were found: ${hostConflicts.join(', ')}`);
|
|
1194
|
+
}
|
|
655
1195
|
if (plan.conflict.includes('CLAUDE.md')) {
|
|
656
1196
|
errors.push('CLAUDE.md already exists. It is the root agent entry contract; ask the user to confirm refactoring or merging it with the Harness entry contract before editing, backing up, or overwriting.');
|
|
657
1197
|
}
|
|
@@ -660,92 +1200,58 @@ export function generate({
|
|
|
660
1200
|
created,
|
|
661
1201
|
errors,
|
|
662
1202
|
plan,
|
|
1203
|
+
...(globalPlan ? { globalPlan } : {}),
|
|
1204
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
663
1205
|
summary: createSummary(plan, { noWrites: true }),
|
|
1206
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan, { noWrites: true }), globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1207
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans, { noWrites: true }) } : {}),
|
|
664
1208
|
warnings,
|
|
1209
|
+
installScope: normalizedInstallScope,
|
|
665
1210
|
};
|
|
666
1211
|
}
|
|
667
1212
|
|
|
668
1213
|
try {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1214
|
+
const metadata = installMetadata({ installScope: normalizedInstallScope, resolvedGlobalDir, hostPlans });
|
|
1215
|
+
const projectWrite = writeGeneratedPlan({
|
|
1216
|
+
plan,
|
|
1217
|
+
fileSpecs,
|
|
1218
|
+
specsByDest,
|
|
1219
|
+
resolvedDir,
|
|
1220
|
+
vars,
|
|
1221
|
+
optional,
|
|
1222
|
+
metadata,
|
|
1223
|
+
});
|
|
1224
|
+
created.push(...projectWrite.created);
|
|
1225
|
+
|
|
1226
|
+
let globalCreated = [];
|
|
1227
|
+
let hostCreated = [];
|
|
1228
|
+
if (globalPlan) {
|
|
1229
|
+
const globalWrite = writeGeneratedPlan({
|
|
1230
|
+
plan: globalPlan,
|
|
1231
|
+
fileSpecs: globalFileSpecs,
|
|
1232
|
+
specsByDest: globalSpecsByDest,
|
|
1233
|
+
resolvedDir: resolvedGlobalDir,
|
|
1234
|
+
vars,
|
|
1235
|
+
optional,
|
|
1236
|
+
metadata,
|
|
1237
|
+
createdPrefix: 'global:',
|
|
1238
|
+
});
|
|
1239
|
+
created.push(...globalWrite.created);
|
|
1240
|
+
globalCreated = globalWrite.created;
|
|
688
1241
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
harnessVersionSpec = specsByDest.get(file);
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
const spec = specsByDest.get(file);
|
|
706
|
-
const destPath = path.join(resolvedDir, ...file.split('/'));
|
|
707
|
-
const __slWrite = symlinkInChain(resolvedDir, destPath);
|
|
708
|
-
if (__slWrite) throw new Error(`Cannot create/overwrite ${file}: symlinked directory in path (${__slWrite})`);
|
|
709
|
-
// Symlink safety: reject symlinks before writing to prevent path traversal outside workspace
|
|
710
|
-
if (fs.existsSync(destPath)) {
|
|
711
|
-
try { if (fs.lstatSync(destPath).isSymbolicLink()) throw new Error('Symlink rejected'); } catch (e) { throw new Error(`Cannot create/overwrite ${file}: ${e.message}`); }
|
|
712
|
-
}
|
|
713
|
-
let content = spec.type === 'empty'
|
|
714
|
-
? ''
|
|
715
|
-
: renderTemplate(spec.src, vars);
|
|
716
|
-
content = registerOptionalContent(file, content, optional.selectedSkills);
|
|
717
|
-
content = registerExternalRecommendations(file, content, optional.selectedRecommendations);
|
|
718
|
-
|
|
719
|
-
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
720
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
721
|
-
created.push(file);
|
|
722
|
-
generatedFiles.push({ dest: file, content });
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
if (harnessVersionSpec) {
|
|
726
|
-
const rendered = renderTemplate(harnessVersionSpec.src, vars);
|
|
727
|
-
let parsed;
|
|
728
|
-
try {
|
|
729
|
-
parsed = JSON.parse(rendered);
|
|
730
|
-
} catch {
|
|
731
|
-
parsed = {};
|
|
732
|
-
}
|
|
733
|
-
parsed.generator = vars.generatorVersion;
|
|
734
|
-
parsed.generated = vars.generatedTimestamp;
|
|
735
|
-
parsed.options = optional.selectedSkills.map(s => s.id);
|
|
736
|
-
parsed.externalRecommendations = optional.selectedRecommendations.map(item => item.id);
|
|
737
|
-
parsed.autoCheck = true;
|
|
738
|
-
parsed.checksums = computeChecksums(generatedFiles);
|
|
739
|
-
// Build sources map from fileSpecs (all writable files, not just generatedFiles)
|
|
740
|
-
const allWritableSpecs = writableFiles
|
|
741
|
-
.map(f => specsByDest.get(f))
|
|
742
|
-
.filter(Boolean);
|
|
743
|
-
parsed.sources = computeSources(allWritableSpecs);
|
|
744
|
-
const hvContent = JSON.stringify(parsed, null, 2) + '\n';
|
|
745
|
-
const hvDestPath = path.join(resolvedDir, ...HARNESS_VERSION_DEST.split('/'));
|
|
746
|
-
fs.mkdirSync(path.dirname(hvDestPath), { recursive: true });
|
|
747
|
-
fs.writeFileSync(hvDestPath, hvContent, 'utf-8');
|
|
748
|
-
created.push(HARNESS_VERSION_DEST);
|
|
1242
|
+
for (const hostPlan of hostPlans) {
|
|
1243
|
+
const hostWrite = writeGeneratedPlan({
|
|
1244
|
+
plan: hostPlan.plan,
|
|
1245
|
+
fileSpecs: hostPlan.fileSpecs,
|
|
1246
|
+
specsByDest: hostPlan.specsByDest,
|
|
1247
|
+
resolvedDir: hostPlan.root,
|
|
1248
|
+
vars,
|
|
1249
|
+
optional,
|
|
1250
|
+
metadata,
|
|
1251
|
+
createdPrefix: `host:${hostPlan.host}:`,
|
|
1252
|
+
});
|
|
1253
|
+
created.push(...hostWrite.created);
|
|
1254
|
+
hostCreated.push(...hostWrite.created);
|
|
749
1255
|
}
|
|
750
1256
|
|
|
751
1257
|
return {
|
|
@@ -753,8 +1259,13 @@ export function generate({
|
|
|
753
1259
|
created,
|
|
754
1260
|
errors,
|
|
755
1261
|
plan,
|
|
1262
|
+
...(globalPlan ? { globalPlan, globalCreated, globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1263
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })), hostCreated } : {}),
|
|
756
1264
|
summary: createSummary(plan),
|
|
1265
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan) } : {}),
|
|
1266
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans), ownershipClasses: createOwnershipClasses({ resolvedGlobalDir, hostPlans }) } : {}),
|
|
757
1267
|
warnings,
|
|
1268
|
+
installScope: normalizedInstallScope,
|
|
758
1269
|
};
|
|
759
1270
|
|
|
760
1271
|
} catch (err) {
|
|
@@ -764,8 +1275,13 @@ export function generate({
|
|
|
764
1275
|
created,
|
|
765
1276
|
errors,
|
|
766
1277
|
plan,
|
|
1278
|
+
...(globalPlan ? { globalPlan, globalDir: normalizePath(resolvedGlobalDir) } : {}),
|
|
1279
|
+
...(hostPlans.length ? { hostPlans: hostPlans.map(hostPlan => ({ host: hostPlan.host, root: normalizePath(hostPlan.root), plan: hostPlan.plan })) } : {}),
|
|
767
1280
|
summary: createSummary(plan),
|
|
1281
|
+
...(globalPlan ? { globalSummary: createSummary(globalPlan) } : {}),
|
|
1282
|
+
...(hostPlans.length ? { hostSummary: createHostSummary(hostPlans) } : {}),
|
|
768
1283
|
warnings,
|
|
1284
|
+
installScope: normalizedInstallScope,
|
|
769
1285
|
};
|
|
770
1286
|
}
|
|
771
1287
|
}
|