@lemoncode/lemony 0.4.0 → 0.5.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/catalog/VERSION +1 -1
- package/catalog/agents/implementer.md +14 -3
- package/catalog/agents/orchestrator.md +56 -8
- package/catalog/agents/reviewer.md +8 -2
- package/catalog/agents/spec-author.md +3 -1
- package/catalog/commands/pause.md +8 -5
- package/catalog/harness.config.schema.json +4 -0
- package/catalog/hooks/init.sh +36 -8
- package/catalog/hooks/session-close.sh +15 -7
- package/catalog/schemas/tier2-events-history.md +14 -0
- package/catalog/schemas/tier2-events.md +11 -9
- package/catalog/skills/a11y-audit/SKILL.md +3 -1
- package/catalog/skills/prd-to-spec/SKILL.md +25 -0
- package/catalog/skills/resolve-discovery/SKILL.md +6 -6
- package/catalog/skills/tdd/SKILL.md +3 -0
- package/catalog/templates/claude-code/harness.config.yml.tpl +10 -4
- package/dist/cli.mjs +329 -112
- package/package.json +6 -6
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
3
|
import { access, appendFile, chmod, lstat, mkdir, open, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, delimiter, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { argv, cwd, env, exit, stderr, stdin, stdout } from "node:process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
-
import {
|
|
8
|
+
import { parse, parseDocument } from "yaml";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import { constants } from "node:fs";
|
|
@@ -27,7 +27,8 @@ const DEPRECATED_IMPLEMENTATION_KEYS = ["pre_commit_review"];
|
|
|
27
27
|
const HARNESS_CONFIG_SCHEMA_FILENAME = "harness.config.schema.json";
|
|
28
28
|
const CONFIG_KEY_SINCE = {
|
|
29
29
|
gates: "0.3.0",
|
|
30
|
-
"implementation.auto_commit": "0.4.0"
|
|
30
|
+
"implementation.auto_commit": "0.4.0",
|
|
31
|
+
"design_tokens.verify": "0.5.0"
|
|
31
32
|
};
|
|
32
33
|
const TASK_STORAGE_REPO_PLACEHOLDER = "OWNER/REPO";
|
|
33
34
|
const TARGETS = ["claude-code"];
|
|
@@ -120,7 +121,10 @@ const taskStorageSchema = z.object({
|
|
|
120
121
|
}).strict();
|
|
121
122
|
const rollbackSchema = z.object({ keep_snapshots: z.union([z.int().positive(), z.literal("unlimited")]).default(3) }).strict().prefault({});
|
|
122
123
|
const telemetrySchema = z.object({ enabled: z.boolean().default(true) }).strict().prefault({});
|
|
123
|
-
const designTokensSchema = z.object({
|
|
124
|
+
const designTokensSchema = z.object({
|
|
125
|
+
scan_extensions: z.array(z.string()).default([]),
|
|
126
|
+
verify: z.string().trim().min(1).optional()
|
|
127
|
+
}).strict().prefault({});
|
|
124
128
|
const mergeSchema = z.object({
|
|
125
129
|
checks_timeout_secs: z.int().positive().default(600),
|
|
126
130
|
allow_no_checks: z.boolean().default(false)
|
|
@@ -230,8 +234,6 @@ const setConfigValues = (rawYaml, updates) => {
|
|
|
230
234
|
}
|
|
231
235
|
for (const key of DEPRECATED_CONFIG_KEYS) if (doc.has(key)) doc.delete(key);
|
|
232
236
|
for (const key of DEPRECATED_PATHS_KEYS) if (doc.hasIn(["paths", key])) doc.deleteIn(["paths", key]);
|
|
233
|
-
const pathsNode = doc.get("paths", true);
|
|
234
|
-
if (isMap(pathsNode) && pathsNode.items.length === 0) doc.delete("paths");
|
|
235
237
|
for (const key of DEPRECATED_IMPLEMENTATION_KEYS) if (doc.hasIn(["implementation", key])) doc.deleteIn(["implementation", key]);
|
|
236
238
|
return doc.toString();
|
|
237
239
|
};
|
|
@@ -259,8 +261,6 @@ const pointerScalar = z.union([
|
|
|
259
261
|
z.null()
|
|
260
262
|
]).optional();
|
|
261
263
|
const pointerFrontmatterSchema = z.object({
|
|
262
|
-
active_task: pointerScalar,
|
|
263
|
-
branch: pointerScalar,
|
|
264
264
|
session_start_ts: pointerScalar,
|
|
265
265
|
last_close_ts: pointerScalar
|
|
266
266
|
});
|
|
@@ -1452,6 +1452,7 @@ const flatten = (color, backdrop) => ({
|
|
|
1452
1452
|
b: color.b * color.a + backdrop.b * (1 - color.a),
|
|
1453
1453
|
a: 1
|
|
1454
1454
|
});
|
|
1455
|
+
const compositeOver = (color, ground) => flatten(color, ground.a < 1 ? flatten(ground, WHITE) : ground);
|
|
1455
1456
|
const linearizeChannel = (channel) => {
|
|
1456
1457
|
const s = channel / 255;
|
|
1457
1458
|
return s <= .03928 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4;
|
|
@@ -1507,15 +1508,17 @@ const runContrast = async (inputs) => {
|
|
|
1507
1508
|
};
|
|
1508
1509
|
const tokens = collectTokens(parsed);
|
|
1509
1510
|
const problems = [];
|
|
1510
|
-
const specs = discoverPairs(tokens, problems);
|
|
1511
1511
|
const modes = declaredModes(tokens);
|
|
1512
|
+
const specs = discoverPairs(tokens, modes, problems);
|
|
1512
1513
|
const pairs = measurePairs(specs, modes, tokens);
|
|
1514
|
+
const verify = inputs.verify ? await runVerifier(inputs.verify) : void 0;
|
|
1513
1515
|
return {
|
|
1514
|
-
ok: problems.length === 0 && pairs.every((pair) => pair.passes),
|
|
1516
|
+
ok: problems.length === 0 && pairs.every((pair) => pair.passes) && (verify === void 0 || verify.code === 0),
|
|
1515
1517
|
tokensFound: true,
|
|
1516
1518
|
pairsChecked: pairs.length,
|
|
1517
1519
|
pairs,
|
|
1518
|
-
problems
|
|
1520
|
+
problems,
|
|
1521
|
+
...verify === void 0 ? {} : { verify }
|
|
1519
1522
|
};
|
|
1520
1523
|
};
|
|
1521
1524
|
const emptyResult$1 = (tokensFound) => ({
|
|
@@ -1554,19 +1557,17 @@ const declaredModes = (tokens) => {
|
|
|
1554
1557
|
for (const token of tokens.values()) {
|
|
1555
1558
|
const modes = readExtension(token.node, MODES_EXTENSION);
|
|
1556
1559
|
if (isRecord(modes)) {
|
|
1557
|
-
for (const name of Object.keys(modes)) if (name.trim().length > 0) names.add(name);
|
|
1560
|
+
for (const name of Object.keys(modes)) if (name.trim().length > 0 && name !== "base") names.add(name);
|
|
1558
1561
|
}
|
|
1559
1562
|
}
|
|
1560
1563
|
return [...names].toSorted();
|
|
1561
1564
|
};
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
for (const spec of extensionPairs(tokens, problems)) add(spec);
|
|
1569
|
-
return [...merged.values()];
|
|
1565
|
+
const pairKey = (spec) => `${spec.foreground}|${spec.background}`;
|
|
1566
|
+
const discoverPairs = (tokens, modes, problems) => {
|
|
1567
|
+
const convention = new Map(conventionPairs(tokens).map((spec) => [pairKey(spec), spec]));
|
|
1568
|
+
const explicit = extensionPairs(tokens, modes, problems);
|
|
1569
|
+
for (const spec of explicit) convention.delete(pairKey(spec));
|
|
1570
|
+
return [...convention.values(), ...explicit];
|
|
1570
1571
|
};
|
|
1571
1572
|
const conventionPairs = (tokens) => {
|
|
1572
1573
|
const pairs = [];
|
|
@@ -1583,77 +1584,193 @@ const conventionPairs = (tokens) => {
|
|
|
1583
1584
|
foreground: path,
|
|
1584
1585
|
background: basePath,
|
|
1585
1586
|
level: DEFAULT_LEVEL,
|
|
1587
|
+
floors: {},
|
|
1586
1588
|
source: "convention"
|
|
1587
1589
|
});
|
|
1588
1590
|
}
|
|
1589
1591
|
return pairs;
|
|
1590
1592
|
};
|
|
1591
|
-
const
|
|
1593
|
+
const DECLARATION_SHAPE = "{ against: \"{token.path}\" | [\"{token.path}\", …], level?, floors?, modes?, over? }";
|
|
1594
|
+
const DECLARATION_KEYS = /* @__PURE__ */ new Set([
|
|
1595
|
+
"against",
|
|
1596
|
+
"level",
|
|
1597
|
+
"floors",
|
|
1598
|
+
"modes",
|
|
1599
|
+
"over"
|
|
1600
|
+
]);
|
|
1601
|
+
const extensionPairs = (tokens, modes, problems) => {
|
|
1592
1602
|
const pairs = [];
|
|
1593
1603
|
for (const [path, token] of tokens) {
|
|
1594
1604
|
const declaration = readExtension(token.node, CONTRAST_EXTENSION);
|
|
1595
1605
|
if (declaration === void 0) continue;
|
|
1596
1606
|
const entries = Array.isArray(declaration) ? declaration : [declaration];
|
|
1607
|
+
const declared = /* @__PURE__ */ new Set();
|
|
1597
1608
|
for (const entry of entries) {
|
|
1598
|
-
if (!isRecord(entry)
|
|
1599
|
-
problems.push(`token "${path}" has a malformed ${CONTRAST_EXTENSION} declaration — expected {
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
const against = aliasTarget(entry["against"]) ?? entry["against"];
|
|
1603
|
-
if (against === path) {
|
|
1604
|
-
problems.push(`token "${path}" declares contrast against itself — a pair needs two different tokens.`);
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
if (!tokens.has(against)) {
|
|
1608
|
-
problems.push(`token "${path}" contrast-against "${entry["against"]}" does not resolve to a known token.`);
|
|
1609
|
+
if (!isRecord(entry)) {
|
|
1610
|
+
problems.push(`token "${path}" has a malformed ${CONTRAST_EXTENSION} declaration — expected ${DECLARATION_SHAPE}.`);
|
|
1609
1611
|
continue;
|
|
1610
1612
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
+
const unknownKeys = Object.keys(entry).filter((key) => !DECLARATION_KEYS.has(key));
|
|
1614
|
+
if (unknownKeys.length > 0) {
|
|
1615
|
+
problems.push(`token "${path}" has unknown ${CONTRAST_EXTENSION} key(s) ${unknownKeys.map((key) => `"${key}"`).join(", ")} — expected ${DECLARATION_SHAPE}.`);
|
|
1613
1616
|
continue;
|
|
1614
1617
|
}
|
|
1618
|
+
const before = problems.length;
|
|
1619
|
+
const backgrounds = tokenPathList(entry["against"], path, "against", problems);
|
|
1620
|
+
if (backgrounds.length === 0) continue;
|
|
1615
1621
|
if (!resolveColor(path, "base", tokens)) {
|
|
1616
1622
|
problems.push(`token "${path}" declares a contrast pair but is not itself a colour token.`);
|
|
1617
1623
|
continue;
|
|
1618
1624
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
+
const level = normalizeLevel(entry["level"], path, problems);
|
|
1626
|
+
const floors = normalizeFloors(entry["floors"], path, modes, problems);
|
|
1627
|
+
const ruleModes = normalizeModes(entry["modes"], path, modes, problems);
|
|
1628
|
+
const measuredModes = (ruleModes ?? modes).filter((mode) => mode !== BASE_MODE);
|
|
1629
|
+
const ownDefects = unresolvedModes(path, measuredModes, tokens);
|
|
1630
|
+
for (const mode of ownDefects) {
|
|
1631
|
+
const problem = `token "${path}" declares a contrast pair but its "${mode}" mode override does not resolve to a colour.`;
|
|
1632
|
+
if (!problems.includes(problem)) problems.push(problem);
|
|
1633
|
+
}
|
|
1634
|
+
if (ruleModes !== void 0) {
|
|
1635
|
+
for (const mode of Object.keys(floors)) if (mode !== "base" && !ruleModes.includes(mode)) problems.push(`token "${path}" sets a contrast floor for mode "${mode}" but restricts the rule to ${ruleModes.map((name) => `"${name}"`).join("/")} — the floor would never apply.`);
|
|
1636
|
+
}
|
|
1637
|
+
const grounds = entry["over"] === void 0 ? [] : tokenPathList(entry["over"], path, "over", problems);
|
|
1638
|
+
const colourGrounds = [];
|
|
1639
|
+
for (const ground of grounds) if (ground === path || backgrounds.includes(ground)) problems.push(`token "${path}" composites over "${ground}", which is an end of the pair itself — \`over\` names the opaque ground beneath the background.`);
|
|
1640
|
+
else if (isColourTarget(ground, path, "over", measuredModes, tokens, problems)) colourGrounds.push(ground);
|
|
1641
|
+
const colourBackgrounds = [];
|
|
1642
|
+
for (const against of backgrounds) if (against === path) problems.push(`token "${path}" declares contrast against itself — a pair needs two different tokens.`);
|
|
1643
|
+
else if (isColourTarget(against, path, "against", measuredModes, tokens, problems)) colourBackgrounds.push(against);
|
|
1644
|
+
if (ownDefects.length > 0 || problems.length > before) continue;
|
|
1645
|
+
for (const background of colourBackgrounds) for (const over of colourGrounds.length === 0 ? [void 0] : colourGrounds) {
|
|
1646
|
+
const key = `${background}|${over ?? ""}`;
|
|
1647
|
+
if (declared.has(key)) problems.push(`token "${path}" declares contrast against "${background}"${over === void 0 ? "" : ` over "${over}"`} more than once — one declaration per pair.`);
|
|
1648
|
+
declared.add(key);
|
|
1649
|
+
}
|
|
1650
|
+
if (ownDefects.length > 0 || problems.length > before) continue;
|
|
1651
|
+
for (const background of colourBackgrounds) {
|
|
1652
|
+
const base = {
|
|
1653
|
+
foreground: path,
|
|
1654
|
+
background,
|
|
1655
|
+
level,
|
|
1656
|
+
floors,
|
|
1657
|
+
source: "extension"
|
|
1658
|
+
};
|
|
1659
|
+
const withModes = ruleModes === void 0 ? base : {
|
|
1660
|
+
...base,
|
|
1661
|
+
modes: ruleModes
|
|
1662
|
+
};
|
|
1663
|
+
if (colourGrounds.length === 0) pairs.push(withModes);
|
|
1664
|
+
else for (const over of colourGrounds) pairs.push({
|
|
1665
|
+
...withModes,
|
|
1666
|
+
over
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1625
1669
|
}
|
|
1626
1670
|
}
|
|
1627
1671
|
return pairs;
|
|
1628
1672
|
};
|
|
1673
|
+
const tokenPathList = (raw, path, field, problems) => {
|
|
1674
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
1675
|
+
if (list.length === 0 || !list.every((item) => typeof item === "string")) {
|
|
1676
|
+
problems.push(`token "${path}" has a malformed ${CONTRAST_EXTENSION} declaration — \`${field}\` must be a token path or a non-empty array of them (expected ${DECLARATION_SHAPE}).`);
|
|
1677
|
+
return [];
|
|
1678
|
+
}
|
|
1679
|
+
return list.map((item) => aliasTarget(item) ?? item);
|
|
1680
|
+
};
|
|
1681
|
+
const isColourTarget = (target, path, field, modes, tokens, problems) => {
|
|
1682
|
+
if (!tokens.has(target)) {
|
|
1683
|
+
problems.push(`token "${path}" contrast-${field} "${target}" does not resolve to a known token.`);
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
if (!resolveColor(target, "base", tokens)) {
|
|
1687
|
+
problems.push(`token "${path}" contrast-${field} "${target}" is not a colour token.`);
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1690
|
+
const unresolved = unresolvedModes(target, modes, tokens);
|
|
1691
|
+
for (const mode of unresolved) problems.push(`token "${path}" contrast-${field} "${target}" has a "${mode}" mode override that does not resolve to a colour.`);
|
|
1692
|
+
return unresolved.length === 0;
|
|
1693
|
+
};
|
|
1694
|
+
const unresolvedModes = (target, modes, tokens) => modes.filter((mode) => resolveColor(target, mode, tokens) === void 0);
|
|
1629
1695
|
const normalizeLevel = (raw, path, problems) => {
|
|
1630
1696
|
if (raw === void 0) return DEFAULT_LEVEL;
|
|
1631
1697
|
if (typeof raw === "string" && CONTRAST_LEVELS.includes(raw)) return raw;
|
|
1632
1698
|
problems.push(`token "${path}" has an unknown contrast level "${String(raw)}" — use ${CONTRAST_LEVELS.join("/")}.`);
|
|
1633
1699
|
return DEFAULT_LEVEL;
|
|
1634
1700
|
};
|
|
1701
|
+
const normalizeFloors = (raw, path, modes, problems) => {
|
|
1702
|
+
if (raw === void 0) return {};
|
|
1703
|
+
if (!isRecord(raw)) {
|
|
1704
|
+
problems.push(`token "${path}" has malformed contrast floors — expected { base?: number, <mode>?: number }.`);
|
|
1705
|
+
return {};
|
|
1706
|
+
}
|
|
1707
|
+
const floors = Object.create(null);
|
|
1708
|
+
for (const [mode, value] of Object.entries(raw)) {
|
|
1709
|
+
if (mode !== "base" && !modes.includes(mode)) {
|
|
1710
|
+
problems.push(`token "${path}" sets a contrast floor for mode "${mode}", which no ${MODES_EXTENSION} block declares — use ${[BASE_MODE, ...modes].join("/")}.`);
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1 || value > 21) {
|
|
1714
|
+
problems.push(`token "${path}" has an invalid contrast floor for mode "${mode}" (${String(value)}) — a WCAG ratio is a number between 1 and 21.`);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
floors[mode] = value;
|
|
1718
|
+
}
|
|
1719
|
+
return floors;
|
|
1720
|
+
};
|
|
1721
|
+
const normalizeModes = (raw, path, modes, problems) => {
|
|
1722
|
+
if (raw === void 0) return void 0;
|
|
1723
|
+
if (!Array.isArray(raw) || raw.length === 0 || !raw.every((item) => typeof item === "string")) {
|
|
1724
|
+
problems.push(`token "${path}" has malformed contrast modes — expected a non-empty array of mode names.`);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const unknown = raw.filter((mode) => mode !== "base" && !modes.includes(mode));
|
|
1728
|
+
if (unknown.length > 0) {
|
|
1729
|
+
problems.push(`token "${path}" restricts contrast to mode(s) ${unknown.map((mode) => `"${mode}"`).join(", ")}, which no ${MODES_EXTENSION} block declares — use ${[BASE_MODE, ...modes].join("/")}.`);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
return [...new Set(raw)];
|
|
1733
|
+
};
|
|
1734
|
+
const floorFor = (spec, mode) => ownFloor(spec.floors, mode) ?? ownFloor(spec.floors, "base") ?? WCAG_FLOORS[spec.level];
|
|
1735
|
+
const ownFloor = (floors, mode) => Object.hasOwn(floors, mode) ? floors[mode] : void 0;
|
|
1635
1736
|
const measurePairs = (specs, modes, tokens) => {
|
|
1636
1737
|
const rows = [];
|
|
1637
1738
|
for (const spec of specs) {
|
|
1638
|
-
const
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
for (const mode of
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
rows.push(makeRow(spec, mode, fg, bg));
|
|
1739
|
+
const base = resolvePair(spec, BASE_MODE, tokens);
|
|
1740
|
+
if (!base) continue;
|
|
1741
|
+
const explicit = spec.modes !== void 0;
|
|
1742
|
+
const rowModes = spec.modes ?? ["base", ...modes];
|
|
1743
|
+
for (const mode of rowModes) {
|
|
1744
|
+
const colours = mode === "base" ? base : resolvePair(spec, mode, tokens) ?? base;
|
|
1745
|
+
if (!explicit && mode !== "base" && sameColor(colours.fg, base.fg) && sameColor(colours.bg, base.bg) && floorFor(spec, mode) === floorFor(spec, "base")) continue;
|
|
1746
|
+
rows.push(makeRow(spec, mode, colours.fg, colours.bg));
|
|
1647
1747
|
}
|
|
1648
1748
|
}
|
|
1649
1749
|
return rows;
|
|
1650
1750
|
};
|
|
1751
|
+
const resolvePair = (spec, mode, tokens) => {
|
|
1752
|
+
const resolveEnd = (path) => resolveColor(path, mode, tokens) ?? (mode === "base" ? void 0 : resolveColor(path, "base", tokens));
|
|
1753
|
+
const fg = resolveEnd(spec.foreground);
|
|
1754
|
+
const against = resolveEnd(spec.background);
|
|
1755
|
+
if (!fg || !against) return void 0;
|
|
1756
|
+
if (spec.over === void 0) return {
|
|
1757
|
+
fg,
|
|
1758
|
+
bg: against
|
|
1759
|
+
};
|
|
1760
|
+
const ground = resolveEnd(spec.over);
|
|
1761
|
+
if (!ground) return void 0;
|
|
1762
|
+
return {
|
|
1763
|
+
fg,
|
|
1764
|
+
bg: compositeOver(against, ground)
|
|
1765
|
+
};
|
|
1766
|
+
};
|
|
1651
1767
|
const makeRow = (spec, mode, foreground, background) => {
|
|
1652
1768
|
const ratio = contrastRatio(foreground, background);
|
|
1653
|
-
const floor =
|
|
1769
|
+
const floor = floorFor(spec, mode);
|
|
1654
1770
|
return {
|
|
1655
1771
|
foreground: spec.foreground,
|
|
1656
1772
|
background: spec.background,
|
|
1773
|
+
...spec.over === void 0 ? {} : { over: spec.over },
|
|
1657
1774
|
mode,
|
|
1658
1775
|
level: spec.level,
|
|
1659
1776
|
ratio,
|
|
@@ -1677,6 +1794,13 @@ const resolveColor = (path, mode, tokens, seen = /* @__PURE__ */ new Set()) => {
|
|
|
1677
1794
|
if (alias !== void 0) return resolveColor(alias, mode, tokens, seen);
|
|
1678
1795
|
return typeof value === "string" ? parseColor(value) : void 0;
|
|
1679
1796
|
};
|
|
1797
|
+
const runVerifier = async (hook) => {
|
|
1798
|
+
const result = await hook.run("sh", ["-c", hook.command]);
|
|
1799
|
+
return {
|
|
1800
|
+
command: hook.command,
|
|
1801
|
+
...result
|
|
1802
|
+
};
|
|
1803
|
+
};
|
|
1680
1804
|
//#endregion
|
|
1681
1805
|
//#region src/design-tokens/design-sync.constant.ts
|
|
1682
1806
|
const DESIGN_TOOL_EXTENSION = "com.lemony.design-tool";
|
|
@@ -2097,16 +2221,18 @@ const SPEC_SIDE_KINDS = [
|
|
|
2097
2221
|
"malformed-risk-marker",
|
|
2098
2222
|
"malformed-task-refs",
|
|
2099
2223
|
"unknown-risk-class",
|
|
2100
|
-
"dangling-requirement-ref"
|
|
2224
|
+
"dangling-requirement-ref",
|
|
2225
|
+
"unticked-completed-task"
|
|
2101
2226
|
];
|
|
2102
2227
|
const GROUP_HEADER = /^##\s+Group\s+(\d+)\b/;
|
|
2103
2228
|
const RISK_MARKER = /\[risk:\s*([^\]]*)\]\s*$/;
|
|
2104
2229
|
const RISK_MARKER_ALL = /\[risk:/gi;
|
|
2105
2230
|
const RISK_MARKER_LOOKALIKE = /\[\s*risks?\s*:/i;
|
|
2106
|
-
const TASK_LINE = /^\s*[-*+]\s*\[[ xX]\]\s*(?:\*\*|__|`)?\s*(T\d+)\b/;
|
|
2231
|
+
const TASK_LINE = /^\s*[-*+]\s*\[([ xX])\]\s*(?:\*\*|__|`)?\s*(T\d+)\b/;
|
|
2107
2232
|
const TASK_REFS = /^\(((?:R\d+)(?:\s*,\s*R\d+)*)\)/;
|
|
2108
|
-
const
|
|
2109
|
-
const
|
|
2233
|
+
const TASK_REFS_LOOKALIKE_ALL = /\(\s*R\d+/g;
|
|
2234
|
+
const TASK_TITLE_WRAPPER = /^\s*[-*+]\s*\[[ xX]\]\s*(\*\*|__)/;
|
|
2235
|
+
const TASK_REFS_AFTER_TITLE_CLOSE = /^\s*(\(\s*R\d+)/;
|
|
2110
2236
|
const REQUIREMENT_LINE = /^\s*-\s*\*\*(R\d+)\*\*/;
|
|
2111
2237
|
const TEST_FILE = /(^|\/)__tests__\/|\.(spec|test)\.[cm]?[jt]sx?$/;
|
|
2112
2238
|
//#endregion
|
|
@@ -2157,7 +2283,7 @@ const parseTasksSpec = (text) => {
|
|
|
2157
2283
|
const blockEnd = taskBlockEnd(lines, index);
|
|
2158
2284
|
const parts = lines.slice(index, blockEnd).map((part) => part.trim());
|
|
2159
2285
|
index = blockEnd - 1;
|
|
2160
|
-
const task = parseTaskBlock(match[1] ?? "", parts);
|
|
2286
|
+
const task = parseTaskBlock(match[2] ?? "", (match[1] ?? " ") !== " ", parts);
|
|
2161
2287
|
if (task.malformedRefs) malformedTaskRefs.push(task.id);
|
|
2162
2288
|
if (current) {
|
|
2163
2289
|
current.tasks.push(task.task);
|
|
@@ -2198,7 +2324,7 @@ const taskBlockEnd = (lines, start) => {
|
|
|
2198
2324
|
}
|
|
2199
2325
|
return end;
|
|
2200
2326
|
};
|
|
2201
|
-
const parseTaskBlock = (id, parts) => {
|
|
2327
|
+
const parseTaskBlock = (id, ticked, parts) => {
|
|
2202
2328
|
const block = parts.join(" ");
|
|
2203
2329
|
const opener = refsOpener(parts, block);
|
|
2204
2330
|
const refs = opener === void 0 ? null : declarationAt(parts, block, opener);
|
|
@@ -2207,9 +2333,10 @@ const parseTaskBlock = (id, parts) => {
|
|
|
2207
2333
|
id,
|
|
2208
2334
|
task: {
|
|
2209
2335
|
id,
|
|
2336
|
+
ticked,
|
|
2210
2337
|
requirementRefs: [...new Set(requirementRefs)]
|
|
2211
2338
|
},
|
|
2212
|
-
malformedRefs: refs === null &&
|
|
2339
|
+
malformedRefs: refs === null && hasRefLookalike(parts)
|
|
2213
2340
|
};
|
|
2214
2341
|
};
|
|
2215
2342
|
const declarationAt = (parts, block, opener) => {
|
|
@@ -2230,17 +2357,40 @@ const declarationClosesLine = (parts, end) => {
|
|
|
2230
2357
|
return /^(?:\*\*|__)/.test(tail) && lineEnds.has(end + 2);
|
|
2231
2358
|
};
|
|
2232
2359
|
const refsOpener = (parts, block) => {
|
|
2233
|
-
const
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2360
|
+
const first = parts[0] ?? "";
|
|
2361
|
+
const attempted = [];
|
|
2362
|
+
for (const lookalike of first.matchAll(TASK_REFS_LOOKALIKE_ALL)) {
|
|
2363
|
+
if (isLinkTarget(first, lookalike.index)) continue;
|
|
2364
|
+
attempted.push(lookalike.index);
|
|
2365
|
+
break;
|
|
2366
|
+
}
|
|
2367
|
+
const afterTitle = afterTitleClose(first, block);
|
|
2368
|
+
if (afterTitle !== void 0) attempted.push(afterTitle);
|
|
2369
|
+
if (attempted.length > 0) return Math.min(...attempted);
|
|
2370
|
+
const closing = [];
|
|
2371
|
+
let offset = first.length + 1;
|
|
2239
2372
|
for (const part of parts.slice(1)) {
|
|
2240
|
-
|
|
2373
|
+
for (const lookalike of part.matchAll(TASK_REFS_LOOKALIKE_ALL)) {
|
|
2374
|
+
if (isLinkTarget(part, lookalike.index)) continue;
|
|
2375
|
+
const at = offset + lookalike.index;
|
|
2376
|
+
if (declarationAt(parts, block, at) !== null) closing.push(at);
|
|
2377
|
+
}
|
|
2241
2378
|
offset += part.length + 1;
|
|
2242
2379
|
}
|
|
2243
|
-
return
|
|
2380
|
+
return closing.length === 1 ? closing[0] : void 0;
|
|
2381
|
+
};
|
|
2382
|
+
const hasRefLookalike = (parts) => parts.some((part) => [...part.matchAll(TASK_REFS_LOOKALIKE_ALL)].some((lookalike) => !isLinkTarget(part, lookalike.index)));
|
|
2383
|
+
const isLinkTarget = (line, index) => line[index - 1] === "]";
|
|
2384
|
+
const afterTitleClose = (first, block) => {
|
|
2385
|
+
const wrapper = TASK_TITLE_WRAPPER.exec(first);
|
|
2386
|
+
const marker = wrapper?.[1];
|
|
2387
|
+
if (!wrapper || marker === void 0) return void 0;
|
|
2388
|
+
const close = block.indexOf(marker, wrapper[0].length);
|
|
2389
|
+
if (close < 0) return void 0;
|
|
2390
|
+
const tail = block.slice(close + marker.length);
|
|
2391
|
+
const opener = TASK_REFS_AFTER_TITLE_CLOSE.exec(tail);
|
|
2392
|
+
if (!opener) return void 0;
|
|
2393
|
+
return close + marker.length + opener[0].indexOf("(");
|
|
2244
2394
|
};
|
|
2245
2395
|
const parseRiskMarker = (line) => {
|
|
2246
2396
|
const marker = RISK_MARKER.exec(line);
|
|
@@ -2381,7 +2531,7 @@ const runLedgerValidate = async (inputs) => {
|
|
|
2381
2531
|
});
|
|
2382
2532
|
for (const id of spec.malformedTaskRefs) problems.push({
|
|
2383
2533
|
kind: "malformed-task-refs",
|
|
2384
|
-
message: `${id} carries a "(R<n>"-shaped
|
|
2534
|
+
message: `${id} carries a "(R<n>"-shaped ref list that is not a declaration: a declaration is one comma-separated "(R1, R2)" group that ends a line — no ranges ("R1–R3"), nothing after the closing paren on that line — at the end of the task's first line, right after the title's bold close, or the only group ending a continuation line. Left as-is it reads as a task that references no requirement, and its slice shrinks to itself.`,
|
|
2385
2535
|
subject: id
|
|
2386
2536
|
});
|
|
2387
2537
|
const groups = resolveGroups(spec.groups, address, problems);
|
|
@@ -2402,6 +2552,7 @@ const runLedgerValidate = async (inputs) => {
|
|
|
2402
2552
|
});
|
|
2403
2553
|
}
|
|
2404
2554
|
}
|
|
2555
|
+
checkTicks(spec.groups, address, problems);
|
|
2405
2556
|
const slice = sliceForGroups(groups);
|
|
2406
2557
|
result.basis = slice.basis;
|
|
2407
2558
|
const dangling = await collectDanglingRefs(requirementsPath, slice.ids, problems);
|
|
@@ -2437,6 +2588,17 @@ const resolveGroups = (groups, address, problems) => {
|
|
|
2437
2588
|
message: `tasks.md declares ${groups.length} group(s); step ${address.step} has none. One step is one group.`
|
|
2438
2589
|
});
|
|
2439
2590
|
};
|
|
2591
|
+
const checkTicks = (groups, address, problems) => {
|
|
2592
|
+
const passed = address.kind === "full-pass" ? groups : groups.filter((group) => group.index <= address.step);
|
|
2593
|
+
for (const group of passed) for (const task of group.tasks) {
|
|
2594
|
+
if (task.ticked) continue;
|
|
2595
|
+
problems.push({
|
|
2596
|
+
kind: "unticked-completed-task",
|
|
2597
|
+
message: `${task.id} (Group ${group.index}) is still "[ ]" in tasks.md, in a group the loop has passed. The Implementer ticks a task when it reaches green; an unticked task here is a forgotten mark or work that was never done — the human decides which.`,
|
|
2598
|
+
subject: task.id
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2440
2602
|
const collectDanglingRefs = async (requirementsPath, sliceIds, problems) => {
|
|
2441
2603
|
const referenced = sliceIds.filter((id) => id.startsWith("R"));
|
|
2442
2604
|
if (referenced.length === 0) return [];
|
|
@@ -3039,7 +3201,8 @@ const prunePrefix = async (repoRoot, options = {}) => {
|
|
|
3039
3201
|
}
|
|
3040
3202
|
if (sentOffset > size) return { prunedBytes: 0 };
|
|
3041
3203
|
const stateDir = stateDirOf(repoRoot);
|
|
3042
|
-
const
|
|
3204
|
+
const id = `${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
3205
|
+
const drainPath = join(stateDir, `${DRAINING_BASENAME}.${id}`);
|
|
3043
3206
|
const metaPath = `${drainPath}.meta`;
|
|
3044
3207
|
await mkdir(stateDir, { recursive: true });
|
|
3045
3208
|
await writeFile(metaPath, JSON.stringify({ sent_offset: sentOffset }));
|
|
@@ -3080,14 +3243,15 @@ const appendRejects = async (repoRoot, rejects, now) => {
|
|
|
3080
3243
|
const path = join(repoRoot, REJECTED_RELPATH);
|
|
3081
3244
|
await mkdir(dirname(path), { recursive: true });
|
|
3082
3245
|
const ts = now().toISOString();
|
|
3083
|
-
|
|
3246
|
+
const body = rejects.map((reject) => {
|
|
3084
3247
|
const entry = {
|
|
3085
3248
|
ts,
|
|
3086
3249
|
reason: reject.reason,
|
|
3087
3250
|
line: reject.line
|
|
3088
3251
|
};
|
|
3089
3252
|
return JSON.stringify(entry);
|
|
3090
|
-
}).join("\n")
|
|
3253
|
+
}).join("\n");
|
|
3254
|
+
await appendFile(path, `${body}\n`);
|
|
3091
3255
|
return rejects.length;
|
|
3092
3256
|
};
|
|
3093
3257
|
//#endregion
|
|
@@ -3759,7 +3923,8 @@ const checkVersionPin = (deps, config) => {
|
|
|
3759
3923
|
};
|
|
3760
3924
|
const checkCliResolution = async (deps) => {
|
|
3761
3925
|
const name = "cli-resolution";
|
|
3762
|
-
|
|
3926
|
+
const localBin = join(deps.repoRoot, "node_modules", ".bin", LEMONY_BIN);
|
|
3927
|
+
if (await isExecutable(localBin)) return {
|
|
3763
3928
|
name,
|
|
3764
3929
|
status: "ok",
|
|
3765
3930
|
detail: `Telemetry CLI resolves from node_modules/.bin/${LEMONY_BIN} (the durable setup).`
|
|
@@ -3954,22 +4119,26 @@ const isReadable = async (path) => {
|
|
|
3954
4119
|
}
|
|
3955
4120
|
};
|
|
3956
4121
|
//#endregion
|
|
4122
|
+
//#region src/status/parse-task-branch.ts
|
|
4123
|
+
const TASK_BRANCH = /^harness\/(\d+)-.+$/;
|
|
4124
|
+
const parseTaskBranch = (branch) => branch === null ? null : TASK_BRANCH.exec(branch)?.[1] ?? null;
|
|
4125
|
+
//#endregion
|
|
3957
4126
|
//#region src/status/status.ts
|
|
3958
4127
|
const PAUSED_LABEL = "harness:status:paused-for-clarification";
|
|
3959
4128
|
const runStatus = async (deps) => {
|
|
3960
4129
|
const { repoRoot } = deps;
|
|
3961
4130
|
const config = await readHarnessConfig(repoRoot);
|
|
3962
|
-
const
|
|
4131
|
+
const lastSession = await readPointer(repoRoot, deps.readGitUserEmail);
|
|
3963
4132
|
const { branch, behind } = await deps.gitBehind(repoRoot);
|
|
3964
4133
|
const provider = createTaskTrackerProvider(config.task_storage.type, deps.runCommand);
|
|
3965
4134
|
const openDiscoveries = await countOpenDiscoveries(provider, config.task_storage.repo);
|
|
3966
4135
|
return {
|
|
3967
4136
|
vendorVersion: config.vendor_version,
|
|
3968
4137
|
taskStorageRepo: config.task_storage.repo,
|
|
3969
|
-
activeTask:
|
|
3970
|
-
branch
|
|
4138
|
+
activeTask: parseTaskBranch(branch),
|
|
4139
|
+
branch,
|
|
3971
4140
|
behind,
|
|
3972
|
-
lastSession
|
|
4141
|
+
lastSession,
|
|
3973
4142
|
openDiscoveries,
|
|
3974
4143
|
designToolDrift: await readDriftState(repoRoot)
|
|
3975
4144
|
};
|
|
@@ -3986,11 +4155,7 @@ const readDriftState = async (repoRoot) => {
|
|
|
3986
4155
|
}
|
|
3987
4156
|
};
|
|
3988
4157
|
const readPointer = async (repoRoot, readGitUserEmail) => {
|
|
3989
|
-
const empty =
|
|
3990
|
-
activeTask: null,
|
|
3991
|
-
branch: null,
|
|
3992
|
-
lastSession: null
|
|
3993
|
-
};
|
|
4158
|
+
const empty = null;
|
|
3994
4159
|
const email = await readGitUserEmail(repoRoot);
|
|
3995
4160
|
if (!email) return empty;
|
|
3996
4161
|
const slug = email.split("@")[0] ?? "";
|
|
@@ -4006,12 +4171,7 @@ const readPointer = async (repoRoot, readGitUserEmail) => {
|
|
|
4006
4171
|
if (!match) return empty;
|
|
4007
4172
|
const parsed = pointerFrontmatterSchema.safeParse(parse(match[1] ?? ""));
|
|
4008
4173
|
if (!parsed.success) return empty;
|
|
4009
|
-
|
|
4010
|
-
return {
|
|
4011
|
-
activeTask: normalize(front.active_task),
|
|
4012
|
-
branch: normalize(front.branch),
|
|
4013
|
-
lastSession: normalize(front.last_close_ts)
|
|
4014
|
-
};
|
|
4174
|
+
return normalize(parsed.data.last_close_ts);
|
|
4015
4175
|
};
|
|
4016
4176
|
const normalize = (value) => {
|
|
4017
4177
|
if (value === null || value === void 0) return null;
|
|
@@ -4083,7 +4243,7 @@ const COMMANDS = [
|
|
|
4083
4243
|
},
|
|
4084
4244
|
{
|
|
4085
4245
|
name: "review-ledger",
|
|
4086
|
-
summary: "Validate the Reviewer's evidence ledger (the JSON sidecar under the task's state) against the spec's groups, the anchored diff and the declared gates: shape, criteria-vs-slice, unknown risk classes, the mutant floor, the gates floor (deterministic, agent-free; exits non-zero on an invalid ledger).",
|
|
4246
|
+
summary: "Validate the Reviewer's evidence ledger (the JSON sidecar under the task's state) against the spec's groups, the anchored diff and the declared gates: shape, criteria-vs-slice, unknown risk classes, the mutant floor, the gates floor, the tick check on passed groups (deterministic, agent-free; exits non-zero on an invalid ledger).",
|
|
4087
4247
|
usage: "lemony review-ledger validate --task-id=<id> --anchor=<oid> (--step=<N> | --full-pass)"
|
|
4088
4248
|
},
|
|
4089
4249
|
{
|
|
@@ -4236,7 +4396,8 @@ const snapshotDestPaths = (version, bundle) => {
|
|
|
4236
4396
|
];
|
|
4237
4397
|
};
|
|
4238
4398
|
const writeSnapshot = async (repoRoot, version, bundle) => {
|
|
4239
|
-
const
|
|
4399
|
+
const root = snapshotsRootDir(repoRoot);
|
|
4400
|
+
const staging = join(root, STAGING_DIR);
|
|
4240
4401
|
await assertNoSymlinkTraversal(repoRoot, snapshotDestPaths(version, bundle));
|
|
4241
4402
|
await rm(staging, {
|
|
4242
4403
|
recursive: true,
|
|
@@ -4595,9 +4756,10 @@ const COMPANION_DOCS = [
|
|
|
4595
4756
|
"partition"
|
|
4596
4757
|
];
|
|
4597
4758
|
const renderFile = async (templatePath, relPath, vars) => {
|
|
4759
|
+
const template = await readFile(templatePath, "utf8");
|
|
4598
4760
|
return {
|
|
4599
4761
|
relPath,
|
|
4600
|
-
content: renderTemplate(
|
|
4762
|
+
content: renderTemplate(template, vars),
|
|
4601
4763
|
executable: false
|
|
4602
4764
|
};
|
|
4603
4765
|
};
|
|
@@ -4673,7 +4835,8 @@ const appendGitignoreBlock = async (repoRoot) => {
|
|
|
4673
4835
|
await writeFile(gitignorePath, next);
|
|
4674
4836
|
return ".gitignore";
|
|
4675
4837
|
}
|
|
4676
|
-
|
|
4838
|
+
const prefix = current.length === 0 ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
4839
|
+
await writeFile(gitignorePath, `${current}${prefix}${GITIGNORE_BLOCK}\n`);
|
|
4677
4840
|
return ".gitignore";
|
|
4678
4841
|
};
|
|
4679
4842
|
const removeGitignoreBlock = async (repoRoot) => {
|
|
@@ -4834,9 +4997,10 @@ const runInstall = async (options) => {
|
|
|
4834
4997
|
const losers = await collectLosers(repoRoot, actions);
|
|
4835
4998
|
let snapshotVersion = null;
|
|
4836
4999
|
if (losers.length > 0) {
|
|
5000
|
+
const baseline = new Map(vendorFiles.map((file) => [file.relPath, file.content]));
|
|
4837
5001
|
await writeSnapshot(repoRoot, vendorVersion, {
|
|
4838
5002
|
working: losers,
|
|
4839
|
-
baseline
|
|
5003
|
+
baseline
|
|
4840
5004
|
});
|
|
4841
5005
|
snapshotVersion = vendorVersion;
|
|
4842
5006
|
}
|
|
@@ -4893,7 +5057,8 @@ const runInstall = async (options) => {
|
|
|
4893
5057
|
};
|
|
4894
5058
|
};
|
|
4895
5059
|
const collectLosers = (repoRoot, actions) => {
|
|
4896
|
-
|
|
5060
|
+
const losingPaths = actions.filter((action) => action.kind === "pick-vendor").map((action) => action.relPath);
|
|
5061
|
+
return collectWorkingFiles(repoRoot, losingPaths);
|
|
4897
5062
|
};
|
|
4898
5063
|
//#endregion
|
|
4899
5064
|
//#region src/install/resolve-collision.ts
|
|
@@ -4932,12 +5097,14 @@ const runReconcile = async (inputs) => {
|
|
|
4932
5097
|
const fromVersion = config.vendor_version;
|
|
4933
5098
|
const target = config.target;
|
|
4934
5099
|
const taskStorageRepo = config.task_storage.repo;
|
|
4935
|
-
const
|
|
5100
|
+
const rawConfig = await readFile(join(repoRoot, HARNESS_CONFIG_FILENAME), "utf8");
|
|
5101
|
+
const predatedKeys = findPredatedKeys(rawConfig, fromVersion, toVersion, inputs.configKeySince ?? CONFIG_KEY_SINCE);
|
|
4936
5102
|
const baselineVersion = await findBaselineVersion(repoRoot);
|
|
4937
5103
|
const baseline = baselineVersion ? await readBaseline(repoRoot, baselineVersion) : /* @__PURE__ */ new Map();
|
|
4938
5104
|
const hadBaseline = baseline.size > 0;
|
|
4939
5105
|
const capabilities = await scanRepo(repoRoot);
|
|
4940
|
-
const
|
|
5106
|
+
const catalogSkills = await readCatalogSkills(vendorRoot);
|
|
5107
|
+
const skills = selectSkills(catalogSkills, capabilities);
|
|
4941
5108
|
const vendorNew = await materializeVendorFiles({
|
|
4942
5109
|
vendorRoot,
|
|
4943
5110
|
target,
|
|
@@ -4973,14 +5140,16 @@ const runReconcile = async (inputs) => {
|
|
|
4973
5140
|
predatedKeys
|
|
4974
5141
|
};
|
|
4975
5142
|
if (hadBaseline) {
|
|
5143
|
+
const working = await collectWorkingFiles(repoRoot, [...baseline.keys()]);
|
|
4976
5144
|
await writeSnapshot(repoRoot, fromVersion, {
|
|
4977
|
-
working
|
|
5145
|
+
working,
|
|
4978
5146
|
baseline
|
|
4979
5147
|
});
|
|
4980
5148
|
await rotateSnapshots(repoRoot, config.rollback.keep_snapshots);
|
|
4981
5149
|
}
|
|
4982
5150
|
await applyActions(repoRoot, actions);
|
|
4983
|
-
const
|
|
5151
|
+
const templateRoot = join(vendorRoot, "templates", target);
|
|
5152
|
+
const { lostHookCommands } = await resyncSpecialCased(repoRoot, templateRoot);
|
|
4984
5153
|
const labelSync = inputs.runCommand ? await syncLabels(taskStorageRepo, createTaskTrackerProvider(config.task_storage.type, inputs.runCommand)) : null;
|
|
4985
5154
|
await writeBaseline(repoRoot, toVersion, vendorNew);
|
|
4986
5155
|
await writeConfigValues(repoRoot, configBumps);
|
|
@@ -5014,7 +5183,8 @@ const applyActions = async (repoRoot, actions) => {
|
|
|
5014
5183
|
default: return;
|
|
5015
5184
|
}
|
|
5016
5185
|
}));
|
|
5017
|
-
|
|
5186
|
+
const prunedPaths = actions.filter((action) => action.kind === "prune").map((action) => action.relPath);
|
|
5187
|
+
await pruneEmptyDirs(repoRoot, prunedPaths);
|
|
5018
5188
|
};
|
|
5019
5189
|
const summarize = (actions) => {
|
|
5020
5190
|
const conflictedFiles = [];
|
|
@@ -5044,12 +5214,10 @@ const summarize = (actions) => {
|
|
|
5044
5214
|
winner: "vendor"
|
|
5045
5215
|
});
|
|
5046
5216
|
break;
|
|
5047
|
-
case "pick-client":
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
});
|
|
5052
|
-
break;
|
|
5217
|
+
case "pick-client": pickedFiles.push({
|
|
5218
|
+
relPath: action.relPath,
|
|
5219
|
+
winner: "client"
|
|
5220
|
+
});
|
|
5053
5221
|
}
|
|
5054
5222
|
return {
|
|
5055
5223
|
conflictedFiles,
|
|
@@ -5155,11 +5323,12 @@ const runRollback = async (options) => {
|
|
|
5155
5323
|
await assertNoSymlinkTraversal(repoRoot, [...restorePaths, ...removePaths]);
|
|
5156
5324
|
await Promise.all(bundle.working.map((file) => writeManaged(repoRoot, file.relPath, file.content, file.executable)));
|
|
5157
5325
|
await Promise.all(removePaths.map((path) => rm(join(repoRoot, path), { force: true })));
|
|
5158
|
-
|
|
5326
|
+
const baselineFiles = [...bundle.baseline].map(([relPath, content]) => ({
|
|
5159
5327
|
relPath,
|
|
5160
5328
|
content,
|
|
5161
5329
|
executable: false
|
|
5162
|
-
}))
|
|
5330
|
+
}));
|
|
5331
|
+
await writeBaseline(repoRoot, target, baselineFiles);
|
|
5163
5332
|
await writeConfigValues(repoRoot, { vendor_version: target });
|
|
5164
5333
|
return {
|
|
5165
5334
|
repoRoot,
|
|
@@ -5355,6 +5524,31 @@ const makeRunCommand = () => async (cmd, args) => {
|
|
|
5355
5524
|
};
|
|
5356
5525
|
}
|
|
5357
5526
|
};
|
|
5527
|
+
const makeVerifierRunCommand = () => (cmd, args) => new Promise((resolveResult) => {
|
|
5528
|
+
const child = spawn(cmd, args, { stdio: [
|
|
5529
|
+
"ignore",
|
|
5530
|
+
"pipe",
|
|
5531
|
+
"pipe"
|
|
5532
|
+
] });
|
|
5533
|
+
const out = [];
|
|
5534
|
+
const err = [];
|
|
5535
|
+
child.stdout.on("data", (chunk) => out.push(chunk));
|
|
5536
|
+
child.stderr.on("data", (chunk) => err.push(chunk));
|
|
5537
|
+
child.on("error", (error) => {
|
|
5538
|
+
resolveResult({
|
|
5539
|
+
code: error.code === "ENOENT" ? 127 : 1,
|
|
5540
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
5541
|
+
stderr: `${Buffer.concat(err).toString("utf8")}${error.message}\n`
|
|
5542
|
+
});
|
|
5543
|
+
});
|
|
5544
|
+
child.on("close", (code, signal) => {
|
|
5545
|
+
resolveResult({
|
|
5546
|
+
code: code ?? 1,
|
|
5547
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
5548
|
+
stderr: `${Buffer.concat(err).toString("utf8")}${signal ? `killed by ${signal}\n` : ""}`
|
|
5549
|
+
});
|
|
5550
|
+
});
|
|
5551
|
+
});
|
|
5358
5552
|
const buildResolveDeps = () => {
|
|
5359
5553
|
const rl = createInterface({
|
|
5360
5554
|
input: stdin,
|
|
@@ -5379,10 +5573,10 @@ const gitBehind = async (repoRoot) => {
|
|
|
5379
5573
|
repoRoot,
|
|
5380
5574
|
"symbolic-ref",
|
|
5381
5575
|
"--quiet",
|
|
5382
|
-
"--short",
|
|
5383
5576
|
"HEAD"
|
|
5384
5577
|
]);
|
|
5385
|
-
const
|
|
5578
|
+
const ref = branchResult.code === 0 ? branchResult.stdout.trim() : "";
|
|
5579
|
+
const branch = ref.startsWith("refs/heads/") ? ref.slice(11) || null : null;
|
|
5386
5580
|
const headRef = await run("git", [
|
|
5387
5581
|
"-C",
|
|
5388
5582
|
repoRoot,
|
|
@@ -5700,16 +5894,18 @@ const telemetryEnable = async (repoRoot) => {
|
|
|
5700
5894
|
for (const line of formatTelemetryStatus(report)) console.log(line);
|
|
5701
5895
|
};
|
|
5702
5896
|
const telemetrySend = async (repoRoot) => {
|
|
5897
|
+
const endpoint = env["LEMONY_TELEMETRY_ENDPOINT"] ?? "https://lemony-telemetry.lemoncode.workers.dev";
|
|
5703
5898
|
const result = await sendTelemetry({
|
|
5704
5899
|
repoRoot,
|
|
5705
|
-
endpoint
|
|
5900
|
+
endpoint
|
|
5706
5901
|
});
|
|
5707
5902
|
console.log(`telemetry ${result.outcome} (${result.segmentsSent} sent, ${result.quarantined} quarantined)`);
|
|
5708
5903
|
};
|
|
5709
5904
|
const telemetryFlush = async (repoRoot) => {
|
|
5905
|
+
const endpoint = env["LEMONY_TELEMETRY_ENDPOINT"] ?? "https://lemony-telemetry.lemoncode.workers.dev";
|
|
5710
5906
|
const result = await sendTelemetry({
|
|
5711
5907
|
repoRoot,
|
|
5712
|
-
endpoint
|
|
5908
|
+
endpoint
|
|
5713
5909
|
});
|
|
5714
5910
|
for (const line of formatTelemetryFlush(result)) console.log(line);
|
|
5715
5911
|
};
|
|
@@ -5764,20 +5960,41 @@ const designTokensValidate = async (args) => {
|
|
|
5764
5960
|
exit(1);
|
|
5765
5961
|
};
|
|
5766
5962
|
const designTokensContrast = async () => {
|
|
5767
|
-
const
|
|
5963
|
+
const verify = (await pathExists(join(cwd(), "harness.config.yml")) ? await readHarnessConfig(cwd()) : void 0)?.design_tokens.verify;
|
|
5964
|
+
const result = await runContrast({
|
|
5965
|
+
repoRoot: cwd(),
|
|
5966
|
+
...verify === void 0 ? {} : { verify: {
|
|
5967
|
+
command: verify,
|
|
5968
|
+
run: makeVerifierRunCommand()
|
|
5969
|
+
} }
|
|
5970
|
+
});
|
|
5768
5971
|
if (!result.tokensFound) {
|
|
5769
5972
|
console.log(`No docs/design-tokens.json — design-tokens contrast skipped (consume-if-exists; the harness never creates it).`);
|
|
5770
5973
|
return;
|
|
5771
5974
|
}
|
|
5772
5975
|
const failures = result.pairs.filter((pair) => !pair.passes);
|
|
5976
|
+
const forwardVerifier = () => {
|
|
5977
|
+
if (result.verify === void 0) return;
|
|
5978
|
+
const { stdout: out, stderr: err } = result.verify;
|
|
5979
|
+
if (out.length > 0) stdout.write(out.endsWith("\n") ? out : `${out}\n`);
|
|
5980
|
+
if (err.length > 0) stderr.write(err.endsWith("\n") ? err : `${err}\n`);
|
|
5981
|
+
};
|
|
5773
5982
|
if (result.ok) {
|
|
5774
5983
|
console.log(result.pairsChecked === 0 ? `design-tokens contrast: no foreground/background pairs found (use the on-* convention or $extensions["com.lemony.contrast"]).` : `design-tokens contrast OK: ${result.pairsChecked} pair(s) meet their WCAG floor.`);
|
|
5984
|
+
forwardVerifier();
|
|
5985
|
+
if (result.verify !== void 0) console.log(`design-tokens verify OK: \`${result.verify.command}\` exited 0.`);
|
|
5775
5986
|
return;
|
|
5776
5987
|
}
|
|
5777
|
-
|
|
5778
|
-
|
|
5988
|
+
const failedVerifier = result.verify !== void 0 && result.verify.code !== 0 ? result.verify : void 0;
|
|
5989
|
+
console.error(`design-tokens contrast found ${failures.length + result.problems.length + (failedVerifier ? 1 : 0)} issue(s):`);
|
|
5990
|
+
for (const pair of failures) {
|
|
5991
|
+
const over = pair.over === void 0 ? "" : ` over ${pair.over}`;
|
|
5992
|
+
console.error(` ${pair.foreground} on ${pair.background}${over} (${pair.mode}, ${pair.level}): ${pair.ratio}:1 < ${pair.floor}:1`);
|
|
5993
|
+
}
|
|
5779
5994
|
for (const problem of result.problems) console.error(` ${problem}`);
|
|
5780
|
-
|
|
5995
|
+
forwardVerifier();
|
|
5996
|
+
if (failedVerifier) console.error(` design_tokens.verify \`${failedVerifier.command}\` exited ${failedVerifier.code}.`);
|
|
5997
|
+
process.exitCode = 1;
|
|
5781
5998
|
};
|
|
5782
5999
|
const designTokensImport = async (args) => {
|
|
5783
6000
|
const from = parseFlag(args, "from");
|