harness-everything 0.36.1 → 0.36.3

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 (28) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +17 -0
  4. package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
  5. package/ci/mechanism-335-baseline-evidence.test.js +355 -7
  6. package/docs/doc-evidence-baseline-binding.md +28 -7
  7. package/docs/document-lifecycle.md +60 -8
  8. package/harness-everything/SKILL.md +1 -1
  9. package/harness-everything/scripts/doc-evidence.js +69 -9
  10. package/harness-everything/scripts/project-docs-resolver.js +30 -13
  11. package/multi-agent-workspace/SKILL.md +1 -1
  12. package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
  13. package/opencode-plugin/plugin.json +1 -1
  14. package/package.json +1 -1
  15. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  16. package/plugins/harness-everything/plugin.json +1 -1
  17. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  18. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +69 -9
  19. package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
  20. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  21. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
  22. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  23. package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
  24. package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
  25. package/sbom.cdx.json +7 -7
  26. package/to-spec/SKILL.md +1 -1
  27. package/to-spec/scripts/check-project-docs.js +136 -4
  28. package/to-spec/scripts/project-docs-resolver.js +30 -13
@@ -4,7 +4,7 @@ description: "Turn the conversation into a written spec — feature PRD, CLI/API
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.34.0
7
+ version: 0.36.3
8
8
  ---
9
9
 
10
10
  # To Spec
@@ -37,7 +37,7 @@ const fs = require('fs');
37
37
  const path = require('path');
38
38
  // Keep project-doc path decisions in the resolver carried by the installed
39
39
  // multi-agent-workspace skill. This script consumes the same contract.
40
- const { getWorkspaceRoot, getRepoManifestHomes, detectProjectDocsConventions, validateDocsConfig } =
40
+ const { getWorkspaceRoot, getRepoManifestHomes, detectProjectDocsConventions, validateDocsConfig, normalizeHostProjectDocs } =
41
41
  require('./project-docs-resolver');
42
42
 
43
43
  function loadManifestHelper() {
@@ -150,6 +150,75 @@ function isPlainObject(value) {
150
150
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
151
151
  }
152
152
 
153
+ function isPathInside(rootPath, candidatePath) {
154
+ const relativePath = path.relative(rootPath, candidatePath);
155
+ return relativePath === '' || (
156
+ relativePath !== '..' &&
157
+ !relativePath.startsWith(`..${path.sep}`) &&
158
+ !path.isAbsolute(relativePath)
159
+ );
160
+ }
161
+
162
+ function inspectInitPath(workspaceRoot, targetPath) {
163
+ let realWorkspaceRoot;
164
+ try {
165
+ realWorkspaceRoot = fs.realpathSync(workspaceRoot);
166
+ } catch (error) {
167
+ return { error: `workspace root cannot be resolved (${error.message})` };
168
+ }
169
+
170
+ try {
171
+ fs.lstatSync(targetPath);
172
+ } catch (error) {
173
+ if (error.code !== 'ENOENT') return { error: `path cannot be inspected (${error.message})` };
174
+
175
+ let parentPath = path.dirname(targetPath);
176
+ while (true) {
177
+ try {
178
+ fs.lstatSync(parentPath);
179
+ } catch (parentError) {
180
+ if (parentError.code !== 'ENOENT') return { error: `parent cannot be inspected (${parentError.message})` };
181
+ const nextParent = path.dirname(parentPath);
182
+ if (nextParent === parentPath) return { error: 'no existing parent could be found' };
183
+ parentPath = nextParent;
184
+ continue;
185
+ }
186
+
187
+ let realParentPath;
188
+ try {
189
+ realParentPath = fs.realpathSync(parentPath);
190
+ } catch (parentError) {
191
+ return { error: `parent cannot be resolved (${parentError.message})` };
192
+ }
193
+ let parentStats;
194
+ try {
195
+ parentStats = fs.statSync(realParentPath);
196
+ } catch (parentError) {
197
+ return { error: `parent cannot be inspected (${parentError.message})` };
198
+ }
199
+ if (!parentStats.isDirectory()) return { error: 'cannot be read safely because its nearest existing parent is not a directory' };
200
+ if (!isPathInside(realWorkspaceRoot, realParentPath)) return { error: 'nearest existing parent resolves outside the workspace' };
201
+ return { exists: false };
202
+ }
203
+ }
204
+
205
+ let realTargetPath;
206
+ try {
207
+ realTargetPath = fs.realpathSync(targetPath);
208
+ } catch (error) {
209
+ return { error: `path cannot be resolved (${error.message})` };
210
+ }
211
+ let targetStats;
212
+ try {
213
+ targetStats = fs.statSync(realTargetPath);
214
+ } catch (error) {
215
+ return { error: `path cannot be inspected (${error.message})` };
216
+ }
217
+ if (!targetStats.isFile()) return { error: 'cannot be read because it is not a regular file' };
218
+ if (!isPathInside(realWorkspaceRoot, realTargetPath)) return { error: 'resolves outside the workspace' };
219
+ return { exists: true };
220
+ }
221
+
153
222
  // init never overwrites a configured value: absent/empty required fields are
154
223
  // filled, matching flags are accepted as no-ops, and a differing flag is a
155
224
  // field-specific conflict that aborts before any write.
@@ -179,6 +248,65 @@ function printConflicts(conflicts) {
179
248
  }
180
249
  }
