changeledger 0.9.0 → 0.10.0

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.
@@ -1,8 +1,49 @@
1
1
  import { cssIdent } from './security.js';
2
2
  import { html, nothing, svg } from './templates.js';
3
+ import { splitGraduationHistory } from './view-parts.js';
3
4
 
4
5
  const clip = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);
5
6
 
7
+ // Plain-text excerpt for spec cards: strips the leading graduation-history
8
+ // blockquote, picks the first prose paragraph (skipping headings, remaining
9
+ // blockquotes and code fences) and removes inline Markdown syntax. The result
10
+ // is interpolated as text (lit-html), never as HTML.
11
+ function firstProseParagraph(text) {
12
+ const paragraphs = String(text ?? '').split(/\n\s*\n/);
13
+ for (const para of paragraphs) {
14
+ const trimmed = para.trim();
15
+ if (!trimmed) continue;
16
+ if (/^#{1,6}\s/.test(trimmed)) continue;
17
+ if (/^>/.test(trimmed)) continue;
18
+ if (/^```/.test(trimmed)) continue;
19
+ return trimmed;
20
+ }
21
+ return '';
22
+ }
23
+
24
+ function stripMarkdown(text) {
25
+ return text
26
+ .replace(/`([^`]*)`/g, '$1')
27
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
28
+ .replace(/__([^_]+)__/g, '$1')
29
+ .replace(/\*([^*]+)\*/g, '$1')
30
+ .replace(/_([^_]+)_/g, '$1')
31
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
32
+ .replace(/\s+/g, ' ')
33
+ .trim();
34
+ }
35
+
36
+ export function specExcerpt(body, maxLen = 160) {
37
+ const { after } = splitGraduationHistory(body);
38
+ return clip(stripMarkdown(firstProseParagraph(after)), maxLen);
39
+ }
40
+
41
+ // Most recently updated truth first.
42
+ export function sortSpecsByUpdated(specs) {
43
+ const time = (s) => Date.parse(s?.updated || '') || 0;
44
+ return [...specs].sort((a, b) => time(b) - time(a));
45
+ }
46
+
6
47
  export function graphSvg(changes) {
7
48
  if (!changes.length) {
8
49
  return html`<p class="empty">No changes match the current filters.</p>`;
@@ -79,17 +120,19 @@ export function graphSvg(changes) {
79
120
  }
80
121
 
81
122
  export function specsListHtml(specs, fmtDateTime) {
82
- return specs.length
83
- ? specs.map(
84
- (s, i) => html`<div class="spec-card" data-i=${i}>
85
- <div class="spec-title">${s.title}</div>
86
- <div class="card-meta">
87
- <span title=${s.updated || ''}>${fmtDateTime(s.updated)}</span>
88
- ${(s.tags || []).map((t) => html`<span class="pill">${t}</span>`)}
89
- </div>
90
- </div>`,
91
- )
92
- : html`<p class="empty">No specs yet. Truth graduates here as changes complete.</p>`;
123
+ if (!specs.length) {
124
+ return html`<p class="empty">No specs yet. Truth graduates here as changes complete.</p>`;
125
+ }
126
+ return sortSpecsByUpdated(specs).map(
127
+ (s, i) => html`<div class="spec-card" data-i=${i}>
128
+ <div class="spec-title">${s.title}</div>
129
+ <div class="card-meta">
130
+ <span title=${s.updated || ''}>${fmtDateTime(s.updated)}</span>
131
+ ${(s.tags || []).map((t) => html`<span class="pill">${t}</span>`)}
132
+ </div>
133
+ <p class="spec-excerpt">${specExcerpt(s.body)}</p>
134
+ </div>`,
135
+ );
93
136
  }
94
137
 
95
138
  export function fmtDuration(ms) {
@@ -111,15 +154,86 @@ function barRows(items, label, value, fmt = (v) => v) {
111
154
  );
112
155
  }
113
156
 
114
- export function metricsHtml(metrics = {}) {
157
+ // Hand-rolled SVG bar chart for throughput: one bar per day, a date label and
158
+ // the numeric count both drawn as text so the value is visible without a
159
+ // tooltip (no charting dependency — same precedent as `graphSvg`).
160
+ function throughputSvg(tp) {
161
+ if (!tp.length) return html`<p class="empty">No closed changes yet.</p>`;
162
+
163
+ const width = 640;
164
+ const height = 180;
165
+ const padTop = 22;
166
+ const padBottom = 30;
167
+ const padSide = 12;
168
+ const plotWidth = Math.max(1, width - padSide * 2);
169
+ const plotHeight = Math.max(1, height - padTop - padBottom);
170
+ const max = Math.max(1, ...tp.map((t) => t.count));
171
+ const slot = plotWidth / tp.length;
172
+ const barWidth = Math.max(2, Math.min(48, slot - 8));
173
+
174
+ const bars = tp.map((t, i) => {
175
+ const barHeight = (t.count / max) * plotHeight;
176
+ const x = padSide + i * slot + (slot - barWidth) / 2;
177
+ const y = height - padBottom - barHeight;
178
+ return svg`<g class="tp-bar">
179
+ <rect class="tp-rect" x=${x} y=${y} width=${barWidth} height=${barHeight}></rect>
180
+ <text class="tp-value" x=${x + barWidth / 2} y=${Math.max(12, y - 6)} text-anchor="middle">${t.count}</text>
181
+ <text class="tp-date" x=${x + barWidth / 2} y=${height - padBottom + 16} text-anchor="middle">${t.date}</text>
182
+ </g>`;
183
+ });
184
+
185
+ return html`<svg
186
+ class="throughput-svg"
187
+ viewBox=${`0 0 ${width} ${height}`}
188
+ height=${height}
189
+ role="img"
190
+ aria-label="Throughput per day"
191
+ >
192
+ <line
193
+ class="tp-axis"
194
+ x1=${padSide}
195
+ y1=${height - padBottom}
196
+ x2=${width - padSide}
197
+ y2=${height - padBottom}
198
+ ></line>
199
+ ${bars}
200
+ </svg>`;
201
+ }
202
+
203
+ function metricsTable(rows, columnLabel, labelOf) {
204
+ return rows.length
205
+ ? html`<table class="grid">
206
+ <thead>
207
+ <tr><th>${columnLabel}</th><th>Closed</th><th>Avg cycle</th></tr>
208
+ </thead>
209
+ <tbody>
210
+ ${rows.map(
211
+ (r) => html`<tr>
212
+ <td>${labelOf(r)}</td>
213
+ <td class="mono">${r.closed}</td>
214
+ <td class="mono">${fmtDuration(r.avgCycleMs)}</td>
215
+ </tr>`,
216
+ )}
217
+ </tbody>
218
+ </table>`
219
+ : html`<p class="empty">No closed changes yet.</p>`;
220
+ }
221
+
222
+ export function metricsHtml(metrics = {}, totalChanges = 0) {
223
+ if (!totalChanges) {
224
+ return html`<p class="empty">No changes match the current filters.</p>`;
225
+ }
226
+
115
227
  const wip = metrics.wip || {};
116
228
  const wipTotal = Object.values(wip).reduce((a, b) => a + b, 0);
117
229
  const cards = [
118
230
  ['Closed', metrics.count ?? 0],
119
- ['Avg cycle', fmtDuration(metrics.avgCycleMs)],
120
- ['Median cycle', fmtDuration(metrics.medianCycleMs)],
231
+ ['Cycle p50', fmtDuration(metrics.p50CycleMs)],
232
+ ['Cycle p85', fmtDuration(metrics.p85CycleMs)],
121
233
  ['WIP', wipTotal],
122
234
  ['Blocked time', fmtDuration(metrics.blockedMs)],
235
+ ['Validation wait', fmtDuration(metrics.validationWaitMs)],
236
+ ['Review retries', metrics.reviewRetries ?? 0],
123
237
  ].map(
124
238
  ([label, val]) =>
125
239
  html`<div class="metric-card">
@@ -140,15 +254,6 @@ export function metricsHtml(metrics = {}) {
140
254
  )
141
255
  : html`<p class="empty">No data yet.</p>`;
142
256
 
143
- const tp = metrics.throughput || [];
144
- const tpBars = tp.length
145
- ? barRows(
146
- tp,
147
- (t) => html`<span class="mono">${t.date}</span>`,
148
- (t) => t.count,
149
- )
150
- : html`<p class="empty">No closed changes yet.</p>`;
151
-
152
257
  const aging = metrics.aging || [];
153
258
  const agingRows = aging.length
154
259
  ? html`<ul class="git-commits">
@@ -160,32 +265,39 @@ export function metricsHtml(metrics = {}) {
160
265
  : html`<p class="empty">Nothing in progress.</p>`;
161
266
 
162
267
  const byType = metrics.byType || [];
163
- const typeRows = byType.length
164
- ? html`<table class="grid">
165
- <thead>
166
- <tr><th>Type</th><th>Closed</th><th>Avg cycle</th></tr>
167
- </thead>
168
- <tbody>
169
- ${byType.map(
170
- (t) => html`<tr>
171
- <td><span class="type-tag" style=${`--type-color: var(--${cssIdent(t.type)})`}>${t.type}</span></td>
172
- <td class="mono">${t.closed}</td>
173
- <td class="mono">${fmtDuration(t.avgCycleMs)}</td>
174
- </tr>`,
175
- )}
176
- </tbody>
177
- </table>`
178
- : html`<p class="empty">No closed changes yet.</p>`;
268
+ const typeRows = metricsTable(
269
+ byType,
270
+ 'Type',
271
+ (t) =>
272
+ html`<span class="type-tag" style=${`--type-color: var(--${cssIdent(t.type)})`}>${t.type}</span>`,
273
+ );
274
+
275
+ const byOwner = metrics.byOwner || [];
276
+ const ownerRows = metricsTable(byOwner, 'Owner', (o) =>
277
+ o.owner === 'unassigned' ? html`<span class="muted">Unassigned</span>` : o.owner,
278
+ );
179
279
 
180
280
  return html`
181
281
  <div class="metrics-cards">${cards}</div>
182
- ${wipChips.length ? html`<div class="detail-meta">${wipChips}</div>` : nothing}
183
- <h3 class="metrics-h">Avg time in status (lead time per stage)</h3>
184
- <div>${leadBars}</div>
185
- <h3 class="metrics-h">Throughput (closed per day)</h3>
186
- <div>${tpBars}</div>
187
- <h3 class="metrics-h">Aging — in progress</h3>
188
- ${agingRows}
189
- <h3 class="metrics-h">By type</h3>
190
- ${typeRows}`;
282
+ <div class="metrics-grid">
283
+ <section class="metrics-panel">
284
+ <h3 class="metrics-h">Throughput (closed per day)</h3>
285
+ ${throughputSvg(metrics.throughput || [])}
286
+ </section>
287
+ <section class="metrics-panel">
288
+ <h3 class="metrics-h">Avg time in status (lead time per stage)</h3>
289
+ <div>${leadBars}</div>
290
+ </section>
291
+ <section class="metrics-panel">
292
+ <h3 class="metrics-h">Aging — in progress</h3>
293
+ ${wipChips.length ? html`<div class="detail-meta">${wipChips}</div>` : nothing}
294
+ ${agingRows}
295
+ </section>
296
+ <section class="metrics-panel">
297
+ <h3 class="metrics-h">By type</h3>
298
+ ${typeRows}
299
+ <h3 class="metrics-h">By owner</h3>
300
+ ${ownerRows}
301
+ </section>
302
+ </div>`;
191
303
  }
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { gitRefs } from '../../git.mjs';
5
- import { publicDir } from '../../paths.mjs';
5
+ import { packageRoot, publicDir } from '../../paths.mjs';
6
6
  import { loadRepoAsync } from '../../repo.mjs';
7
7
  import {
8
8
  applyConfigMigration,
@@ -49,10 +49,21 @@ function vendorFile(route) {
49
49
  const MIME = {
50
50
  '.html': 'text/html; charset=utf-8',
51
51
  '.js': 'text/javascript; charset=utf-8',
52
+ '.mjs': 'text/javascript; charset=utf-8',
52
53
  '.css': 'text/css; charset=utf-8',
53
54
  '.json': 'application/json; charset=utf-8',
54
55
  };
55
56
 
57
+ // The pure metrics module is shared verbatim between the CLI and the browser:
58
+ // the client dynamic-imports it so filtered metrics reuse the exact same
59
+ // computation as the server payload (no reimplementation). `lifecycle.mjs` is
60
+ // its only relative import, so both must be reachable under the same route
61
+ // prefix for the browser's module resolution to find it. Neither file is
62
+ // under `publicDir`; this is a narrow, explicit allowlist rather than opening
63
+ // up the rest of `src/` the way `publicDir` static assets are.
64
+ const SHARED_MODULES_DIR = path.join(packageRoot, 'src');
65
+ const SHARED_MODULES = new Set(['metrics.mjs', 'lifecycle.mjs']);
66
+
56
67
  // Defensive headers for a local-only UI: never sniff types, never cache, and
57
68
  // forbid embedding in a frame (clickjacking).
58
69
  const SECURITY_HEADERS = {
@@ -215,6 +226,13 @@ export function createRequestListener(cwd, localOnly, token) {
215
226
  return;
216
227
  }
217
228
 
229
+ if (route.startsWith('/shared/')) {
230
+ const shared = sharedModuleFile(route);
231
+ if (shared) send(res, 200, MIME['.mjs'], fs.readFileSync(shared));
232
+ else send(res, 404, 'text/plain', 'Not found');
233
+ return;
234
+ }
235
+
218
236
  const vendor = vendorFile(route);
219
237
  if (vendor) {
220
238
  if (fs.existsSync(vendor)) send(res, 200, MIME['.js'], fs.readFileSync(vendor));
@@ -239,7 +257,10 @@ export function createRequestListener(cwd, localOnly, token) {
239
257
  };
240
258
  }
241
259
 
242
- export function staticFile(route) {
260
+ // Resolves `route` to a file under `root`, refusing anything that decodes,
261
+ // resolves, or (post-symlink) realpaths outside of it. Shared by the public
262
+ // static assets and the narrower shared-module allowlist below.
263
+ function assetFile(root, route) {
243
264
  let decoded;
244
265
  try {
245
266
  decoded = decodeURIComponent(route);
@@ -249,16 +270,29 @@ export function staticFile(route) {
249
270
  if (decoded.includes('\0')) return null;
250
271
 
251
272
  const rel = decoded.replace(/^\/+/, '');
252
- const file = path.resolve(publicDir, rel);
253
- if (!isInsidePath(publicDir, file)) return null;
273
+ const file = path.resolve(root, rel);
274
+ if (!isInsidePath(root, file)) return null;
254
275
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return null;
255
276
 
256
- const realPublic = fs.realpathSync(publicDir);
277
+ const realRoot = fs.realpathSync(root);
257
278
  const realFile = fs.realpathSync(file);
258
- if (!isInsidePath(realPublic, realFile)) return null;
279
+ if (!isInsidePath(realRoot, realFile)) return null;
259
280
  return file;
260
281
  }
261
282
 
283
+ export function staticFile(route) {
284
+ return assetFile(publicDir, route);
285
+ }
286
+
287
+ // Serves only the whitelisted shared modules — never arbitrary files under
288
+ // `src/` — even though containment is checked the same way as public assets.
289
+ export function sharedModuleFile(route) {
290
+ if (!route.startsWith('/shared/')) return null;
291
+ const name = route.slice('/shared/'.length);
292
+ if (!SHARED_MODULES.has(name)) return null;
293
+ return assetFile(SHARED_MODULES_DIR, `/${name}`);
294
+ }
295
+
262
296
  function isInsidePath(root, target) {
263
297
  const rel = path.relative(path.resolve(root), path.resolve(target));
264
298
  return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
@@ -3,7 +3,7 @@
3
3
 
4
4
  # Schema version — used to detect and migrate outdated configurations.
5
5
  # Run `changeledger config migrate --dry-run` to inspect available migrations.
6
- schema_version: 1
6
+ schema_version: 2
7
7
 
8
8
  # Language for generated documentation CONTENT. Structure (headings, keys, enums)
9
9
  # is always English. See `changeledger context spec`.
@@ -23,6 +23,7 @@ release:
23
23
  audit: none
24
24
  refactor: none
25
25
  chore: none
26
+ quick: patch
26
27
 
27
28
  # Optional Definition of Ready path/command hints. When present, tasks that
28
29
  # reference CRs should name at least one target and one verification matching
@@ -58,3 +59,8 @@ types:
58
59
  review_required: true
59
60
  chore:
60
61
  stages: [request, plan]
62
+ # Small, reversible, single-concern work with no persistent-truth or public
63
+ # surface impact. If scope grows mid-execution, discard and recreate under
64
+ # the correct type — see `changeledger context spec`.
65
+ quick:
66
+ stages: [request, log]
@@ -0,0 +1,22 @@
1
+ # Read-Only Audit Delegate
2
+
3
+ This is a self-contained delegated context. It replaces the ChangeLedger core
4
+ for this role; do not run `changeledger context` or load another ChangeLedger
5
+ context.
6
+
7
+ This is a post-review inspection of a change already sitting in
8
+ `in-validation`, waiting for human acceptance. The review gate already ran;
9
+ you do not repeat it and you do not move the change. This role is read-only:
10
+ do not modify files, do not change Git state, do not mutate the ledger, do not
11
+ change status, do not add Log entries, and do not delegate any part of the
12
+ work.
13
+
14
+ Inspect the selected change, every `CRn`, the actual diff, tests and Git
15
+ history against what the document claims. Contrast criteria, code and
16
+ evidence; note any drift, residue or unresolved risk.
17
+
18
+ Return findings and evidence only — file:line references, what you confirmed,
19
+ what you could not confirm. Do not decide or state whether the change should
20
+ be accepted, sent back or blocked, and do not name or suggest a lifecycle
21
+ command: that decision belongs to the human waiting at `in-validation`, not to
22
+ this role.
@@ -0,0 +1,37 @@
1
+ # Delegation skeleton — role: audit
2
+
3
+ Fill every `{{placeholder}}` before handing this prompt to the subagent. Delete
4
+ guidance in parentheses. The auditor is a read-only inspection of a change
5
+ already sitting in `in-validation`, waiting for human acceptance; it is not a
6
+ review and never moves the change.
7
+
8
+ ---
9
+
10
+ You are a READ-ONLY AUDIT delegate. Do not delegate any part of this to
11
+ another agent; execute it yourself.
12
+
13
+ Why this is delegated: {{reason}} (independent inspection after review already
14
+ passed and the change is waiting for a human at `in-validation`).
15
+
16
+ Your prompt identifies you as a ChangeLedger delegate. As your only
17
+ ChangeLedger load, run `changeledger agent-context audit {{change_id}}` and
18
+ read it through its END sentinel; do not load the orchestrator core.
19
+
20
+ Change under audit: {{change_id}}.
21
+
22
+ Boundaries — expressed by effect, not by tool name: do not modify any file, do
23
+ not change Git state, and do not mutate the ledger. You inspect and report
24
+ only; the review gate already ran, so do not issue a verdict and do not name
25
+ or suggest a lifecycle command.
26
+
27
+ Expected output: {{expected_output}} (findings and evidence — file:line
28
+ references, what was confirmed, what could not be, and any drift or residue).
29
+
30
+ Difficulty or risk that set the model choice: {{difficulty_or_risk}}.
31
+
32
+ Return to the orchestrator or the human waiting at `in-validation` the
33
+ findings and evidence above; you never move the change.
34
+
35
+ Integration criterion: {{integration}} (how the orchestrator or human uses the
36
+ findings, e.g. it informs the human's accept/reject decision at
37
+ `in-validation`).
@@ -18,6 +18,10 @@ load, run `changeledger agent-context implementation {{change_id}}` and read it
18
18
  through its END sentinel; do not load the orchestrator core. It supplies the
19
19
  selected change with its acceptance criteria and Plan.
20
20
 
21
+ If that command does not resolve the change, or the working tree does not
22
+ contain its document, stop and report instead of proceeding: never reconstruct
23
+ the change from memory, and never continue from another base.
24
+
21
25
  Files you own: {{files}} (the only paths you may modify).
22
26
 
23
27
  Boundaries — expressed by effect, not by tool name: modify only the files under
@@ -54,7 +54,7 @@ Operational inspection and visibility:
54
54
 
55
55
  - `changeledger list [--status S] [--type T] [--json]`
56
56
  - `changeledger show <id> [--json]`
57
- - `changeledger archive <id>` / `changeledger unarchive <id>`
57
+ - `changeledger archive <id>` (reversible by manually editing `archived: false` in frontmatter)
58
58
 
59
59
  Use Mermaid where it communicates persistent relationships better than prose.
60
60
  After closure, share a brief retrospective. New work needs a newly authorized
@@ -16,6 +16,11 @@ While the complete core remains available in the active conversation, a new
16
16
  human message alone does not trigger a reload. Load only the specialized mode or
17
17
  change-id context required by a real task or lifecycle transition.
18
18
 
19
+ Every BEGIN line carries `rev:<hash>`. After a compaction, retest a retained
20
+ capture with `changeledger context [mode] --have <rev>` before recapturing it in
21
+ full: a match returns a short `unchanged` confirmation, a mismatch returns the
22
+ complete output, and the very first capture of a session is always full.
23
+
19
24
  1. Work starts with conversation. Read-only investigation may clarify a request,
20
25
  but create no change or implementation artifact until there is enough clarity
21
26
  to document faithfully **and** the human explicitly authorizes documentation. A direct request such
@@ -38,7 +43,9 @@ change-id context required by a real task or lifecycle transition.
38
43
  If no approved or in-progress change applies, do not silently edit repository
39
44
  files. Create or update a change, or ask the human whether a purely operational,
40
45
  reversible edit with no persistent truth or observable behavior change should be
41
- done directly. If unsure, document it in ChangeLedger.
46
+ done directly. If unsure, document it in ChangeLedger. For small, reversible,
47
+ single-concern work with observable behavior, use the `quick` type instead of
48
+ bypassing documentation — see `changeledger context spec`.
42
49
 
43
50
  Humans consume changes in `changeledger view`; write for the rendered view.
44
51
 
@@ -51,9 +58,11 @@ transitions and task markers.
51
58
  Delegate only with a clear boundary and benefit. Each delegation prompt states at least
52
59
  ownership, expected output and integration criterion; the task context carries the full
53
60
  prompt contract. Get a complete role skeleton to fill in with `changeledger agent-prompt
54
- <role>` (investigation | implementation | review). Coding agents must know they share the
55
- codebase and must not revert others' work. Do not over-shard or overlap write surfaces
56
- without an explicit integration plan. Size the model to the task's difficulty and risk.
61
+ <role>` (investigation | implementation | review | audit). Coding agents must know they
62
+ share the codebase and must not revert others' work. Do not over-shard or overlap write
63
+ surfaces without an explicit integration plan. Size the model to the task's difficulty
64
+ and risk. `audit` is a read-only post-review inspection of a change already in
65
+ `in-validation`; it never issues a verdict or moves the change.
57
66
 
58
67
  ## Lifecycle
59
68
 
@@ -108,6 +117,7 @@ Prefer structured CLI queries before scanning files:
108
117
 
109
118
  - `changeledger list --status approved`: find approved changes ready to implement.
110
119
  - `changeledger graduate --pending`: find accepted changes whose graduation decision is unresolved.
120
+ - `changeledger search <terms...>`: find related changes (incl. archived) and specs by content before investigating from scratch.
111
121
 
112
122
  Run `changeledger help` or `changeledger <command> --help` for exact CLI syntax.
113
123
  Structure is always English. Each context delivers the effective policy that
@@ -50,7 +50,8 @@ Every prompt states:
50
50
  - the owned files, area or investigation question;
51
51
  - the expected output;
52
52
  - the difficulty or risk that informed model choice;
53
- - the integration criterion.
53
+ - the integration criterion;
54
+ - for roles that write, the expected baseline (branch or commit) the delegate must verify it is working from.
54
55
 
55
56
  Tell coding delegates they share the codebase: stay inside assigned ownership,
56
57
  do not revert others' edits and report overlapping changes instead of silently
@@ -13,7 +13,9 @@ current throughout execution.
13
13
  ## Git protects traceability
14
14
 
15
15
  Never implement approved changes on `main`, `master`, or `dev`; create or switch
16
- to a work branch or ask the human before continuing. Inspect the worktree first. If
16
+ to a work branch or ask the human before continuing. When the config declares
17
+ `git.integration_branch`, create change branches from it and integrate the
18
+ finished result into it; `main` stays reserved for releases. Inspect the worktree first. If
17
19
  unrelated changes exist, do not include them silently; ask the human whether to
18
20
  stash, commit, ignore or include them before changing the worktree.
19
21
 
@@ -35,7 +37,16 @@ Commit messages use the canonical shape:
35
37
  feat(scope): description [#20260629-234939]
36
38
  ```
37
39
 
38
- Use the actual change id and the appropriate conventional type. If shared files make a combined commit
40
+ Use the actual change id and the appropriate conventional type. Referencing more
41
+ than one change is separate brackets, one per id (`[#A] [#B]`) — never a comma
42
+ list inside one bracket. Ledger meta-commits (status, review, log, archive)
43
+ carry the marker like any other commit; only merge commits and `chore(release)`
44
+ prep are exempt. `changeledger commit -m "<type>(<scope>): <desc>" [--id <id>]...`
45
+ composes and creates the commit for you: it resolves the single `in-progress`
46
+ change automatically when `--id` is omitted, and fails without committing if
47
+ that is ambiguous or the subject isn't conventional. `changeledger check
48
+ --commits [<base>]` lints `<base>..HEAD` for the marker with the same
49
+ exemptions — run it before requesting review. If shared files make a combined commit
39
50
  unavoidable, record it in Log or the handoff and name every change sharing the
40
51
  surface.
41
52
 
@@ -53,6 +64,7 @@ Useful mutation commands:
53
64
  - `changeledger owner <id> <name|->`
54
65
  - `changeledger review <id> pass|fail`
55
66
  - `changeledger check [id]`
67
+ - `changeledger commit -m "<type>(<scope>): <desc>" [--id <id>]...`
56
68
 
57
69
  When implementation and every task are complete, move to `in-review` if the type
58
70
  requires independent review by running this ordered gate — do not reconstruct it from memory:
@@ -38,7 +38,7 @@ headings. Required and optional frontmatter:
38
38
  ---
39
39
  id: "20260613-134548"
40
40
  title: Short, clear title
41
- type: feature # feature | bug | audit | refactor | chore
41
+ type: feature # feature | bug | audit | refactor | chore | quick
42
42
  status: draft # lifecycle value
43
43
  created: 2026-06-13T13:45:48Z # full ISO 8601 UTC
44
44
  depends_on: [] # change ids or external project:id refs
@@ -78,11 +78,23 @@ Default activation matrix:
78
78
  | audit | ✓ | ✓ | — | — | — | ✓ |
79
79
  | refactor | ✓ | — | ✓ | — | ✓ | ✓ |
80
80
  | chore | ✓ | — | — | — | ✓ | — |
81
+ | quick | ✓ | — | — | — | — | ✓ |
81
82
 
82
83
  The configured matrix is authoritative. For bugs, Investigation contains the
83
84
  root cause; for audits, it is the core analysis. Proposal includes the chosen
84
85
  solution, discarded alternatives and scenarios.
85
86
 
87
+ `quick` is a lighter lane for small, reversible, single-concern work that does
88
+ not expand public surface or persistent truth (`specs/`): document only
89
+ Request and Log, ~10-15 lines. It keeps the same human `draft → approved` gate
90
+ and `[#id]` commit marker as any other type, skipping only `in-review`. If
91
+ scope grows mid-execution beyond that eligibility, discard the change and
92
+ recreate it under the correct type instead of continuing under `quick`.
93
+
94
+ Before writing Investigation, run `changeledger search <terms from the request>`
95
+ and record anything relevant as `depends_on` or a mention — do not rediscover
96
+ work another change or spec already covers.
97
+
86
98
  When a relationship, flow or architecture is clearer visually, use a Mermaid
87
99
  block and keep its text as the source; the viewer renders it.
88
100
 
@@ -155,6 +167,7 @@ title, stage prose, scenario content and task descriptions.
155
167
  - `changeledger check [id]` — validate one change or the repository.
156
168
  - `changeledger list [--status S] [--type T] [--json]` — inspect/filter changes.
157
169
  - `changeledger show <id> [--json]` — inspect one resolved change.
170
+ - `changeledger search <terms...> [--type T] [--status S] [--json]` — find related changes and specs by content.
158
171
  - `changeledger owner <id> <name|->` — set or clear responsibility.
159
172
 
160
173
  Run `changeledger <command> --help` for exact options; the commands support the