@onedotmint/charterleaf 0.4.1 → 0.4.2
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/AGENTS_SNIPPET.md +15 -4
- package/README.md +60 -24
- package/extensions/charterleaf.js +43 -13
- package/lib/project.js +63 -17
- package/package.json +2 -1
- package/skills/charterleaf/SKILL.md +7 -1
package/AGENTS_SNIPPET.md
CHANGED
|
@@ -11,10 +11,21 @@ owner per fact and link to it rather than duplicating it.
|
|
|
11
11
|
|
|
12
12
|
Charterleaf automation is explicit opt-in, not implied by this entry or
|
|
13
13
|
by an existing `specs/` directory. In Pi, the user chooses Auto, Confirm,
|
|
14
|
-
Reference or Not Enable through `/charterleaf
|
|
15
|
-
`
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Reference or Not Enable through `/charterleaf`. In Git, the saved mode lives in
|
|
15
|
+
`charterleaf.json` under `git rev-parse --path-format=absolute --git-common-dir`
|
|
16
|
+
(usually `.git/charterleaf.json`), shared by all branches and linked worktrees,
|
|
17
|
+
not independent clones. Outside Git, it remains `specs/.charterleaf.json`.
|
|
18
|
+
The local config is authoritative, including saved `off`; a branch's legacy config
|
|
19
|
+
cannot override it. Only when local config is absent may first load import a valid
|
|
20
|
+
saved mode from current-branch legacy `specs/.charterleaf.json`, without overwriting local
|
|
21
|
+
config or changing the legacy file/worktree. Invalid config means an error and
|
|
22
|
+
disabled automation; never recover lost authorization from Git history or sessions.
|
|
23
|
+
No saved choice means off, not a global Auto default: no automatic scan, adoption
|
|
24
|
+
prompt or document creation. Enabling Git projects creates no `specs/` directory or
|
|
25
|
+
documents; a branch without specs remains usable for normal reads/edits and later
|
|
26
|
+
justified spec creation under the selected mode. Specs route from the current
|
|
27
|
+
worktree, not the shared config directory. Do not change activation config on the
|
|
28
|
+
user's behalf.
|
|
18
29
|
|
|
19
30
|
For manual use on a concrete repository path, run from the project root:
|
|
20
31
|
|
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.2.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
|
|
@@ -51,14 +51,17 @@ directly and supplies the bundled CLI entry point for optional manual checks.
|
|
|
51
51
|
|
|
52
52
|
The **Core/CLI** runs standalone on Node.js **20+**, has no dependencies,
|
|
53
53
|
and requires no Pi. The **Pi Extension** is tested against
|
|
54
|
-
`@earendil-works/pi-coding-agent` **1.1.0** and uses `agent_before_settle`:
|
|
54
|
+
`@earendil-works/pi-coding-agent` **1.1.0 and 1.4.0** and uses `agent_before_settle`:
|
|
55
55
|
it requires a host with that compatible Extension API, not just any Pi
|
|
56
56
|
version. That Pi host requires Node.js **22.19.0+**; the Core's lower
|
|
57
57
|
minimum does not make older Node versions suitable for the host.
|
|
58
58
|
|
|
59
59
|
## Native Pi modes
|
|
60
60
|
|
|
61
|
-
Run `/charterleaf` in Pi for the native project mode menu
|
|
61
|
+
Run `/charterleaf` in Pi for the native project mode menu. It distinguishes
|
|
62
|
+
**not configured** from explicitly **disabled**, explains that existing specs do
|
|
63
|
+
not enable automation, and describes the current mode. Saving confirms what the
|
|
64
|
+
selected mode allows, not just where its config was written:
|
|
62
65
|
|
|
63
66
|
| Choice | Behavior |
|
|
64
67
|
| --- | --- |
|
|
@@ -67,20 +70,35 @@ Run `/charterleaf` in Pi for the native project mode menu:
|
|
|
67
70
|
| **Reference** | Read and check; report proposals or conflicts, never modify specs. |
|
|
68
71
|
| **Not Enable** | Turn automation off; keep existing documents. |
|
|
69
72
|
|
|
70
|
-
The choice is saved immediately in `
|
|
73
|
+
The choice is saved immediately in `charterleaf.json` under the absolute directory
|
|
74
|
+
returned by `git rev-parse --path-format=absolute --git-common-dir` (usually
|
|
75
|
+
`.git/charterleaf.json`). All branches and linked worktrees of that repository
|
|
76
|
+
share the mode; independent clones do not. Outside Git, the path remains
|
|
77
|
+
`specs/.charterleaf.json`. The config format is unchanged:
|
|
71
78
|
|
|
72
79
|
```json
|
|
73
80
|
{ "version": 1, "mode": "off" }
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
Valid modes are `auto`, `confirm`, `reference`, and `off`.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
Valid modes are `auto`, `confirm`, `reference`, and `off`. The new repository-local
|
|
84
|
+
config is authoritative whenever it exists, including saved `off`; a branch's
|
|
85
|
+
legacy `specs/.charterleaf.json` cannot override it. If no local config exists,
|
|
86
|
+
the first load imports the saved mode from a valid legacy config on the current branch,
|
|
87
|
+
without overwriting a local config or changing/removing the legacy file or tracked
|
|
88
|
+
worktree. Invalid config reports an error and disables automation, not a fallback
|
|
89
|
+
to another source. Lost authorization is never recovered from Git history or sessions.
|
|
90
|
+
|
|
91
|
+
With no saved choice, the mode stays **off**; existing `specs/` alone never enables
|
|
92
|
+
automation. This is repository-local opt-in, **not a global Auto default**.
|
|
93
|
+
Activation is an explicit user choice through `/charterleaf`, not an Agent decision.
|
|
94
|
+
Enabling a Git project creates only the local config, never a `specs/` directory or
|
|
95
|
+
spec documents. A branch without `specs/` is valid: normal code reads/edits and later
|
|
96
|
+
evidence-based spec creation still work under the selected mode. Spec content and
|
|
97
|
+
routing always belong to the current worktree, not the shared Git directory.
|
|
98
|
+
Outside Git, saving may create the `specs/` directory for the config only.
|
|
99
|
+
Disabling preserves documents; cancel changes nothing. Disabled/unconfigured
|
|
100
|
+
projects get no automatic adoption prompt, task context, scan, or document bootstrap.
|
|
101
|
+
The two manual CLI commands remain unchanged and independent of this config.
|
|
84
102
|
|
|
85
103
|
Without UI, saved Auto and Reference modes still read/check constraints;
|
|
86
104
|
Confirm denies spec mutations because approval is unavailable. A refused identical
|
|
@@ -109,9 +127,11 @@ request**, not when a parallel sibling tool finishes.
|
|
|
109
127
|
Automatic spec bodies have a **24,000-byte budget per task** and an
|
|
110
128
|
**8,000-byte per-file limit**. Larger files or exhausted budgets require
|
|
111
129
|
an explicit complete `read`; a partial/truncated read is not full
|
|
112
|
-
knowledge. Native Pi
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
knowledge. Native Pi raw pages and AFT numbered pages (offset/limit or
|
|
131
|
+
startLine/endLine) are credited only when their exact content covers the same
|
|
132
|
+
spec version with no gaps; AFT line labels must be sequential and correct.
|
|
133
|
+
Display newlines and CRLF line endings do not prevent complete-read credit;
|
|
134
|
+
ordinary prose about truncation does not count as a tool truncation notice. On a scoped miss, the Extension supplies metadata only for
|
|
115
135
|
available living specs, capped at 12 entries and 2,000 characters;
|
|
116
136
|
`charterleaf related <path>` provides the full routing list. A miss never
|
|
117
137
|
proves that no constraints apply.
|
|
@@ -130,8 +150,9 @@ again. This is task-local bookkeeping, not a watcher or persistent index.
|
|
|
130
150
|
|
|
131
151
|
Knowledge capture happens **during normal work**, not just after code changes:
|
|
132
152
|
development, debugging, substantive code/test/architecture investigation and
|
|
133
|
-
accepted technical decisions can establish or uncover durable knowledge.
|
|
134
|
-
with
|
|
153
|
+
accepted technical decisions can establish or uncover durable knowledge. An enabled
|
|
154
|
+
project with no spec documents (or no `specs/` directory in Git) is a valid initial
|
|
155
|
+
state. When evidence warrants
|
|
135
156
|
it, the Agent creates a concise scoped spec using ordinary authorized `write`/`edit`;
|
|
136
157
|
Core immediately routes its `id`/`applies_to` on later related code reads or edits.
|
|
137
158
|
There is no initialization scan, document quota or adoption wizard.
|
|
@@ -363,7 +384,8 @@ Important limits:
|
|
|
363
384
|
path that was checked; spec authorization also preserves the original path's
|
|
364
385
|
protected identity when a spec symlink points elsewhere inside the project.
|
|
365
386
|
Foreign UNC paths are not interpreted as local paths; symlink escapes are
|
|
366
|
-
rejected for project spec writes. Native Windows
|
|
387
|
+
rejected for project spec writes. Native Windows CLI, helper/Extension tests and
|
|
388
|
+
RPC mode persistence are covered by hosted CI; platform-specific permissions still apply.
|
|
367
389
|
- `related` has no filesystem awareness of the supplied target path and
|
|
368
390
|
no proof of routing coverage. Task-local hashes are not a continuous
|
|
369
391
|
monitor of external changes. A single spec line larger than Pi's read byte
|
|
@@ -386,6 +408,7 @@ Verification commands:
|
|
|
386
408
|
|
|
387
409
|
```bash
|
|
388
410
|
npm test
|
|
411
|
+
npm run test:package # real tarball install and tests against installed code
|
|
389
412
|
npm run test:pi # isolated real RPC/UI smoke; no model calls
|
|
390
413
|
CHARTERLEAF_MODEL=deepseek/deepseek-flash npm run test:organic # paid isolated A–G acceptance
|
|
391
414
|
CHARTERLEAF_MODEL=deepseek/deepseek-flash CHARTERLEAF_BASELINE=5d470db npm run test:context # paid comparison against v0.4.0
|
|
@@ -396,10 +419,20 @@ node bin/charterleaf.js related extensions/charterleaf.js
|
|
|
396
419
|
node bin/charterleaf.js lint
|
|
397
420
|
```
|
|
398
421
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
422
|
+
GitHub Actions runs the installed-package suite on Ubuntu and Windows with Node
|
|
423
|
+
20.0.0, 22.19.0 and 24.x, plus Pi 1.1.0/1.4.0 smoke checks on host-compatible Node.
|
|
424
|
+
CI makes no model calls and retains packaging/test logs. `PI_CLI` can select the
|
|
425
|
+
Pi Node entry point for smoke checks without relying on platform-specific shims.
|
|
426
|
+
|
|
427
|
+
The [passing eight-job release-candidate matrix](https://github.com/onedotmint/Charterleaf/actions/runs/40078816360)
|
|
428
|
+
checks the actual npm tarball after isolated installation, including byte equality,
|
|
429
|
+
CLI commands and Core/Extension/recovery tests. Each Ubuntu job passes all 227 tests;
|
|
430
|
+
each Windows job passes 220 with zero failures and seven platform-specific skips
|
|
431
|
+
(one Linux/WSL path check, two POSIX permission checks and four SIGKILL checks).
|
|
432
|
+
Windows symlink tests run, rather than being skipped. Host-compatible jobs also
|
|
433
|
+
pass real Pi RPC loading/native selection, cancellation, mode changes, legacy
|
|
434
|
+
migration and cross-branch/process persistence. RPC checks use isolated temporary
|
|
435
|
+
projects/settings and make no model calls.
|
|
403
436
|
|
|
404
437
|
The 0.4.1 organic-capture acceptance used real `deepseek/deepseek-flash` sessions
|
|
405
438
|
in isolated Git projects. Actual tool results, approval records, specs, routing,
|
|
@@ -407,10 +440,13 @@ file snapshots/diffs, original tests and an independent exact-money oracle were
|
|
|
407
440
|
checked, not just assistant claims. Empty-spec development and investigation,
|
|
408
441
|
ordinary-query non-triggers, authoritative-document references, bug-versus-contract
|
|
409
442
|
handling, cross-session reuse and mode/read-only/no-UI permissions all have passing
|
|
410
|
-
evidence. **The strict matrix was not consistently all-green:**
|
|
443
|
+
evidence. **The earlier strict matrix was not consistently all-green:** a focused
|
|
411
444
|
A→F1→F2 run passed A and F1, while F2 omitted its knowledge-completion comment and
|
|
412
445
|
received one redundant fallback review. Its actual routing, code behavior and
|
|
413
|
-
unchanged-spec checks passed.
|
|
446
|
+
unchanged-spec checks passed. A later complete pre-0.4.2 run on the same model
|
|
447
|
+
passed all 15 scenarios and 231 assertions, including F1/F2 without redundant reviews.
|
|
448
|
+
The final Windows Git-root path fix was subsequently validated by cross-platform
|
|
449
|
+
CI, not another paid model run. These bounded examples do not prove model reliability
|
|
414
450
|
on every task or provider, or guarantee zero redundant reviews.
|
|
415
451
|
|
|
416
452
|
The controlled organic-capture comparison (`scripts/context-smoke.js`) keeps cwd,
|
|
@@ -4,7 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { pathToFileURL } = require('node:url');
|
|
6
6
|
const { resolveRelated, lint } = require('../lib/charterleaf.js');
|
|
7
|
-
const {
|
|
7
|
+
const { projectRoot, projectPath, toolPath, modeConfigPath, isModeConfig, readMode, saveMode, fingerprint, gitSnapshot, changedSince } = require('../lib/project.js');
|
|
8
8
|
|
|
9
9
|
const OPTIONS = [
|
|
10
10
|
'Auto (recommended) — read constraints and maintain only durable knowledge',
|
|
@@ -13,6 +13,12 @@ const OPTIONS = [
|
|
|
13
13
|
'Not Enable — disable; keep existing documents',
|
|
14
14
|
];
|
|
15
15
|
const MODES = ['auto', 'confirm', 'reference', 'off'];
|
|
16
|
+
const MODE_EFFECTS = {
|
|
17
|
+
auto: 'Automatic constraint loading and code edit checks; maintain only justified durable specs.',
|
|
18
|
+
confirm: 'Automatic constraint loading and code edit checks; approve each spec modification.',
|
|
19
|
+
reference: 'Automatic constraint loading and code edit checks; never modify specs.',
|
|
20
|
+
off: 'Automation off: no automatic constraint loading or code edit checks; existing documents preserved.',
|
|
21
|
+
};
|
|
16
22
|
const isSpec = (file) => file === 'specs' || file.startsWith('specs/');
|
|
17
23
|
const message = (content) => ({ type: 'custom_message', customType: 'charterleaf', content, display: false });
|
|
18
24
|
|
|
@@ -32,8 +38,14 @@ module.exports = function charterleaf(pi) {
|
|
|
32
38
|
return state;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
function related(file, root) {
|
|
42
|
+
// Mode survives branch switches even when the new branch has no specs yet.
|
|
43
|
+
if (!fs.existsSync(path.join(root, 'specs'))) return { global: [], scoped: [], changes: [], available: [] };
|
|
44
|
+
return resolveRelated(file, root);
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
function route(s, file) {
|
|
36
|
-
const result =
|
|
48
|
+
const result = related(file, s.root);
|
|
37
49
|
const notes = [];
|
|
38
50
|
let required = false;
|
|
39
51
|
for (const spec of [...result.global, ...result.scoped, ...result.changes]) {
|
|
@@ -68,12 +80,23 @@ module.exports = function charterleaf(pi) {
|
|
|
68
80
|
|
|
69
81
|
function completePages(s, file, hash, event) {
|
|
70
82
|
const text = event.content.filter((part) => part.type === 'text').map((part) => part.text).join('\n');
|
|
71
|
-
const footer = /\n\n
|
|
83
|
+
const footer = /\n\n(?:\[(?:Showing lines \d+-\d+ of \d+[^\n]*|\d+ more lines in file\.[^\n]*)Use offset=\d+ to continue\.\]|\(File is large; use startLine\/endLine or offset\/limit to read a section\.\))$/;
|
|
72
84
|
const body = text.replace(footer, '');
|
|
73
85
|
const lines = fs.readFileSync(path.join(s.root, file), 'utf8').split('\n');
|
|
74
86
|
const start = (event.input.startLine || event.input.offset || 1) - 1;
|
|
75
|
-
|
|
76
|
-
if (start < 0
|
|
87
|
+
let end = start + body.split('\n').length;
|
|
88
|
+
if (start < 0) return false;
|
|
89
|
+
if (end > lines.length || lines.slice(start, end).join('\n') !== body) {
|
|
90
|
+
const numbered = body.split('\n');
|
|
91
|
+
// AFT adds sequential line labels and a final display newline, not file content.
|
|
92
|
+
if (numbered.at(-1) === '') numbered.pop();
|
|
93
|
+
if (!numbered.length || !numbered.every((line, i) => {
|
|
94
|
+
const match = line.match(/^ *(\d+): (.*)$/);
|
|
95
|
+
return match && Number(match[1]) === start + i + 1 && match[2] === lines[start + i]?.replace(/\r$/, '');
|
|
96
|
+
})) return false;
|
|
97
|
+
end = start + numbered.length;
|
|
98
|
+
if (end === lines.length - 1 && lines.at(-1) === '') end++;
|
|
99
|
+
}
|
|
77
100
|
let pages = s.pages.get(file);
|
|
78
101
|
if (!pages || pages.hash !== hash) pages = { hash, ranges: [] };
|
|
79
102
|
pages.ranges.push([start, end]);
|
|
@@ -107,13 +130,19 @@ module.exports = function charterleaf(pi) {
|
|
|
107
130
|
const root = projectRoot(ctx.cwd);
|
|
108
131
|
let mode;
|
|
109
132
|
try { mode = readMode(root); } catch { mode = 'invalid config (disabled)'; }
|
|
110
|
-
const
|
|
133
|
+
const config = modeConfigPath(root);
|
|
134
|
+
const status = mode === 'off'
|
|
135
|
+
? (fs.existsSync(config) ? 'off (disabled)' : 'off (not configured)') : mode;
|
|
136
|
+
const effect = MODE_EFFECTS[mode] || MODE_EFFECTS.off;
|
|
137
|
+
const adoption = !MODES.includes(mode) || mode === 'off'
|
|
138
|
+
? '\nExisting specs do not enable automation; choose a mode to enable it.' : '';
|
|
139
|
+
const choice = await ctx.ui.select(`Charterleaf — ${status}\n${root}\n${effect}${adoption}`, OPTIONS);
|
|
111
140
|
const index = OPTIONS.indexOf(choice);
|
|
112
141
|
if (index < 0) return;
|
|
113
142
|
if (MODES[index] === 'off' && mode === 'off') return;
|
|
114
143
|
saveMode(root, MODES[index]);
|
|
115
144
|
state = undefined;
|
|
116
|
-
ctx.ui.notify(`Charterleaf: ${MODES[index]} (saved to ${
|
|
145
|
+
ctx.ui.notify(`Charterleaf: ${MODES[index]} — ${MODE_EFFECTS[MODES[index]]} (saved to ${config})`, 'info');
|
|
117
146
|
} catch (error) { report(ctx, `Charterleaf: ${error.message}`); }
|
|
118
147
|
},
|
|
119
148
|
});
|
|
@@ -180,6 +209,9 @@ module.exports = function charterleaf(pi) {
|
|
|
180
209
|
if (!['read', 'write', 'edit'].includes(event.toolName)) return;
|
|
181
210
|
const mutation = event.toolName !== 'read';
|
|
182
211
|
try {
|
|
212
|
+
if (mutation && isModeConfig(s.root, ctx.cwd, event.input.path)) {
|
|
213
|
+
return { block: true, reason: 'Only the user may change Charterleaf mode via /charterleaf.' };
|
|
214
|
+
}
|
|
183
215
|
const file = projectPath(s.root, ctx.cwd, event.input.path);
|
|
184
216
|
const lexical = typeof event.input.path === 'string'
|
|
185
217
|
? path.relative(s.root, path.resolve(ctx.cwd, toolPath(event.input.path))).replaceAll('\\', '/') : null;
|
|
@@ -192,9 +224,6 @@ module.exports = function charterleaf(pi) {
|
|
|
192
224
|
// Execute the exact normalized path checked here, including Pi's @/file:/space aliases.
|
|
193
225
|
const target = path.join(s.root, file);
|
|
194
226
|
event.input.path = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/.test(target) ? pathToFileURL(target).href : target;
|
|
195
|
-
if (mutation && (file === CONFIG || lexical === CONFIG || file === projectPath(s.root, s.root, CONFIG))) {
|
|
196
|
-
return { block: true, reason: 'Only the user may change Charterleaf mode via /charterleaf.' };
|
|
197
|
-
}
|
|
198
227
|
if (mutation && (isSpec(file) || (lexical && isSpec(lexical)))) {
|
|
199
228
|
if (s.mode === 'reference') return { block: true, reason: 'Charterleaf Reference mode: report a proposal; do not modify specs.' };
|
|
200
229
|
if (s.mode === 'confirm') {
|
|
@@ -272,7 +301,8 @@ module.exports = function charterleaf(pi) {
|
|
|
272
301
|
}
|
|
273
302
|
} else if (!call.mutation) {
|
|
274
303
|
if (isSpec(call.file)) {
|
|
275
|
-
const fullRead =
|
|
304
|
+
const fullRead = !/^ *\d+: /.test(visible) &&
|
|
305
|
+
event.input.offset === undefined && event.input.startLine === undefined &&
|
|
276
306
|
event.input.limit === undefined && event.input.endLine === undefined &&
|
|
277
307
|
event.details?.truncation?.truncated !== true &&
|
|
278
308
|
!event.content.some((part) => part.type === 'text' &&
|
|
@@ -317,13 +347,13 @@ module.exports = function charterleaf(pi) {
|
|
|
317
347
|
const observed = changedSince(s.before, after);
|
|
318
348
|
const owned = [...s.owned].filter(([file, hash]) => fingerprint(s.root, file) === hash && !isSpec(file)).map(([file]) => file);
|
|
319
349
|
const unattributed = observed.filter((file) => !s.owned.has(file) || fingerprint(s.root, file) !== s.owned.get(file));
|
|
320
|
-
const emptySpecs = s.knowledge === 'no-value' &&
|
|
350
|
+
const emptySpecs = s.knowledge === 'no-value' && related('', s.root).available.length === 0;
|
|
321
351
|
if (s.knowledge === 'no-value' && !(emptySpecs && owned.length > 0)) return;
|
|
322
352
|
const candidate = s.knowledge === 'review';
|
|
323
353
|
if (!candidate && !owned.length && !unattributed.length && !(s.opaque && !comparable)) return;
|
|
324
354
|
const relevant = new Set();
|
|
325
355
|
for (const file of [...owned, ...unattributed].slice(0, 50)) {
|
|
326
|
-
const result =
|
|
356
|
+
const result = related(file, s.root);
|
|
327
357
|
for (const spec of [...result.global, ...result.scoped, ...result.changes]) relevant.add(spec);
|
|
328
358
|
}
|
|
329
359
|
const content = `Charterleaf final review — once only; mode=${s.mode}.\n` +
|
package/lib/project.js
CHANGED
|
@@ -12,9 +12,11 @@ const CONFIG = 'specs/.charterleaf.json';
|
|
|
12
12
|
|
|
13
13
|
function projectRoot(cwd) {
|
|
14
14
|
const start = fs.realpathSync(cwd);
|
|
15
|
+
let legacyRoot;
|
|
15
16
|
for (let current = start; ; current = path.dirname(current)) {
|
|
16
|
-
if (fs.existsSync(path.join(current, '.git'))
|
|
17
|
-
if (path.
|
|
17
|
+
if (fs.existsSync(path.join(current, '.git'))) return current;
|
|
18
|
+
if (!legacyRoot && fs.existsSync(path.join(current, CONFIG))) legacyRoot = current;
|
|
19
|
+
if (path.dirname(current) === current) return legacyRoot || start;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -52,7 +54,7 @@ function toolPath(input) {
|
|
|
52
54
|
return value;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function
|
|
57
|
+
function absoluteToolPath(cwd, input) {
|
|
56
58
|
if (typeof input !== 'string' || !input || input.includes('\0')) return null;
|
|
57
59
|
let value = toolPath(input);
|
|
58
60
|
if (process.platform !== 'win32' && /^[A-Za-z]:\//.test(value)) {
|
|
@@ -61,35 +63,77 @@ function projectPath(root, cwd, input) {
|
|
|
61
63
|
// A foreign UNC host cannot safely be interpreted as a local Linux path.
|
|
62
64
|
return null;
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
return path.resolve(cwd, value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function projectPath(root, cwd, input) {
|
|
70
|
+
const lexical = absoluteToolPath(cwd, input);
|
|
71
|
+
if (!lexical) return null;
|
|
65
72
|
const resolved = canonical(lexical);
|
|
66
73
|
if (!inside(root, lexical) || !inside(root, resolved)) return null;
|
|
67
74
|
return path.relative(root, resolved).replaceAll('\\', '/') || '.';
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
77
|
+
function modeConfigPath(root) {
|
|
78
|
+
let directory = root;
|
|
79
|
+
let file = path.join(root, CONFIG);
|
|
80
|
+
if (fs.existsSync(path.join(root, '.git'))) {
|
|
81
|
+
const env = { ...process.env };
|
|
82
|
+
// Git hooks and wrappers may export another repository's location; it is not this project's authority.
|
|
83
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE']) delete env[key];
|
|
84
|
+
directory = fs.realpathSync(execFileSync('git', ['-C', root, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
85
|
+
encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'], env,
|
|
86
|
+
}).trim());
|
|
87
|
+
file = path.join(directory, 'charterleaf.json');
|
|
88
|
+
}
|
|
89
|
+
if (!inside(directory, canonical(file))) throw new Error('Charterleaf config escapes project storage');
|
|
90
|
+
return file;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readConfig(file) {
|
|
74
94
|
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
75
|
-
if (config
|
|
95
|
+
if (config?.version !== 1 || !MODES.includes(config.mode)) throw new Error('Invalid Charterleaf config (expected version: 1 and a valid mode)');
|
|
76
96
|
return config.mode;
|
|
77
97
|
}
|
|
78
98
|
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
function readMode(root) {
|
|
100
|
+
const file = modeConfigPath(root);
|
|
101
|
+
if (fs.existsSync(file)) return readConfig(file);
|
|
102
|
+
const legacy = path.join(root, CONFIG);
|
|
103
|
+
if (file === legacy || !fs.existsSync(legacy)) return 'off';
|
|
104
|
+
if (!inside(root, canonical(legacy))) throw new Error('Charterleaf config escapes project');
|
|
105
|
+
// Preserve a prior explicit choice without changing tracked files or replacing a concurrent choice.
|
|
106
|
+
persistMode(file, readConfig(legacy), false);
|
|
107
|
+
return readConfig(file);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function persistMode(file, mode, replace) {
|
|
83
111
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
84
112
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
85
113
|
try {
|
|
86
|
-
fs.writeFileSync(temporary, `${JSON.stringify({ version: 1, mode }, null, 2)}\n`, { flag: 'wx' });
|
|
87
|
-
fs.renameSync(temporary, file);
|
|
114
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ version: 1, mode }, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
115
|
+
if (replace) fs.renameSync(temporary, file);
|
|
116
|
+
else {
|
|
117
|
+
try { fs.linkSync(temporary, file); }
|
|
118
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
119
|
+
}
|
|
88
120
|
} finally {
|
|
89
121
|
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
90
122
|
}
|
|
91
123
|
}
|
|
92
124
|
|
|
125
|
+
function saveMode(root, mode) {
|
|
126
|
+
if (!MODES.includes(mode)) throw new Error('Invalid Charterleaf mode');
|
|
127
|
+
persistMode(modeConfigPath(root), mode, true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isModeConfig(root, cwd, input) {
|
|
131
|
+
const lexical = absoluteToolPath(cwd, input);
|
|
132
|
+
if (!lexical) return false;
|
|
133
|
+
const target = canonical(lexical);
|
|
134
|
+
return [path.join(root, CONFIG), modeConfigPath(root)].some((file) => target === canonical(file));
|
|
135
|
+
}
|
|
136
|
+
|
|
93
137
|
function fingerprint(root, relative) {
|
|
94
138
|
const file = path.join(root, relative);
|
|
95
139
|
if (!inside(root, canonical(file))) return 'outside';
|
|
@@ -110,7 +154,9 @@ function gitSnapshot(root) {
|
|
|
110
154
|
encoding: 'utf8', timeout: 3000, maxBuffer: 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
155
|
});
|
|
112
156
|
try {
|
|
113
|
-
|
|
157
|
+
// Windows Git may expand an 8.3 temp-directory alias to its long path.
|
|
158
|
+
root = fs.realpathSync.native(root);
|
|
159
|
+
if (fs.realpathSync.native(git(['rev-parse', '--show-toplevel']).trim()) !== root) return null;
|
|
114
160
|
// Compare index and worktree separately; this also works before the first commit.
|
|
115
161
|
const names = [git(['diff', '--no-renames', '--name-only', '-z']), git(['diff', '--cached', '--no-renames', '--name-only', '-z']),
|
|
116
162
|
git(['ls-files', '--others', '--exclude-standard', '-z'])].join('\0').split('\0').filter(Boolean);
|
|
@@ -139,4 +185,4 @@ function changedSince(before, after) {
|
|
|
139
185
|
before.index?.get(name) !== after.index?.get(name)).sort();
|
|
140
186
|
}
|
|
141
187
|
|
|
142
|
-
module.exports = { CONFIG, MODES, projectRoot, projectPath, toolPath, readMode, saveMode, fingerprint, gitSnapshot, changedSince };
|
|
188
|
+
module.exports = { CONFIG, MODES, projectRoot, projectPath, toolPath, modeConfigPath, isModeConfig, readMode, saveMode, fingerprint, gitSnapshot, changedSince };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onedotmint/charterleaf",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "node --test",
|
|
41
|
+
"test:package": "node scripts/package-smoke.js",
|
|
41
42
|
"test:pi": "node scripts/pi-smoke.js",
|
|
42
43
|
"test:organic": "node scripts/organic-smoke.js",
|
|
43
44
|
"test:context": "node scripts/context-smoke.js"
|
|
@@ -10,11 +10,17 @@ The essential rules live in [runtime.md](runtime.md); read it only if not alread
|
|
|
10
10
|
|
|
11
11
|
Read this guide only for unfamiliar spec formats, staged changes or manual CLI use. Ordinary code edits need no Skill read or CLI call when applicable constraints have already been supplied.
|
|
12
12
|
|
|
13
|
+
## Mode scope
|
|
14
|
+
|
|
15
|
+
The user chooses the mode only through `/charterleaf`. In Git, it is saved in `charterleaf.json` under `git rev-parse --path-format=absolute --git-common-dir` (usually `.git/charterleaf.json`), shared across the repository's branches and linked worktrees, not independent clones. Outside Git, keep using `specs/.charterleaf.json`. No saved choice means off, never a global Auto default.
|
|
16
|
+
|
|
17
|
+
An existing local config is authoritative, including saved `off`; branch-local legacy config cannot override it. Only if local config is absent, first load imports the saved mode from a valid current-branch legacy `specs/.charterleaf.json` without overwriting local config or changing/removing the legacy file or tracked worktree. Invalid config must report an error and disable automation, not fall back. Never recover lost authorization from Git history or sessions. Enabling a Git project creates neither `specs/` nor spec documents; spec content and routing remain in the current worktree. The two CLI commands are unchanged and independent of mode storage.
|
|
18
|
+
|
|
13
19
|
## Ownership and durable writing
|
|
14
20
|
|
|
15
21
|
Source owns implementation; tests own executable verification; `AGENTS.md` owns project entry/navigation; specs own durable constraints. Never copy signatures, inventories, current state or existing documentation into specs. Link the authoritative owner instead. Charterleaf is not task management, planning, memory, evidence, history or a project workflow.
|
|
16
22
|
|
|
17
|
-
Capture during ordinary development, debugging, code/test/architecture investigation or technical decisions; do not wait for settlement or require existing specs. An enabled project
|
|
23
|
+
Capture during ordinary development, debugging, code/test/architecture investigation or technical decisions; do not wait for settlement or require existing specs. An enabled project with no spec documents (or no `specs/` directory in Git) is a valid starting state, not a bootstrap request; normal code reads/edits and later justified spec creation remain allowed under the selected mode.
|
|
18
24
|
|
|
19
25
|
Distinguish **newly established knowledge** (an accepted new contract/decision) from **newly discovered knowledge** (an existing but ineffectively documented constraint). Either can qualify without code changes; neither obliges every task to produce a document. Apply all three checks:
|
|
20
26
|
|