harness-everything 0.42.1 → 0.43.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/.agents/harness-everything/document-inventory.json +167 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +73 -0
- package/CHANGELOG.md +17 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +5 -4
- package/ci/mechanism-2ax-doc-lifecycle.test.js +816 -0
- package/ci/mechanism-325-3b-s2b-wiring.test.js +11 -0
- package/ci/mechanism-325-f1-doc-check.test.js +178 -0
- package/docs/own-repo-governance-e1-plan-2026-10-25-2139.md +253 -0
- package/docs/platform-capabilities.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/sbom.cdx.json +7 -7
- package/scripts/doc-governance-guard.js +335 -0
|
@@ -1382,6 +1382,278 @@ function gitOutput(root, args, encoding = 'buffer') {
|
|
|
1382
1382
|
return execFileSync('git', ['-C', root, ...args], { encoding, maxBuffer: 64 * 1024 * 1024 });
|
|
1383
1383
|
}
|
|
1384
1384
|
|
|
1385
|
+
function nulFields(bytes) {
|
|
1386
|
+
const fields = [];
|
|
1387
|
+
let start = 0;
|
|
1388
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
1389
|
+
if (bytes[index] !== 0) continue;
|
|
1390
|
+
fields.push(bytes.subarray(start, index));
|
|
1391
|
+
start = index + 1;
|
|
1392
|
+
}
|
|
1393
|
+
if (start < bytes.length) fields.push(bytes.subarray(start));
|
|
1394
|
+
return fields;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function gitPathValue(bytes) {
|
|
1398
|
+
const decoded = bytes.toString('utf8');
|
|
1399
|
+
if (Buffer.from(decoded, 'utf8').equals(bytes)) return { path: decoded, matchPath: decoded, encoding: 'utf-8' };
|
|
1400
|
+
let escaped = '';
|
|
1401
|
+
for (const byte of bytes) escaped += byte >= 0x20 && byte <= 0x7e && byte !== 0x5c
|
|
1402
|
+
? String.fromCharCode(byte)
|
|
1403
|
+
: `\\x${byte.toString(16).padStart(2, '0')}`;
|
|
1404
|
+
return { path: escaped, matchPath: escaped, encoding: 'escaped-bytes' };
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function inventoryPathIsDocument(pathBytes, manifest) {
|
|
1408
|
+
return matchesAny(gitPathValue(pathBytes).matchPath, manifest.documentGlobs);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function inventoryPathDisposition(pathBytes, manifest, mode) {
|
|
1412
|
+
const display = gitPathValue(pathBytes);
|
|
1413
|
+
if (!inventoryPathIsDocument(pathBytes, manifest)) {
|
|
1414
|
+
if (mode === '160000') return { role: 'gitlink', lifecycle: 'external', disposition: 'excluded', reason: 'Gitlink identity is retained; inventory does not recurse into submodules.' };
|
|
1415
|
+
if (mode === '120000') return { role: 'symlink', lifecycle: 'current', disposition: 'excluded', reason: 'Symlink identity is retained; inventory does not follow its target.' };
|
|
1416
|
+
return { role: 'non-document endpoint', lifecycle: 'unknown', disposition: 'excluded', reason: 'Non-document endpoint is retained only to preserve Git rename identity.' };
|
|
1417
|
+
}
|
|
1418
|
+
const rule = manifest.rules.find(item => matchesAny(display.matchPath, [item.match]));
|
|
1419
|
+
if (rule) return { role: rule.role, lifecycle: rule.lifecycle, disposition: rule.disposition, reason: rule.reason };
|
|
1420
|
+
return { role: 'unknown', lifecycle: 'unknown', disposition: 'unknown', reason: 'No manifest rule classifies this tracked document path.' };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function assertNoSymlinkPathAncestors(workspace, relative, { allowLeafSymlink = false, allowMissing = false, label = 'path' } = {}) {
|
|
1424
|
+
let current = workspace;
|
|
1425
|
+
const components = relative.split(path.sep);
|
|
1426
|
+
for (let index = 0; index < components.length; index++) {
|
|
1427
|
+
current = path.join(current, components[index]);
|
|
1428
|
+
let entry;
|
|
1429
|
+
try { entry = fs.lstatSync(current); } catch (error) {
|
|
1430
|
+
if (allowMissing && error.code === 'ENOENT') return;
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
const leaf = index === components.length - 1;
|
|
1434
|
+
if (entry.isSymbolicLink()) {
|
|
1435
|
+
if (allowLeafSymlink && leaf) return;
|
|
1436
|
+
throw new Error(`${label} cannot pass through a symbolic link`);
|
|
1437
|
+
}
|
|
1438
|
+
if (!leaf && !entry.isDirectory()) throw new Error(`${label} parent path components must be directories`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function loadInventoryManifest(root, manifestPath) {
|
|
1443
|
+
const workspace = fs.realpathSync(root);
|
|
1444
|
+
const absolute = path.resolve(workspace, manifestPath);
|
|
1445
|
+
const relative = path.relative(workspace, absolute);
|
|
1446
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1447
|
+
throw new Error('--manifest must name a file within --workspace');
|
|
1448
|
+
}
|
|
1449
|
+
assertNoSymlinkPathAncestors(workspace, relative, { label: '--manifest path' });
|
|
1450
|
+
const stat = fs.lstatSync(absolute);
|
|
1451
|
+
if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error('--manifest must be a regular file no larger than 1 MiB');
|
|
1452
|
+
const bytes = fs.readFileSync(absolute);
|
|
1453
|
+
let manifest;
|
|
1454
|
+
try { manifest = JSON.parse(bytes.toString('utf8')); } catch (error) { throw new Error(`cannot parse inventory manifest: ${error.message}`); }
|
|
1455
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest) || manifest.schemaVersion !== 1
|
|
1456
|
+
|| !Array.isArray(manifest.documentGlobs) || !manifest.documentGlobs.length
|
|
1457
|
+
|| !manifest.documentGlobs.every(item => typeof item === 'string' && item.length)
|
|
1458
|
+
|| !Array.isArray(manifest.rules)) {
|
|
1459
|
+
throw new Error('inventory manifest requires schemaVersion 1, documentGlobs, and rules');
|
|
1460
|
+
}
|
|
1461
|
+
for (const rule of manifest.rules) {
|
|
1462
|
+
if (!rule || typeof rule !== 'object' || typeof rule.match !== 'string' || !rule.match
|
|
1463
|
+
|| typeof rule.role !== 'string' || !rule.role
|
|
1464
|
+
|| typeof rule.lifecycle !== 'string' || !rule.lifecycle
|
|
1465
|
+
|| !['applicable', 'inventory-only', 'excluded', 'unknown', 'unavailable'].includes(rule.disposition)
|
|
1466
|
+
|| typeof rule.reason !== 'string' || !rule.reason) {
|
|
1467
|
+
throw new Error('each inventory rule requires match, role, lifecycle, disposition, and reason');
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return { manifest, absolute, sha256: crypto.createHash('sha256').update(bytes).digest('hex') };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function proposedInventoryPath(workspace, value) {
|
|
1474
|
+
if (typeof value !== 'string' || !value || path.isAbsolute(value)) throw new Error('--proposed-untracked must name a relative path within --workspace');
|
|
1475
|
+
const absolute = path.resolve(workspace, value);
|
|
1476
|
+
const relative = path.relative(workspace, absolute);
|
|
1477
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1478
|
+
throw new Error('--proposed-untracked must name a path within --workspace');
|
|
1479
|
+
}
|
|
1480
|
+
assertNoSymlinkPathAncestors(workspace, relative, { allowLeafSymlink: true, allowMissing: true, label: '--proposed-untracked path' });
|
|
1481
|
+
return Buffer.from(relative.split(path.sep).join('/'), 'utf8');
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function gitTreeEntries(root, commit) {
|
|
1485
|
+
const fields = nulFields(gitOutput(root, ['ls-tree', '-r', '-z', '--full-tree', commit]));
|
|
1486
|
+
const result = new Map();
|
|
1487
|
+
for (const field of fields) {
|
|
1488
|
+
const tab = field.indexOf(0x09);
|
|
1489
|
+
if (tab < 0) throw new Error('malformed NUL-delimited Git tree entry');
|
|
1490
|
+
const header = field.subarray(0, tab).toString('ascii').split(' ');
|
|
1491
|
+
if (header.length !== 3 || !/^\d{6}$/.test(header[0]) || !/^(blob|commit)$/.test(header[1]) || !/^[0-9a-f]{40,64}$/.test(header[2])) {
|
|
1492
|
+
throw new Error('malformed Git tree identity');
|
|
1493
|
+
}
|
|
1494
|
+
const relative = Buffer.from(field.subarray(tab + 1));
|
|
1495
|
+
result.set(relative.toString('base64'), { pathBytes: relative, mode: header[0], type: header[1], oid: header[2] });
|
|
1496
|
+
}
|
|
1497
|
+
return result;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function gitDiffEntries(root, commit) {
|
|
1501
|
+
const fields = nulFields(gitOutput(root, ['diff', '--raw', '--no-abbrev', '-z', '--find-renames', commit, '--']));
|
|
1502
|
+
const result = new Map();
|
|
1503
|
+
for (let index = 0; index < fields.length;) {
|
|
1504
|
+
const header = fields[index++].toString('ascii');
|
|
1505
|
+
const match = /^:(\d{6}) (\d{6}) ([0-9a-f]{40,64}) ([0-9a-f]{40,64}) ([A-Z][0-9]*)$/.exec(header);
|
|
1506
|
+
if (!match || index >= fields.length) throw new Error('malformed NUL-delimited Git diff entry');
|
|
1507
|
+
const [, oldMode, newMode, oldOid, newOid, statusText] = match;
|
|
1508
|
+
const status = statusText[0];
|
|
1509
|
+
const first = Buffer.from(fields[index++]);
|
|
1510
|
+
if (status === 'R' || status === 'C') {
|
|
1511
|
+
if (index >= fields.length) throw new Error('truncated NUL-delimited rename entry');
|
|
1512
|
+
const second = Buffer.from(fields[index++]);
|
|
1513
|
+
const shared = { oldMode, newMode, oldOid, newOid, statusText };
|
|
1514
|
+
result.set(first.toString('base64'), { ...shared, pathBytes: first, status: 'renamed-from', partner: second, endpoint: 'source' });
|
|
1515
|
+
result.set(second.toString('base64'), { ...shared, pathBytes: second, status: 'renamed-to', partner: first, endpoint: 'target' });
|
|
1516
|
+
} else {
|
|
1517
|
+
const isDelete = status === 'D';
|
|
1518
|
+
result.set(first.toString('base64'), {
|
|
1519
|
+
oldMode, newMode, oldOid, newOid, statusText, pathBytes: first,
|
|
1520
|
+
status: status === 'A' ? 'added' : isDelete ? 'deleted' : status === 'T' ? 'type-changed' : status === 'U' ? 'unmerged' : 'modified',
|
|
1521
|
+
endpoint: isDelete ? 'source' : 'target'
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function gitBlobOid(root, bytes) {
|
|
1529
|
+
// shortcut: Hash raw worktree bytes without Git filters; use a trusted path-aware oracle if canonical filtered OIDs are required.
|
|
1530
|
+
return execFileSync('git', ['-C', root, 'hash-object', '--stdin'], { encoding: 'utf8', input: bytes, maxBuffer: 1024 * 1024 }).trim();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function gitFilesystemPath(root, pathBytes) {
|
|
1534
|
+
const display = gitPathValue(pathBytes);
|
|
1535
|
+
if (display.encoding === 'utf-8') return path.join(path.resolve(root), ...display.path.split('/'));
|
|
1536
|
+
if (process.platform === 'win32') throw new Error('non-UTF-8 Git path bytes are not openable on Windows');
|
|
1537
|
+
return Buffer.concat([Buffer.from(`${path.resolve(root)}${path.sep}`), pathBytes]);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function worktreeGitIdentity(root, pathBytes) {
|
|
1541
|
+
const absolute = gitFilesystemPath(root, pathBytes);
|
|
1542
|
+
const stat = fs.lstatSync(absolute);
|
|
1543
|
+
let mode;
|
|
1544
|
+
let bytes;
|
|
1545
|
+
if (stat.isSymbolicLink()) {
|
|
1546
|
+
mode = '120000';
|
|
1547
|
+
bytes = fs.readlinkSync(absolute, { encoding: 'buffer' });
|
|
1548
|
+
} else if (stat.isFile()) {
|
|
1549
|
+
mode = stat.mode & 0o111 ? '100755' : '100644';
|
|
1550
|
+
bytes = fs.readFileSync(absolute);
|
|
1551
|
+
} else {
|
|
1552
|
+
return { mode: null, oid: null, type: null, reason: 'candidate path is not a regular file or symlink; no target was opened' };
|
|
1553
|
+
}
|
|
1554
|
+
return { mode, oid: gitBlobOid(root, bytes), type: 'blob', oidBasis: 'raw worktree bytes; no Git filters applied' };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function buildGitDocumentInventory(root, baseRef, manifestPath, proposedPaths = []) {
|
|
1558
|
+
const workspace = fs.realpathSync(root);
|
|
1559
|
+
const loaded = loadInventoryManifest(workspace, manifestPath);
|
|
1560
|
+
const commit = gitOutput(workspace, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${baseRef}^{commit}`], 'utf8').trim();
|
|
1561
|
+
const tree = gitOutput(workspace, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${commit}^{tree}`], 'utf8').trim();
|
|
1562
|
+
const baseEntries = gitTreeEntries(workspace, commit);
|
|
1563
|
+
const changes = gitDiffEntries(workspace, commit);
|
|
1564
|
+
const proposedByKey = new Map(proposedPaths.map(value => {
|
|
1565
|
+
const bytes = proposedInventoryPath(workspace, value);
|
|
1566
|
+
return [bytes.toString('base64'), bytes];
|
|
1567
|
+
}));
|
|
1568
|
+
const relevantKeys = new Set();
|
|
1569
|
+
for (const [key, item] of baseEntries) {
|
|
1570
|
+
if (inventoryPathIsDocument(item.pathBytes, loaded.manifest) || item.mode === '120000' || item.mode === '160000') relevantKeys.add(key);
|
|
1571
|
+
}
|
|
1572
|
+
for (const [key, change] of changes) {
|
|
1573
|
+
const item = baseEntries.get(key);
|
|
1574
|
+
if (inventoryPathIsDocument(change.pathBytes, loaded.manifest)
|
|
1575
|
+
|| change.partner && inventoryPathIsDocument(change.partner, loaded.manifest)
|
|
1576
|
+
|| item && inventoryPathIsDocument(item.pathBytes, loaded.manifest)
|
|
1577
|
+
|| item && (item.mode === '120000' || item.mode === '160000')
|
|
1578
|
+
|| change.oldMode === '120000' || change.oldMode === '160000' || change.newMode === '120000' || change.newMode === '160000') {
|
|
1579
|
+
relevantKeys.add(key);
|
|
1580
|
+
if (change.partner) relevantKeys.add(change.partner.toString('base64'));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
for (const key of proposedByKey.keys()) relevantKeys.add(key);
|
|
1584
|
+
const records = [];
|
|
1585
|
+
for (const key of relevantKeys) {
|
|
1586
|
+
const base = baseEntries.get(key) || null;
|
|
1587
|
+
const change = changes.get(key) || null;
|
|
1588
|
+
const recordPathBytes = base ? base.pathBytes : change ? change.pathBytes : proposedByKey.get(key);
|
|
1589
|
+
if (!recordPathBytes) continue;
|
|
1590
|
+
let current = null;
|
|
1591
|
+
let status = change ? change.status : base ? 'unchanged' : 'added';
|
|
1592
|
+
let currentReason = null;
|
|
1593
|
+
if (change && change.status !== 'deleted' && change.status !== 'renamed-from') {
|
|
1594
|
+
if (change.newMode === '160000') {
|
|
1595
|
+
current = /^[0-9a-f]{40,64}$/.test(change.newOid) && !/^0+$/.test(change.newOid)
|
|
1596
|
+
? { mode: change.newMode, oid: change.newOid, type: 'commit' }
|
|
1597
|
+
: null;
|
|
1598
|
+
if (!current) currentReason = 'gitlink candidate identity unavailable without recursing into the submodule';
|
|
1599
|
+
} else if (change.newMode !== '000000') {
|
|
1600
|
+
try { current = worktreeGitIdentity(workspace, recordPathBytes); }
|
|
1601
|
+
catch (error) { currentReason = `working-tree identity unavailable: ${error.message}`; }
|
|
1602
|
+
}
|
|
1603
|
+
} else if (!change && base) {
|
|
1604
|
+
current = { mode: base.mode, oid: base.oid, type: base.type, oidBasis: 'unchanged base-tree object' };
|
|
1605
|
+
} else if (!base && !change && proposedByKey.has(key)) {
|
|
1606
|
+
try { current = worktreeGitIdentity(workspace, recordPathBytes); }
|
|
1607
|
+
catch (error) { currentReason = `proposed untracked identity unavailable: ${error.message}`; status = 'unavailable'; }
|
|
1608
|
+
}
|
|
1609
|
+
if (current && current.reason) { currentReason = current.reason; current = null; status = 'unavailable'; }
|
|
1610
|
+
const identityMode = (current && current.mode) || (base && base.mode) || (change && change.oldMode);
|
|
1611
|
+
const document = inventoryPathIsDocument(recordPathBytes, loaded.manifest);
|
|
1612
|
+
const disposition = inventoryPathDisposition(recordPathBytes, loaded.manifest, identityMode);
|
|
1613
|
+
const shown = gitPathValue(recordPathBytes);
|
|
1614
|
+
const record = {
|
|
1615
|
+
path: shown.path,
|
|
1616
|
+
pathEncoding: shown.encoding,
|
|
1617
|
+
pathBytesBase64: recordPathBytes.toString('base64'),
|
|
1618
|
+
document,
|
|
1619
|
+
kind: identityMode === '160000' ? 'gitlink' : identityMode === '120000' ? 'symlink' : 'file',
|
|
1620
|
+
status,
|
|
1621
|
+
base: base ? { mode: base.mode, type: base.type, oid: base.oid } : null,
|
|
1622
|
+
current,
|
|
1623
|
+
...disposition,
|
|
1624
|
+
validation: { outcome: 'unchecked', reason: 'P1 inventory does not execute semantic validators.' }
|
|
1625
|
+
};
|
|
1626
|
+
if (currentReason) record.currentReason = currentReason;
|
|
1627
|
+
if (change && change.partner) {
|
|
1628
|
+
const partner = gitPathValue(change.partner);
|
|
1629
|
+
record.rename = { endpoint: change.endpoint, partnerPath: partner.path, partnerPathBytesBase64: change.partner.toString('base64') };
|
|
1630
|
+
}
|
|
1631
|
+
records.push(record);
|
|
1632
|
+
}
|
|
1633
|
+
records.sort((left, right) => Buffer.compare(Buffer.from(left.pathBytesBase64, 'base64'), Buffer.from(right.pathBytesBase64, 'base64')));
|
|
1634
|
+
const documents = records.filter(item => item.document);
|
|
1635
|
+
const classified = documents.filter(item => item.role && item.lifecycle && item.disposition && item.reason);
|
|
1636
|
+
const identity = records.map(item => ({ pathBytesBase64: item.pathBytesBase64, status: item.status, base: item.base, current: item.current, rename: item.rename || null }));
|
|
1637
|
+
return {
|
|
1638
|
+
schemaVersion: 1,
|
|
1639
|
+
base: { requestedRef: baseRef, commit, tree },
|
|
1640
|
+
candidate: {
|
|
1641
|
+
kind: 'working-tree',
|
|
1642
|
+
scope: 'tracked document paths, explicit proposed paths, and symlink/gitlink and rename endpoint identities',
|
|
1643
|
+
proposedUntrackedPaths: proposedByKey.size,
|
|
1644
|
+
sha256: crypto.createHash('sha256').update(JSON.stringify(identity)).digest('hex')
|
|
1645
|
+
},
|
|
1646
|
+
manifest: { path: path.relative(workspace, loaded.absolute).split(path.sep).join('/'), sha256: loaded.sha256 },
|
|
1647
|
+
coverage: {
|
|
1648
|
+
identifiedDocumentPaths: documents.length,
|
|
1649
|
+
explicitDispositionPaths: classified.length,
|
|
1650
|
+
complete: documents.length === classified.length,
|
|
1651
|
+
claim: 'disposition coverage only; semantic validation was not performed'
|
|
1652
|
+
},
|
|
1653
|
+
records
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1385
1657
|
function parseInventoryDoc(relativePath, bytes) {
|
|
1386
1658
|
const parsed = parseFrontmatter(Buffer.from(bytes).toString('utf8'));
|
|
1387
1659
|
if (parsed.error || !parsed.data || typeof parsed.data.id !== 'string') return null;
|
|
@@ -2299,10 +2571,14 @@ function buildIndex(docs, config, options = {}) {
|
|
|
2299
2571
|
|
|
2300
2572
|
function parseCli(argv) {
|
|
2301
2573
|
const [command, ...rest] = argv;
|
|
2302
|
-
const options = { command, changedProvided: false, changedPaths: [], all: false };
|
|
2574
|
+
const options = { command, changedProvided: false, changedPaths: [], proposedUntracked: [], all: false };
|
|
2303
2575
|
for (let index = 0; index < rest.length; index++) {
|
|
2304
2576
|
const arg = rest[index];
|
|
2305
2577
|
if (arg === '--all') options.all = true;
|
|
2578
|
+
else if (arg === '--proposed-untracked') {
|
|
2579
|
+
if (index + 1 >= rest.length) throw new Error(`${arg} requires a value`);
|
|
2580
|
+
options.proposedUntracked.push(rest[++index]);
|
|
2581
|
+
}
|
|
2306
2582
|
else if (arg === '--changed-path') {
|
|
2307
2583
|
if (index + 1 >= rest.length) throw new Error(`${arg} requires a value`);
|
|
2308
2584
|
const value = rest[++index];
|
|
@@ -2310,16 +2586,24 @@ function parseCli(argv) {
|
|
|
2310
2586
|
options.changedProvided = true;
|
|
2311
2587
|
options.changedPaths.push(value);
|
|
2312
2588
|
}
|
|
2313
|
-
else if (arg === '--workspace' || arg === '--config' || arg === '--changed' || arg === '--base') {
|
|
2589
|
+
else if (arg === '--workspace' || arg === '--config' || arg === '--changed' || arg === '--base' || arg === '--manifest') {
|
|
2314
2590
|
if (index + 1 >= rest.length) throw new Error(`${arg} requires a value`);
|
|
2315
2591
|
const value = rest[++index];
|
|
2316
2592
|
if (arg === '--workspace') options.workspace = value;
|
|
2317
2593
|
if (arg === '--config') options.configFile = value;
|
|
2318
2594
|
if (arg === '--changed') { options.changedProvided = true; options.changedPaths = options.changedPaths.concat(value.split(',').map(item => item.trim()).filter(Boolean)); }
|
|
2319
2595
|
if (arg === '--base') options.base = value;
|
|
2596
|
+
if (arg === '--manifest') options.manifestFile = value;
|
|
2320
2597
|
} else throw new Error(`unknown argument: ${arg}`);
|
|
2321
2598
|
}
|
|
2599
|
+
if (command === 'inventory') {
|
|
2600
|
+
if (!options.workspace || !options.manifestFile || !options.base) throw new Error('inventory requires --workspace, --manifest, and --base');
|
|
2601
|
+
if (options.configFile || options.changedProvided || options.all) throw new Error('inventory accepts only --workspace, --manifest, and --base');
|
|
2602
|
+
return options;
|
|
2603
|
+
}
|
|
2604
|
+
if (options.proposedUntracked.length) throw new Error('--proposed-untracked is only valid with inventory');
|
|
2322
2605
|
if (!['lint', 'index'].includes(command)) throw new Error('usage: doc-lifecycle.js <lint|index> --workspace <dir> --config <json-file, relative to --workspace> [--changed a,b | --changed-path <path> (repeatable, cumulative) | --all] [--base <git-rev>]');
|
|
2606
|
+
if (options.manifestFile) throw new Error('--manifest is only valid with inventory');
|
|
2323
2607
|
if (!options.workspace || !options.configFile) throw new Error('--workspace and --config are required');
|
|
2324
2608
|
if (!options.changedProvided && !options.all) throw new Error('lint and index require --changed, --changed-path, or --all');
|
|
2325
2609
|
if (options.all && options.changedProvided) throw new Error('choose either --changed/--changed-path or --all');
|
|
@@ -2361,6 +2645,15 @@ function main(argv = process.argv.slice(2)) {
|
|
|
2361
2645
|
let options;
|
|
2362
2646
|
try { options = parseCli(argv); } catch (error) { process.stderr.write(`${error.message}\n`); return 2; }
|
|
2363
2647
|
const workspacePath = path.resolve(options.workspace);
|
|
2648
|
+
if (options.command === 'inventory') {
|
|
2649
|
+
try {
|
|
2650
|
+
process.stdout.write(`${JSON.stringify(buildGitDocumentInventory(workspacePath, options.base, options.manifestFile, options.proposedUntracked), null, 2)}\n`);
|
|
2651
|
+
return 0;
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
process.stderr.write(`inventory failed: ${error.message}\n`);
|
|
2654
|
+
return 2;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2364
2657
|
if (options.changedProvided) {
|
|
2365
2658
|
try { options.changedPaths = options.changedPaths.map(item => normalizeChangedPath(workspacePath, item)); } catch (error) {
|
|
2366
2659
|
process.stderr.write(`${error.message}\n`);
|
|
@@ -2399,6 +2692,6 @@ function main(argv = process.argv.slice(2)) {
|
|
|
2399
2692
|
return analysis.diagnostics.some(item => item.severity === 'error') ? 1 : 0;
|
|
2400
2693
|
}
|
|
2401
2694
|
|
|
2402
|
-
module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, analyzeDocs, resolveRef, lintDocs, buildIndex, diffRevisions };
|
|
2695
|
+
module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, analyzeDocs, resolveRef, lintDocs, buildIndex, diffRevisions, buildGitDocumentInventory };
|
|
2403
2696
|
|
|
2404
2697
|
if (require.main === module) process.exitCode = main();
|
package/package.json
CHANGED
|
@@ -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.43.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",
|