harness-everything 0.36.2 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +7 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
- package/docs/document-lifecycle.md +60 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- 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/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +136 -4
- package/to-spec/scripts/project-docs-resolver.js +30 -13
|
@@ -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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
152
|
-
|
|
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:
|
|
5
|
+
"serialNumber": "urn:uuid:f2d00cc7-b0ba-5fb7-aa66-45210b7b3234",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.36.3",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.36.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.36.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
152
|
-
|
|
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,
|