harness-everything 0.47.4 → 0.48.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.
Files changed (60) hide show
  1. package/.agents/harness-everything/project-docs.json +2 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +33 -0
  5. package/behavioral-evals/README.md +17 -0
  6. package/behavioral-evals/case-validator.js +15 -1
  7. package/behavioral-evals/cases/baseline-tdd-workflow.yaml +4 -0
  8. package/behavioral-evals/run.js +172 -0
  9. package/behavioral-evals/transcript-parser.js +43 -0
  10. package/ci/fixtures/doc-obligations/reconciliation-s2a/run13-workflow-requirements.json +42 -0
  11. package/ci/mechanism-2s-tdd-quality.test.js +149 -6
  12. package/ci/mechanism-e1-dependency-artifact-scope.test.js +657 -0
  13. package/ci/mechanism-prb-outline-review.test.js +1 -1
  14. package/ci/test-behavioral-case-validation.js +215 -1
  15. package/code-review/SKILL.md +1 -1
  16. package/code-review/references/review-playbook.md +9 -0
  17. package/docs/doc-workflow-reconciliation.md +3 -1
  18. package/harness-everything/SKILL.md +1 -1
  19. package/harness-everything/scripts/doc-lifecycle.js +11 -3
  20. package/harness-everything/scripts/project-docs-resolver.js +12 -0
  21. package/hooks/scripts/lib/doc-reconciliation-core.js +56 -0
  22. package/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
  23. package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
  24. package/multi-agent-workspace/SKILL.md +1 -1
  25. package/multi-agent-workspace/scripts/project-docs-resolver.js +12 -0
  26. package/opencode-plugin/plugin.json +1 -1
  27. package/package.json +1 -1
  28. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  29. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +56 -0
  30. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
  31. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
  32. package/plugins/harness-everything/plugin.json +1 -1
  33. package/plugins/harness-everything/skills/code-review/SKILL.md +1 -1
  34. package/plugins/harness-everything/skills/code-review/references/review-playbook.md +9 -0
  35. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  36. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +11 -3
  37. package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +12 -0
  38. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  39. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +12 -0
  40. package/plugins/harness-everything/skills/tdd/SKILL.md +1 -1
  41. package/plugins/harness-everything/skills/tdd/fixtures/quality-scenarios.json +10 -2
  42. package/plugins/harness-everything/skills/tdd/references/quality-model.md +2 -0
  43. package/plugins/harness-everything/skills/tdd/schemas/quality-evidence.schema.json +21 -0
  44. package/plugins/harness-everything/skills/tdd/schemas/quality-report.schema.json +2 -0
  45. package/plugins/harness-everything/skills/tdd/scripts/quality-gate.js +165 -1
  46. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  47. package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +12 -0
  48. package/plugins/harness-everything/skills/zoom-out/SKILL.md +1 -1
  49. package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +1 -0
  50. package/sbom.cdx.json +7 -7
  51. package/tdd/SKILL.md +1 -1
  52. package/tdd/fixtures/quality-scenarios.json +10 -2
  53. package/tdd/references/quality-model.md +2 -0
  54. package/tdd/schemas/quality-evidence.schema.json +21 -0
  55. package/tdd/schemas/quality-report.schema.json +2 -0
  56. package/tdd/scripts/quality-gate.js +165 -1
  57. package/to-spec/SKILL.md +1 -1
  58. package/to-spec/scripts/project-docs-resolver.js +12 -0
  59. package/zoom-out/SKILL.md +1 -1
  60. package/zoom-out/references/circuit-breaker.md +1 -0
@@ -4,14 +4,17 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { execFileSync } = require('child_process');
6
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');
7
+ const { convertWorkspaceFiles } = U.runtimeModule('doc-lifecycle.js');
8
+ const { resolveDocsConfigCaptured, validateDocsConfig } = U.runtimeModule('project-docs-resolver.js');
9
9
  const { deriveDocFacts, deriveDocObligations } = U.runtimeModule('doc-obligations.js');
10
10
 
11
11
  const MAX_FILES = 10000;
12
12
  const MAX_BYTES = 64 * 1024 * 1024;
13
13
  const POSIX_MODES = process.platform !== 'win32';
14
- function fail(code) { throw new Error(code); }
14
+ const PROJECT_CONFIG_PATH = '.agents/harness-everything/project-docs.json';
15
+ const SUPPORTED_ARTIFACT_ROOTS = new Set(['node_modules', '.husky/_']);
16
+ const POLICY_VERSION = 'dependency-artifact-scope/1';
17
+ function fail(code, reasonCode) { throw Object.assign(new Error(code), reasonCode ? { reasonCode } : {}); }
15
18
  // The JS realpath keeps Windows 8.3 spellings (RUNNER~1) while Git reports the long name, so
16
19
  // directory identity uses native resolution and is case-insensitive on Windows (as workflow-isolation).
17
20
  function canonicalPath(value) { return fs.realpathSync.native(path.resolve(value)); }
@@ -64,7 +67,249 @@ function captureBaseline(root, pin) {
64
67
  validatePaths(records);
65
68
  return { commit, treeRef, records };
66
69
  }
