jig-ui 0.14.1 → 0.15.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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.0
4
+
5
+ The gate holds an agent on warnings, waits while it asks the owner a question,
6
+ and reaches projects where the skill is installed globally.
7
+
8
+ ### Changed
9
+
10
+ - **`gate` blocks on warnings in the files the agent changed**, not only on
11
+ mechanical errors. One em dash survived two runs of fresh agents on a real
12
+ site: the first saw the warning, called it pre-existing, and finished. The
13
+ gate still reads only changed files, so old warnings elsewhere never trap an
14
+ agent, and it now also runs when a changed file carries interface text.
15
+ - **The skill asks the user to run each command.** `decide`, `spec`, `mockup`,
16
+ `make` and `critique` hold their procedures in the command, not in the
17
+ skill. An agent told in prose to "run decide" made up five questions of its
18
+ own and wrote a `DECISIONS.md` with no direction and no `## Unresolved`
19
+ section.
20
+
21
+ ### Added
22
+
23
+ - **`jig-allow <ID>: <why>`** waives one warning, on its own line or the line
24
+ above, in any comment syntax. Some warnings are a detector's guess: `A-01`
25
+ cannot tell a chosen violet from a default one. Warnings only, a reason is
26
+ required, and `check` lists every waiver it honoured, with its reason, on
27
+ every run. Waived findings are left out of `warnings=`.
28
+ - **`install --agent claude --hook` on a global install** writes only this
29
+ project's Stop hook. The hook was written only by a project install, which is
30
+ refused while the skill is global, so most projects never had a gate.
31
+
32
+ ### Fixed
33
+
34
+ - **A question to the owner is not a finish.** After `decide`, `spec` or
35
+ `mockup`, a stop whose last message asks a question is allowed, and the
36
+ command's output is checked on the stop after the answer. Before, the gate
37
+ refused `decide`'s first question because `DECISIONS.md` did not exist yet,
38
+ and on the second refusal the agent wrote the file itself with no answers.
39
+ - **The gate finds `DECISIONS.md` beside the token layer** that `brand` in
40
+ `jig.config.json` names. It looked in three fixed places, rejected a
41
+ correctly placed file, and the agent moved the file to satisfy it.
42
+ - **`jig seo` says an endpoint is not a page**, so `pages=` can be checked
43
+ against a site's route files.
44
+
3
45
  ## 0.14.1
4
46
 
5
47
  `jig seo` cites only what a rule says.
package/README.md CHANGED
@@ -53,10 +53,19 @@ Every agent supports both scopes.
53
53
  answer yes when an interactive install offers it. It adds one entry to
54
54
  `.claude/settings.json`, keeping everything already in that file, and runs `jig gate`
55
55
  when the agent tries to finish: it holds the agent there while the files it changed
56
- fail `check`, or while the `/jig` command it just ran left its work incomplete — a
57
- spec that is prose, a critique with no verdict files, a review that never rendered
58
- the page. After three attempts it lets the agent stop and says the work is
59
- unfinished.
56
+ have a `check` error or warning, or while the `/jig` command it just ran left its
57
+ work incomplete — a spec that is prose, a critique with no verdict files, a review
58
+ that never rendered the page. After three attempts it lets the agent stop and says
59
+ the work is unfinished.
60
+
61
+ A warning that is right as it stands is waived on its own line, in a comment
62
+ reading `jig-allow <ID>: <why>` (`/* jig-allow A-01: the brand is violet */`). The
63
+ reason is required, an error cannot be waived, and `check` lists every waiver it
64
+ honoured, with its reason, on every run.
65
+
66
+ The skill and the hook are installed separately. With Jig installed globally, run
67
+ `install --agent claude --hook` in a project to add only that project's hook: a
68
+ global hook would run on every stop in every project on the machine.
60
69
 
61
70
  It is off by default, and `--yes` never adds it, because that is the path an agent
62
71
  takes and nobody is there to consent. It exists because weak models skip any step
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { readFileSync as readFileSync30 } from "fs";
5
+ import { readFileSync as readFileSync31 } from "fs";
6
6
  import { homedir as homedir2 } from "os";
7
- import { join as join37 } from "path";
7
+ import { join as join38 } from "path";
8
8
 
9
9
  // src/paths.ts
10
10
  import { existsSync } from "fs";
@@ -568,11 +568,19 @@ function install(opts) {
568
568
  } catch {
569
569
  globalManifest = null;
570
570
  }
571
+ if (globalManifest && opts.hook && opts.agent === "claude") {
572
+ return {
573
+ written: [],
574
+ skipped: [],
575
+ stopHook: installStopHook(opts.projectRoot, opts.version),
576
+ hookOnly: `Jig's skill is installed globally for 'claude' (${globalReferenceDir} under your home directory), so it stays there. Added only this project's Stop hook.`
577
+ };
578
+ }
571
579
  if (globalManifest) {
572
580
  return {
573
581
  written: [],
574
582
  skipped: [],
575
- warning: `Jig is already installed globally for '${opts.agent}' (${globalReferenceDir} under your home directory). Installing again at project scope would leave two contradicting '${opts.agent}' skills. Run 'jig update' to refresh the global install instead, or choose a different --agent for this project.`
583
+ warning: `Jig is already installed globally for '${opts.agent}' (${globalReferenceDir} under your home directory). Installing again at project scope would leave two contradicting '${opts.agent}' skills. Run 'jig update' to refresh the global install instead, or choose a different --agent for this project.` + (opts.agent === "claude" ? ` To add only this project's Stop hook, run install again with --hook.` : "")
576
584
  };
577
585
  }
578
586
  }
@@ -1455,8 +1463,8 @@ function sectionsOf(entries) {
1455
1463
  }
1456
1464
 
1457
1465
  // src/commands/check.ts
1458
- import { existsSync as existsSync14, readFileSync as readFileSync18 } from "fs";
1459
- import { join as join23 } from "path";
1466
+ import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
1467
+ import { join as join24 } from "path";
1460
1468
 
1461
1469
  // src/check/files.ts
1462
1470
  import { execFileSync } from "child_process";
@@ -1689,6 +1697,11 @@ function formatReport(findings, meta) {
1689
1697
  lines.push(` ${meta.exempt.slice(0, 8).join(", ")}${n > 8 ? `, and ${n - 8} more` : ""}`);
1690
1698
  }
1691
1699
  }
1700
+ if (meta.waived && meta.waived.length > 0) {
1701
+ lines.push("");
1702
+ lines.push(` ${plural(meta.waived.length, "warning")} waived in the source with jig-allow:`);
1703
+ for (const w of meta.waived) lines.push(` ${w.ruleId.padEnd(6)}${w.file}:${w.line} "${w.reason}"`);
1704
+ }
1692
1705
  if (meta.unscanned && meta.unscanned.count > 0) {
1693
1706
  const exts = meta.unscanned.extensions.join(", ");
1694
1707
  lines.push("");
@@ -3779,6 +3792,46 @@ function applyExemptions(files, patterns) {
3779
3792
  };
3780
3793
  }
3781
3794
 
