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.
- package/AGENTS.md +5 -1
- package/README.md +5 -0
- package/bin/changeledger.mjs +127 -20
- package/package.json +1 -1
- package/src/check.mjs +12 -0
- package/src/commands/agent-context.mjs +2 -1
- package/src/commands/agent-prompt.mjs +1 -1
- package/src/commands/agent.mjs +3 -3
- package/src/commands/check.mjs +55 -0
- package/src/commands/commit.mjs +39 -0
- package/src/commands/context.mjs +57 -23
- package/src/commands/fix.mjs +72 -0
- package/src/commands/register.mjs +9 -1
- package/src/commands/search.mjs +56 -0
- package/src/config-migration.mjs +44 -9
- package/src/config.mjs +13 -0
- package/src/contract.mjs +67 -13
- package/src/fix.mjs +127 -0
- package/src/framing.mjs +8 -0
- package/src/git.mjs +119 -2
- package/src/metrics.mjs +42 -0
- package/src/search.mjs +162 -0
- package/src/viewer/domain.mjs +1 -1
- package/src/viewer/public/app.js +61 -11
- package/src/viewer/public/styles.css +76 -5
- package/src/viewer/public/view-renderers.js +160 -48
- package/src/viewer/server/router.mjs +40 -6
- package/templates/config.yml +7 -1
- package/templates/contract/agent-contexts/audit.md +22 -0
- package/templates/contract/agent-prompts/audit.md +37 -0
- package/templates/contract/agent-prompts/implementation.md +4 -0
- package/templates/contract/close.md +1 -1
- package/templates/contract/core.md +14 -4
- package/templates/contract/delegation.md +2 -1
- package/templates/contract/implement.md +14 -2
- package/templates/contract/spec.md +14 -1
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
['
|
|
120
|
-
['
|
|
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 =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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(
|
|
253
|
-
if (!isInsidePath(
|
|
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
|
|
277
|
+
const realRoot = fs.realpathSync(root);
|
|
257
278
|
const realFile = fs.realpathSync(file);
|
|
258
|
-
if (!isInsidePath(
|
|
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));
|
package/templates/config.yml
CHANGED
|
@@ -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:
|
|
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>`
|
|
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
|
|
55
|
-
codebase and must not revert others' work. Do not over-shard or overlap write
|
|
56
|
-
without an explicit integration plan. Size the model to the task's difficulty
|
|
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.
|
|
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.
|
|
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
|