67
- function captureCandidate(root, baseline) {
70
+
71
+ function pathUnderRoot(relative, root) {
72
+ const candidate = String(relative).replace(/\\/g, '/').normalize('NFC').toLowerCase();
73
+ const expected = root.normalize('NFC').toLowerCase();
74
+ return candidate === expected || candidate.startsWith(`${expected}/`);
75
+ }
76
+ function rootMention(value, root) {
77
+ const normalized = String(value).replace(/\\/g, '/').normalize('NFC').toLowerCase();
78
+ const needle = root.normalize('NFC').toLowerCase();
79
+ let offset = 0;
80
+ while ((offset = normalized.indexOf(needle, offset)) >= 0) {
81
+ const before = offset === 0 || !/[a-z0-9_.-]/.test(normalized[offset - 1]);
82
+ const after = offset + needle.length;
83
+ const boundary = after === normalized.length || /[/*?\s"'`:#),;\]]/.test(normalized[after]);
84
+ if (before && boundary) return true;
85
+ offset++;
86
+ }
87
+ return false;
88
+ }
89
+ function globSegmentMatches(rawPattern, rawSegment) {
90
+ // Same NFC + lowercase folding as rootMention()/pathUnderRoot(), so a wildcard spelling cannot dodge it.
91
+ const pattern = rawPattern.normalize('NFC').toLowerCase();
92
+ const segment = rawSegment.normalize('NFC').toLowerCase();
93
+ if (/[\[\]{}]/.test(pattern)) return true;
94
+ const escaped = pattern.split(/([*?])/).map(part => part === '*' ? '[\\s\\S]*' : part === '?' ? '[\\s\\S]'
95
+ : part.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')).join('');
96
+ return new RegExp(`^${escaped}$`).test(segment);
97
+ }
98
+ function relativeSelectorMayIntersectRoot(selector, root) {
99
+ const normalized = selector.replace(/\\/g, '/');
100
+ if (/[\[\]{}]/.test(normalized) || normalized.split('/').includes('..')) return true;
101
+ // `.` and empty segments are redundant anywhere in a workspace-relative selector.
102
+ const pattern = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
103
+ const target = root.split('/');
104
+ let states = new Set([0]);
105
+ const closure = values => {
106
+ const expanded = new Set(values);
107
+ let changed = true;
108
+ while (changed) {
109
+ changed = false;
110
+ for (const index of expanded) {
111
+ if (pattern[index] === '**' && !expanded.has(index + 1)) {
112
+ expanded.add(index + 1);
113
+ changed = true;
114
+ }
115
+ }
116
+ }
117
+ return expanded;
118
+ };
119
+ states = closure(states);
120
+ for (const segment of target) {
121
+ const next = new Set();
122
+ for (const index of states) {
123
+ if (index >= pattern.length) continue;
124
+ if (pattern[index] === '**') next.add(index);
125
+ else if (globSegmentMatches(pattern[index], segment)) next.add(index + 1);
126
+ }
127
+ states = closure(next);
128
+ if (!states.size) return false;
129
+ }
130
+ return states.size > 0;
131
+ }
132
+ function relativePathWithin(workspace, candidate, pathApi) {
133
+ const relative = pathApi.relative(workspace, candidate);
134
+ if (!relative) return '';
135
+ const normalized = relative.replace(/\\/g, '/');
136
+ // Case and filesystem aliases that cannot be resolved exactly must fail closed.
137
+ const workspaceParts = workspace.replace(/\\/g, '/').split('/').filter(Boolean);
138
+ const candidateParts = candidate.replace(/\\/g, '/').split('/').filter(Boolean);
139
+ const samePrefix = workspaceParts.length <= candidateParts.length
140
+ && workspaceParts.every((part, index) => part.normalize('NFC').toLowerCase()
141
+ === candidateParts[index].normalize('NFC').toLowerCase());
142
+ if (samePrefix) return candidateParts.slice(workspaceParts.length).join('/');
143
+ if (pathApi.isAbsolute(relative) || normalized === '..' || normalized.startsWith('../')) return null;
144
+ return normalized;
145
+ }
146
+ function realpathWithMissingSuffix(candidate, pathApi) {
147
+ let current = candidate;
148
+ const suffix = [];
149
+ while (true) {
150
+ try { return pathApi.resolve(fs.realpathSync.native(current), ...suffix); }
151
+ catch (error) {
152
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code)) return null;
153
+ const parent = pathApi.dirname(current);
154
+ if (parent === current) return null;
155
+ suffix.unshift(pathApi.basename(current));
156
+ current = parent;
157
+ }
158
+ }
159
+ }
160
+ function absoluteSelectorRelatives(selector, workspaceRoot) {
161
+ const windowsDrive = /^[A-Za-z]:/.test(selector);
162
+ if ((windowsDrive || selector.startsWith('\\') || selector.startsWith('//')) && process.platform !== 'win32') return true;
163
+ const pathApi = process.platform === 'win32' ? path.win32 : path;
164
+ if (windowsDrive && !pathApi.isAbsolute(selector)) return true;
165
+ if (!pathApi.isAbsolute(selector)) return null;
166
+
167
+ const workspace = canonicalPath(workspaceRoot);
168
+ const absolute = pathApi.resolve(selector);
169
+ const lexicalRelative = relativePathWithin(workspace, absolute, pathApi);
170
+ const resolved = realpathWithMissingSuffix(absolute, pathApi);
171
+ if (!resolved) return true;
172
+ const realRelative = relativePathWithin(workspace, resolved, pathApi);
173
+ if (lexicalRelative !== null && realRelative !== null) return [lexicalRelative, realRelative];
174
+ if (lexicalRelative !== null || realRelative !== null) return true;
175
+ // A glob outside the workspace can still expand into it (via a parent such as /tmp/*/node_modules/**
176
+ // or a symlinked entry under an unrelated directory); only literal outside paths are provably disjoint.
177
+ if (/[*?\[\]{}]/.test(selector)) return true;
178
+ return false;
179
+ }
180
+ function selectorMayIntersectRoot(selector, root, workspaceRoot) {
181
+ if (typeof selector !== 'string' || !selector.trim()) return true;
182
+ const absolute = absoluteSelectorRelatives(selector, workspaceRoot);
183
+ if (absolute === true) return true;
184
+ if (absolute === false) return false;
185
+ if (absolute !== null) return absolute.some(relative => relativeSelectorMayIntersectRoot(relative || '.', root));
186
+ return relativeSelectorMayIntersectRoot(selector, root);
187
+ }
188
+ function valueMayIntersectRoots(value, roots, workspaceRoot) {
189
+ if (typeof value === 'string') {
190
+ const absolute = absoluteSelectorRelatives(value, workspaceRoot);
191
+ if (absolute === true) return true;
192
+ if (absolute === false) return false;
193
+ if (absolute !== null) return roots.some(root => absolute.some(relative => {
194
+ const selector = relative || '.';
195
+ return rootMention(selector, root)
196
+ || ((selector.includes('/') || /[*?\[\]{}]/.test(selector)) && selectorMayIntersectRoot(selector, root, workspaceRoot));
197
+ }));
198
+ return roots.some(root => rootMention(value, root)
199
+ || ((!value.trim() || value.includes('/') || /[*?\[\]{}]/.test(value))
200
+ && selectorMayIntersectRoot(value, root, workspaceRoot)));
201
+ }
202
+ if (Array.isArray(value)) return value.some(item => valueMayIntersectRoots(item, roots, workspaceRoot));
203
+ if (U.record(value)) return Object.entries(value).some(([key, item]) => valueMayIntersectRoots(key, roots, workspaceRoot)
204
+ || valueMayIntersectRoots(item, roots, workspaceRoot));
205
+ return false;
206
+ }
207
+ function withoutTaskPinIdentity(value) {
208
+ if (Array.isArray(value)) return value.map(withoutTaskPinIdentity);
209
+ if (!U.record(value)) return value;
210
+ return Object.fromEntries(Object.entries(value)
211
+ .filter(([key]) => !/ids?$/i.test(key))
212
+ .map(([key, item]) => [key, withoutTaskPinIdentity(item)]));
213
+ }
214
+ function projectConfigFromBytes(bytes) {
215
+ try {
216
+ const config = JSON.parse(Buffer.from(bytes).toString('utf8'));
217
+ return { config, valid: validateDocsConfig(config).valid };
218
+ } catch { return { config: null, valid: false }; }
219
+ }
220
+ function baselineProjectConfig(baseline) {
221
+ const source = baseline.records.find(item => item.path === PROJECT_CONFIG_PATH && item.presence === 'present');
222
+ return source ? { source, ...projectConfigFromBytes(source.storedBytes) } : { source: null, config: null, valid: false };
223
+ }
224
+ function capturedProjectConfig(config) {
225
+ const source = config.capture.sources.find(item => item.path === PROJECT_CONFIG_PATH);
226
+ return source && source.presence === 'present'
227
+ ? { source, ...projectConfigFromBytes(source.bytes) }
228
+ : { source: source || null, config: null, valid: false };
229
+ }
230
+ function dependencyRoots(config) {
231
+ return U.record(config) && U.record(config.inventory)
232
+ && Object.prototype.hasOwnProperty.call(config.inventory, 'dependencyArtifactRoots')
233
+ ? config.inventory.dependencyArtifactRoots : undefined;
234
+ }
235
+ function sameRootField(left, right) {
236
+ if (left === undefined || right === undefined) return left === right;
237
+ return U.stable([...left].sort()) === U.stable([...right].sort());
238
+ }
239
+ function validateExcludedRoots(workspaceRoot, roots) {
240
+ const workspace = canonicalPath(workspaceRoot);
241
+ const states = [];
242
+ for (const root of roots) {
243
+ let parent = workspace;
244
+ let presence = 'present-directory';
245
+ for (const component of root.split('/')) {
246
+ let entries;
247
+ try { entries = fs.readdirSync(parent, { withFileTypes: true }); }
248
+ catch { fail('inventory-incomplete', 'dependency-root-parent-unreadable'); }
249
+ const aliases = entries.filter(entry => entry.name.normalize('NFC').toLowerCase() === component.normalize('NFC').toLowerCase());
250
+ if (aliases.length > 1 || aliases.some(entry => entry.name !== component)) fail('inventory-unsupported', 'dependency-root-alias');
251
+ const current = path.join(parent, component);
252
+ let stat;
253
+ try { stat = fs.lstatSync(current); }
254
+ catch (error) {
255
+ if (error.code === 'ENOENT') { presence = 'absent'; break; }
256
+ fail('inventory-incomplete', 'dependency-root-unreadable');
257
+ }
258
+ if (stat.isSymbolicLink() || !stat.isDirectory()) fail('inventory-unsupported', 'dependency-root-not-plain-directory');
259
+ let real;
260
+ try { real = fs.realpathSync.native(current); }
261
+ catch { fail('inventory-incomplete', 'dependency-root-unreadable'); }
262
+ const within = process.platform === 'win32'
263
+ ? real.toLowerCase().startsWith(`${workspace.toLowerCase()}${path.sep}`)
264
+ : real.startsWith(`${workspace}${path.sep}`);
265
+ if (!within) fail('inventory-unsupported', 'dependency-root-outside-workspace');
266
+ parent = current;
267
+ }
268
+ states.push({ path: root, presence });
269
+ }
270
+ return states;
271
+ }
272
+ function guardedPolicy(root, baseline, capturedConfig, input) {
273
+ const base = baselineProjectConfig(baseline);
274
+ const current = capturedProjectConfig(capturedConfig);
275
+ const baselineRoots = base.valid ? dependencyRoots(base.config) : undefined;
276
+ const candidateRoots = current.valid ? dependencyRoots(current.config) : undefined;
277
+ if (current.valid && candidateRoots !== undefined && (!base.valid || !sameRootField(baselineRoots, candidateRoots))) {
278
+ fail('inventory-unsupported', 'dependency-artifact-policy-changed-or-unpinned');
279
+ }
280
+ if (!base.valid || baselineRoots === undefined || baselineRoots.length === 0) return null;
281
+ if (!current.valid || !sameRootField(baselineRoots, candidateRoots)) fail('inventory-unsupported', 'dependency-artifact-policy-changed-or-unpinned');
282
+ const roots = [...baselineRoots].sort();
283
+ const indexed = git(root, ['ls-files', '-z']).toString('utf8').split('\0').filter(Boolean);
284
+ if (baseline.records.some(item => roots.some(value => pathUnderRoot(item.path, value)))) fail('inventory-unsupported', 'baseline-path-overlap');
285
+ if (indexed.some(item => roots.some(value => pathUnderRoot(item, value)))) fail('inventory-unsupported', 'index-path-overlap');
286
+ if (capturedConfig.capture.sources.some(item => roots.some(value => pathUnderRoot(item.path, value)))) {
287
+ fail('inventory-unsupported', 'config-source-overlap');
288
+ }
289
+ const configs = [base.config, capturedConfig.resolution.config];
290
+ if (configs.some(config => {
291
+ if (!U.record(config)) return false;
292
+ const selectors = Object.fromEntries(Object.entries(config).filter(([key]) => key !== 'inventory'));
293
+ return valueMayIntersectRoots(selectors, roots, root);
294
+ })) fail('inventory-unsupported', 'config-selector-overlap');
295
+ if (valueMayIntersectRoots(withoutTaskPinIdentity(input.task), roots, root) || valueMayIntersectRoots(input.scope, roots, root)) {
296
+ fail('inventory-unsupported', 'task-input-overlap');
297
+ }
298
+ return {
299
+ roots,
300
+ baselineCommit: baseline.commit,
301
+ storedBytesDigest: U.sha(base.source.storedBytes),
302
+ guardOutcomes: [
303
+ { guard: 'baseline-paths', state: 'clear', reason: 'No baseline path intersects an approved root.' },
304
+ { guard: 'index-paths', state: 'clear', reason: 'No current index path intersects an approved root.' },
305
+ { guard: 'config-sources', state: 'clear', reason: 'No baseline or current config source intersects an approved root.' },
306
+ { guard: 'config-selectors', state: 'clear', reason: 'Baseline and current config declarations are disjoint from approved roots.' },
307
+ { guard: 'task-inputs', state: 'clear', reason: 'No explicit task or requirement input intersects an approved root.' }
308
+ ]
309
+ };
310
+ }
311
+ function captureCandidate(root, baseline, excludedRoots = []) {
312
+ const excludedRootStates = excludedRoots.length ? validateExcludedRoots(root, excludedRoots) : [];
68
313
  const raw = [];
69
314
  let total = 0;
70
315
  let visited = 0;
@@ -72,8 +317,9 @@ function captureCandidate(root, baseline) {
72
317
  const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => U.compare(a.name, b.name));
73
318
  for (const entry of entries) {
74
319
  if (!prefix && entry.name === '.git') continue;
75
- if (++visited > MAX_FILES) fail('inventory-unsupported');
76
320
  const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
321
+ if (excludedRoots.includes(relative)) continue;
322
+ if (++visited > MAX_FILES) fail('inventory-unsupported');
77
323
  const absolute = path.join(dir, entry.name);
78
324
  const stat = fs.lstatSync(absolute);
79
325
  if (stat.isDirectory()) {
@@ -102,7 +348,8 @@ function captureCandidate(root, baseline) {
102
348
  }
103
349
  }
104
350
  // 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]));
351
+ const stored = new Map(convertWorkspaceFiles(root, raw.map(item => ({ path: item.path, bytes: item.raw })))
352
+ .map(item => [item.path, item.bytes]));
106
353
  const records = raw.map(item => {
107
354
  const converted = stored.get(item.path);
108
355
  if (!converted) fail('inventory-unsupported');
@@ -111,7 +358,7 @@ function captureCandidate(root, baseline) {
111
358
  return fileRecord(item.path, item.mode, converted);
112
359
  });
113
360
  for (const item of baseline.records) if (!stored.has(item.path)) records.push(fileRecord(item.path, '', Buffer.alloc(0), 'deleted'));
114
- return records;
361
+ return { records, excludedRootStates };
115
362
  }
116
363
  const progressKeys = new Set(['status', 'state', 'evidence', 'loaded', 'loadedAt', 'updatedAt', 'createdAt', 'completedAt', 'timestamp', 'report', 'reports']);
117
364
  // Progress fields are excluded only at their defined record locations (top-level task/workflow, each
@@ -169,8 +416,20 @@ function produceDocSnapshot(input, dependencies = {}) {
169
416
  const baseline = captureBaseline(root, detached.baseline);
170
417
  const config = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
171
418
  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 };
419
+ const policy = guardedPolicy(root, baseline, config, detached);
420
+ const firstCapture = captureCandidate(root, baseline, policy?.roots || []);
421
+ const coverage = policy ? {
422
+ kind: 'project-with-dependency-artifact-exclusions',
423
+ policyVersion: POLICY_VERSION,
424
+ policySource: { path: PROJECT_CONFIG_PATH, baselineCommit: policy.baselineCommit, storedBytesDigest: policy.storedBytesDigest },
425
+ dependencyArtifactRoots: policy.roots,
426
+ excluded: ['.git', ...policy.roots],
427
+ excludedRootStates: firstCapture.excludedRootStates,
428
+ guardOutcomes: [...policy.guardOutcomes,
429
+ { guard: 'excluded-root-types', state: 'clear', reason: 'Each approved root is an in-workspace directory or absent.', roots: firstCapture.excludedRootStates }],
430
+ scope: U.clone(detached.scope)
431
+ } : { kind: 'whole-project', excluded: ['.git'], scope: U.clone(detached.scope) };
432
+ const candidate = { records: firstCapture.records, coverage };
174
433
  // Same owned config bytes must be the bytes covered by the working-tree capture.
175
434
  for (const source of config.capture.sources) {
176
435
  const file = candidate.records.find(r => r.path === source.path && r.presence === 'present');
@@ -189,18 +448,21 @@ function produceDocSnapshot(input, dependencies = {}) {
189
448
  const facts = derived.facts;
190
449
  const obligations = deriveDocObligations(facts);
191
450
  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);
451
+ const again = captureCandidate(root, baseline, policy?.roots || []);
193
452
  const configAgain = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
453
+ const policyAgain = guardedPolicy(root, baseline, configAgain, detached);
194
454
  let finalContext;
195
455
  try { finalContext = contextFor(detached, originProvider); }
196
456
  catch { fail('snapshot-changed'); }
197
- if (U.snapshotDigest('candidate', baseline, coverage, again) !== candidate.digest || U.configDigest(configAgain) !== config.digest
457
+ if (U.snapshotDigest('candidate', baseline, coverage, again.records) !== candidate.digest
458
+ || U.stable(again.excludedRootStates) !== U.stable(firstCapture.excludedRootStates)
459
+ || U.stable(policyAgain) !== U.stable(policy) || U.configDigest(configAgain) !== config.digest
198
460
  || U.stable(originContext) !== U.stable(finalContext)) fail('snapshot-changed');
199
461
  return { state: 'ready', snapshot: { identity, baseline, candidate, config, originContext, taskSemantics: planIdentity, facts, obligations, derivationReasons }, diagnostics: [] };
200
462
  } catch (error) {
201
463
  const codes = ['invalid-input', 'baseline-unavailable', 'inventory-incomplete', 'inventory-unsupported', 'snapshot-changed', 'config-unavailable', 'unsupported-prd-root', 'identity-mismatch', 'origin-unavailable', 'origin-untrusted'];
202
464
  const code = codes.includes(error.message) ? error.message : 'inventory-unsupported';
203
- return { state: 'unavailable', snapshot: null, diagnostics: [U.diagnostic(code)] };
465
+ return { state: 'unavailable', snapshot: null, diagnostics: [U.diagnostic(code, error.reasonCode ? { reasonCode: error.reasonCode } : {})] };
204
466
  }
205
467
  }
206
468
  module.exports = { produceDocSnapshot };
@@ -77,7 +77,7 @@ const OUTLINE_INVENTORY_PIN = Object.freeze({
77
77
  inputSha: '0ef0541c6cae550e8269ba712e03797e0e987dfb',
78
78
  approvalRef: { issue: 325, commentId: '6436207729', decision: 1 },
79
79
  rowDomainMappings: Object.freeze([Object.freeze({ rowIds: Object.freeze(['15', '16', '17', '18']),
80
- sourceDomain: 'VERI', domain: 'EVID', approvalRef: '../e1-feasibility-2026-10-28/owner-decision-veri-evid.md' })]),
80
+ sourceDomain: 'VERI', domain: 'EVID', approvalRef: 'https://github.com/dyphn1/Harness-everything/issues/325#issuecomment-6706805112' })]),
81
81
  retainedArtifactPath: `inventory/${OUTLINE_INVENTORY_SHA256}.md`,
82
82
  });
83
83
  const OUTLINE_MARKER = 'HARNESS_OUTLINE_DISPATCH_V1';
@@ -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.47.4",
4
+ "version": "0.48.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.40.0
7
+ version: 0.47.5
8
8
  ---
9
9
 
10
10
  # Code Review
@@ -19,6 +19,15 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
19
19
  - [ ] Follow each important diagnostic/fact through every consumer and ancestor. Check whether required facts are computed before derived authority, indexes, or output.
20
20
  - [ ] Compare new rules with previously accepted rules and controls. Separate display severity from safety prerequisites.
21
21
 
22
+ ## Check evidence a gate trusts
23
+
24
+ Apply these when the change grades, gates or labels evidence. Authors SHOULD run them as a self-review before push.
25
+
26
+ - [ ] **Claimed vs observed.** Separate what the subject reports about itself (a tool's `completed` status, a result marker, an agent-supplied expected value, a status label) from what the checker observes independently (exit code, output, a recomputed result, the quoted source). A self-report alone never passes; when nothing independent is observable, the result is inconclusive.
27
+ - [ ] **Select, then judge.** When the last, first or any item of a sequence decides (last run, final message, latest attempt), select from every attempt in order before filtering by outcome. Test both orders: failure then success, and success then failure or no result.
28
+ - [ ] **One witness per rule.** Give each detection rule a negative case that triggers only that rule; a case that also trips an exit code or another pattern cannot show the rule works. Probe positive cases at structural boundaries (inside a token, string or quote), not only at safe separators.
29
+ - [ ] **Comparable subjects, honest labels.** A comparison or conflict needs both sides to describe the same target. A status name claims only what was checked: a consistent quote is not a validated diagnosis.
30
+
22
31
  ## Check tests and output
23
32
 
24
33
  - [ ] Ask whether each test still passes if the behavior is disabled, bypassed, or broken (mutation thought experiment). Confirm the test is included in CI.
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.47.3
7
+ version: 0.48.0
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -1701,6 +1701,15 @@ function parseInventoryDoc(relativePath, bytes) {
1701
1701
  };
1702
1702
  }
1703
1703
 
1704
+ function convertWorkspaceFiles(root, inventory) {
1705
+ const files = (Array.isArray(inventory) ? inventory : []).map(item => ({
1706
+ relative: item.path,
1707
+ bytes: Buffer.from(item.bytes)
1708
+ }));
1709
+ const rules = readWorkingTreeRules(root, files.filter(item => item.bytes.includes(0x0d)).map(item => item.relative));
1710
+ return files.map(item => ({ path: item.relative, bytes: workingStoredBytes(root, item.relative, item.bytes, rules) }));
1711
+ }
1712
+
1704
1713
  function scanWorkspaceFiles(root, scope = null) {
1705
1714
  if (!root) return [];
1706
1715
  const workspace = path.resolve(root);
@@ -1728,8 +1737,7 @@ function scanWorkspaceFiles(root, scope = null) {
1728
1737
  }
1729
1738
  }
1730
1739
  walk(workspace, '');
1731
- const rules = readWorkingTreeRules(workspace, files.filter(item => item.bytes.includes(0x0d)).map(item => item.relative));
1732
- return files.map(item => ({ path: item.relative, bytes: workingStoredBytes(workspace, item.relative, item.bytes, rules) }));
1740
+ return convertWorkspaceFiles(workspace, files.map(item => ({ path: item.relative, bytes: item.bytes })));
1733
1741
  }
1734
1742
 
1735
1743
  function scanWorkspaceInventory(root) {
@@ -2785,6 +2793,6 @@ function main(argv = process.argv.slice(2)) {
2785
2793
  return analysis.diagnostics.some(item => item.severity === 'error') ? 1 : 0;
2786
2794
  }
2787
2795
 
2788
- module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, buildIndexSnapshot, configSourceInventory, diffRevisions, buildGitDocumentInventory };
2796
+ module.exports = { readGitBaseInventory, scanWorkspaceFiles, convertWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, buildIndexSnapshot, configSourceInventory, diffRevisions, buildGitDocumentInventory };
2789
2797
 
2790
2798
  if (require.main === module) process.exitCode = main();
@@ -21,6 +21,7 @@ const PROJECT_DOCS_CONFIG = path.join('.agents', 'harness-everything', 'project-
21
21
  const PROJECT_DOC_ROLES = Object.freeze(['prd', 'adr', 'ticket', 'reference', 'log', 'report']);
22
22
  const PROJECT_DOC_CONFIG_KEYS = new Set([
23
23
  'governed', 'domains', 'legacy', 'derived', 'tickets',
24
+ 'inventory',
24
25
  'docLocation', 'tracker', 'issueDefinition', 'contextMap', 'contextMapPath',
25
26
  'decisionLocation', 'decisions', 'adrLocation', 'adr',
26
27
  'domainLocation', 'contextLocation', 'contexts', 'domain',
@@ -327,6 +328,17 @@ function validateDocsConfig(config) {
327
328
  }
328
329
  }
329
330
 
331
+ if (Object.prototype.hasOwnProperty.call(config, 'inventory')) {
332
+ const inventory = config.inventory;
333
+ const roots = inventory && inventory.dependencyArtifactRoots;
334
+ const supportedRoots = new Set(['node_modules', '.husky/_']);
335
+ if (!isPlainObject(inventory) || Object.keys(inventory).some(key => key !== 'dependencyArtifactRoots')
336
+ || !Array.isArray(roots) || roots.some(root => typeof root !== 'string' || !supportedRoots.has(root))
337
+ || new Set(roots).size !== roots.length) {
338
+ invalid('invalid-dependency-artifact-roots', 'inventory.dependencyArtifactRoots must contain distinct exact roots from node_modules and .husky/_');
339
+ }
340
+ }
341
+
330
342
  if (Object.prototype.hasOwnProperty.call(config, 'tickets') &&
331
343
  config.tickets !== 'tracker' && !isRepoRelativePath(config.tickets)) {
332
344
  invalid('invalid-tickets', 'tickets must be "tracker" or a repo-relative path');
@@ -4,7 +4,7 @@ description: "Scaffold a verified multi-agent workspace and select bounded speci
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.39.0
7
+ version: 0.48.0
8
8
  ---
9
9
 
10
10
  # Multi-Agent Workspace
@@ -21,6 +21,7 @@ const PROJECT_DOCS_CONFIG = path.join('.agents', 'harness-everything', 'project-
21
21
  const PROJECT_DOC_ROLES = Object.freeze(['prd', 'adr', 'ticket', 'reference', 'log', 'report']);
22
22
  const PROJECT_DOC_CONFIG_KEYS = new Set([
23
23
  'governed', 'domains', 'legacy', 'derived', 'tickets',
24
+ 'inventory',
24
25
  'docLocation', 'tracker', 'issueDefinition', 'contextMap', 'contextMapPath',
25
26
  'decisionLocation', 'decisions', 'adrLocation', 'adr',
26
27
  'domainLocation', 'contextLocation', 'contexts', 'domain',
@@ -327,6 +328,17 @@ function validateDocsConfig(config) {
327
328
  }
328
329
  }
329
330
 
331
+ if (Object.prototype.hasOwnProperty.call(config, 'inventory')) {
332
+ const inventory = config.inventory;
333
+ const roots = inventory && inventory.dependencyArtifactRoots;
334
+ const supportedRoots = new Set(['node_modules', '.husky/_']);
335
+ if (!isPlainObject(inventory) || Object.keys(inventory).some(key => key !== 'dependencyArtifactRoots')
336
+ || !Array.isArray(roots) || roots.some(root => typeof root !== 'string' || !supportedRoots.has(root))
337
+ || new Set(roots).size !== roots.length) {
338
+ invalid('invalid-dependency-artifact-roots', 'inventory.dependencyArtifactRoots must contain distinct exact roots from node_modules and .husky/_');
339
+ }
340
+ }
341
+
330
342
  if (Object.prototype.hasOwnProperty.call(config, 'tickets') &&
331
343
  config.tickets !== 'tracker' && !isRepoRelativePath(config.tickets)) {
332
344
  invalid('invalid-tickets', 'tickets must be "tracker" or a repo-relative path');
@@ -4,7 +4,7 @@ description: "Drive Standard Tasks (Tier 2) with Test-Driven Development (RED/GR
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.13.0
7
+ version: 0.47.5
8
8
  ---
9
9
 
10
10
  # Test-Driven Development
@@ -8,8 +8,16 @@
8
8
  { "name": "weighted integration failure", "mutation": "the 0.15 positive dimension scores zero", "expectedScore": 85, "expectedResult": "FAIL" },
9
9
  { "name": "weighted unit failure", "mutation": "unit determinism is N/A and the 0.15 positive dimension scores zero", "expectedScore": 83.33, "expectedResult": "FAIL" },
10
10
  { "name": "NON_CONFORMANT", "mutation": "sourceConformance = NON_CONFORMANT", "expectedResult": "FAIL" },
11
- { "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT and sourceNotes is present", "expectedResult": "PASS" },
12
- { "name": "SOURCE_CONFLICT", "mutation": "sourceValidity = SOURCE_CONFLICT with a traced additional source", "expectedResult": "FAIL" },
11
+ { "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT, sourceNotes is present, and sourceEvidence quotes the defective assertion against the observed value; sourceEvidenceStatus is REVIEWER_REQUIRED, never VERIFIED, because a single-source mismatch does not prove the source is wrong", "expectedResult": "PASS" },
12
+ { "name": "SOURCE_DEFECT without sourceEvidence", "mutation": "sourceValidity = SOURCE_DEFECT with sourceNotes but no sourceEvidence", "expectedResult": "FAIL" },
13
+ { "name": "SOURCE_CONFLICT", "mutation": "sourceValidity = SOURCE_CONFLICT with a traced additional source and sourceEvidence quoting both sources", "expectedResult": "FAIL" },
14
+ { "name": "SOURCE_CONFLICT without sourceEvidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources but no sourceEvidence", "expectedResult": "FAIL" },
15
+ { "name": "SOURCE_CONFLICT node-versus-value", "mutation": "sourceValidity = SOURCE_CONFLICT citing only expect(list.head.value).toEqual(1) from one spec, observed value is an Element object", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
16
+ { "name": "SOURCE_CONFLICT agreeing expectations", "mutation": "sourceValidity = SOURCE_CONFLICT with two sources expecting the same value and a failing observed value; the mismatch is implementation behaviour", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
17
+ { "name": "SOURCE_CONFLICT two-source evidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting disagreeing expectations for the same subject list.head and a failing observed value; one quote (toBeInstanceOf) is not mechanically checkable, so sourceEvidenceStatus is REVIEWER_REQUIRED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
18
+ { "name": "SOURCE_CONFLICT relabelled expected", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting the identical assertion expect(list.head).toEqual(1) but expected values 1 and 3; each expected must match its quoted literal, so the second entry is rejected", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
19
+ { "name": "SOURCE_CONFLICT verified literal disagreement", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting expect(list.head.value).toBe(1) and expect(list.head.value).toBe('1') with matching expected values 1 and '1' and a failing observed value; sourceEvidenceStatus is VERIFIED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
20
+ { "name": "SOURCE_CONFLICT unrelated assertions", "mutation": "sourceValidity = SOURCE_CONFLICT quoting expect(foo).toEqual(1) from one source and expect(bar).toEqual(3) from another; unequal expectations about different subjects", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
13
21
  { "name": "malformed input", "mutation": "requirements is not an array", "expectedResult": "FAIL" },
14
22
  { "name": "unexpected input", "mutation": "one unexpectedInput check is FAIL", "expectedResult": "FAIL" },
15
23
  { "name": "error handling", "mutation": "one errorHandling check is SKIPPED", "expectedResult": "FAIL" },
@@ -2,6 +2,8 @@
2
2
 
3
3
  This model turns each source requirement into auditable evidence. The authoritative source remains the contract even when it appears wrong. Record a suspected documentation problem as `SOURCE_DEFECT`; record disagreement between traced sources as `SOURCE_CONFLICT`. Never reinterpret either into a passing implementation. If no authoritative source exists, use `INFERRED` and state the inference basis.
4
4
 
5
+ Before declaring either outcome, `sourceEvidence` quotes the exact conflicting assertions, names each declared source by path, and compares each `expected` value with the `observed` value from the run. A `SOURCE_CONFLICT` needs at least two distinct sources whose expected values disagree about the same `subject` — the contract target each quoted assertion constrains, written as it appears in the quote (`list.head` is not `list.head.value`). Unequal expectations about unrelated targets are not a conflict. If the quoted expectations agree and the run still fails, the mismatch is in the implementation: record `NON_CONFORMANT`, not `SOURCE_CONFLICT`. Each `expected` must match a mechanically readable literal in the quoted assertion (`toEqual`, `toStrictEqual`, `toBe`, or `assert.*` with a JSON or single-quoted literal); otherwise the claim is marked `REVIEWER_REQUIRED` and the gate does not treat it as an established disagreement. `VERIFIED` applies only to a `SOURCE_CONFLICT` whose two quoted literals disagree. A `SOURCE_DEFECT` is always `REVIEWER_REQUIRED`, because a mismatch with the observed value does not prove the source is wrong; the implementation may be the one at fault.
6
+
5
7
  ## Profile contract
6
8
 
7
9
  Every requirement declares exactly one `testProfile`: `unit` or `integration`. The selected profile defines which case classes are applicable; the common scoring and source rules do not make every test use identical evidence.
@@ -149,6 +149,18 @@
149
149
  }
150
150
  ]
151
151
  },
152
+ "sourceEvidence": {
153
+ "type": "object",
154
+ "additionalProperties": false,
155
+ "required": ["source", "assertion", "expected", "observed"],
156
+ "properties": {
157
+ "source": { "type": "string", "minLength": 1 },
158
+ "subject": { "type": "string", "minLength": 1 },
159
+ "assertion": { "type": "string", "minLength": 1 },
160
+ "expected": {},
161
+ "observed": {}
162
+ }
163
+ },
152
164
  "requirement": {
153
165
  "type": "object",
154
166
  "additionalProperties": false,
@@ -161,6 +173,7 @@
161
173
  "sourceConformance": { "enum": ["PASS", "NON_CONFORMANT"] },
162
174
  "sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT"] },
163
175
  "sourceNotes": { "type": "string", "minLength": 1 },
176
+ "sourceEvidence": { "type": "array", "items": { "$ref": "#/$defs/sourceEvidence" } },
164
177
  "skippedTests": { "type": "array" },
165
178
  "flakyTests": { "type": "array" },
166
179
  "naDeclarations": { "type": "array" },
@@ -186,6 +199,14 @@
186
199
  }
187
200
  },
188
201
  "allOf": [
202
+ {
203
+ "if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "enum": ["SOURCE_DEFECT", "SOURCE_CONFLICT"] } } },
204
+ "then": { "required": ["sourceEvidence"], "properties": { "sourceEvidence": { "minItems": 1 } } }
205
+ },
206
+ {
207
+ "if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "const": "SOURCE_CONFLICT" } } },
208
+ "then": { "properties": { "sourceEvidence": { "items": { "required": ["subject"] } } } }
209
+ },
189
210
  {
190
211
  "if": {
191
212
  "required": ["testProfile"],
@@ -96,6 +96,8 @@
96
96
  "sourceConformance": { "enum": ["PASS", "NON_CONFORMANT", null] },
97
97
  "sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT", null] },
98
98
  "sourceNotes": { "type": "string" },
99
+ "sourceEvidence": { "type": "array" },
100
+ "sourceEvidenceStatus": { "enum": ["VERIFIED", "REVIEWER_REQUIRED"], "description": "VERIFIED only for a SOURCE_CONFLICT whose two quoted literals disagree; SOURCE_DEFECT is always REVIEWER_REQUIRED." },
99
101
  "dimensions": { "$ref": "#/$defs/dimensions" },
100
102
  "skippedTests": { "type": "array" },
101
103
  "flakyTests": { "type": "array" },