@onedotmint/charterleaf 0.4.0 → 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 +21 -5
- package/README.md +148 -49
- package/extensions/charterleaf.js +92 -21
- package/lib/project.js +63 -17
- package/package.json +5 -2
- package/skills/charterleaf/SKILL.md +34 -2
- package/skills/charterleaf/runtime.md +8 -4
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
|
|
|
@@ -28,7 +39,12 @@ With automation enabled, use supplied constraints or explicitly read the
|
|
|
28
39
|
named specs before retrying a gated write/edit. Use standard `write`/`edit`
|
|
29
40
|
for spec changes: Confirm requires each approval; Reference allows no
|
|
30
41
|
spec changes. Do not bypass these cooperative checks with Bash or another
|
|
31
|
-
writer.
|
|
42
|
+
writer. During normal development or investigation, capture newly established
|
|
43
|
+
or newly discovered knowledge only with reliable evidence, long-term value and a
|
|
44
|
+
documentation/discoverability gap; no code change or prior spec is required.
|
|
45
|
+
Prefer short scoped references to authoritative owners over copying documents.
|
|
46
|
+
Explicit no-file-write/read-only instructions override Auto. No justified durable
|
|
47
|
+
knowledge means no spec edit; never relax a spec merely
|
|
32
48
|
to accommodate conflicting code. Preserve user/parallel work and keep
|
|
33
49
|
state, plans, evidence and history in their existing authoritative owners,
|
|
34
50
|
not in a new Charterleaf workflow.
|
package/README.md
CHANGED
|
@@ -36,10 +36,10 @@ Install once in Pi to discover the bundled Extension and Skill:
|
|
|
36
36
|
pi install npm:@onedotmint/charterleaf
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`.tgz` file).
|
|
42
|
-
|
|
39
|
+
For the standalone CLI, use `npm install -g @onedotmint/charterleaf`.
|
|
40
|
+
For local development, use `pi install /absolute/path/to/charterleaf` (a package
|
|
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`.
|
|
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.
|
|
@@ -128,18 +148,56 @@ requires fresh delivery; successful standard spec writes invalidate loaded state
|
|
|
128
148
|
Compaction invalidates loaded context so constraints must be delivered
|
|
129
149
|
again. This is task-local bookkeeping, not a watcher or persistent index.
|
|
130
150
|
|
|
151
|
+
Knowledge capture happens **during normal work**, not just after code changes:
|
|
152
|
+
development, debugging, substantive code/test/architecture investigation and
|
|
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
|
|
156
|
+
it, the Agent creates a concise scoped spec using ordinary authorized `write`/`edit`;
|
|
157
|
+
Core immediately routes its `id`/`applies_to` on later related code reads or edits.
|
|
158
|
+
There is no initialization scan, document quota or adoption wizard.
|
|
159
|
+
|
|
131
160
|
At successful, actionable `agent_before_settle`, the Extension may request
|
|
132
|
-
**at most one final semantic review per task
|
|
133
|
-
|
|
161
|
+
**at most one final semantic review per task**. Actual development remains a
|
|
162
|
+
fallback, but an unresolved evidenced knowledge candidate can also trigger it
|
|
163
|
+
without file changes or Git. The existing Agent makes the semantic judgment; the
|
|
164
|
+
Extension observes standalone assistant lifecycle comments outside fenced examples
|
|
165
|
+
(`charterleaf: review` or an explicit `covered`/`no-value`/`proposed` knowledge
|
|
166
|
+
conclusion; an exact terminal conclusion also works inline), never interprets read
|
|
167
|
+
counts as knowledge. A generic `done` cannot suppress the development fallback.
|
|
168
|
+
In an empty-spec project, a development fallback
|
|
169
|
+
remains eligible when the Agent says `no-value`; this gives the existing single
|
|
170
|
+
review a chance to catch a missed discovery gap. These are
|
|
171
|
+
cooperative intent signals, not evidence or permissions. Already judged/captured,
|
|
172
|
+
proposed or effectively documented knowledge needs no repeat review when a
|
|
173
|
+
completion signal is observed. If the Agent
|
|
174
|
+
omits its completion signal, the bounded fallback can repeat an already answered
|
|
175
|
+
knowledge judgment; zero redundant reviews is not guaranteed. Successful
|
|
176
|
+
structurally valid standard spec writes mark capture complete; later actual code
|
|
177
|
+
edits or distinct candidates may rearm. Opaque activity after capture still uses
|
|
178
|
+
bounded Git comparison when available. Ordinary views, queries and chat do not
|
|
179
|
+
by themselves request another model call.
|
|
180
|
+
|
|
181
|
+
For the development fallback, the Extension separates successful standard-tool
|
|
182
|
+
paths whose observed content is still
|
|
134
183
|
unchanged from **unattributed** changes observed against the task's Git
|
|
135
184
|
baseline. Neither a touched file nor a dirty worktree proves ownership
|
|
136
185
|
of every line. Preserve existing user dirt and parallel work; unattributed
|
|
137
186
|
changes alone must not justify spec edits. No automatic revert, stage,
|
|
138
187
|
commit or push is performed.
|
|
139
188
|
|
|
140
|
-
**No durable knowledge means no spec edit.**
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
**No justified durable knowledge means no spec edit.** Distinguish **newly
|
|
190
|
+
established** contracts/decisions from **newly discovered** existing constraints;
|
|
191
|
+
both can qualify even in a code-read-only investigation. Require reliable evidence
|
|
192
|
+
(accepted decisions, authoritative docs, tests or sufficiently verified behavior),
|
|
193
|
+
future mistake-prevention value, and a documentation/discoverability gap. If an
|
|
194
|
+
existing authoritative document covers it, prefer a short routed reference when
|
|
195
|
+
needed; otherwise do nothing. Keep non-obvious contracts, invariants, boundaries,
|
|
196
|
+
pitfalls or accepted decisions — not code summaries, progress, debugging logs,
|
|
197
|
+
temporary plans/todos or speculation. Current code alone is not an accepted
|
|
198
|
+
correct contract. Explicit **no-file-write/read-only** instructions override Auto:
|
|
199
|
+
report a finding/proposal without writing. Confirm/Reference retain their existing
|
|
200
|
+
permissions. A
|
|
143
201
|
code/spec conflict is not permission to accommodate incorrect code:
|
|
144
202
|
distinguish implementation bugs, authoritatively obsolete specs and
|
|
145
203
|
explicitly intended contract changes. When uncertain, preserve the spec
|
|
@@ -256,7 +314,8 @@ specs/
|
|
|
256
314
|
|
|
257
315
|
A spec records a constraint whose absence would plausibly cause a future
|
|
258
316
|
developer or agent to make a wrong decision. It should be non-obvious,
|
|
259
|
-
durable, **already true**,
|
|
317
|
+
durable, **already true or explicitly accepted**, evidenced, mistake-preventing,
|
|
318
|
+
and not already effectively documented. Good fits: contracts,
|
|
260
319
|
invariants, architecture boundaries, non-goals, recurring pitfalls,
|
|
261
320
|
accepted rationale, verification constraints.
|
|
262
321
|
|
|
@@ -274,8 +333,8 @@ review comes first, or a multi-step migration is underway. A change must
|
|
|
274
333
|
contain at least one of `ADD`, `MODIFY`, `REMOVE`; `PRESERVE` alone is
|
|
275
334
|
not a change.
|
|
276
335
|
|
|
277
|
-
Otherwise, implement
|
|
278
|
-
changed
|
|
336
|
+
Otherwise, implement/verify as needed and maintain the living spec when durable
|
|
337
|
+
truth changed **or an existing valuable constraint lacks effective documentation**. After an accepted change is implemented, merge the durable
|
|
279
338
|
result into living specs and delete the change file. Git keeps history.
|
|
280
339
|
|
|
281
340
|
## Agent conventions
|
|
@@ -325,7 +384,8 @@ Important limits:
|
|
|
325
384
|
path that was checked; spec authorization also preserves the original path's
|
|
326
385
|
protected identity when a spec symlink points elsewhere inside the project.
|
|
327
386
|
Foreign UNC paths are not interpreted as local paths; symlink escapes are
|
|
328
|
-
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.
|
|
329
389
|
- `related` has no filesystem awareness of the supplied target path and
|
|
330
390
|
no proof of routing coverage. Task-local hashes are not a continuous
|
|
331
391
|
monitor of external changes. A single spec line larger than Pi's read byte
|
|
@@ -348,34 +408,73 @@ Verification commands:
|
|
|
348
408
|
|
|
349
409
|
```bash
|
|
350
410
|
npm test
|
|
351
|
-
npm run test:
|
|
411
|
+
npm run test:package # real tarball install and tests against installed code
|
|
412
|
+
npm run test:pi # isolated real RPC/UI smoke; no model calls
|
|
413
|
+
CHARTERLEAF_MODEL=deepseek/deepseek-flash npm run test:organic # paid isolated A–G acceptance
|
|
414
|
+
CHARTERLEAF_MODEL=deepseek/deepseek-flash CHARTERLEAF_BASELINE=5d470db npm run test:context # paid comparison against v0.4.0
|
|
415
|
+
node scripts/organic-smoke.js --self-test # harness/oracle check, no model calls
|
|
352
416
|
node --test tests/extension.test.js
|
|
353
417
|
npm pack --dry-run
|
|
354
418
|
node bin/charterleaf.js related extensions/charterleaf.js
|
|
355
419
|
node bin/charterleaf.js lint
|
|
356
420
|
```
|
|
357
421
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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.
|
|
436
|
+
|
|
437
|
+
The 0.4.1 organic-capture acceptance used real `deepseek/deepseek-flash` sessions
|
|
438
|
+
in isolated Git projects. Actual tool results, approval records, specs, routing,
|
|
439
|
+
file snapshots/diffs, original tests and an independent exact-money oracle were
|
|
440
|
+
checked, not just assistant claims. Empty-spec development and investigation,
|
|
441
|
+
ordinary-query non-triggers, authoritative-document references, bug-versus-contract
|
|
442
|
+
handling, cross-session reuse and mode/read-only/no-UI permissions all have passing
|
|
443
|
+
evidence. **The earlier strict matrix was not consistently all-green:** a focused
|
|
444
|
+
A→F1→F2 run passed A and F1, while F2 omitted its knowledge-completion comment and
|
|
445
|
+
received one redundant fallback review. Its actual routing, code behavior and
|
|
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
|
|
450
|
+
on every task or provider, or guarantee zero redundant reviews.
|
|
451
|
+
|
|
452
|
+
The controlled organic-capture comparison (`scripts/context-smoke.js`) keeps cwd,
|
|
453
|
+
package paths, prompt and native tools identical. On `deepseek/deepseek-flash`:
|
|
454
|
+
disabled 1,616 input tokens; v0.4.0 baseline 2,036 (increment 420); organic runtime
|
|
455
|
+
2,142 (increment 526), or **106 additional fixed input tokens**. Input here includes
|
|
456
|
+
cached input. Each chat made exactly one request and no tool calls; the full Skill
|
|
457
|
+
remains on demand. Absolute path tokenization and provider tokenizers vary, and
|
|
458
|
+
cache-hit sizes affect uncached charges, so these are measured examples, not fixed
|
|
459
|
+
billing guarantees. The script defaults to `HEAD`; set `CHARTERLEAF_BASELINE` to the
|
|
460
|
+
older revision being compared (the command above uses the v0.4.0 release commit).
|
|
461
|
+
Substantive work requests **zero or at most one** extra model review: observed
|
|
462
|
+
capture/coverage/proposal completion skips the fallback; empty-spec development can
|
|
463
|
+
still get the one fallback after a `no-value` judgment.
|
|
464
|
+
|
|
465
|
+
The real-model acceptance script retains raw RPC/JSONL, conversations, actual tool
|
|
466
|
+
records, before/after files, tracked/untracked diffs, validation output and individual
|
|
467
|
+
assertions in the printed private temporary directory, including failures. It copies
|
|
468
|
+
credentials only to isolated settings and removes that copy on normal/error
|
|
469
|
+
completion; a forced process kill may require deleting the private credential
|
|
470
|
+
copy manually. It does not
|
|
471
|
+
import personal extensions/instructions or modify other repositories, and it is not
|
|
472
|
+
an OS security sandbox. Both paid scripts require explicit `CHARTERLEAF_MODEL=provider/id`; neither chooses
|
|
473
|
+
another provider automatically. The requested model must be available;
|
|
474
|
+
`--case A,B,...` selects bounded scenarios. F cases run A first, then B only if A failed to capture; they reuse only
|
|
475
|
+
model-written knowledge in the same project across fresh sessions. A's failure
|
|
476
|
+
remains a failed assertion; a successful B→F chain does not conceal it. Model usage counts visible assistant responses, not hidden
|
|
477
|
+
provider HTTP retries. Cost fixtures also remove copied credentials on exit.
|
|
379
478
|
|
|
380
479
|
For an interactive smoke check in a disposable project, install the local
|
|
381
480
|
package in a compatible Pi host and run `/charterleaf`: check immediate
|
|
@@ -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
|
|
|
@@ -26,14 +32,20 @@ module.exports = function charterleaf(pi) {
|
|
|
26
32
|
if (mode === 'off') { state = undefined; return; }
|
|
27
33
|
if (!state || state.root !== root) state = {
|
|
28
34
|
root, mode, loaded: new Map(), staged: new Map(), pages: new Map(), pending: new Map(), denied: new Set(),
|
|
29
|
-
owned: new Map(), parents: new Map(), envelopes: new Map(), before: gitSnapshot(root), budget: 24000, reviewed: false, miss: false, opaque: false,
|
|
35
|
+
owned: new Map(), parents: new Map(), envelopes: new Map(), before: gitSnapshot(root), budget: 24000, reviewed: false, miss: false, opaque: false, knowledge: undefined,
|
|
30
36
|
};
|
|
31
37
|
state.mode = mode;
|
|
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
|
});
|
|
@@ -148,6 +177,23 @@ module.exports = function charterleaf(pi) {
|
|
|
148
177
|
} };
|
|
149
178
|
});
|
|
150
179
|
|
|
180
|
+
// Semantic intent comes from the existing agent, never from file/read counts.
|
|
181
|
+
pi.on('message_end', (event, ctx) => {
|
|
182
|
+
if (event.message.role !== 'assistant' || ['error', 'aborted'].includes(event.message.stopReason)) return;
|
|
183
|
+
let s;
|
|
184
|
+
try { s = current(ctx); } catch { return; }
|
|
185
|
+
if (!s) return;
|
|
186
|
+
const text = event.message.content.filter((part) => part.type === 'text').map((part) => part.text).join('\n');
|
|
187
|
+
const prose = text.replace(/```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)/g, '');
|
|
188
|
+
const signal = prose.match(/<!-- charterleaf: (covered|no-value|proposed) -->\s*$/) ||
|
|
189
|
+
[...prose.matchAll(/^<!-- charterleaf: (review|covered|no-value|proposed) -->[ \t]*$/gm)].at(-1);
|
|
190
|
+
if (signal) {
|
|
191
|
+
if (signal[1] === 'proposed' && s.mode === 'confirm' && !s.proposed) return;
|
|
192
|
+
s.knowledge = signal[1];
|
|
193
|
+
if (s.knowledge !== 'review') { s.capture = gitSnapshot(s.root); s.opaque = false; }
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
151
197
|
// A sibling tool call has not seen another sibling's result yet. Promote only at the next model request.
|
|
152
198
|
pi.on('context', (_event, ctx) => {
|
|
153
199
|
if (!state || state.root !== projectRoot(ctx.cwd)) return;
|
|
@@ -163,6 +209,9 @@ module.exports = function charterleaf(pi) {
|
|
|
163
209
|
if (!['read', 'write', 'edit'].includes(event.toolName)) return;
|
|
164
210
|
const mutation = event.toolName !== 'read';
|
|
165
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
|
+
}
|
|
166
215
|
const file = projectPath(s.root, ctx.cwd, event.input.path);
|
|
167
216
|
const lexical = typeof event.input.path === 'string'
|
|
168
217
|
? path.relative(s.root, path.resolve(ctx.cwd, toolPath(event.input.path))).replaceAll('\\', '/') : null;
|
|
@@ -175,19 +224,17 @@ module.exports = function charterleaf(pi) {
|
|
|
175
224
|
// Execute the exact normalized path checked here, including Pi's @/file:/space aliases.
|
|
176
225
|
const target = path.join(s.root, file);
|
|
177
226
|
event.input.path = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/.test(target) ? pathToFileURL(target).href : target;
|
|
178
|
-
if (mutation && (file === CONFIG || lexical === CONFIG || file === projectPath(s.root, s.root, CONFIG))) {
|
|
179
|
-
return { block: true, reason: 'Only the user may change Charterleaf mode via /charterleaf.' };
|
|
180
|
-
}
|
|
181
227
|
if (mutation && (isSpec(file) || (lexical && isSpec(lexical)))) {
|
|
182
228
|
if (s.mode === 'reference') return { block: true, reason: 'Charterleaf Reference mode: report a proposal; do not modify specs.' };
|
|
183
229
|
if (s.mode === 'confirm') {
|
|
230
|
+
s.proposed = true;
|
|
184
231
|
if (!ctx.hasUI) return { block: true, reason: 'Charterleaf Confirm mode requires interactive approval. Report the proposal without writing.' };
|
|
185
232
|
const proposal = JSON.stringify(event.input, null, 2);
|
|
186
233
|
if (proposal.length > 12000) return { block: true, reason: 'Spec proposal too large to approve safely. Use smaller standard edits.' };
|
|
187
234
|
if (s.denied.has(proposal)) return { block: true, reason: 'This exact spec proposal was already refused in this task. Do not retry; report it without writing.' };
|
|
188
235
|
const before = fingerprint(s.root, file);
|
|
189
236
|
const approved = await ctx.ui.confirm(`Charterleaf: modify ${file}?`,
|
|
190
|
-
`Approve this exact ${event.toolName} only if the durable
|
|
237
|
+
`Approve this exact ${event.toolName} only if the evidenced durable knowledge maintenance is intended. Never relax a spec just to match conflicting code.\n\n${proposal}`, { timeout: 60000 });
|
|
191
238
|
if (!approved) {
|
|
192
239
|
s.denied.add(proposal);
|
|
193
240
|
return { block: true, reason: 'Spec modification not approved. Preserve the current spec and report the proposal; do not retry.' };
|
|
@@ -223,7 +270,12 @@ module.exports = function charterleaf(pi) {
|
|
|
223
270
|
s.envelopes.delete(event.toolCallId);
|
|
224
271
|
if (event.isError) {
|
|
225
272
|
if (call?.mutation) {
|
|
226
|
-
try {
|
|
273
|
+
try {
|
|
274
|
+
if (fingerprint(s.root, call.file) !== call.before) {
|
|
275
|
+
s.opaque = true;
|
|
276
|
+
if (s.knowledge !== 'review') s.knowledge = undefined;
|
|
277
|
+
}
|
|
278
|
+
} catch { s.opaque = true; s.knowledge = undefined; }
|
|
227
279
|
}
|
|
228
280
|
if (envelope) return { content: [...event.content, { type: 'text', text: `[Charterleaf]\n${envelope}` }] };
|
|
229
281
|
return;
|
|
@@ -233,18 +285,24 @@ module.exports = function charterleaf(pi) {
|
|
|
233
285
|
if (call) {
|
|
234
286
|
const hash = fingerprint(s.root, call.file);
|
|
235
287
|
if (call.mutation && hash !== call.before) {
|
|
288
|
+
if (s.knowledge !== 'review') s.knowledge = undefined;
|
|
236
289
|
s.owned.set(call.file, hash);
|
|
237
290
|
if (isSpec(call.file)) {
|
|
238
291
|
s.loaded.delete(call.file);
|
|
239
292
|
s.pages.delete(call.file);
|
|
240
293
|
let output = '';
|
|
241
294
|
const stream = { write: (text) => { output += text; } };
|
|
242
|
-
lint(s.root, { stdout: stream, stderr: stream })
|
|
295
|
+
if (lint(s.root, { stdout: stream, stderr: stream }) === 0) {
|
|
296
|
+
s.knowledge = 'captured';
|
|
297
|
+
s.capture = gitSnapshot(s.root);
|
|
298
|
+
s.opaque = false;
|
|
299
|
+
}
|
|
243
300
|
note = `Charterleaf structural check (not semantic verification):\n${output.slice(0, 4000)}`;
|
|
244
301
|
}
|
|
245
302
|
} else if (!call.mutation) {
|
|
246
303
|
if (isSpec(call.file)) {
|
|
247
|
-
const fullRead =
|
|
304
|
+
const fullRead = !/^ *\d+: /.test(visible) &&
|
|
305
|
+
event.input.offset === undefined && event.input.startLine === undefined &&
|
|
248
306
|
event.input.limit === undefined && event.input.endLine === undefined &&
|
|
249
307
|
event.details?.truncation?.truncated !== true &&
|
|
250
308
|
!event.content.some((part) => part.type === 'text' &&
|
|
@@ -278,30 +336,43 @@ module.exports = function charterleaf(pi) {
|
|
|
278
336
|
if (!s || s.reviewed || event.outcome !== 'completed') return;
|
|
279
337
|
s.reviewed = true;
|
|
280
338
|
try {
|
|
281
|
-
if (
|
|
339
|
+
if (['captured', 'covered', 'proposed'].includes(s.knowledge)) {
|
|
340
|
+
if (!s.opaque) return;
|
|
341
|
+
const afterCapture = gitSnapshot(s.root);
|
|
342
|
+
if (s.capture && afterCapture && !changedSince(s.capture, afterCapture).some((file) => !isSpec(file))) return;
|
|
343
|
+
}
|
|
344
|
+
if (!['review', 'no-value'].includes(s.knowledge) && !s.owned.size && !s.opaque) return;
|
|
282
345
|
const after = gitSnapshot(s.root);
|
|
283
346
|
const comparable = s.before && after;
|
|
284
347
|
const observed = changedSince(s.before, after);
|
|
285
348
|
const owned = [...s.owned].filter(([file, hash]) => fingerprint(s.root, file) === hash && !isSpec(file)).map(([file]) => file);
|
|
286
349
|
const unattributed = observed.filter((file) => !s.owned.has(file) || fingerprint(s.root, file) !== s.owned.get(file));
|
|
287
|
-
|
|
350
|
+
const emptySpecs = s.knowledge === 'no-value' && related('', s.root).available.length === 0;
|
|
351
|
+
if (s.knowledge === 'no-value' && !(emptySpecs && owned.length > 0)) return;
|
|
352
|
+
const candidate = s.knowledge === 'review';
|
|
353
|
+
if (!candidate && !owned.length && !unattributed.length && !(s.opaque && !comparable)) return;
|
|
288
354
|
const relevant = new Set();
|
|
289
355
|
for (const file of [...owned, ...unattributed].slice(0, 50)) {
|
|
290
|
-
const result =
|
|
356
|
+
const result = related(file, s.root);
|
|
291
357
|
for (const spec of [...result.global, ...result.scoped, ...result.changes]) relevant.add(spec);
|
|
292
358
|
}
|
|
293
359
|
const content = `Charterleaf final review — once only; mode=${s.mode}.\n` +
|
|
360
|
+
(candidate ? 'The agent identified unresolved durable knowledge in this task; file changes are not required. Judge the conversation evidence, not the signal itself.\n' : '') +
|
|
361
|
+
(emptySpecs ? 'Development in an empty-spec project: verify the no-value conclusion against evidence of existing undocumented constraints, not just newly changed contracts.\n' : '') +
|
|
294
362
|
`Successful standard tool edits still unchanged at settlement: ${JSON.stringify(owned.slice(0, 50))}.\n` +
|
|
295
363
|
`Observed worktree changes NOT attributable to this agent: ${JSON.stringify(unattributed.slice(0, 50))}. ` +
|
|
296
364
|
'These may belong to the user or a parallel session; preserve them; do not modify specs based solely on them. Existing dirty content is not yours.\n' +
|
|
297
365
|
`Possibly affected specs (routing hints only): ${JSON.stringify([...relevant].slice(0, 50))}.\n` +
|
|
298
366
|
(!comparable ? 'Git change inspection unavailable (baseline or final snapshot missing). Opaque tool changes cannot be identified or attributed. Review only evidence in this conversation.\n' : '') +
|
|
299
|
-
'
|
|
300
|
-
'
|
|
367
|
+
'Consider newly established AND newly discovered knowledge, including read-only investigation or accepted decisions; no code change is required. In an empty-spec project, tests or accepted behavior that reveal a non-obvious invariant without effective code-path guidance is a discovery gap; do not dismiss it merely because tests pass or the implementation already works. ' +
|
|
368
|
+
'No justified durable knowledge means no spec edit; no code summary, task log or plans. ' +
|
|
369
|
+
'Record only evidenced, non-obvious contracts, invariants, boundaries, pitfalls or accepted decisions whose absence risks future mistakes and which are not effectively documented. ' +
|
|
370
|
+
'Current code alone is not an accepted contract. Prefer a short routed reference to an authoritative document over copying it; do not duplicate knowledge already captured in this task. ' +
|
|
371
|
+
'Honor explicit no-file-write/read-only instructions even in Auto; report only. ' +
|
|
301
372
|
'A code/spec conflict is not permission to rewrite the spec: distinguish implementation bug, obsolete spec and explicitly intended contract change. ' +
|
|
302
373
|
'If uncertain, preserve the spec and report the conflict. ' +
|
|
303
374
|
(s.mode === 'reference' ? 'Reference: report necessary proposals/conflicts only; never write specs. ' :
|
|
304
|
-
s.mode === 'confirm' ? 'Confirm:
|
|
375
|
+
s.mode === 'confirm' ? 'Confirm: invoke the exact spec write/edit to open the Extension approval dialog; the extension must obtain approval before execution. If already refused/no UI, report without retrying. ' :
|
|
305
376
|
'Auto: update only when durable knowledge is justified by this task; otherwise leave specs unchanged. ') +
|
|
306
377
|
'For unfamiliar authoring/staging details, read the guide. Use standard write/edit and lint. ' +
|
|
307
378
|
'Report task results/tests, not routine review bookkeeping; do not start another development cycle.';
|
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,7 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"scripts": {
|
|
40
40
|
"test": "node --test",
|
|
41
|
-
"test:
|
|
41
|
+
"test:package": "node scripts/package-smoke.js",
|
|
42
|
+
"test:pi": "node scripts/pi-smoke.js",
|
|
43
|
+
"test:organic": "node scripts/organic-smoke.js",
|
|
44
|
+
"test:context": "node scripts/context-smoke.js"
|
|
42
45
|
},
|
|
43
46
|
"pi": {
|
|
44
47
|
"extensions": [
|
|
@@ -10,18 +10,50 @@ 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
|
-
|
|
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.
|
|
24
|
+
|
|
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:
|
|
26
|
+
|
|
27
|
+
1. Reliable evidence: accepted user intent, authoritative documentation, tests or sufficiently verified implementation behavior. Current code alone does not establish an accepted correct contract.
|
|
28
|
+
2. Long-term value: ignoring it could plausibly cause a future developer/Agent to make a wrong decision.
|
|
29
|
+
3. A documentation or discoverability gap: inspect relevant existing owners/specs before writing. If an authoritative document already covers it, add only a short scoped reference when routing is genuinely missing; otherwise do nothing. Never duplicate a captured rule. Executable tests provide evidence, not automatically effective code-path discovery: accepted non-obvious invariants scattered among tests may warrant a short scoped constraint/reference even when no new contract was established. Do not copy test cases or document trivial assertions; judge whether a future code change could miss the boundary.
|
|
30
|
+
|
|
31
|
+
Honor explicit no-file-write/read-only restrictions in every mode. Otherwise **code edits are allowed normally in all three modes**, including Reference and Confirm. Auto can create or update specs immediately when justified; Confirm uses the existing exact-spec-write approval; Reference only proposes spec changes, never restricts implementation edits. **In Confirm, invoke standard `write`/`edit` with the exact spec proposal to open the Extension's approval dialog; do not merely ask for prose permission and stop.** The call is an approval request, not an unauthorized mutation: the Extension pauses it before execution, denies it without UI, and only executes the exact operation if approved. If evidence conflicts, verify authority and distinguish a bug from an accepted change, never rewrite truth to match suspect code.
|
|
32
|
+
|
|
33
|
+
Place qualifying non-obvious knowledge in:
|
|
18
34
|
|
|
19
35
|
- `specs/constitution.md`: rare project-wide boundaries;
|
|
20
36
|
- `specs/capabilities/`: observable contracts, invariants and non-goals;
|
|
21
37
|
- `specs/engineering/`: long-lived implementation boundaries, recurring pitfalls and verification constraints;
|
|
22
38
|
- `specs/decisions/`: accepted rationale and revisit conditions.
|
|
23
39
|
|
|
24
|
-
Living capability/engineering specs need a unique `id` and non-empty `applies_to` list
|
|
40
|
+
Living capability/engineering specs need a unique `id` and non-empty `applies_to` list, including the first organically created spec. For example:
|
|
41
|
+
|
|
42
|
+
```markdown
|
|
43
|
+
---
|
|
44
|
+
id: engineering.amounts
|
|
45
|
+
applies_to:
|
|
46
|
+
- src/amounts.js
|
|
47
|
+
- tests/amounts.test.js
|
|
48
|
+
---
|
|
49
|
+
# Amount boundary
|
|
50
|
+
|
|
51
|
+
Preserve the accepted rounding contract; see [accounting policy](../../docs/accounting.md#rounding) and its executable cases in `tests/amounts.test.js`.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use this shape, not this example's rule, for the actual project evidence. Route rationale in `decisions/` through a relevant capability/engineering reference when automatic code-path discovery is needed. Keep scope narrow, preserve existing formats and requirement IDs, and verify explicit local references normally. `anchors` and `verified_by` are not required. Do not create speculative standards or code summaries to satisfy a final review. Task progress, implementation inventories, one-off debugging, temporary plans/todos and unverified behavior never belong in Specs.
|
|
55
|
+
|
|
56
|
+
The runtime's standalone assistant comments (outside code fences; exact terminal completion comments also work inline) `<!-- charterleaf: review -->`, `<!-- charterleaf: covered -->`, `<!-- charterleaf: no-value -->` and `<!-- charterleaf: proposed -->` are task-local lifecycle signals, not spec contents, evidence or permissions. `review` means a concrete evidenced candidate still needs knowledge judgment; `covered` means relevant authority is already effectively discoverable; `no-value` means the evidence/value/gap checks found no qualifying knowledge; `proposed` means a Reference/read-only proposal was reported or a Confirm proposal passed through the gate and was refused/no UI. Use these exact comments; put explanation outside, never inside them. If an explicit no-write task surfaced a qualified constraint, report the finding/proposal in the normal answer and finish the knowledge judgment there rather than requesting a continuation just to repeat it. Ordinary code/test completion is not any of those conclusions: a generic `done` comment is deliberately ignored. Never use `covered` merely because tests exercise a boundary without effective code-path discovery. In an enabled empty-spec project, do not reject a non-obvious accepted invariant merely because the tests already exercise it. Waiting for human approval is not unresolved knowledge judgment: do not request a semantic continuation just to ask the same permission again. After submitting an exact proposal to the approval gate, handle approval/refusal/no UI and finish normally. New distinct findings may rearm, but the Extension continues at most once. Successful structurally valid standard spec writes also mark capture complete; later code edits rearm the development fallback. In an empty-spec project, owned development after `no-value` still permits the single fallback to check for missed discovery gaps. Omitting a recognized conclusion can cause a redundant fallback even when the normal answer already states that nothing needs documenting. Plain reads, file counts, search results and task wording are not semantic discovery signals.
|
|
25
57
|
|
|
26
58
|
## Optional staged changes
|
|
27
59
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# Charterleaf runtime
|
|
2
2
|
|
|
3
|
-
Use
|
|
3
|
+
Use supplied specs; no bulk/unchanged rereads. Blocked write/edit: inspect constraints/read the complete spec, then retry; never bypass via another writer. Routing: a hint.
|
|
4
4
|
|
|
5
|
-
Modes govern specs, not code: Auto maintains
|
|
5
|
+
Modes govern specs, not code: Auto maintains justified knowledge; Confirm calls spec write/edit to open Extension approval (refusal/no UI: preserve, no retry); Reference proposes, never writes specs. All modes allow normal code edits. Use standard write/edit only. User-owned config. read-only/no-file-write requests override Auto. Not a filesystem sandbox.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Internal/refactor requests do not authorize changing return or ownership contracts. Conflicts: verify authority, fix bugs; never relax specs to match code. Invariant-breaking method? Preserve spec/tests; get an explicit contract decision before coding. Uncertain: preserve and report. Preserve user/parallel work; Git dirt/touched files aren't line ownership. No automatic revert/commit/push.
|
|
8
|
+
|
|
9
|
+
## Organic knowledge capture
|
|
10
|
+
|
|
11
|
+
During development/debugging/investigation/decisions capture newly established OR newly discovered knowledge, even without code changes/prior specs. Need reliable evidence (accepted decisions, authoritative docs, tests or verified behavior), mistake-prevention value and a documentation/discoverability gap. Code alone is not an accepted contract. Check owners; tests aren't automatic code-path routing; prefer short routed references over copying docs. New capability/engineering specs: unique id, narrow applies_to; see guide. No justified knowledge: no spec. No code summaries/progress/logs/plans/todos/speculation.
|
|
8
12
|
|
|
9
13
|
## One final semantic review
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
Judge during work, not by read counts. Use one exact comment: <!-- charterleaf: review --> (evidenced pending), <!-- charterleaf: covered --> (effective authority), <!-- charterleaf: no-value --> (no value), <!-- charterleaf: proposed --> (Reference/refused Confirm/read-only report). Reasons outside, never inside comments. NOT merely finishing code/tests. Signals aren't evidence/permission. At most one extra review; no loop. Report results/tests, not review logs. Lint is structural only.
|