cadet-agent 0.45.0 → 0.47.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/README.md CHANGED
@@ -9,7 +9,7 @@ Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished g
9
9
  - `cadet-agent.md` is the thin global directive: identity, non-negotiable rules, workflow routing, hard-gate protocol, and skill dispatch.
10
10
  - `Harness.md` is the canonical harness contract: budgets, evidence-backed gates, retries, context tiers, tool routing, privacy, and escalation.
11
11
  - `harness.schema.json` and `state.schema.json` are the machine-readable schemas for harness records and session state.
12
- - `skills/` contains scoped workflow-phase skills (PlanningReview, Requirements, Architecture, Spike, StoryBreakdown, TDD, Debugging, CodeReview, Resume, MCPSetup, AgentReviewer).
12
+ - `skills/` contains scoped workflow-phase skills (PlanningReview, Requirements, Architecture, Spike, StoryBreakdown, TDD, Debugging, CodeReview, Resume, MCPSetup, AgentReviewer, Handoff, Reconciliation).
13
13
  - `templates/` contains runtime templates for planning artifacts.
14
14
  - `.cadet/harness.json` holds repository-local budget/policy overrides (preserved by sync).
15
15
  - `.cadet/runs/` holds sanitized run ledgers (preserved by sync; no secrets or raw prompts by default).
@@ -28,7 +28,7 @@ Cadet-Agent is **not a one-shot code generator**. It won't spit out a finished g
28
28
 
29
29
  ## Cross-IDE Support
30
30
 
31
- Cadet-Agent provides full workflow parity across five IDEs. The same 10 skills + reviewer are available in each:
31
+ Cadet-Agent provides full workflow parity across five IDEs. The same 11 skills + reviewer are available in each:
32
32
 
33
33
  | Feature | GitHub Copilot | Cursor | Continue | Claude Code | Deep Code |
34
34
  |---|---|---|---|---|---|
@@ -44,6 +44,7 @@ Cadet-Agent provides full workflow parity across five IDEs. The same 10 skills +
44
44
  | Code Review | ✅ | ✅ | ✅ | ✅ | ✅ |
45
45
  | Resume | ✅ | ✅ | ✅ | ✅ | ✅ |
46
46
  | MCP Setup | ✅ | ✅ | ✅ | ✅ | ✅ |
47
+ | Reconciliation | ✅ | ✅ | ✅ | ✅ | ✅ |
47
48
  | Reviewer mode | Agent picker | Rule toggle | `/cadet-agent-reviewer` | `/cadet-agent-reviewer` | `cadet-agent-reviewer` skill |
48
49
  | Git guard | PreToolUse hook | Manual | Manual | Manual | `permissions.ask` (`mutate-git-log`) |
49
50
 
@@ -199,6 +200,7 @@ cadet-agent state seal # write the active work item's
199
200
  cadet-agent state transition --to review # enforce the matrix + evidence
200
201
  cadet-agent harness verify --gate testsPassed --files src/a.cs # bounded, classified loop
201
202
  cadet-agent harness report # budget consumption and failures (no secrets)
203
+ cadet-agent harness reconcile # reconcile the planning chain against state.json (read-only)
202
204
  cadet-agent harness cleanup --older-than-ms <n> # apply the retention policy (bound required)
203
205
  cadet-agent harness capabilities # available CLI/Unity/MCP/hook/token/cost telemetry
204
206
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadet-agent",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Cross-IDE agent framework for Unity/C# game-development — one-command install",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  workItemIdOf, loadPolicy, RunLedger, loadRun, listRuns, cleanupRuns, buildReport, formatReport,
8
8
  runVerificationLoop, commandForGate, detectCapabilities, runsDir, gitChangedFiles, PolicyError, StateError,
9
9
  detectRepoRole, describeRepoRole, GATES, manualConfirmation,
10
+ gitChangeSet, DEFAULT_REPORT_DIR,
11
+ reconcileArtifacts, PLANS_DEFAULT_DIR,
10
12
  parseTestInventory, parseStoryCriteria, compareCoverage, describeCoverageGaps,
11
13
  parseReachabilityDeclaration, validateReachabilityDeclaration, collectWorkItems,
12
14
  findDeferralCycles, readSiblingDeclarations, normalizeWorkItemRef, describeReachabilityGaps,
