harness-everything 0.46.3 → 0.47.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 (32) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +23 -1
  4. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/README.md +33 -8
  5. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/inventory/2741bd4514d2cd68f57d93a2b3ce94cbe597ca62d30e26f0ca58e882df5b03c6.md +128 -0
  6. package/benchmarks/results/live-host/claude-341-outline-dispatch-2026-10-28/sanitize-outline-capture.js +105 -0
  7. package/ci/mechanism-e1-derived-regenerate.test.js +333 -0
  8. package/ci/mechanism-outline-sanitizer.test.js +79 -0
  9. package/ci/mechanism-prb-outline-review.test.js +278 -31
  10. package/docs/doc-workflow-reconciliation.md +12 -8
  11. package/docs/platform-capabilities.md +1 -1
  12. package/harness-everything/SKILL.md +1 -1
  13. package/harness-everything/scripts/doc-check.js +106 -8
  14. package/harness-everything/scripts/doc-lifecycle.js +49 -1
  15. package/hooks/scripts/lib/doc-reconciliation-events.js +22 -0
  16. package/hooks/scripts/lib/doc-reconciliation-outline-capture.sanitized.jsonl +24 -0
  17. package/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -1
  18. package/hooks/scripts/lib/doc-reconciliation-provider.js +168 -15
  19. package/hooks/scripts/lib/doc-reconciliation-qualification.json +20 -0
  20. package/opencode-plugin/plugin.json +1 -1
  21. package/package.json +1 -1
  22. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  23. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +22 -0
  24. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-outline-capture.sanitized.jsonl +24 -0
  25. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +3 -1
  26. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +168 -15
  27. package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-qualification.json +20 -0
  28. package/plugins/harness-everything/plugin.json +1 -1
  29. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  30. package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +106 -8
  31. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +49 -1
  32. package/sbom.cdx.json +7 -7
@@ -47,7 +47,7 @@ const QUALIFICATION_EXPORT_SCHEMA = 'harness.doc-reconciliation.qualification.v1
47
47
  const HEX64 = /^[0-9a-f]{64}$/;
48
48
  const PRE_CAPTURE_TOOLS = ['Bash', 'PowerShell', 'Task', 'Agent', 'Write', 'Edit', 'apply_patch'];
49
49
  const STDOUT_CAP_BYTES = 1024 * 1024;
50
- const DOC_CHECK_FLAGS = new Set(['--workspace', '--config', '--changed-path', '--base']);
50
+ const DOC_CHECK_FLAGS = new Set(['--workspace', '--config', '--changed-path', '--base', '--derived-view']);
51
51
  const DOC_CHECK_BOOLEAN_FLAGS = new Set(['--conflict-assessment']);
52
52
  const DOC_CHECK_SCHEMA = 'harness.doc-check.check.v1';
53
53
  const DOC_CHECK_CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
@@ -76,6 +76,8 @@ const OUTLINE_INVENTORY_PIN = Object.freeze({
76
76
  runId: 'e1-inv-1',
77
77
  inputSha: '0ef0541c6cae550e8269ba712e03797e0e987dfb',
78
78
  approvalRef: { issue: 325, commentId: '6436207729', decision: 1 },
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' })]),
79
81
  retainedArtifactPath: `inventory/${OUTLINE_INVENTORY_SHA256}.md`,
80
82
  });
81
83
  const OUTLINE_MARKER = 'HARNESS_OUTLINE_DISPATCH_V1';
@@ -99,7 +101,7 @@ function isUncPathPrefix(token, command, index) {
99
101
  return token === '' && command[index] === '\\' && command[index + 1] === '\\';
100
102
  }
101
103
  // Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
102
- const SHIPPED_DOC_CHECK_DIGEST = '1afd94d66b356c94206d2441529ec0b7fafa1bcb453b0f1e2cf6994b1e4858e5';
104
+ const SHIPPED_DOC_CHECK_DIGEST = '804a093b31afde07ea6ec40e39fc758b501966bfa2a15c53a5275ad3171b0755';
103
105
  // This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
104
106
  const DOC_CHECK_INSTALL_LAYOUTS = [
105
107
  ['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
@@ -405,6 +407,8 @@ function resolveDocCheckInvocation(command, cwd) {
405
407
  if (flag === '--changed-path') changedPaths.push(value);
406
408
  else {
407
409
  if (values.has(flag)) return rejected('duplicate-flag');
410
+ if (flag === '--derived-view' && (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value)
411
+ || value.split(/[\\/]/).includes('..') || /[*?\[\]{}]/.test(value))) return rejected('derived-view-invalid');
408
412
  values.set(flag, value);
409
413
  }
410
414
  }
@@ -425,7 +429,8 @@ function resolveDocCheckInvocation(command, cwd) {
425
429
  }
426
430
  }
427
431
  return { kind: 'accepted', reason: null, installed: registered.installed, digest, argv,
428
- conflictAssessment, base: values.get('--base') || null, changedPaths };
432
+ conflictAssessment, base: values.get('--base') || null, config: values.get('--config'),
433
+ derivedView: values.get('--derived-view') || null, changedPaths };
429
434
  }
430
435
 
431
436
  // Observation-time digests of the checked paths, read from the workspace when the check ran. Absent paths are omitted.
@@ -439,6 +444,29 @@ function observedDigestsFor(root, paths) {
439
444
  return digests;
440
445
  }
