@onedotmint/charterleaf 0.4.2 → 0.4.3

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/README.md CHANGED
@@ -39,7 +39,7 @@ pi install npm:@onedotmint/charterleaf
39
39
  For the standalone CLI, use `npm install -g @onedotmint/charterleaf`.
40
40
  For local development, use `pi install /absolute/path/to/charterleaf` (a package
41
41
  directory, not a `.tgz` file). To test the CLI from a local package, run `npm pack`
42
- then `npm install -g ./onedotmint-charterleaf-0.4.2.tgz`.
42
+ then `npm install -g ./onedotmint-charterleaf-0.4.3.tgz`.
43
43
 
44
44
  Restart Pi or run `/reload` after installation, then use `/charterleaf` in each
45
45
  project you want to enable. To stop automation, choose **Not Enable** in that
@@ -206,8 +206,10 @@ workflow or another development cycle.
206
206
 
207
207
  ## CLI
208
208
 
209
- Charterleaf exposes exactly two deterministic commands. Run them from a
210
- repository root containing `specs/`:
209
+ Charterleaf exposes exactly two deterministic commands. Run them from the project
210
+ root or a subdirectory: the nearest ancestor containing `specs/` is used, without
211
+ crossing a Git repository or worktree boundary. The target path remains
212
+ repo-relative, not relative to the shell's current directory.
211
213
 
