@dzhechkov/p-replicator 1.9.0 → 1.10.1

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.
@@ -6,7 +6,7 @@ const { execFileSync } = require('child_process');
6
6
  const {
7
7
  green, red, yellow, cyan, bold, dim,
8
8
  info, success, warn, error: logError,
9
- readManifest, fileExists,
9
+ readManifest, fileExists, artifactState,
10
10
  MANIFEST_FILE, COMPONENTS,
11
11
  } = require('../utils');
12
12
 
@@ -29,6 +29,22 @@ function run(options) {
29
29
  function fail(msg) { console.log(` ${red('\u2717')} ${msg}`); issues++; }
30
30
  function hint(msg) { console.log(` ${yellow('!')} ${msg}`); warnings++; }
31
31
 
32
+ /**
33
+ * One reporter for every component, because a two-way branch cannot say three things.
34
+ *
35
+ * The first version of this change swapped the CONDITION to artifactState and left the else
36
+ * branch intact, so a whitespace-only artifact was reported as "missing" — the exact collapse
37
+ * AR-4 forbids. And settings.json and the hooks were still on fileExists, so a ZERO-BYTE
38
+ * settings.json (no hooks wired at all) and a zero-byte hook both received a checkmark.
39
+ */
40
+ function reportArtifact(label, filePath, missingNote) {
41
+ const state = artifactState(filePath);
42
+ if (state === 'present') { pass(label); return; }
43
+ if (state === 'empty') { fail(`${label} \u2014 EMPTY, cannot load`); return; }
44
+ fail(`${label} \u2014 missing${missingNote ? ' (' + missingNote + ')' : ''}`);
45
+ }
46
+
47
+
32
48
  console.log(bold('P-Replicator \u2014 Health Check'));
33
49
  console.log('');
34
50
 
@@ -59,11 +75,7 @@ function run(options) {
59
75
  console.log(bold(`Skills (expected ${EXPECTED_SKILLS.length}):`));
60
76
  for (const skill of EXPECTED_SKILLS) {
61
77
  const skillPath = path.join(claudeDir, 'skills', skill, 'SKILL.md');
62
- if (fileExists(skillPath)) {
63
- pass(skill);
64
- } else {
65
- fail(`${skill} \u2014 SKILL.md missing`);
66
- }
78
+ reportArtifact(skill, skillPath, 'SKILL.md');
67
79
  }
68
80
  console.log('');
69
81
 
@@ -71,11 +83,7 @@ function run(options) {
71
83
  console.log(bold(`Commands (expected ${EXPECTED_COMMANDS.length}):`));
72
84
  for (const cmd of EXPECTED_COMMANDS) {
73
85
  const cmdPath = path.join(claudeDir, 'commands', `${cmd}.md`);
74
- if (fileExists(cmdPath)) {
75
- pass(`/${cmd}`);
76
- } else {
77
- fail(`/${cmd} \u2014 ${cmd}.md missing`);
78
- }
86
+ reportArtifact(`/${cmd}`, cmdPath, undefined);
79
87
  }
80
88
  console.log('');
81
89
 
@@ -83,11 +91,7 @@ function run(options) {
83
91
  console.log(bold(`Agents (expected ${EXPECTED_AGENTS.length}):`));
84
92
  for (const agent of EXPECTED_AGENTS) {
85
93
  const agentPath = path.join(claudeDir, 'agents', `${agent}.md`);
86
- if (fileExists(agentPath)) {
87
- pass(agent);
88
- } else {
89
- fail(`${agent} \u2014 ${agent}.md missing`);
90
- }
94
+ reportArtifact(agent, agentPath, undefined);
91
95
  }
92
96
  console.log('');
93
97
 
@@ -95,22 +99,14 @@ function run(options) {
95
99
  console.log(bold(`Rules (expected ${EXPECTED_RULES.length}):`));
96
100
  for (const rule of EXPECTED_RULES) {
97
101
  const rulePath = path.join(claudeDir, 'rules', `${rule}.md`);
98
- if (fileExists(rulePath)) {
99
- pass(rule);
100
- } else {
101
- fail(`${rule} \u2014 ${rule}.md missing`);
102
- }
102
+ reportArtifact(rule, rulePath, undefined);
103
103
  }
104
104
  console.log('');
105
105
 
106
106
  // ── 6b) Settings (hooks config) ─────────────────────────────────────────
107
107
  console.log(bold('Hooks (settings.json):'));
108
108
  const settingsPath = path.join(claudeDir, 'settings.json');
109
- if (fileExists(settingsPath)) {
110
- pass('settings.json');
111
- } else {
112
- fail('settings.json — missing (SessionStart + Stop hooks not configured)');
113
- }
109
+ reportArtifact('settings.json', settingsPath, 'SessionStart + Stop hooks not configured');
114
110
  console.log('');
115
111
 
116
112
  // ── 6c) Hook scripts (cross-platform Node, v1.4.1+) ─────────────────────
@@ -119,11 +115,7 @@ function run(options) {
119
115
  console.log(bold(`Hook scripts (expected ${hookKeys.length}):`));
120
116
  for (const hook of hookKeys) {
121
117
  const hookPath = path.join(claudeDir, 'hooks', `${hook}.cjs`);
122
- if (fileExists(hookPath)) {
123
- pass(`${hook}.cjs`);
124
- } else {
125
- fail(`${hook}.cjs — missing (referenced by settings.json)`);
126
- }
118
+ reportArtifact(`${hook}.cjs`, hookPath, 'referenced by settings.json');
127
119
  }
128
120
  console.log('');
129
121
  }
@@ -148,6 +140,26 @@ function run(options) {
148
140
  }
149
141
  console.log('');
150
142
 
143
+ // ── The insights carrier: THREE states, and none of them fails ───────────
144
+ //
145
+ // A project that has recorded no insight is a NEW project, and a check that refuses a new project
146
+ // is a check people disable — taking the real signal with it. The defect was never the emptiness:
147
+ // it was that ABSENT and EMPTY rendered identically, so a carrier that had never been created
148
+ // looked exactly like one being used and found empty. MEASURED across four real projects on one
149
+ // machine: 11 -> 10 -> 6 -> 0 insights, with every surface reporting OK throughout.
150
+ const insightsIndex = path.join(claudeDir, 'insights', 'index.md');
151
+ const insightsState = artifactState(insightsIndex);
152
+ if (insightsState === 'missing') {
153
+ hint('insights carrier: NOT STARTED — no .claude/insights/index.md. Record one with /myinsights');
154
+ } else if (insightsState === 'empty') {
155
+ hint('insights carrier: EXISTS but holds ZERO entries — nothing is injected at SessionStart');
156
+ } else {
157
+ const entries = (fs.readFileSync(insightsIndex, 'utf-8')
158
+ .match(/^##\s+\d{4}-\d{2}-\d{2}/gm) || []).length;
159
+ pass('insights carrier: ' + entries + ' entr' + (entries === 1 ? 'y' : 'ies') + ' recorded');
160
+ }
161
+ console.log('');
162
+
151
163
  // ── 8) Summary ──────────────────────────────────────────────────────────
152
164
  console.log(bold('\u2500'.repeat(50)));
153
165
  if (issues === 0 && warnings === 0) {
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const {
5
5
  green, red, yellow, cyan, bold, dim,
6
6
  info, success, error: logError,
7
- readManifest, fileExists,
7
+ readManifest, fileExists, artifactState,
8
8
  COMPONENTS, getItemRelativePath,
9
9
  } = require('../utils');
10
10
 
@@ -44,8 +44,14 @@ function run(options) {
44
44
  const label = groupKey === 'commands'
45
45
  ? `/${itemKey}`
46
46
  : `${groupKey}: ${itemKey}`;
47
- if (fileExists(full)) {
47
+ const state = artifactState(full);
48
+ if (state === 'present') {
48
49
  pass(`${label} ${dim('— ' + desc)}`);
50
+ } else if (state === 'empty') {
51
+ // NOT "missing". The cures differ: missing -> run `update`; empty -> something truncated
52
+ // your file, and `update` would silently repair it without you ever learning that. Naming
53
+ // both "missing" moves the silence up one level instead of removing it.
54
+ fail(`${label} — EMPTY, cannot load (${rel})`);
49
55
  } else {
50
56
  fail(`${label} — missing (${rel})`);
51
57
  }
@@ -91,6 +97,25 @@ function run(options) {
91
97
  console.log('');
92
98
 
93
99
  // ── Summary ─────────────────────────────────────────────────────────────
100
+ // The insights carrier: three states, none of them failing.
101
+ //
102
+ // FR-5 asks for three SURFACES, and the first pass shipped two — doctor and the statusline —
103
+ // leaving verify producing identical output for absent, empty and populated. Cross-family review
104
+ // caught it. A carrier that never existed must not read like one being used and found empty:
105
+ // that indistinguishability is what let 27 recorded insights become 0 across four real projects.
106
+ const insightsIndex = path.join(targetDir, '.claude', 'insights', 'index.md');
107
+ const insightsState = artifactState(insightsIndex);
108
+ if (insightsState === 'missing') {
109
+ hint('insights carrier: NOT STARTED — no .claude/insights/index.md. Record one with /myinsights');
110
+ } else if (insightsState === 'empty') {
111
+ hint('insights carrier: EXISTS but holds ZERO entries — nothing is injected at SessionStart');
112
+ } else {
113
+ const entries = (require('fs').readFileSync(insightsIndex, 'utf-8')
114
+ .match(/^##\s+\d{4}-\d{2}-\d{2}/gm) || []).length;
115
+ pass(`insights carrier: ${entries} entr${entries === 1 ? 'y' : 'ies'} recorded`);
116
+ }
117
+ console.log('');
118
+
94
119
  console.log(bold('─'.repeat(60)));
95
120
  if (issues === 0 && warnings === 0) {
96
121
  success(bold('All artifacts verified.'));
package/src/utils.js CHANGED
@@ -94,6 +94,30 @@ function copyDirFiltered(src, dest, filterFn) {
94
94
  /**
95
95
  * Returns true if the path exists.
96
96
  */
97
+ /**
98
+ * Three states, because two were not enough.
99
+ *
100
+ * `fileExists` answers about PRESENCE and is deliberately left alone — it has 31 call sites, several
101
+ * of which ask a genuine presence question about files that may legitimately hold nothing. This is
102
+ * the predicate for the different question: is this artifact USABLE?
103
+ *
104
+ * MEASURED 2026-08-27: 31 artifacts truncated to zero bytes — every SKILL.md, command, rule and
105
+ * agent — and both `verify` and `doctor` reported clean with exit 0. Deleting one was caught. The
106
+ * gap was exactly this: accessSync asks whether the path resolves, never what is in it.
107
+ *
108
+ * Whitespace counts as empty. A file holding a newline is exactly as dead as one holding nothing,
109
+ * and a size check alone would pass it.
110
+ */
111
+ function artifactState(filePath) {
112
+ let body;
113
+ try {
114
+ body = fs.readFileSync(filePath, 'utf-8');
115
+ } catch {
116
+ return 'missing';
117
+ }
118
+ return body.trim().length === 0 ? 'empty' : 'present';
119
+ }
120
+
97
121
  function fileExists(filePath) {
98
122
  try {
99
123
  fs.accessSync(filePath);
@@ -524,6 +548,7 @@ function getItemRelativePath(comp, itemKey) {
524
548
  // ===========================================================================
525
549
 
526
550
  module.exports = {
551
+ artifactState,
527
552
  // Colors
528
553
  green, red, yellow, blue, cyan, bold, dim, gray,
529
554
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Capture and recall development insights. Append a new insight to `.claude/insights/index.md` with structured fields (problem, solution, tags). Auto-injected into context on SessionStart for relevant tasks.
2
+ description: Capture and recall development insights. Append a new insight to `.claude/insights/index.md` with structured fields (problem, solution, tags). The three most recent are injected into context at SessionStart.
3
3
  argument-hint: '[recall <query> | <free-form insight>]'
4
4
  ---
5
5
 
@@ -8,9 +8,24 @@ argument-hint: '[recall <query> | <free-form insight>]'
8
8
  ## Purpose
9
9
 
10
10
  Build a project-local knowledge base of "грабли" (rakes) — errors, workarounds,
11
- discoveries — so they don't have to be re-learned. Insights are auto-loaded
12
- into Claude Code context on each session start (via `SessionStart` hook in
13
- `.claude/settings.json`) when their tags match the current task.
11
+ discoveries — so they don't have to be re-learned.
12
+
13
+ **What actually happens, stated exactly.** The `SessionStart` hook
14
+ (`.claude/hooks/session-insights.cjs`, wired in `.claude/settings.json`) reads
15
+ `.claude/insights/index.md` and injects the **three most recent entries**, by their
16
+ order in the file. It prints them under the heading *"Recent project insights"* —
17
+ which is what they are.
18
+
19
+ **There is no tag matching, and it is not an omission.** The hook fires at
20
+ `SessionStart`, BEFORE you have said anything, so there is no current task to match
21
+ tags against. Tags remain useful for a human reading or grepping the file, and for
22
+ `/myinsights recall <query>`, which searches on demand — when a query exists.
23
+
24
+ **The consequence, so nobody is surprised by it.** The file is append-only and the
25
+ hook takes the LAST three. As a project accumulates entries — `insights-capture.md`
26
+ plans for 50+ — earlier ones stop being injected. Selection by relevance would need
27
+ to happen at a moment when a task is known; that is a separate design question, and
28
+ it is filed rather than quietly implied here.
14
29
 
15
30
  ## Modes
16
31
 
@@ -69,4 +84,6 @@ mistakes without manual recall.
69
84
 
70
85
  - `.claude/rules/insights-capture.md` — when/how to capture
71
86
  - `.claude/hooks/session-insights.cjs` — session injection
72
- - `/harvest` — extracts reusable patterns from insights at project end
87
+ - `/harvest` — extracts reusable knowledge at project end. **Honest limit:** it does
88
+ NOT read `.claude/insights/index.md` today (`grep -ci insight` over `harvest.md`
89
+ returns 0). The capture→harvest link is a stated intention, not a wired path.
@@ -36,7 +36,13 @@ const DOCS = [
36
36
  { file: 'Refinement.md' },
37
37
  { file: 'Completion.md' },
38
38
  { file: 'Research_Findings.md' },
39
- { file: 'Final_Summary.md' },
39
+ // REPORTED, not required. MEASURED 2026-08-27 against a real completed /replicate project:
40
+ // 8 of 9 promised documents were produced and this one was NOT, though replicate.md and
41
+ // sparc-prd-mini both promise it (three places, including a whole SYNTHESIS phase). One project
42
+ // is not enough evidence to decide whether the pipeline is broken or the document is optional in
43
+ // practice — and blocking on it would have refused every project that ran like that one.
44
+ // The discrepancy is filed; until it is settled this reports rather than refuses.
45
+ { file: 'Final_Summary.md', optional: true, expected: true },
40
46
  { file: 'C4_Diagrams.md', optional: true },
41
47
  { file: 'ADR.md', optional: true },
42
48
  ];
@@ -88,6 +94,17 @@ const GAP = /\[GAP:[^\]\n]*\]/g;
88
94
 
89
95
  const SUSPECT = /\[[^\]\n]{1,80}\](?![(\[])/g;
90
96
 
97
+ /**
98
+ * A markdown TASK-LIST CHECKBOX is not a placeholder.
99
+ *
100
+ * MEASURED 2026-08-27 against a real project: `- [ ] AC покрыты автотестами` and 16 siblings were
101
+ * reported as "possibly unfilled". Worse, that project's own Completion.md TEACHES the convention —
102
+ * "флажки `[ ]` при каждом FR-GROWTH-00N" — so this warning fired on the notation the pipeline
103
+ * itself prescribes. Noise on a legitimate convention trains people to ignore warnings, which is
104
+ * the failure this whole checker exists to prevent, one level down.
105
+ */
106
+ const CHECKBOX = /^\s*(?:[-*+]\s+)?\[[ xX]?\]/;
107
+
91
108
  function scan(body) {
92
109
  const clean = stripFences(body).replace(GAP, '');
93
110
  const blocking = [];
@@ -98,11 +115,21 @@ function scan(body) {
98
115
  }
99
116
  }
100
117
  const warn = [];
101
- SUSPECT.lastIndex = 0;
102
- for (let m = SUSPECT.exec(clean); m !== null && warn.length < 3; m = SUSPECT.exec(clean)) {
103
- const t = m[0];
104
- if (/^\[\^?\d+\]$/.test(t)) continue; // a citation or footnote, not a placeholder
105
- warn.push(t.slice(0, 40));
118
+ // Line-wise, so a checkbox can be recognised by its POSITION in the line — `[ ]` anywhere else
119
+ // is not a task item. A table cell holding `| [ ] |` counts too: the same convention, in a table.
120
+ for (const line of clean.split('\n')) {
121
+ if (warn.length >= 3) break;
122
+ // STRIP the checkbox, do not skip the LINE. Skipping it would be an escape hatch: a genuine
123
+ // placeholder could hide behind a checkbox prefix, which is exactly what the test found when
124
+ // the first version skipped whole lines.
125
+ const rest = line.replace(CHECKBOX, '').replace(/\|\s*\[[ xX]?\]\s*\|/g, '| |');
126
+ SUSPECT.lastIndex = 0;
127
+ for (let m = SUSPECT.exec(rest); m !== null && warn.length < 3; m = SUSPECT.exec(rest)) {
128
+ const t = m[0];
129
+ if (/^\[\^?\d+\]$/.test(t)) continue; // a citation or footnote
130
+ if (/^\[[ xX]?\]$/.test(t)) continue; // a bare checkbox mid-line
131
+ warn.push(t.slice(0, 40));
132
+ }
106
133
  }
107
134
  return { blocking, warn };
108
135
  }
@@ -127,6 +154,7 @@ function main() {
127
154
  try { body = fs.readFileSync(abs, 'utf-8'); } catch (e) {
128
155
  if (e && e.code === 'ENOENT') {
129
156
  if (!d.optional) problems.push(d.file + ': отсутствует');
157
+ else if (d.expected) warnings.push(d.file + ': отсутствует, хотя конвейер его обещает');
130
158
  continue; // an optional absence is a legitimate answer
131
159
  }
132
160
  cannotCheck('не читается ' + d.file + ': ' + ((e && e.message) || e));
@@ -59,8 +59,19 @@ function cannotCheck(reason, hint) {
59
59
  process.exit(2);
60
60
  }
61
61
 
62
+ /**
63
+ * Absolutise ONCE, at the boundary.
64
+ *
65
+ * The invariant this restores: one frame of reference per path. A relative `-f` handed to a
66
+ * subprocess whose cwd we also override is resolved TWICE against two different origins, and the
67
+ * directory component appears twice. Keeping the argument relative past this point is what made
68
+ * `check-ports.cjs projects/01` report `.../projects/01/projects/01/docker-compose.yml`.
69
+ *
70
+ * Absolute from here on means the existence checks, the `-f` argument and the printed cure all name
71
+ * the same object — so the cure REPRODUCES the failure instead of refuting it.
72
+ */
62
73
  function resolveCompose(arg) {
63
- const target = arg || '.';
74
+ const target = path.resolve(process.cwd(), arg || '.');
64
75
  let file = target;
65
76
  try {
66
77
  if (fs.statSync(target).isDirectory()) file = path.join(target, 'docker-compose.yml');
@@ -74,16 +85,37 @@ function resolveCompose(arg) {
74
85
  /** The normalised config. Parsing the raw YAML would re-implement `extends`, interpolation and the
75
86
  * short `"5432:5432"` form — and the short form is exactly where a hand parser gets host_ip wrong. */
76
87
  function normalisedConfig(file) {
77
- const r = spawnSync('docker', ['compose', '-f', file, 'config'],
78
- { encoding: 'utf8', cwd: path.dirname(path.resolve(file)) });
88
+ // NO cwd override deliberately, and the deletion is the fix rather than a tidy-up.
89
+ //
90
+ // It used to be `cwd: path.dirname(path.resolve(file))`, which is how the doubling happened: `-f`
91
+ // was relative and got re-resolved against the cwd this very option installed. Absolutising `file`
92
+ // alone would have made the line harmless while leaving the false premise that compose needs its
93
+ // cwd set — and the next relative path added here would reopen the class.
94
+ //
95
+ // MEASURED (Compose v5.1.1), same absolute -f from two different cwds, byte-identical output:
96
+ // project name -> from the file's directory, not cwd
97
+ // build: ./app -> context resolved under the file's directory
98
+ // .env discovery -> the project-dir .env won; the cwd's .env was NOT even a fallback
99
+ // env_file: ./x.env -> compose still demanded the project-dir copy
100
+ // All three candidate justifications are project-directory-derived, and the project directory
101
+ // comes from the -f path. Scoped honestly: this is Compose v2+ semantics; v1 differed.
102
+ const r = spawnSync('docker', ['compose', '-f', file, 'config'], { encoding: 'utf8' });
79
103
  if (r.error && r.error.code === 'ENOENT') {
80
104
  cannotCheck('docker недоступен на этой машине',
81
105
  'без него нормализованный конфиг получить нечем, а разбирать YAML руками — значит ошибиться на короткой форме портов');
82
106
  }
83
107
  if (r.status !== 0) {
108
+ // Report, do not guess. The old hint said "обычно это незаданная переменная" — a cause that
109
+ // CANNOT produce this exit: a plain unset ${VAR} makes `docker compose config` exit 0 with a
110
+ // warning; only the required form ${VAR:?msg} exits 1. It named a subset of an already-narrow
111
+ // class while the actual cause was this checker's own invocation.
112
+ //
113
+ // And the cure now carries the ABSOLUTE path actually passed. It used to print the relative form
114
+ // without the cwd override — i.e. the invocation that SUCCEEDS — so the tool handed the user a
115
+ // reproducer that refuted it.
84
116
  const why = String(r.stderr || '').trim().split('\n')[0] || 'причина неизвестна';
85
117
  cannotCheck('docker compose config вернул ошибку: ' + why,
86
- 'обычно это незаданная переменная; посмотреть: docker compose -f ' + file + ' config');
118
+ 'повторить ровно то, что делали мы: docker compose -f ' + file + ' config');
87
119
  }
88
120
  return String(r.stdout || '');
89
121
  }
@@ -198,10 +198,19 @@ function parsePlans() {
198
198
  return safeListDir(dir).filter((f) => f.endsWith('.md')).length;
199
199
  }
200
200
 
201
+ /**
202
+ * THREE states, because two were not enough.
203
+ *
204
+ * `{count:0}` was returned both for a carrier that does not exist and for one that exists and holds
205
+ * nothing — so a project that had never recorded an insight rendered identically to one being used
206
+ * and found empty. That indistinguishability is what let 27 recorded insights become 0 across four
207
+ * real projects without any surface saying so (MEASURED 2026-08-27).
208
+ */
201
209
  function parseInsights() {
202
210
  const p = path.join(CWD, '.claude', 'insights', 'index.md');
203
211
  const text = safeReadText(p);
204
- if (!text) return { count: 0, lastDate: null };
212
+ if (text === null || text === undefined) return { count: 0, lastDate: null, started: false };
213
+ if (!text.trim()) return { count: 0, lastDate: null, started: true };
205
214
  const headings = text.match(/^##\s+\d{4}-\d{2}-\d{2}/gm) || [];
206
215
  // Last date: extract from last heading
207
216
  let lastDate = null;
@@ -210,7 +219,7 @@ function parseInsights() {
210
219
  const m = last.match(/\d{4}-\d{2}-\d{2}/);
211
220
  if (m) lastDate = m[0];
212
221
  }
213
- return { count: headings.length, lastDate };
222
+ return { count: headings.length, lastDate, started: true };
214
223
  }
215
224
 
216
225
  function parseToolkit() {
@@ -467,7 +476,9 @@ function buildToolkit(toolkit, expected) {
467
476
 
468
477
  function buildStatus(insights, lastTest, mcpServers, settingsStatus, keysarium) {
469
478
  const parts = [];
470
- parts.push(`💡 ${bold('Insights')} ${insights.count > 0 ? green('●' + insights.count) : '0'}` +
479
+ // '0' meant two different things: no carrier at all, and a carrier holding nothing. A dash
480
+ // says the first; a zero says the second. The reader can now tell which one they are looking at.
481
+ parts.push(`💡 ${bold('Insights')} ${insights.count > 0 ? green('●' + insights.count) : insights.started ? '0' : dim('—')}` +
471
482
  (insights.lastDate ? ` ${dim('(' + insights.lastDate + ')')}` : ''));
472
483
 
473
484
  if (lastTest && typeof lastTest.passed === 'number') {
@@ -498,7 +509,7 @@ function main() {
498
509
  const validation = safeRun(() => parseValidationScore(), null);
499
510
  const adrs = safeRun(() => parseAdrs(), 0);
500
511
  const plans = safeRun(() => parsePlans(), 0);
501
- const insights = safeRun(() => parseInsights(), { count: 0, lastDate: null });
512
+ const insights = safeRun(() => parseInsights(), { count: 0, lastDate: null, started: false });
502
513
  const toolkit = safeRun(() => parseToolkit(), { skills: 0, commands: 0, agents: 0, rules: 0, hooks: 0 });
503
514
  const expected = parseExpectedToolkit();
504
515
  const settingsStatus = safeRun(() => parseSettingsStatus(manifest), null);
@@ -15,12 +15,34 @@ echo ""
15
15
 
16
16
  # Check if file argument provided
17
17
  if [ -z "$1" ]; then
18
+ echo "⚠️ check did NOT run: no argument given"
18
19
  echo "Usage: $0 <file-or-directory>"
19
- exit 1
20
+ exit 2
20
21
  fi
21
22
 
22
23
  TARGET="$1"
23
24
 
25
+ # ── Findings counter ─────────────────────────────────────────────────────────
26
+ #
27
+ # This script DETECTED correctly and could not FAIL. MEASURED 2026-08-27: deliberately awful input
28
+ # produced red verdicts on screen and exit 0, while a nonexistent path exited 1 — "I could not
29
+ # check" was louder than "I found violations", so any gate reading 1 could not tell them apart.
30
+ #
31
+ # Nothing about the detection changed. What was missing is that nobody counted.
32
+ #
33
+ # 0 ran, found nothing
34
+ # 1 ran, found violations — the count is printed
35
+ # 2 COULD NOT CHECK: no argument, or a target that does not exist
36
+ FINDINGS=0
37
+ finding() { FINDINGS=$((FINDINGS + 1)); }
38
+
39
+ if [ ! -e "$TARGET" ]; then
40
+ echo "⚠️ check did NOT run: '$TARGET' does not exist"
41
+ echo " → This is NOT a clean bill: nothing was examined."
42
+ exit 2
43
+ fi
44
+
45
+
24
46
  # Function to assess correctness
25
47
  assess_correctness() {
26
48
  echo "📊 CORRECTNESS CHECK"
@@ -28,7 +50,7 @@ assess_correctness() {
28
50
 
29
51
  # Check for common bug patterns
30
52
  if grep -r "TODO\|FIXME\|BUG\|HACK" "$TARGET" 2>/dev/null; then
31
- echo -e "${RED}🔴 FAILING: Found TODO/FIXME/BUG/HACK comments${NC}"
53
+ echo -e "${RED}🔴 FAILING: Found TODO/FIXME/BUG/HACK comments${NC}"; finding
32
54
  echo " → This code admits it's broken. Fix it before review."
33
55
  return 0
34
56
  fi
@@ -51,14 +73,14 @@ assess_performance() {
51
73
  # Check for nested loops (potential O(n²))
52
74
  nested_loops=$(grep -r "for.*{" "$TARGET" | wc -l)
53
75
  if [ "$nested_loops" -gt 5 ]; then
54
- echo -e "${RED}🔴 FAILING: Found $nested_loops loops${NC}"
76
+ echo -e "${RED}🔴 FAILING: Found $nested_loops loops${NC}"; finding
55
77
  echo " → Are you creating O(n²) complexity where O(n) exists?"
56
78
  echo " → Use hash maps, sets, or better algorithms."
57
79
  fi
58
80
 
59
81
  # Check for synchronous I/O in hot paths
60
82
  if grep -r "readFileSync\|writeFileSync" "$TARGET" 2>/dev/null; then
61
- echo -e "${RED}🔴 FAILING: Synchronous file I/O detected${NC}"
83
+ echo -e "${RED}🔴 FAILING: Synchronous file I/O detected${NC}"; finding
62
84
  echo " → You're blocking the event loop. Use async operations."
63
85
  fi
64
86
 
@@ -74,7 +96,7 @@ assess_error_handling() {
74
96
  # Check for try/catch usage
75
97
  try_count=$(grep -r "try\|catch" "$TARGET" 2>/dev/null | wc -l)
76
98
  if [ "$try_count" -eq 0 ]; then
77
- echo -e "${RED}🔴 FAILING: No error handling found${NC}"
99
+ echo -e "${RED}🔴 FAILING: No error handling found${NC}"; finding
78
100
  echo " → What happens when this code fails? It crashes."
79
101
  else
80
102
  echo -e "${GREEN}✓ Found error handling (verify it's sufficient)${NC}"
@@ -82,7 +104,7 @@ assess_error_handling() {
82
104
 
83
105
  # Check for empty catch blocks
84
106
  if grep -A 1 "catch" "$TARGET" 2>/dev/null | grep -q "^\s*}"; then
85
- echo -e "${RED}🔴 FAILING: Empty catch blocks detected${NC}"
107
+ echo -e "${RED}🔴 FAILING: Empty catch blocks detected${NC}"; finding
86
108
  echo " → Swallowing errors silently is worse than crashing."
87
109
  fi
88
110
  }
@@ -118,7 +140,7 @@ assess_testability() {
118
140
  if [ -d "tests" ] || [ -d "test" ] || [ -d "__tests__" ]; then
119
141
  echo -e "${GREEN}✓ Test directory exists${NC}"
120
142
  else
121
- echo -e "${RED}🔴 FAILING: No test directory found${NC}"
143
+ echo -e "${RED}🔴 FAILING: No test directory found${NC}"; finding
122
144
  echo " → Where are the tests? Did you even test this?"
123
145
  fi
124
146
 
@@ -177,3 +199,14 @@ echo " - Tests exist and pass"
177
199
  echo " - Code is clear and maintainable"
178
200
  echo ""
179
201
  echo "If you wouldn't deploy this to production, don't submit it for review."
202
+
203
+ # ── Verdict ──────────────────────────────────────────────────────────────────
204
+ # ADDED, never substituted: the closing prose above is this skill's character and a reader wants it.
205
+ # What follows is the same answer in a form a gate can act on.
206
+ echo ""
207
+ if [ "$FINDINGS" -gt 0 ]; then
208
+ echo "VERDICT: $FINDINGS finding(s). Not ready."
209
+ exit 1
210
+ fi
211
+ echo "VERDICT: 0 findings."
212
+ exit 0