harness-everything 0.32.0 → 0.34.0
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 +2 -1
- package/CHANGELOG.md +33 -0
- package/README.md +3 -2
- package/ci/fixtures/doc-lifecycle/prd-revisions/config.json +9 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/config.json +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/expected.json +12 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r003.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r001.md +20 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r002.md +26 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r001.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-dropped.body.md +35 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-incomplete.body.md +37 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-one-replaced.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-overlap.body.md +39 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-001.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-002.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-001.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-002.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/GRPH-010.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/legacy/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/GRAPH-007.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/legacy/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v1/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v2/GRPH-007.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-003.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-004.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-005.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-006.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-007.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-020.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-021.md +9 -0
- package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/expected.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/config.json +1 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/adr/GRPH-003.md +8 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/prd/graph.md +10 -0
- package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/expected.json +1 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +759 -0
- package/ci/mechanism-2ax-doc-lifecycle.test.js +1061 -0
- package/ci/mechanism-2ay-prd-revisions.test.js +2891 -0
- package/ci/mechanism-2az-step-type-registry.test.js +86 -0
- package/ci/openai-plugin-package.test.js +1 -1
- package/ci/release-consistency-check.js +26 -9
- package/ci/release-consistency-check.test.js +30 -0
- package/ci/router-workflow-plan.test.js +36 -0
- package/code-review/SKILL.md +41 -0
- package/code-review/references/review-playbook.md +40 -0
- package/code-review/templates/finding.template.md +33 -0
- package/docs/document-lifecycle.md +206 -0
- package/docs/openai-plugin.md +1 -1
- package/docs/workflows/code-review.md +58 -0
- package/evals/code-review/eval.yaml +26 -0
- package/evals/code-review/tasks/negative-grill-me.yaml +16 -0
- package/evals/code-review/tasks/negative-security-review.yaml +16 -0
- package/evals/code-review/tasks/negative-tdd.yaml +16 -0
- package/evals/code-review/tasks/negative-verification-loop.yaml +16 -0
- package/evals/code-review/tasks/positive-pr-review.yaml +16 -0
- package/evals/code-review/tasks/positive-revised-head-rereview.yaml +16 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/references/skill-registry.md +1 -0
- package/harness-everything/scripts/doc-lifecycle.js +2401 -0
- package/harness-everything/scripts/project-docs-resolver.js +764 -0
- package/harness-everything/scripts/routing-keywords.json +7 -0
- package/hooks/scripts/lib/binding-resolver.js +1 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +437 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +1 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +41 -0
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +40 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +33 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/references/skill-registry.md +1 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +2401 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +764 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +7 -0
- 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 +437 -30
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +168 -54
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +749 -96
- package/sbom.cdx.json +7 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +168 -54
- package/to-spec/scripts/project-docs-resolver.js +749 -96
|
@@ -611,6 +611,13 @@
|
|
|
611
611
|
"bindings": [
|
|
612
612
|
"zoom-out"
|
|
613
613
|
]
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"id": "review",
|
|
617
|
+
"bindings": [
|
|
618
|
+
"code-review"
|
|
619
|
+
],
|
|
620
|
+
"regex": "\\b(?:code[- ]review|pr review)\\b|\\b(?:re-)?review\\s+(?:(?:(?:this|the|my)\\s+)?(?:pr\\b|pull request\\b|diff\\b|(?:finished\\s+)?implementation\\b(?!\\s+plan\\b))|the\\s+(?:fix\\b|new\\s+head\\b))|程式碼審查|代码审查"
|
|
614
621
|
}
|
|
615
622
|
]
|
|
616
623
|
}
|
package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js
CHANGED
|
@@ -17,6 +17,15 @@ const PROJECT_DOC_FIELDS = Object.freeze({
|
|
|
17
17
|
domain: ['domainLocation', 'domains', 'contextLocation', 'contexts', 'domain'],
|
|
18
18
|
architecture: ['architectureLocation', 'architectures', 'architecture']
|
|
19
19
|
});
|
|
20
|
+
const PROJECT_DOCS_CONFIG = path.join('.agents', 'harness-everything', 'project-docs.json');
|
|
21
|
+
const PROJECT_DOC_ROLES = Object.freeze(['prd', 'adr', 'ticket', 'reference', 'log', 'report']);
|
|
22
|
+
const PROJECT_DOC_CONFIG_KEYS = new Set([
|
|
23
|
+
'governed', 'domains', 'legacy', 'derived', 'tickets',
|
|
24
|
+
'docLocation', 'tracker', 'issueDefinition', 'contextMap', 'contextMapPath',
|
|
25
|
+
'decisionLocation', 'decisions', 'adrLocation', 'adr',
|
|
26
|
+
'domainLocation', 'contextLocation', 'contexts', 'domain',
|
|
27
|
+
'architectureLocation', 'architectures', 'architecture'
|
|
28
|
+
]);
|
|
20
29
|
const UNBOUND_WORKSPACE_KEY = 'unbound-workspace';
|
|
21
30
|
|
|
22
31
|
function getWorkspaceRoot(start, allowNonGit = false) {
|
|
@@ -63,20 +72,373 @@ function getRepoManifestHomes(workspaceRoot) {
|
|
|
63
72
|
return PLATFORM_HOMES.map(home => path.join(workspaceRoot, home));
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
function
|
|
75
|
+
function isPlainObject(value) {
|
|
76
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
77
|
+
const prototype = Object.getPrototypeOf(value);
|
|
78
|
+
return prototype === Object.prototype || prototype === null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function projectDocsConfigPath(workspaceRoot) {
|
|
82
|
+
return path.join(workspaceRoot, PROJECT_DOCS_CONFIG);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizedHostHome(activeHost) {
|
|
86
|
+
if (typeof activeHost !== 'string') return null;
|
|
87
|
+
const name = activeHost.trim().toLowerCase().replace(/^\./, '');
|
|
88
|
+
return PLATFORM_HOMES.find(home => home.slice(1) === name) || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readHostProjectDocsConfigs(workspaceRoot) {
|
|
67
92
|
const manifests = [];
|
|
68
|
-
for (
|
|
93
|
+
for (let index = 0; index < PLATFORM_HOMES.length; index++) {
|
|
94
|
+
const homeName = PLATFORM_HOMES[index];
|
|
95
|
+
const home = path.join(workspaceRoot, homeName);
|
|
69
96
|
const file = path.join(home, 'harness-everything', 'manifest.json');
|
|
70
97
|
if (!fs.existsSync(file)) continue;
|
|
71
98
|
const data = readJson(file);
|
|
72
|
-
if (data && data.projectDocs &&
|
|
73
|
-
manifests.push({ file, projectDocs: data.projectDocs });
|
|
99
|
+
if (data && data.projectDocs && isPlainObject(data.projectDocs)) {
|
|
100
|
+
manifests.push({ file, home: homeName, projectDocs: data.projectDocs });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return manifests;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inspectHostProjectDocs(workspaceRoot) {
|
|
107
|
+
const entries = [];
|
|
108
|
+
for (const home of PLATFORM_HOMES) {
|
|
109
|
+
const file = path.join(workspaceRoot, home, 'harness-everything', 'manifest.json');
|
|
110
|
+
if (!fs.existsSync(file)) continue;
|
|
111
|
+
const entry = { file, home, projectDocs: null, warnings: [], diagnostics: [], hasProjectDocs: false };
|
|
112
|
+
let manifest;
|
|
113
|
+
try {
|
|
114
|
+
manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
entry.diagnostics.push({
|
|
117
|
+
code: 'invalid-host-config',
|
|
118
|
+
severity: 'error',
|
|
119
|
+
path: configSource(workspaceRoot, file),
|
|
120
|
+
message: `host manifest at ${configSource(workspaceRoot, file)} is not valid JSON`
|
|
121
|
+
});
|
|
122
|
+
entries.push(entry);
|
|
123
|
+
continue;
|
|
74
124
|
}
|
|
125
|
+
if (!isPlainObject(manifest)) {
|
|
126
|
+
entry.diagnostics.push({
|
|
127
|
+
code: 'invalid-host-config',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
path: configSource(workspaceRoot, file),
|
|
130
|
+
message: `host manifest at ${configSource(workspaceRoot, file)} must contain a JSON object`
|
|
131
|
+
});
|
|
132
|
+
entries.push(entry);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!Object.prototype.hasOwnProperty.call(manifest, 'projectDocs')) {
|
|
136
|
+
entries.push(entry);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
entry.hasProjectDocs = true;
|
|
140
|
+
if (!isPlainObject(manifest.projectDocs)) {
|
|
141
|
+
entry.diagnostics.push({
|
|
142
|
+
code: 'invalid-host-config',
|
|
143
|
+
severity: 'error',
|
|
144
|
+
path: configSource(workspaceRoot, file),
|
|
145
|
+
message: `projectDocs in ${configSource(workspaceRoot, file)} must be a JSON object`
|
|
146
|
+
});
|
|
147
|
+
entries.push(entry);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
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
|
+
}
|
|
164
|
+
|
|
165
|
+
const validation = validateDocsConfig(projectDocs);
|
|
166
|
+
const source = configSource(workspaceRoot, file);
|
|
167
|
+
entry.warnings.push(...validation.warnings.map(item => ({ ...item, path: source })));
|
|
168
|
+
if (!validation.valid) {
|
|
169
|
+
const details = validation.diagnostics.map(item => item.message).join('; ');
|
|
170
|
+
entry.diagnostics.push({
|
|
171
|
+
code: 'invalid-host-config',
|
|
172
|
+
severity: 'error',
|
|
173
|
+
path: source,
|
|
174
|
+
message: `projectDocs in ${source} failed validation${details ? `: ${details}` : ''}`
|
|
175
|
+
});
|
|
176
|
+
entries.push(entry);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
entry.projectDocs = projectDocs;
|
|
180
|
+
entries.push(entry);
|
|
75
181
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
182
|
+
return entries;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readProjectDocs(workspaceRoot, options = {}) {
|
|
186
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
187
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
188
|
+
const root = path.resolve(resolvedRoot);
|
|
189
|
+
const selection = resolveDocsConfig(root, options);
|
|
190
|
+
const firstSource = selection.sources && selection.sources[0];
|
|
191
|
+
const file = firstSource ? path.resolve(root, ...firstSource.split('/')) : null;
|
|
192
|
+
return {
|
|
193
|
+
file,
|
|
194
|
+
projectDocs: selection.config,
|
|
195
|
+
source: selection.status,
|
|
196
|
+
status: selection.status,
|
|
197
|
+
invalid: selection.status === 'invalid',
|
|
198
|
+
sources: selection.sources || [],
|
|
199
|
+
warnings: selection.warnings || [],
|
|
200
|
+
diagnostics: selection.diagnostics || []
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isRepoRelativePath(value) {
|
|
205
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
206
|
+
const normalized = value.trim().replace(/\\/g, '/');
|
|
207
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)) return false;
|
|
208
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) return false;
|
|
209
|
+
return !normalized.split('/').includes('..');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Config globs must match the shape the matcher canonicalizes against:
|
|
213
|
+
// forward slashes, repo-relative, no "./" or "../" leading segments. A
|
|
214
|
+
// non-canonical value would validate but silently match nothing.
|
|
215
|
+
function getPatternIssue(value) {
|
|
216
|
+
if (typeof value !== 'string' || !value.trim()) return 'must be a non-empty string';
|
|
217
|
+
if (value.includes('\\')) return 'must use forward slashes (backslashes are not allowed)';
|
|
218
|
+
if (value.startsWith('/') || /^[A-Za-z]:\//.test(value)) return 'must be repo-relative (no leading "/")';
|
|
219
|
+
if (value.startsWith('./') || value === '.') return 'must be canonical (no leading "./")';
|
|
220
|
+
if (value.startsWith('../')) return 'must be canonical (no leading "../")';
|
|
221
|
+
if (value.split('/').includes('..')) return 'must not contain ".." segments';
|
|
222
|
+
if (value.split('/').includes('.')) return 'must not contain "." path segments';
|
|
223
|
+
if (value.split('/').includes('')) return 'must not contain empty path segments (no "//" or trailing "/")';
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function validateDocsConfig(config) {
|
|
228
|
+
const diagnostics = [];
|
|
229
|
+
const warnings = [];
|
|
230
|
+
function invalid(code, message) {
|
|
231
|
+
diagnostics.push({ code, severity: 'error', path: PROJECT_DOCS_CONFIG.replace(/\\/g, '/'), message });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!isPlainObject(config)) {
|
|
235
|
+
invalid('invalid-config-root', 'project-docs.json must contain a JSON object');
|
|
236
|
+
return { valid: false, diagnostics, warnings };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const key of Object.keys(config).sort()) {
|
|
240
|
+
if (!PROJECT_DOC_CONFIG_KEYS.has(key)) {
|
|
241
|
+
warnings.push({
|
|
242
|
+
code: 'unknown-config-key',
|
|
243
|
+
severity: 'warning',
|
|
244
|
+
path: PROJECT_DOCS_CONFIG.replace(/\\/g, '/'),
|
|
245
|
+
message: `unknown project-docs.json key: ${key}`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (Object.prototype.hasOwnProperty.call(config, 'governed')) {
|
|
251
|
+
if (!Array.isArray(config.governed)) {
|
|
252
|
+
invalid('invalid-governed', 'governed must be an array');
|
|
253
|
+
} else {
|
|
254
|
+
config.governed.forEach((entry, index) => {
|
|
255
|
+
if (!isPlainObject(entry) || getPatternIssue(entry.path)) {
|
|
256
|
+
const issue = isPlainObject(entry) ? getPatternIssue(entry.path) : 'must be an object with a path';
|
|
257
|
+
invalid('invalid-governed-entry', `governed[${index}].path ${issue} (must be a canonical repo-relative glob like "docs/**"); got ${JSON.stringify(entry && entry.path)}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!PROJECT_DOC_ROLES.includes(entry.role)) {
|
|
261
|
+
invalid('invalid-governed-role', `governed[${index}].role must be one of ${PROJECT_DOC_ROLES.join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(config, 'domains')) {
|
|
268
|
+
if (!Array.isArray(config.domains)) {
|
|
269
|
+
invalid('invalid-domains', 'domains must be an array');
|
|
270
|
+
} else {
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
config.domains.forEach((domain, index) => {
|
|
273
|
+
if (typeof domain !== 'string' || !/^[A-Z][A-Z0-9]{1,9}$/.test(domain)) {
|
|
274
|
+
invalid('invalid-domain', `domains[${index}] must match ^[A-Z][A-Z0-9]{1,9}$`);
|
|
275
|
+
} else if (seen.has(domain)) {
|
|
276
|
+
invalid('duplicate-domain', `domains contains duplicate value ${domain}`);
|
|
277
|
+
}
|
|
278
|
+
seen.add(domain);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (Object.prototype.hasOwnProperty.call(config, 'legacy')) {
|
|
284
|
+
if (!Array.isArray(config.legacy)) {
|
|
285
|
+
invalid('invalid-legacy', 'legacy must be an array of repo-relative globs');
|
|
286
|
+
} else {
|
|
287
|
+
config.legacy.forEach((pattern, index) => {
|
|
288
|
+
const issue = getPatternIssue(pattern);
|
|
289
|
+
if (issue) invalid('invalid-legacy-entry', `legacy[${index}] ${issue} (must be a canonical repo-relative glob like "docs/**"); got ${JSON.stringify(pattern)}`);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (Object.prototype.hasOwnProperty.call(config, 'derived')) {
|
|
295
|
+
if (!Array.isArray(config.derived)) {
|
|
296
|
+
invalid('invalid-derived', 'derived must be an array');
|
|
297
|
+
} else {
|
|
298
|
+
config.derived.forEach((entry, index) => {
|
|
299
|
+
if (!isPlainObject(entry) || getPatternIssue(entry.path) ||
|
|
300
|
+
typeof entry.generator !== 'string' || !entry.generator.trim()) {
|
|
301
|
+
const issue = isPlainObject(entry) ? (getPatternIssue(entry.path) || null) : null;
|
|
302
|
+
if (issue) {
|
|
303
|
+
invalid('invalid-derived-entry', `derived[${index}].path ${issue} (must be a canonical repo-relative path like "docs/gen/index.md"); got ${JSON.stringify(entry && entry.path)}`);
|
|
304
|
+
} else {
|
|
305
|
+
invalid('invalid-derived-entry', `derived[${index}] must have non-empty path and generator strings`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (Object.prototype.hasOwnProperty.call(config, 'tickets') &&
|
|
313
|
+
config.tickets !== 'tracker' && !isRepoRelativePath(config.tickets)) {
|
|
314
|
+
invalid('invalid-tickets', 'tickets must be "tracker" or a repo-relative path');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { valid: diagnostics.length === 0, diagnostics, warnings };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function canonicalize(value) {
|
|
321
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
322
|
+
if (!isPlainObject(value)) return value;
|
|
323
|
+
const normalized = {};
|
|
324
|
+
for (const key of Object.keys(value).sort()) normalized[key] = canonicalize(value[key]);
|
|
325
|
+
return normalized;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function canonicalJson(value) {
|
|
329
|
+
return JSON.stringify(canonicalize(value));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function configSource(workspaceRoot, file) {
|
|
333
|
+
return relativePath(workspaceRoot, file);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
337
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
338
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
339
|
+
const root = path.resolve(resolvedRoot);
|
|
340
|
+
const projectFile = projectDocsConfigPath(root);
|
|
341
|
+
if (fs.existsSync(projectFile)) {
|
|
342
|
+
const source = configSource(root, projectFile);
|
|
343
|
+
let config;
|
|
344
|
+
try {
|
|
345
|
+
config = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
346
|
+
} catch {
|
|
347
|
+
return {
|
|
348
|
+
status: 'invalid',
|
|
349
|
+
sources: [source],
|
|
350
|
+
config: null,
|
|
351
|
+
warnings: [],
|
|
352
|
+
diagnostics: [{ code: 'invalid-project-config-json', severity: 'error', path: source, message: 'project-docs.json must contain valid JSON' }]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const validation = validateDocsConfig(config);
|
|
357
|
+
const diagnostics = validation.diagnostics.map(item => ({ ...item, path: source }));
|
|
358
|
+
const warnings = validation.warnings.map(item => ({ ...item, path: source }));
|
|
359
|
+
if (!validation.valid) {
|
|
360
|
+
return { status: 'invalid', sources: [source], config: null, warnings, diagnostics };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const hosts = inspectHostProjectDocs(root);
|
|
364
|
+
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
365
|
+
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
366
|
+
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
367
|
+
const differing = validHosts.filter(entry => canonicalJson(entry.projectDocs) !== canonicalJson(config));
|
|
368
|
+
if (differing.length) {
|
|
369
|
+
warnings.push({
|
|
370
|
+
code: 'project-overrides-host',
|
|
371
|
+
severity: 'warning',
|
|
372
|
+
path: source,
|
|
373
|
+
message: 'project-docs.json wholly overrides differing host projectDocs values',
|
|
374
|
+
candidates: differing.map(entry => configSource(root, entry.file))
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return { status: 'project', sources: [source], config, warnings: [...warnings, ...hostWarnings], diagnostics: [...diagnostics, ...hostDiagnostics] };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const hosts = inspectHostProjectDocs(root);
|
|
381
|
+
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
382
|
+
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
383
|
+
const activeHome = normalizedHostHome(options.activeHost);
|
|
384
|
+
if (activeHome) {
|
|
385
|
+
const activeHost = hosts.find(entry => entry.home === activeHome);
|
|
386
|
+
const activeFile = activeHost ? activeHost.file : path.join(root, activeHome, 'harness-everything', 'manifest.json');
|
|
387
|
+
const activeSource = configSource(root, activeFile);
|
|
388
|
+
if (activeHost && activeHost.diagnostics.length) {
|
|
389
|
+
return {
|
|
390
|
+
status: 'invalid',
|
|
391
|
+
sources: [activeSource],
|
|
392
|
+
config: null,
|
|
393
|
+
warnings: [...hostWarnings],
|
|
394
|
+
diagnostics: [...hostDiagnostics]
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (!activeHost || !activeHost.hasProjectDocs) {
|
|
398
|
+
return {
|
|
399
|
+
status: 'none',
|
|
400
|
+
sources: [activeSource],
|
|
401
|
+
config: null,
|
|
402
|
+
warnings: [
|
|
403
|
+
...hostWarnings,
|
|
404
|
+
{
|
|
405
|
+
code: 'missing-project-docs-config',
|
|
406
|
+
severity: 'warning',
|
|
407
|
+
path: activeSource,
|
|
408
|
+
message: `active host has no projectDocs config; add .agents/harness-everything/project-docs.json`
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
diagnostics: [...hostDiagnostics]
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
status: 'host',
|
|
416
|
+
sources: [activeSource],
|
|
417
|
+
config: activeHost.projectDocs,
|
|
418
|
+
warnings: [...hostWarnings],
|
|
419
|
+
diagnostics: [...hostDiagnostics]
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
424
|
+
if (!validHosts.length) return { status: 'none', sources: [], config: null, warnings: hostWarnings, diagnostics: hostDiagnostics };
|
|
425
|
+
const sources = validHosts.map(entry => configSource(root, entry.file));
|
|
426
|
+
const firstConfig = validHosts[0].projectDocs;
|
|
427
|
+
const equal = validHosts.every(entry => canonicalJson(entry.projectDocs) === canonicalJson(firstConfig));
|
|
428
|
+
if (equal) return { status: 'host', sources, config: firstConfig, warnings: hostWarnings, diagnostics: hostDiagnostics };
|
|
429
|
+
return {
|
|
430
|
+
status: 'ambiguous',
|
|
431
|
+
sources,
|
|
432
|
+
config: null,
|
|
433
|
+
warnings: hostWarnings,
|
|
434
|
+
diagnostics: [{
|
|
435
|
+
code: 'ambiguous-host-project-docs',
|
|
436
|
+
severity: 'error',
|
|
437
|
+
path: sources[0],
|
|
438
|
+
message: 'host projectDocs configurations differ and no active host selects one',
|
|
439
|
+
candidates: sources
|
|
440
|
+
}, ...hostDiagnostics]
|
|
441
|
+
};
|
|
80
442
|
}
|
|
81
443
|
|
|
82
444
|
function cleanConfiguredPath(value) {
|
|
@@ -230,8 +592,9 @@ function resolveProjectDocs(workspaceRoot, options = {}) {
|
|
|
230
592
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
231
593
|
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass --workspace <path> explicitly');
|
|
232
594
|
const root = path.resolve(resolvedRoot);
|
|
233
|
-
const manifest = readProjectDocs(root);
|
|
234
|
-
const
|
|
595
|
+
const manifest = readProjectDocs(root, options);
|
|
596
|
+
const hasValidConfig = manifest.status === 'project' || manifest.status === 'host';
|
|
597
|
+
const projectDocs = hasValidConfig ? manifest.projectDocs || {} : {};
|
|
235
598
|
const contextMap = parseContextMap(root, projectDocs.contextMap || projectDocs.contextMapPath);
|
|
236
599
|
const paths = {};
|
|
237
600
|
|
|
@@ -272,7 +635,11 @@ function resolveProjectDocs(workspaceRoot, options = {}) {
|
|
|
272
635
|
logs: path.join(runtimeRoot, 'logs'),
|
|
273
636
|
roles: path.join(runtimeRoot, 'roles')
|
|
274
637
|
},
|
|
275
|
-
|
|
638
|
+
projectDocsStatus: manifest.status,
|
|
639
|
+
projectDocsSources: manifest.sources,
|
|
640
|
+
projectDocsWarnings: manifest.warnings,
|
|
641
|
+
projectDocsDiagnostics: manifest.diagnostics,
|
|
642
|
+
projectDocs: hasValidConfig && manifest.file ? { ...projectDocs, manifestPath: manifest.file } : null,
|
|
276
643
|
contextMap: contextMap ? { path: contextMap.path, links: contextMap.links } : null,
|
|
277
644
|
paths,
|
|
278
645
|
// Plural aliases make the contract readable to callers that treat these
|
|
@@ -283,14 +650,40 @@ function resolveProjectDocs(workspaceRoot, options = {}) {
|
|
|
283
650
|
};
|
|
284
651
|
}
|
|
285
652
|
|
|
286
|
-
function detectProjectDocsConventions(workspaceRoot) {
|
|
653
|
+
function detectProjectDocsConventions(workspaceRoot, options = {}) {
|
|
654
|
+
const REQUIRED_CONFIG_FIELDS = ['docLocation', 'tracker', 'issueDefinition'];
|
|
287
655
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
288
656
|
if (!resolvedRoot) return {};
|
|
289
657
|
const root = path.resolve(resolvedRoot);
|
|
290
|
-
const
|
|
658
|
+
const selection = resolveDocsConfig(root, options);
|
|
659
|
+
if (selection.status === 'invalid' || selection.status === 'ambiguous') {
|
|
660
|
+
return {
|
|
661
|
+
status: selection.status,
|
|
662
|
+
sources: selection.sources || [],
|
|
663
|
+
warnings: selection.warnings || [],
|
|
664
|
+
diagnostics: selection.diagnostics || [],
|
|
665
|
+
missing: []
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const configured = selection.config || {};
|
|
291
669
|
const inferred = {};
|
|
292
|
-
|
|
293
|
-
|
|
670
|
+
for (const field of REQUIRED_CONFIG_FIELDS) {
|
|
671
|
+
if (typeof configured[field] === 'string' && configured[field].trim()) inferred[field] = configured[field];
|
|
672
|
+
}
|
|
673
|
+
// Heuristic completion is a resource for UNSELECTED workspaces only: a
|
|
674
|
+
// project or selected-host configuration is authoritative for exactly the
|
|
675
|
+
// fields it declares, so missing selected fields are reported instead of
|
|
676
|
+
// being silently authorized from workspace conventions.
|
|
677
|
+
if (selection.status !== 'none') {
|
|
678
|
+
const missing = REQUIRED_CONFIG_FIELDS.filter(field => !inferred[field]);
|
|
679
|
+
return {
|
|
680
|
+
...inferred,
|
|
681
|
+
status: selection.status,
|
|
682
|
+
sources: selection.sources || [],
|
|
683
|
+
warnings: selection.warnings || [],
|
|
684
|
+
diagnostics: selection.diagnostics || [],
|
|
685
|
+
missing
|
|
686
|
+
};
|
|
294
687
|
}
|
|
295
688
|
if (!inferred.docLocation) {
|
|
296
689
|
for (const dir of ['docs/roadmaps', 'docs/reference', 'docs/specs', 'docs', 'doc']) {
|
|
@@ -300,23 +693,35 @@ function detectProjectDocsConventions(workspaceRoot) {
|
|
|
300
693
|
}
|
|
301
694
|
}
|
|
302
695
|
}
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
696
|
+
if (!inferred.tracker) {
|
|
697
|
+
if (pathIsDir(path.join(root, '.github', 'ISSUE_TEMPLATE'))) {
|
|
698
|
+
inferred.tracker = 'GitHub Issues via .github/ISSUE_TEMPLATE/';
|
|
699
|
+
} else if (pathIsDir(path.join(root, 'docs', 'roadmaps'))) {
|
|
700
|
+
inferred.tracker = 'docs/roadmaps/ local markdown tickets';
|
|
701
|
+
} else if (pathIsDir(path.join(root, 'tasks', 'tickets'))) {
|
|
702
|
+
inferred.tracker = 'tasks/tickets/ local markdown tickets';
|
|
703
|
+
} else if (pathIsDir(path.join(root, '.scratch'))) {
|
|
704
|
+
inferred.tracker = '.scratch/ local markdown workspace';
|
|
705
|
+
} else if (inferred.docLocation) {
|
|
706
|
+
inferred.tracker = `${inferred.docLocation}tickets/ local markdown tickets`;
|
|
707
|
+
}
|
|
313
708
|
}
|
|
314
|
-
if (inferred.
|
|
315
|
-
inferred.
|
|
316
|
-
|
|
317
|
-
|
|
709
|
+
if (!inferred.issueDefinition) {
|
|
710
|
+
if (inferred.tracker && inferred.tracker.includes('ISSUE_TEMPLATE')) {
|
|
711
|
+
inferred.issueDefinition = '.github/ISSUE_TEMPLATE/ template structure';
|
|
712
|
+
} else if (inferred.tracker) {
|
|
713
|
+
inferred.issueDefinition = 'Title + acceptance criteria + Status: ready-for-agent';
|
|
714
|
+
}
|
|
318
715
|
}
|
|
319
|
-
|
|
716
|
+
const missing = REQUIRED_CONFIG_FIELDS.filter(field => !inferred[field]);
|
|
717
|
+
return {
|
|
718
|
+
...inferred,
|
|
719
|
+
status: selection.status,
|
|
720
|
+
sources: selection.sources || [],
|
|
721
|
+
warnings: selection.warnings || [],
|
|
722
|
+
diagnostics: selection.diagnostics || [],
|
|
723
|
+
missing
|
|
724
|
+
};
|
|
320
725
|
}
|
|
321
726
|
|
|
322
727
|
function isIgnored(workspaceRoot, target) {
|
|
@@ -347,6 +752,8 @@ module.exports = {
|
|
|
347
752
|
getRuntimeRoot,
|
|
348
753
|
getRepoManifestHomes,
|
|
349
754
|
readProjectDocs,
|
|
755
|
+
resolveDocsConfig,
|
|
756
|
+
validateDocsConfig,
|
|
350
757
|
parseContextMap,
|
|
351
758
|
resolveProjectDocs,
|
|
352
759
|
detectProjectDocsConventions,
|