@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 +11 -5
- package/extensions/charterleaf.js +2 -1
- package/lib/charterleaf.js +24 -12
- package/package.json +1 -1
- package/skills/charterleaf/SKILL.md +3 -1
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.
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/lib/charterleaf.js
CHANGED
|
@@ -122,7 +122,8 @@ function globMatches(pattern, targetPath) {
|
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
source += '$';
|
|
125
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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),
|
|
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,
|
|
415
|
-
`duplicate requirement id ${reqId} (also in ${relativeDisplay(first,
|
|
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,
|
|
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.
|
|
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.
|