@lemoncode/lemony 0.1.1-alpha.0 → 0.1.1-alpha.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/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import { delimiter, dirname, extname, join, relative, resolve, sep } from "node:
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 { parse, parseDocument } from "yaml";
8
+ import { isMap, 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";
@@ -17,27 +17,20 @@ const appendEvent = async (eventsPath, event) => {
17
17
  //#endregion
18
18
  //#region src/config/config.constant.ts
19
19
  const HARNESS_CONFIG_FILENAME = "harness.config.yml";
20
- const DEPRECATED_CONFIG_KEYS = ["profile"];
20
+ const DEPRECATED_CONFIG_KEYS = ["profile", "agents"];
21
+ const DEPRECATED_PATHS_KEYS = [
22
+ "state",
23
+ "skills",
24
+ "agents"
25
+ ];
21
26
  const HARNESS_CONFIG_SCHEMA_FILENAME = "harness.config.schema.json";
22
27
  const TASK_STORAGE_REPO_PLACEHOLDER = "OWNER/REPO";
23
28
  const TARGETS = ["claude-code"];
24
29
  const TASK_STORAGE_TYPES = ["github"];
25
30
  const TASK_STORAGE_REPO_PATTERN = /^[^\s/]+\/[^\s/]+$/;
26
- const AGENTS = [
27
- "orchestrator",
28
- "spec-author",
29
- "implementer",
30
- "reviewer",
31
- "architect",
32
- "ui-designer"
33
- ];
34
- const REQUIRED_AGENT = "orchestrator";
35
31
  const VENDOR_VERSION_REGEX = /^\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?$/;
36
32
  const VENDOR_VERSION_EXAMPLE = "0.1.0-alpha.0";
37
33
  const PATHS_DEFAULTS = {
38
- state: ".claude/state",
39
- skills: ".claude/skills",
40
- agents: ".claude/agents",
41
34
  playbooks: "docs/playbooks",
42
35
  playbooks_global: "~/.claude/playbooks"
43
36
  };
@@ -106,9 +99,6 @@ const levenshtein = (a, b) => {
106
99
  //#endregion
107
100
  //#region src/config/config.schema.ts
108
101
  const pathsSchema = z.object({
109
- state: z.string().default(PATHS_DEFAULTS.state),
110
- skills: z.string().default(PATHS_DEFAULTS.skills),
111
- agents: z.string().default(PATHS_DEFAULTS.agents),
112
102
  playbooks: z.string().default(PATHS_DEFAULTS.playbooks),
113
103
  playbooks_global: z.string().default(PATHS_DEFAULTS.playbooks_global)
114
104
  }).strict().prefault({});
@@ -123,7 +113,6 @@ const harnessConfigSchema = z.object({
123
113
  vendor_version: z.string().regex(VENDOR_VERSION_REGEX),
124
114
  target: z.enum(TARGETS),
125
115
  task_storage: taskStorageSchema,
126
- agents: z.array(z.enum(AGENTS)).refine((agents) => agents.includes(REQUIRED_AGENT), { error: `agents must include "${REQUIRED_AGENT}" (the orchestrator is the hat).` }),
127
116
  paths: pathsSchema,
128
117
  rollback: rollbackSchema,
129
118
  telemetry: telemetrySchema,
@@ -149,6 +138,12 @@ const readHarnessConfig = async (repoRoot) => {
149
138
  if (!parsed || typeof parsed !== "object") throw new Error(`${HARNESS_CONFIG_FILENAME} is empty or malformed.`);
150
139
  const candidate = { ...parsed };
151
140
  for (const key of DEPRECATED_CONFIG_KEYS) delete candidate[key];
141
+ const paths = candidate.paths;
142
+ if (paths && typeof paths === "object" && !Array.isArray(paths)) {
143
+ const pathsCandidate = { ...paths };
144
+ for (const key of DEPRECATED_PATHS_KEYS) delete pathsCandidate[key];
145
+ candidate.paths = pathsCandidate;
146
+ }
152
147
  const result = harnessConfigSchema.safeParse(candidate);
153
148
  if (!result.success) throw new Error(`${HARNESS_CONFIG_FILENAME} is invalid:\n${formatConfigError(harnessConfigSchema, result.error)}`);
154
149
  return result.data;
@@ -204,6 +199,9 @@ const setConfigValues = (rawYaml, updates) => {
204
199
  const doc = parseDocument(rawYaml);
205
200
  for (const [key, value] of Object.entries(updates)) if (doc.has(key)) doc.set(key, value);
206
201
  for (const key of DEPRECATED_CONFIG_KEYS) if (doc.has(key)) doc.delete(key);
202
+ for (const key of DEPRECATED_PATHS_KEYS) if (doc.hasIn(["paths", key])) doc.deleteIn(["paths", key]);
203
+ const pathsNode = doc.get("paths", true);
204
+ if (isMap(pathsNode) && pathsNode.items.length === 0) doc.delete("paths");
207
205
  return doc.toString();
208
206
  };
209
207
  const writeConfigValues = async (repoRoot, updates) => {
@@ -226,6 +224,19 @@ const pointerFrontmatterSchema = z.object({
226
224
  });
227
225
  Object.keys(pointerFrontmatterSchema.shape);
228
226
  //#endregion
227
+ //#region src/paths/claude-paths.constant.ts
228
+ const CLAUDE_DIR = ".claude";
229
+ const STATE_DIR = join(CLAUDE_DIR, "state");
230
+ const SKILLS_DIR = join(CLAUDE_DIR, "skills");
231
+ const AGENTS_DIR = join(CLAUDE_DIR, "agents");
232
+ const HOOKS_DIR = join(CLAUDE_DIR, "hooks");
233
+ const COMMANDS_DIR = join(CLAUDE_DIR, "commands");
234
+ const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
235
+ const HARNESS_DIR = join(CLAUDE_DIR, ".harness");
236
+ const BASELINE_ROOT_REL = join(HARNESS_DIR, "baseline");
237
+ const SNAPSHOTS_ROOT_REL = join(HARNESS_DIR, "snapshots");
238
+ const EVENTS_RELPATH = join(STATE_DIR, "events.jsonl");
239
+ //#endregion
229
240
  //#region src/events/envelope.ts
230
241
  const buildEnvelope = (input) => {
231
242
  const ts = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
@@ -399,7 +410,6 @@ const NUMERIC_FIELDS = /* @__PURE__ */ new Set([
399
410
  "review_iterations"
400
411
  ]);
401
412
  const BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["auto_close"]);
402
- const EVENTS_RELPATH$1 = join(".claude", "state", "events.jsonl");
403
413
  const isEventType = (value) => EVENT_TYPES.includes(value);
404
414
  const coerceField = (key, raw) => {
405
415
  if (NUMERIC_FIELDS.has(key)) {
@@ -454,7 +464,7 @@ const runEmit = async (options) => {
454
464
  const issues = parsed.error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
455
465
  throw new Error(`Event validation failed for type "${rawType}":\n${issues}`);
456
466
  }
457
- const eventsPath = join(repoRoot, EVENTS_RELPATH$1);
467
+ const eventsPath = join(repoRoot, EVENTS_RELPATH);
458
468
  await appendEvent(eventsPath, parsed.data);
459
469
  return {
460
470
  event: parsed.data,
@@ -2221,9 +2231,8 @@ const sanitizeLine = (raw, tier) => {
2221
2231
  };
2222
2232
  //#endregion
2223
2233
  //#region src/telemetry/telemetry.constant.ts
2224
- const EVENTS_RELPATH = join(".claude", "state", "events.jsonl");
2225
- const CURSOR_RELPATH = join(".claude", "state", "telemetry-cursor.json");
2226
- const REJECTED_RELPATH = join(".claude", "state", "telemetry-rejected.jsonl");
2234
+ const CURSOR_RELPATH = join(STATE_DIR, "telemetry-cursor.json");
2235
+ const REJECTED_RELPATH = join(STATE_DIR, "telemetry-rejected.jsonl");
2227
2236
  const PAYLOAD_CAP_BYTES = 1e6;
2228
2237
  const DEFAULT_TIMEOUT_MS = 3e3;
2229
2238
  const PRUNE_THRESHOLD_BYTES = 5e6;
@@ -2285,7 +2294,7 @@ const chunkTail = (rawTail, tier, capBytes = PAYLOAD_CAP_BYTES) => {
2285
2294
  };
2286
2295
  //#endregion
2287
2296
  //#region src/telemetry/consent.constant.ts
2288
- const CONSENT_RELPATH = join(".claude", "state", "telemetry-consent.json");
2297
+ const CONSENT_RELPATH = join(STATE_DIR, "telemetry-consent.json");
2289
2298
  const ENV_DISABLE_VALUES = [
2290
2299
  "1",
2291
2300
  "true",
@@ -2681,7 +2690,7 @@ const formatTelemetryShow = (report) => {
2681
2690
  };
2682
2691
  //#endregion
2683
2692
  //#region src/telemetry/telemetry-notice.constant.ts
2684
- const NOTICE_SENTINEL_RELPATH = join(".claude", "state", ".telemetry-notice-shown");
2693
+ const NOTICE_SENTINEL_RELPATH = join(STATE_DIR, ".telemetry-notice-shown");
2685
2694
  const NOTICE_MESSAGE = "Anonymous telemetry is ON — run `lemony telemetry disable` to opt out. See PRIVACY.md.";
2686
2695
  //#endregion
2687
2696
  //#region src/telemetry/telemetry-notice.ts
@@ -3071,7 +3080,7 @@ const checkLabels = async (deps, config) => {
3071
3080
  };
3072
3081
  const checkHooks = async (repoRoot) => {
3073
3082
  const name = "hooks";
3074
- const settingsPath = join(repoRoot, ".claude", "settings.json");
3083
+ const settingsPath = join(repoRoot, SETTINGS_FILE);
3075
3084
  let raw;
3076
3085
  try {
3077
3086
  raw = await readFile(settingsPath, "utf8");
@@ -3327,7 +3336,7 @@ const readPointer = async (repoRoot, readGitUserEmail) => {
3327
3336
  if (!email) return empty;
3328
3337
  const slug = email.split("@")[0] ?? "";
3329
3338
  if (!slug || slug.includes("/") || slug.includes("\\") || slug.includes("..")) return empty;
3330
- const pointerPath = join(repoRoot, ".claude", "state", `current-${slug}.md`);
3339
+ const pointerPath = join(repoRoot, STATE_DIR, `current-${slug}.md`);
3331
3340
  let raw;
3332
3341
  try {
3333
3342
  raw = await readFile(pointerPath, "utf8");
@@ -3460,7 +3469,6 @@ const renderTemplate = (template, vars) => template.replace(PLACEHOLDER, (_match
3460
3469
  });
3461
3470
  //#endregion
3462
3471
  //#region src/baseline/baseline.ts
3463
- const BASELINE_ROOT_REL = join(".claude", ".harness", "baseline");
3464
3472
  const STAGING_DIR$1 = ".staging";
3465
3473
  const isENOENT$1 = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
3466
3474
  const baselineRootDir = (repoRoot) => join(repoRoot, BASELINE_ROOT_REL);
@@ -3525,7 +3533,6 @@ const findBaselineVersion = async (repoRoot) => {
3525
3533
  };
3526
3534
  //#endregion
3527
3535
  //#region src/snapshot/snapshot.ts
3528
- const SNAPSHOTS_ROOT_REL = join(".claude", ".harness", "snapshots");
3529
3536
  const STAGING_DIR = ".staging";
3530
3537
  const WORKING_SUBDIR = "working";
3531
3538
  const BASELINE_SUBDIR = "baseline";
@@ -3785,12 +3792,11 @@ const lcsMatches = (base, other) => {
3785
3792
  };
3786
3793
  //#endregion
3787
3794
  //#region src/update/engine.ts
3788
- const SKILLS_PREFIX = join(".claude", "skills");
3789
3795
  const cohesionKey = (relPath) => {
3790
- const prefix = SKILLS_PREFIX + sep;
3796
+ const prefix = SKILLS_DIR + sep;
3791
3797
  if (!relPath.startsWith(prefix)) return relPath;
3792
3798
  const name = relPath.slice(prefix.length).split(sep)[0];
3793
- return name ? join(SKILLS_PREFIX, name) : relPath;
3799
+ return name ? join(SKILLS_DIR, name) : relPath;
3794
3800
  };
3795
3801
  const runEngine = async (inputs) => {
3796
3802
  const { baseline, readClient, onConflict } = inputs;
@@ -3935,23 +3941,23 @@ const materializeVendorFiles = async (ctx) => {
3935
3941
  const templateRoot = join(vendorRoot, "templates", target);
3936
3942
  const vendorVersion = vars.vendor_version ?? "";
3937
3943
  const entryProtocol = await renderFile(join(templateRoot, "agents.md.tpl"), "agents.md", vars);
3938
- const agentFiles = await Promise.all(CORE_AGENTS.map(async (role) => withAssetFrontmatter(await renderFile(join(vendorRoot, "agents", `${role}.md`), join(".claude", "agents", `${role}.md`), {
3944
+ const agentFiles = await Promise.all(CORE_AGENTS.map(async (role) => withAssetFrontmatter(await renderFile(join(vendorRoot, "agents", `${role}.md`), join(AGENTS_DIR, `${role}.md`), {
3939
3945
  SKILLS: renderRoleSkills(skills, role),
3940
3946
  vendor_version: vendorVersion
3941
3947
  }), vendorVersion)));
3942
- const fitAssessment = await copyFileEntry(join(vendorRoot, "agents", "fit-assessment.md"), join(".claude", "agents", "fit-assessment.md"));
3948
+ const fitAssessment = await copyFileEntry(join(vendorRoot, "agents", "fit-assessment.md"), join(AGENTS_DIR, "fit-assessment.md"));
3943
3949
  const skillFiles = (await Promise.all(skills.map(async (skill) => {
3944
3950
  const skillDir = join(vendorRoot, "skills", skill.name);
3945
3951
  const rels = await listFiles(skillDir);
3946
- return Promise.all(rels.map(async (rel) => withAssetFrontmatter(await copyFileEntry(join(skillDir, rel), join(".claude", "skills", skill.name, rel)), vendorVersion)));
3952
+ return Promise.all(rels.map(async (rel) => withAssetFrontmatter(await copyFileEntry(join(skillDir, rel), join(SKILLS_DIR, skill.name, rel)), vendorVersion)));
3947
3953
  }))).flat();
3948
3954
  const playbooksReadme = await renderFile(join(templateRoot, "docs", "playbooks", "README.md.tpl"), join("docs", "playbooks", "README.md"), vars);
3949
3955
  const hooksSrc = join(vendorRoot, "hooks");
3950
- const hookFiles = await Promise.all((await listFiles(hooksSrc)).filter((rel) => rel.endsWith(".sh") && !rel.includes(sep)).map((rel) => copyFileEntry(join(hooksSrc, rel), join(".claude", "hooks", rel), true)));
3956
+ const hookFiles = await Promise.all((await listFiles(hooksSrc)).filter((rel) => rel.endsWith(".sh") && !rel.includes(sep)).map((rel) => copyFileEntry(join(hooksSrc, rel), join(HOOKS_DIR, rel), true)));
3951
3957
  const hookLibSrc = join(hooksSrc, "lib");
3952
- const hookLibFiles = await pathExists(hookLibSrc) ? await Promise.all((await listFiles(hookLibSrc)).filter((rel) => rel.endsWith(".sh")).map((rel) => copyFileEntry(join(hookLibSrc, rel), join(".claude", "hooks", "lib", rel), true))) : [];
3958
+ const hookLibFiles = await pathExists(hookLibSrc) ? await Promise.all((await listFiles(hookLibSrc)).filter((rel) => rel.endsWith(".sh")).map((rel) => copyFileEntry(join(hookLibSrc, rel), join(HOOKS_DIR, "lib", rel), true))) : [];
3953
3959
  const commandsSrc = join(vendorRoot, "commands");
3954
- const commandFiles = await pathExists(commandsSrc) ? await Promise.all((await listFiles(commandsSrc)).map(async (rel) => withAssetFrontmatter(await copyFileEntry(join(commandsSrc, rel), join(".claude", "commands", rel)), vendorVersion))) : [];
3960
+ const commandFiles = await pathExists(commandsSrc) ? await Promise.all((await listFiles(commandsSrc)).map(async (rel) => withAssetFrontmatter(await copyFileEntry(join(commandsSrc, rel), join(COMMANDS_DIR, rel)), vendorVersion))) : [];
3955
3961
  const configSchema = await copyFileEntry(join(vendorRoot, HARNESS_CONFIG_SCHEMA_FILENAME), HARNESS_CONFIG_SCHEMA_FILENAME);
3956
3962
  return [
3957
3963
  entryProtocol,
@@ -4081,7 +4087,7 @@ const diffLostCommands = (existingHooks, templateHooks) => {
4081
4087
  //#endregion
4082
4088
  //#region src/install/resync.ts
4083
4089
  const resyncSpecialCased = async (repoRoot, templateRoot) => {
4084
- const { lostCommands } = await mergeSettingsHooks(join(templateRoot, ".claude", "settings.json.tpl"), join(repoRoot, ".claude", "settings.json"));
4090
+ const { lostCommands } = await mergeSettingsHooks(join(templateRoot, CLAUDE_DIR, "settings.json.tpl"), join(repoRoot, SETTINGS_FILE));
4085
4091
  return {
4086
4092
  lostHookCommands: lostCommands,
4087
4093
  gitignorePath: await appendGitignoreBlock(repoRoot)
@@ -4128,11 +4134,11 @@ const runInstall = async (options) => {
4128
4134
  });
4129
4135
  await assertNoSymlinkTraversal(repoRoot, [
4130
4136
  ...vendorFiles.map((file) => file.relPath),
4131
- join(".claude", "settings.json"),
4137
+ SETTINGS_FILE,
4132
4138
  ".gitignore",
4133
- join(".claude", "state", "history.md"),
4134
- join(".claude", "state", "sessions"),
4135
- ...COMMITTED_STATE_DIRS.map((dir) => join(".claude", "state", dir, ".gitkeep")),
4139
+ join(STATE_DIR, "history.md"),
4140
+ join(STATE_DIR, "sessions"),
4141
+ ...COMMITTED_STATE_DIRS.map((dir) => join(STATE_DIR, dir, ".gitkeep")),
4136
4142
  HARNESS_CONFIG_FILENAME
4137
4143
  ]);
4138
4144
  const readClient = async (relPath) => {
@@ -4186,9 +4192,8 @@ const runInstall = async (options) => {
4186
4192
  await writeBaseline(repoRoot, vendorVersion, vendorFiles);
4187
4193
  const claudeMdPresent = await pathExists(join(repoRoot, CLAUDE_MD_FILENAME));
4188
4194
  const devDependencyMissing = await inspectDevDependency(repoRoot) === "missing";
4189
- const stateFiles = await Promise.all([writeOut(repoRoot, join(".claude", "state", "history.md"), renderTemplate(await readFile(join(templateRoot, "state", "history.md.tpl"), "utf8"), vars)), ...COMMITTED_STATE_DIRS.map((dir) => writeOut(repoRoot, join(".claude", "state", dir, ".gitkeep"), ""))]);
4190
- await mkdir(join(repoRoot, ".claude", "state", "sessions"), { recursive: true });
4191
- const settingsRelPath = join(".claude", "settings.json");
4195
+ const stateFiles = await Promise.all([writeOut(repoRoot, join(STATE_DIR, "history.md"), renderTemplate(await readFile(join(templateRoot, "state", "history.md.tpl"), "utf8"), vars)), ...COMMITTED_STATE_DIRS.map((dir) => writeOut(repoRoot, join(STATE_DIR, dir, ".gitkeep"), ""))]);
4196
+ await mkdir(join(repoRoot, STATE_DIR, "sessions"), { recursive: true });
4192
4197
  const { lostHookCommands, gitignorePath: gitignore } = await resyncSpecialCased(repoRoot, templateRoot);
4193
4198
  const configFile = await writeOut(repoRoot, HARNESS_CONFIG_FILENAME, renderTemplate(await readFile(join(templateRoot, "harness.config.yml.tpl"), "utf8"), vars));
4194
4199
  return {
@@ -4201,7 +4206,7 @@ const runInstall = async (options) => {
4201
4206
  filesWritten: [
4202
4207
  ...vendorPaths,
4203
4208
  ...stateFiles,
4204
- settingsRelPath,
4209
+ SETTINGS_FILE,
4205
4210
  ...gitignore ? [gitignore] : [],
4206
4211
  configFile
4207
4212
  ],
@@ -4276,7 +4281,7 @@ const runReconcile = async (inputs) => {
4276
4281
  await refuseOnResidualMarkers(managedPaths, readClient);
4277
4282
  await assertNoSymlinkTraversal(repoRoot, [
4278
4283
  ...managedPaths,
4279
- join(".claude", "settings.json"),
4284
+ SETTINGS_FILE,
4280
4285
  ".gitignore"
4281
4286
  ]);
4282
4287
  const actions = await runEngine({
@@ -4417,8 +4422,6 @@ const runRepair = async (options) => {
4417
4422
  };
4418
4423
  //#endregion
4419
4424
  //#region src/uninstall/uninstall.ts
4420
- const HARNESS_DIR = join(".claude", ".harness");
4421
- const SETTINGS_RELPATH = join(".claude", "settings.json");
4422
4425
  const DOCS_PREFIX = `docs${sep}`;
4423
4426
  const runUninstall = async (options) => {
4424
4427
  const { repoRoot } = options;
@@ -4429,14 +4432,14 @@ const runUninstall = async (options) => {
4429
4432
  const removedFiles = [...(await readBaseline(repoRoot, baselineVersion)).keys()].filter((relPath) => !relPath.startsWith(DOCS_PREFIX)).toSorted();
4430
4433
  await assertNoSymlinkTraversal(repoRoot, [
4431
4434
  ...removedFiles,
4432
- SETTINGS_RELPATH,
4435
+ SETTINGS_FILE,
4433
4436
  ".gitignore",
4434
4437
  HARNESS_DIR,
4435
4438
  HARNESS_CONFIG_FILENAME
4436
4439
  ]);
4437
4440
  await Promise.all(removedFiles.map((relPath) => rm(join(repoRoot, relPath), { force: true })));
4438
4441
  await pruneEmptyDirs(repoRoot, removedFiles);
4439
- const settings = await removeSettingsHooks(join(repoRoot, SETTINGS_RELPATH));
4442
+ const settings = await removeSettingsHooks(join(repoRoot, SETTINGS_FILE));
4440
4443
  const gitignoreStripped = await removeGitignoreBlock(repoRoot) !== null;
4441
4444
  await rm(join(repoRoot, HARNESS_DIR), {
4442
4445
  recursive: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lemoncode/lemony",
3
- "version": "0.1.1-alpha.0",
3
+ "version": "0.1.1-alpha.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,