@lemoncode/lemony 0.1.1 → 0.1.2
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/hooks/init.sh +0 -0
- package/catalog/hooks/lib/lemony.sh +0 -0
- package/catalog/hooks/lib/playbook-scan.sh +60 -1
- package/catalog/hooks/lib/transcript-grep.sh +0 -0
- package/catalog/hooks/require-playbook.sh +0 -0
- package/catalog/hooks/session-close.sh +0 -0
- package/catalog/hooks/suggest-playbook.sh +0 -0
- package/dist/cli.mjs +19 -8
- package/package.json +22 -27
package/catalog/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.2
|
package/catalog/hooks/init.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -324,6 +324,50 @@ glob_to_regex() {
|
|
|
324
324
|
GLOB_RE="^${re}$"
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
# Normalize an absolute payload file path to a repo-relative one, assigning the
|
|
328
|
+
# result to the global REPO_RELATIVE_PATH. `applies_to` globs are repo-relative
|
|
329
|
+
# (what the frontmatter documents and every shipped playbook assumes), but Claude
|
|
330
|
+
# Code sends `file_path` ABSOLUTE — so without this every glob that does not start
|
|
331
|
+
# with `**/` can never match and the playbook it guards is silently inert: the
|
|
332
|
+
# `**/` idiom compiles to a leading `(.*/)?` that happens to swallow the whole
|
|
333
|
+
# absolute prefix, hiding the problem behind the one glob shape that appears to work.
|
|
334
|
+
#
|
|
335
|
+
# Fast path: a fork-free textual prefix strip, correct whenever `pwd` (repo_root)
|
|
336
|
+
# and the payload path agree on symlink resolution — the overwhelmingly common
|
|
337
|
+
# case (repos live under an unsymlinked $HOME). Fallback (only when the textual
|
|
338
|
+
# strip finds no prefix): the two producers may disagree on symlink resolution
|
|
339
|
+
# (macOS /tmp → /private/tmp; repo_root is `$(pwd)`, filepath is the tool
|
|
340
|
+
# payload), so resolve both sides with `pwd -P` — bash-3.2-safe, no `realpath`
|
|
341
|
+
# dependency — and retry. A path genuinely outside the repo keeps its absolute
|
|
342
|
+
# form; callers MUST treat a still-absolute result as "outside the repo" and skip
|
|
343
|
+
# matching, because an absolute path does not reliably fail to match a
|
|
344
|
+
# repo-relative glob: a leading `**/` compiles to an optional `(.*/)?` that
|
|
345
|
+
# swallows the whole absolute prefix, so `**/src/**/*.ts` would otherwise match
|
|
346
|
+
# `/some/other/repo/src/app.ts` and gate a write in an unrelated repository.
|
|
347
|
+
_relativize_to_repo() {
|
|
348
|
+
local repo_root="$1"
|
|
349
|
+
local filepath="$2"
|
|
350
|
+
|
|
351
|
+
# Fast path: quoted prefix forces a literal (non-glob) strip.
|
|
352
|
+
REPO_RELATIVE_PATH="${filepath#"$repo_root"/}"
|
|
353
|
+
[ "$REPO_RELATIVE_PATH" != "$filepath" ] && return 0
|
|
354
|
+
|
|
355
|
+
# Fallback: resolve symlinks on both sides and retry. `cd` may fail for a
|
|
356
|
+
# not-yet-created parent dir on a new-file Write, or a path outside the repo —
|
|
357
|
+
# in either case fall through to keeping the original absolute path.
|
|
358
|
+
local dir="${filepath%/*}" base="${filepath##*/}" real_root real_dir
|
|
359
|
+
real_root="$(cd "$repo_root" 2>/dev/null && pwd -P)" || real_root=""
|
|
360
|
+
real_dir="$(cd "$dir" 2>/dev/null && pwd -P)" || real_dir=""
|
|
361
|
+
if [ -n "$real_root" ] && [ -n "$real_dir" ]; then
|
|
362
|
+
local real_path="$real_dir/$base"
|
|
363
|
+
REPO_RELATIVE_PATH="${real_path#"$real_root"/}"
|
|
364
|
+
[ "$REPO_RELATIVE_PATH" != "$real_path" ] && return 0
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
# Genuinely outside the repo (or unresolvable): keep the original path.
|
|
368
|
+
REPO_RELATIVE_PATH="$filepath"
|
|
369
|
+
}
|
|
370
|
+
|
|
327
371
|
# Populate MATCHED_PLAYBOOKS with playbooks whose `applies_to` list contains a
|
|
328
372
|
# glob that matches the given file path. One awk fork reads every playbook's
|
|
329
373
|
# frontmatter; glob→regex translation and matching are in-process (no fork).
|
|
@@ -338,6 +382,21 @@ playbook_scan_for_path() {
|
|
|
338
382
|
# empty under `set -u`, even though arr was explicitly initialized to `()`.
|
|
339
383
|
[ ${#ALL_PLAYBOOKS[@]} -gt 0 ] || return 0
|
|
340
384
|
|
|
385
|
+
# Match repo-relative globs against a repo-relative path, not the absolute
|
|
386
|
+
# payload path. One-time normalization — the fast path is fork-free.
|
|
387
|
+
_relativize_to_repo "$repo_root" "$filepath"
|
|
388
|
+
local match_path="$REPO_RELATIVE_PATH"
|
|
389
|
+
|
|
390
|
+
# Still absolute => the target lives outside the repo root, so no
|
|
391
|
+
# repo-relative glob applies to it. Bail BEFORE the match loop rather than
|
|
392
|
+
# relying on the globs not to match: a leading `**/` becomes an optional
|
|
393
|
+
# `(.*/)?` which swallows any absolute prefix, so `**/src/**/*.ts` matches
|
|
394
|
+
# `/elsewhere/src/app.ts` and the hook blocks a write in another repository
|
|
395
|
+
# until this repo's playbooks are read. Enforcement stops at the repo edge.
|
|
396
|
+
case "$match_path" in
|
|
397
|
+
/*) return 0 ;;
|
|
398
|
+
esac
|
|
399
|
+
|
|
341
400
|
local file field value seen="|"
|
|
342
401
|
GLOB_RE=""
|
|
343
402
|
while IFS=$'\t' read -r file field value; do
|
|
@@ -347,7 +406,7 @@ playbook_scan_for_path() {
|
|
|
347
406
|
# loop `break`ed on first match) — track which files already matched.
|
|
348
407
|
case "$seen" in *"|${file}|"*) continue ;; esac
|
|
349
408
|
glob_to_regex "$value"
|
|
350
|
-
if [[ "$
|
|
409
|
+
if [[ "$match_path" =~ $GLOB_RE ]]; then
|
|
351
410
|
MATCHED_PLAYBOOKS+=("$file")
|
|
352
411
|
seen="${seen}${file}|"
|
|
353
412
|
fi
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/dist/cli.mjs
CHANGED
|
@@ -45,7 +45,8 @@ const formatIssue = (schema, issue) => {
|
|
|
45
45
|
const valid = validKeysAt(schema, issue.path);
|
|
46
46
|
return issue.keys.map((key) => {
|
|
47
47
|
const guess = valid ? nearestKey(key, valid) : null;
|
|
48
|
-
|
|
48
|
+
const hint = guess ? ` Did you mean "${guess}"?` : "";
|
|
49
|
+
return `Unknown key "${key}" in ${where}.${hint}`;
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
if (issue.code === "invalid_format" && issue.format === "regex") {
|
|
@@ -206,7 +207,8 @@ const setConfigValues = (rawYaml, updates) => {
|
|
|
206
207
|
};
|
|
207
208
|
const writeConfigValues = async (repoRoot, updates) => {
|
|
208
209
|
const configPath = join(repoRoot, HARNESS_CONFIG_FILENAME);
|
|
209
|
-
|
|
210
|
+
const raw = await readFile(configPath, "utf8");
|
|
211
|
+
await writeFile(configPath, setConfigValues(raw, updates));
|
|
210
212
|
};
|
|
211
213
|
//#endregion
|
|
212
214
|
//#region src/config/pointer.schema.ts
|
|
@@ -1079,7 +1081,8 @@ const runValidate = async (inputs) => {
|
|
|
1079
1081
|
filesScanned: 0,
|
|
1080
1082
|
violations: []
|
|
1081
1083
|
};
|
|
1082
|
-
const
|
|
1084
|
+
const raw = await readFile(tokenPath, "utf8");
|
|
1085
|
+
const fileCheck = validateTokenFile(raw);
|
|
1083
1086
|
if (!fileCheck.ok) return {
|
|
1084
1087
|
ok: false,
|
|
1085
1088
|
tokensFound: true,
|
|
@@ -1410,7 +1413,8 @@ const linearizeChannel = (channel) => {
|
|
|
1410
1413
|
const relativeLuminance = ({ r, g, b }) => .2126 * linearizeChannel(r) + .7152 * linearizeChannel(g) + .0722 * linearizeChannel(b);
|
|
1411
1414
|
const contrastRatio = (foreground, background) => {
|
|
1412
1415
|
const bg = background.a < 1 ? flatten(background, WHITE) : background;
|
|
1413
|
-
const
|
|
1416
|
+
const fg = foreground.a < 1 ? flatten(foreground, bg) : foreground;
|
|
1417
|
+
const l1 = relativeLuminance(fg);
|
|
1414
1418
|
const l2 = relativeLuminance(bg);
|
|
1415
1419
|
const light = Math.max(l1, l2);
|
|
1416
1420
|
const dark = Math.min(l1, l2);
|
|
@@ -1457,7 +1461,9 @@ const runContrast = async (inputs) => {
|
|
|
1457
1461
|
};
|
|
1458
1462
|
const tokens = collectTokens(parsed);
|
|
1459
1463
|
const problems = [];
|
|
1460
|
-
const
|
|
1464
|
+
const specs = discoverPairs(tokens, problems);
|
|
1465
|
+
const modes = declaredModes(tokens);
|
|
1466
|
+
const pairs = measurePairs(specs, modes, tokens);
|
|
1461
1467
|
return {
|
|
1462
1468
|
ok: problems.length === 0 && pairs.every((pair) => pair.passes),
|
|
1463
1469
|
tokensFound: true,
|
|
@@ -3303,7 +3309,8 @@ const runStatus = async (deps) => {
|
|
|
3303
3309
|
const config = await readHarnessConfig(repoRoot);
|
|
3304
3310
|
const pointer = await readPointer(repoRoot, deps.readGitUserEmail);
|
|
3305
3311
|
const { branch, behind } = await deps.gitBehind(repoRoot);
|
|
3306
|
-
const
|
|
3312
|
+
const provider = createTaskTrackerProvider(config.task_storage.type, deps.runCommand);
|
|
3313
|
+
const openDiscoveries = await countOpenDiscoveries(provider, config.task_storage.repo);
|
|
3307
3314
|
return {
|
|
3308
3315
|
vendorVersion: config.vendor_version,
|
|
3309
3316
|
taskStorageRepo: config.task_storage.repo,
|
|
@@ -3659,7 +3666,10 @@ const hasConflictMarkers = (content) => CONFLICT_FENCE.test(content);
|
|
|
3659
3666
|
//#endregion
|
|
3660
3667
|
//#region src/merge/three-way-merge.ts
|
|
3661
3668
|
const threeWayMerge = (base, client, vendor) => {
|
|
3662
|
-
const
|
|
3669
|
+
const baseLines = base.split("\n");
|
|
3670
|
+
const clientLines = client.split("\n");
|
|
3671
|
+
const vendorLines = vendor.split("\n");
|
|
3672
|
+
const regions = diff3Regions(baseLines, clientLines, vendorLines);
|
|
3663
3673
|
const out = [];
|
|
3664
3674
|
let conflicted = false;
|
|
3665
3675
|
for (const region of regions) {
|
|
@@ -4824,12 +4834,13 @@ const update = async (args) => {
|
|
|
4824
4834
|
const repoRoot = cwd();
|
|
4825
4835
|
const onConflict = parseOnConflict(args);
|
|
4826
4836
|
const dryRun = args.includes("--dry-run");
|
|
4837
|
+
const allowDowngrade = args.includes("--allow-downgrade");
|
|
4827
4838
|
const result = await runUpdate({
|
|
4828
4839
|
repoRoot,
|
|
4829
4840
|
vendorRoot: VENDOR_ROOT,
|
|
4830
4841
|
onConflict,
|
|
4831
4842
|
dryRun,
|
|
4832
|
-
allowDowngrade
|
|
4843
|
+
allowDowngrade,
|
|
4833
4844
|
runCommand: makeRunCommand()
|
|
4834
4845
|
});
|
|
4835
4846
|
const verb = (applied, preview) => dryRun ? preview : applied;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lemoncode/lemony",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Lemony — a Harness for AI Coding. Vendor package: installer, agent role catalog, generic skill catalog, hooks, and templates for a Spec-Driven Development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=24",
|
|
31
|
-
"
|
|
31
|
+
"pnpm": ">=11.5.1"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"registry": "https://registry.npmjs.org",
|
|
@@ -43,6 +43,24 @@
|
|
|
43
43
|
"agents",
|
|
44
44
|
"scaffolding"
|
|
45
45
|
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@changesets/cli": "2.31.1",
|
|
48
|
+
"@stryker-mutator/core": "9.6.1",
|
|
49
|
+
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
50
|
+
"@types/node": "24.13.3",
|
|
51
|
+
"husky": "9.1.7",
|
|
52
|
+
"lint-staged": "17.2.0",
|
|
53
|
+
"oxlint": "1.75.0",
|
|
54
|
+
"prettier": "3.9.6",
|
|
55
|
+
"tsdown": "0.22.14",
|
|
56
|
+
"tsx": "4.23.1",
|
|
57
|
+
"typescript": "7.0.2",
|
|
58
|
+
"vitest": "4.1.10"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"yaml": "2.9.0",
|
|
62
|
+
"zod": "4.4.3"
|
|
63
|
+
},
|
|
46
64
|
"scripts": {
|
|
47
65
|
"build": "tsdown",
|
|
48
66
|
"check-types": "tsc --noEmit",
|
|
@@ -60,30 +78,7 @@
|
|
|
60
78
|
"notice:check": "tsx scripts/generate-notice.ts --check",
|
|
61
79
|
"telemetry:aggregate": "tsx scripts/aggregate-telemetry.ts",
|
|
62
80
|
"changeset": "changeset",
|
|
63
|
-
"changeset:version": "changeset version && tsx scripts/sync-catalog-version.ts",
|
|
81
|
+
"changeset:version": "changeset version && pnpm install --lockfile-only && tsx scripts/sync-catalog-version.ts",
|
|
64
82
|
"changeset:publish": "node --run build && changeset publish"
|
|
65
|
-
},
|
|
66
|
-
"devDependencies": {
|
|
67
|
-
"@changesets/cli": "2.31.0",
|
|
68
|
-
"@stryker-mutator/core": "9.6.1",
|
|
69
|
-
"@stryker-mutator/vitest-runner": "9.6.1",
|
|
70
|
-
"@types/node": "24.13.2",
|
|
71
|
-
"oxlint": "1.71.0",
|
|
72
|
-
"prettier": "3.9.1",
|
|
73
|
-
"tsdown": "0.22.3",
|
|
74
|
-
"tsx": "4.22.4",
|
|
75
|
-
"typescript": "6.0.3",
|
|
76
|
-
"vitest": "4.1.9"
|
|
77
|
-
},
|
|
78
|
-
"dependencies": {
|
|
79
|
-
"yaml": "2.9.0",
|
|
80
|
-
"zod": "4.4.3"
|
|
81
|
-
},
|
|
82
|
-
"overrides": {
|
|
83
|
-
"typed-rest-client": {
|
|
84
|
-
"qs": "6.15.2"
|
|
85
|
-
},
|
|
86
|
-
"js-yaml": "4.2.0",
|
|
87
|
-
"vite": "8.0.16"
|
|
88
83
|
}
|
|
89
|
-
}
|
|
84
|
+
}
|