3795
+ // src/check/waiver.ts
3796
+ import { readFileSync as readFileSync16 } from "fs";
3797
+ import { join as join21 } from "path";
3798
+ var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
3799
+ function readWaiver(line) {
3800
+ const m = WAIVER.exec(line);
3801
+ if (!m) return void 0;
3802
+ const reason = m[2].replace(/\s*(\*\/\s*\}?|-->|#\})\s*$/, "").trim();
3803
+ if (!/\w/.test(reason)) return void 0;
3804
+ return { ids: m[1].split(/\s*,\s*/), reason };
3805
+ }
3806
+ function applyWaivers(root, findings) {
3807
+ const lines = /* @__PURE__ */ new Map();
3808
+ const linesOf = (file) => {
3809
+ let cached = lines.get(file);
3810
+ if (!cached) {
3811
+ try {
3812
+ cached = readFileSync16(join21(root, file), "utf8").split("\n");
3813
+ } catch {
3814
+ cached = [];
3815
+ }
3816
+ lines.set(file, cached);
3817
+ }
3818
+ return cached;
3819
+ };
3820
+ const kept = [];
3821
+ const waived = [];
3822
+ for (const f of findings) {
3823
+ if (f.severity !== "warning") {
3824
+ kept.push(f);
3825
+ continue;
3826
+ }
3827
+ const source = linesOf(f.file);
3828
+ const waiver = [source[f.line - 1], source[f.line - 2]].map((l) => l === void 0 ? void 0 : readWaiver(l)).find((w) => w?.ids.includes(f.ruleId));
3829
+ if (waiver) waived.push({ ...f, reason: waiver.reason });
3830
+ else kept.push(f);
3831
+ }
3832
+ return { findings: kept, waived };
3833
+ }
3834
+
3782
3835
  // src/check/responsive.ts
3783
3836
  var MEDIA_RE = /@media\b([^{]*)\{/gi;
3784
3837
  var VIEWPORT_FEATURE_RE = /\b(?:min-|max-)?(?:width|height|aspect-ratio|orientation|resolution)\b|\b(?:any-)?(?:pointer|hover)\b/i;
@@ -3821,8 +3874,8 @@ function hasMenuToggle(raw) {
3821
3874
  }
3822
3875
 
3823
3876
  // src/check/mode-wiring.ts
3824
- import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
3825
- import { dirname as dirname6, join as join21 } from "path";
3877
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
3878
+ import { dirname as dirname6, join as join22 } from "path";
3826
3879
  var MODES = ["editorial", "product", "operator"];
3827
3880
  function configProblems(config) {
3828
3881
  const problems = [];
@@ -3844,10 +3897,10 @@ function configProblems(config) {
3844
3897
  }
3845
3898
  function modeWiringProblems(projectRoot) {
3846
3899
  let config;
3847
- const path = join21(projectRoot, "jig.config.json");
3900
+ const path = join22(projectRoot, "jig.config.json");
3848
3901
  if (!existsSync12(path)) return [];
3849
3902
  try {
3850
- config = JSON.parse(readFileSync16(path, "utf8"));
3903
+ config = JSON.parse(readFileSync17(path, "utf8"));
3851
3904
  } catch {
3852
3905
  return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
3853
3906
  }
@@ -3858,14 +3911,14 @@ function modeWiringProblems(projectRoot) {
3858
3911
  if (modes.length === 0) return [];
3859
3912
  const dir = dirname6(config.brand);
3860
3913
  const rel = (f) => dir === "." ? f : `${dir}/${f}`;
3861
- if (!existsSync12(join21(projectRoot, rel("theme.css")))) return [];
3914
+ if (!existsSync12(join22(projectRoot, rel("theme.css")))) return [];
3862
3915
  const problems = [];
3863
3916
  modes.forEach((mode, i) => {
3864
3917
  const barrel = rel(i === 0 ? "theme.css" : `theme.${mode}.css`);
3865
3918
  const modeFile = `mode.${mode}.css`;
3866
3919
  let body;
3867
3920
  try {
3868
- body = readFileSync16(join21(projectRoot, barrel), "utf8");
3921
+ body = readFileSync17(join22(projectRoot, barrel), "utf8");
3869
3922
  } catch {
3870
3923
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
3871
3924
  return;
@@ -3877,7 +3930,7 @@ function modeWiringProblems(projectRoot) {
3877
3930
  barrel,
3878
3931
  message: `jig.config.json declares ${mode}, but ${barrel} imports ${imported ? `mode.${imported}.css` : "no mode file"} \u2014 the page is styled at ${imported ?? "no"} density`
3879
3932
  });
3880
- } else if (!existsSync12(join21(projectRoot, rel(modeFile)))) {
3933
+ } else if (!existsSync12(join22(projectRoot, rel(modeFile)))) {
3881
3934
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
3882
3935
  }
3883
3936
  });
@@ -3885,8 +3938,8 @@ function modeWiringProblems(projectRoot) {
3885
3938
  }
3886
3939
 
3887
3940
  // src/check/token-audit.ts
3888
- import { existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as readFileSync17 } from "fs";
3889
- import { join as join22 } from "path";
3941
+ import { existsSync as existsSync13, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
3942
+ import { join as join23 } from "path";
3890
3943
  var TEXT_FLOOR = 4.5;
3891
3944
  var UI_FLOOR = 3;
3892
3945
  var FOREGROUNDS = [
@@ -3942,24 +3995,24 @@ function floorFor(token) {
3942
3995
  function tokenFiles(projectRoot) {
3943
3996
  const read = (rel) => {
3944
3997
  try {
3945
- return readFileSync17(join22(projectRoot, ...rel.split("/")), "utf8");
3998
+ return readFileSync18(join23(projectRoot, ...rel.split("/")), "utf8");
3946
3999
  } catch {
3947
4000
  return null;
3948
4001
  }
3949
4002
  };
3950
- const statePath = join22(projectRoot, ".jig", "state.json");
4003
+ const statePath = join23(projectRoot, ".jig", "state.json");
3951
4004
  if (existsSync13(statePath)) {
3952
4005
  try {
3953
- const state = JSON.parse(readFileSync17(statePath, "utf8"));
4006
+ const state = JSON.parse(readFileSync18(statePath, "utf8"));
3954
4007
  const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
3955
4008
  const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
3956
4009
  if (out.length > 0) return out;
3957
4010
  } catch {
3958
4011
  }
3959
4012
  }
3960
- const legacy = join22(projectRoot, ".jig", "tokens");
4013
+ const legacy = join23(projectRoot, ".jig", "tokens");
3961
4014
  if (!existsSync13(legacy)) return [];
3962
- return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync17(join22(legacy, f), "utf8") }));
4015
+ return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync18(join23(legacy, f), "utf8") }));
3963
4016
  }
3964
4017
  function auditTokenLayer(projectRoot) {
3965
4018
  const files = tokenFiles(projectRoot);
@@ -4027,15 +4080,15 @@ function auditTokenLayer(projectRoot) {
4027
4080
  }
4028
4081
 
4029
4082
  // src/commands/check.ts
4030
- var LEGACY_INDEX_REL = join23(".jig", "rules.index.json");
4083
+ var LEGACY_INDEX_REL = join24(".jig", "rules.index.json");
4031
4084
  function resolveIndexPath(projectRoot) {
4032
- const legacy = join23(projectRoot, LEGACY_INDEX_REL);
4085
+ const legacy = join24(projectRoot, LEGACY_INDEX_REL);
4033
4086
  if (existsSync14(legacy)) return legacy;
4034
- return join23(assetRoot(), "rules.index.json");
4087
+ return join24(assetRoot(), "rules.index.json");
4035
4088
  }
4036
4089
  function resolveMode(projectRoot) {
4037
4090
  try {
4038
- const config = JSON.parse(readFileSync18(join23(projectRoot, "jig.config.json"), "utf8"));
4091
+ const config = JSON.parse(readFileSync19(join24(projectRoot, "jig.config.json"), "utf8"));
4039
4092
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
4040
4093
  if (modes.length === 1) return modes[0];
4041
4094
  return modes.length > 1 ? "mixed" : "unknown";
@@ -4052,14 +4105,14 @@ function summariseUnscanned(files) {
4052
4105
  }
4053
4106
  function countSpecs() {
4054
4107
  try {
4055
- return loadSpecs(join23(assetRoot(), "rules")).length;
4108
+ return loadSpecs(join24(assetRoot(), "rules")).length;
4056
4109
  } catch {
4057
4110
  return 0;
4058
4111
  }
4059
4112
  }
4060
4113
  function check(opts) {
4061
4114
  const indexPath = resolveIndexPath(opts.projectRoot);
4062
- const index = validateIndex(JSON.parse(readFileSync18(indexPath, "utf8")));
4115
+ const index = validateIndex(JSON.parse(readFileSync19(indexPath, "utf8")));
4063
4116
  const selection = selectFiles(opts.projectRoot, opts.all);
4064
4117
  const exemptions = readExemptions(opts.projectRoot);
4065
4118
  const { scanned: files, exempt, byPattern } = applyExemptions(selection.files, exemptions);
@@ -4069,7 +4122,7 @@ function check(opts) {
4069
4122
  const projectParticipates = stylesheets.some((f) => {
4070
4123
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
4071
4124
  try {
4072
- return participatesInTokenLayer(readFileSync18(join23(opts.projectRoot, f), "utf8"), tokens);
4125
+ return participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
4073
4126
  } catch {
4074
4127
  return false;
4075
4128
  }
@@ -4077,7 +4130,7 @@ function check(opts) {
4077
4130
  const projectResponsive = stylesheets.some((f) => {
4078
4131
  if (!isStyleBearing(f)) return false;
4079
4132
  try {
4080
- const raw = readFileSync18(join23(opts.projectRoot, f), "utf8");
4133
+ const raw = readFileSync19(join24(opts.projectRoot, f), "utf8");
4081
4134
  return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
4082
4135
  } catch {
4083
4136
  return false;
@@ -4086,14 +4139,14 @@ function check(opts) {
4086
4139
  const viewportFitCover = stylesheets.some((f) => {
4087
4140
  if (!isStyleBearing(f)) return false;
4088
4141
  try {
4089
- return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync18(join23(opts.projectRoot, f), "utf8"));
4142
+ return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync19(join24(opts.projectRoot, f), "utf8"));
4090
4143
  } catch {
4091
4144
  return false;
4092
4145
  }
4093
4146
  });
4094
4147
  const projectMenuToggle = stylesheets.some((f) => {
4095
4148
  try {
4096
- return hasMenuToggle(readFileSync18(join23(opts.projectRoot, f), "utf8"));
4149
+ return hasMenuToggle(readFileSync19(join24(opts.projectRoot, f), "utf8"));
4097
4150
  } catch {
4098
4151
  return false;
4099
4152
  }
@@ -4101,14 +4154,14 @@ function check(opts) {
4101
4154
  const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
4102
4155
  const withStyles = files.filter((f) => {
4103
4156
  try {
4104
- const src = readFileSync18(join23(opts.projectRoot, f), "utf8");
4157
+ const src = readFileSync19(join24(opts.projectRoot, f), "utf8");
4105
4158
  return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
4106
4159
  } catch {
4107
4160
  return false;
4108
4161
  }
4109
4162
  }).length;
4110
4163
  const resolvedMode = resolveMode(opts.projectRoot);
4111
- const findings = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties);
4164
+ const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties);
4112
4165
  const TOKEN_RULE = {
4113
4166
  "--text-prose": "B-75",
4114
4167
  "--size-touch-target": "E-51"
@@ -4116,7 +4169,7 @@ function check(opts) {
4116
4169
  for (const problem of auditTokenLayer(opts.projectRoot)) {
4117
4170
  const ruleId = TOKEN_RULE[problem.token] ?? "C-19";
4118
4171
  const entry = index.find((e) => e.id === ruleId);
4119
- findings.push({
4172
+ detected.push({
4120
4173
  ruleId,
4121
4174
  detector: "token-layer",
4122
4175
  bucket: entry?.bucket ?? "mechanical",
@@ -4126,6 +4179,7 @@ function check(opts) {
4126
4179
  message: problem.message
4127
4180
  });
4128
4181
  }
4182
+ const { findings, waived } = applyWaivers(opts.projectRoot, detected);
4129
4183
  const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
4130
4184
  const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
4131
4185
  const noTokenLayer = (
@@ -4133,7 +4187,7 @@ function check(opts) {
4133
4187
  // told to add a token @import it has nowhere to put.
4134
4188
  cssFiles.length > 0 && cssFiles.every((f) => {
4135
4189
  try {
4136
- return !participatesInTokenLayer(readFileSync18(join23(opts.projectRoot, f), "utf8"), tokens);
4190
+ return !participatesInTokenLayer(readFileSync19(join24(opts.projectRoot, f), "utf8"), tokens);
4137
4191
  } catch {
4138
4192
  return true;
4139
4193
  }
@@ -4154,18 +4208,19 @@ function check(opts) {
4154
4208
  exempt,
4155
4209
  exemptPatterns: byPattern,
4156
4210
  scope: selection.mode,
4157
- modeUnwired: modeWiringProblems(opts.projectRoot)
4211
+ modeUnwired: modeWiringProblems(opts.projectRoot),
4212
+ waived
4158
4213
  });
4159
- return { findings, report, hasError };
4214
+ return { findings, report, hasError, waived };
4160
4215
  }
4161
4216
 
4162
4217
  // src/commands/init.ts
4163
- import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync22, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
4164
- import { dirname as dirname7, isAbsolute, join as join27, resolve as resolve2, sep as sep3 } from "path";
4218
+ import { existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
4219
+ import { dirname as dirname7, isAbsolute, join as join28, resolve as resolve2, sep as sep3 } from "path";
4165
4220
 
4166
4221
  // src/init/detect.ts
4167
- import { readFileSync as readFileSync19 } from "fs";
4168
- import { join as join24 } from "path";
4222
+ import { readFileSync as readFileSync20 } from "fs";
4223
+ import { join as join25 } from "path";
4169
4224
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
4170
4225
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
4171
4226
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -4182,7 +4237,7 @@ var FRAMEWORK_DEPS = [
4182
4237
  function detectFramework(projectRoot) {
4183
4238
  let pkg;
4184
4239
  try {
4185
- pkg = JSON.parse(readFileSync19(join24(projectRoot, "package.json"), "utf8"));
4240
+ pkg = JSON.parse(readFileSync20(join25(projectRoot, "package.json"), "utf8"));
4186
4241
  } catch {
4187
4242
  return void 0;
4188
4243
  }
@@ -4200,7 +4255,7 @@ function detect(projectRoot) {
4200
4255
  for (const f of cssFiles) {
4201
4256
  let source;
4202
4257
  try {
4203
- source = readFileSync19(join24(projectRoot, f), "utf8");
4258
+ source = readFileSync20(join25(projectRoot, f), "utf8");
4204
4259
  } catch {
4205
4260
  continue;
4206
4261
  }
@@ -4235,8 +4290,8 @@ function isIgnored(projectRoot, relPath) {
4235
4290
  }
4236
4291
 
4237
4292
  // src/init/derive.ts
4238
- import { readFileSync as readFileSync20 } from "fs";
4239
- import { join as join25 } from "path";
4293
+ import { readFileSync as readFileSync21 } from "fs";
4294
+ import { join as join26 } from "path";
4240
4295
  var DEFAULT_PROPOSAL = {
4241
4296
  h: 264,
4242
4297
  s: 0,
@@ -4269,7 +4324,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
4269
4324
  for (const file of cssFiles) {
4270
4325
  let source;
4271
4326
  try {
4272
- source = readFileSync20(join25(projectRoot, file), "utf8");
4327
+ source = readFileSync21(join26(projectRoot, file), "utf8");
4273
4328
  } catch {
4274
4329
  continue;
4275
4330
  }
@@ -4318,7 +4373,7 @@ function balancedBraceBody(source, openIdx) {
4318
4373
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
4319
4374
  let source;
4320
4375
  try {
4321
- source = readFileSync20(join25(projectRoot, tailwindConfigFile), "utf8");
4376
+ source = readFileSync21(join26(projectRoot, tailwindConfigFile), "utf8");
4322
4377
  } catch {
4323
4378
  return null;
4324
4379
  }
@@ -4374,7 +4429,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
4374
4429
  for (const file of cssFiles) {
4375
4430
  let source;
4376
4431
  try {
4377
- source = readFileSync20(join25(projectRoot, file), "utf8");
4432
+ source = readFileSync21(join26(projectRoot, file), "utf8");
4378
4433
  } catch {
4379
4434
  continue;
4380
4435
  }
@@ -4534,8 +4589,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
4534
4589
  }
4535
4590
 
4536
4591
  // src/init/project-name.ts
4537
- import { readFileSync as readFileSync21 } from "fs";
4538
- import { basename, join as join26 } from "path";
4592
+ import { readFileSync as readFileSync22 } from "fs";
4593
+ import { basename, join as join27 } from "path";
4539
4594
  function slugify(name) {
4540
4595
  const withoutScope = name.replace(/^@[^/]+\//, "");
4541
4596
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -4543,7 +4598,7 @@ function slugify(name) {
4543
4598
  }
4544
4599
  function deriveProjectSlug(projectRoot) {
4545
4600
  try {
4546
- const pkg = JSON.parse(readFileSync21(join26(projectRoot, "package.json"), "utf8"));
4601
+ const pkg = JSON.parse(readFileSync22(join27(projectRoot, "package.json"), "utf8"));
4547
4602
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
4548
4603
  } catch {
4549
4604
  }
@@ -4697,9 +4752,9 @@ function isValidSurfaceArray(v) {
4697
4752
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
4698
4753
  if (!existsSync15(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
4699
4754
  try {
4700
- const parsed = JSON.parse(readFileSync22(configAbsPath, "utf8"));
4755
+ const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
4701
4756
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
4702
- const brandResolves = brandCandidate ? existsSync15(join27(projectRoot, ...brandCandidate.split("/"))) : false;
4757
+ const brandResolves = brandCandidate ? existsSync15(join28(projectRoot, ...brandCandidate.split("/"))) : false;
4703
4758
  return {
4704
4759
  brand: brandResolves ? brandCandidate : fallbackBrand,
4705
4760
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -4712,7 +4767,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
4712
4767
  if (!existsSync15(configAbsPath)) return { path: null };
4713
4768
  let declared;
4714
4769
  try {
4715
- declared = JSON.parse(readFileSync22(configAbsPath, "utf8")).brand;
4770
+ declared = JSON.parse(readFileSync23(configAbsPath, "utf8")).brand;
4716
4771
  } catch {
4717
4772
  return { path: null };
4718
4773
  }
@@ -4742,7 +4797,7 @@ function defaultTokenDir(detection) {
4742
4797
  function readConfiguredSurfaces(configAbsPath) {
4743
4798
  if (!existsSync15(configAbsPath)) return null;
4744
4799
  try {
4745
- const parsed = JSON.parse(readFileSync22(configAbsPath, "utf8"));
4800
+ const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
4746
4801
  return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
4747
4802
  } catch {
4748
4803
  return null;
@@ -4752,7 +4807,7 @@ function isCssModule(path) {
4752
4807
  return /\.module\.(css|scss)$/i.test(path);
4753
4808
  }
4754
4809
  function legacyBrandFile(projectRoot) {
4755
- const dir = join27(projectRoot, ".jig", "tokens");
4810
+ const dir = join28(projectRoot, ".jig", "tokens");
4756
4811
  if (!existsSync15(dir)) return void 0;
4757
4812
  const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
4758
4813
  return found ? relKey(".jig", "tokens", found) : void 0;
@@ -4784,7 +4839,7 @@ var LEGACY_PAIR_RE = new RegExp(
4784
4839
  `@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
4785
4840
  );
4786
4841
  function wireBarrel(absPath, barrelImport) {
4787
- const content = existsSync15(absPath) ? readFileSync22(absPath, "utf8") : "";
4842
+ const content = existsSync15(absPath) ? readFileSync23(absPath, "utf8") : "";
4788
4843
  const line = `@import "${barrelImport}";`;
4789
4844
  if (content.includes(line)) return { status: "already-present" };
4790
4845
  const legacy = LEGACY_PAIR_RE.exec(content);
@@ -4872,7 +4927,7 @@ async function init(opts) {
4872
4927
  }
4873
4928
  }
4874
4929
  if (opts.yes) {
4875
- const declared = readConfiguredSurfaces(join27(opts.projectRoot, "jig.config.json"));
4930
+ const declared = readConfiguredSurfaces(join28(opts.projectRoot, "jig.config.json"));
4876
4931
  const inEffect = declared ?? surfaces;
4877
4932
  const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
4878
4933
  log(
@@ -4891,7 +4946,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
4891
4946
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
4892
4947
  const projectSlug = deriveProjectSlug(opts.projectRoot);
4893
4948
  const configRelPath = "jig.config.json";
4894
- const configAbsPath = join27(opts.projectRoot, configRelPath);
4949
+ const configAbsPath = join28(opts.projectRoot, configRelPath);
4895
4950
  const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
4896
4951
  if (configuredBrand.refused) {
4897
4952
  log(
@@ -4925,7 +4980,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
4925
4980
  }
4926
4981
  }
4927
4982
  const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
4928
- const brandAbsPath = join27(opts.projectRoot, ...brandRelPath.split("/"));
4983
+ const brandAbsPath = join28(opts.projectRoot, ...brandRelPath.split("/"));
4929
4984
  const tokensRelDir = brandRelPath.split("/").slice(0, -1);
4930
4985
  log(
4931
4986
  `Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
@@ -4958,7 +5013,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
4958
5013
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
4959
5014
  }
4960
5015
  if (brandAction === "written") {
4961
- const defaultCssRaw = readFileSync22(join27(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
5016
+ const defaultCssRaw = readFileSync23(join28(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
4962
5017
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
4963
5018
  mkdirSync6(dirname7(brandAbsPath), { recursive: true });
4964
5019
  writeFileSync5(brandAbsPath, content, "utf8");
@@ -4966,7 +5021,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
4966
5021
  }
4967
5022
  if (relocateTo && configAction !== "written") {
4968
5023
  try {
4969
- const existing = JSON.parse(readFileSync22(configAbsPath, "utf8"));
5024
+ const existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
4970
5025
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
4971
5026
  `;
4972
5027
  writeFileSync5(configAbsPath, content, "utf8");
@@ -4985,12 +5040,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
4985
5040
  } else if (configAction === "merged") {
4986
5041
  let existing = {};
4987
5042
  try {
4988
- existing = JSON.parse(readFileSync22(configAbsPath, "utf8"));
5043
+ existing = JSON.parse(readFileSync23(configAbsPath, "utf8"));
4989
5044
  } catch {
4990
5045
  existing = {};
4991
5046
  }
4992
5047
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
4993
- const brandStillResolves = existingBrand ? existsSync15(join27(opts.projectRoot, ...existingBrand.split("/"))) : false;
5048
+ const brandStillResolves = existingBrand ? existsSync15(join28(opts.projectRoot, ...existingBrand.split("/"))) : false;
4994
5049
  const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
4995
5050
  const merged = {
4996
5051
  brand: brandStillResolves ? existingBrand : brandRelPath,
@@ -5005,12 +5060,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5005
5060
  }
5006
5061
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
5007
5062
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
5008
- const wiringBrandAbsPath = join27(opts.projectRoot, ...effectiveConfig.brand.split("/"));
5063
+ const wiringBrandAbsPath = join28(opts.projectRoot, ...effectiveConfig.brand.split("/"));
5009
5064
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
5010
5065
  const modeAbsPaths = {};
5011
5066
  for (const mode of declaredModes) {
5012
5067
  const modeFileName = `mode.${mode}.css`;
5013
- const modeAbsPath2 = join27(opts.projectRoot, ...tokensRelDir, modeFileName);
5068
+ const modeAbsPath2 = join28(opts.projectRoot, ...tokensRelDir, modeFileName);
5014
5069
  modeAbsPaths[mode] = modeAbsPath2;
5015
5070
  const modeRelPath = relKey(...tokensRelDir, modeFileName);
5016
5071
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -5018,7 +5073,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5018
5073
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
5019
5074
  else if (modeState.modified) modeAction = "skipped-edited";
5020
5075
  if (modeAction === "written") {
5021
- const modeSourceRaw = readFileSync22(join27(opts.packageRoot, "tokens", modeFileName), "utf8");
5076
+ const modeSourceRaw = readFileSync23(join28(opts.packageRoot, "tokens", modeFileName), "utf8");
5022
5077
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
5023
5078
  mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
5024
5079
  writeFileSync5(modeAbsPath2, content, "utf8");
@@ -5032,7 +5087,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5032
5087
  const barrelFor = (mode) => mode === primaryMode ? "theme.css" : `theme.${mode}.css`;
5033
5088
  for (const mode of declaredModes) {
5034
5089
  const rel = relKey(...tokensRelDir, barrelFor(mode));
5035
- const abs = join27(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
5090
+ const abs = join28(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
5036
5091
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5037
5092
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5038
5093
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5047,7 +5102,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5047
5102
  const declared = declaredTokenNames(
5048
5103
  [brandAbsPath, modeAbsPath].map((f) => {
5049
5104
  try {
5050
- return readFileSync22(f, "utf8");
5105
+ return readFileSync23(f, "utf8");
5051
5106
  } catch {
5052
5107
  return "";
5053
5108
  }
@@ -5072,7 +5127,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5072
5127
  log(` This changes how components are written. Both styles are correct.`);
5073
5128
  const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
5074
5129
  if (answer === "y" || answer === "yes") {
5075
- const abs = join27(opts.projectRoot, ...tokensRelDir, "utilities.css");
5130
+ const abs = join28(opts.projectRoot, ...tokensRelDir, "utilities.css");
5076
5131
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5077
5132
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5078
5133
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5083,8 +5138,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5083
5138
  files[rel] = checksum(content);
5084
5139
  const target = findWireTarget(detection);
5085
5140
  const importPath = target ? relativeImportPath(
5086
- dirname7(join27(opts.projectRoot, target)),
5087
- join27(opts.projectRoot, ...tokensRelDir, "utilities.css")
5141
+ dirname7(join28(opts.projectRoot, target)),
5142
+ join28(opts.projectRoot, ...tokensRelDir, "utilities.css")
5088
5143
  ) : `./${rel}`;
5089
5144
  log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
5090
5145
  log(` @import "tailwindcss";`);
@@ -5102,11 +5157,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5102
5157
  writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
5103
5158
  if (relocateTo && currentBrand) {
5104
5159
  const oldDir = currentBrand.split("/").slice(0, -1);
5105
- const oldAbsDir = join27(opts.projectRoot, ...oldDir);
5160
+ const oldAbsDir = join28(opts.projectRoot, ...oldDir);
5106
5161
  if (existsSync15(oldAbsDir)) {
5107
5162
  for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
5108
5163
  const rel = relKey(...oldDir, name);
5109
- const abs = join27(oldAbsDir, name);
5164
+ const abs = join28(oldAbsDir, name);
5110
5165
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5111
5166
  if (!state.tracked || state.modified) {
5112
5167
  log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
@@ -5120,21 +5175,21 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5120
5175
  const wireTarget = findWireTarget(detection);
5121
5176
  let wiring;
5122
5177
  if (wireTarget) {
5123
- const targetAbsDir = dirname7(join27(opts.projectRoot, wireTarget));
5178
+ const targetAbsDir = dirname7(join28(opts.projectRoot, wireTarget));
5124
5179
  const barrelImport = relativeImportPath(
5125
5180
  targetAbsDir,
5126
- join27(opts.projectRoot, ...tokensRelDir, "theme.css")
5181
+ join28(opts.projectRoot, ...tokensRelDir, "theme.css")
5127
5182
  );
5128
5183
  const snippet = `@import "${barrelImport}";`;
5129
5184
  try {
5130
5185
  if (relocateTo && currentBrand) {
5131
- const abs = join27(opts.projectRoot, wireTarget);
5186
+ const abs = join28(opts.projectRoot, wireTarget);
5132
5187
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
5133
- const before = existsSync15(abs) ? readFileSync22(abs, "utf8") : "";
5188
+ const before = existsSync15(abs) ? readFileSync23(abs, "utf8") : "";
5134
5189
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
5135
5190
  if (after !== before) writeFileSync5(abs, after, "utf8");
5136
5191
  }
5137
- const outcome = wireBarrel(join27(opts.projectRoot, wireTarget), barrelImport);
5192
+ const outcome = wireBarrel(join28(opts.projectRoot, wireTarget), barrelImport);
5138
5193
  wiring = { target: wireTarget, status: outcome.status, snippet };
5139
5194
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
5140
5195
  log(`
@@ -5151,7 +5206,7 @@ Could not write to ${wireTarget}: ${err.message}`);
5151
5206
  } else {
5152
5207
  const barrelImport = relativeImportPath(
5153
5208
  opts.projectRoot,
5154
- join27(opts.projectRoot, ...tokensRelDir, "theme.css")
5209
+ join28(opts.projectRoot, ...tokensRelDir, "theme.css")
5155
5210
  );
5156
5211
  const snippet = `@import "${barrelImport}";`;
5157
5212
  wiring = { target: null, status: "print-only", snippet };
@@ -5160,7 +5215,7 @@ Could not write to ${wireTarget}: ${err.message}`);
5160
5215
  if (pasteTargets.length > 0) {
5161
5216
  log("Paste the block for whichever of these is your global stylesheet:");
5162
5217
  for (const target of pasteTargets) {
5163
- const dir = dirname7(join27(opts.projectRoot, target));
5218
+ const dir = dirname7(join28(opts.projectRoot, target));
5164
5219
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
5165
5220
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
5166
5221
  log(`
@@ -5207,28 +5262,28 @@ Could not write to ${wireTarget}: ${err.message}`);
5207
5262
  }
5208
5263
 
5209
5264
  // src/commands/verdicts.ts
5210
- import { existsSync as existsSync20, readFileSync as readFileSync26 } from "fs";
5211
- import { join as join33, resolve as resolve4 } from "path";
5265
+ import { existsSync as existsSync20, readFileSync as readFileSync27 } from "fs";
5266
+ import { join as join34, resolve as resolve4 } from "path";
5212
5267
 
5213
5268
  // src/rules/citations.ts
5214
- import { join as join28 } from "path";
5269
+ import { join as join29 } from "path";
5215
5270
  function citableIds(packageRoot2) {
5216
- const rulesDir = join28(packageRoot2, "rules");
5271
+ const rulesDir = join29(packageRoot2, "rules");
5217
5272
  const ids = [
5218
- ...loadRules(rulesDir, join28(packageRoot2, "rules.index.json")).map((r) => r.id),
5273
+ ...loadRules(rulesDir, join29(packageRoot2, "rules.index.json")).map((r) => r.id),
5219
5274
  ...loadSpecs(rulesDir).map((s) => s.id)
5220
5275
  ];
5221
5276
  return [...new Set(ids)].sort();
5222
5277
  }
5223
5278
 
5224
5279
  // src/probe/check.ts
5225
- import { existsSync as existsSync18, readdirSync as readdirSync13, readFileSync as readFileSync24 } from "fs";
5226
- import { join as join31 } from "path";
5280
+ import { existsSync as existsSync18, readdirSync as readdirSync13, readFileSync as readFileSync25 } from "fs";
5281
+ import { join as join32 } from "path";
5227
5282
 
5228
5283
  // src/probe/save.ts
5229
- import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync23, writeFileSync as writeFileSync6 } from "fs";
5284
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync24, writeFileSync as writeFileSync6 } from "fs";
5230
5285
  import { fileURLToPath as fileURLToPath2 } from "url";
5231
- import { join as join30, relative as relative3, resolve as resolve3 } from "path";
5286
+ import { join as join31, relative as relative3, resolve as resolve3 } from "path";
5232
5287
 
5233
5288
  // src/probe/script.ts
5234
5289
  var PROBE_VERSION = 5;
@@ -5375,7 +5430,7 @@ var PROBE_SCRIPT = `(async () => {
5375
5430
  import { spawn } from "child_process";
5376
5431
  import { existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
5377
5432
  import { homedir, tmpdir } from "os";
5378
- import { join as join29 } from "path";
5433
+ import { join as join30 } from "path";
5379
5434
  var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
5380
5435
  var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
5381
5436
  var CHROME_APPS = [
@@ -5387,10 +5442,10 @@ var CHROME_APPS = [
5387
5442
  ];
5388
5443
  function cachedBrowsers() {
5389
5444
  const roots = [
5390
- join29(homedir(), ".cache", "ms-playwright"),
5391
- join29(homedir(), "Library", "Caches", "ms-playwright"),
5392
- join29(homedir(), "AppData", "Local", "ms-playwright"),
5393
- join29(homedir(), ".cache", "puppeteer", "chrome")
5445
+ join30(homedir(), ".cache", "ms-playwright"),
5446
+ join30(homedir(), "Library", "Caches", "ms-playwright"),
5447
+ join30(homedir(), "AppData", "Local", "ms-playwright"),
5448
+ join30(homedir(), ".cache", "puppeteer", "chrome")
5394
5449
  ];
5395
5450
  const found = [];
5396
5451
  for (const root of roots) {
@@ -5410,7 +5465,7 @@ function cachedBrowsers() {
5410
5465
  ["chrome-win", "chrome.exe"],
5411
5466
  ["chrome-win64", "chrome.exe"]
5412
5467
  ]) {
5413
- const candidate = join29(root, entry, ...rel);
5468
+ const candidate = join30(root, entry, ...rel);
5414
5469
  if (existsSync16(candidate)) found.push(candidate);
5415
5470
  }
5416
5471
  }
@@ -5433,7 +5488,7 @@ function which(command) {
5433
5488
  const dirs = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
5434
5489
  for (const dir of dirs) {
5435
5490
  if (!dir) continue;
5436
- const candidate = join29(dir, command);
5491
+ const candidate = join30(dir, command);
5437
5492
  if (existsSync16(candidate)) return candidate;
5438
5493
  }
5439
5494
  return void 0;
@@ -5445,7 +5500,7 @@ async function runProbe(opts) {
5445
5500
  "No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
5446
5501
  );
5447
5502
  }
5448
- const profile = mkdtempSync(join29(tmpdir(), "jig-probe-"));
5503
+ const profile = mkdtempSync(join30(tmpdir(), "jig-probe-"));
5449
5504
  const child = spawn(chrome, [
5450
5505
  "--headless=new",
5451
5506
  "--remote-debugging-port=0",
@@ -5558,11 +5613,11 @@ function saveProbe(opts) {
5558
5613
  throw new Error(`The probe ran at ${url || "no url"}, which is not a file in this project. Open the page you built \u2014 a file:// URL under the project \u2014 and probe that.`);
5559
5614
  }
5560
5615
  probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
5561
- probe.pageChecksum = checksum(readFileSync23(page, "utf8"));
5616
+ probe.pageChecksum = checksum(readFileSync24(page, "utf8"));
5562
5617
  probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
5563
- const dir = join30(opts.projectRoot, ".jig", "critique", opts.surface);
5618
+ const dir = join31(opts.projectRoot, ".jig", "critique", opts.surface);
5564
5619
  mkdirSync7(dir, { recursive: true });
5565
- const path = join30(dir, `probe-${width}.json`);
5620
+ const path = join31(dir, `probe-${width}.json`);
5566
5621
  writeFileSync6(path, JSON.stringify(probe), "utf8");
5567
5622
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
5568
5623
  }
@@ -5591,17 +5646,17 @@ async function runAndSaveProbes(opts) {
5591
5646
  return saved;
5592
5647
  }
5593
5648
  async function ensureProbes(opts) {
5594
- const dir = join30(opts.projectRoot, ".jig", "critique", opts.surface);
5649
+ const dir = join31(opts.projectRoot, ".jig", "critique", opts.surface);
5595
5650
  const abs = resolve3(opts.projectRoot, opts.page);
5596
5651
  let current;
5597
5652
  try {
5598
- current = checksum(readFileSync23(abs, "utf8"));
5653
+ current = checksum(readFileSync24(abs, "utf8"));
5599
5654
  } catch {
5600
5655
  return { recorded: [], reason: `${opts.page} could not be read` };
5601
5656
  }
5602
5657
  const missing = PROBE_WIDTHS.filter((width) => {
5603
5658
  try {
5604
- const probe = JSON.parse(readFileSync23(join30(dir, `probe-${width}.json`), "utf8"));
5659
+ const probe = JSON.parse(readFileSync24(join31(dir, `probe-${width}.json`), "utf8"));
5605
5660
  return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
5606
5661
  } catch {
5607
5662
  return true;
@@ -5616,9 +5671,9 @@ async function ensureProbes(opts) {
5616
5671
  return { recorded: missing };
5617
5672
  }
5618
5673
  function critiquedSurfaces(projectRoot) {
5619
- const root = join30(projectRoot, ".jig", "critique");
5674
+ const root = join31(projectRoot, ".jig", "critique");
5620
5675
  if (!existsSync17(root)) return [];
5621
- return readdirSync12(root).filter((surface) => existsSync17(join30(root, surface, "screen.json")) || existsSync17(join30(root, surface, "code.json")));
5676
+ return readdirSync12(root).filter((surface) => existsSync17(join31(root, surface, "screen.json")) || existsSync17(join31(root, surface, "code.json")));
5622
5677
  }
5623
5678
 
5624
5679
  // src/probe/check.ts
@@ -5630,7 +5685,7 @@ function stampProblem(projectRoot, file, p) {
5630
5685
  if (!page) return `${file} names a page outside this project (${p.url ?? "no url"}).`;
5631
5686
  let current;
5632
5687
  try {
5633
- current = readFileSync24(page, "utf8");
5688
+ current = readFileSync25(page, "utf8");
5634
5689
  } catch {
5635
5690
  return `${file} was taken on ${p.pageFile}, which no longer exists.`;
5636
5691
  }
@@ -5644,7 +5699,7 @@ function readProbes(projectRoot, dir, errors) {
5644
5699
  const probes = [];
5645
5700
  for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
5646
5701
  try {
5647
- const p = JSON.parse(readFileSync24(join31(dir, f), "utf8"));
5702
+ const p = JSON.parse(readFileSync25(join32(dir, f), "utf8"));
5648
5703
  if (p.jigProbe !== PROBE_VERSION || typeof p.width !== "number") {
5649
5704
  errors.push(`${f} is not output of \`jig probe\` (version ${PROBE_VERSION}). Re-run the probe; do not write it by hand.`);
5650
5705
  continue;
@@ -5736,13 +5791,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
5736
5791
  }
5737
5792
 
5738
5793
  // src/check/decisions.ts
5739
- import { existsSync as existsSync19, readFileSync as readFileSync25 } from "fs";
5740
- import { join as join32 } from "path";
5794
+ import { existsSync as existsSync19, readFileSync as readFileSync26 } from "fs";
5795
+ import { join as join33, posix as posix3 } from "path";
5741
5796
  var HEADING3 = /^(#{2,3})\s+(.+?)\s*$/gm;
5742
5797
  var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
5743
5798
  function decisionsFile(projectRoot) {
5744
- for (const candidate of ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"]) {
5745
- if (existsSync19(join32(projectRoot, candidate))) return candidate;
5799
+ const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
5800
+ try {
5801
+ const brand = JSON.parse(readFileSync26(join33(projectRoot, "jig.config.json"), "utf8")).brand;
5802
+ if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
5803
+ } catch {
5804
+ }
5805
+ for (const candidate of candidates) {
5806
+ if (existsSync19(join33(projectRoot, candidate))) return candidate;
5746
5807
  }
5747
5808
  return void 0;
5748
5809
  }
@@ -5751,7 +5812,7 @@ function decisionNames(projectRoot) {
5751
5812
  if (!path) return [];
5752
5813
  let body;
5753
5814
  try {
5754
- body = readFileSync25(join32(projectRoot, path), "utf8");
5815
+ body = readFileSync26(join33(projectRoot, path), "utf8");
5755
5816
  } catch {
5756
5817
  return [];
5757
5818
  }
@@ -5770,17 +5831,17 @@ var ABSENCE = /\b(rule (not found|does not exist)|context unavailable|cannot (fi
5770
5831
  function readJson(path, errors) {
5771
5832
  if (!existsSync20(path)) return null;
5772
5833
  try {
5773
- return JSON.parse(readFileSync26(path, "utf8"));
5834
+ return JSON.parse(readFileSync27(path, "utf8"));
5774
5835
  } catch (e) {
5775
5836
  errors.push(`${path}: not valid JSON (${e.message})`);
5776
5837
  return {};
5777
5838
  }
5778
5839
  }
5779
5840
  function specIndexable(projectRoot, surface) {
5780
- const path = join33(projectRoot, ".jig", "specs", `${surface}.spec.md`);
5841
+ const path = join34(projectRoot, ".jig", "specs", `${surface}.spec.md`);
5781
5842
  let front = "";
5782
5843
  try {
5783
- front = readFileSync26(path, "utf8").split(/^---\s*$/m)[1] ?? "";
5844
+ front = readFileSync27(path, "utf8").split(/^---\s*$/m)[1] ?? "";
5784
5845
  } catch {
5785
5846
  }
5786
5847
  const declared = /^\s*indexable\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
@@ -5790,9 +5851,9 @@ function specIndexable(projectRoot, surface) {
5790
5851
  return mode !== "product" && mode !== "operator";
5791
5852
  }
5792
5853
  function specNeedsNav(projectRoot, surface) {
5793
- const path = join33(projectRoot, ".jig", "specs", `${surface}.spec.md`);
5854
+ const path = join34(projectRoot, ".jig", "specs", `${surface}.spec.md`);
5794
5855
  if (!existsSync20(path)) return false;
5795
- const front = readFileSync26(path, "utf8").split(/^---\s*$/m)[1] ?? "";
5856
+ const front = readFileSync27(path, "utf8").split(/^---\s*$/m)[1] ?? "";
5796
5857
  const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
5797
5858
  const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
5798
5859
  return navField || navRegion;
@@ -5843,7 +5904,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
5843
5904
  function checkDecisions(projectRoot, dir, errors) {
5844
5905
  const required = decisionNames(projectRoot);
5845
5906
  if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
5846
- const file = readJson(join33(dir, "decisions.json"), errors);
5907
+ const file = readJson(join34(dir, "decisions.json"), errors);
5847
5908
  if (!file) {
5848
5909
  errors.push(
5849
5910
  `decisions.json is missing. Every decision in DECISIONS.md is judged against the built page, one verdict each: ${required.slice(0, 4).join(", ")}${required.length > 4 ? `, and ${required.length - 4} more` : ""}. A page can satisfy every rule and still break what this project decided.`
@@ -5888,7 +5949,7 @@ function checkDecisions(projectRoot, dir, errors) {
5888
5949
  function verifyVerdicts(opts) {
5889
5950
  const root = opts.packageRoot ?? assetRoot();
5890
5951
  const errors = [];
5891
- const index = JSON.parse(readFileSync26(join33(root, "rules.index.json"), "utf8"));
5952
+ const index = JSON.parse(readFileSync27(join34(root, "rules.index.json"), "utf8"));
5892
5953
  const judgment = index.filter((r) => r.bucket === "judgment");
5893
5954
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
5894
5955
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
@@ -5897,9 +5958,9 @@ function verifyVerdicts(opts) {
5897
5958
  ...judgment.map((r) => [r.id, r.pass ?? ""])
5898
5959
  ]);
5899
5960
  const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
5900
- const dir = join33(opts.projectRoot, ".jig", "critique", opts.surface);
5901
- const screenFile = readJson(join33(dir, "screen.json"), errors);
5902
- const codeFile = readJson(join33(dir, "code.json"), errors);
5961
+ const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
5962
+ const screenFile = readJson(join34(dir, "screen.json"), errors);
5963
+ const codeFile = readJson(join34(dir, "code.json"), errors);
5903
5964
  const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
5904
5965
  const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors);
5905
5966
  const code = checkArm("code", codeFile, codeIds, passOf, specIds, [], errors);
@@ -5934,20 +5995,20 @@ function verifyVerdicts(opts) {
5934
5995
  }
5935
5996
 
5936
5997
  // src/commands/gate.ts
5937
- import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync28, writeFileSync as writeFileSync7 } from "fs";
5998
+ import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync29, writeFileSync as writeFileSync7 } from "fs";
5938
5999
  import { createHash as createHash2 } from "crypto";
5939
- import { join as join35 } from "path";
6000
+ import { join as join36 } from "path";
5940
6001
 
5941
6002
  // src/check/spec-shape.ts
5942
- import { existsSync as existsSync21, readFileSync as readFileSync27, readdirSync as readdirSync14, statSync as statSync2 } from "fs";
5943
- import { join as join34 } from "path";
6003
+ import { existsSync as existsSync21, readFileSync as readFileSync28, readdirSync as readdirSync14, statSync as statSync2 } from "fs";
6004
+ import { join as join35 } from "path";
5944
6005
  function newestSpec(projectRoot) {
5945
- const dir = join34(projectRoot, ".jig", "specs");
6006
+ const dir = join35(projectRoot, ".jig", "specs");
5946
6007
  if (!existsSync21(dir)) return void 0;
5947
6008
  const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
5948
6009
  if (files.length === 0) return void 0;
5949
- const newest = files.map((f) => ({ f, at: statSync2(join34(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
5950
- return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync27(join34(dir, newest), "utf8") };
6010
+ const newest = files.map((f) => ({ f, at: statSync2(join35(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
6011
+ return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync28(join35(dir, newest), "utf8") };
5951
6012
  }
5952
6013
  function specProblems(spec) {
5953
6014
  const parts = spec.body.split(/^---\s*$/m);
@@ -6019,7 +6080,7 @@ function lastJigCommand(transcriptPath) {
6019
6080
  if (!transcriptPath || !existsSync22(transcriptPath)) return void 0;
6020
6081
  let text;
6021
6082
  try {
6022
- text = readFileSync28(transcriptPath, "utf8");
6083
+ text = readFileSync29(transcriptPath, "utf8");
6023
6084
  } catch {
6024
6085
  return void 0;
6025
6086
  }
@@ -6033,14 +6094,41 @@ function lastJigCommand(transcriptPath) {
6033
6094
  }
6034
6095
  return found;
6035
6096
  }
6097
+ function lastAssistantText(transcriptPath) {
6098
+ if (!transcriptPath || !existsSync22(transcriptPath)) return void 0;
6099
+ let found;
6100
+ try {
6101
+ for (const line of readFileSync29(transcriptPath, "utf8").split("\n")) {
6102
+ if (!line.includes('"assistant"')) continue;
6103
+ let entry;
6104
+ try {
6105
+ entry = JSON.parse(line);
6106
+ } catch {
6107
+ continue;
6108
+ }
6109
+ if (entry.type !== "assistant" || !Array.isArray(entry.message?.content)) continue;
6110
+ const text = entry.message.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("");
6111
+ if (text.trim()) found = text;
6112
+ }
6113
+ } catch {
6114
+ return void 0;
6115
+ }
6116
+ return found;
6117
+ }
6118
+ function asksOwner(text) {
6119
+ if (!text) return false;
6120
+ const last = text.trim().split("\n").filter((l) => l.trim()).pop() ?? "";
6121
+ return /\?\s*$/.test(last.replace(/[*_`"')\]\s]+$/, ""));
6122
+ }
6123
+ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup"]);
6036
6124
  function commandProblems(root, command) {
6037
6125
  const problems = [];
6038
6126
  const spec = newestSpec(root);
6039
6127
  if (command === "decide") {
6040
- const file = ["jig/DECISIONS.md", "DECISIONS.md", ".jig/DECISIONS.md"].map((p) => join35(root, p)).find((p) => existsSync22(p));
6041
- if (!file) problems.push("decide wrote no DECISIONS.md beside the token layer.");
6128
+ const found = decisionsFile(root);
6129
+ if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
6042
6130
  else {
6043
- const body = readFileSync28(file, "utf8");
6131
+ const body = readFileSync29(join36(root, found), "utf8");
6044
6132
  if (!/^##\s+Unresolved\s*$/im.test(body)) {
6045
6133
  problems.push("DECISIONS.md has no `## Unresolved` section. Round 3 asks by name what is still undecided; write what the owner named, or `None named by the owner.`");
6046
6134
  }
@@ -6061,13 +6149,13 @@ function commandProblems(root, command) {
6061
6149
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
6062
6150
  if (/^approved/i.test(mockup)) {
6063
6151
  if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
6064
- else if (!/^https?:/i.test(at) && !existsSync22(join35(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
6152
+ else if (!/^https?:/i.test(at) && !existsSync22(join36(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
6065
6153
  else if (!/^https?:/i.test(at) && !at.startsWith(".jig/mockups/")) problems.push(`The drawing is at ${at}. A mockup lives in .jig/mockups/, outside what check scans and outside what ships.`);
6066
6154
  }
6067
6155
  }
6068
6156
  if (command === "critique") {
6069
- const dir = join35(root, ".jig", "critique");
6070
- const surfaces = existsSync22(dir) ? readdirSync15(dir).filter((s) => existsSync22(join35(dir, s, "screen.json")) || existsSync22(join35(dir, s, "code.json"))) : [];
6157
+ const dir = join36(root, ".jig", "critique");
6158
+ const surfaces = existsSync22(dir) ? readdirSync15(dir).filter((s) => existsSync22(join36(dir, s, "screen.json")) || existsSync22(join36(dir, s, "code.json"))) : [];
6071
6159
  for (const surface of surfaces) {
6072
6160
  const v = verifyVerdicts({ projectRoot: root, surface });
6073
6161
  if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
@@ -6089,36 +6177,43 @@ function surfacePage(projectRoot, surface) {
6089
6177
  const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
6090
6178
  const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
6091
6179
  for (const candidate of candidates) {
6092
- if (candidate && existsSync22(join35(projectRoot, candidate))) return candidate;
6180
+ if (candidate && existsSync22(join36(projectRoot, candidate))) return candidate;
6093
6181
  }
6094
6182
  return void 0;
6095
6183
  }
6096
6184
  function gate(opts) {
6097
6185
  const root = opts.projectRoot;
6098
- if (!existsSync22(join35(root, "jig.config.json")) && !existsSync22(join35(root, ".jig"))) {
6186
+ if (!existsSync22(join36(root, "jig.config.json")) && !existsSync22(join36(root, ".jig"))) {
6099
6187
  return { block: false, reason: "" };
6100
6188
  }
6101
6189
  const command = lastJigCommand(opts.input.transcript_path);
6102
- const problems = command ? commandProblems(root, command).map((p) => `/jig ${command}: ${p}`) : [];
6190
+ const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
6191
+ const problems = command && !waiting ? commandProblems(root, command).map((p) => `/jig ${command}: ${p}`) : [];
6103
6192
  const selection = selectFiles(root, false);
6104
- const changedStyles = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f));
6105
- if (changedStyles) {
6193
+ const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
6194
+ if (changedUi) {
6106
6195
  const result = check({ projectRoot: root, homeDir: "", version: opts.version, all: false, ci: false });
6107
6196
  const errors = result.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error");
6108
- if (errors.length > 0) {
6109
- const shown = errors.slice(0, 8).map((f) => ` ${f.ruleId} ${f.file}:${f.line} ${f.message}`);
6197
+ const warnings = result.findings.filter((f) => f.severity === "warning");
6198
+ const blocking = [...errors, ...warnings];
6199
+ if (blocking.length > 0) {
6200
+ const shown = blocking.slice(0, 8).map((f) => ` ${f.severity === "error" ? "error " : "warning"} ${f.ruleId} ${f.file}:${f.line} ${f.message}`);
6201
+ const counted = [
6202
+ errors.length > 0 ? `${errors.length} mechanical error(s)` : "",
6203
+ warnings.length > 0 ? `${warnings.length} warning(s)` : ""
6204
+ ].filter(Boolean).join(" and ");
6110
6205
  problems.push(
6111
- `jig check: ${errors.length} mechanical error(s) in the files you changed. Fix every one, then run \`jig check\` again.
6112
- ` + shown.join("\n") + (errors.length > shown.length ? `
6113
- \u2026 and ${errors.length - shown.length} more` : "")
6206
+ `jig check: ${counted} in the files you changed. Fix every one. A warning that is right as it stands can be waived on its own line with a comment reading \`jig-allow <ID>: <why>\`; an error cannot. Then run \`jig check\` again.
6207
+ ` + shown.join("\n") + (blocking.length > shown.length ? `
6208
+ \u2026 and ${blocking.length - shown.length} more` : "")
6114
6209
  );
6115
6210
  }
6116
6211
  }
6117
- const critiqueDir = join35(root, ".jig", "critique");
6212
+ const critiqueDir = join36(root, ".jig", "critique");
6118
6213
  if (existsSync22(critiqueDir)) {
6119
6214
  for (const surface of readdirSync15(critiqueDir)) {
6120
- const dir = join35(critiqueDir, surface);
6121
- if (!existsSync22(join35(dir, "screen.json")) && !existsSync22(join35(dir, "code.json"))) continue;
6215
+ const dir = join36(critiqueDir, surface);
6216
+ if (!existsSync22(join36(dir, "screen.json")) && !existsSync22(join36(dir, "code.json"))) continue;
6122
6217
  const v = verifyVerdicts({ projectRoot: root, surface });
6123
6218
  if (!v.ok) {
6124
6219
  const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
@@ -6132,10 +6227,10 @@ function gate(opts) {
6132
6227
  }
6133
6228
  const session = opts.input.session_id ?? "unknown";
6134
6229
  const key = `${session}:${createHash2("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
6135
- const stateFile = join35(root, ".jig", "gate.json");
6230
+ const stateFile = join36(root, ".jig", "gate.json");
6136
6231
  let state = {};
6137
6232
  try {
6138
- state = JSON.parse(readFileSync28(stateFile, "utf8"));
6233
+ state = JSON.parse(readFileSync29(stateFile, "utf8"));
6139
6234
  } catch {
6140
6235
  }
6141
6236
  if (problems.length === 0) {
@@ -6165,15 +6260,15 @@ ${problems.join("\n\n")}`
6165
6260
  }
6166
6261
  function save(file, state) {
6167
6262
  try {
6168
- mkdirSync8(join35(file, ".."), { recursive: true });
6263
+ mkdirSync8(join36(file, ".."), { recursive: true });
6169
6264
  writeFileSync7(file, JSON.stringify(state), "utf8");
6170
6265
  } catch {
6171
6266
  }
6172
6267
  }
6173
6268
 
6174
6269
  // src/commands/seo.ts
6175
- import { readFileSync as readFileSync29 } from "fs";
6176
- import { join as join36 } from "path";
6270
+ import { readFileSync as readFileSync30 } from "fs";
6271
+ import { join as join37 } from "path";
6177
6272
  var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
6178
6273
  var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
6179
6274
  var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
@@ -6214,7 +6309,7 @@ function seo(opts) {
6214
6309
  for (const file of files) {
6215
6310
  let raw;
6216
6311
  try {
6217
- raw = readFileSync29(join36(root, file), "utf8");
6312
+ raw = readFileSync30(join37(root, file), "utf8");
6218
6313
  } catch {
6219
6314
  continue;
6220
6315
  }
@@ -6270,7 +6365,7 @@ function seo(opts) {
6270
6365
 
6271
6366
  // src/index.ts
6272
6367
  var packageRoot = getPackageRoot();
6273
- var { version } = JSON.parse(readFileSync30(join37(packageRoot, "package.json"), "utf8"));
6368
+ var { version } = JSON.parse(readFileSync31(join38(packageRoot, "package.json"), "utf8"));
6274
6369
  function warnIfUnpublishedPin() {
6275
6370
  if (isPublishedBuild(packageRoot)) return;
6276
6371
  console.warn(
@@ -6315,6 +6410,12 @@ program.command("install").description("Install Jig rules and the agent skill fi
6315
6410
  console.warn(result.warning);
6316
6411
  return;
6317
6412
  }
6413
+ if (result.hookOnly) {
6414
+ console.log(result.hookOnly);
6415
+ if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
6416
+ if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
6417
+ return;
6418
+ }
6318
6419
  console.log(`Installed Jig v${version} for ${opts.agent} (${opts.scope} scope)`);
6319
6420
  warnIfUnpublishedPin();
6320
6421
  for (const f of result.written) console.log(` + ${f}`);
@@ -6456,7 +6557,7 @@ program.command("probe").description("Print the render probe. With --save, read
6456
6557
  program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
6457
6558
  let input = {};
6458
6559
  try {
6459
- if (!process.stdin.isTTY) input = JSON.parse(readFileSync30(0, "utf8") || "{}");
6560
+ if (!process.stdin.isTTY) input = JSON.parse(readFileSync31(0, "utf8") || "{}");
6460
6561
  } catch {
6461
6562
  }
6462
6563
  const cwd = input.cwd ?? process.cwd();
@@ -6495,7 +6596,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
6495
6596
  });
6496
6597
  function resolveSurfaceMode(projectRoot) {
6497
6598
  try {
6498
- const config = JSON.parse(readFileSync30(join37(projectRoot, "jig.config.json"), "utf8"));
6599
+ const config = JSON.parse(readFileSync31(join38(projectRoot, "jig.config.json"), "utf8"));
6499
6600
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
6500
6601
  return modes.length === 1 ? modes[0] : void 0;
6501
6602
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "A design system for coding agents. 130 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -759,8 +759,9 @@ menu and reported "matched exactly".
759
759
  **1. `check` passes.** Run `{{scripts_path}} check --all` and paste its
760
760
  `JIG_CHECK:` line into your report, as printed. `mechanical=fail:<n>` means not done:
761
761
  fix every error and run it again. No mechanical finding is non-blocking, and you do
762
- not get to decide one is. Warnings are listed and either fixed or explained one by
763
- one. A report with no `JIG_CHECK:` line, or one you typed yourself, is not a finished
762
+ not get to decide one is. Every warning is fixed, or waived on its own line with
763
+ `jig-allow <ID>: <why>` (see `gate`); an explanation in your report is not a waiver.
764
+ A report with no `JIG_CHECK:` line, or one you typed yourself, is not a finished
764
765
  build.
765
766
 
766
767
  **2. The build matches the approved mockup at each size.** Skip only when
@@ -1162,9 +1163,10 @@ safe to run on the first day and on somebody else's codebase. It audits the whol
1162
1163
  which is what one file cannot: a route whose metadata says `noindex` sitting in
1163
1164
  the sitemap, two pages claiming the same title, a sitemap that lists nothing or
1164
1165
  lists paths instead of full URLs. It counts the pages it found, a dynamic route
1165
- once, and whether a sitemap and a robots file exist; neither is a finding, since
1166
- no rule asks for them and a project with no domain yet cannot write an honest
1167
- sitemap. Do not invent one to clear a count.
1166
+ once and an endpoint (`rss.xml.ts`, `[id].txt.ts`, `api/`) not at all, since it
1167
+ serves no page. It also counts whether a sitemap and a robots file exist;
1168
+ neither is a finding, since no rule asks for them and a project with no domain
1169
+ yet cannot write an honest sitemap. Do not invent one to clear a count.
1168
1170
 
1169
1171
  It is not a phase. The per-page half belongs where the copy is decided and
1170
1172
  written — `spec` records whether a page is indexable and what it claims, `make`
@@ -1176,10 +1178,18 @@ positioning it had already dropped.
1176
1178
  ## gate
1177
1179
 
1178
1180
  Not a command to run. In Claude Code, `install` adds a Stop hook that runs
1179
- `{{scripts_path}} gate` whenever you try to finish. If files you changed fail `check`,
1180
- or a critique's verdict files fail `verdicts`, it refuses to let you stop and hands
1181
- you the failures as the reason. **Fix what it names.** Do not edit verdict files to
1182
- satisfy it, and do not report the work as done while it is blocking. After three
1181
+ `{{scripts_path}} gate` whenever you try to finish. If files you changed have a
1182
+ `check` error or warning, or a critique's verdict files fail `verdicts`, it refuses
1183
+ to let you stop and hands you the failures as the reason. **Fix what it names.** Do
1184
+ not edit verdict files to satisfy it, and do not report the work as done while it is
1185
+ blocking.
1186
+
1187
+ A warning you are sure is right as it stands, because the detector guessed and the
1188
+ guess is wrong here, is waived on its own line or the line above, in the file's
1189
+ comment syntax: `jig-allow <ID>: <why>`. The reason is what the user reads, so write
1190
+ the case, not "intended". An error cannot be waived, a waiver with no reason waives
1191
+ nothing, and `check` prints every waiver it honoured. Waiving a warning to get past
1192
+ the gate, when the fix was available, is the failure the gate exists to catch. After three
1183
1193
  refusals in one session it lets you stop; then tell the user plainly that the work is
1184
1194
  not finished and what is still failing.
1185
1195
 
@@ -14,18 +14,21 @@ cite the number when you follow or deliberately break one.
14
14
  b. `DECISIONS.md` exists beside the token files — `jig/DECISIONS.md` by
15
15
  default, or wherever `brand` in `{{config_file}}` puts them — and has
16
16
  substance: not empty, no `[TODO]` markers left in it, more than a few
17
- lines. If it does not, run `{{command_prefix}}decide` and stop until it has
18
- been written. Do not write it yourself from this task's prompt — it records
19
- decisions the project has made, and a file you author alone records
20
- decisions *you* have made, which is the opposite thing wearing the same
21
- name.
17
+ lines. If it does not, stop and ask the user to run
18
+ `{{command_prefix}}decide`. The interview lives in that command, not in
19
+ this file: questions you make up from here are not `decide`, and neither
20
+ is a file written from their answers. Do not write it yourself from this
21
+ task's prompt — it records decisions the project has made, and a file you
22
+ author alone records decisions *you* have made, which is the opposite
23
+ thing wearing the same name.
22
24
  1. Load `{{rules_path}}/00-anti-patterns.md` and `{{rules_path}}/01-modes.md`.
23
25
  2. Determine the mode from `{{config_file}}`, or infer it using the procedure in
24
26
  `{{rules_path}}/01-modes.md` and state the inference in one line before building.
25
- 3. **Building a page, feature or functionality?** Run it through
27
+ 3. **Building a page, feature or functionality?** It goes through
26
28
  `{{command_prefix}}spec` → `{{command_prefix}}mockup` (low fidelity) →
27
29
  `{{command_prefix}}make` (high fidelity) → `{{command_prefix}}critique`, one at a
28
- time. Never design the whole product up front. Critique findings go back to
30
+ time, each one run by the user. The procedure for each step is in the command,
31
+ so ask for the step you need rather than doing it from this summary. Never design the whole product up front. Critique findings go back to
29
32
  `make`, then `critique` again, until clean or accepted by the user — then the
30
33
  next feature.
31
34
  **Building a screen rather than a single component?** Read `L-01 · Layout