@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 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`; the saved project mode in
15
- `specs/.charterleaf.json` controls automation. Missing config or `off`
16
- means no automatic scan, adoption prompt or document creation. Do not
17
- change activation config on the user's behalf.
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. No durable change means no spec edit; never relax a spec merely
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
- **v0.4 is not yet published.** Until publication, install the local package
40
- with `pi install /absolute/path/to/charterleaf` (a package directory, not a
41
- `.tgz` file). For the standalone CLI, pack with `npm pack`, then use `npm install -g ./onedotmint-charterleaf-0.4.0.tgz`; after publication,
42
- use `npm install -g @onedotmint/charterleaf`.
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 `specs/.charterleaf.json`:
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`. Missing config
77
- means **off**; existing `specs/` alone never enables automation. Activation
78
- is an explicit user choice, not an Agent decision. Enabling creates only
79
- this config (and its directory if needed), not a constitution or a pile
80
- of spec documents. Disabling preserves documents; cancel changes nothing.
81
- Disabled/unconfigured projects get no automatic adoption prompt, task
82
- context, scan, or document bootstrap. Manual CLI use is independent of
83
- this config.
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 offset pages are credited only when their exact content
113
- covers the same spec version with no gaps; ordinary prose about truncation does
114
- not count as a tool truncation notice. On a scoped miss, the Extension supplies metadata only for
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** after development. It
133
- separates successful standard-tool paths whose observed content is still
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.** Keep only established,
141
- non-obvious, mistake-preventing contracts, invariants, boundaries,
142
- pitfalls or accepted decisions — not code summaries or task logs. A
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**, and mistake-preventing. Good fits: contracts,
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, verify, and update the living spec if durable truth
278
- changed. After an accepted change is implemented, merge the durable
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 behavior needs platform testing.
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:pi # requires compatible Pi on PATH; isolated RPC UI smoke, no model calls
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
- Validated on Node.js 24.21.0 and Pi 1.1.0: 149 automated Core/Extension tests,
359
- structural lint, dry-run npm packaging, and real Pi RPC loading/native selection
360
- for cancellation, mode changes, disable and cross-process restore. The RPC smoke
361
- uses isolated temporary projects/settings and makes no model calls.
362
-
363
- Independent acceptance also exercised real `openai/gpt-5.6-terra` sessions in
364
- isolated Git projects: ordinary refactors, intentional API updates, durable
365
- rounding knowledge, conflicts, all three modes, no-UI Confirm, parallel nested
366
- tools, oversized specs, dirty/concurrently modified files, Bash writes and
367
- invalid metadata. Evidence was checked against tool results and file diffs,
368
- not assistant claims. These bounded examples do not prove model reliability
369
- on every task or provider. The original activation message measured 8,450 UTF-8
370
- bytes (8.25 KiB), mostly the full Skill; it was not all native Pi context or a
371
- Token count. After the dedicated context audit, compact rules plus activation
372
- measure about 2 KiB: in a controlled first-request comparison the Charterleaf
373
- increment fell from 1,720 to 403 provider input tokens, with identical native
374
- prompt/tools. Smaller total context does not guarantee the same reduction in
375
- uncached input charges: cache-hit sizes vary. Substantive work still requests
376
- at most one extra model review. A model
377
- initially misreported a blocked edit as completed; clearer error text corrected
378
- the observed regression, but final claims still need independent verification.
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 { CONFIG, projectRoot, projectPath, toolPath, readMode, saveMode, fingerprint, gitSnapshot, changedSince } = require('../lib/project.js');
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 = resolveRelated(file, s.root);
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\[(?:Showing lines \d+-\d+ of \d+[^\n]*|\d+ more lines in file\.[^\n]*)Use offset=\d+ to continue\.\]$/;
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
- const end = start + body.split('\n').length;
76
- if (start < 0 || end > lines.length || lines.slice(start, end).join('\n') !== body) return false;
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 choice = await ctx.ui.select(`Charterleaf — ${mode}\n${root}`, OPTIONS);
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 ${CONFIG})`, 'info');
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 contract change is intended. Never relax a spec just to match conflicting code.\n\n${proposal}`, { timeout: 60000 });
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 { if (fingerprint(s.root, call.file) !== call.before) s.opaque = true; } catch { s.opaque = true; }
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 = event.input.offset === undefined && event.input.startLine === undefined &&
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 (!s.owned.size && !s.opaque) return;
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
- if (!owned.length && !unattributed.length && !(s.opaque && !comparable)) return;
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 = resolveRelated(file, s.root);
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
- 'No durable change means no spec edit; no code summary, task log or plans. ' +
300
- 'Record only established, non-obvious contracts, invariants, boundaries, pitfalls or accepted decisions. ' +
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: propose the exact spec edit with standard write/edit; the extension must obtain approval before execution. If already refused/no UI, report without retrying. ' :
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')) || fs.existsSync(path.join(current, CONFIG))) return current;
17
- if (path.dirname(current) === current) return start;
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 projectPath(root, cwd, input) {
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
- const lexical = path.resolve(cwd, value);
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 readMode(root) {
71
- const file = path.join(root, CONFIG);
72
- if (!fs.existsSync(file)) return 'off';
73
- if (!inside(root, canonical(file))) throw new Error('Charterleaf config escapes project');
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.version !== 1 || !MODES.includes(config.mode)) throw new Error('Invalid Charterleaf config (expected version: 1 and a valid mode)');
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 saveMode(root, mode) {
80
- if (!MODES.includes(mode)) throw new Error('Invalid Charterleaf mode');
81
- const file = path.join(root, CONFIG);
82
- if (!inside(root, canonical(file))) throw new Error('Charterleaf config escapes project');
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
- if (fs.realpathSync(git(['rev-parse', '--show-toplevel']).trim()) !== root) return null;
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.0",
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:pi": "node scripts/pi-smoke.js"
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
- Place only established, non-obvious knowledge whose absence could cause a future mistake:
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. 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.
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 constraints supplied with standard read results; do not load all specs or reread unchanged supplied bodies. A blocked write/edit requires inspecting the constraints or reading the named complete spec before retrying; never bypass it through another writer. Routing is a hint, not proof of coverage.
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 only justified durable knowledge; Confirm edits code normally and submits exact spec write/edit to the Extension's approval dialog (refusal/no UI: preserve spec, do not retry); Reference reports proposals/conflicts, never writes specs. Use standard write/edit only. Activation config is user-owned via /charterleaf. These guards are not a filesystem sandbox.
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
- A code/spec conflict is not permission to relax the spec. Fix implementation bugs; change contracts only with authoritative evidence or explicit accepted intent. Internal/refactor requests do not authorize changing return or ownership contracts. If a requested method violates an invariant, preserve spec/tests and ask for an explicit contract decision before coding. When uncertain, preserve the spec and report the conflict. Preserve user/parallel work; a touched file or dirty worktree does not prove line ownership. Never automatically revert, commit or push.
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
- Only established, non-obvious, mistake-preventing contracts, invariants, boundaries, pitfalls or accepted decisions belong in specs. No durable change means no spec edit: exclude implementation summaries, tasks, progress, debugging and plans. At most one final review may follow development; do not start another cycle. Report task results/tests, not routine review bookkeeping. Spec writes receive structural lint, not semantic verification.
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.