181
250
 
251
+ function readHostManifestForInit(workspaceRoot, manifestPath) {
252
+ const inspected = inspectInitPath(workspaceRoot, manifestPath);
253
+ if (inspected.error) return { error: `unsafe path: ${inspected.error}` };
254
+ if (!inspected.exists) return { exists: false, data: readManifest(manifestPath) };
255
+
256
+ let raw;
257
+ try {
258
+ raw = fs.readFileSync(manifestPath, 'utf8');
259
+ } catch (error) {
260
+ return { error: `cannot be read (${error.message})` };
261
+ }
262
+
263
+ let data;
264
+ try {
265
+ data = JSON.parse(raw);
266
+ } catch (error) {
267
+ return { error: `invalid JSON (${error.message})` };
268
+ }
269
+ if (!isPlainObject(data)) return { error: 'manifest root must be a JSON object' };
270
+
271
+ for (const field of ['skills', 'agents', 'generated']) {
272
+ if (Object.prototype.hasOwnProperty.call(data, field) && !Array.isArray(data[field])) {
273
+ return { error: `manifest ${field} must be an array` };
274
+ }
275
+ }
276
+ if (!Array.isArray(data.skills)) data.skills = [];
277
+ if (!Array.isArray(data.agents)) data.agents = [];
278
+
279
+ if (Object.prototype.hasOwnProperty.call(data, 'projectDocs')) {
280
+ const normalizedProjectDocs = normalizeHostProjectDocs(data.projectDocs).projectDocs;
281
+ const validation = validateDocsConfig(normalizedProjectDocs);
282
+ if (!validation.valid) {
283
+ const details = validation.diagnostics.map(item => `${item.code}: ${item.message}`).join('; ');
284
+ return { error: `projectDocs failed schema validation (${details})` };
285
+ }
286
+ }
287
+
288
+ return { exists: true, data };
289
+ }
290
+
291
+ function rejectHostManifestInit(workspaceRoot, manifestPath, reason) {
292
+ const relativePath = path.relative(workspaceRoot, manifestPath).replace(/\\/g, '/');
293
+ console.error(`[Project Docs Check] ${relativePath} cannot be safely updated: ${reason}; no host manifests were changed.`);
294
+ const guidance = reason.startsWith('cannot be read') || reason.includes('not a regular file')
295
+ ? `Make ${relativePath} a readable manifest file (not a directory), then rerun init.`
296
+ : reason.startsWith('unsafe path')
297
+ ? `Make ${relativePath} a regular file that resolves inside this workspace, then rerun init.`
298
+ : `Repair the JSON structure or projectDocs schema in ${relativePath}, then rerun init.`;
299
+ console.error(`=> ${guidance}`);
300
+ process.exit(1);
301
+ }
302
+
303
+ function rejectProjectFileInit(workspaceRoot, projectFile, reason) {
304
+ const relativePath = path.relative(workspaceRoot, projectFile).replace(/\\/g, '/');
305
+ console.error(`[Project Docs Check] ${relativePath} cannot be safely updated: unsafe path (${reason}); no files were changed.`);
306
+ console.error(`=> Make ${relativePath} a regular file that resolves inside this workspace, then rerun init.`);
307
+ process.exit(1);
308
+ }
309
+
182
310
  function runInit(workspaceRoot, args) {
183
311
  const flags = parseFlags(args);
184
312
  // Required values must be non-empty after trimming, the same presence rule
@@ -205,7 +333,9 @@ function runInit(workspaceRoot, args) {
205
333
  // the authoritative repair target: fill only its absent/empty fields,
206
334
  // preserve every other key, validate the candidate, and write only it.
207
335
  const projectFile = path.join(workspaceRoot, '.agents', 'harness-everything', 'project-docs.json');
208
- if (fs.existsSync(projectFile)) {
336
+ const projectFilePath = inspectInitPath(workspaceRoot, projectFile);
337
+ if (projectFilePath.error) rejectProjectFileInit(workspaceRoot, projectFile, projectFilePath.error);
338
+ if (projectFilePath.exists) {
209
339
  const rel = path.relative(workspaceRoot, projectFile).replace(/\\/g, '/');
210
340
  let existing;
211
341
  try {
@@ -252,8 +382,10 @@ function runInit(workspaceRoot, args) {
252
382
  const writableHomes = [];
253
383
  for (const home of homes) {
254
384
  const manifestPath = getManifestPath(home);
255
- if (!fs.existsSync(path.dirname(manifestPath))) continue;
256
- const data = readManifest(manifestPath);
385
+ const result = readHostManifestForInit(workspaceRoot, manifestPath);
386
+ if (result.error) rejectHostManifestInit(workspaceRoot, manifestPath, result.error);
387
+ if (!result.exists && !fs.existsSync(path.dirname(manifestPath))) continue;
388
+ const data = result.data;
257
389
  const existingProjectDocs = isPlainObject(data.projectDocs) ? data.projectDocs : null;
258
390
  writableHomes.push({ manifestPath, data, existingProjectDocs });
259
391
  }
@@ -78,6 +78,25 @@ function isPlainObject(value) {
78
78
  return prototype === Object.prototype || prototype === null;
79
79
  }
80
80
 
81
+ function normalizeHostProjectDocs(projectDocs) {
82
+ if (!isPlainObject(projectDocs)) return { projectDocs, warnings: [] };
83
+
84
+ const normalized = { ...projectDocs };
85
+ const warnings = [];
86
+ if (typeof normalized.domains === 'string') {
87
+ if (typeof normalized.domainLocation !== 'string' || !normalized.domainLocation.trim()) {
88
+ normalized.domainLocation = normalized.domains;
89
+ }
90
+ delete normalized.domains;
91
+ warnings.push({
92
+ code: 'deprecated-domains-string',
93
+ severity: 'warning',
94
+ message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
95
+ });
96
+ }
97
+ return { projectDocs: normalized, warnings };
98
+ }
99
+
81
100
  function projectDocsConfigPath(workspaceRoot) {
82
101
  return path.join(workspaceRoot, PROJECT_DOCS_CONFIG);
83
102
  }
@@ -148,22 +167,12 @@ function inspectHostProjectDocs(workspaceRoot) {
148
167
  continue;
149
168
  }
150
169
 
151
- const projectDocs = { ...manifest.projectDocs };
152
- if (typeof projectDocs.domains === 'string') {
153
- if (typeof projectDocs.domainLocation !== 'string' || !projectDocs.domainLocation.trim()) {
154
- projectDocs.domainLocation = projectDocs.domains;
155
- }
156
- delete projectDocs.domains;
157
- entry.warnings.push({
158
- code: 'deprecated-domains-string',
159
- severity: 'warning',
160
- path: configSource(workspaceRoot, file),
161
- message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
162
- });
163
- }
170
+ const normalizedHost = normalizeHostProjectDocs(manifest.projectDocs);
171
+ const projectDocs = normalizedHost.projectDocs;
164
172
 
165
173
  const validation = validateDocsConfig(projectDocs);
166
174
  const source = configSource(workspaceRoot, file);
175
+ entry.warnings.push(...normalizedHost.warnings.map(item => ({ ...item, path: source })));
167
176
  entry.warnings.push(...validation.warnings.map(item => ({ ...item, path: source })));
168
177
  if (!validation.valid) {
169
178
  const details = validation.diagnostics.map(item => item.message).join('; ');
@@ -236,6 +245,13 @@ function validateDocsConfig(config) {
236
245
  return { valid: false, diagnostics, warnings };
237
246
  }
238
247
 
248
+ for (const field of ['docLocation', 'tracker', 'issueDefinition']) {
249
+ if (config[field] !== undefined && typeof config[field] !== 'string') {
250
+ const diagnosticField = field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
251
+ invalid(`invalid-${diagnosticField}-type`, `${field} must be a string when provided`);
252
+ }
253
+ }
254
+
239
255
  for (const key of Object.keys(config).sort()) {
240
256
  if (!PROJECT_DOC_CONFIG_KEYS.has(key)) {
241
257
  warnings.push({
@@ -754,6 +770,7 @@ module.exports = {
754
770
  readProjectDocs,
755
771
  resolveDocsConfig,
756
772
  validateDocsConfig,
773
+ normalizeHostProjectDocs,
757
774
  parseContextMap,
758
775
  resolveProjectDocs,
759
776
  detectProjectDocsConventions,
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:74100c9d-ebbb-559a-8ab0-a9ac5fd581c6",
5
+ "serialNumber": "urn:uuid:f2d00cc7-b0ba-5fb7-aa66-45210b7b3234",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-20T03:51:47.041Z",
8
+ "timestamp": "2026-10-20T14:22:39.675Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.36.1"
13
+ "version": "0.36.3"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.36.1",
18
+ "bom-ref": "pkg:npm/harness-everything@0.36.3",
19
19
  "name": "harness-everything",
20
- "version": "0.36.1",
21
- "purl": "pkg:npm/harness-everything@0.36.1"
20
+ "version": "0.36.3",
21
+ "purl": "pkg:npm/harness-everything@0.36.3"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -1999,7 +1999,7 @@
1999
1999
  ]
2000
2000
  },
2001
2001
  {
2002
- "ref": "pkg:npm/harness-everything@0.36.1",
2002
+ "ref": "pkg:npm/harness-everything@0.36.3",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
package/to-spec/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: "Turn the conversation into a written spec — feature PRD, CLI/API
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.34.0
7
+ version: 0.36.3
8
8
  ---
9
9
 
10
10
  # To Spec
@@ -37,7 +37,7 @@ const fs = require('fs');
37
37
  const path = require('path');
38
38
  // Keep project-doc path decisions in the resolver carried by the installed
39
39
  // multi-agent-workspace skill. This script consumes the same contract.
40
- const { getWorkspaceRoot, getRepoManifestHomes, detectProjectDocsConventions, validateDocsConfig } =
40
+ const { getWorkspaceRoot, getRepoManifestHomes, detectProjectDocsConventions, validateDocsConfig, normalizeHostProjectDocs } =
41
41
  require('./project-docs-resolver');
42
42
 
43
43
  function loadManifestHelper() {
@@ -150,6 +150,75 @@ function isPlainObject(value) {
150
150
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
151
151
  }
152
152
 
153
+ function isPathInside(rootPath, candidatePath) {
154
+ const relativePath = path.relative(rootPath, candidatePath);
155
+ return relativePath === '' || (
156
+ relativePath !== '..' &&
157
+ !relativePath.startsWith(`..${path.sep}`) &&
158
+ !path.isAbsolute(relativePath)
159
+ );
160
+ }
161
+
162
+ function inspectInitPath(workspaceRoot, targetPath) {
163
+ let realWorkspaceRoot;
164
+ try {
165
+ realWorkspaceRoot = fs.realpathSync(workspaceRoot);
166
+ } catch (error) {
167
+ return { error: `workspace root cannot be resolved (${error.message})` };
168
+ }
169
+
170
+ try {
171
+ fs.lstatSync(targetPath);
172
+ } catch (error) {
173
+ if (error.code !== 'ENOENT') return { error: `path cannot be inspected (${error.message})` };
174
+
175
+ let parentPath = path.dirname(targetPath);
176
+ while (true) {
177
+ try {
178
+ fs.lstatSync(parentPath);
179
+ } catch (parentError) {
180
+ if (parentError.code !== 'ENOENT') return { error: `parent cannot be inspected (${parentError.message})` };
181
+ const nextParent = path.dirname(parentPath);
182
+ if (nextParent === parentPath) return { error: 'no existing parent could be found' };
183
+ parentPath = nextParent;
184
+ continue;
185
+ }
186
+
187
+ let realParentPath;
188
+ try {
189
+ realParentPath = fs.realpathSync(parentPath);
190
+ } catch (parentError) {
191
+ return { error: `parent cannot be resolved (${parentError.message})` };
192
+ }
193
+ let parentStats;
194
+ try {
195
+ parentStats = fs.statSync(realParentPath);
196
+ } catch (parentError) {
197
+ return { error: `parent cannot be inspected (${parentError.message})` };
198
+ }
199
+ if (!parentStats.isDirectory()) return { error: 'cannot be read safely because its nearest existing parent is not a directory' };
200
+ if (!isPathInside(realWorkspaceRoot, realParentPath)) return { error: 'nearest existing parent resolves outside the workspace' };
201
+ return { exists: false };
202
+ }
203
+ }
204
+
205
+ let realTargetPath;
206
+ try {
207
+ realTargetPath = fs.realpathSync(targetPath);
208
+ } catch (error) {
209
+ return { error: `path cannot be resolved (${error.message})` };
210
+ }
211
+ let targetStats;
212
+ try {
213
+ targetStats = fs.statSync(realTargetPath);
214
+ } catch (error) {
215
+ return { error: `path cannot be inspected (${error.message})` };
216
+ }
217
+ if (!targetStats.isFile()) return { error: 'cannot be read because it is not a regular file' };
218
+ if (!isPathInside(realWorkspaceRoot, realTargetPath)) return { error: 'resolves outside the workspace' };
219
+ return { exists: true };
220
+ }
221
+
153
222
  // init never overwrites a configured value: absent/empty required fields are
154
223
  // filled, matching flags are accepted as no-ops, and a differing flag is a
155
224
  // field-specific conflict that aborts before any write.
@@ -179,6 +248,65 @@ function printConflicts(conflicts) {
179
248
  }
180
249
  }
181
250
 
251
+ function readHostManifestForInit(workspaceRoot, manifestPath) {
252
+ const inspected = inspectInitPath(workspaceRoot, manifestPath);
253
+ if (inspected.error) return { error: `unsafe path: ${inspected.error}` };
254
+ if (!inspected.exists) return { exists: false, data: readManifest(manifestPath) };
255
+
256
+ let raw;
257
+ try {
258
+ raw = fs.readFileSync(manifestPath, 'utf8');
259
+ } catch (error) {
260
+ return { error: `cannot be read (${error.message})` };
261
+ }
262
+
263
+ let data;
264
+ try {
265
+ data = JSON.parse(raw);
266
+ } catch (error) {
267
+ return { error: `invalid JSON (${error.message})` };
268
+ }
269
+ if (!isPlainObject(data)) return { error: 'manifest root must be a JSON object' };
270
+
271
+ for (const field of ['skills', 'agents', 'generated']) {
272
+ if (Object.prototype.hasOwnProperty.call(data, field) && !Array.isArray(data[field])) {
273
+ return { error: `manifest ${field} must be an array` };
274
+ }
275
+ }
276
+ if (!Array.isArray(data.skills)) data.skills = [];
277
+ if (!Array.isArray(data.agents)) data.agents = [];
278
+
279
+ if (Object.prototype.hasOwnProperty.call(data, 'projectDocs')) {
280
+ const normalizedProjectDocs = normalizeHostProjectDocs(data.projectDocs).projectDocs;
281
+ const validation = validateDocsConfig(normalizedProjectDocs);
282
+ if (!validation.valid) {
283
+ const details = validation.diagnostics.map(item => `${item.code}: ${item.message}`).join('; ');
284
+ return { error: `projectDocs failed schema validation (${details})` };
285
+ }
286
+ }
287
+
288
+ return { exists: true, data };
289
+ }
290
+
291
+ function rejectHostManifestInit(workspaceRoot, manifestPath, reason) {
292
+ const relativePath = path.relative(workspaceRoot, manifestPath).replace(/\\/g, '/');
293
+ console.error(`[Project Docs Check] ${relativePath} cannot be safely updated: ${reason}; no host manifests were changed.`);
294
+ const guidance = reason.startsWith('cannot be read') || reason.includes('not a regular file')
295
+ ? `Make ${relativePath} a readable manifest file (not a directory), then rerun init.`
296
+ : reason.startsWith('unsafe path')
297
+ ? `Make ${relativePath} a regular file that resolves inside this workspace, then rerun init.`
298
+ : `Repair the JSON structure or projectDocs schema in ${relativePath}, then rerun init.`;
299
+ console.error(`=> ${guidance}`);
300
+ process.exit(1);
301
+ }
302
+
303
+ function rejectProjectFileInit(workspaceRoot, projectFile, reason) {
304
+ const relativePath = path.relative(workspaceRoot, projectFile).replace(/\\/g, '/');
305
+ console.error(`[Project Docs Check] ${relativePath} cannot be safely updated: unsafe path (${reason}); no files were changed.`);
306
+ console.error(`=> Make ${relativePath} a regular file that resolves inside this workspace, then rerun init.`);
307
+ process.exit(1);
308
+ }
309
+
182
310
  function runInit(workspaceRoot, args) {
183
311
  const flags = parseFlags(args);
184
312
  // Required values must be non-empty after trimming, the same presence rule
@@ -205,7 +333,9 @@ function runInit(workspaceRoot, args) {
205
333
  // the authoritative repair target: fill only its absent/empty fields,
206
334
  // preserve every other key, validate the candidate, and write only it.
207
335
  const projectFile = path.join(workspaceRoot, '.agents', 'harness-everything', 'project-docs.json');
208
- if (fs.existsSync(projectFile)) {
336
+ const projectFilePath = inspectInitPath(workspaceRoot, projectFile);
337
+ if (projectFilePath.error) rejectProjectFileInit(workspaceRoot, projectFile, projectFilePath.error);
338
+ if (projectFilePath.exists) {
209
339
  const rel = path.relative(workspaceRoot, projectFile).replace(/\\/g, '/');
210
340
  let existing;
211
341
  try {
@@ -252,8 +382,10 @@ function runInit(workspaceRoot, args) {
252
382
  const writableHomes = [];
253
383
  for (const home of homes) {
254
384
  const manifestPath = getManifestPath(home);
255
- if (!fs.existsSync(path.dirname(manifestPath))) continue;
256
- const data = readManifest(manifestPath);
385
+ const result = readHostManifestForInit(workspaceRoot, manifestPath);
386
+ if (result.error) rejectHostManifestInit(workspaceRoot, manifestPath, result.error);
387
+ if (!result.exists && !fs.existsSync(path.dirname(manifestPath))) continue;
388
+ const data = result.data;
257
389
  const existingProjectDocs = isPlainObject(data.projectDocs) ? data.projectDocs : null;
258
390
  writableHomes.push({ manifestPath, data, existingProjectDocs });
259
391
  }
@@ -78,6 +78,25 @@ function isPlainObject(value) {
78
78
  return prototype === Object.prototype || prototype === null;
79
79
  }
80
80
 
81
+ function normalizeHostProjectDocs(projectDocs) {
82
+ if (!isPlainObject(projectDocs)) return { projectDocs, warnings: [] };
83
+
84
+ const normalized = { ...projectDocs };
85
+ const warnings = [];
86
+ if (typeof normalized.domains === 'string') {
87
+ if (typeof normalized.domainLocation !== 'string' || !normalized.domainLocation.trim()) {
88
+ normalized.domainLocation = normalized.domains;
89
+ }
90
+ delete normalized.domains;
91
+ warnings.push({
92
+ code: 'deprecated-domains-string',
93
+ severity: 'warning',
94
+ message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
95
+ });
96
+ }
97
+ return { projectDocs: normalized, warnings };
98
+ }
99
+
81
100
  function projectDocsConfigPath(workspaceRoot) {
82
101
  return path.join(workspaceRoot, PROJECT_DOCS_CONFIG);
83
102
  }
@@ -148,22 +167,12 @@ function inspectHostProjectDocs(workspaceRoot) {
148
167
  continue;
149
168
  }
150
169
 
151
- const projectDocs = { ...manifest.projectDocs };
152
- if (typeof projectDocs.domains === 'string') {
153
- if (typeof projectDocs.domainLocation !== 'string' || !projectDocs.domainLocation.trim()) {
154
- projectDocs.domainLocation = projectDocs.domains;
155
- }
156
- delete projectDocs.domains;
157
- entry.warnings.push({
158
- code: 'deprecated-domains-string',
159
- severity: 'warning',
160
- path: configSource(workspaceRoot, file),
161
- message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
162
- });
163
- }
170
+ const normalizedHost = normalizeHostProjectDocs(manifest.projectDocs);
171
+ const projectDocs = normalizedHost.projectDocs;
164
172
 
165
173
  const validation = validateDocsConfig(projectDocs);
166
174
  const source = configSource(workspaceRoot, file);
175
+ entry.warnings.push(...normalizedHost.warnings.map(item => ({ ...item, path: source })));
167
176
  entry.warnings.push(...validation.warnings.map(item => ({ ...item, path: source })));
168
177
  if (!validation.valid) {
169
178
  const details = validation.diagnostics.map(item => item.message).join('; ');
@@ -236,6 +245,13 @@ function validateDocsConfig(config) {
236
245
  return { valid: false, diagnostics, warnings };
237
246
  }
238
247
 
248
+ for (const field of ['docLocation', 'tracker', 'issueDefinition']) {
249
+ if (config[field] !== undefined && typeof config[field] !== 'string') {
250
+ const diagnosticField = field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
251
+ invalid(`invalid-${diagnosticField}-type`, `${field} must be a string when provided`);
252
+ }
253
+ }
254
+
239
255
  for (const key of Object.keys(config).sort()) {
240
256
  if (!PROJECT_DOC_CONFIG_KEYS.has(key)) {
241
257
  warnings.push({
@@ -754,6 +770,7 @@ module.exports = {
754
770
  readProjectDocs,
755
771
  resolveDocsConfig,
756
772
  validateDocsConfig,
773
+ normalizeHostProjectDocs,
757
774
  parseContextMap,
758
775
  resolveProjectDocs,
759
776
  detectProjectDocsConventions,