changeledger 0.9.0 → 0.11.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.
@@ -22,6 +22,8 @@
22
22
  --status-done: #3fb950;
23
23
  --status-discarded: #9aa0aa;
24
24
  --status-muted: #8b929e;
25
+ /* fallback only: app.js keeps this synced to the real .topbar height */
26
+ --header-height: 55px;
25
27
  font-family: "Avenir Next", Avenir, ui-sans-serif, sans-serif;
26
28
  color-scheme: dark;
27
29
  }
@@ -115,17 +117,20 @@ body {
115
117
  /* Project management */
116
118
  .projects-view {
117
119
  padding: 18px;
120
+ height: calc(100dvh - var(--header-height));
121
+ box-sizing: border-box;
118
122
  }
119
123
  .projects-shell {
120
124
  display: grid;
121
125
  grid-template-columns: minmax(320px, 0.8fr) minmax(460px, 1.2fr);
122
126
  gap: 16px;
123
- max-width: 1500px;
124
- margin: 0 auto;
127
+ height: 100%;
125
128
  }
126
129
  .projects-list,
127
130
  .project-editor {
128
131
  min-width: 0;
132
+ min-height: 0;
133
+ overflow-y: auto;
129
134
  border: 1px solid var(--line);
130
135
  border-radius: 14px;
131
136
  background: var(--panel);
@@ -303,8 +308,16 @@ body {
303
308
  cursor: wait;
304
309
  }
305
310
  @media (max-width: 900px) {
311
+ .projects-view {
312
+ height: auto;
313
+ }
306
314
  .projects-shell {
307
315
  grid-template-columns: 1fr;
316
+ height: auto;
317
+ }
318
+ .projects-list,
319
+ .project-editor {
320
+ overflow-y: visible;
308
321
  }
309
322
  .project-row {
310
323
  grid-template-columns: 10px minmax(0, 1fr) auto;
@@ -894,6 +907,49 @@ body {
894
907
  font-size: 13px;
895
908
  border-bottom: 1px solid var(--line);
896
909
  }
910
+ .muted {
911
+ color: var(--muted);
912
+ }
913
+ .metrics-grid {
914
+ display: grid;
915
+ grid-template-columns: repeat(2, minmax(0, 1fr));
916
+ gap: 14px;
917
+ margin-top: 18px;
918
+ }
919
+ @media (max-width: 1100px) {
920
+ .metrics-grid {
921
+ grid-template-columns: minmax(0, 1fr);
922
+ }
923
+ }
924
+ .metrics-panel {
925
+ background: var(--panel);
926
+ border: 1px solid var(--line);
927
+ border-radius: 8px;
928
+ padding: 4px 18px 16px;
929
+ min-width: 0;
930
+ }
931
+ .metrics-panel .metrics-h:first-child {
932
+ margin-top: 14px;
933
+ }
934
+ .throughput-svg {
935
+ width: 100%;
936
+ height: auto;
937
+ }
938
+ .tp-rect {
939
+ fill: var(--accent, #4a9eff);
940
+ }
941
+ .tp-value {
942
+ font-size: 11px;
943
+ fill: var(--text);
944
+ }
945
+ .tp-date {
946
+ font-size: 10px;
947
+ fill: var(--muted);
948
+ }
949
+ .tp-axis {
950
+ stroke: var(--line);
951
+ stroke-width: 1;
952
+ }
897
953
 
898
954
  /* Overlay + detail */
899
955
  .detail-open {
@@ -1321,10 +1377,10 @@ body {
1321
1377
  /* Specs view */
1322
1378
  .specs-view {
1323
1379
  padding: 16px;
1324
- display: flex;
1325
- flex-direction: column;
1380
+ display: grid;
1381
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
1326
1382
  gap: 10px;
1327
- max-width: 760px;
1383
+ align-items: start;
1328
1384
  }
1329
1385
  .spec-card {
1330
1386
  background: var(--panel);
@@ -1340,6 +1396,21 @@ body {
1340
1396
  font-weight: 600;
1341
1397
  margin-bottom: 6px;
1342
1398
  }
1399
+ .spec-excerpt {
1400
+ margin: 8px 0 0;
1401
+ color: var(--muted);
1402
+ font-size: 0.9em;
1403
+ overflow: hidden;
1404
+ display: -webkit-box;
1405
+ -webkit-line-clamp: 3;
1406
+ -webkit-box-orient: vertical;
1407
+ }
1408
+
1409
+ @media (max-width: 680px) {
1410
+ .specs-view {
1411
+ grid-template-columns: 1fr;
1412
+ }
1413
+ }
1343
1414
  .empty {
1344
1415
  color: var(--muted);
1345
1416
  padding: 8px;
@@ -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: 3
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
@@ -34,6 +35,10 @@ release:
34
35
  changes_dir: .changeledger/changes
35
36
  specs_dir: .changeledger/specs
36
37
 
38
+ # Git integration: change branches start from and merge into this branch
39
+ git:
40
+ integration_branch:
41
+
37
42
  # Valid lifecycle statuses (order = progress)
38
43
  statuses: [draft, approved, in-progress, in-review, in-validation, blocked, done, discarded]
39
44
 
@@ -58,3 +63,8 @@ types:
58
63
  review_required: true
59
64
  chore:
60
65
  stages: [request, plan]
66
+ # Small, reversible, single-concern work with no persistent-truth or public
67
+ # surface impact. If scope grows mid-execution, discard and recreate under
68
+ # the correct type — see `changeledger context spec`.
69
+ quick:
70
+ 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