@@ -58,6 +60,8 @@ function showHelp() {
58
60
  cadet-agent harness verify-acs Verify declared AC↔test coverage against a test report
59
61
  cadet-agent harness verify-reachability Verify a story's declared reachability (opt-in)
60
62
  cadet-agent harness report Summarize budget consumption and failures
63
+ cadet-agent harness changes List the files a story changed, with status, counts, and links
64
+ cadet-agent harness reconcile Reconcile the planning chain against state.json (read-only)
61
65
  cadet-agent harness cleanup Apply the retention policy to .cadet/runs/
62
66
  cadet-agent harness capabilities Report available CLI/Unity/MCP/hook/token/cost telemetry
63
67
 
@@ -78,6 +82,10 @@ function showHelp() {
78
82
  --report Test report to derive the inventory from (harness verify-acs|matrix-check)
79
83
  --matrix TDD matrix markdown to check (harness matrix-check)
80
84
  --inventory Newline-separated test names, when no report is available (harness matrix-check)
85
+ --range Base revision to diff instead of the working tree (harness changes)
86
+ --relative-to Directory the emitted links are relative to (harness changes; default .cadet/reports)
87
+ --include-cadet Keep .cadet/ bookkeeping among the listed files (harness changes)
88
+ --plans-dir Directory holding the planning artifacts (harness reconcile; default .cadet/agent/project-plans)
81
89
  --agents-md keep|overwrite|merge for an existing AGENTS.md (init/sync)
82
90
  --older-than-ms Age bound, in ms, for records cleanup may delete (harness cleanup; required)
83
91
  --keep always|active|<work-item ids> for what stays in state.json (state compact; required)
@@ -164,6 +172,10 @@ function parseArgs(argv) {
164
172
  case '--commit': opts.commitGiven = true; opts.commit = value(a); break;
165
173
  case '--matrix': opts.matrix = value(a); break;
166
174
  case '--inventory': opts.inventory = value(a); break;
175
+ case '--range': opts.range = value(a); break;
176
+ case '--relative-to': opts.relativeTo = value(a); break;
177
+ case '--include-cadet': opts.includeCadet = true; break;
178
+ case '--plans-dir': opts.plansDir = value(a); break;
167
179
  case '--write-coverage': opts.writeCoverage = true; break;
168
180
  case '--strict-orphans': opts.strictOrphans = true; break;
169
181
  case '--dry-run': opts.dryRun = true; break;
@@ -1293,6 +1305,109 @@ async function cmdHarness(opts) {
1293
1305
  return;
1294
1306
  }
1295
1307
 
1308
+ // Read-only. Produces the deterministic half of a Change Report — which files
1309
+ // changed, how, and by how much — so the agent never assembles that table from
1310
+ // memory. The other half, why each file changed, is not knowable from git and
1311
+ // stays the agent's job. See .cadet/agent/core/skills/CodeReview.md.
1312
+ //
1313
+ // A missing git is NOT a usage error here. This command informs a review that
1314
+ // can still be completed, so it reports the limitation and exits 0 rather than
1315
+ // blocking the review; the report records it under Limits.
1316
+ if (sub === 'changes') {
1317
+ const relativeTo = opts.relativeTo || DEFAULT_REPORT_DIR;
1318
+ const changes = gitChangeSet(opts.targetDir, {
1319
+ range: opts.range || null,
1320
+ relativeTo,
1321
+ includeCadet: opts.includeCadet === true,
1322
+ });
1323
+
1324
+ const { exists, state } = readState(opts.targetDir);
1325
+ const item = exists ? state?.activeWorkItem ?? null : null;
1326
+ const workItem = item ? { epicId: item.epicId ?? null, storyId: item.storyId ?? null } : null;
1327
+
1328
+ const payload = {
1329
+ ok: true,
1330
+ available: changes.available,
1331
+ workItem,
1332
+ range: opts.range || 'working-tree',
1333
+ relativeTo,
1334
+ files: changes.files,
1335
+ counts: changes.counts,
1336
+ reason: changes.reason,
1337
+ };
1338
+ if (opts.format === 'json') {
1339
+ emit(opts, '', payload);
1340
+ return;
1341
+ }
1342
+
1343
+ if (!changes.available) {
1344
+ console.log(`\n⚠️ Change inventory unavailable: ${changes.reason}`);
1345
+ console.log(' Do not list files from memory — record this as a limit of the report.');
1346
+ return;
1347
+ }
1348
+
1349
+ const c = changes.counts;
1350
+ console.log(`\nChange inventory (${payload.range}) — ${changes.files.length} file(s)`);
1351
+ console.log(` ${c.added} added · ${c.modified} modified · ${c.renamed} renamed · ${c.deleted} deleted`);
1352
+ if (changes.files.length === 0) {
1353
+ console.log('\n (no changes)');
1354
+ } else {
1355
+ console.log('');
1356
+ for (const f of changes.files) {
1357
+ const lines = f.added === null && f.deleted === null ? 'new' : `+${f.added ?? 0}/-${f.deleted ?? 0}`;
1358
+ console.log(` ${f.status} ${lines.padEnd(10)} ${f.path}`);
1359
+ }
1360
+ }
1361
+ const label = workItem ? `${workItem.epicId || 'none'}::${workItem.storyId || 'none'}` : 'none';
1362
+ console.log(`\n Links relative to ${relativeTo} · work item: ${label}`);
1363
+ return;
1364
+ }
1365
+
1366
+ // Read-only. Reconciles the planning chain against state.json: the mechanical
1367
+ // half of the Reconciliation skill. It reports the inconsistencies it can prove
1368
+ // from the artifacts and never repairs one — the skill proposes repairs for the
1369
+ // user to approve. See .cadet/agent/core/skills/Reconciliation.md.
1370
+ //
1371
+ // Exit 0 whatever the verdict: the verdict is the payload, and a caller reading
1372
+ // `--format json` must not have to tolerate a failure exit to get it. A run with
1373
+ // no planning artifacts at all is a legitimate state (a framework-source repo, a
1374
+ // small change), not an error.
1375
+ if (sub === 'reconcile') {
1376
+ const { exists, state } = readState(opts.targetDir);
1377
+ const result = reconcileArtifacts(opts.targetDir, {
1378
+ state: exists ? state : null,
1379
+ plansDir: opts.plansDir || PLANS_DEFAULT_DIR,
1380
+ story: opts.story || null,
1381
+ });
1382
+
1383
+ if (opts.format === 'json') {
1384
+ emit(opts, '', result);
1385
+ return;
1386
+ }
1387
+ if (!result.available) {
1388
+ console.log(`\nℹ️ Nothing to reconcile: ${result.reason}`);
1389
+ return;
1390
+ }
1391
+
1392
+ const s = result.summary;
1393
+ console.log(`\nReconcile ${result.plansDir}${result.scopedEpic ? ` (${result.scopedEpic})` : ''} — verdict: ${result.verdict}`);
1394
+ console.log(` ${result.artifacts.epicCount} epic(s), ${result.artifacts.storyCount} story file(s)`);
1395
+ console.log(` ${s.total} finding(s): ${s.blocking} blocking · ${s.warning} warning · ${s.info} info`);
1396
+ if (s.total === 0) {
1397
+ console.log('\n ✅ The chain is internally consistent.');
1398
+ } else {
1399
+ console.log('');
1400
+ for (const f of result.findings) {
1401
+ console.log(` [${f.severity}] ${f.id} ${f.code} — ${f.subject}`);
1402
+ console.log(` ${f.detail}${f.evidence ? ` (${f.evidence})` : ''}`);
1403
+ }
1404
+ }
1405
+ if (result.verdict === 'unknown') {
1406
+ console.log('\n ⚠️ At least one artifact could not be read, so consistency cannot be certified.');
1407
+ }
1408
+ return;
1409
+ }
1410
+
1296
1411
  // AR-5. Reconcile a TDD matrix's DELIVERED test-name claims against a compiled
1297
1412
  // inventory. Read-only: it reports, and never writes state, so it can be run at
1298
1413
  // authoring time (before anything has been implemented) as well as in a gate.
@@ -1390,7 +1505,7 @@ async function cmdHarness(opts) {
1390
1505
  return;
1391
1506
  }
1392
1507
 
1393
- fail(opts, `Unknown harness subcommand: ${sub || '(none)'}. Use record|confirm|verify|verify-acs|verify-reachability|matrix-check|report|cleanup|capabilities.`);
1508
+ fail(opts, `Unknown harness subcommand: ${sub || '(none)'}. Use record|confirm|verify|verify-acs|verify-reachability|matrix-check|report|changes|reconcile|cleanup|capabilities.`);
1394
1509
  }
1395
1510
 
1396
1511
  export async function run(argv) {
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Change inventory — what a story actually touched, and how much.
3
+ *
4
+ * Why this exists: the Change Report's file table has to be *the same table*
5
+ * every run. When the agent assembled that list by hand — a `git status` here, a
6
+ * remembered path there — the result varied by run: a file dropped, a status
7
+ * guessed, a link that did not resolve, a line count invented. The rows are the
8
+ * part of the report that is mechanically knowable, so they are computed here
9
+ * and the agent supplies only the prose that is not.
10
+ *
11
+ * This is a read-only probe. It runs `git` and parses output; it never writes.
12
+ *
13
+ * Availability contract: mirrors `gitChangedFiles` (`util.mjs`). Returns
14
+ * `{ available: false, reason }` when git cannot be asked — "not a repository"
15
+ * and "no changes" must not look alike, so callers state the limitation rather
16
+ * than reporting an empty change set as a clean one.
17
+ */
18
+
19
+ import { spawnSync } from 'node:child_process';
20
+ import { isAbsolute, join, relative } from 'node:path';
21
+
22
+ /** Default report directory, from which file links are made relative. */
23
+ export const DEFAULT_REPORT_DIR = '.cadet/reports';
24
+
25
+ /**
26
+ * Cadet's own bookkeeping. A story's change report should not lead with the
27
+ * ledger and state files its own gate checks rewrote. Same set and same reason
28
+ * as `util.mjs#CADET_MACHINERY`.
29
+ */
30
+ const CADET_MACHINERY = ['.cadet/state.json', '.cadet/runs/', '.cadet/archive/'];
31
+
32
+ function isCadetMachinery(relPath) {
33
+ return CADET_MACHINERY.some((p) => (p.endsWith('/') ? relPath.startsWith(p) : relPath === p));
34
+ }
35
+
36
+ /**
37
+ * Collapse porcelain's two status columns (`XY`) to the single letter the
38
+ * report shows. The staged column wins when it says something, because that is
39
+ * the status the eventual commit will carry; the worktree column is the
40
+ * fallback. `??` is an untracked file, which a reader reads as "added".
41
+ */
42
+ const STATUS_LETTERS = { R: 'R', C: 'R', A: 'A', D: 'D', M: 'M', T: 'M' };
43
+
44
+ function statusFromPorcelain(xy) {
45
+ if (xy === '??') return 'A';
46
+ const [staged, unstaged] = xy;
47
+ return STATUS_LETTERS[staged] || STATUS_LETTERS[unstaged] || 'M';
48
+ }
49
+
50
+ /**
51
+ * Normalize a `--numstat` path field, which spells renames three ways:
52
+ * `new`, `old => new`, and `dir/{old => new}/file`. Only the new path is kept,
53
+ * matching the name-status side, so the two maps join on the same key.
54
+ */
55
+ function normalizeNumstatPath(raw) {
56
+ const path = String(raw || '').trim();
57
+ if (!path.includes(' => ')) return path;
58
+ const braced = path.match(/^(.*)\{(.*) => (.*)\}(.*)$/);
59
+ if (braced) return `${braced[1]}${braced[3]}${braced[4]}`.replace(/\/{2,}/g, '/');
60
+ return path.split(' => ').pop().trim();
61
+ }
62
+
63
+ function defaultGitRunner(cmd, args) {
64
+ try {
65
+ return spawnSync(cmd, args, { encoding: 'utf-8', windowsHide: true });
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Run one git command and return its stdout, or `{ error }` describing why the
73
+ * question could not be asked. Every call goes through here so a single probe
74
+ * failure is reported the same way regardless of which command failed.
75
+ */
76
+ function runGit(cwd, args, runner) {
77
+ let res;
78
+ try {
79
+ res = runner('git', ['-C', cwd, ...args]);
80
+ } catch (err) {
81
+ return { error: `git invocation failed: ${err.message}` };
82
+ }
83
+ if (!res) return { error: 'git is not available' };
84
+ if (res.error || res.status === null) {
85
+ return { error: 'git is not installed or could not be executed' };
86
+ }
87
+ if (res.status !== 0) {
88
+ return { error: String(res.stderr || '').trim() || `git exited ${res.status}` };
89
+ }
90
+ return { stdout: String(res.stdout || '') };
91
+ }
92
+
93
+ /**
94
+ * Files and statuses for the change set. Untracked files appear only in the
95
+ * working-tree form; a `--range` diff is a commit-to-commit question and cannot
96
+ * see them.
97
+ */
98
+ function readNameStatus(cwd, { runner, range }) {
99
+ if (range) {
100
+ const res = runGit(cwd, ['diff', '--name-status', range, '--'], runner);
101
+ if (res.error) return { error: res.error };
102
+ const files = [];
103
+ for (const line of res.stdout.split(/\r?\n/)) {
104
+ if (!line.trim()) continue;
105
+ // "M\tpath" — or "R100\told\tnew", where the new path is the one that exists.
106
+ const fields = line.split('\t');
107
+ if (fields.length < 2) continue;
108
+ const path = fields[fields.length - 1].trim();
109
+ if (!path) continue;
110
+ files.push({ path: path.replace(/\\/g, '/'), status: fields[0][0] });
111
+ }
112
+ return { files };
113
+ }
114
+
115
+ const res = runGit(cwd, ['status', '--porcelain', '--untracked-files=all'], runner);
116
+ if (res.error) return { error: res.error };
117
+ const files = [];
118
+ for (const line of res.stdout.split(/\r?\n/)) {
119
+ if (!line.trim()) continue;
120
+ // Porcelain v1: XY<space>path, with renames written "old -> new".
121
+ const xy = line.slice(0, 2);
122
+ let path = line.slice(3).trim();
123
+ if (path.includes(' -> ')) path = path.split(' -> ').pop().trim();
124
+ path = path.replace(/^"|"$/g, '');
125
+ if (!path) continue;
126
+ files.push({ path: path.replace(/\\/g, '/'), status: statusFromPorcelain(xy) });
127
+ }
128
+ return { files };
129
+ }
130
+
131
+ /**
132
+ * Accumulate one count onto a prior one. A file can appear in both the staged
133
+ * and the unstaged numstat, and those counts add. `null` means git reported no
134
+ * number (a binary file); it stays unknown unless a real number was seen.
135
+ */
136
+ function addCount(prior, value) {
137
+ if (value === null) return prior === undefined ? null : prior;
138
+ return (prior ?? 0) + value;
139
+ }
140
+
141
+ /** Added/deleted counts per path, merged across the staged and unstaged diffs. */
142
+ function mergeNumstat(target, stdout) {
143
+ for (const line of String(stdout || '').split(/\r?\n/)) {
144
+ if (!line.trim()) continue;
145
+ const [added, deleted, ...rest] = line.split('\t');
146
+ if (rest.length === 0) continue;
147
+ const path = normalizeNumstatPath(rest.join('\t'));
148
+ if (!path) continue;
149
+ // A binary file reports "-" for both; that is genuinely unknown, not zero.
150
+ const a = /^\d+$/.test(added) ? Number(added) : null;
151
+ const d = /^\d+$/.test(deleted) ? Number(deleted) : null;
152
+ const prior = target.get(path);
153
+ target.set(path, {
154
+ added: addCount(prior?.added, a),
155
+ deleted: addCount(prior?.deleted, d),
156
+ });
157
+ }
158
+ }
159
+
160
+ function readNumstat(cwd, { runner, range }) {
161
+ const counts = new Map();
162
+ if (range) {
163
+ const res = runGit(cwd, ['diff', '--numstat', range, '--'], runner);
164
+ if (res.error) return { error: res.error };
165
+ mergeNumstat(counts, res.stdout);
166
+ return { counts };
167
+ }
168
+ const unstaged = runGit(cwd, ['diff', '--numstat', '--'], runner);
169
+ if (unstaged.error) return { error: unstaged.error };
170
+ mergeNumstat(counts, unstaged.stdout);
171
+ const staged = runGit(cwd, ['diff', '--cached', '--numstat', '--'], runner);
172
+ if (staged.error) return { error: staged.error };
173
+ mergeNumstat(counts, staged.stdout);
174
+ return { counts };
175
+ }
176
+
177
+ /**
178
+ * The change inventory for a directory.
179
+ *
180
+ * @param {string} cwd Repository root to ask about.
181
+ * @param {object} [options]
182
+ * @param {Function} [options.runner] Injectable git runner, for tests.
183
+ * @param {string|null} [options.range] Diff a base revision instead of the working tree.
184
+ * @param {string} [options.relativeTo] Directory the `link` fields are made relative to.
185
+ * @param {boolean} [options.includeCadet] Keep `.cadet/` bookkeeping in the list.
186
+ * @returns {{available: boolean, files: Array, counts: object, reason: string|null}}
187
+ */
188
+ export function gitChangeSet(cwd, {
189
+ runner = defaultGitRunner,
190
+ range = null,
191
+ relativeTo = DEFAULT_REPORT_DIR,
192
+ includeCadet = false,
193
+ } = {}) {
194
+ const empty = { added: 0, modified: 0, deleted: 0, renamed: 0 };
195
+
196
+ const named = readNameStatus(cwd, { runner, range });
197
+ if (named.error) return { available: false, files: [], counts: empty, reason: named.error };
198
+
199
+ const counted = readNumstat(cwd, { runner, range });
200
+ if (counted.error) return { available: false, files: [], counts: empty, reason: counted.error };
201
+
202
+ // Links are relative to the report, not the repository root: a report at
203
+ // `.cadet/reports/x.md` must point at `../../Assets/Foo.cs` or the click does
204
+ // nothing. Computing it here is the whole reason the link is not hand-written.
205
+ const baseDir = isAbsolute(relativeTo) ? relativeTo : join(cwd, relativeTo);
206
+
207
+ const seen = new Map();
208
+ for (const file of named.files) {
209
+ if (!includeCadet && isCadetMachinery(file.path)) continue;
210
+ if (seen.has(file.path)) continue;
211
+ const count = counted.counts.get(file.path);
212
+ const linkTarget = relative(baseDir, join(cwd, file.path)).replace(/\\/g, '/');
213
+ seen.set(file.path, {
214
+ path: file.path,
215
+ status: file.status,
216
+ // Untracked files have no diff, so no count exists. Null, not zero: a
217
+ // zero would read as "changed nothing", which is a different claim.
218
+ added: count?.added ?? null,
219
+ deleted: count?.deleted ?? null,
220
+ link: `[${file.path}](${linkTarget})`,
221
+ });
222
+ }
223
+
224
+ const files = [...seen.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
225
+ const counts = { ...empty };
226
+ for (const f of files) {
227
+ if (f.status === 'A') counts.added += 1;
228
+ else if (f.status === 'D') counts.deleted += 1;
229
+ else if (f.status === 'R') counts.renamed += 1;
230
+ else counts.modified += 1;
231
+ }
232
+
233
+ return { available: true, files, counts, reason: null };
234
+ }
@@ -123,6 +123,20 @@ export const COMMANDS = {
123
123
  mutates: false,
124
124
  summary: 'Summarize budget consumption and failures.',
125
125
  },
126
+ 'harness changes': {
127
+ mutates: false,
128
+ summary: 'List the files a story changed, with status, line counts, and links.',
129
+ // Read-only by construction: it runs `git status`/`git diff` and parses the
130
+ // output. It does not author the Change Report — the agent does, from the
131
+ // template — so there is no artifact for it to write and nothing to dry-run.
132
+ },
133
+ 'harness reconcile': {
134
+ mutates: false,
135
+ summary: 'Reconcile the planning chain (requirements, design, plan, epics, stories) against state.json.',
136
+ // Read-only, and deliberately so: it reports the provable inconsistencies and
137
+ // never repairs one. An agent that could reconcile artifacts unattended could
138
+ // rewrite the design it is meant to be checking against.
139
+ },
126
140
  'harness matrix-check': {
127
141
  mutates: false,
128
142
  summary: 'Reconcile a TDD matrix against a compiled test inventory.',
@@ -23,6 +23,13 @@ export {
23
23
  newId, isUuid, sha256, sha256Bytes, hashFile, hashTree, hashCriteria, timestamp, canonicalJson, changedFiles, gitChangedFiles,
24
24
  } from './util.mjs';
25
25
 
26
+ export { DEFAULT_REPORT_DIR, gitChangeSet } from './changes.mjs';
27
+
28
+ export {
29
+ PLANS_DEFAULT_DIR, REQUIRED_ARTIFACTS, RECONCILE_SEVERITIES, RECONCILE_VERDICTS,
30
+ DEFAULT_MAX_DOC_BYTES, parseStoryHeader, parseEpicHeader, collectArtifacts, reconcileArtifacts,
31
+ } from './reconcile.mjs';
32
+
26
33
  export {
27
34
  STATE_VERSION, READABLE_STATE_VERSIONS, HISTORY_EXTERNAL_SINCE, isHistoryExternal,
28
35
  validateState, migrateStateV1toV2, migrateStateDocument, migrateStateFile, parseTargetVersion,