harness-everything 0.38.0 → 0.40.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 +1 -1
- package/AGENTS.md +79 -63
- package/CHANGELOG.md +32 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/README.md +76 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/a-reviews-b.md +191 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/b-reviews-a.md +100 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/baseline-probes.log +47 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-red.log +85 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-transition.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/coordinator-probes.txt +84 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/cross-suite.txt +18 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-bom.log +3 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-consistency.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-core.log +198 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-probes.log +27 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-producer.log +83 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-review.md +89 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-transitions.log +5 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-a.md +166 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-b.md +264 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/logs.sha256 +15 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.log +62 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.txt +313 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.log +51 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.txt +94 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/semantic-transitions.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/source.sha256 +9 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-core.log +201 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-core.log +197 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-all.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-gate-summary.json +22 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +489 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/vectors.json +617 -0
- package/ci/mechanism-325-3b-s2a-core.test.js +840 -0
- package/ci/mechanism-325-3b-s2a-producer.test.js +564 -0
- package/code-review/SKILL.md +3 -3
- package/code-review/references/review-playbook.md +4 -0
- package/code-review/templates/finding.template.md +6 -0
- package/docs/doc-workflow-reconciliation.md +357 -0
- package/evals/zoom-out/tasks/negative-first-edge-case-review.yaml +16 -0
- package/evals/zoom-out/tasks/positive-repair-loop.yaml +15 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/harness-everything/scripts/routing-keywords.json +4 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +3 -3
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +4 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +6 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +4 -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 +42 -8
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +4 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +7 -0
- package/plugins/harness-everything/skills/zoom-out/templates/zoom-out-report.template.md +4 -2
- package/sbom.cdx.json +7 -7
- package/scripts/errors-sync.js +90 -0
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +42 -8
- package/zoom-out/SKILL.md +4 -3
- package/zoom-out/references/circuit-breaker.md +7 -0
- package/zoom-out/templates/zoom-out-report.template.md +4 -2
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const { _internals: U } = require('./doc-reconciliation-core');
|
|
7
|
+
const { scanWorkspaceFiles } = U.runtimeModule('doc-lifecycle.js');
|
|
8
|
+
const { resolveDocsConfigCaptured } = U.runtimeModule('project-docs-resolver.js');
|
|
9
|
+
const { deriveDocFacts, deriveDocObligations } = U.runtimeModule('doc-obligations.js');
|
|
10
|
+
|
|
11
|
+
const MAX_FILES = 10000;
|
|
12
|
+
const MAX_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const POSIX_MODES = process.platform !== 'win32';
|
|
14
|
+
function fail(code) { throw new Error(code); }
|
|
15
|
+
// The JS realpath keeps Windows 8.3 spellings (RUNNER~1) while Git reports the long name, so
|
|
16
|
+
// directory identity uses native resolution and is case-insensitive on Windows (as workflow-isolation).
|
|
17
|
+
function canonicalPath(value) { return fs.realpathSync.native(path.resolve(value)); }
|
|
18
|
+
function samePath(a, b) {
|
|
19
|
+
const left = canonicalPath(a);
|
|
20
|
+
const right = canonicalPath(b);
|
|
21
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
22
|
+
}
|
|
23
|
+
function git(root, args, options = {}) {
|
|
24
|
+
const env = { ...process.env };
|
|
25
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', ['-C', root, ...args], { env, timeout: 10000, maxBuffer: MAX_BYTES, stdio: ['pipe', 'pipe', 'pipe'], ...options });
|
|
28
|
+
} catch { fail('baseline-unavailable'); }
|
|
29
|
+
}
|
|
30
|
+
function fileRecord(relative, mode, storedBytes, presence = 'present') {
|
|
31
|
+
return { path: relative, presence, type: presence === 'present' ? 'file' : '', mode,
|
|
32
|
+
storedBytes: Buffer.from(storedBytes), digest: U.sha(storedBytes) };
|
|
33
|
+
}
|
|
34
|
+
function validatePaths(records) {
|
|
35
|
+
const names = new Set();
|
|
36
|
+
for (const item of records) {
|
|
37
|
+
const name = item.path;
|
|
38
|
+
if (!name || name.includes('\\') || name.includes('\ufffd') || name.startsWith('/')
|
|
39
|
+
|| name.split('/').some(segment => !segment || segment === '..' || segment === '.')) fail('inventory-unsupported');
|
|
40
|
+
const folded = name.normalize('NFC').toLowerCase();
|
|
41
|
+
if (names.has(folded)) fail('inventory-unsupported');
|
|
42
|
+
names.add(folded);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function captureBaseline(root, pin) {
|
|
46
|
+
if (!U.record(pin) || !/^[a-f0-9]{40,64}$/.test(pin.commit || '') || !U.text(pin.root)
|
|
47
|
+
|| !samePath(pin.root, root)) fail('baseline-unavailable');
|
|
48
|
+
const actualRoot = git(root, ['rev-parse', '--show-toplevel']).toString('utf8').trim();
|
|
49
|
+
if (!samePath(actualRoot, root)) fail('baseline-unavailable');
|
|
50
|
+
const commit = git(root, ['rev-parse', '--verify', '--end-of-options', `${pin.commit}^{commit}`]).toString('utf8').trim();
|
|
51
|
+
const treeRef = git(root, ['rev-parse', '--verify', '--end-of-options', `${commit}^{tree}`]).toString('utf8').trim();
|
|
52
|
+
const entries = git(root, ['ls-tree', '-r', '-z', '--full-tree', treeRef]).toString('utf8').split('\0').filter(Boolean);
|
|
53
|
+
if (entries.length > MAX_FILES) fail('inventory-unsupported');
|
|
54
|
+
const records = [];
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const match = entry.match(/^(100644|100755) blob ([a-f0-9]{40,64})\t(.+)$/s);
|
|
58
|
+
if (!match) fail('inventory-unsupported');
|
|
59
|
+
const storedBytes = git(root, ['cat-file', 'blob', match[2]]);
|
|
60
|
+
total += storedBytes.length;
|
|
61
|
+
if (total > MAX_BYTES) fail('inventory-unsupported');
|
|
62
|
+
records.push(fileRecord(match[3], match[1], storedBytes));
|
|
63
|
+
}
|
|
64
|
+
validatePaths(records);
|
|
65
|
+
return { commit, treeRef, records };
|
|
66
|
+
}
|
|
67
|
+
function captureCandidate(root, baseline) {
|
|
68
|
+
const raw = [];
|
|
69
|
+
let total = 0;
|
|
70
|
+
let visited = 0;
|
|
71
|
+
function walk(dir, prefix) {
|
|
72
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => U.compare(a.name, b.name));
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!prefix && entry.name === '.git') continue;
|
|
75
|
+
if (++visited > MAX_FILES) fail('inventory-unsupported');
|
|
76
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
77
|
+
const absolute = path.join(dir, entry.name);
|
|
78
|
+
const stat = fs.lstatSync(absolute);
|
|
79
|
+
if (stat.isDirectory()) {
|
|
80
|
+
// Windows stats carry no POSIX permission bits (directories report 0o666); an unreadable
|
|
81
|
+
// path there makes readdir/readFile throw instead.
|
|
82
|
+
if (POSIX_MODES && (!(stat.mode & 0o444) || !(stat.mode & 0o111))) fail('inventory-incomplete');
|
|
83
|
+
walk(absolute, relative);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!stat.isFile()) fail('inventory-unsupported');
|
|
87
|
+
// Unreadable files must remain unavailable even when the process has elevated read access.
|
|
88
|
+
if (POSIX_MODES && !(stat.mode & 0o444)) fail('inventory-incomplete');
|
|
89
|
+
const bytes = fs.readFileSync(absolute);
|
|
90
|
+
total += bytes.length;
|
|
91
|
+
if (raw.length >= MAX_FILES || total > MAX_BYTES) fail('inventory-unsupported');
|
|
92
|
+
raw.push({ path: relative, raw: bytes, mode: stat.mode & 0o111 ? '100755' : '100644' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
walk(root, '');
|
|
96
|
+
validatePaths(raw);
|
|
97
|
+
// Refuse filters/encodings that the existing stored-byte reader does not model.
|
|
98
|
+
if (raw.length) {
|
|
99
|
+
const attrs = git(root, ['check-attr', '-z', '--stdin', 'filter', 'working-tree-encoding', 'ident'], { input: Buffer.from(raw.map(r => r.path).join('\0') + '\0') }).toString('utf8').split('\0');
|
|
100
|
+
for (let i = 0; i + 2 < attrs.length; i += 3) {
|
|
101
|
+
if (!['unspecified', 'unset'].includes(attrs[i + 2])) fail('inventory-unsupported');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Lifecycle owns text/eol/autocrlf conversion. A completeness walk above detects its skipped paths.
|
|
105
|
+
const stored = new Map(scanWorkspaceFiles(root).map(item => [item.path, item.bytes]));
|
|
106
|
+
const records = raw.map(item => {
|
|
107
|
+
const converted = stored.get(item.path);
|
|
108
|
+
if (!converted) fail('inventory-unsupported');
|
|
109
|
+
// An unexplained non-CR difference means the two readers observed different bytes.
|
|
110
|
+
if (!item.raw.includes(13) && !item.raw.equals(converted)) fail('snapshot-changed');
|
|
111
|
+
return fileRecord(item.path, item.mode, converted);
|
|
112
|
+
});
|
|
113
|
+
for (const item of baseline.records) if (!stored.has(item.path)) records.push(fileRecord(item.path, '', Buffer.alloc(0), 'deleted'));
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
const progressKeys = new Set(['status', 'state', 'evidence', 'loaded', 'loadedAt', 'updatedAt', 'createdAt', 'completedAt', 'timestamp', 'report', 'reports']);
|
|
117
|
+
// Progress fields are excluded only at their defined record locations (top-level task/workflow, each
|
|
118
|
+
// requirement, each requirement binding). Nested objects keep every key: a nested `status`/`state` there
|
|
119
|
+
// is semantic and must change taskScopeDigest (PR #353 review P2).
|
|
120
|
+
function withoutProgress(value, keys = progressKeys) {
|
|
121
|
+
if (!U.record(value)) return U.clone(value);
|
|
122
|
+
return Object.fromEntries(Object.keys(value).filter(key => !keys.has(key)).map(key => [key, U.clone(value[key])]));
|
|
123
|
+
}
|
|
124
|
+
// A binding's `report`/`reports` object configures the binding; only its load/status records are progress.
|
|
125
|
+
const bindingProgressKeys = new Set([...progressKeys].filter(key => key !== 'report' && key !== 'reports'));
|
|
126
|
+
function taskSemantics(task) {
|
|
127
|
+
const semantic = withoutProgress(task);
|
|
128
|
+
if (Array.isArray(task?.requirements)) {
|
|
129
|
+
semantic.requirements = task.requirements.map(requirement => {
|
|
130
|
+
const item = withoutProgress(requirement);
|
|
131
|
+
if (U.record(requirement) && Array.isArray(requirement.bindings)) item.bindings = requirement.bindings.map(binding => withoutProgress(binding, bindingProgressKeys));
|
|
132
|
+
return item;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return semantic;
|
|
136
|
+
}
|
|
137
|
+
function contextFor(input, provider) {
|
|
138
|
+
if (!provider || typeof provider.resolveContext !== 'function') fail('origin-unavailable');
|
|
139
|
+
const response = provider.resolveContext({ workspaceRoot: input.workspaceRoot, workflowId: input.workflow.workflowId, sessionId: input.workflow.sessionId });
|
|
140
|
+
if (response?.status !== 'accepted') fail(response?.status === 'rejected' ? 'origin-untrusted' : 'origin-unavailable');
|
|
141
|
+
const c = response.context;
|
|
142
|
+
if (!U.record(c) || !U.text(response.originRef) || !U.text(c.workspaceId) || !U.text(c.baselineReference)
|
|
143
|
+
|| !U.text(c.principals?.author) || !Array.isArray(c.principals?.executors) || !c.principals.executors.every(U.text)) fail('origin-untrusted');
|
|
144
|
+
for (const key of ['sessionId', 'workflowId', 'workflowRevision']) if (c[key] !== input.workflow[key]) fail('identity-mismatch');
|
|
145
|
+
if (c.taskId !== input.task.taskId || U.stable(c.activeHost) !== U.stable(input.activeHost)) fail('identity-mismatch');
|
|
146
|
+
if (c.workspaceRoot && !samePath(c.workspaceRoot, input.workspaceRoot)) fail('identity-mismatch');
|
|
147
|
+
// The provider must bind the concrete workflow-start pin; a bare reference cannot authorize a
|
|
148
|
+
// re-anchored baseline (PR #353 review P1).
|
|
149
|
+
if (!U.record(c.baseline) || !U.text(c.baseline.commit) || !U.text(c.baseline.root)
|
|
150
|
+
|| !U.record(input.baseline) || c.baseline.commit !== input.baseline.commit || !samePath(c.baseline.root, input.baseline.root)) fail('baseline-unavailable');
|
|
151
|
+
const root = canonicalPath(input.workspaceRoot);
|
|
152
|
+
return U.clone({ ...c, baseline: { ...c.baseline, root }, workspaceRoot: root, originRef: response.originRef });
|
|
153
|
+
}
|
|
154
|
+
function filesOf(records, config) {
|
|
155
|
+
// The inventory is whole-project: binary records keep their stored bytes in the identity framing and are
|
|
156
|
+
// omitted from semantic derivation unless the derive contract can observe them (PR #353 review P2).
|
|
157
|
+
return U.deriveFiles(records, config);
|
|
158
|
+
}
|
|
159
|
+
function produceDocSnapshot(input, dependencies = {}) {
|
|
160
|
+
const originProvider = dependencies?.originProvider;
|
|
161
|
+
try {
|
|
162
|
+
if (!U.record(input) || !U.text(input.workspaceRoot) || !U.record(input.workflow) || !U.record(input.task)
|
|
163
|
+
|| !U.text(input.task.taskId) || !Array.isArray(input.task.requirements) || !U.record(input.scope)
|
|
164
|
+
|| !Number.isInteger(input.workflow.workflowRevision) || input.workflow.workflowRevision < 0) fail('invalid-input');
|
|
165
|
+
if ((input.prdRoot === undefined ? 'docs/prd/' : input.prdRoot) !== 'docs/prd/' || (input.scope.prdRoot && input.scope.prdRoot !== 'docs/prd/')) fail('unsupported-prd-root');
|
|
166
|
+
const root = canonicalPath(input.workspaceRoot);
|
|
167
|
+
const detached = U.clone(input);
|
|
168
|
+
const originContext = contextFor(detached, originProvider);
|
|
169
|
+
const baseline = captureBaseline(root, detached.baseline);
|
|
170
|
+
const config = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
|
|
171
|
+
if (!['project', 'host'].includes(config.resolution.status) || config.capture.sources.some(s => s.presence === 'unreadable')) fail('config-unavailable');
|
|
172
|
+
const coverage = { kind: 'whole-project', excluded: ['.git'], scope: U.clone(detached.scope) };
|
|
173
|
+
const candidate = { records: captureCandidate(root, baseline), coverage };
|
|
174
|
+
// Same owned config bytes must be the bytes covered by the working-tree capture.
|
|
175
|
+
for (const source of config.capture.sources) {
|
|
176
|
+
const file = candidate.records.find(r => r.path === source.path && r.presence === 'present');
|
|
177
|
+
if (source.presence === 'present' ? !file || !U.bytes(source.bytes).equals(file.storedBytes) : Boolean(file)) fail('snapshot-changed');
|
|
178
|
+
}
|
|
179
|
+
config.digest = U.configDigest(config);
|
|
180
|
+
baseline.digest = U.snapshotDigest('baseline', baseline, coverage, baseline.records);
|
|
181
|
+
candidate.digest = U.snapshotDigest('candidate', baseline, coverage, candidate.records);
|
|
182
|
+
const planIdentity = { task: taskSemantics(detached.task), scope: U.clone(detached.scope), workflow: withoutProgress(detached.workflow), coverage, configDigest: config.digest, prdRoot: 'docs/prd/' };
|
|
183
|
+
const identity = { workspaceId: originContext.workspaceId, sessionId: originContext.sessionId, workflowId: originContext.workflowId,
|
|
184
|
+
workflowRevision: originContext.workflowRevision, taskId: originContext.taskId, taskScopeDigest: U.structuredDigest('doc-task-scope-v1', planIdentity),
|
|
185
|
+
baselineDigest: baseline.digest, candidateDigest: candidate.digest, configDigest: config.digest, prdRoot: 'docs/prd/' };
|
|
186
|
+
const derived = deriveDocFacts({ base: { files: filesOf(baseline.records, config.resolution.config) },
|
|
187
|
+
candidate: { files: filesOf(candidate.records, config.resolution.config) }, config: config.resolution.config });
|
|
188
|
+
if (!derived.facts?.inputKnown) fail('inventory-incomplete');
|
|
189
|
+
const facts = derived.facts;
|
|
190
|
+
const obligations = deriveDocObligations(facts);
|
|
191
|
+
const derivationReasons = Object.fromEntries(obligations.map(o => [o.id, { applicability: o.applicability, reason: `captured facts derive ${o.capability}: ${o.applicability}`, factRefs: ['facts', ...(o.scope?.paths || []).map(p => `inventory:${p}`)] }]));
|
|
192
|
+
const again = captureCandidate(root, baseline);
|
|
193
|
+
const configAgain = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
|
|
194
|
+
let finalContext;
|
|
195
|
+
try { finalContext = contextFor(detached, originProvider); }
|
|
196
|
+
catch { fail('snapshot-changed'); }
|
|
197
|
+
if (U.snapshotDigest('candidate', baseline, coverage, again) !== candidate.digest || U.configDigest(configAgain) !== config.digest
|
|
198
|
+
|| U.stable(originContext) !== U.stable(finalContext)) fail('snapshot-changed');
|
|
199
|
+
return { state: 'ready', snapshot: { identity, baseline, candidate, config, originContext, taskSemantics: planIdentity, facts, obligations, derivationReasons }, diagnostics: [] };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const codes = ['invalid-input', 'baseline-unavailable', 'inventory-incomplete', 'inventory-unsupported', 'snapshot-changed', 'config-unavailable', 'unsupported-prd-root', 'identity-mismatch', 'origin-unavailable', 'origin-untrusted'];
|
|
202
|
+
const code = codes.includes(error.message) ? error.message : 'inventory-unsupported';
|
|
203
|
+
return { state: 'unavailable', snapshot: null, diagnostics: [U.diagnostic(code)] };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
module.exports = { produceDocSnapshot };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.40.0",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -4,7 +4,7 @@ description: "Independently review a PR, diff or finished implementation for cor
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.40.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Code Review
|
|
@@ -30,10 +30,10 @@ Independently review completed changes for correctness, contract violations, and
|
|
|
30
30
|
|
|
31
31
|
## Workflow
|
|
32
32
|
|
|
33
|
-
1. Pin repository, contract, base/head, changed scope, and CI state; inspect relevant callers and consumers.
|
|
33
|
+
1. Pin repository, verbatim acceptance contract, base/head, changed scope, and CI state; inspect relevant callers and consumers.
|
|
34
34
|
2. Independently trace behavior and test applicable counterexamples. Separate severity from safety conditions.
|
|
35
35
|
3. Report only supported findings with evidence tier, smallest correction, regression, and positive control. Use `<this-skill-dir>/templates/finding.template.md`.
|
|
36
|
-
4. On re-review, disposition every prior finding at the new HEAD, then inspect for regressions.
|
|
36
|
+
4. On re-review, disposition every prior finding at the new HEAD, re-trace the acceptance path, then inspect for regressions.
|
|
37
37
|
5. Keep review read-only unless explicitly authorized. Do not claim “bug-free.”
|
|
38
38
|
|
|
39
39
|
In the interim, bind explicitly on an existing review step: `{"stepType":"review","optionalBindings":["code-review"]}`.
|
|
@@ -5,6 +5,7 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
|
|
|
5
5
|
## Establish scope and evidence
|
|
6
6
|
|
|
7
7
|
- [ ] Pin repository, PR, base/head SHA, user requirement or PRD/ADR, changed files, relevant callers/callees, and current CI state. Avoid unrelated checkout areas.
|
|
8
|
+
- [ ] Quote the acceptance clauses and owner bounds verbatim, with their source. Keep them apart from the chosen implementation (designs, existing tests) and the evidence: a design choice or a test expectation is not a requirement.
|
|
8
9
|
- [ ] Make an independent pass. Treat author explanations and tests as leads, not proof; derive expected behavior from contracts and source.
|
|
9
10
|
- [ ] Label evidence honestly: **executed/reproduced** (command, result, environment, SHA); **source-traced** (concrete path and preconditions); **inconclusive** (missing facts or ambiguous semantics; do not present as a proven blocker).
|
|
10
11
|
- [ ] Inventory user-facing entry points and configuration handoffs (CLI flags, cwd, path spellings, config loading). Exercise them end-to-end, not only the recently edited internals.
|
|
@@ -24,10 +25,13 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
|
|
|
24
25
|
- [ ] When output text changes, compare the intended changed lines with literal templates; do not exclude changed lines from assertions.
|
|
25
26
|
- [ ] Dedupe findings. Use P1 for severe merge blockers, P2 for meaningful correctness/contract defects, P3 for low-risk issues; avoid speculation.
|
|
26
27
|
- [ ] For each finding, include exact path:line or diff anchor at HEAD SHA, trigger, observed behavior, violated contract and impact, evidence tier, smallest fix, negative regression, positive control, and disposition.
|
|
28
|
+
- [ ] Name the exact acceptance clause each finding violates, with a witness (starting state, input, expected result, actual result) and the trust assumption. A finding that maps to no clause gets scope `unresolved` for the owner; do not drop it, and do not present it as a blocker. Record severity and scope separately.
|
|
27
29
|
|
|
28
30
|
## Re-review and conclude
|
|
29
31
|
|
|
30
32
|
- [ ] Pin the new HEAD. Disposition every prior finding separately: resolved, still open, superseded, or inconclusive. Then inspect fix-induced side effects and new changes.
|
|
33
|
+
- [ ] Re-trace one normal acceptance path end to end plus the consumers the fix affects, even when the request names only the last fix. A diff-only re-review misses unchanged sites that make the same decision.
|
|
34
|
+
- [ ] If this round and the previous one each found new defects in the same decision class, say so and recommend `zoom-out` (site inventory against the violated clause) before another point fix.
|
|
31
35
|
- [ ] Stay read-only unless the user/workflow explicitly authorizes action. Do not edit, comment, label, or merge on review authority alone.
|
|
32
36
|
- [ ] Summarize scope/SHA, CI, covered and unexamined risks, fixed/open/new counts, and evidence-grounded merge recommendation. “No actionable findings in examined scope” does not mean “bug-free.” Distinguish repository tests from live-host evidence.
|
|
33
37
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- Repository / PR:
|
|
6
6
|
- Base SHA → reviewed HEAD SHA:
|
|
7
7
|
- Requirement / PRD / ADR:
|
|
8
|
+
- Scope anchor (acceptance clauses and owner bounds, verbatim, with source):
|
|
9
|
+
- Prior rounds (findings per decision class, for re-review):
|
|
8
10
|
- Changed files and relevant callers/consumers:
|
|
9
11
|
- CI state and commands observed:
|
|
10
12
|
- Counterexample categories covered:
|
|
@@ -17,6 +19,9 @@
|
|
|
17
19
|
- **Location:** `<path>:<line or diff anchor> @ <HEAD SHA>`
|
|
18
20
|
- **Trigger:** <minimal input, state transition, or interleaving>
|
|
19
21
|
- **Behavior → impact:** <observed or source-traced behavior and violated contract>
|
|
22
|
+
- **Violates:** <exact acceptance clause from the scope anchor>
|
|
23
|
+
- **Witness:** <starting state → input → expected result → actual result; trust assumption>
|
|
24
|
+
- **Scope:** <in-scope | out-of-scope (known limit) | unresolved (owner decision)>
|
|
20
25
|
- **Evidence tier:** <executed/reproduced | source-traced | inconclusive; include exact command/result or preconditions>
|
|
21
26
|
- **Minimal correction:** <smallest feasible fix>
|
|
22
27
|
- **Regression:** Negative case: <case that fails before fix>; positive control: <valid behavior that remains valid>
|
|
@@ -30,4 +35,5 @@
|
|
|
30
35
|
- **CI evidence:**
|
|
31
36
|
- **Covered / unexamined risks:**
|
|
32
37
|
- **Findings:** <fixed / open / new counts on re-review>
|
|
38
|
+
- **Convergence:** <new findings per decision class this round vs the previous round; recommend `zoom-out` when a class keeps producing new findings>
|
|
33
39
|
- **Conclusion:** <merge recommendation grounded in evidence; “no actionable findings in examined scope” is not a claim of bug-free code>
|
package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js
CHANGED
|
@@ -122,15 +122,17 @@ function readHostProjectDocsConfigs(workspaceRoot) {
|
|
|
122
122
|
return manifests;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function inspectHostProjectDocs(workspaceRoot) {
|
|
125
|
+
function inspectHostProjectDocs(workspaceRoot, readSource = null) {
|
|
126
126
|
const entries = [];
|
|
127
127
|
for (const home of PLATFORM_HOMES) {
|
|
128
128
|
const file = path.join(workspaceRoot, home, 'harness-everything', 'manifest.json');
|
|
129
|
-
|
|
129
|
+
const captured = readSource ? readSource(file) : null;
|
|
130
|
+
if (captured ? captured.presence === 'absent' : !fs.existsSync(file)) continue;
|
|
130
131
|
const entry = { file, home, projectDocs: null, warnings: [], diagnostics: [], hasProjectDocs: false };
|
|
131
132
|
let manifest;
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
if (captured && captured.presence !== 'present') throw new Error('unreadable source');
|
|
135
|
+
manifest = JSON.parse(captured ? captured.bytes.toString('utf8') : fs.readFileSync(file, 'utf8'));
|
|
134
136
|
} catch {
|
|
135
137
|
entry.diagnostics.push({
|
|
136
138
|
code: 'invalid-host-config',
|
|
@@ -349,16 +351,18 @@ function configSource(workspaceRoot, file) {
|
|
|
349
351
|
return relativePath(workspaceRoot, file);
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
354
|
+
function resolveDocsConfigSelection(workspaceRoot, options, readSource) {
|
|
353
355
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
354
356
|
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
355
357
|
const root = path.resolve(resolvedRoot);
|
|
356
358
|
const projectFile = projectDocsConfigPath(root);
|
|
357
|
-
|
|
359
|
+
const capturedProject = readSource(projectFile);
|
|
360
|
+
if (capturedProject.presence !== 'absent') {
|
|
358
361
|
const source = configSource(root, projectFile);
|
|
359
362
|
let config;
|
|
360
363
|
try {
|
|
361
|
-
|
|
364
|
+
if (capturedProject.presence !== 'present') throw new Error('unreadable source');
|
|
365
|
+
config = JSON.parse(capturedProject.bytes.toString('utf8'));
|
|
362
366
|
} catch {
|
|
363
367
|
return {
|
|
364
368
|
status: 'invalid',
|
|
@@ -376,7 +380,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
376
380
|
return { status: 'invalid', sources: [source], config: null, warnings, diagnostics };
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
const hosts = inspectHostProjectDocs(root);
|
|
383
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
380
384
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
381
385
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
382
386
|
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
@@ -393,7 +397,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
393
397
|
return { status: 'project', sources: [source], config, warnings: [...warnings, ...hostWarnings], diagnostics: [...diagnostics, ...hostDiagnostics] };
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
const hosts = inspectHostProjectDocs(root);
|
|
400
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
397
401
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
398
402
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
399
403
|
const activeHome = normalizedHostHome(options.activeHost);
|
|
@@ -457,6 +461,35 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function resolveDocsConfigCaptured(workspaceRoot, options = {}) {
|
|
465
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
466
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
467
|
+
const root = path.resolve(resolvedRoot);
|
|
468
|
+
const records = new Map();
|
|
469
|
+
const readSource = file => {
|
|
470
|
+
const key = configSource(root, file);
|
|
471
|
+
if (!records.has(key)) {
|
|
472
|
+
let bytes = Buffer.alloc(0);
|
|
473
|
+
let presence = 'present';
|
|
474
|
+
try { bytes = Buffer.from(fs.readFileSync(file)); }
|
|
475
|
+
catch (error) { presence = error.code === 'ENOENT' ? 'absent' : 'unreadable'; }
|
|
476
|
+
records.set(key, { path: key, presence, bytes, digest: crypto.createHash('sha256').update(bytes).digest('hex') });
|
|
477
|
+
}
|
|
478
|
+
return records.get(key);
|
|
479
|
+
};
|
|
480
|
+
const resolution = resolveDocsConfigSelection(root, options, readSource);
|
|
481
|
+
let activeHost = null;
|
|
482
|
+
try { activeHost = JSON.parse(JSON.stringify(options.activeHost)); } catch { /* no serializable host context */ }
|
|
483
|
+
const sources = [...records.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
484
|
+
const selectionInputs = sources.map(({ path: sourcePath, presence }) => ({ path: sourcePath, presence }));
|
|
485
|
+
selectionInputs.push({ status: resolution.status, sources: [...resolution.sources], activeHost, config: resolution.config });
|
|
486
|
+
return { resolution, capture: { activeHost, sources, selectionInputs } };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
490
|
+
return resolveDocsConfigCaptured(workspaceRoot, options).resolution;
|
|
491
|
+
}
|
|
492
|
+
|
|
460
493
|
function cleanConfiguredPath(value) {
|
|
461
494
|
if (typeof value !== 'string') return null;
|
|
462
495
|
const trimmed = value.trim();
|
|
@@ -769,6 +802,7 @@ module.exports = {
|
|
|
769
802
|
getRepoManifestHomes,
|
|
770
803
|
readProjectDocs,
|
|
771
804
|
resolveDocsConfig,
|
|
805
|
+
resolveDocsConfigCaptured,
|
|
772
806
|
validateDocsConfig,
|
|
773
807
|
normalizeHostProjectDocs,
|
|
774
808
|
parseContextMap,
|
package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js
CHANGED
|
@@ -122,15 +122,17 @@ function readHostProjectDocsConfigs(workspaceRoot) {
|
|
|
122
122
|
return manifests;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function inspectHostProjectDocs(workspaceRoot) {
|
|
125
|
+
function inspectHostProjectDocs(workspaceRoot, readSource = null) {
|
|
126
126
|
const entries = [];
|
|
127
127
|
for (const home of PLATFORM_HOMES) {
|
|
128
128
|
const file = path.join(workspaceRoot, home, 'harness-everything', 'manifest.json');
|
|
129
|
-
|
|
129
|
+
const captured = readSource ? readSource(file) : null;
|
|
130
|
+
if (captured ? captured.presence === 'absent' : !fs.existsSync(file)) continue;
|
|
130
131
|
const entry = { file, home, projectDocs: null, warnings: [], diagnostics: [], hasProjectDocs: false };
|
|
131
132
|
let manifest;
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
if (captured && captured.presence !== 'present') throw new Error('unreadable source');
|
|
135
|
+
manifest = JSON.parse(captured ? captured.bytes.toString('utf8') : fs.readFileSync(file, 'utf8'));
|
|
134
136
|
} catch {
|
|
135
137
|
entry.diagnostics.push({
|
|
136
138
|
code: 'invalid-host-config',
|
|
@@ -349,16 +351,18 @@ function configSource(workspaceRoot, file) {
|
|
|
349
351
|
return relativePath(workspaceRoot, file);
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
354
|
+
function resolveDocsConfigSelection(workspaceRoot, options, readSource) {
|
|
353
355
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
354
356
|
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
355
357
|
const root = path.resolve(resolvedRoot);
|
|
356
358
|
const projectFile = projectDocsConfigPath(root);
|
|
357
|
-
|
|
359
|
+
const capturedProject = readSource(projectFile);
|
|
360
|
+
if (capturedProject.presence !== 'absent') {
|
|
358
361
|
const source = configSource(root, projectFile);
|
|
359
362
|
let config;
|
|
360
363
|
try {
|
|
361
|
-
|
|
364
|
+
if (capturedProject.presence !== 'present') throw new Error('unreadable source');
|
|
365
|
+
config = JSON.parse(capturedProject.bytes.toString('utf8'));
|
|
362
366
|
} catch {
|
|
363
367
|
return {
|
|
364
368
|
status: 'invalid',
|
|
@@ -376,7 +380,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
376
380
|
return { status: 'invalid', sources: [source], config: null, warnings, diagnostics };
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
const hosts = inspectHostProjectDocs(root);
|
|
383
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
380
384
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
381
385
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
382
386
|
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
@@ -393,7 +397,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
393
397
|
return { status: 'project', sources: [source], config, warnings: [...warnings, ...hostWarnings], diagnostics: [...diagnostics, ...hostDiagnostics] };
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
const hosts = inspectHostProjectDocs(root);
|
|
400
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
397
401
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
398
402
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
399
403
|
const activeHome = normalizedHostHome(options.activeHost);
|
|
@@ -457,6 +461,35 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function resolveDocsConfigCaptured(workspaceRoot, options = {}) {
|
|
465
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
466
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
467
|
+
const root = path.resolve(resolvedRoot);
|
|
468
|
+
const records = new Map();
|
|
469
|
+
const readSource = file => {
|
|
470
|
+
const key = configSource(root, file);
|
|
471
|
+
if (!records.has(key)) {
|
|
472
|
+
let bytes = Buffer.alloc(0);
|
|
473
|
+
let presence = 'present';
|
|
474
|
+
try { bytes = Buffer.from(fs.readFileSync(file)); }
|
|
475
|
+
catch (error) { presence = error.code === 'ENOENT' ? 'absent' : 'unreadable'; }
|
|
476
|
+
records.set(key, { path: key, presence, bytes, digest: crypto.createHash('sha256').update(bytes).digest('hex') });
|
|
477
|
+
}
|
|
478
|
+
return records.get(key);
|
|
479
|
+
};
|
|
480
|
+
const resolution = resolveDocsConfigSelection(root, options, readSource);
|
|
481
|
+
let activeHost = null;
|
|
482
|
+
try { activeHost = JSON.parse(JSON.stringify(options.activeHost)); } catch { /* no serializable host context */ }
|
|
483
|
+
const sources = [...records.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
484
|
+
const selectionInputs = sources.map(({ path: sourcePath, presence }) => ({ path: sourcePath, presence }));
|
|
485
|
+
selectionInputs.push({ status: resolution.status, sources: [...resolution.sources], activeHost, config: resolution.config });
|
|
486
|
+
return { resolution, capture: { activeHost, sources, selectionInputs } };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
490
|
+
return resolveDocsConfigCaptured(workspaceRoot, options).resolution;
|
|
491
|
+
}
|
|
492
|
+
|
|
460
493
|
function cleanConfiguredPath(value) {
|
|
461
494
|
if (typeof value !== 'string') return null;
|
|
462
495
|
const trimmed = value.trim();
|
|
@@ -769,6 +802,7 @@ module.exports = {
|
|
|
769
802
|
getRepoManifestHomes,
|
|
770
803
|
readProjectDocs,
|
|
771
804
|
resolveDocsConfig,
|
|
805
|
+
resolveDocsConfigCaptured,
|
|
772
806
|
validateDocsConfig,
|
|
773
807
|
normalizeHostProjectDocs,
|
|
774
808
|
parseContextMap,
|
|
@@ -122,15 +122,17 @@ function readHostProjectDocsConfigs(workspaceRoot) {
|
|
|
122
122
|
return manifests;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function inspectHostProjectDocs(workspaceRoot) {
|
|
125
|
+
function inspectHostProjectDocs(workspaceRoot, readSource = null) {
|
|
126
126
|
const entries = [];
|
|
127
127
|
for (const home of PLATFORM_HOMES) {
|
|
128
128
|
const file = path.join(workspaceRoot, home, 'harness-everything', 'manifest.json');
|
|
129
|
-
|
|
129
|
+
const captured = readSource ? readSource(file) : null;
|
|
130
|
+
if (captured ? captured.presence === 'absent' : !fs.existsSync(file)) continue;
|
|
130
131
|
const entry = { file, home, projectDocs: null, warnings: [], diagnostics: [], hasProjectDocs: false };
|
|
131
132
|
let manifest;
|
|
132
133
|
try {
|
|
133
|
-
|
|
134
|
+
if (captured && captured.presence !== 'present') throw new Error('unreadable source');
|
|
135
|
+
manifest = JSON.parse(captured ? captured.bytes.toString('utf8') : fs.readFileSync(file, 'utf8'));
|
|
134
136
|
} catch {
|
|
135
137
|
entry.diagnostics.push({
|
|
136
138
|
code: 'invalid-host-config',
|
|
@@ -349,16 +351,18 @@ function configSource(workspaceRoot, file) {
|
|
|
349
351
|
return relativePath(workspaceRoot, file);
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
function
|
|
354
|
+
function resolveDocsConfigSelection(workspaceRoot, options, readSource) {
|
|
353
355
|
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
354
356
|
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
355
357
|
const root = path.resolve(resolvedRoot);
|
|
356
358
|
const projectFile = projectDocsConfigPath(root);
|
|
357
|
-
|
|
359
|
+
const capturedProject = readSource(projectFile);
|
|
360
|
+
if (capturedProject.presence !== 'absent') {
|
|
358
361
|
const source = configSource(root, projectFile);
|
|
359
362
|
let config;
|
|
360
363
|
try {
|
|
361
|
-
|
|
364
|
+
if (capturedProject.presence !== 'present') throw new Error('unreadable source');
|
|
365
|
+
config = JSON.parse(capturedProject.bytes.toString('utf8'));
|
|
362
366
|
} catch {
|
|
363
367
|
return {
|
|
364
368
|
status: 'invalid',
|
|
@@ -376,7 +380,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
376
380
|
return { status: 'invalid', sources: [source], config: null, warnings, diagnostics };
|
|
377
381
|
}
|
|
378
382
|
|
|
379
|
-
const hosts = inspectHostProjectDocs(root);
|
|
383
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
380
384
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
381
385
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
382
386
|
const validHosts = hosts.filter(entry => entry.projectDocs);
|
|
@@ -393,7 +397,7 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
393
397
|
return { status: 'project', sources: [source], config, warnings: [...warnings, ...hostWarnings], diagnostics: [...diagnostics, ...hostDiagnostics] };
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
const hosts = inspectHostProjectDocs(root);
|
|
400
|
+
const hosts = inspectHostProjectDocs(root, readSource);
|
|
397
401
|
const hostDiagnostics = hosts.flatMap(entry => entry.diagnostics);
|
|
398
402
|
const hostWarnings = hosts.flatMap(entry => entry.warnings);
|
|
399
403
|
const activeHome = normalizedHostHome(options.activeHost);
|
|
@@ -457,6 +461,35 @@ function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
|
457
461
|
};
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function resolveDocsConfigCaptured(workspaceRoot, options = {}) {
|
|
465
|
+
const resolvedRoot = workspaceRoot || getWorkspaceRoot();
|
|
466
|
+
if (!resolvedRoot) throw new Error('cannot resolve project docs outside a git workspace; pass a workspace root explicitly');
|
|
467
|
+
const root = path.resolve(resolvedRoot);
|
|
468
|
+
const records = new Map();
|
|
469
|
+
const readSource = file => {
|
|
470
|
+
const key = configSource(root, file);
|
|
471
|
+
if (!records.has(key)) {
|
|
472
|
+
let bytes = Buffer.alloc(0);
|
|
473
|
+
let presence = 'present';
|
|
474
|
+
try { bytes = Buffer.from(fs.readFileSync(file)); }
|
|
475
|
+
catch (error) { presence = error.code === 'ENOENT' ? 'absent' : 'unreadable'; }
|
|
476
|
+
records.set(key, { path: key, presence, bytes, digest: crypto.createHash('sha256').update(bytes).digest('hex') });
|
|
477
|
+
}
|
|
478
|
+
return records.get(key);
|
|
479
|
+
};
|
|
480
|
+
const resolution = resolveDocsConfigSelection(root, options, readSource);
|
|
481
|
+
let activeHost = null;
|
|
482
|
+
try { activeHost = JSON.parse(JSON.stringify(options.activeHost)); } catch { /* no serializable host context */ }
|
|
483
|
+
const sources = [...records.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
484
|
+
const selectionInputs = sources.map(({ path: sourcePath, presence }) => ({ path: sourcePath, presence }));
|
|
485
|
+
selectionInputs.push({ status: resolution.status, sources: [...resolution.sources], activeHost, config: resolution.config });
|
|
486
|
+
return { resolution, capture: { activeHost, sources, selectionInputs } };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveDocsConfig(workspaceRoot, options = {}) {
|
|
490
|
+
return resolveDocsConfigCaptured(workspaceRoot, options).resolution;
|
|
491
|
+
}
|
|
492
|
+
|
|
460
493
|
function cleanConfiguredPath(value) {
|
|
461
494
|
if (typeof value !== 'string') return null;
|
|
462
495
|
const trimmed = value.trim();
|
|
@@ -769,6 +802,7 @@ module.exports = {
|
|
|
769
802
|
getRepoManifestHomes,
|
|
770
803
|
readProjectDocs,
|
|
771
804
|
resolveDocsConfig,
|
|
805
|
+
resolveDocsConfigCaptured,
|
|
772
806
|
validateDocsConfig,
|
|
773
807
|
normalizeHostProjectDocs,
|
|
774
808
|
parseContextMap,
|