441
446
 
447
+ function observedConfigSourcesFor(root, sources) {
448
+ if (!Array.isArray(sources)) return [];
449
+ return sources.map(source => {
450
+ const relative = source && source.path;
451
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) {
452
+ return { path: String(relative || ''), presence: 'unsupported' };
453
+ }
454
+ const file = path.resolve(root, relative);
455
+ const within = path.relative(path.resolve(root), file);
456
+ if (!within || within === '..' || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) {
457
+ return { path: relative, presence: 'unsupported' };
458
+ }
459
+ try {
460
+ if (!fs.lstatSync(file).isFile()) return { path: relative, presence: 'unsupported' };
461
+ return { path: relative, presence: 'present', digest: crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') };
462
+ } catch (error) {
463
+ return error.code === 'ENOENT' || error.code === 'ENOTDIR'
464
+ ? { path: relative, presence: 'missing' }
465
+ : { path: relative, presence: 'unreadable' };
466
+ }
467
+ });
468
+ }
469
+
442
470
  function textOf(value) { return typeof value === 'string' && value.trim().length > 0; }
443
471
  function recordOf(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
444
472
  function compareKeys(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
@@ -695,7 +723,7 @@ function parseInventoryPinRows(bytes) {
695
723
 
696
724
  function validateOutlineInventoryPin({ bytes, pin, expectedPin = OUTLINE_INVENTORY_PIN } = {}) {
697
725
  if (!Buffer.isBuffer(bytes) || !recordOf(pin) || !recordOf(expectedPin)) return { state: 'unresolved', reason: 'inventory-unavailable' };
698
- const fields = ['source', 'sha256', 'runId', 'inputSha', 'approvalRef', 'retainedArtifactPath'];
726
+ const fields = ['source', 'sha256', 'runId', 'inputSha', 'approvalRef', 'rowDomainMappings', 'retainedArtifactPath'];
699
727
  const actualPin = Object.fromEntries(fields.map(field => [field, pin[field]]));
700
728
  const trustedPin = Object.fromEntries(fields.map(field => [field, expectedPin[field]]));
701
729
  if (captureJson(actualPin) !== captureJson(trustedPin)) return { state: 'unresolved', reason: 'inventory-pin-mismatch' };
@@ -716,10 +744,18 @@ function resolveInventoryRow(inventory, rowId) {
716
744
  if (!parsed || parsed.state !== 'parsed' || !textOf(rowId)) return { state: 'unresolved', reason: 'inventory-row-missing' };
717
745
  const matches = [...parsed.rows.entries()].filter(([id]) => id === rowId);
718
746
  const domains = parsed.domains.get(rowId) || [];
719
- if (matches.length !== 1 || matches[0][1] !== 1 || domains.length !== 1) {
747
+ if (matches.length !== 1 || matches[0][1] !== 1) {
720
748
  return { state: 'unresolved', reason: 'inventory-row-ambiguous' };
721
749
  }
722
- return { state: 'accepted', row: { rowId, domain: domains[0] } };
750
+ const mappings = (Array.isArray(inventory.pin?.rowDomainMappings) ? inventory.pin.rowDomainMappings : [])
751
+ .filter(item => recordOf(item) && Array.isArray(item.rowIds) && item.rowIds.includes(rowId));
752
+ if (mappings.length > 1) return { state: 'unresolved', reason: 'inventory-row-ambiguous' };
753
+ const mapping = mappings[0];
754
+ if (mapping && (!domains.includes(mapping.sourceDomain) || !textOf(mapping.domain) || !textOf(mapping.approvalRef))) {
755
+ return { state: 'unresolved', reason: 'inventory-row-mapping-invalid' };
756
+ }
757
+ if (!mapping && domains.length !== 1) return { state: 'unresolved', reason: 'inventory-row-ambiguous' };
758
+ return { state: 'accepted', row: { rowId, domain: mapping ? mapping.domain : domains[0] } };
723
759
  }
724
760
 
725
761
  function hasSubstantiveDecision(text) {
@@ -1165,11 +1201,20 @@ function validateOutlineCaptureQualification(input, expectedHost = HOST_ID) {
1165
1201
  const qualificationError = validateQualificationExport(expectedHost);
1166
1202
  if (qualificationError) return { open: false, reason: qualificationError };
1167
1203
  const exported = JSON.parse(fs.readFileSync(path.join(__dirname, EXPORT_FILE), 'utf8'));
1168
- const mapping = exported.outlineDispatchCapture;
1169
- const source = exported.sanitizedCapture;
1170
- const records = fs.readFileSync(path.join(__dirname, source.file), 'utf8').split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line));
1171
- if (!outlineCaptureMappingValid(mapping, records)) return { open: false, reason: 'outline-live-capture-unqualified' };
1172
- return { open: true, source: 'validated-export', captureFile: source.file };
1204
+ const wrapper = exported.outlineDispatchCapture;
1205
+ if (!recordOf(wrapper) || !textOf(wrapper.hostVersion) || !textOf(wrapper.run) || !textOf(wrapper.baseline)
1206
+ || !textOf(wrapper.file) || /[\\/]/.test(wrapper.file) || wrapper.file.includes('..')) {
1207
+ return { open: false, reason: 'outline-live-capture-unqualified' };
1208
+ }
1209
+ const bytes = fs.readFileSync(path.join(__dirname, wrapper.file));
1210
+ if (crypto.createHash('sha256').update(bytes).digest('hex') !== wrapper.sha256) {
1211
+ return { open: false, reason: 'outline-live-capture-unqualified' };
1212
+ }
1213
+ const records = bytes.toString('utf8').split(/\r?\n/).filter(line => line.trim()).map(line => JSON.parse(line));
1214
+ if (!records.every(recordOf) || !outlineCaptureMappingValid(wrapper.mapping, records)) {
1215
+ return { open: false, reason: 'outline-live-capture-unqualified' };
1216
+ }
1217
+ return { open: true, source: 'validated-export', captureFile: wrapper.file };
1173
1218
  } catch (error) {
1174
1219
  return { open: false, reason: `outline-live-capture-unqualified:${error.code || 'invalid'}` };
1175
1220
  }
@@ -1456,20 +1501,31 @@ function resolveOutlineEvidence({ safeInput, capture, captures, request, originR
1456
1501
  // are covered by the clean-O seam check and by unattributedWriters below.
1457
1502
  const journal = Array.isArray(captures) ? captures : [];
1458
1503
  const dispatchIndex = journal.indexOf(correlation.pre);
1459
- const stoppedBeforeDispatch = start => {
1504
+ const uniqueLifecycle = start => {
1505
+ if (!textOf(start.session_id) || !textOf(start.agent_id)) return null;
1460
1506
  const lifecycle = event => journal.filter(item => recordOf(item) && item.event === event
1461
1507
  && item.session_id === start.session_id && textOf(start.agent_id) && item.agent_id === start.agent_id);
1462
1508
  // Exactly one Start and one Stop per (session_id, agent_id); a duplicate either way stays ambiguous.
1463
1509
  const starts = lifecycle('SubagentStart');
1464
1510
  const stops = lifecycle('SubagentStop');
1465
- if (starts.length !== 1 || starts[0] !== start || stops.length !== 1) return false;
1511
+ if (starts.length !== 1 || starts[0] !== start || stops.length !== 1) return null;
1466
1512
  const startIndex = journal.indexOf(start);
1467
1513
  const stopIndex = journal.indexOf(stops[0]);
1468
- return dispatchIndex >= 0 && startIndex >= 0 && startIndex < stopIndex && stopIndex < dispatchIndex;
1514
+ if (startIndex < 0 || stopIndex <= startIndex) return null;
1515
+ return { startIndex, stopIndex };
1516
+ };
1517
+ const stoppedBeforeDispatch = start => {
1518
+ const lifecycle = uniqueLifecycle(start);
1519
+ return Boolean(lifecycle && dispatchIndex >= 0 && lifecycle.stopIndex < dispatchIndex);
1520
+ };
1521
+ const startedAfterWorkerStop = start => {
1522
+ const lifecycle = uniqueLifecycle(start);
1523
+ const workerStopIndex = journal.indexOf(correlation.stop);
1524
+ return Boolean(lifecycle && workerStopIndex >= 0 && lifecycle.startIndex > workerStopIndex);
1469
1525
  };
1470
1526
  const competingStarts = journal.filter(item => recordOf(item)
1471
1527
  && item.event === 'SubagentStart' && item.ref !== worker.ref && sameCanonicalPath(item.cwd, workerRoot)
1472
- && !stoppedBeforeDispatch(item));
1528
+ && !stoppedBeforeDispatch(item) && !startedAfterWorkerStop(item));
1473
1529
  if (competingStarts.length) return fail('outline-competing-writer-start');
1474
1530
  const unattributedWriters = (Array.isArray(captures) ? captures : []).filter(item => possibleWriterCapture(item)
1475
1531
  && (item.agent_id !== worker.agent_id || !sameCanonicalPath(item.cwd, workerRoot)));
@@ -1725,6 +1781,92 @@ const createClaudeOriginProvider = input => {
1725
1781
  if (!paths.length) return rejected('scope-empty', `${capability} checked no governed paths`);
1726
1782
  let corpusBinding;
1727
1783
  let baselineArtifactDigests;
1784
+ let derivedBinding;
1785
+ if (capability === 'derived-regenerate') {
1786
+ if (!registered.derivedView || paths.length !== 1 || paths[0] !== registered.derivedView) {
1787
+ return rejected('capability-mismatch', 'derived-regenerate requires the exact registered --derived-view path');
1788
+ }
1789
+ const binding = projection.binding;
1790
+ const corpus = binding && binding.sourceCorpus;
1791
+ const config = binding && binding.config;
1792
+ const generator = binding && binding.generator;
1793
+ const generation = binding && binding.generation;
1794
+ const comparison = binding && binding.comparison;
1795
+ if (!recordOf(binding) || binding.view !== registered.derivedView || !recordOf(corpus) || corpus.complete !== true
1796
+ || !Array.isArray(corpus.paths) || !corpus.paths.length || !recordOf(corpus.digests)
1797
+ || JSON.stringify([...new Set(corpus.paths)].sort(compareKeys)) !== JSON.stringify(corpus.paths)
1798
+ || JSON.stringify(Object.keys(corpus.digests).sort(compareKeys)) !== JSON.stringify(corpus.paths)
1799
+ || corpus.paths.some(relative => !textOf(relative) || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')
1800
+ || !HEX64.test(String(corpus.digests[relative] || '')))
1801
+ || !recordOf(config) || !textOf(config.path) || path.isAbsolute(config.path)
1802
+ || config.path.split(/[\\/]/).includes('..') || !HEX64.test(String(config.digest || ''))
1803
+ || !Array.isArray(config.sources) || !recordOf(generator) || !textOf(generator.path)
1804
+ || !HEX64.test(String(generator.digest || '')) || !recordOf(generation) || generation.state !== 'pass'
1805
+ || !HEX64.test(String(generation.outputDigest || '')) || !recordOf(comparison) || comparison.state !== 'equal'
1806
+ || comparison.targetDigest !== generation.outputDigest) {
1807
+ return rejected('derived-binding-invalid', 'derived-regenerate stdout lacks complete generation, comparison, and input binding');
1808
+ }
1809
+ const configPath = path.resolve(cap.cwd, registered.config);
1810
+ const configRelative = path.relative(path.resolve(cap.cwd), configPath).split(path.sep).join('/');
1811
+ const observedDerived = recordOf(cap.observedDerivedDigests) ? cap.observedDerivedDigests : {};
1812
+ if (configRelative !== config.path || observedDerived[config.path] !== config.digest
1813
+ || observedDerived[registered.derivedView] !== comparison.targetDigest) {
1814
+ return rejected('derived-input-changed-after-check', 'the selected config or derived target changed before host capture');
1815
+ }
1816
+ const expectedObservedPaths = [...new Set([...corpus.paths, config.path, registered.derivedView])].sort(compareKeys);
1817
+ if (JSON.stringify(Object.keys(observedDerived).sort(compareKeys)) !== JSON.stringify(expectedObservedPaths)
1818
+ || corpus.paths.some(relative => observedDerived[relative] !== corpus.digests[relative])) {
1819
+ return rejected('derived-input-changed-after-check', 'a source path is missing or changed before host capture');
1820
+ }
1821
+ const configSources = config.sources;
1822
+ if (JSON.stringify(configSources.map(item => item && item.path)) !== JSON.stringify([...new Set(configSources.map(item => item && item.path))].sort(compareKeys))
1823
+ || configSources.some(item => !recordOf(item) || !textOf(item.path) || path.isAbsolute(item.path)
1824
+ || item.path.split(/[\\/]/).includes('..') || !['present', 'missing'].includes(item.presence)
1825
+ || (item.presence === 'present' ? !HEX64.test(String(item.digest || '')) : item.digest !== undefined))) {
1826
+ return rejected('derived-config-binding-invalid', 'derived-regenerate config source inventory is malformed');
1827
+ }
1828
+ const observedConfigSources = cap.observedDerivedConfigSources;
1829
+ if (!Array.isArray(observedConfigSources) || JSON.stringify(observedConfigSources) !== JSON.stringify(configSources)) {
1830
+ return rejected('config-changed-after-check', 'a config source changed between check and host capture');
1831
+ }
1832
+ const configSource = configSources.find(item => item.path === config.path);
1833
+ if (!configSource || configSource.presence !== 'present' || configSource.digest !== config.digest) {
1834
+ return rejected('derived-config-binding-invalid', 'the selected config is absent from the observed config inventory');
1835
+ }
1836
+ let configData;
1837
+ try { configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
1838
+ catch { return rejected('derived-config-binding-invalid', 'the selected config cannot be read during assessment'); }
1839
+ const selectedViews = Array.isArray(configData.derived) ? configData.derived.filter(item => item && item.path === registered.derivedView) : [];
1840
+ const expectedGenerator = `node harness-everything/scripts/doc-lifecycle.js index --workspace . --config ${registered.config} --all`;
1841
+ if (selectedViews.length !== 1 || selectedViews[0].generator !== expectedGenerator) {
1842
+ return rejected('derived-generator-unsupported', 'the selected config does not declare the supported lifecycle index operation');
1843
+ }
1844
+ let generatorPath;
1845
+ try { generatorPath = fs.realpathSync(path.join(path.dirname(registered.installed), 'doc-lifecycle.js')); }
1846
+ catch { return rejected('derived-generator-unavailable', 'the installed lifecycle index generator is missing'); }
1847
+ if (generator.path !== generatorPath || fileDigest(generatorPath) !== generator.digest) {
1848
+ return rejected('derived-generator-mismatch', 'the lifecycle generator differs from the implementation observed in stdout');
1849
+ }
1850
+ let currentConfigSources;
1851
+ try {
1852
+ const lifecycle = require(generatorPath);
1853
+ currentConfigSources = lifecycle.configSourceInventory(cap.cwd, configPath);
1854
+ } catch { return rejected('derived-config-binding-invalid', 'the lifecycle config inventory is unavailable'); }
1855
+ if (!currentConfigSources || currentConfigSources.complete !== true
1856
+ || JSON.stringify(currentConfigSources.sources) !== JSON.stringify(configSources)) {
1857
+ return rejected('config-changed-after-check', 'the lifecycle config source inventory changed after the check');
1858
+ }
1859
+ if (!recordOf(request.identity) || !HEX64.test(String(request.identity.baselineDigest || ''))
1860
+ || !HEX64.test(String(request.identity.candidateDigest || '')) || !HEX64.test(String(request.identity.configDigest || ''))) {
1861
+ return rejected('identity-missing', 'derived-regenerate needs the complete snapshot identity');
1862
+ }
1863
+ derivedBinding = {
1864
+ ...binding,
1865
+ baselineDigest: request.identity.baselineDigest,
1866
+ candidateDigest: request.identity.candidateDigest,
1867
+ configIdentityDigest: request.identity.configDigest,
1868
+ };
1869
+ }
1728
1870
  if (capability === 'conflict-check') {
1729
1871
  const assessment = report.conflictAssessment;
1730
1872
  const corpus = assessment && assessment.corpus;
@@ -1797,6 +1939,7 @@ const createClaudeOriginProvider = input => {
1797
1939
  artifactDigests,
1798
1940
  ...(Object.keys(baselineArtifacts).length ? { baselineArtifacts } : {}),
1799
1941
  ...(corpusBinding ? { corpusBinding } : {}),
1942
+ ...(derivedBinding ? { derivedBinding } : {}),
1800
1943
  captured: { responseRef: cap.responseRef, responseDigest: cap.responseDigest },
1801
1944
  };
1802
1945
  return { status: 'accepted', originRef, event, correlation: { toolCorrelationId: cap.tool_use_id, resultRef: cap.responseRef } };
@@ -2131,6 +2274,16 @@ function buildCaptureRecord(payload, eventName, workspaceRoot) {
2131
2274
  const report = JSON.parse(stdout);
2132
2275
  checkedPaths = ['doc-lint', 'link-check'].flatMap(capability => (recordOf(report.capabilities) && recordOf(report.capabilities[capability])
2133
2276
  && Array.isArray(report.capabilities[capability].checkedPaths) ? report.capabilities[capability].checkedPaths : []));
2277
+ const derived = recordOf(report.capabilities) && report.capabilities['derived-regenerate'];
2278
+ const derivedBinding = recordOf(derived) && recordOf(derived.binding) ? derived.binding : null;
2279
+ if (installedCheck.derivedView && typeof workspaceRoot === 'string' && workspaceRoot && derivedBinding && derivedBinding.view === installedCheck.derivedView
2280
+ && recordOf(derivedBinding.sourceCorpus) && Array.isArray(derivedBinding.sourceCorpus.paths)
2281
+ && recordOf(derivedBinding.config) && Array.isArray(derivedBinding.config.sources)) {
2282
+ checkedPaths.push(derivedBinding.view);
2283
+ record.observedDerivedDigests = observedDigestsFor(workspaceRoot,
2284
+ [...derivedBinding.sourceCorpus.paths, derivedBinding.config.path, derivedBinding.view]);
2285
+ record.observedDerivedConfigSources = observedConfigSourcesFor(workspaceRoot, derivedBinding.config.sources);
2286
+ }
2134
2287
  const conflict = recordOf(report.capabilities) && report.capabilities['conflict-check'];
2135
2288
  if (installedCheck.conflictAssessment === true && report.mode === 'conflict-assessment'
2136
2289
  && recordOf(conflict) && conflict.state === 'pass' && recordOf(conflict.corpus)) {
@@ -30,5 +30,25 @@
30
30
  "sanitizedCapture": {
31
31
  "file": "doc-reconciliation-capture.sanitized.jsonl",
32
32
  "sha256": "db4d8c85d2ba0fd06f53f455f31b125dd194e9a22cb337687bc0e295a16f02e5"
33
+ },
34
+ "outlineDispatchCapture": {
35
+ "mapping": {
36
+ "version": "claude-outline-dispatch-mapping/2",
37
+ "promptField": "tool_input.prompt",
38
+ "dispatchKey": "session_id+tool_use_id",
39
+ "runInBackgroundField": "tool_input.run_in_background",
40
+ "isolationPresenceField": "tool_input.isolation",
41
+ "resultAgentIdField": "tool_response.agentId",
42
+ "resultStatusField": "tool_response.status",
43
+ "workerStartEvent": "SubagentStart",
44
+ "workerStopEvent": "SubagentStop",
45
+ "workerIdentity": "session_id+agent_id",
46
+ "workerRootField": "cwd"
47
+ },
48
+ "file": "doc-reconciliation-outline-capture.sanitized.jsonl",
49
+ "sha256": "6feb440af12aaf878a0bed5cb750f29e27d271a9add93001e151746980230b7f",
50
+ "hostVersion": "2.1.312",
51
+ "run": "run8",
52
+ "baseline": "1bd0f805031c5577a6e7f72a9bc5b41618b71fc1"
33
53
  }
34
54
  }
@@ -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.46.3",
4
+ "version": "0.47.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: "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.46.0
7
+ version: 0.47.0
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -8,13 +8,14 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const crypto = require('crypto');
10
10
  const { execFileSync, spawnSync } = require('child_process');
11
- const { collectDocs } = require('./doc-lifecycle');
11
+ const { isDeepStrictEqual } = require('node:util');
12
+ const { collectDocs, buildIndexSnapshot } = require('./doc-lifecycle');
12
13
 
13
14
  // Logical command IDs (A3 section 3): stdout that repeats an ID is not trusted as credit by itself.
14
15
  const CHECK_ID = 'harness.doc-check.check.v1';
15
16
  const COMPARE_ID = 'harness.doc-check.compare-derived.v1';
16
- const USAGE = 'usage: doc-check.js check --workspace <dir> --config <json-file, relative to --workspace> --changed-path <path> [--changed-path <path> ...] [--base <git-rev>] [--conflict-assessment]';
17
- const VALUE_FLAGS = ['--workspace', '--config', '--changed-path', '--base'];
17
+ const USAGE = 'usage: doc-check.js check --workspace <dir> --config <json-file, relative to --workspace> --changed-path <path> [--changed-path <path> ...] [--base <git-rev>] [--derived-view <path>] [--conflict-assessment]';
18
+ const VALUE_FLAGS = ['--workspace', '--config', '--changed-path', '--base', '--derived-view'];
18
19
  const CONFLICT_SCHEMA = 'harness.doc-check.conflict-assessment.v1';
19
20
  const EXTERNAL = /^(?:https?:|mailto:)/i;
20
21
  const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
@@ -91,10 +92,11 @@ function parseArgs(argv) {
91
92
  if (!VALUE_FLAGS.includes(flag) || rest[index + 1] === undefined) throw new Error(`${USAGE}\nunknown or incomplete argument: ${flag}`);
92
93
  const value = rest[++index];
93
94
  if (value.startsWith('--')) throw new Error(`${USAGE}\n${flag} requires a value, not a flag: ${value}`);
94
- if (flag === '--changed-path') options.changedPaths.push(value);
95
+ if (flag === '--changed-path') options.changedPaths.push(value);
95
96
  else {
96
- if (options[flag.slice(2)] !== undefined) throw new Error(`${USAGE}\nrepeated argument: ${flag}`);
97
- options[flag.slice(2)] = value;
97
+ const key = flag === '--derived-view' ? 'derivedView' : flag.slice(2);
98
+ if (options[key] !== undefined) throw new Error(`${USAGE}\nrepeated argument: ${flag}`);
99
+ options[key] = value;
98
100
  }
99
101
  }
100
102
  if (!options.workspace || !options.config) throw new Error(`${USAGE}\n--workspace and --config are required`);
@@ -310,7 +312,7 @@ function compareDerived(argv) {
310
312
  }
311
313
 
312
314
  const GLOB_CHARS = /[*?[\]{}]/;
313
- const VIEW_REASON = 'derived view not verified: run its authorized generator, then compare-derived';
315
+ const VIEW_REASON = 'derived view not verified: request check --derived-view to regenerate and compare it';
314
316
 
315
317
  // Near-miss comparison only (separators, leading ./, trailing /, case); never used to accept a view.
316
318
  function normalizeView(value) {
@@ -366,6 +368,97 @@ function derivedCoverage(config, { workspace, configRelative, configChanged, aff
366
368
  return entries;
367
369
  }
368
370
 
371
+ function digest(bytes) { return crypto.createHash('sha256').update(bytes).digest('hex'); }
372
+
373
+ function derivedRegenerate(workspace, options, config, configBytes) {
374
+ const view = options.derivedView;
375
+ const unavailable = reason => ({ state: 'unavailable', checkedPaths: [view], reason });
376
+ if (typeof view !== 'string' || !view || path.isAbsolute(view) || view.includes('\\')
377
+ || view.split('/').some(part => !part || part === '.' || part === '..') || GLOB_CHARS.test(view)) {
378
+ return { state: 'unsupported', checkedPaths: [], reason: 'view must be a literal workspace-relative path' };
379
+ }
380
+ const configRelative = workspaceRelative(workspace, options.config);
381
+ if (!configRelative || path.isAbsolute(options.config) || options.config.includes('\\')
382
+ || !/^[A-Za-z0-9._/-]+$/.test(options.config)
383
+ || options.config.split('/').some(part => !part || part === '.' || part === '..')) {
384
+ return { state: 'unsupported', checkedPaths: [view], reason: 'config must be a canonical workspace-relative path' };
385
+ }
386
+ const selected = (Array.isArray(config.derived) ? config.derived : []).filter(item => item && item.path === view);
387
+ if (selected.length !== 1) return { state: 'unsupported', checkedPaths: [view], reason: 'view must name exactly one declared derived path' };
388
+ const governedPatterns = [
389
+ ...(Array.isArray(config.governed) ? config.governed.map(item => item && item.path) : []),
390
+ ...(Array.isArray(config.legacy) ? config.legacy : [])
391
+ ].filter(item => typeof item === 'string');
392
+ if (governedPatterns.some(pattern => globToRegExp(pattern).test(view))) {
393
+ return { state: 'unsupported', checkedPaths: [view], reason: 'a derived view cannot be governed by its own index' };
394
+ }
395
+ const expectedGenerator = `node harness-everything/scripts/doc-lifecycle.js index --workspace . --config ${options.config} --all`;
396
+ if (selected[0].generator !== expectedGenerator) {
397
+ return { state: 'unsupported', checkedPaths: [view], reason: 'configured generator is not the supported lifecycle index operation' };
398
+ }
399
+
400
+ const target = path.resolve(workspace, view);
401
+ const generatorPath = path.resolve(__dirname, 'doc-lifecycle.js');
402
+ const rootReal = fs.realpathSync(workspace);
403
+ const targetBytes = () => {
404
+ const stat = fs.lstatSync(target);
405
+ if (!stat.isFile()) throw new Error('view is not a regular file');
406
+ const real = fs.realpathSync(target);
407
+ const relative = path.relative(rootReal, real);
408
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
409
+ throw new Error('view resolves outside the workspace');
410
+ }
411
+ return readRegularFile(target);
412
+ };
413
+
414
+ try {
415
+ const configPath = path.resolve(workspace, options.config);
416
+ const configReal = fs.realpathSync(configPath);
417
+ const configPathRelative = path.relative(rootReal, configReal);
418
+ if (!configPathRelative || configPathRelative === '..' || configPathRelative.startsWith(`..${path.sep}`)
419
+ || !fs.lstatSync(configPath).isFile()) return { state: 'unsupported', checkedPaths: [view], reason: 'config is not a workspace-contained regular file' };
420
+ const beforeTarget = targetBytes();
421
+ const generatorDigest = digest(fs.readFileSync(generatorPath, 'utf8').replace(/\r\n/g, '\n'));
422
+ const first = buildIndexSnapshot(workspace, configPath, config, configBytes);
423
+ const currentConfigBytes = fs.readFileSync(configPath);
424
+ const currentConfig = JSON.parse(currentConfigBytes.toString('utf8'));
425
+ const second = buildIndexSnapshot(workspace, configPath, currentConfig, currentConfigBytes);
426
+ const afterTarget = targetBytes();
427
+ const currentGeneratorDigest = digest(fs.readFileSync(generatorPath, 'utf8').replace(/\r\n/g, '\n'));
428
+ const outputDigest = digest(first.bytes);
429
+ const targetDigest = digest(beforeTarget);
430
+ const stable = digest(configBytes) === digest(currentConfigBytes)
431
+ && first.corpus.complete === true && second.corpus.complete === true
432
+ && first.configInventory.complete === true && second.configInventory.complete === true
433
+ && isDeepStrictEqual(first.corpus.paths, second.corpus.paths)
434
+ && isDeepStrictEqual(first.corpus.digests, second.corpus.digests)
435
+ && isDeepStrictEqual(first.configInventory.sources, second.configInventory.sources)
436
+ && isDeepStrictEqual(first.configSources, second.configSources)
437
+ && Buffer.compare(first.bytes, second.bytes) === 0
438
+ && generatorDigest === currentGeneratorDigest
439
+ && digest(afterTarget) === targetDigest;
440
+ const equal = Buffer.compare(first.bytes, beforeTarget) === 0;
441
+ const comparisonState = stable ? (equal ? 'equal' : 'stale') : 'unavailable';
442
+ const binding = {
443
+ view,
444
+ sourceCorpus: { complete: first.corpus.complete === true, paths: first.corpus.paths, digests: first.corpus.digests },
445
+ config: { path: configRelative, digest: digest(configBytes), sources: first.configInventory.sources },
446
+ generator: { path: fs.realpathSync(generatorPath), digest: generatorDigest },
447
+ generation: { state: 'pass', outputDigest },
448
+ comparison: { state: comparisonState, targetDigest },
449
+ };
450
+ return {
451
+ state: stable && equal ? 'pass' : stable ? 'stale' : 'unavailable',
452
+ checkedPaths: [view],
453
+ ...(stable ? {} : { reason: 'source, config, generator, or target changed during the check' }),
454
+ ...(!stable && !first.corpus.complete ? { reason: 'source corpus is incomplete' } : {}),
455
+ binding,
456
+ };
457
+ } catch (error) {
458
+ return unavailable(error.message || 'generation or comparison unavailable');
459
+ }
460
+ }
461
+
369
462
  function check(options) {
370
463
  const workspace = path.resolve(options.workspace);
371
464
  const configPath = path.resolve(workspace, options.config);
@@ -422,7 +515,7 @@ function check(options) {
422
515
  // SHOULD reminders only: they never change the exit code and are never diagnostics.
423
516
  const changed = new Set(options.changedPaths.map(value => path.relative(workspace, path.resolve(workspace, value)).split(path.sep).join('/')));
424
517
  // F3 applicability (conservative): a governed or legacy source, an unknown or deleted path, a derived output or the config
425
- // itself marks every declared view unverified. check still never runs a generator.
518
+ // itself marks every declared view unverified until the explicit derived-view mode is selected.
426
519
  const configRelative = path.relative(workspace, configPath).split(path.sep).join('/');
427
520
  const declaredViews = [...new Set((Array.isArray(config.derived) ? config.derived : []).map(entry => entry.path))];
428
521
  const affectsViews = [...changed].some(item => {
@@ -465,6 +558,11 @@ function check(options) {
465
558
  },
466
559
  }
467
560
  };
561
+ if (options.derivedView) {
562
+ const derived = derivedRegenerate(workspace, options, config, configBytes);
563
+ result.capabilities['derived-regenerate'] = derived;
564
+ if (derived.state === 'pass') result.coverage.unsupported = result.coverage.unsupported.filter(item => item.path !== options.derivedView || item.state !== 'unverified');
565
+ }
468
566
  if (options.conflictAssessment) {
469
567
  const scopePaths = [...new Set(options.changedPaths.map(value => workspaceRelative(workspace, value)).filter(Boolean))].sort();
470
568
  const corpus = analysis.corpus;
@@ -1839,6 +1839,24 @@ function readWorkspaceConfigSources(root, explicitConfigPath, fallbackConfig) {
1839
1839
  return sources.length ? sources : [fallbackConfig];
1840
1840
  }
1841
1841
 
1842
+ function configSourceInventory(root, explicitConfigPath) {
1843
+ const sources = [];
1844
+ let complete = true;
1845
+ for (const relativePath of [...configPathsForWorkspace(root, explicitConfigPath)].sort(compareText)) {
1846
+ const file = path.join(root, ...relativePath.split('/'));
1847
+ try {
1848
+ const stat = fs.lstatSync(file);
1849
+ if (!stat.isFile()) { sources.push({ path: relativePath, presence: 'unsupported' }); complete = false; continue; }
1850
+ const bytes = fs.readFileSync(file);
1851
+ sources.push({ path: relativePath, presence: 'present', digest: crypto.createHash('sha256').update(bytes).digest('hex') });
1852
+ } catch (error) {
1853
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') sources.push({ path: relativePath, presence: 'missing' });
1854
+ else { sources.push({ path: relativePath, presence: 'unreadable' }); complete = false; }
1855
+ }
1856
+ }
1857
+ return { complete, sources };
1858
+ }
1859
+
1842
1860
  function readGitBaseInventory(root, baseRef, config, explicitConfigPath = null) {
1843
1861
  if (!root || typeof baseRef !== 'string' || !baseRef.trim()) return { available: false, inventory: [], files: [] };
1844
1862
  try {
@@ -2602,6 +2620,27 @@ function buildIndex(docs, config, options = {}) {
2602
2620
  return `${lines.join('\n').replace(/\n+$/, '')}\n`;
2603
2621
  }
2604
2622
 
2623
+ function buildIndexSnapshot(workspace, configPath, config, configBytes, options = {}) {
2624
+ const root = path.resolve(workspace);
2625
+ const corpus = { complete: false, skipped: [], paths: [], digests: {} };
2626
+ const docs = collectDocs(root, config, corpus);
2627
+ const relativeConfig = path.relative(root, configPath);
2628
+ corpus.config = relativeConfig && relativeConfig !== '..' && !relativeConfig.startsWith(`..${path.sep}`)
2629
+ ? { path: relativeConfig.split(path.sep).join('/'), digest: crypto.createHash('sha256').update(configBytes).digest('hex') }
2630
+ : null;
2631
+ const configSources = readWorkspaceConfigSources(root, configPath, config);
2632
+ const configInventory = configSourceInventory(root, configPath);
2633
+ const mode = { all: true, workspace: root, base: options.base, configPath, configSources };
2634
+ const analysis = analyzeDocs(docs, config, mode);
2635
+ const bytes = Buffer.from(buildIndex(docs, config, {
2636
+ ...mode,
2637
+ diagnostics: analysis.diagnostics,
2638
+ _graph: analysis._graph,
2639
+ _retirementEligibleSources: analysis._retirementEligibleSources
2640
+ }), 'utf8');
2641
+ return { bytes, corpus, configSources, configInventory };
2642
+ }
2643
+
2605
2644
  function parseCli(argv) {
2606
2645
  const [command, ...rest] = argv;
2607
2646
  const options = { command, changedProvided: false, changedPaths: [], proposedUntracked: [], all: false };
@@ -2709,6 +2748,15 @@ function main(argv = process.argv.slice(2)) {
2709
2748
  process.stderr.write(`${error.message}\n`);
2710
2749
  return 2;
2711
2750
  }
2751
+ if (options.command === 'index' && options.all) {
2752
+ try {
2753
+ process.stdout.write(buildIndexSnapshot(workspacePath, configPath, config, configBytes, options).bytes);
2754
+ return 0;
2755
+ } catch (error) {
2756
+ process.stderr.write(`index generation failed: ${error.message}\n`);
2757
+ return 2;
2758
+ }
2759
+ }
2712
2760
  const corpus = options.all ? { complete: false, skipped: [], paths: [], digests: {} } : null;
2713
2761
  const docs = collectDocs(workspacePath, config, corpus);
2714
2762
  if (corpus) {
@@ -2737,6 +2785,6 @@ function main(argv = process.argv.slice(2)) {
2737
2785
  return analysis.diagnostics.some(item => item.severity === 'error') ? 1 : 0;
2738
2786
  }
2739
2787
 
2740
- module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, diffRevisions, buildGitDocumentInventory };
2788
+ module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, buildIndexSnapshot, configSourceInventory, diffRevisions, buildGitDocumentInventory };
2741
2789
 
2742
2790
  if (require.main === module) process.exitCode = main();
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:194f592d-6226-519c-a5b8-9f6750fb3c19",
5
+ "serialNumber": "urn:uuid:76fc9d39-b249-59ad-bcf8-de01347662b8",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-28T05:22:07.321Z",
8
+ "timestamp": "2026-10-29T00:00:39.955Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.46.3"
13
+ "version": "0.47.0"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.46.3",
18
+ "bom-ref": "pkg:npm/harness-everything@0.47.0",
19
19
  "name": "harness-everything",
20
- "version": "0.46.3",
21
- "purl": "pkg:npm/harness-everything@0.46.3"
20
+ "version": "0.47.0",
21
+ "purl": "pkg:npm/harness-everything@0.47.0"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -2033,7 +2033,7 @@
2033
2033
  ]
2034
2034
  },
2035
2035
  {
2036
- "ref": "pkg:npm/harness-everything@0.46.3",
2036
+ "ref": "pkg:npm/harness-everything@0.47.0",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",