212
214
  ```bash
213
215
  charterleaf related <path>
@@ -424,7 +426,7 @@ GitHub Actions runs the installed-package suite on Ubuntu and Windows with Node
424
426
  CI makes no model calls and retains packaging/test logs. `PI_CLI` can select the
425
427
  Pi Node entry point for smoke checks without relying on platform-specific shims.
426
428
 
427
- The [passing eight-job release-candidate matrix](https://github.com/onedotmint/Charterleaf/actions/runs/40078816360)
429
+ For v0.4.2, the [passing eight-job release-candidate matrix](https://github.com/onedotmint/Charterleaf/actions/runs/40078816360)
428
430
  checks the actual npm tarball after isolated installation, including byte equality,
429
431
  CLI commands and Core/Extension/recovery tests. Each Ubuntu job passes all 227 tests;
430
432
  each Windows job passes 220 with zero failures and seven platform-specific skips
@@ -447,7 +449,11 @@ unchanged-spec checks passed. A later complete pre-0.4.2 run on the same model
447
449
  passed all 15 scenarios and 231 assertions, including F1/F2 without redundant reviews.
448
450
  The final Windows Git-root path fix was subsequently validated by cross-platform
449
451
  CI, not another paid model run. These bounded examples do not prove model reliability
450
- on every task or provider, or guarantee zero redundant reviews.
452
+ on every task or provider, or guarantee zero redundant reviews. A subsequent
453
+ case-A rerun on `deepseek/deepseek-flash` failed knowledge-capture assertions on
454
+ both the v0.4.2 baseline and the patch candidate: accounting tests passed, but
455
+ the model concluded `covered` without writing a routed spec. Automatic knowledge
456
+ capture remains model-dependent; passing structural tests does not prove it.
451
457
 
452
458
  The controlled organic-capture comparison (`scripts/context-smoke.js`) keeps cwd,
453
459
  package paths, prompt and native tools identical. On `deepseek/deepseek-flash`:
@@ -65,7 +65,8 @@ module.exports = function charterleaf(pi) {
65
65
  s.budget -= size;
66
66
  s.staged.set(spec, hash);
67
67
  } else {
68
- notes.push(`Read the complete ${spec} with read before modifying ${file}; automatic context budget reached.`);
68
+ const pages = Math.ceil(size / 8000);
69
+ notes.push(`Read the complete ${spec} (${size} bytes, ~${pages} page(s) via offset/limit) before modifying ${file}; it exceeds the ${size > 8000 ? '8 KB per-file limit' : 'per-task context budget'}.`);
69
70
  }
70
71
  }
71
72
  if (!result.scoped.length && !s.miss) {
@@ -122,7 +122,8 @@ function globMatches(pattern, targetPath) {
122
122
  });
123
123
 
124
124
  source += '$';
125
- return new RegExp(source).test(normalizedPath);
125
+ // Windows filesystems are case-insensitive, so a path's casing is not meaningful there.
126
+ return new RegExp(source, process.platform === 'win32' ? 'i' : '').test(normalizedPath);
126
127
  }
127
128
 
128
129
  function walkMarkdown(root) {
@@ -142,6 +143,15 @@ function walkMarkdown(root) {
142
143
  });
143
144
  }
144
145
 
146
+ // CLI discovery must not inherit another repository's constraints.
147
+ function resolveSpecRoot(cwd) {
148
+ for (let current = path.resolve(cwd); ; current = path.dirname(current)) {
149
+ const candidate = path.join(current, 'specs');
150
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return current;
151
+ if (fs.existsSync(path.join(current, '.git')) || path.dirname(current) === current) return null;
152
+ }
153
+ }
154
+
145
155
  function relativeDisplay(filePath, cwd) {
146
156
  const relative = path.relative(cwd, filePath);
147
157
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) return relative.replaceAll('\\', '/');
@@ -153,7 +163,8 @@ function write(stream, line) {
153
163
  }
154
164
 
155
165
  function resolveRelated(repoPath, cwd = process.cwd()) {
156
- const specRoot = path.join(cwd, 'specs');
166
+ const root = path.resolve(cwd);
167
+ const specRoot = path.join(root, 'specs');
157
168
  if (!fs.existsSync(specRoot) || !fs.statSync(specRoot).isDirectory()) {
158
169
  throw new Error('ERROR specs/: not found');
159
170
  }
@@ -163,7 +174,7 @@ function resolveRelated(repoPath, cwd = process.cwd()) {
163
174
  const errors = [];
164
175
 
165
176
  for (const filePath of walkMarkdown(specRoot)) {
166
- const display = relativeDisplay(filePath, cwd);
177
+ const display = relativeDisplay(filePath, root);
167
178
  let parsed;
168
179
  try {
169
180
  parsed = parseFrontmatter(fs.readFileSync(filePath, 'utf8'));
@@ -317,7 +328,8 @@ function allRequirementIds(text) {
317
328
  function lint(cwd = process.cwd(), streams = {}) {
318
329
  const stdout = streams.stdout || process.stdout;
319
330
  const stderr = streams.stderr || process.stderr;
320
- const specRoot = path.join(cwd, 'specs');
331
+ const root = path.resolve(cwd);
332
+ const specRoot = path.join(root, 'specs');
321
333
  if (!fs.existsSync(specRoot) || !fs.statSync(specRoot).isDirectory()) {
322
334
  write(stderr, 'ERROR specs/: not found');
323
335
  return 2;
@@ -332,15 +344,15 @@ function lint(cwd = process.cwd(), streams = {}) {
332
344
  const paths = walkMarkdown(specRoot);
333
345
 
334
346
  for (const filePath of paths) {
335
- const display = relativeDisplay(filePath, cwd);
347
+ const display = relativeDisplay(filePath, root);
336
348
  let result;
337
349
  try {
338
350
  result = parseFrontmatter(fs.readFileSync(filePath, 'utf8'));
339
- parsed.set(filePath, result);
340
351
  } catch (error) {
341
352
  errors.push([display, `invalid frontmatter: ${error.message}`]);
342
353
  continue;
343
354
  }
355
+ parsed.set(filePath, result);
344
356
 
345
357
  const frontmatter = result.frontmatter || {};
346
358
  const living = isLivingSpec(filePath, specRoot);
@@ -353,7 +365,7 @@ function lint(cwd = process.cwd(), streams = {}) {
353
365
  } else {
354
366
  const specId = rawSpecId.trim();
355
367
  if (ids.has(specId)) {
356
- errors.push([display, `duplicate spec id ${specId} (also in ${relativeDisplay(ids.get(specId), cwd)})`]);
368
+ errors.push([display, `duplicate spec id ${specId} (also in ${relativeDisplay(ids.get(specId), root)})`]);
357
369
  } else {
358
370
  ids.set(specId, filePath);
359
371
  }
@@ -411,8 +423,8 @@ function lint(cwd = process.cwd(), streams = {}) {
411
423
  const first = reqPaths[0];
412
424
  for (const filePath of reqPaths.slice(1)) {
413
425
  errors.push([
414
- relativeDisplay(filePath, cwd),
415
- `duplicate requirement id ${reqId} (also in ${relativeDisplay(first, cwd)})`,
426
+ relativeDisplay(filePath, root),
427
+ `duplicate requirement id ${reqId} (also in ${relativeDisplay(first, root)})`,
416
428
  ]);
417
429
  }
418
430
  }
@@ -425,7 +437,7 @@ function lint(cwd = process.cwd(), streams = {}) {
425
437
  const allExistingRequirements = new Set(globalRequirements.keys());
426
438
  for (const filePath of paths) {
427
439
  if (path.dirname(filePath) !== path.join(specRoot, 'changes') || !parsed.has(filePath)) continue;
428
- const display = relativeDisplay(filePath, cwd);
440
+ const display = relativeDisplay(filePath, root);
429
441
  const { frontmatter: rawFrontmatter, body } = parsed.get(filePath);
430
442
  const frontmatter = rawFrontmatter || {};
431
443
  const sections = changeSections(stripHtmlComments(body));
@@ -534,7 +546,7 @@ function main(argv = process.argv.slice(2), cwd = process.cwd(), streams = {}) {
534
546
  return 0;
535
547
  }
536
548
  if (rest.length !== 1) return usageError('related requires exactly one path', streams);
537
- return related(rest[0], path.resolve(cwd), streams);
549
+ return related(rest[0], resolveSpecRoot(cwd) || path.resolve(cwd), streams);
538
550
  }
539
551
  if (command === 'lint') {
540
552
  if (rest.length === 1 && (rest[0] === '--help' || rest[0] === '-h')) {
@@ -542,7 +554,7 @@ function main(argv = process.argv.slice(2), cwd = process.cwd(), streams = {}) {
542
554
  return 0;
543
555
  }
544
556
  if (rest.length) return usageError('lint takes no arguments', streams);
545
- return lint(path.resolve(cwd), streams);
557
+ return lint(resolveSpecRoot(cwd) || path.resolve(cwd), streams);
546
558
  }
547
559
  return usageError(`unknown command ${command}`, streams);
548
560
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onedotmint/charterleaf",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Lightweight Agent-Native Constraint Layer: opt-in spec discovery and maintenance for Pi, with a standalone zero-dependency CLI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -61,7 +61,9 @@ Use `specs/changes/<name>.md` only when an accepted future contract must coexist
61
61
 
62
62
  ## CLI details
63
63
 
64
- Run from the project root:
64
+ Run from the project root or a subdirectory. CLI discovery stops at the nearest
65
+ `specs/` directory or Git repository/worktree boundary; target paths stay
66
+ repo-relative:
65
67
 
66
68
  - `charterleaf related <path>` returns constitution as Global, capability/engineering specs matching `applies_to`, and active changes affecting returned spec IDs. It returns paths/metadata, not bodies. Decisions and explicit references are followed manually when relevant. A scoped miss is not proof that no constraints apply.
67
69
  - `charterleaf lint` checks frontmatter, IDs/globs, duplicate IDs, change references/deltas and conflicting active changes. `0 structural errors` does not prove semantic correctness, coverage or code/spec agreement. The Extension also lints standard spec writes.