changeledger 0.8.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.
Files changed (48) hide show
  1. package/AGENTS.md +11 -3
  2. package/README.md +10 -1
  3. package/bin/changeledger.mjs +204 -21
  4. package/package.json +1 -1
  5. package/src/check.mjs +12 -0
  6. package/src/commands/agent-context.mjs +65 -0
  7. package/src/commands/agent-prompt.mjs +22 -0
  8. package/src/commands/agent.mjs +13 -10
  9. package/src/commands/check.mjs +55 -0
  10. package/src/commands/commit.mjs +39 -0
  11. package/src/commands/context.mjs +60 -27
  12. package/src/commands/fix.mjs +72 -0
  13. package/src/commands/register.mjs +9 -1
  14. package/src/commands/search.mjs +56 -0
  15. package/src/config-migration.mjs +44 -9
  16. package/src/config.mjs +13 -0
  17. package/src/contract.mjs +73 -15
  18. package/src/fix.mjs +127 -0
  19. package/src/framing.mjs +30 -0
  20. package/src/git.mjs +119 -2
  21. package/src/lifecycle.mjs +2 -2
  22. package/src/metrics.mjs +42 -0
  23. package/src/search.mjs +162 -0
  24. package/src/viewer/domain.mjs +2 -2
  25. package/src/viewer/public/app-state.js +62 -6
  26. package/src/viewer/public/app.js +138 -39
  27. package/src/viewer/public/index.html +2 -2
  28. package/src/viewer/public/state.js +6 -2
  29. package/src/viewer/public/styles.css +76 -5
  30. package/src/viewer/public/view-renderers.js +160 -48
  31. package/src/viewer/server/router.mjs +40 -6
  32. package/templates/config.yml +7 -1
  33. package/templates/contract/agent-contexts/audit.md +22 -0
  34. package/templates/contract/agent-contexts/implementation.md +18 -0
  35. package/templates/contract/agent-contexts/investigation.md +14 -0
  36. package/templates/contract/agent-contexts/review.md +22 -0
  37. package/templates/contract/agent-prompts/audit.md +37 -0
  38. package/templates/contract/agent-prompts/implementation.md +42 -0
  39. package/templates/contract/agent-prompts/investigation.md +41 -0
  40. package/templates/contract/agent-prompts/review.md +36 -0
  41. package/templates/contract/budgets.yml +16 -0
  42. package/templates/contract/close.md +19 -14
  43. package/templates/contract/core.md +41 -30
  44. package/templates/contract/delegation.md +2 -1
  45. package/templates/contract/implement.md +24 -10
  46. package/templates/contract/review.md +7 -9
  47. package/templates/contract/spec.md +14 -1
  48. package/templates/contract/validation.md +1 -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
