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.
- package/AGENTS.md +11 -3
- package/README.md +10 -1
- package/bin/changeledger.mjs +204 -21
- package/package.json +1 -1
- package/src/check.mjs +12 -0
- package/src/commands/agent-context.mjs +65 -0
- package/src/commands/agent-prompt.mjs +22 -0
- package/src/commands/agent.mjs +13 -10
- package/src/commands/check.mjs +55 -0
- package/src/commands/commit.mjs +39 -0
- package/src/commands/context.mjs +60 -27
- 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 +73 -15
- package/src/fix.mjs +127 -0
- package/src/framing.mjs +30 -0
- package/src/git.mjs +119 -2
- package/src/lifecycle.mjs +2 -2
- package/src/metrics.mjs +42 -0
- package/src/search.mjs +162 -0
- package/src/viewer/domain.mjs +2 -2
- package/src/viewer/public/app-state.js +62 -6
- package/src/viewer/public/app.js +138 -39
- package/src/viewer/public/index.html +2 -2
- package/src/viewer/public/state.js +6 -2
- 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-contexts/implementation.md +18 -0
- package/templates/contract/agent-contexts/investigation.md +14 -0
- package/templates/contract/agent-contexts/review.md +22 -0
- package/templates/contract/agent-prompts/audit.md +37 -0
- package/templates/contract/agent-prompts/implementation.md +42 -0
- package/templates/contract/agent-prompts/investigation.md +41 -0
- package/templates/contract/agent-prompts/review.md +36 -0
- package/templates/contract/budgets.yml +16 -0
- package/templates/contract/close.md +19 -14
- package/templates/contract/core.md +41 -30
- package/templates/contract/delegation.md +2 -1
- package/templates/contract/implement.md +24 -10
- package/templates/contract/review.md +7 -9
- package/templates/contract/spec.md +14 -1
- package/templates/contract/validation.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
This is the ChangeLedger repo itself. It dogfoods its own format: changes live
|
|
4
4
|
under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
5
5
|
|
|
6
|
-
<!--
|
|
6
|
+
<!-- CHANGELEDGER BOOTSTRAP BEGIN v2 -->
|
|
7
7
|
> [!IMPORTANT]
|
|
8
8
|
> This repo uses **ChangeLedger**. Immediately after reading this file — before
|
|
9
|
-
> planning, investigating, or acting — run `changeledger context` directly.
|
|
10
|
-
>
|
|
9
|
+
> planning, investigating, or acting — a normal agent must run `changeledger context` directly.
|
|
10
|
+
> Only a delegated leaf whose prompt was emitted by `changeledger agent-prompt <role>`
|
|
11
|
+
> runs `changeledger agent-context <role> [change-id]` instead; the role in the
|
|
12
|
+
> prompt and command must match. No other agent may skip the core context.
|
|
13
|
+
> On the first invocation, retain complete stdout through the `CHANGELEDGER CONTEXT END` line,
|
|
14
|
+
> or the `CHANGELEDGER AGENT CONTEXT END` line for that delegated path:
|
|
11
15
|
> no pipes, filters, summaries, previews or voluntary output limits. If the tool
|
|
12
16
|
> exposes an output budget, reserve enough for the whole response. A missing END
|
|
13
17
|
> after that is exceptional recovery: stop and re-run with a larger capture. If
|
|
@@ -16,6 +20,10 @@ under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
|
16
20
|
>
|
|
17
21
|
> Do not create or modify files without an authorized change; the core context
|
|
18
22
|
> defines the workflow, the task contexts, and the narrow operational exception.
|
|
23
|
+
> After a compaction, verify a retained capture with `changeledger context
|
|
24
|
+
> [mode] --have <rev>` (the BEGIN line's `rev:`) instead of recapturing in
|
|
25
|
+
> full; a mismatch still returns the complete output.
|
|
26
|
+
<!-- CHANGELEDGER BOOTSTRAP END -->
|
|
19
27
|
|
|
20
28
|
The canonical ChangeLedger contract is split into task-focused fragments under
|
|
21
29
|
[`templates/contract/`](templates/contract/). The deterministic
|
package/README.md
CHANGED
|
@@ -137,11 +137,20 @@ The contract ships as task-focused fragments and is compiled on demand:
|
|
|
137
137
|
changeledger context # minimal non-negotiable core
|
|
138
138
|
changeledger context <change-id> # lifecycle-aware rules + selected change
|
|
139
139
|
changeledger context review # explicit task mode
|
|
140
|
+
changeledger agent-prompt <role> # portable delegation skeleton
|
|
141
|
+
changeledger agent-context <role> [id] # self-contained context for that delegate
|
|
140
142
|
```
|
|
141
143
|
|
|
142
144
|
`init` places a small fail-closed bootstrap in the project-owned `AGENTS.md`;
|
|
143
145
|
there is no linked or copied contract under `.changeledger/`. Run
|
|
144
|
-
`changeledger register` after upgrading to refresh that bootstrap.
|
|
146
|
+
`changeledger register` after upgrading to refresh that bootstrap. Normal agents
|
|
147
|
+
load `context`; a delegated leaf identified by `agent-prompt` loads only its
|
|
148
|
+
matching `agent-context`, without the orchestrator core.
|
|
149
|
+
|
|
150
|
+
Roles are `investigation`, `implementation`, `review` and `audit`. `audit` is a
|
|
151
|
+
read-only inspection of a change already in `in-validation` — after review has
|
|
152
|
+
already passed — for a human or orchestrator to consult before accepting or
|
|
153
|
+
rejecting it; it never moves the change or records a verdict.
|
|
145
154
|
|
|
146
155
|
### Upgrading an existing repo's configuration
|
|
147
156
|
|
package/bin/changeledger.mjs
CHANGED
|
@@ -8,13 +8,19 @@ import {
|
|
|
8
8
|
list,
|
|
9
9
|
log,
|
|
10
10
|
owner,
|
|
11
|
+
reopen,
|
|
11
12
|
review,
|
|
12
13
|
show,
|
|
13
14
|
status,
|
|
14
15
|
task,
|
|
16
|
+
validation,
|
|
15
17
|
} from '../src/commands/agent.mjs';
|
|
18
|
+
import { agentContext } from '../src/commands/agent-context.mjs';
|
|
19
|
+
import { agentPrompt } from '../src/commands/agent-prompt.mjs';
|
|
16
20
|
import { check } from '../src/commands/check.mjs';
|
|
21
|
+
import { commit } from '../src/commands/commit.mjs';
|
|
17
22
|
import { context } from '../src/commands/context.mjs';
|
|
23
|
+
import { fix } from '../src/commands/fix.mjs';
|
|
18
24
|
import {
|
|
19
25
|
graduate,
|
|
20
26
|
pendingGraduation,
|
|
@@ -25,6 +31,7 @@ import { init } from '../src/commands/init.mjs';
|
|
|
25
31
|
import { newChange } from '../src/commands/new.mjs';
|
|
26
32
|
import { registerRepo } from '../src/commands/register.mjs';
|
|
27
33
|
import { initReleaseHistory, recordRelease, releasePlan } from '../src/commands/release.mjs';
|
|
34
|
+
import { runSearch } from '../src/commands/search.mjs';
|
|
28
35
|
import { view } from '../src/commands/view.mjs';
|
|
29
36
|
import { findChangeledgerDir } from '../src/config.mjs';
|
|
30
37
|
import { applyMigration } from '../src/config-migration.mjs';
|
|
@@ -34,11 +41,12 @@ const { version } = createRequire(import.meta.url)('../package.json');
|
|
|
34
41
|
|
|
35
42
|
const USAGE = `ChangeLedger (changeledger)
|
|
36
43
|
|
|
37
|
-
Run \`changeledger context\` first in any repo
|
|
44
|
+
Run \`changeledger context\` first in any repo unless a ChangeLedger delegation
|
|
45
|
+
prompt identifies your role and tells you to run \`agent-context\` instead.
|
|
38
46
|
|
|
39
|
-
changeledger init | register | new | view | check | context
|
|
40
|
-
changeledger status | discard | review | owner | archive
|
|
41
|
-
changeledger log | task | list | show | graduate | config | release
|
|
47
|
+
changeledger init | register | new | view | check | fix | context | agent-context
|
|
48
|
+
changeledger commit | status | discard | review | owner | archive
|
|
49
|
+
changeledger log | task | list | show | search | graduate | config | release
|
|
42
50
|
|
|
43
51
|
Run \`changeledger <command> --help\` for that command's syntax, values and examples.`;
|
|
44
52
|
|
|
@@ -55,6 +63,11 @@ function action(fn) {
|
|
|
55
63
|
};
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
// Collects a repeatable option (e.g. `--id`) into an array across invocations.
|
|
67
|
+
function collect(value, previous) {
|
|
68
|
+
return previous.concat([value]);
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
program
|
|
59
72
|
.name('changeledger')
|
|
60
73
|
.description('ChangeLedger (changeledger)')
|
|
@@ -62,7 +75,7 @@ program
|
|
|
62
75
|
.helpOption('-h, --help', 'display help for command')
|
|
63
76
|
.addHelpText(
|
|
64
77
|
'after',
|
|
65
|
-
'\nRun `changeledger context` first
|
|
78
|
+
'\nRun `changeledger context` first unless a ChangeLedger delegation prompt tells your role to use `agent-context`.\n' +
|
|
66
79
|
"Run `changeledger <command> --help` for that command's syntax, values and examples.",
|
|
67
80
|
);
|
|
68
81
|
|
|
@@ -131,9 +144,25 @@ program
|
|
|
131
144
|
.description('validate the repo or one change')
|
|
132
145
|
.argument('[id]')
|
|
133
146
|
.option('--json', 'print JSON')
|
|
147
|
+
.option(
|
|
148
|
+
'--commits [base]',
|
|
149
|
+
'lint commit subjects on <base>..HEAD for the [#id] marker (base auto-detected if omitted)',
|
|
150
|
+
)
|
|
151
|
+
.addHelpText(
|
|
152
|
+
'after',
|
|
153
|
+
['', 'Examples:', ' changeledger check --commits', ' changeledger check --commits main'].join(
|
|
154
|
+
'\n',
|
|
155
|
+
),
|
|
156
|
+
)
|
|
134
157
|
.action((id, options) => {
|
|
135
158
|
try {
|
|
136
|
-
const args = [
|
|
159
|
+
const args = [
|
|
160
|
+
...(id ? [id] : []),
|
|
161
|
+
...(options.json ? ['--json'] : []),
|
|
162
|
+
...(options.commits !== undefined
|
|
163
|
+
? ['--commits', ...(typeof options.commits === 'string' ? [options.commits] : [])]
|
|
164
|
+
: []),
|
|
165
|
+
];
|
|
137
166
|
process.exit(check(args));
|
|
138
167
|
} catch (e) {
|
|
139
168
|
console.error(`Error: ${e.message}`);
|
|
@@ -141,6 +170,52 @@ program
|
|
|
141
170
|
}
|
|
142
171
|
});
|
|
143
172
|
|
|
173
|
+
program
|
|
174
|
+
.command('fix')
|
|
175
|
+
.description('repair mechanical, unambiguous format defects (or one change)')
|
|
176
|
+
.argument('[id]')
|
|
177
|
+
.option('--dry-run', 'print the proposed diff without writing')
|
|
178
|
+
.action((id, options) => {
|
|
179
|
+
try {
|
|
180
|
+
const args = [...(id ? [id] : []), ...(options.dryRun ? ['--dry-run'] : [])];
|
|
181
|
+
process.exit(fix(args));
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error(`Error: ${e.message}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command('commit')
|
|
190
|
+
.description('compose the canonical [#id] marker and create a git commit')
|
|
191
|
+
.requiredOption('-m, --message <subject>', 'conventional subject: type(scope): description')
|
|
192
|
+
.option(
|
|
193
|
+
'--id <change-id>',
|
|
194
|
+
'change id to reference (repeatable); auto-resolved from the single in-progress change if omitted',
|
|
195
|
+
collect,
|
|
196
|
+
[],
|
|
197
|
+
)
|
|
198
|
+
.addHelpText(
|
|
199
|
+
'after',
|
|
200
|
+
[
|
|
201
|
+
'',
|
|
202
|
+
'When --id is omitted, the single in-progress change is used automatically;',
|
|
203
|
+
'zero or multiple in-progress changes require --id explicitly. Repeat --id',
|
|
204
|
+
'for a multi-id subject: each id gets its own bracket ([#A] [#B]).',
|
|
205
|
+
'',
|
|
206
|
+
'Examples:',
|
|
207
|
+
' changeledger commit -m "feat(cli): add helper"',
|
|
208
|
+
' changeledger commit -m "feat(cli): add helper" --id 20260711-000001',
|
|
209
|
+
' changeledger commit -m "feat(cli): add helper" --id 20260711-000001 --id 20260711-000002',
|
|
210
|
+
].join('\n'),
|
|
211
|
+
)
|
|
212
|
+
.action(
|
|
213
|
+
action((options) => {
|
|
214
|
+
const subject = commit({ message: options.message, ids: options.id });
|
|
215
|
+
console.log(`Committed: ${subject}`);
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
144
219
|
program
|
|
145
220
|
.command('context')
|
|
146
221
|
.description('print deterministic task context')
|
|
@@ -148,6 +223,10 @@ program
|
|
|
148
223
|
'[mode-or-change-id]',
|
|
149
224
|
'spec|implement|review|release, or a change id (pack inferred from its status)',
|
|
150
225
|
)
|
|
226
|
+
.option(
|
|
227
|
+
'--have <rev>',
|
|
228
|
+
'skip the full reload when this matches the current rev (short `unchanged` confirmation instead)',
|
|
229
|
+
)
|
|
151
230
|
.addHelpText(
|
|
152
231
|
'after',
|
|
153
232
|
[
|
|
@@ -168,6 +247,10 @@ program
|
|
|
168
247
|
'are inferred the same way from the change id; they are not modes you pass',
|
|
169
248
|
'explicitly.',
|
|
170
249
|
'',
|
|
250
|
+
'Each BEGIN line carries `rev:<hash>`. After a compaction, pass the rev your',
|
|
251
|
+
'retained capture carried as `--have <rev>`: a match prints a short confirmation',
|
|
252
|
+
'instead of reloading the full body; a mismatch prints the complete output.',
|
|
253
|
+
'',
|
|
171
254
|
'Examples:',
|
|
172
255
|
' changeledger context',
|
|
173
256
|
' changeledger context spec',
|
|
@@ -175,9 +258,62 @@ program
|
|
|
175
258
|
' changeledger context review',
|
|
176
259
|
' changeledger context release',
|
|
177
260
|
' changeledger context 20260630-225212',
|
|
261
|
+
' changeledger context --have 0123456789ab',
|
|
262
|
+
].join('\n'),
|
|
263
|
+
)
|
|
264
|
+
.action(action((input, options) => context(input, { have: options.have })));
|
|
265
|
+
|
|
266
|
+
program
|
|
267
|
+
.command('agent-prompt')
|
|
268
|
+
.description('print a portable delegation prompt skeleton for a role')
|
|
269
|
+
.argument('<role>', 'investigation | implementation | review | audit')
|
|
270
|
+
.addHelpText(
|
|
271
|
+
'after',
|
|
272
|
+
[
|
|
273
|
+
'',
|
|
274
|
+
'Prints a fill-in-the-blanks delegation prompt for the given role. Works',
|
|
275
|
+
'outside a ChangeLedger repo — the skeletons ship inside the package.',
|
|
276
|
+
'',
|
|
277
|
+
'`audit` is a read-only inspection of a change already in `in-validation`,',
|
|
278
|
+
'after review already passed; it never issues a verdict or moves the change.',
|
|
279
|
+
'',
|
|
280
|
+
'Examples:',
|
|
281
|
+
' changeledger agent-prompt investigation',
|
|
282
|
+
' changeledger agent-prompt implementation',
|
|
283
|
+
' changeledger agent-prompt review',
|
|
284
|
+
' changeledger agent-prompt audit',
|
|
285
|
+
].join('\n'),
|
|
286
|
+
)
|
|
287
|
+
.action(action((role) => agentPrompt(role)));
|
|
288
|
+
|
|
289
|
+
program
|
|
290
|
+
.command('agent-context')
|
|
291
|
+
.description('print a self-contained minimal context for a delegated role')
|
|
292
|
+
.argument('<role>', 'investigation | implementation | review | audit')
|
|
293
|
+
.argument(
|
|
294
|
+
'[change-id]',
|
|
295
|
+
'optional for investigation; required for implementation, review and audit',
|
|
296
|
+
)
|
|
297
|
+
.addHelpText(
|
|
298
|
+
'after',
|
|
299
|
+
[
|
|
300
|
+
'',
|
|
301
|
+
'Use only when a delegation prompt emitted by `changeledger agent-prompt`',
|
|
302
|
+
'identifies you as that role. This replaces the normal core bootstrap for',
|
|
303
|
+
'the delegated leaf; normal agents still run `changeledger context` first.',
|
|
304
|
+
'',
|
|
305
|
+
'`audit` requires a change in `in-validation`; it is read-only inspection',
|
|
306
|
+
'after review already passed, and never issues a verdict or moves the change.',
|
|
307
|
+
'',
|
|
308
|
+
'Examples:',
|
|
309
|
+
' changeledger agent-context investigation',
|
|
310
|
+
' changeledger agent-context investigation <id>',
|
|
311
|
+
' changeledger agent-context implementation <id>',
|
|
312
|
+
' changeledger agent-context review <id>',
|
|
313
|
+
' changeledger agent-context audit <id>',
|
|
178
314
|
].join('\n'),
|
|
179
315
|
)
|
|
180
|
-
.action(action((
|
|
316
|
+
.action(action((role, changeId) => agentContext(role, changeId)));
|
|
181
317
|
|
|
182
318
|
program
|
|
183
319
|
.command('status')
|
|
@@ -192,7 +328,7 @@ program
|
|
|
192
328
|
[
|
|
193
329
|
'',
|
|
194
330
|
'Terminal moves are not accepted here: use `changeledger discard <id> "<reason>"`',
|
|
195
|
-
'to discard
|
|
331
|
+
'to discard. Only human validation in the viewer can reach done.',
|
|
196
332
|
'',
|
|
197
333
|
'Examples:',
|
|
198
334
|
' changeledger status <id> in-progress',
|
|
@@ -201,11 +337,41 @@ program
|
|
|
201
337
|
)
|
|
202
338
|
.action(
|
|
203
339
|
action((id, st) => {
|
|
204
|
-
status(id, st);
|
|
340
|
+
status(id, st, process.cwd(), { actor: 'agent' });
|
|
205
341
|
console.log(`#${id} → ${st}`);
|
|
206
342
|
}),
|
|
207
343
|
);
|
|
208
344
|
|
|
345
|
+
program
|
|
346
|
+
.command('validation')
|
|
347
|
+
.description('reject an in-validation change; accepting it remains human-only')
|
|
348
|
+
.argument('<id>')
|
|
349
|
+
.argument('<verdict>', 'fail')
|
|
350
|
+
.argument('<reason...>')
|
|
351
|
+
.action(
|
|
352
|
+
action((id, verdict, reasonParts) => {
|
|
353
|
+
if (verdict !== 'fail')
|
|
354
|
+
throw new Error(
|
|
355
|
+
'validation only accepts fail; human validation in the viewer accepts changes',
|
|
356
|
+
);
|
|
357
|
+
const reason = reasonParts.join(' ').trim();
|
|
358
|
+
validation(id, 'fail', { reason, actor: 'agent' });
|
|
359
|
+
console.log(`#${id} validation fail`);
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
program
|
|
364
|
+
.command('reopen')
|
|
365
|
+
.description('reopen a provisional done change with a reason')
|
|
366
|
+
.argument('<id>')
|
|
367
|
+
.argument('<reason...>')
|
|
368
|
+
.action(
|
|
369
|
+
action((id, reasonParts) => {
|
|
370
|
+
reopen(id, reasonParts.join(' ').trim(), process.cwd(), { actor: 'agent' });
|
|
371
|
+
console.log(`#${id} → in-progress`);
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
|
|
209
375
|
program
|
|
210
376
|
.command('discard')
|
|
211
377
|
.description('discard a change (terminal; keeps the record and reason)')
|
|
@@ -280,6 +446,8 @@ program
|
|
|
280
446
|
' changeledger archive <id>',
|
|
281
447
|
' changeledger archive --graduated',
|
|
282
448
|
' changeledger archive --graduated --dry-run',
|
|
449
|
+
'',
|
|
450
|
+
'To reverse an archive, edit `archived: false` in the change frontmatter directly.',
|
|
283
451
|
].join('\n'),
|
|
284
452
|
)
|
|
285
453
|
.action(
|
|
@@ -295,22 +463,11 @@ program
|
|
|
295
463
|
}
|
|
296
464
|
if (options.dryRun) throw new Error('--dry-run requires --graduated');
|
|
297
465
|
if (!id) throw new Error('archive requires <id> or --graduated');
|
|
298
|
-
archive(id
|
|
466
|
+
archive(id);
|
|
299
467
|
console.log(`#${id} archived`);
|
|
300
468
|
}),
|
|
301
469
|
);
|
|
302
470
|
|
|
303
|
-
program
|
|
304
|
-
.command('unarchive')
|
|
305
|
-
.description('show a change in the viewer')
|
|
306
|
-
.argument('<id>')
|
|
307
|
-
.action(
|
|
308
|
-
action((id) => {
|
|
309
|
-
archive(id, false);
|
|
310
|
-
console.log(`#${id} unarchived`);
|
|
311
|
-
}),
|
|
312
|
-
);
|
|
313
|
-
|
|
314
471
|
program
|
|
315
472
|
.command('log')
|
|
316
473
|
.description('append a timestamped Log entry')
|
|
@@ -389,6 +546,32 @@ program
|
|
|
389
546
|
}),
|
|
390
547
|
);
|
|
391
548
|
|
|
549
|
+
program
|
|
550
|
+
.command('search')
|
|
551
|
+
.description('deterministic lexical search over changes (incl. archived) and specs')
|
|
552
|
+
.argument('<query...>', 'search terms')
|
|
553
|
+
.option('--limit <n>', 'max results (default 10)')
|
|
554
|
+
.option(
|
|
555
|
+
'--type <type>',
|
|
556
|
+
'filter by a change type configured in .changeledger/config.yml (types:); excludes specs',
|
|
557
|
+
)
|
|
558
|
+
.option(
|
|
559
|
+
'--status <status>',
|
|
560
|
+
'filter by a change status configured in .changeledger/config.yml (statuses:); excludes specs',
|
|
561
|
+
)
|
|
562
|
+
.option('--json', 'print JSON')
|
|
563
|
+
.addHelpText(
|
|
564
|
+
'after',
|
|
565
|
+
[
|
|
566
|
+
'',
|
|
567
|
+
'Examples:',
|
|
568
|
+
' changeledger search wallet',
|
|
569
|
+
' changeledger search wallet --type bug --status done',
|
|
570
|
+
' changeledger search "app check" --json',
|
|
571
|
+
].join('\n'),
|
|
572
|
+
)
|
|
573
|
+
.action(action((queryParts, options) => runSearch(queryParts, options)));
|
|
574
|
+
|
|
392
575
|
program
|
|
393
576
|
.command('graduate')
|
|
394
577
|
.description('graduate a done change to persistent truth')
|
package/package.json
CHANGED
package/src/check.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// { errors, warnings }. No IO — the `changeledger check` command does the IO and printing.
|
|
3
3
|
|
|
4
4
|
import { parseChange } from './change.mjs';
|
|
5
|
+
import { hasFixableDefects } from './fix.mjs';
|
|
5
6
|
import { CANONICAL_STATUSES, canTransition, parseLogEvent } from './lifecycle.mjs';
|
|
6
7
|
import { compareVersions, parseVersion, RELEASE_IMPACTS } from './release.mjs';
|
|
7
8
|
|
|
@@ -32,6 +33,7 @@ export function checkRepo({ config, changes, specs = [], releases = [] }, opts =
|
|
|
32
33
|
const fm = c.frontmatter ?? {};
|
|
33
34
|
|
|
34
35
|
checkConflictMarkers(c, err);
|
|
36
|
+
checkAutoFixable(c, fm, warn);
|
|
35
37
|
|
|
36
38
|
for (const k of REQUIRED) if (!(k in fm)) err(c, `missing frontmatter "${k}"`);
|
|
37
39
|
if (fm.created && !ISO_UTC.test(fm.created)) err(c, `created not ISO 8601 UTC: ${fm.created}`);
|
|
@@ -317,6 +319,16 @@ function* graduationMarkers(change) {
|
|
|
317
319
|
// underline, but ChangeLedger uses ATX headings, so this is safe in practice.
|
|
318
320
|
const CONFLICT = /^(<{7}|={7}|>{7})(\s|$)/;
|
|
319
321
|
|
|
322
|
+
// Hints at `changeledger fix` rather than duplicating its repair rules: reuses
|
|
323
|
+
// the same pure detector so the two commands can never disagree about what
|
|
324
|
+
// counts as auto-fixable.
|
|
325
|
+
function checkAutoFixable(c, fm, warn) {
|
|
326
|
+
if (typeof c.text !== 'string') return;
|
|
327
|
+
if (hasFixableDefects(c.text)) {
|
|
328
|
+
warn(c, `auto-fixable format defect(s) found; run: changeledger fix ${fm.id ?? ''}`.trimEnd());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
320
332
|
function checkConflictMarkers(c, err) {
|
|
321
333
|
if (typeof c.text !== 'string') return;
|
|
322
334
|
const lines = c.text.split('\n');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseChange } from '../change.mjs';
|
|
4
|
+
import { findChangeledgerDir, loadConfig } from '../config.mjs';
|
|
5
|
+
import { beginSentinel, endSentinel, VERSION } from '../framing.mjs';
|
|
6
|
+
import { contractTemplatesDir } from '../paths.mjs';
|
|
7
|
+
import { resolveChange } from '../repo.mjs';
|
|
8
|
+
import { transversalPolicy } from './context.mjs';
|
|
9
|
+
|
|
10
|
+
const ROLES = ['investigation', 'implementation', 'review', 'audit'];
|
|
11
|
+
const ALLOWED_STATUSES = {
|
|
12
|
+
implementation: ['approved', 'in-progress'],
|
|
13
|
+
review: ['in-review'],
|
|
14
|
+
audit: ['in-validation'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function requireRepo(cwd) {
|
|
18
|
+
const dir = findChangeledgerDir(cwd);
|
|
19
|
+
if (!dir) throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function capsule(role) {
|
|
24
|
+
return fs
|
|
25
|
+
.readFileSync(path.join(contractTemplatesDir, 'agent-contexts', `${role}.md`), 'utf8')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function selectedChange(role, changeId, cwd) {
|
|
30
|
+
if (role !== 'investigation' && !changeId) {
|
|
31
|
+
throw new Error(`role ${role} requires a change id`);
|
|
32
|
+
}
|
|
33
|
+
if (!changeId) return undefined;
|
|
34
|
+
|
|
35
|
+
const resolved = resolveChange(cwd, changeId);
|
|
36
|
+
const text = fs.readFileSync(resolved.file, 'utf8');
|
|
37
|
+
const { id, status } = parseChange(text).frontmatter;
|
|
38
|
+
const allowed = ALLOWED_STATUSES[role];
|
|
39
|
+
if (allowed && !allowed.includes(status)) {
|
|
40
|
+
const expected = allowed.join(' or ');
|
|
41
|
+
throw new Error(`role ${role} requires change status ${expected}; got ${status}`);
|
|
42
|
+
}
|
|
43
|
+
return { id, text };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildAgentContext(role, changeId, cwd = process.cwd()) {
|
|
47
|
+
if (!ROLES.includes(role)) {
|
|
48
|
+
throw new Error(`Unknown role "${role}" — valid roles: ${ROLES.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
const changeledgerDir = requireRepo(cwd);
|
|
51
|
+
const selected = selectedChange(role, changeId, cwd);
|
|
52
|
+
const change = selected ? ` — change: #${selected.id}` : '';
|
|
53
|
+
const sections = [
|
|
54
|
+
beginSentinel('AGENT CONTEXT', `role: ${role}${change} — v${VERSION}`),
|
|
55
|
+
transversalPolicy(loadConfig(changeledgerDir)),
|
|
56
|
+
capsule(role),
|
|
57
|
+
];
|
|
58
|
+
if (selected) sections.push('---\n\n# Selected change', selected.text.trim());
|
|
59
|
+
sections.push(endSentinel('AGENT CONTEXT'));
|
|
60
|
+
return `${sections.join('\n\n')}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function agentContext(role, changeId, cwd = process.cwd(), output = console.log) {
|
|
64
|
+
output(buildAgentContext(role, changeId, cwd).trimEnd());
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { beginSentinel, endSentinel, VERSION } from '../framing.mjs';
|
|
4
|
+
import { contractTemplatesDir } from '../paths.mjs';
|
|
5
|
+
|
|
6
|
+
// Portable role skeletons ship inside the package, so this command works even
|
|
7
|
+
// outside an initialized ChangeLedger repo — it never reads project config.
|
|
8
|
+
const ROLES = ['investigation', 'implementation', 'review', 'audit'];
|
|
9
|
+
|
|
10
|
+
export function buildAgentPrompt(role) {
|
|
11
|
+
if (!ROLES.includes(role)) {
|
|
12
|
+
throw new Error(`Unknown role "${role}" — valid roles: ${ROLES.join(', ')}`);
|
|
13
|
+
}
|
|
14
|
+
const file = path.join(contractTemplatesDir, 'agent-prompts', `${role}.md`);
|
|
15
|
+
const body = fs.readFileSync(file, 'utf8').trim();
|
|
16
|
+
const begin = beginSentinel('AGENT PROMPT', `role: ${role} — v${VERSION}`);
|
|
17
|
+
return `${begin}\n\n${body}\n\n${endSentinel('AGENT PROMPT')}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function agentPrompt(role, output = console.log) {
|
|
21
|
+
output(buildAgentPrompt(role).trimEnd());
|
|
22
|
+
}
|
package/src/commands/agent.mjs
CHANGED
|
@@ -23,7 +23,7 @@ export function status(
|
|
|
23
23
|
id,
|
|
24
24
|
newStatus,
|
|
25
25
|
cwd = process.cwd(),
|
|
26
|
-
{ ownerHandle = defaultOwnerHandle } = {},
|
|
26
|
+
{ ownerHandle = defaultOwnerHandle, actor = 'human' } = {},
|
|
27
27
|
) {
|
|
28
28
|
const { config, file } = locate(cwd, id);
|
|
29
29
|
if (newStatus === 'discarded') {
|
|
@@ -34,6 +34,9 @@ export function status(
|
|
|
34
34
|
if (newStatus === 'done') {
|
|
35
35
|
throw new Error('to complete a change use human validation in the viewer');
|
|
36
36
|
}
|
|
37
|
+
if (newStatus === 'approved' && actor !== 'human') {
|
|
38
|
+
throw new Error('only the human-facing viewer can approve a draft change');
|
|
39
|
+
}
|
|
37
40
|
if (!(config.statuses ?? []).includes(newStatus)) {
|
|
38
41
|
throw new Error(`Invalid status "${newStatus}". Valid: ${(config.statuses ?? []).join(', ')}`);
|
|
39
42
|
}
|
|
@@ -41,7 +44,7 @@ export function status(
|
|
|
41
44
|
mutateFileAtomic(file, (text) => {
|
|
42
45
|
const fm = parseChange(text).frontmatter;
|
|
43
46
|
if (fm.status === 'done' && newStatus === 'in-progress') {
|
|
44
|
-
throw new Error('
|
|
47
|
+
throw new Error('to reopen a done change use `changeledger reopen <id> "<reason>"`');
|
|
45
48
|
}
|
|
46
49
|
// Validate the move before any in-memory mutation, so an illegal transition
|
|
47
50
|
// leaves the file byte-for-byte unchanged. The review gate reads review_required
|
|
@@ -110,7 +113,7 @@ export function review(id, verdict, { mode, reason } = {}, cwd = process.cwd())
|
|
|
110
113
|
|
|
111
114
|
// Records the human verdict for the complete change. This is intentionally
|
|
112
115
|
// separate from `status`: only the human-facing viewer may close a change.
|
|
113
|
-
export function validation(id, verdict, { reason } = {}, cwd = process.cwd()) {
|
|
116
|
+
export function validation(id, verdict, { reason, actor = 'human' } = {}, cwd = process.cwd()) {
|
|
114
117
|
const { config, file } = locate(cwd, id);
|
|
115
118
|
mutateFileAtomic(file, (text) => {
|
|
116
119
|
const fm = parseChange(text).frontmatter;
|
|
@@ -137,7 +140,7 @@ export function validation(id, verdict, { reason } = {}, cwd = process.cwd()) {
|
|
|
137
140
|
nowUtc(),
|
|
138
141
|
verdict === 'pass'
|
|
139
142
|
? 'validation → done (human accepted)'
|
|
140
|
-
: `validation → in-progress (
|
|
143
|
+
: `validation → in-progress (${actor} rejected): ${reason}`,
|
|
141
144
|
);
|
|
142
145
|
if (verdict === 'pass') assertChangeTextValid(config, path.basename(file), text);
|
|
143
146
|
return text;
|
|
@@ -145,9 +148,9 @@ export function validation(id, verdict, { reason } = {}, cwd = process.cwd()) {
|
|
|
145
148
|
return file;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
//
|
|
151
|
+
// Correction path while `done` is still provisional. Graduation,
|
|
149
152
|
// skip, archive and release membership are durable boundaries and fail closed.
|
|
150
|
-
export function reopen(id, reason, cwd = process.cwd()) {
|
|
153
|
+
export function reopen(id, reason, cwd = process.cwd(), { actor = 'human' } = {}) {
|
|
151
154
|
if (!String(reason ?? '').trim()) throw new Error('reopen requires a reason');
|
|
152
155
|
const { config, file, repoRoot } = locate(cwd, id);
|
|
153
156
|
const releasesDir = resolveReleasesDir(repoRoot);
|
|
@@ -171,7 +174,7 @@ export function reopen(id, reason, cwd = process.cwd()) {
|
|
|
171
174
|
reviewRequired: Boolean(config.types?.[fm.type]?.review_required),
|
|
172
175
|
});
|
|
173
176
|
text = setStatus(text, 'in-progress');
|
|
174
|
-
return appendLog(text, nowUtc(), `status: done → in-progress (
|
|
177
|
+
return appendLog(text, nowUtc(), `status: done → in-progress (${actor} reopened): ${reason}`);
|
|
175
178
|
});
|
|
176
179
|
return file;
|
|
177
180
|
});
|
|
@@ -209,11 +212,11 @@ export function discard(id, reason, cwd = process.cwd()) {
|
|
|
209
212
|
return file;
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
export function archive(id,
|
|
215
|
+
export function archive(id, cwd = process.cwd()) {
|
|
213
216
|
const { file } = locate(cwd, id);
|
|
214
217
|
mutateFileAtomic(file, (text) => {
|
|
215
|
-
text = setArchived(text,
|
|
216
|
-
return appendLog(text, nowUtc(),
|
|
218
|
+
text = setArchived(text, true);
|
|
219
|
+
return appendLog(text, nowUtc(), 'archived');
|
|
217
220
|
});
|
|
218
221
|
return file;
|
|
219
222
|
}
|
package/src/commands/check.mjs
CHANGED
|
@@ -1,12 +1,67 @@
|
|
|
1
1
|
import { checkRepo } from '../check.mjs';
|
|
2
|
+
import { findChangeledgerDir, integrationBranch, loadConfig } from '../config.mjs';
|
|
2
3
|
import { getSchemaVersion, SUPPORTED_SCHEMA_VERSION } from '../config-migration.mjs';
|
|
3
4
|
import { checkContract } from '../contract.mjs';
|
|
5
|
+
import { defaultBaseBranch, lintCommitRange } from '../git.mjs';
|
|
4
6
|
import { loadRepo } from '../repo.mjs';
|
|
5
7
|
|
|
8
|
+
// Declared integration branch, when a ChangeLedger repo (and the key) exists.
|
|
9
|
+
// Outside a repo the lint still works on plain git, so absence is undefined,
|
|
10
|
+
// not an error; a malformed declared value still fails fast in
|
|
11
|
+
// `integrationBranch`.
|
|
12
|
+
function configuredIntegrationBranch(cwd) {
|
|
13
|
+
const changeledgerDir = findChangeledgerDir(cwd);
|
|
14
|
+
if (!changeledgerDir) return undefined;
|
|
15
|
+
return integrationBranch(loadConfig(changeledgerDir));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Lints `base..HEAD` for the canonical `[#id]` commit marker (merges and
|
|
19
|
+
// `chore(release)` prep are exempt) — no ChangeLedger repo required, just git.
|
|
20
|
+
function checkCommits(args, commitsIdx, cwd, output, json) {
|
|
21
|
+
const provided = args[commitsIdx + 1];
|
|
22
|
+
const base = provided && !provided.startsWith('--') ? provided : undefined;
|
|
23
|
+
|
|
24
|
+
let resolvedBase;
|
|
25
|
+
let violations;
|
|
26
|
+
try {
|
|
27
|
+
resolvedBase = base ?? configuredIntegrationBranch(cwd) ?? defaultBaseBranch(cwd);
|
|
28
|
+
violations = lintCommitRange(cwd, `${resolvedBase}..HEAD`);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (json)
|
|
31
|
+
output.log(
|
|
32
|
+
JSON.stringify(
|
|
33
|
+
{ errors: [{ file: '(commits)', message: e.message }], warnings: [] },
|
|
34
|
+
null,
|
|
35
|
+
2,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
else output.error(` error (commits): ${e.message}`);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const errors = violations.map((v) => ({
|
|
43
|
+
file: '(commits)',
|
|
44
|
+
message: `${v.sha} missing [#id] marker: "${v.subject}"`,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
if (json) {
|
|
48
|
+
output.log(JSON.stringify({ errors, warnings: [] }, null, 2));
|
|
49
|
+
return errors.length ? 1 : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const e of errors) output.error(` error ${e.file}: ${e.message}`);
|
|
53
|
+
const scope = `commits ${resolvedBase}..HEAD`;
|
|
54
|
+
if (!errors.length) output.log(`✓ ${scope} valid`);
|
|
55
|
+
else output.log(`\n${errors.length} error(s) — ${scope}`);
|
|
56
|
+
return errors.length ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
6
59
|
// Validates the repo (or a single change with `changeledger check <id>`). Prints findings
|
|
7
60
|
// and returns an exit code (1 if errors).
|
|
8
61
|
export function check(args = [], cwd = process.cwd(), output = console) {
|
|
9
62
|
const json = args.includes('--json');
|
|
63
|
+
const commitsIdx = args.indexOf('--commits');
|
|
64
|
+
if (commitsIdx !== -1) return checkCommits(args, commitsIdx, cwd, output, json);
|
|
10
65
|
const id = args.find((a) => !a.startsWith('--'));
|
|
11
66
|
|
|
12
67
|
let repo;
|