← Commit history
SKILL.mdadded+54
@@ -0,0 +1,54 @@+---+name: prose-lint+description: >+  Deterministic house-style linter. Run before shipping ANY user-facing copy+  (READMEs, wiki pages, release notes, chat announcements, UI strings) to catch+  em-dashes and the tics that make AI-written text read as AI-written. Trigger+  words: prose lint, lint this copy, remove em-dashes, de-AI this, does this+  sound AI-written, check my prose, make it read less generated.+---++# prose-lint++`prose-lint` is the deterministic enforcement side of the Adom house style. The+judgment lives in the `prose-style` skill; this CLI is the fast, mechanical check.++## Use it++```+prose-lint README.md                 # lint one or more files+prose-lint docs/*.md                  # globs are fine+cat page.md | prose-lint              # or stdin+prose-lint --fix README.md            # apply the safe 1:1 word swaps in place+prose-lint --json page.md             # { status, data: {counts, files}, hints }+prose-lint --strict page.md           # exit non-zero on ANY finding, not just em-dashes+```++Default exit codes: `0` clean or soft-only, `1` hard findings (em-dashes / spaced+`--`), `2` usage error. Under `--strict`, any finding fails.++## What it catches++<!-- prose-lint-disable (this list names the banned words on purpose) -->++- **Hard (error):** em-dashes, en-dashes in prose, and spaced double-hyphens.+- **Tells (warn):** AI-favorite words (delve, seamless, robust, leverage, utilize,+  showcase, in-depth, cutting-edge, myriad, ...) and boilerplate cadence+  ("it's not just X, it's Y", "in today's ... world", "when it comes to", "whether+  you're X or Y", "plays a pivotal role", ...).+- **Filler (info):** "in order to", "a variety of", empty intensifiers.++<!-- prose-lint-enable -->++It is markdown/code-aware: it will **not** flag a `--flag` inside a code span, a+number range like `2-5 V`, a URL, or YAML front matter. So it is safe to run over+mixed prose+code files.++## Where to run it++- Before `adom-wiki page publish` / `pkg publish` on any page with prose.+- In component-page / README generators, over the generated copy.+- On release notes and Google Chat announcements before they go out.++`--fix` only performs the unambiguous word-for-word swaps. Em-dashes and cadence+need a human rewrite. See the `prose-style` skill for how.
bin/prose-lintadded+385
@@ -0,0 +1,385 @@+#!/usr/bin/env node+'use strict';+/*+ * prose-lint — Adom house-style linter.+ *+ * Flags em-dashes and the tics that make AI-written copy read as AI-written.+ * Deterministic, zero-dependency, markdown/code-aware (it will not flag a+ * `--flag` in a code span, a number range in a table, or a URL).+ *+ * Contract (adom-cli-design): human output is line-oriented and ends with an+ * OK:/ERROR: verdict + Hint: lines; `--json` emits { status, data, hints }.+ * Exit 0 = clean or soft-only; exit 1 = hard findings (em-dashes) — or any+ * finding under --strict; exit 2 = usage error.+ */++const fs = require('fs');+const path = require('path');++// ----------------------------------------------------------------------------+// Rules. severity: 'error' (hard — em-dashes), 'warn' (AI tells), 'info' (filler)+// A rule is { id, severity, re, msg, fix? }. `re` must be a global regex.+// `msg(m)` receives the RegExp match array. `fix` (optional) is the literal+// replacement for a safe 1:1 swap used by --fix.+// ----------------------------------------------------------------------------++// tell-words → plain-language suggestion (also drives --fix for 1:1 swaps)+const TELL_WORDS = {+  'delve': 'dig in / look at',+  'seamless': 'smooth / simple',+  'seamlessly': 'smoothly / cleanly',+  'in-depth': 'detailed / thorough',+  'robust': 'solid / reliable',+  'leverage': 'use',+  'leverages': 'uses',+  'leveraging': 'using',+  'utilize': 'use',+  'utilizes': 'uses',+  'utilizing': 'using',+  'utilise': 'use',+  'boasts': 'has',+  'cutting-edge': 'new / modern',+  'state-of-the-art': 'modern',+  'unlock': 'enable / get',+  'unleash': 'release / enable',+  'elevate': 'improve / raise',+  'empower': 'let / help',+  'empowers': 'lets / helps',+  'realm': 'area / world',+  'landscape': 'field / market',+  'tapestry': 'mix',+  'testament': 'proof / sign',+  'foster': 'build / support',+  'fosters': 'builds / supports',+  'underscore': 'show / stress',+  'underscores': 'shows / stresses',+  'showcase': 'show',+  'showcases': 'shows',+  'showcasing': 'showing',+  'embark': 'start',+  'ever-evolving': 'changing',+  'ever-changing': 'changing',+  'fast-paced': 'fast',+  'game-changer': 'big deal',+  'game-changing': 'major',+  'revolutionize': 'change / improve',+  'revolutionizes': 'changes / improves',+  'revolutionizing': 'changing / improving',+  'harness': 'use',+  'harnesses': 'uses',+  'streamline': 'simplify / speed up',+  'streamlines': 'simplifies',+  'holistic': 'whole / overall',+  'myriad': 'many',+  'plethora': 'plenty / many',+  'bustling': 'busy',+  'meticulous': 'careful',+  'meticulously': 'carefully',+  'vibrant': 'lively',+  'pivotal': 'key',+  'paramount': 'critical / top',+  'nestled': 'set / located',+  'endeavor': 'effort / try',+  'facilitate': 'help / enable',+  'facilitates': 'helps / enables',+  'commence': 'start',+  'commences': 'starts',+  'plethora of': 'many',+};+// 1:1 lowercase swaps safe for --fix (no "/" alternatives in value)+const SAFE_FIX = {+  'utilize': 'use', 'utilizes': 'uses', 'utilizing': 'using', 'utilise': 'use',+  'leverage': 'use', 'leverages': 'uses', 'leveraging': 'using',+  'commence': 'start', 'commences': 'starts',+  'facilitate': 'help', 'facilitates': 'helps',+  'boasts': 'has', 'myriad': 'many', 'plethora': 'plenty',+};++function tellWordRule() {+  const words = Object.keys(TELL_WORDS).filter((w) => !w.includes(' '));+  // longest first so "in-depth" wins over "depth"; escape hyphens+  const alt = words+    .sort((a, b) => b.length - a.length)+    .map((w) => w.replace(/[-]/g, '\\-'))+    .join('|');+  return {+    id: 'tell-word',+    severity: 'warn',+    re: new RegExp('\\b(' + alt + ')\\b', 'gi'),+    msg: (m) => {+      const key = m[1].toLowerCase();+      const sug = TELL_WORDS[key];+      return `AI-tell word "${m[1]}"` + (sug ? ` — prefer plain language (e.g. ${sug})` : '');+    },+  };+}++const RULES = [+  // --- hard: em/en-dash used as an em-dash in prose ---+  {+    id: 'em-dash',+    severity: 'error',+    re: /—/g,+    msg: () => 'Em-dash (—) in prose — replace with a comma, colon, period, or parentheses',+  },+  {+    id: 'ascii-em-dash',+    severity: 'error',+    re: / -- /g,+    msg: () => 'Spaced double-hyphen ( -- ) reads as an em-dash — use a comma, colon, or period',+  },+  {+    id: 'en-dash',+    severity: 'warn',+    // en-dash NOT between digits (a real number range like 2–5 is fine)+    re: /(?<!\d)–(?!\d)|(?<=\s)–(?=\s)/g,+    msg: () => 'En-dash (–) in prose — use a hyphen for compounds or rewrite',+  },+  tellWordRule(),+  // --- warn: AI cadence / boilerplate structures ---+  { id: 'cadence-not-just', severity: 'warn', re: /\bit['’]?s not just\b[^.?!\n]{0,60}?,\s*it['’]?s\b/gi,+    msg: () => '"it\'s not just X, it\'s Y" — classic AI cadence; state the point plainly' },+  { id: 'cadence-not-only', severity: 'warn', re: /\bnot only\b[^.?!\n]{0,60}?\bbut also\b/gi,+    msg: () => '"not only X but also Y" — tighten to a plain list or sentence' },+  { id: 'cadence-in-today', severity: 'warn', re: /\bin today['’]?s\b[^.?!\n]{0,40}?\b(world|landscape|era|market|age|environment)\b/gi,+    msg: () => '"In today\'s [adjective] world" opener — cut it and start with the point' },+  { id: 'cadence-when-it-comes', severity: 'warn', re: /\bwhen it comes to\b/gi,+    msg: () => '"When it comes to X" — usually deletable filler' },+  { id: 'cadence-thats-where', severity: 'warn', re: /\bthat['’]?s where\b[^.?!\n]{0,40}?\bcomes? in\b/gi,+    msg: () => '"That\'s where X comes in" — marketing cadence; say what it does' },+  { id: 'cadence-whether', severity: 'warn', re: /\bwhether you['’]?re\b[^.?!\n]{0,60}?\bor\b/gi,+    msg: () => '"Whether you\'re X or Y" opener — reads as landing-page copy' },+  { id: 'cadence-goodbye', severity: 'warn', re: /\bsay goodbye to\b/gi,+    msg: () => '"Say goodbye to X" — ad copy; describe the change directly' },+  { id: 'cadence-look-no-further', severity: 'warn', re: /\blook no further\b/gi,+    msg: () => '"Look no further" — ad cliché; delete' },+  { id: 'cadence-heart-of', severity: 'warn', re: /\bat the heart of\b/gi,+    msg: () => '"At the heart of X" — vague; name the actual relationship' },+  { id: 'cadence-plays-role', severity: 'warn', re: /\bplays? an? (crucial|pivotal|vital|key|significant|important) role\b/gi,+    msg: () => '"plays a [crucial] role" — say what it actually does' },+  { id: 'cadence-navigate', severity: 'warn', re: /\bnavigat(e|ing) the complexit/gi,+    msg: () => '"navigate the complexities of X" — filler; be concrete' },+  { id: 'cadence-result-q', severity: 'warn', re: /\bThe result\?/g,+    msg: () => 'Rhetorical "The result?" fragment — write a full sentence' },+  // --- info: low-signal filler / intensifiers ---+  { id: 'filler-in-order-to', severity: 'info', re: /\bin order to\b/gi, fix: 'to',+    msg: () => '"in order to" → "to"' },+  { id: 'filler-a-variety-of', severity: 'info', re: /\ba (variety|number) of\b/gi,+    msg: () => '"a variety/number of" — prefer "several", "some", or a count' },+  { id: 'filler-intensifier', severity: 'info', re: /\b(very|really|simply|basically|actually|just literally|truly)\b/gi,+    msg: (m) => `filler intensifier "${m[1]}" — usually deletable` },+];++// ----------------------------------------------------------------------------+// Masking: build skip-ranges so we never lint inside code / URLs / front matter.+// Ranges are [start,end) offsets in the original text; offsets are preserved so+// line:col stays accurate.+// ----------------------------------------------------------------------------+function skipRanges(text) {+  const ranges = [];+  // whole-file opt-out+  if (/prose-lint-disable-file/.test(text)) return [[0, text.length]];+  const add = (re) => { let m; re.lastIndex = 0; while ((m = re.exec(text))) { ranges.push([m.index, m.index + m[0].length]); if (m[0].length === 0) re.lastIndex++; } };+  // opt-out regions: prose-lint-disable ... prose-lint-enable (any comment syntax).+  // A region with no matching enable runs to EOF. Lets reference docs name the+  // very words we ban without tripping the linter.+  {+    const marker = /prose-lint-(disable|enable)/g;+    let m, open = -1;+    while ((m = marker.exec(text))) {+      if (m[1] === 'disable' && open < 0) open = m.index;+      else if (m[1] === 'enable' && open >= 0) { ranges.push([open, m.index + m[0].length]); open = -1; }+    }+    if (open >= 0) ranges.push([open, text.length]);+  }+  // leading YAML front matter+  const fm = /^---\n[\s\S]*?\n---/;+  const fmm = fm.exec(text); if (fmm) ranges.push([0, fmm[0].length]);+  add(/```[\s\S]*?```/g);        // fenced code+  add(/~~~[\s\S]*?~~~/g);+  add(/<pre[\s\S]*?<\/pre>/gi);+  add(/<code[\s\S]*?<\/code>/gi);+  add(/`[^`\n]*`/g);             // inline code+  add(/<[^>\n]+>/g);             // html tags (attributes, not prose)+  add(/\]\(([^)\s]+)/g);         // markdown link/image target (url only)+  add(/^\[[^\]]+\]:\s.*$/gm);    // reference-style link defs+  add(/https?:\/\/[^\s)]+/g);    // bare URLs+  add(/[\w.+-]+@[\w.-]+\.\w+/g); // emails+  add(/^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/gm); // hr / thematic break (--- *** ___)+  return ranges;+}+function inSkip(idx, ranges) { for (const [s, e] of ranges) if (idx >= s && idx < e) return true; return false; }++function offsetToLineCol(text, idx) {+  let line = 1, col = 1;+  for (let i = 0; i < idx; i++) { if (text[i] === '\n') { line++; col = 1; } else col++; }+  return [line, col];+}++function lint(text) {+  const ranges = skipRanges(text);+  const findings = [];+  for (const rule of RULES) {+    let m; rule.re.lastIndex = 0;+    while ((m = rule.re.exec(text))) {+      if (m[0].length === 0) { rule.re.lastIndex++; continue; }+      if (inSkip(m.index, ranges)) continue;+      const [line, col] = offsetToLineCol(text, m.index);+      findings.push({ rule: rule.id, severity: rule.severity, line, col, match: m[0].trim(), message: rule.msg(m) });+    }+  }+  findings.sort((a, b) => a.line - b.line || a.col - b.col);+  return findings;+}++// deterministic 1:1 autofix (safe swaps + "in order to" only)+function applyFix(text) {+  const ranges = skipRanges(text);+  const edits = [];+  const push = (re, repl, keepCase) => {+    let m; re.lastIndex = 0;+    while ((m = re.exec(text))) {+      if (inSkip(m.index, ranges)) continue;+      let r = repl;+      if (keepCase && /^[A-Z]/.test(m[0])) r = r[0].toUpperCase() + r.slice(1);+      edits.push([m.index, m.index + m[0].length, r]);+    }+  };+  for (const [w, r] of Object.entries(SAFE_FIX)) push(new RegExp('\\b' + w.replace(/-/g, '\\-') + '\\b', 'gi'), r, true);+  push(/\bin order to\b/gi, 'to', true);+  edits.sort((a, b) => a[0] - b[0]);+  let out = '', last = 0;+  for (const [s, e, r] of edits) { if (s < last) continue; out += text.slice(last, s) + r; last = e; }+  out += text.slice(last);+  return { text: out, count: edits.length };+}++// ----------------------------------------------------------------------------+// CLI+// ----------------------------------------------------------------------------+function parseArgs(argv) {+  const o = { files: [], json: false, strict: false, fix: false, quiet: false };+  for (let i = 0; i < argv.length; i++) {+    const a = argv[i];+    if (a === '--json') o.json = true;+    else if (a === '--strict') o.strict = true;+    else if (a === '--fix') o.fix = true;+    else if (a === '--quiet' || a === '-q') o.quiet = true;+    else if (a === '--stdin' || a === '-') o.stdin = true;+    else if (a === '--help' || a === '-h') o.help = true;+    else if (a === '--version' || a === '-V') o.version = true;+    else if (a.startsWith('-')) { o.badFlag = a; }+    else o.files.push(a);+  }+  return o;+}++const HELP = `prose-lint — Adom house-style linter (no em-dashes, no AI slop)++USAGE+  prose-lint [FILE...]           lint files (markdown/html/txt)+  prose-lint --stdin < file      lint stdin+  echo "text" | prose-lint       lint stdin (auto when no files)++OPTIONS+  --fix        apply safe 1:1 word swaps in place (utilize→use, "in order to"→to)+  --strict     exit non-zero on ANY finding (default: only em-dashes fail)+  --json       machine-readable { status, data, hints[] }+  --quiet, -q  suppress the per-finding lines (summary + verdict only)+  --version    print version+  --help       this help++EXIT  0 clean/soft-only · 1 hard findings (or any under --strict) · 2 usage`;++const SEV_ORDER = { error: 0, warn: 1, info: 2 };+const SEV_LABEL = { error: 'HARD ', warn: 'warn ', info: 'info ' };++function main() {+  const o = parseArgs(process.argv.slice(2));+  if (o.help) { process.stdout.write(HELP + '\n'); process.exit(0); }+  if (o.version) {+    let v = '0.0.0';+    try { v = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; } catch (e) {}+    process.stdout.write('prose-lint ' + v + '\n'); process.exit(0);+  }+  if (o.badFlag) { process.stderr.write('ERROR: unknown flag ' + o.badFlag + '\nHint: run `prose-lint --help`\n'); process.exit(2); }++  // gather inputs+  const inputs = []; // { name, text }+  if (o.files.length) {+    for (const f of o.files) {+      try { inputs.push({ name: f, text: fs.readFileSync(f, 'utf8') }); }+      catch (e) { process.stderr.write('ERROR: cannot read ' + f + ' (' + e.code + ')\n'); process.exit(2); }+    }+  } else {+    let text = '';+    try { text = fs.readFileSync(0, 'utf8'); } catch (e) {}+    if (!text && process.stdin.isTTY) { process.stdout.write(HELP + '\n'); process.exit(0); }+    inputs.push({ name: '<stdin>', text });+  }++  // --fix mode+  if (o.fix) {+    const results = [];+    for (const inp of inputs) {+      const { text, count } = applyFix(inp.text);+      if (inp.name !== '<stdin>' && count > 0) fs.writeFileSync(inp.name, text);+      else if (inp.name === '<stdin>') process.stdout.write(text);+      results.push({ file: inp.name, fixed: count });+    }+    const total = results.reduce((n, r) => n + r.fixed, 0);+    if (o.json) { process.stdout.write(JSON.stringify({ status: 'ok', data: { fixed: total, files: results }, hints: [] }) + '\n'); }+    else {+      for (const r of results) if (r.file !== '<stdin>') process.stdout.write(`fixed ${r.fixed} in ${r.file}\n`);+      process.stdout.write((total ? 'OK: applied ' + total + ' safe swap(s)' : 'OK: nothing to auto-fix') ++        '\nHint: --fix only handles 1:1 word swaps; em-dashes and cadence need a human rewrite. Re-run without --fix to see what remains.\n');+    }+    process.exit(0);+  }++  // lint mode+  let all = [];+  const perFile = [];+  for (const inp of inputs) {+    const f = lint(inp.text);+    perFile.push({ file: inp.name, findings: f });+    all = all.concat(f.map((x) => ({ file: inp.name, ...x })));+  }+  const counts = { error: 0, warn: 0, info: 0 };+  for (const f of all) counts[f.severity]++;++  if (o.json) {+    const hard = counts.error;+    const status = (o.strict ? all.length : hard) ? 'error' : 'ok';+    process.stdout.write(JSON.stringify({+      status,+      data: { counts, total: all.length, files: perFile },+      hints: hard ? [{ level: 'error', code: 'EM_DASH', message: `${hard} hard finding(s) — em-dashes/AI-tells must go before shipping copy` }] : [],+    }) + '\n');+    process.exit((o.strict ? all.length : hard) ? 1 : 0);+  }++  if (!o.quiet) {+    for (const f of all.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0) || a.line - b.line || a.col - b.col)) {+      process.stdout.write(`${f.file}:${f.line}:${f.col}  ${SEV_LABEL[f.severity]} [${f.rule}]  ${f.message}\n`);+    }+    if (all.length) process.stdout.write('\n');+  }++  if (all.length === 0) {+    process.stdout.write('OK: prose is clean — no em-dashes or AI-tells found\n');+    process.exit(0);+  }+  const parts = [];+  if (counts.error) parts.push(counts.error + ' hard');+  if (counts.warn) parts.push(counts.warn + ' tell');+  if (counts.info) parts.push(counts.info + ' filler');+  const verdict = counts.error ? 'ERROR' : 'OK';+  process.stdout.write(`${verdict}: ${parts.join(', ')} issue(s) across ${inputs.length} file(s)\n`);+  if (counts.error) process.stdout.write('Hint: em-dashes and " -- " are the hard rule — swap for a comma, colon, or period, then re-run.\n');+  if (counts.warn) process.stdout.write('Hint: "tell" lines are advisory; rewrite the ones that make the copy sound generated.\n');+  process.stdout.write('Hint: `prose-lint --fix` applies the safe word swaps automatically.\n');+  process.exit((o.strict ? all.length : counts.error) ? 1 : 0);+}++main();
docs/HERO.mdadded+1
@@ -0,0 +1 @@+Put a 1200x630 `hero.png` in this folder. It is the billboard reused on the page header, the homepage, and the screensaver. Until it exists, `adom-wiki pkg lint` reports the hero as the one outstanding item.
docs/hero.htmladded+47
@@ -0,0 +1,47 @@+<!doctype html><html><head><meta charset="utf-8"><style>+  * { margin:0; padding:0; box-sizing:border-box; }+  html,body { width:1200px; height:750px; }+  body { background:#0a0e13; color:#e9eef5; font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;+    overflow:hidden; position:relative; }+  .glow { position:absolute; inset:0;+    background:radial-gradient(900px 500px at 82% 18%, rgba(0,230,220,.16), transparent 60%),+               radial-gradient(760px 520px at 12% 92%, rgba(188,140,255,.16), transparent 60%); }+  .wrap { position:absolute; inset:0; display:flex; flex-direction:column; justify-content:center; padding:76px 84px; }+  .kicker { font-size:19px; letter-spacing:.32em; text-transform:uppercase; font-weight:700;+    background:linear-gradient(90deg,#00e6dc,#58a6ff 55%,#bc8cff); -webkit-background-clip:text; background-clip:text; color:transparent; }+  h1 { font-size:104px; font-weight:800; letter-spacing:-.03em; margin:14px 0 6px; line-height:.98; }+  h1 .dot { color:#00e6dc; }+  .sub { font-size:34px; font-weight:600; color:#aeb9c6; }+  .accent { height:7px; width:230px; border-radius:6px; margin:26px 0 34px;+    background:linear-gradient(90deg,#00e6dc,#58a6ff 55%,#bc8cff); }+  .card { position:absolute; right:70px; bottom:70px; width:640px; background:#0f151d; border:1px solid #1e2a37;+    border-radius:16px; box-shadow:0 40px 90px rgba(0,0,0,.55); overflow:hidden; font-family:'SF Mono',ui-monospace,Menlo,Consolas,monospace; }+  .bar { height:38px; background:#131b25; border-bottom:1px solid #1e2a37; display:flex; align-items:center; gap:8px; padding:0 14px; }+  .bar i { width:12px; height:12px; border-radius:50%; display:inline-block; }+  .bar .r{background:#ff5f57}.bar .y{background:#febc2e}.bar .g{background:#28c840}+  .bar span{margin-left:10px; font-size:14px; color:#7d8b9a}+  .body { padding:20px 22px; font-size:19px; line-height:1.85; }+  .body .path{color:#7d8b9a}+  .hard{color:#ff6b6b; font-weight:700}+  .warn{color:#febc2e}+  .ok{color:#00e6dc; font-weight:700}+  .rule{color:#8aa0b4}+</style></head><body>+  <div class="glow"></div>+  <div class="wrap">+    <div class="kicker">Adom House Style</div>+    <h1>prose<span class="dot">-</span>lint</h1>+    <div class="accent"></div>+    <div class="sub">No em-dashes.<br>No AI slop.<br>Deterministic.</div>+  </div>+  <div class="card">+    <div class="bar"><i class="r"></i><i class="y"></i><i class="g"></i><span>prose-lint README.md</span></div>+    <div class="body">+      <div><span class="path">README:1:48</span>  <span class="warn">warn</span> <span class="rule">[tell-word]</span> "game-changer"</div>+      <div><span class="path">README:1:61</span>  <span class="hard">HARD</span> <span class="rule">[em-dash]</span> em-dash in prose</div>+      <div><span class="path">README:3:12</span>  <span class="warn">warn</span> <span class="rule">[cadence]</span> "in today's world"</div>+      <div style="margin-top:10px">&nbsp;</div>+      <div class="ok">OK: 1 hard, 2 tell. Fix before shipping.</div>+    </div>+  </div>+</body></html>
docs/hero.pngadded
⋯ 1 unchanged line ⋯
install.shadded+16
@@ -0,0 +1,16 @@+#!/bin/sh+set -e++# Symlink the CLI into ~/.local/bin so `prose-lint` is on PATH. Symlinking (not+# copying) means a reinstall/update never leaves a stale binary behind — the link+# always points at the current module dir.+BIN_DIR="${HOME}/.local/bin"+mkdir -p "$BIN_DIR"+chmod +x "$PWD/bin/prose-lint" 2>/dev/null || true+ln -sfn "$PWD/bin/prose-lint" "$BIN_DIR/prose-lint"++if ! command -v node >/dev/null 2>&1; then+  echo "prose-lint installed, but 'node' was not found on PATH — the CLI needs Node 16+ to run."+else+  echo "prose-lint installed -> $BIN_DIR/prose-lint"+fi
package.jsonadded+54
@@ -0,0 +1,54 @@+{+  "slug": "prose-lint",+  "title": "Prose Lint",+  "type": "app",+  "version": "0.1.0",+  "description": "Deterministic house-style linter for Adom copy. Flags em-dashes and the tics that make AI-written text read as AI-written (tell-words, boilerplate cadence, filler), with line:col, plain-language suggestions, --fix for safe swaps, and a --json envelope. Markdown/code-aware: never flags a --flag in a code span, a number range, or a URL.",+  "tags": [+    "cli",+    "lint",+    "writing",+    "house-style"+  ],+  "dependencies": {},+  "discovery_triggers": [+    "lint this copy",+    "remove em-dashes",+    "does this sound AI-written",+    "check my prose",+    "make this read less AI",+    "prose lint"+  ],+  "discovery_pitch": "Reach for prose-lint before shipping any user-facing copy — READMEs, wiki pages, release notes, UI strings — to catch em-dashes and AI-tells deterministically.",+  "sample_prompts": [+    {+      "label": "Lint a wiki page",+      "prompt": "run prose-lint on readme.md before I publish it"+    },+    {+      "label": "De-AI some copy",+      "prompt": "this paragraph sounds AI-written, check it with prose-lint"+    }+  ],+  "metadata": {+    "activate": {+      "kind": "bridge",+      "skill": "prose-lint",+      "requires": [+        "node"+      ],+      "verbPrefixes": [+        "prose-lint"+      ]+    }+  },+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "hero": {+    "headline": "Prose Lint",+    "subhead": "No em-dashes. No AI slop. Deterministic.",+    "screenshot": "docs/hero.png"+  }+}
uninstall.shadded+6
@@ -0,0 +1,6 @@+#!/bin/sh+set -e++BIN_DIR="${HOME}/.local/bin"+[ -L "$BIN_DIR/prose-lint" ] && rm -f "$BIN_DIR/prose-lint"+echo "prose-lint uninstalled"