changeledger 0.8.0 → 0.9.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 +6 -2
- package/README.md +5 -1
- package/bin/changeledger.mjs +81 -5
- package/package.json +1 -1
- package/src/commands/agent-context.mjs +64 -0
- package/src/commands/agent-prompt.mjs +22 -0
- package/src/commands/agent.mjs +10 -7
- package/src/commands/context.mjs +5 -6
- package/src/contract.mjs +6 -2
- package/src/framing.mjs +22 -0
- package/src/lifecycle.mjs +2 -2
- package/src/viewer/domain.mjs +1 -1
- package/src/viewer/public/app-state.js +62 -6
- package/src/viewer/public/app.js +78 -29
- package/src/viewer/public/index.html +2 -2
- package/src/viewer/public/state.js +6 -2
- 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/implementation.md +38 -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 +18 -13
- package/templates/contract/core.md +30 -29
- package/templates/contract/implement.md +10 -8
- package/templates/contract/review.md +7 -9
- package/templates/contract/validation.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -6,8 +6,12 @@ under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
|
6
6
|
<!-- changeledger -->
|
|
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
|
package/README.md
CHANGED
|
@@ -137,11 +137,15 @@ 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.
|
|
145
149
|
|
|
146
150
|
### Upgrading an existing repo's configuration
|
|
147
151
|
|
package/bin/changeledger.mjs
CHANGED
|
@@ -8,11 +8,15 @@ 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';
|
|
17
21
|
import { context } from '../src/commands/context.mjs';
|
|
18
22
|
import {
|
|
@@ -34,9 +38,10 @@ const { version } = createRequire(import.meta.url)('../package.json');
|
|
|
34
38
|
|
|
35
39
|
const USAGE = `ChangeLedger (changeledger)
|
|
36
40
|
|
|
37
|
-
Run \`changeledger context\` first in any repo
|
|
41
|
+
Run \`changeledger context\` first in any repo unless a ChangeLedger delegation
|
|
42
|
+
prompt identifies your role and tells you to run \`agent-context\` instead.
|
|
38
43
|
|
|
39
|
-
changeledger init | register | new | view | check | context
|
|
44
|
+
changeledger init | register | new | view | check | context | agent-context
|
|
40
45
|
changeledger status | discard | review | owner | archive | unarchive
|
|
41
46
|
changeledger log | task | list | show | graduate | config | release
|
|
42
47
|
|
|
@@ -62,7 +67,7 @@ program
|
|
|
62
67
|
.helpOption('-h, --help', 'display help for command')
|
|
63
68
|
.addHelpText(
|
|
64
69
|
'after',
|
|
65
|
-
'\nRun `changeledger context` first
|
|
70
|
+
'\nRun `changeledger context` first unless a ChangeLedger delegation prompt tells your role to use `agent-context`.\n' +
|
|
66
71
|
"Run `changeledger <command> --help` for that command's syntax, values and examples.",
|
|
67
72
|
);
|
|
68
73
|
|
|
@@ -179,6 +184,47 @@ program
|
|
|
179
184
|
)
|
|
180
185
|
.action(action((input) => context(input)));
|
|
181
186
|
|
|
187
|
+
program
|
|
188
|
+
.command('agent-prompt')
|
|
189
|
+
.description('print a portable delegation prompt skeleton for a role')
|
|
190
|
+
.argument('<role>', 'investigation | implementation | review')
|
|
191
|
+
.addHelpText(
|
|
192
|
+
'after',
|
|
193
|
+
[
|
|
194
|
+
'',
|
|
195
|
+
'Prints a fill-in-the-blanks delegation prompt for the given role. Works',
|
|
196
|
+
'outside a ChangeLedger repo — the skeletons ship inside the package.',
|
|
197
|
+
'',
|
|
198
|
+
'Examples:',
|
|
199
|
+
' changeledger agent-prompt investigation',
|
|
200
|
+
' changeledger agent-prompt implementation',
|
|
201
|
+
' changeledger agent-prompt review',
|
|
202
|
+
].join('\n'),
|
|
203
|
+
)
|
|
204
|
+
.action(action((role) => agentPrompt(role)));
|
|
205
|
+
|
|
206
|
+
program
|
|
207
|
+
.command('agent-context')
|
|
208
|
+
.description('print a self-contained minimal context for a delegated role')
|
|
209
|
+
.argument('<role>', 'investigation | implementation | review')
|
|
210
|
+
.argument('[change-id]', 'optional for investigation; required for implementation and review')
|
|
211
|
+
.addHelpText(
|
|
212
|
+
'after',
|
|
213
|
+
[
|
|
214
|
+
'',
|
|
215
|
+
'Use only when a delegation prompt emitted by `changeledger agent-prompt`',
|
|
216
|
+
'identifies you as that role. This replaces the normal core bootstrap for',
|
|
217
|
+
'the delegated leaf; normal agents still run `changeledger context` first.',
|
|
218
|
+
'',
|
|
219
|
+
'Examples:',
|
|
220
|
+
' changeledger agent-context investigation',
|
|
221
|
+
' changeledger agent-context investigation <id>',
|
|
222
|
+
' changeledger agent-context implementation <id>',
|
|
223
|
+
' changeledger agent-context review <id>',
|
|
224
|
+
].join('\n'),
|
|
225
|
+
)
|
|
226
|
+
.action(action((role, changeId) => agentContext(role, changeId)));
|
|
227
|
+
|
|
182
228
|
program
|
|
183
229
|
.command('status')
|
|
184
230
|
.description("move a change's lifecycle status (agent-owned, non-terminal moves only)")
|
|
@@ -192,7 +238,7 @@ program
|
|
|
192
238
|
[
|
|
193
239
|
'',
|
|
194
240
|
'Terminal moves are not accepted here: use `changeledger discard <id> "<reason>"`',
|
|
195
|
-
'to discard
|
|
241
|
+
'to discard. Only human validation in the viewer can reach done.',
|
|
196
242
|
'',
|
|
197
243
|
'Examples:',
|
|
198
244
|
' changeledger status <id> in-progress',
|
|
@@ -201,11 +247,41 @@ program
|
|
|
201
247
|
)
|
|
202
248
|
.action(
|
|
203
249
|
action((id, st) => {
|
|
204
|
-
status(id, st);
|
|
250
|
+
status(id, st, process.cwd(), { actor: 'agent' });
|
|
205
251
|
console.log(`#${id} → ${st}`);
|
|
206
252
|
}),
|
|
207
253
|
);
|
|
208
254
|
|
|
255
|
+
program
|
|
256
|
+
.command('validation')
|
|
257
|
+
.description('reject an in-validation change; accepting it remains human-only')
|
|
258
|
+
.argument('<id>')
|
|
259
|
+
.argument('<verdict>', 'fail')
|
|
260
|
+
.argument('<reason...>')
|
|
261
|
+
.action(
|
|
262
|
+
action((id, verdict, reasonParts) => {
|
|
263
|
+
if (verdict !== 'fail')
|
|
264
|
+
throw new Error(
|
|
265
|
+
'validation only accepts fail; human validation in the viewer accepts changes',
|
|
266
|
+
);
|
|
267
|
+
const reason = reasonParts.join(' ').trim();
|
|
268
|
+
validation(id, 'fail', { reason, actor: 'agent' });
|
|
269
|
+
console.log(`#${id} validation fail`);
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
program
|
|
274
|
+
.command('reopen')
|
|
275
|
+
.description('reopen a provisional done change with a reason')
|
|
276
|
+
.argument('<id>')
|
|
277
|
+
.argument('<reason...>')
|
|
278
|
+
.action(
|
|
279
|
+
action((id, reasonParts) => {
|
|
280
|
+
reopen(id, reasonParts.join(' ').trim(), process.cwd(), { actor: 'agent' });
|
|
281
|
+
console.log(`#${id} → in-progress`);
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
|
|
209
285
|
program
|
|
210
286
|
.command('discard')
|
|
211
287
|
.description('discard a change (terminal; keeps the record and reason)')
|
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
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'];
|
|
11
|
+
const ALLOWED_STATUSES = {
|
|
12
|
+
implementation: ['approved', 'in-progress'],
|
|
13
|
+
review: ['in-review'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function requireRepo(cwd) {
|
|
17
|
+
const dir = findChangeledgerDir(cwd);
|
|
18
|
+
if (!dir) throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function capsule(role) {
|
|
23
|
+
return fs
|
|
24
|
+
.readFileSync(path.join(contractTemplatesDir, 'agent-contexts', `${role}.md`), 'utf8')
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function selectedChange(role, changeId, cwd) {
|
|
29
|
+
if (role !== 'investigation' && !changeId) {
|
|
30
|
+
throw new Error(`role ${role} requires a change id`);
|
|
31
|
+
}
|
|
32
|
+
if (!changeId) return undefined;
|
|
33
|
+
|
|
34
|
+
const resolved = resolveChange(cwd, changeId);
|
|
35
|
+
const text = fs.readFileSync(resolved.file, 'utf8');
|
|
36
|
+
const { id, status } = parseChange(text).frontmatter;
|
|
37
|
+
const allowed = ALLOWED_STATUSES[role];
|
|
38
|
+
if (allowed && !allowed.includes(status)) {
|
|
39
|
+
const expected = allowed.join(' or ');
|
|
40
|
+
throw new Error(`role ${role} requires change status ${expected}; got ${status}`);
|
|
41
|
+
}
|
|
42
|
+
return { id, text };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildAgentContext(role, changeId, cwd = process.cwd()) {
|
|
46
|
+
if (!ROLES.includes(role)) {
|
|
47
|
+
throw new Error(`Unknown role "${role}" — valid roles: ${ROLES.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
const changeledgerDir = requireRepo(cwd);
|
|
50
|
+
const selected = selectedChange(role, changeId, cwd);
|
|
51
|
+
const change = selected ? ` — change: #${selected.id}` : '';
|
|
52
|
+
const sections = [
|
|
53
|
+
beginSentinel('AGENT CONTEXT', `role: ${role}${change} — v${VERSION}`),
|
|
54
|
+
transversalPolicy(loadConfig(changeledgerDir)),
|
|
55
|
+
capsule(role),
|
|
56
|
+
];
|
|
57
|
+
if (selected) sections.push('---\n\n# Selected change', selected.text.trim());
|
|
58
|
+
sections.push(endSentinel('AGENT CONTEXT'));
|
|
59
|
+
return `${sections.join('\n\n')}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function agentContext(role, changeId, cwd = process.cwd(), output = console.log) {
|
|
63
|
+
output(buildAgentContext(role, changeId, cwd).trimEnd());
|
|
64
|
+
}
|
|
@@ -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'];
|
|
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
|
});
|
package/src/commands/context.mjs
CHANGED
|
@@ -2,12 +2,11 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseChange } from '../change.mjs';
|
|
4
4
|
import { findChangeledgerDir, loadConfig } from '../config.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { beginSentinel, endSentinel, VERSION } from '../framing.mjs';
|
|
6
|
+
import { contractTemplatesDir } from '../paths.mjs';
|
|
6
7
|
import { resolveChange } from '../repo.mjs';
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const END_DELIMITER =
|
|
10
|
-
'===== CHANGELEDGER CONTEXT END — if this line is missing, the output was truncated: stop and re-run =====';
|
|
9
|
+
const END_DELIMITER = endSentinel('CONTEXT');
|
|
11
10
|
const MODES = ['implement', 'review', 'spec', 'release'];
|
|
12
11
|
const MODE_CONTEXT = {
|
|
13
12
|
implement: ['implement', 'delegation', 'handoff'],
|
|
@@ -34,7 +33,7 @@ function fragment(name) {
|
|
|
34
33
|
|
|
35
34
|
function beginDelimiter(mode, changeId) {
|
|
36
35
|
const change = changeId ? ` — change: #${changeId}` : '';
|
|
37
|
-
return
|
|
36
|
+
return beginSentinel('CONTEXT', `mode: ${mode}${change} — v${VERSION}`);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
// Resolved defaults so an agent never reads `.changeledger/config.yml` raw to
|
|
@@ -54,7 +53,7 @@ function effectiveTdd(config) {
|
|
|
54
53
|
|
|
55
54
|
// The transversal policy line every composition anchors on: effective language
|
|
56
55
|
// and tdd with defaults already resolved.
|
|
57
|
-
function transversalPolicy(config) {
|
|
56
|
+
export function transversalPolicy(config) {
|
|
58
57
|
return `Effective policy: language=${effectiveLanguage(config)} — tdd=${effectiveTdd(config)}`;
|
|
59
58
|
}
|
|
60
59
|
|
package/src/contract.mjs
CHANGED
|
@@ -37,8 +37,12 @@ const LEGACY_CONTRACT_HASHES = new Set([
|
|
|
37
37
|
export const REFERENCE = `${MARKER}
|
|
38
38
|
> [!IMPORTANT]
|
|
39
39
|
> This repo uses **ChangeLedger**. Immediately after reading this file — before
|
|
40
|
-
> planning, investigating, or acting — run \`changeledger context\` directly.
|
|
41
|
-
>
|
|
40
|
+
> planning, investigating, or acting — a normal agent must run \`changeledger context\` directly.
|
|
41
|
+
> Only a delegated leaf whose prompt was emitted by \`changeledger agent-prompt <role>\`
|
|
42
|
+
> runs \`changeledger agent-context <role> [change-id]\` instead; the role in the
|
|
43
|
+
> prompt and command must match. No other agent may skip the core context.
|
|
44
|
+
> On the first invocation, retain complete stdout through the \`CHANGELEDGER CONTEXT END\` line,
|
|
45
|
+
> or the \`CHANGELEDGER AGENT CONTEXT END\` line for that delegated path:
|
|
42
46
|
> no pipes, filters, summaries, previews or voluntary output limits. If the tool
|
|
43
47
|
> exposes an output budget, reserve enough for the whole response. A missing END
|
|
44
48
|
> after that is exceptional recovery: stop and re-run with a larger capture. If
|
package/src/framing.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { packageRoot } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
// Single source of the installed version and the anti-truncation sentinels, so
|
|
6
|
+
// `context`, `agent-prompt` and `agent-context` share framing and never diverge
|
|
7
|
+
// through independent copies.
|
|
8
|
+
export const VERSION = JSON.parse(
|
|
9
|
+
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
|
|
10
|
+
).version;
|
|
11
|
+
|
|
12
|
+
const TRUNCATION_SUFFIX = 'if this line is missing, the output was truncated: stop and re-run';
|
|
13
|
+
|
|
14
|
+
// `kind` names the payload (e.g. "CONTEXT", "AGENT PROMPT"); `meta` is the
|
|
15
|
+
// already-formatted descriptor (e.g. "mode: implement — v0.8.0").
|
|
16
|
+
export function beginSentinel(kind, meta) {
|
|
17
|
+
return `===== CHANGELEDGER ${kind} BEGIN — ${meta} =====`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function endSentinel(kind) {
|
|
21
|
+
return `===== CHANGELEDGER ${kind} END — ${TRUNCATION_SUFFIX} =====`;
|
|
22
|
+
}
|
package/src/lifecycle.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The change lifecycle as an explicit, testable graph — the single authority on
|
|
2
2
|
// which status moves are legal. Shared by the CLI (`changeledger status`) and the viewer
|
|
3
3
|
// so both decide validity the same way. The viewer layers an extra human-only
|
|
4
|
-
// policy on top (approval plus final acceptance
|
|
4
|
+
// policy on top (approval plus final acceptance); it never relaxes
|
|
5
5
|
// this graph.
|
|
6
6
|
|
|
7
7
|
export const CANONICAL_STATUSES = [
|
|
@@ -21,7 +21,7 @@ export const CANONICAL_STATUSES = [
|
|
|
21
21
|
// `in-validation` is the universal human gate before done. Review or validation
|
|
22
22
|
// may route back to in-progress, while review may also block. `discarded` is a
|
|
23
23
|
// terminal tombstone reachable only before either closing gate. `done` has one
|
|
24
|
-
// policy-gated
|
|
24
|
+
// policy-gated provisional reopen edge; generic status commands do not own it.
|
|
25
25
|
const TRANSITIONS = {
|
|
26
26
|
draft: ['approved', 'discarded'],
|
|
27
27
|
approved: ['in-progress', 'discarded'],
|
package/src/viewer/domain.mjs
CHANGED
|
@@ -130,7 +130,7 @@ export function changeStatus(projects, { project, id, status, reason }) {
|
|
|
130
130
|
}
|
|
131
131
|
try {
|
|
132
132
|
if (current === 'draft' && status === 'approved') {
|
|
133
|
-
applyStatusCmd(id, status, proj.path);
|
|
133
|
+
applyStatusCmd(id, status, proj.path, { actor: 'human' });
|
|
134
134
|
} else if (current === 'in-validation' && status === 'done') {
|
|
135
135
|
applyValidation(id, 'pass', {}, proj.path);
|
|
136
136
|
} else if (current === 'in-validation' && status === 'in-progress') {
|
|
@@ -7,8 +7,9 @@ const VALID_DETAIL_SIZES = new Set(['compact', 'wide', 'full']);
|
|
|
7
7
|
let storage = null;
|
|
8
8
|
|
|
9
9
|
const emptyProjectFilters = () => ({
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
types: [],
|
|
11
|
+
owners: [],
|
|
12
|
+
includeUnassigned: false,
|
|
12
13
|
statuses: [],
|
|
13
14
|
showArchived: false,
|
|
14
15
|
showDiscarded: false,
|
|
@@ -21,6 +22,9 @@ export const state = {
|
|
|
21
22
|
text: '',
|
|
22
23
|
type: 'all',
|
|
23
24
|
owner: 'all',
|
|
25
|
+
types: new Set(),
|
|
26
|
+
owners: new Set(),
|
|
27
|
+
includeUnassigned: false,
|
|
24
28
|
statuses: new Set(),
|
|
25
29
|
showArchived: false,
|
|
26
30
|
showDiscarded: false,
|
|
@@ -39,8 +43,9 @@ export const state = {
|
|
|
39
43
|
|
|
40
44
|
function currentProjectFilters() {
|
|
41
45
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
types: [...state.filters.types],
|
|
47
|
+
owners: [...state.filters.owners],
|
|
48
|
+
includeUnassigned: state.filters.includeUnassigned,
|
|
44
49
|
statuses: [...state.filters.statuses],
|
|
45
50
|
showArchived: state.filters.showArchived,
|
|
46
51
|
showDiscarded: state.filters.showDiscarded,
|
|
@@ -53,8 +58,23 @@ function saveCurrentProjectFilters() {
|
|
|
53
58
|
|
|
54
59
|
function applyProjectFilters(id) {
|
|
55
60
|
const filters = state.projectFilters[id] ?? emptyProjectFilters();
|
|
56
|
-
state.filters.
|
|
57
|
-
|
|
61
|
+
state.filters.types = new Set(
|
|
62
|
+
Array.isArray(filters.types)
|
|
63
|
+
? filters.types.filter((value) => typeof value === 'string')
|
|
64
|
+
: typeof filters.type === 'string' && filters.type !== 'all'
|
|
65
|
+
? [filters.type]
|
|
66
|
+
: [],
|
|
67
|
+
);
|
|
68
|
+
state.filters.type = state.filters.types.size === 1 ? [...state.filters.types][0] : 'all';
|
|
69
|
+
state.filters.owners = new Set(
|
|
70
|
+
Array.isArray(filters.owners)
|
|
71
|
+
? filters.owners.filter((value) => typeof value === 'string')
|
|
72
|
+
: typeof filters.owner === 'string' && filters.owner !== 'all'
|
|
73
|
+
? [filters.owner]
|
|
74
|
+
: [],
|
|
75
|
+
);
|
|
76
|
+
state.filters.owner = state.filters.owners.size === 1 ? [...state.filters.owners][0] : 'all';
|
|
77
|
+
state.filters.includeUnassigned = filters.includeUnassigned === true;
|
|
58
78
|
state.filters.statuses = new Set(
|
|
59
79
|
Array.isArray(filters.statuses)
|
|
60
80
|
? filters.statuses.filter((value) => typeof value === 'string')
|
|
@@ -144,9 +164,13 @@ export function normalizeRepoState(repo) {
|
|
|
144
164
|
}
|
|
145
165
|
if (state.sortDir !== 1 && state.sortDir !== -1) state.sortDir = 1;
|
|
146
166
|
if (!repo.types.includes(state.filters.type)) state.filters.type = 'all';
|
|
167
|
+
state.filters.types = new Set(
|
|
168
|
+
[...state.filters.types].filter((type) => repo.types.includes(type)),
|
|
169
|
+
);
|
|
147
170
|
const owners = new Set(repo.changes.map((change) => change.owner).filter(Boolean));
|
|
148
171
|
if (state.filters.owner !== 'all' && !owners.has(state.filters.owner))
|
|
149
172
|
state.filters.owner = 'all';
|
|
173
|
+
state.filters.owners = new Set([...state.filters.owners].filter((owner) => owners.has(owner)));
|
|
150
174
|
const statuses = new Set(repo.statuses);
|
|
151
175
|
state.filters.statuses = new Set(
|
|
152
176
|
[...state.filters.statuses].filter((status) => statuses.has(status)),
|
|
@@ -168,16 +192,48 @@ export function setTextFilter(text) {
|
|
|
168
192
|
persistViewerState();
|
|
169
193
|
}
|
|
170
194
|
|
|
195
|
+
export function toggleTypeFilter(type) {
|
|
196
|
+
if (state.filters.types.has(type)) state.filters.types.delete(type);
|
|
197
|
+
else state.filters.types.add(type);
|
|
198
|
+
state.filters.type = state.filters.types.size === 1 ? [...state.filters.types][0] : 'all';
|
|
199
|
+
persistViewerState();
|
|
200
|
+
}
|
|
201
|
+
|
|
171
202
|
export function setTypeFilter(type) {
|
|
203
|
+
state.filters.types = new Set(type === 'all' ? [] : [type]);
|
|
172
204
|
state.filters.type = type;
|
|
173
205
|
persistViewerState();
|
|
174
206
|
}
|
|
175
207
|
|
|
208
|
+
export function toggleOwnerFilter(owner) {
|
|
209
|
+
if (state.filters.owners.has(owner)) state.filters.owners.delete(owner);
|
|
210
|
+
else state.filters.owners.add(owner);
|
|
211
|
+
state.filters.owner = state.filters.owners.size === 1 ? [...state.filters.owners][0] : 'all';
|
|
212
|
+
persistViewerState();
|
|
213
|
+
}
|
|
214
|
+
|
|
176
215
|
export function setOwnerFilter(owner) {
|
|
216
|
+
state.filters.owners = new Set(owner === 'all' ? [] : [owner]);
|
|
177
217
|
state.filters.owner = owner;
|
|
178
218
|
persistViewerState();
|
|
179
219
|
}
|
|
180
220
|
|
|
221
|
+
export function toggleUnassignedOwner() {
|
|
222
|
+
state.filters.includeUnassigned = !state.filters.includeUnassigned;
|
|
223
|
+
persistViewerState();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function clearTypeFilters() {
|
|
227
|
+
state.filters.types.clear();
|
|
228
|
+
persistViewerState();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function clearOwnerFilters() {
|
|
232
|
+
state.filters.owners.clear();
|
|
233
|
+
state.filters.includeUnassigned = false;
|
|
234
|
+
persistViewerState();
|
|
235
|
+
}
|
|
236
|
+
|
|
181
237
|
export function toggleStatusFilter(status) {
|
|
182
238
|
if (state.filters.statuses.has(status)) state.filters.statuses.delete(status);
|
|
183
239
|
else state.filters.statuses.add(status);
|
package/src/viewer/public/app.js
CHANGED
|
@@ -13,24 +13,27 @@ import {
|
|
|
13
13
|
searchAllProjects,
|
|
14
14
|
} from './api.js';
|
|
15
15
|
import {
|
|
16
|
+
clearOwnerFilters,
|
|
16
17
|
clearStatusFilters,
|
|
18
|
+
clearTypeFilters,
|
|
17
19
|
initializeProjects,
|
|
18
20
|
invalidateCache,
|
|
19
21
|
normalizeRepoState,
|
|
20
22
|
restoreViewerState,
|
|
21
23
|
selectProject,
|
|
22
24
|
setDetailPresentation,
|
|
23
|
-
setOwnerFilter,
|
|
24
25
|
setRepo,
|
|
25
26
|
setSortKey,
|
|
26
27
|
setTextFilter,
|
|
27
|
-
setTypeFilter,
|
|
28
28
|
setView,
|
|
29
29
|
state,
|
|
30
30
|
toggleGlobalMode,
|
|
31
|
+
toggleOwnerFilter,
|
|
31
32
|
toggleShowArchived,
|
|
32
33
|
toggleShowDiscarded,
|
|
33
34
|
toggleStatusFilter,
|
|
35
|
+
toggleTypeFilter,
|
|
36
|
+
toggleUnassignedOwner,
|
|
34
37
|
} from './app-state.js';
|
|
35
38
|
import { cssIdent, initMermaid, makeMermaidExpandable, renderMermaid } from './security.js';
|
|
36
39
|
import { boardStatuses, isVisible, passesTombstones } from './state.js';
|
|
@@ -115,27 +118,74 @@ export function showNoProjects(root = document) {
|
|
|
115
118
|
|
|
116
119
|
// Rebuilt on each project load (types/statuses can differ per project).
|
|
117
120
|
function hydrateFilters() {
|
|
118
|
-
litRender(
|
|
119
|
-
html`<option value="all">All types</option>
|
|
120
|
-
${state.repo.types.map((t) => html`<option value=${t}>${t}</option>`)}`,
|
|
121
|
-
$('#type-filter'),
|
|
122
|
-
);
|
|
123
|
-
$('#type-filter').value = state.filters.type;
|
|
124
121
|
$('#lang').textContent = state.repo.language;
|
|
125
|
-
|
|
126
122
|
const owners = [...new Set(state.repo.changes.map((c) => c.owner).filter(Boolean))].sort();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
renderChoiceFilter(
|
|
124
|
+
$('#type-filter'),
|
|
125
|
+
'Type',
|
|
126
|
+
state.repo.types,
|
|
127
|
+
state.filters.types,
|
|
128
|
+
toggleTypeFilter,
|
|
129
|
+
clearTypeFilters,
|
|
130
|
+
);
|
|
131
|
+
renderChoiceFilter(
|
|
130
132
|
$('#owner-filter'),
|
|
133
|
+
'Owner',
|
|
134
|
+
owners,
|
|
135
|
+
state.filters.owners,
|
|
136
|
+
toggleOwnerFilter,
|
|
137
|
+
clearOwnerFilters,
|
|
138
|
+
true,
|
|
131
139
|
);
|
|
132
|
-
if (state.filters.owner !== 'all' && !owners.includes(state.filters.owner)) setOwnerFilter('all');
|
|
133
|
-
$('#owner-filter').value = state.filters.owner;
|
|
134
|
-
$('#owner-filter').style.display = owners.length ? '' : 'none';
|
|
135
|
-
|
|
136
140
|
renderStatusFilter();
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
export function choiceFilterSummary(label, selected, includeUnassigned = false) {
|
|
144
|
+
const count = selected.size + Number(includeUnassigned);
|
|
145
|
+
if (count === 1 && includeUnassigned) return 'Unassigned';
|
|
146
|
+
return count
|
|
147
|
+
? count === 1
|
|
148
|
+
? [...selected][0]
|
|
149
|
+
: `${count} selected`
|
|
150
|
+
: `All ${label.toLowerCase()}s`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderChoiceFilter(host, label, choices, selected, toggle, clear, owners = false) {
|
|
154
|
+
const summary = choiceFilterSummary(label, selected, owners && state.filters.includeUnassigned);
|
|
155
|
+
litRender(
|
|
156
|
+
html`<details class="filter-menu">
|
|
157
|
+
<summary class="filter-trigger">
|
|
158
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 3.25h12M4.25 8h7.5M6.5 12.75h3"></path></svg>
|
|
159
|
+
<span>${summary}</span>
|
|
160
|
+
<svg class="filter-chevron" viewBox="0 0 16 16" aria-hidden="true"><path d="m4.5 6.25 3.5 3.5 3.5-3.5"></path></svg>
|
|
161
|
+
</summary>
|
|
162
|
+
<div class="filter-popover">
|
|
163
|
+
<div class="filter-heading"><span>${label}</span><button type="button" data-clear>Clear</button></div>
|
|
164
|
+
<div class="filter-options">
|
|
165
|
+
${choices.map((choice) => html`<label class="check-option"><input type="checkbox" data-choice=${choice} .checked=${selected.has(choice)} /><span class="check-box" aria-hidden="true"></span><span>${choice}</span></label>`)}
|
|
166
|
+
${owners ? html`<label class="check-option"><input type="checkbox" data-unassigned .checked=${state.filters.includeUnassigned} /><span class="check-box" aria-hidden="true"></span><span>Unassigned</span></label>` : nothing}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</details>`,
|
|
170
|
+
host,
|
|
171
|
+
);
|
|
172
|
+
host.querySelectorAll('[data-choice]').forEach((input) => {
|
|
173
|
+
input.onchange = () => {
|
|
174
|
+
toggle(input.dataset.choice);
|
|
175
|
+
render();
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
if (owners)
|
|
179
|
+
host.querySelector('[data-unassigned]').onchange = () => {
|
|
180
|
+
toggleUnassignedOwner();
|
|
181
|
+
render();
|
|
182
|
+
};
|
|
183
|
+
host.querySelector('[data-clear]').onclick = () => {
|
|
184
|
+
clear();
|
|
185
|
+
render();
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
139
189
|
function renderStatusFilter() {
|
|
140
190
|
const sf = $('#status-filter');
|
|
141
191
|
litRender(
|
|
@@ -1556,16 +1606,13 @@ function bootstrap() {
|
|
|
1556
1606
|
if (active) enterGlobal();
|
|
1557
1607
|
else activateView(state.currentView);
|
|
1558
1608
|
};
|
|
1559
|
-
$('#type-filter').onchange = (e) => {
|
|
1560
|
-
setTypeFilter(e.target.value);
|
|
1561
|
-
render();
|
|
1562
|
-
};
|
|
1563
|
-
$('#owner-filter').onchange = (e) => {
|
|
1564
|
-
setOwnerFilter(e.target.value);
|
|
1565
|
-
render();
|
|
1566
|
-
};
|
|
1567
1609
|
document.addEventListener('pointerdown', (event) => {
|
|
1568
|
-
|
|
1610
|
+
closeFilterMenusOnOutsideClick(
|
|
1611
|
+
['#type-filter', '#owner-filter', '#status-filter'].map((selector) =>
|
|
1612
|
+
$(`${selector} .filter-menu`),
|
|
1613
|
+
),
|
|
1614
|
+
event.target,
|
|
1615
|
+
);
|
|
1569
1616
|
});
|
|
1570
1617
|
$('#view-board').onclick = () => activateView('board');
|
|
1571
1618
|
$('#view-table').onclick = () => activateView('table');
|
|
@@ -1594,10 +1641,12 @@ function bootstrap() {
|
|
|
1594
1641
|
setInterval(load, 5000);
|
|
1595
1642
|
}
|
|
1596
1643
|
|
|
1597
|
-
export function
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1644
|
+
export function closeFilterMenusOnOutsideClick(menus, target) {
|
|
1645
|
+
return menus.reduce((closed, menu) => {
|
|
1646
|
+
if (!menu?.open || menu.contains(target)) return closed;
|
|
1647
|
+
menu.open = false;
|
|
1648
|
+
return true;
|
|
1649
|
+
}, false);
|
|
1601
1650
|
}
|
|
1602
1651
|
|
|
1603
1652
|
// Only a real browser page with the app shell bootstraps; importing the module
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
<select id="project" class="filter" title="Project"></select>
|
|
27
27
|
<input id="search" class="search" type="search" placeholder="Search anything…" />
|
|
28
28
|
<button type="button" id="toggle-global" class="chip" title="Search across all projects">Global</button>
|
|
29
|
-
<
|
|
30
|
-
<
|
|
29
|
+
<div id="type-filter"></div>
|
|
30
|
+
<div id="owner-filter"></div>
|
|
31
31
|
<div id="status-filter"></div>
|
|
32
32
|
<div class="spacer"></div>
|
|
33
33
|
<div class="tabs">
|
|
@@ -22,8 +22,12 @@ function haystack(c) {
|
|
|
22
22
|
// predicate so the rule is testable.
|
|
23
23
|
export function isVisible(c, f) {
|
|
24
24
|
if (!passesTombstones(c, f)) return false;
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const types = f.types ?? new Set(f.type && f.type !== 'all' ? [f.type] : []);
|
|
26
|
+
const owners = f.owners ?? new Set(f.owner && f.owner !== 'all' ? [f.owner] : []);
|
|
27
|
+
if (types.size && !types.has(c.type)) return false;
|
|
28
|
+
if (owners.size || f.includeUnassigned) {
|
|
29
|
+
if (c.owner ? !owners.has(c.owner) : !f.includeUnassigned) return false;
|
|
30
|
+
}
|
|
27
31
|
if (f.statuses.size && !f.statuses.has(c.status)) return false;
|
|
28
32
|
const q = f.text.toLowerCase();
|
|
29
33
|
if (!q) return true;
|
|
@@ -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,38 @@
|
|
|
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
|
+
Files you own: {{files}} (the only paths you may modify).
|
|
22
|
+
|
|
23
|
+
Boundaries — expressed by effect, not by tool name: modify only the files under
|
|
24
|
+
your ownership above; do not revert or overwrite anyone else's work; if you find
|
|
25
|
+
your change overlaps another change's surface, stop and report it instead of
|
|
26
|
+
resolving it silently. Do not mutate the ledger — status, task, log, review and
|
|
27
|
+
graduation transitions are the orchestrator's; you implement and report.
|
|
28
|
+
|
|
29
|
+
Expected output: {{expected_output}} (the code plus the tests that prove the
|
|
30
|
+
cited criteria, red-green; state which criteria you covered).
|
|
31
|
+
|
|
32
|
+
Difficulty or risk that set the model choice: {{difficulty_or_risk}}.
|
|
33
|
+
|
|
34
|
+
Return to the orchestrator only what changed and how it was verified — the files
|
|
35
|
+
touched, the tests run and their result, and any overlap you hit.
|
|
36
|
+
|
|
37
|
+
Integration criterion: {{integration}} (how the orchestrator merges/verifies your
|
|
38
|
+
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
|
+
}
|
|
@@ -20,24 +20,29 @@ tags: []
|
|
|
20
20
|
Choose exactly one explicit graduation mode. A positional slug without a mode
|
|
21
21
|
is an error, so words such as `skip` or `skip-*` can never silently become specs.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
For a new spec, follow this ordered recipe — `--new` alone does not finish it:
|
|
24
|
+
|
|
25
|
+
1. `changeledger graduate <id> <spec-slug> --new` creates a seed from the
|
|
26
|
+
change's Specification or Proposal but leaves graduation pending; it does not
|
|
27
|
+
set `reviewed: true`.
|
|
28
|
+
2. Rewrite the seed as concise durable current truth and remove the explicit
|
|
29
|
+
scaffold marker.
|
|
30
|
+
3. `changeledger graduate <id> <spec-slug> --into` finalizes it; `--into`
|
|
31
|
+
refuses an unrefined marked scaffold and sets `reviewed: true`.
|
|
32
|
+
|
|
33
|
+
Alternatives to the two-step:
|
|
34
|
+
|
|
35
|
+
- For an existing spec, edit its body first, then run
|
|
29
36
|
`changeledger graduate <id> <spec-slug> --into`. It refreshes `updated`, records
|
|
30
|
-
the link
|
|
37
|
+
the link, does not overwrite the body and sets `reviewed: true`.
|
|
31
38
|
- `changeledger graduate <id> --skip [reason]` records that no persistent truth
|
|
32
|
-
changed
|
|
39
|
+
changed and sets `reviewed: true`.
|
|
33
40
|
- `changeledger graduate --pending` lists accepted changes whose decision is
|
|
34
41
|
unresolved.
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
The graduation link remains derivable from the Log marker `graduado a spec`,
|
|
40
|
-
which carries the spec link, rather than from the boolean flag.
|
|
43
|
+
`reviewed: true` means the persistent-truth question was settled, not necessarily
|
|
44
|
+
that a spec was created. The graduation link remains derivable from the Log
|
|
45
|
+
marker `graduado a spec`, which carries the spec link, rather than from the flag.
|
|
41
46
|
|
|
42
47
|
After `--into` or `--skip`, create one final closure commit that coalesces any
|
|
43
48
|
pending `in-review → in-validation → done` ledger updates with the graduation
|
|
@@ -29,11 +29,11 @@ change-id context required by a real task or lifecycle transition.
|
|
|
29
29
|
5. Keep lifecycle, tasks, ownership and Log current while working.
|
|
30
30
|
6. For types that require review, use a fresh clean-context reviewer before
|
|
31
31
|
human validation.
|
|
32
|
-
7. `in-validation` stops only that change; the agent never accepts on the human's behalf, but may start another approved change unless
|
|
32
|
+
7. `in-validation` stops only that change; the agent never accepts on the human's behalf, but may reject with a reason and start another approved change unless its `depends_on` chain (direct or transitive) reaches an `in-validation` change.
|
|
33
33
|
8. After human acceptance, reload `changeledger context <id>` for the `done`
|
|
34
|
-
change, then graduate persistent truth
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
change, then graduate persistent truth or run `changeledger graduate <id>
|
|
35
|
+
--skip [reason]`; archive only after that decision. The close overlay owns
|
|
36
|
+
the full graduation recipe.
|
|
37
37
|
|
|
38
38
|
If no approved or in-progress change applies, do not silently edit repository
|
|
39
39
|
files. Create or update a change, or ask the human whether a purely operational,
|
|
@@ -48,42 +48,43 @@ Files are the source of truth and may be edited directly. CLI helpers are
|
|
|
48
48
|
optional and preferred for error-prone operations such as timestamps, lifecycle
|
|
49
49
|
transitions and task markers.
|
|
50
50
|
|
|
51
|
-
Delegate only with a clear boundary and benefit. Each delegation prompt states
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
the task's difficulty and risk.
|
|
51
|
+
Delegate only with a clear boundary and benefit. Each delegation prompt states at least
|
|
52
|
+
ownership, expected output and integration criterion; the task context carries the full
|
|
53
|
+
prompt contract. Get a complete role skeleton to fill in with `changeledger agent-prompt
|
|
54
|
+
<role>` (investigation | implementation | review). Coding agents must know they share the
|
|
55
|
+
codebase and must not revert others' work. Do not over-shard or overlap write surfaces
|
|
56
|
+
without an explicit integration plan. Size the model to the task's difficulty and risk.
|
|
57
57
|
|
|
58
58
|
## Lifecycle
|
|
59
59
|
|
|
60
|
-
```text
|
|
61
|
-
draft → approved → in-progress
|
|
62
|
-
in-progress → in-review → in-validation → done [review required]
|
|
63
|
-
in-progress → in-validation → done [no review required]
|
|
64
|
-
in-review → in-progress [review retry]
|
|
65
|
-
in-review → blocked → in-progress [review escalation]
|
|
66
|
-
in-validation → in-progress [human rejection]
|
|
67
|
-
done → in-progress [human reopen before durable closure]
|
|
68
|
-
(draft | approved | in-progress | blocked) → discarded
|
|
69
|
-
```
|
|
70
|
-
|
|
71
60
|
- `draft`: documentation awaiting human approval; no implementation.
|
|
72
61
|
- `approved`: ready to start after the Git/worktree checks.
|
|
73
62
|
- `in-progress`: implementation underway.
|
|
74
63
|
- `in-review`: independent review required.
|
|
75
|
-
- `in-validation`: stop
|
|
64
|
+
- `in-validation`: stop for human acceptance or a reasoned rejection.
|
|
76
65
|
- `blocked`: an impediment or decision needs resolution.
|
|
77
66
|
- `done`: the human accepted the complete result; provisional until durable closure.
|
|
78
67
|
- `discarded`: terminal tombstone; never reopen it.
|
|
79
68
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
Who owns each transition and how it is performed:
|
|
70
|
+
|
|
71
|
+
| Transition | Owner | Mechanism |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| draft → approved | human | viewer |
|
|
74
|
+
| approved → in-progress; blocked → in-progress; in-progress → in-review | agent | `changeledger status` |
|
|
75
|
+
| in-progress → in-validation (no review) | agent | `changeledger status` |
|
|
76
|
+
| in-review → in-validation | orchestrator | `changeledger review <id> pass` |
|
|
77
|
+
| in-review → in-progress | orchestrator | `changeledger review <id> fail --retry` |
|
|
78
|
+
| in-review → blocked | orchestrator | `changeledger review <id> fail --block` |
|
|
79
|
+
| in-validation → done | human | viewer |
|
|
80
|
+
| in-validation → in-progress | agent or human | `changeledger validation <id> fail "<reason>"` or viewer |
|
|
81
|
+
| done → in-progress (pending closure) | agent or human | `changeledger reopen <id> "<reason>"` or viewer |
|
|
82
|
+
| draft/approved/in-progress/blocked → discarded | agent (authorized) | `changeledger discard <id> "<reason>"` |
|
|
83
|
+
|
|
84
|
+
`changeledger status <id> <status>` performs the agent-owned moves and does not accept `approved`, `done`, `discarded` or reopening.
|
|
85
|
+
The discard reason is required and logged, and dependencies remain resolvable; `discarded` never reopens.
|
|
86
|
+
A `done` change can reopen only to finish its original scope before graduation/skip, archive or release;
|
|
87
|
+
after durable closure, later work needs a new change.
|
|
87
88
|
|
|
88
89
|
## Context modes
|
|
89
90
|
|
|
@@ -54,13 +54,15 @@ Useful mutation commands:
|
|
|
54
54
|
- `changeledger review <id> pass|fail`
|
|
55
55
|
- `changeledger check [id]`
|
|
56
56
|
|
|
57
|
-
When implementation and every task are complete, move to `in-review` if the
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
When implementation and every task are complete, move to `in-review` if the type
|
|
58
|
+
requires independent review by running this ordered gate — do not reconstruct it from memory:
|
|
59
|
+
1. Confirm every Plan task is complete and its verification passes.
|
|
60
|
+
2. `changeledger status <id> in-review`.
|
|
61
|
+
3. Load `changeledger context review` once; do not reload it to record the verdict unless context was lost (compaction, a new session).
|
|
62
|
+
4. Delegate to a fresh, read-only reviewer with clean context; it reports but never records the verdict itself.
|
|
63
|
+
5. Record the delegate's verdict yourself with `changeledger review <id> pass|fail` — never `log`+`status`.
|
|
64
|
+
|
|
65
|
+
`in-validation`: human accepts; agent rejects with `changeledger validation <id> fail "<reason>"`.
|
|
64
66
|
|
|
65
67
|
## Correction isolation
|
|
66
68
|
|
|
@@ -71,7 +73,7 @@ worktree is its isolation boundary. After `pass`, commit the confirmed correctio
|
|
|
71
73
|
tests and ledger before human validation; this is meaningful correction evidence,
|
|
72
74
|
not a status-only commit.
|
|
73
75
|
|
|
74
|
-
After
|
|
76
|
+
After a rejection (`in-validation → in-progress`), run
|
|
75
77
|
`changeledger context <id>` before modifying implementation; keep the correction
|
|
76
78
|
uncommitted until the human confirms it fixes the reported failure. Do not start
|
|
77
79
|
another task or change while a correction waits; iterate on
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Review-required work must be checked by a fresh subagent with clean context and
|
|
4
4
|
a model sized to the review difficulty. Independence is correctness, not an
|
|
5
|
-
optimization
|
|
5
|
+
optimization.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Get the bounded prompt with `changeledger agent-prompt review`; the delegate
|
|
8
|
+
then loads `changeledger agent-context review <id>`. That self-contained capsule
|
|
9
|
+
owns the inspection checklist, read-only boundary and evidence contract. Do not
|
|
10
|
+
copy its checklist into this orchestrator context.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Record exactly one verdict:
|
|
12
|
+
The orchestrator records exactly one verdict; the read-only reviewer reports its
|
|
13
|
+
finding but never runs the verdict command:
|
|
16
14
|
|
|
17
15
|
- `changeledger review <id> pass` — criteria and Plan pass; move to
|
|
18
16
|
`in-validation`.
|
|
@@ -13,7 +13,7 @@ Acceptance reaches `done`. Rejection requires a reason and returns the same
|
|
|
13
13
|
change to `in-progress`; run `changeledger context <id>` before modifying
|
|
14
14
|
implementation, update Specification/Plan as needed and repeat review when
|
|
15
15
|
configured. The agent never accepts on the human's behalf. Before graduation,
|
|
16
|
-
skip, archive or release,
|
|
16
|
+
skip, archive or release, agent/human may reopen `done` with reason only to
|
|
17
17
|
complete the original authorized scope; broader behavior needs a new change.
|
|
18
18
|
`discarded` never reopens.
|
|
19
19
|
|