- 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,18 @@
1
+ # Implementation 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
+ Implement only the bounded assignment in the delegation prompt and follow the
8
+ selected change's Specification and Plan exactly. With `tdd=on`, write the
9
+ failing test for each assigned criterion, make it pass, then refactor.
10
+
11
+ Modify only the files assigned in the delegation prompt. You share the worktree:
12
+ do not revert or overwrite others' work; stop and report any overlap instead of
13
+ resolving it silently. Do not change Git state, mutate the ledger or delegate
14
+ any part of the work. Lifecycle, tasks, Log, review and integration remain the
15
+ orchestrator's responsibility.
16
+
17
+ Return the files changed, criteria covered, tests run and their results, plus
18
+ any overlap or uncertainty. The orchestrator integrates and records the work.
@@ -0,0 +1,14 @@
1
+ # Investigation 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
+ Answer only the investigation question and ownership boundary in the delegation
8
+ prompt. This role is read-only: do not modify files, Git state or the ledger, and
9
+ do not delegate any part of the work.
10
+
11
+ Inspect the smallest useful surface. Return concrete findings and evidence such
12
+ as file:line references, constraints, risks and a root-cause statement when
13
+ applicable. State what could not be determined. Do not implement fixes or create
14
+ ChangeLedger artifacts.
@@ -0,0 +1,22 @@
1
+ # Independent Review 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
+ Review with clean context and do not trust the implementer's summary. This role
8
+ is read-only: do not modify files, Git state or the ledger, do not record the
9
+ verdict, and do not delegate any part of the work.
10
+
11
+ Inspect the selected change, every `CRn`, every Plan task, tests, the actual diff
12
+ and absence of TODO/FIXME, dead code or unrelated residue. Confirm tasks are
13
+ true rather than merely checked off and that implementation did not drift from
14
+ the authorized document.
15
+
16
+ Deep security, SAST and lint belong to dedicated tools. You may run them
17
+ read-only and report their evidence; ChangeLedger does not reimplement them.
18
+
19
+ Return per-criterion PASS/FAIL evidence and one recommended outcome: pass,
20
+ fail-retry with a reason for a fixable defect inside scope, or fail-block with a
21
+ reason when correction needs scope or product judgment. The orchestrator alone
22
+ records the verdict.
@@ -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`).
@@ -0,0 +1,42 @@
1
+ # Delegation skeleton — role: implementation
2
+
3
+ Fill every `{{placeholder}}` before handing this prompt to the subagent. Delete
4
+ guidance in parentheses. The delegate writes code within a bounded ownership; the
5
+ orchestrator keeps the ledger and integration.
6
+
7
+ ---
8
+
9
+ You are an IMPLEMENTATION delegate. Do not delegate any part of this to another
10
+ agent; execute it yourself.
11
+
12
+ Why this is delegated: {{reason}} (why a separate agent, e.g. a disjoint write
13
+ set that parallelizes safely, a sufficient cheaper model for well-specified
14
+ execution).
15
+
16
+ Your prompt identifies you as a ChangeLedger delegate. As your only ChangeLedger
17
+ load, run `changeledger agent-context implementation {{change_id}}` and read it
18
+ through its END sentinel; do not load the orchestrator core. It supplies the
19
+ selected change with its acceptance criteria and Plan.
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
+
25
+ Files you own: {{files}} (the only paths you may modify).
26
+
27
+ Boundaries — expressed by effect, not by tool name: modify only the files under
28
+ your ownership above; do not revert or overwrite anyone else's work; if you find
29
+ your change overlaps another change's surface, stop and report it instead of
30
+ resolving it silently. Do not mutate the ledger — status, task, log, review and
31
+ graduation transitions are the orchestrator's; you implement and report.
32
+
33
+ Expected output: {{expected_output}} (the code plus the tests that prove the
34
+ cited criteria, red-green; state which criteria you covered).
35
+
36
+ Difficulty or risk that set the model choice: {{difficulty_or_risk}}.
37
+
38
+ Return to the orchestrator only what changed and how it was verified — the files
39
+ touched, the tests run and their result, and any overlap you hit.
40
+
41
+ Integration criterion: {{integration}} (how the orchestrator merges/verifies your
42
+ work, e.g. it re-runs the full gate and records the ledger transitions).
@@ -0,0 +1,41 @@
1
+ # Delegation skeleton — role: investigation
2
+
3
+ Fill every `{{placeholder}}` before handing this prompt to the subagent. Delete
4
+ guidance in parentheses. This is a read-only inquiry: the delegate answers a
5
+ question, it does not change anything.
6
+
7
+ ---
8
+
9
+ You are an INVESTIGATION delegate. Do not delegate any part of this to another
10
+ agent; execute it yourself.
11
+
12
+ Why this is delegated: {{reason}} (why a separate agent, e.g. protect main
13
+ context, parallelize an independent question, bring a stronger model to hard
14
+ analysis).
15
+
16
+ Question you own: {{question}} (the single question or area to investigate;
17
+ state it concretely).
18
+
19
+ Your prompt identifies you as a ChangeLedger delegate. As your only ChangeLedger
20
+ load, run `changeledger agent-context investigation {{change_id}}` and read it
21
+ through its END sentinel; do not load the orchestrator core. There may be no
22
+ change yet: work without a change id. If the optional id below is empty, omit it
23
+ from the command.
24
+
25
+ Optional selected change: {{change_id}} (leave empty when investigating before a
26
+ change exists).
27
+
28
+ Boundaries — expressed by effect, not by tool name: do not modify any file, do
29
+ not change Git state, and do not mutate the ledger (no status, task, log, review
30
+ or graduation). Inspect and read only.
31
+
32
+ Expected output: {{expected_output}} (what the answer must contain, e.g. a
33
+ file:line map, a root-cause statement, a constraints/risks list).
34
+
35
+ Difficulty or risk that set the model choice: {{difficulty_or_risk}}.
36
+
37
+ Return to the orchestrator only findings/data — no narrative, no fixes. State
38
+ clearly what you could not determine.
39
+
40
+ Integration criterion: {{integration}} (how the orchestrator will use the answer,
41
+ e.g. it feeds the Investigation stage of the change).
@@ -0,0 +1,36 @@
1
+ # Delegation skeleton — role: review
2
+
3
+ Fill every `{{placeholder}}` before handing this prompt to the subagent. Delete
4
+ guidance in parentheses. The reviewer is a fresh, clean-context, read-only check;
5
+ the orchestrator alone records the verdict.
6
+
7
+ ---
8
+
9
+ You are an INDEPENDENT REVIEW delegate with clean context. Do not delegate any
10
+ part of this to another agent; execute it yourself.
11
+
12
+ Why this is delegated: {{reason}} (independence is a correctness requirement, not
13
+ an optimization — a fresh reviewer that does not trust the implementer's summary).
14
+
15
+ Your prompt identifies you as a ChangeLedger delegate. As your only ChangeLedger
16
+ load, run `changeledger agent-context review {{change_id}}` and read it through
17
+ its END sentinel; do not load the orchestrator core. Use the inspection
18
+ checklist that agent-context gives you.
19
+
20
+ Change under review: {{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 only;
24
+ you never record the verdict — the orchestrator does that.
25
+
26
+ Expected output: {{expected_output}} (per-criterion PASS/FAIL with concrete
27
+ evidence, plus any drift or residue found).
28
+
29
+ Difficulty or risk that set the model choice: {{difficulty_or_risk}}.
30
+
31
+ Return to the orchestrator a single recommended verdict — one of pass, fail-retry
32
+ with a reason, or fail-block with a reason — with the evidence behind it. The
33
+ orchestrator records it.
34
+
35
+ Integration criterion: {{integration}} (how the orchestrator acts on the verdict,
36
+ e.g. it records pass and moves the change to in-validation).
@@ -0,0 +1,16 @@
1
+ {
2
+ "base": {
3
+ "core": { "target": { "lines": 125, "bytes": 7500 }, "hard": { "lines": 140, "bytes": 9000 } },
4
+ "spec": { "target": { "lines": 280, "bytes": 12000 }, "hard": { "lines": 310, "bytes": 13500 } },
5
+ "implement": { "target": { "lines": 185, "bytes": 8500 }, "hard": { "lines": 205, "bytes": 10000 } },
6
+ "review": { "target": { "lines": 70, "bytes": 3500 }, "hard": { "lines": 85, "bytes": 4500 } },
7
+ "release": { "target": { "lines": 45, "bytes": 2500 }, "hard": { "lines": 60, "bytes": 3500 } }
8
+ },
9
+ "agent": { "target": { "lines": 45, "bytes": 2000 }, "hard": { "lines": 60, "bytes": 3000 } }
10
+ ,"overlays": {
11
+ "blocked": { "target": { "lines": 70, "bytes": 3000 }, "hard": { "lines": 84, "bytes": 3600 } },
12
+ "in-validation": { "target": { "lines": 45, "bytes": 1700 }, "hard": { "lines": 54, "bytes": 2040 } },
13
+ "done": { "target": { "lines": 90, "bytes": 3500 }, "hard": { "lines": 108, "bytes": 4200 } },
14
+ "discarded": { "target": { "lines": 40, "bytes": 1300 }, "hard": { "lines": 48, "bytes": 1560 } }
15
+ }
16
+ }