changeledger 0.7.0 → 0.8.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 +7 -5
- package/bin/changeledger.mjs +1 -1
- package/package.json +1 -1
- package/src/check.mjs +37 -1
- package/src/commands/agent.mjs +44 -4
- package/src/commands/context.mjs +3 -3
- package/src/commands/graduate.mjs +20 -8
- package/src/commands/view.mjs +6 -2
- package/src/contract.mjs +7 -5
- package/src/lifecycle.mjs +3 -2
- package/src/metrics.mjs +3 -3
- package/src/viewer/domain.mjs +16 -6
- package/src/viewer/public/app-state.js +15 -0
- package/src/viewer/public/app.js +115 -13
- package/src/viewer/public/styles.css +205 -14
- package/src/viewer/public/view-parts.js +52 -0
- package/templates/contract/close.md +7 -1
- package/templates/contract/core.md +20 -13
- package/templates/contract/delegation.md +2 -1
- package/templates/contract/implement.md +23 -9
- package/templates/contract/review.md +5 -0
- package/templates/contract/validation.md +16 -2
package/AGENTS.md
CHANGED
|
@@ -6,11 +6,13 @@ 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
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
9
|
+
> planning, investigating, or acting — run `changeledger context` directly.
|
|
10
|
+
> On the first invocation, retain complete stdout through the `CHANGELEDGER CONTEXT END` line:
|
|
11
|
+
> no pipes, filters, summaries, previews or voluntary output limits. If the tool
|
|
12
|
+
> exposes an output budget, reserve enough for the whole response. A missing END
|
|
13
|
+
> after that is exceptional recovery: stop and re-run with a larger capture. If
|
|
14
|
+
> the command is unavailable, stop and restore/install ChangeLedger; do not
|
|
15
|
+
> proceed from memory.
|
|
14
16
|
>
|
|
15
17
|
> Do not create or modify files without an authorized change; the core context
|
|
16
18
|
> defines the workflow, the task contexts, and the narrow operational exception.
|
package/bin/changeledger.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { registerRepo } from '../src/commands/register.mjs';
|
|
|
27
27
|
import { initReleaseHistory, recordRelease, releasePlan } from '../src/commands/release.mjs';
|
|
28
28
|
import { view } from '../src/commands/view.mjs';
|
|
29
29
|
import { findChangeledgerDir } from '../src/config.mjs';
|
|
30
|
-
import { applyMigration
|
|
30
|
+
import { applyMigration } from '../src/config-migration.mjs';
|
|
31
31
|
import { nowUtc } from '../src/paths.mjs';
|
|
32
32
|
|
|
33
33
|
const { version } = createRequire(import.meta.url)('../package.json');
|
package/package.json
CHANGED
package/src/check.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Pure validator: takes a loaded repo ({ config, changes }) and returns
|
|
2
2
|
// { errors, warnings }. No IO — the `changeledger check` command does the IO and printing.
|
|
3
3
|
|
|
4
|
+
import { parseChange } from './change.mjs';
|
|
4
5
|
import { CANONICAL_STATUSES, canTransition, parseLogEvent } from './lifecycle.mjs';
|
|
5
6
|
import { compareVersions, parseVersion, RELEASE_IMPACTS } from './release.mjs';
|
|
6
7
|
|
|
@@ -82,7 +83,7 @@ export function checkRepo({ config, changes, specs = [], releases = [] }, opts =
|
|
|
82
83
|
|
|
83
84
|
if (fm.status === 'done' && tasks.some((t) => t.state !== 'done')) {
|
|
84
85
|
const pending = tasks.filter((t) => t.state !== 'done').length;
|
|
85
|
-
|
|
86
|
+
err(c, `status is "done" but ${pending} task(s) are not done`);
|
|
86
87
|
}
|
|
87
88
|
if (fm.status === 'blocked' && tasks.length && !tasks.some((t) => t.state === 'blocked')) {
|
|
88
89
|
warn(c, 'status is "blocked" but no task is marked [!]');
|
|
@@ -131,6 +132,41 @@ export function checkRepo({ config, changes, specs = [], releases = [] }, opts =
|
|
|
131
132
|
return { errors, warnings };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
// Reuses the scoped validator for a selected change, optionally replacing its
|
|
136
|
+
// in-memory text with a candidate that has not been written yet. Callers gate
|
|
137
|
+
// mutations on errors only; warnings remain informational by contract.
|
|
138
|
+
export function checkSelectedChange(repo, id, candidateText) {
|
|
139
|
+
let changes = repo.changes;
|
|
140
|
+
if (candidateText !== undefined) {
|
|
141
|
+
const index = changes.findIndex((c) => String(c.frontmatter?.id) === String(id));
|
|
142
|
+
if (index !== -1) {
|
|
143
|
+
const current = changes[index];
|
|
144
|
+
const candidate = { ...current, text: candidateText, ...parseChange(candidateText) };
|
|
145
|
+
changes = changes.with(index, candidate);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return checkRepo({ ...repo, changes }, { id });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function assertSelectedChangeValid(repo, id, candidateText) {
|
|
152
|
+
const { errors } = checkSelectedChange(repo, id, candidateText);
|
|
153
|
+
if (!errors.length) return;
|
|
154
|
+
throw new Error(
|
|
155
|
+
`change ${id} failed scoped validation:\n${errors.map((error) => `- ${error.message}`).join('\n')}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validates one already-resolved file without loading or parsing siblings. This
|
|
160
|
+
// is the write-gate path for operations whose scope is exactly one change.
|
|
161
|
+
export function assertChangeTextValid(config, name, text) {
|
|
162
|
+
const parsed = parseChange(text);
|
|
163
|
+
const id = parsed.frontmatter.id;
|
|
164
|
+
assertSelectedChangeValid(
|
|
165
|
+
{ config, changes: [{ name, text, ...parsed }], specs: [], releases: [] },
|
|
166
|
+
id,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
134
170
|
function checkReleases(releases, changesById, err) {
|
|
135
171
|
const seenVersions = new Set();
|
|
136
172
|
const releasedChanges = new Map();
|
package/src/commands/agent.mjs
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
// (list/show). Files remain the source of truth; these are optional helpers
|
|
3
3
|
// that inject correct timestamps/markers and validate transitions.
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
|
-
import { mutateFileAtomic } from '../atomic-write.mjs';
|
|
7
|
+
import { mutateFileAtomic, withFileLock } from '../atomic-write.mjs';
|
|
7
8
|
import { parseChange } from '../change.mjs';
|
|
9
|
+
import { assertChangeTextValid } from '../check.mjs';
|
|
8
10
|
import { ownerHandle as defaultOwnerHandle } from '../git.mjs';
|
|
9
11
|
import { assertTransition } from '../lifecycle.mjs';
|
|
10
12
|
import { nowUtc } from '../paths.mjs';
|
|
13
|
+
import { resolveReleasesDir } from '../release.mjs';
|
|
11
14
|
import { loadRepo, resolveChange } from '../repo.mjs';
|
|
12
15
|
import { appendLog, setArchived, setOwner, setStatus, setTask } from '../writer.mjs';
|
|
13
16
|
|
|
14
17
|
function locate(cwd, id) {
|
|
15
|
-
const { config, file } = resolveChange(cwd, id);
|
|
16
|
-
return { config, file };
|
|
18
|
+
const { config, file, repoRoot } = resolveChange(cwd, id);
|
|
19
|
+
return { config, file, repoRoot };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function status(
|
|
@@ -37,6 +40,9 @@ export function status(
|
|
|
37
40
|
const autoOwner = newStatus === 'in-progress' ? ownerHandle(path.dirname(file)) : '';
|
|
38
41
|
mutateFileAtomic(file, (text) => {
|
|
39
42
|
const fm = parseChange(text).frontmatter;
|
|
43
|
+
if (fm.status === 'done' && newStatus === 'in-progress') {
|
|
44
|
+
throw new Error('only the human-facing viewer can reopen a done change');
|
|
45
|
+
}
|
|
40
46
|
// Validate the move before any in-memory mutation, so an illegal transition
|
|
41
47
|
// leaves the file byte-for-byte unchanged. The review gate reads review_required
|
|
42
48
|
// from the change's type.
|
|
@@ -126,17 +132,51 @@ export function validation(id, verdict, { reason } = {}, cwd = process.cwd()) {
|
|
|
126
132
|
reviewRequired: Boolean(config.types?.[fm.type]?.review_required),
|
|
127
133
|
});
|
|
128
134
|
text = setStatus(text, target);
|
|
129
|
-
|
|
135
|
+
text = appendLog(
|
|
130
136
|
text,
|
|
131
137
|
nowUtc(),
|
|
132
138
|
verdict === 'pass'
|
|
133
139
|
? 'validation → done (human accepted)'
|
|
134
140
|
: `validation → in-progress (human rejected): ${reason}`,
|
|
135
141
|
);
|
|
142
|
+
if (verdict === 'pass') assertChangeTextValid(config, path.basename(file), text);
|
|
143
|
+
return text;
|
|
136
144
|
});
|
|
137
145
|
return file;
|
|
138
146
|
}
|
|
139
147
|
|
|
148
|
+
// Human-only correction path while `done` is still provisional. Graduation,
|
|
149
|
+
// skip, archive and release membership are durable boundaries and fail closed.
|
|
150
|
+
export function reopen(id, reason, cwd = process.cwd()) {
|
|
151
|
+
if (!String(reason ?? '').trim()) throw new Error('reopen requires a reason');
|
|
152
|
+
const { config, file, repoRoot } = locate(cwd, id);
|
|
153
|
+
const releasesDir = resolveReleasesDir(repoRoot);
|
|
154
|
+
fs.mkdirSync(releasesDir, { recursive: true });
|
|
155
|
+
return withFileLock(path.join(releasesDir, '.history'), () => {
|
|
156
|
+
const released = loadRepo(cwd).releases.some((release) =>
|
|
157
|
+
(release.changes ?? []).some((changeId) => String(changeId) === String(id)),
|
|
158
|
+
);
|
|
159
|
+
mutateFileAtomic(file, (text) => {
|
|
160
|
+
const change = { ...parseChange(text), text };
|
|
161
|
+
const fm = change.frontmatter;
|
|
162
|
+
if (fm.status !== 'done')
|
|
163
|
+
throw new Error(`reopen requires status done (current: ${fm.status})`);
|
|
164
|
+
if (fm.reviewed === true) throw new Error('cannot reopen: graduation is already reviewed');
|
|
165
|
+
if (hasGraduationResolution(change))
|
|
166
|
+
throw new Error('cannot reopen: graduation is already resolved');
|
|
167
|
+
if (fm.archived === true) throw new Error('cannot reopen: change is archived');
|
|
168
|
+
if (released) throw new Error('cannot reopen: change belongs to a recorded release');
|
|
169
|
+
assertTransition('done', 'in-progress', {
|
|
170
|
+
type: fm.type,
|
|
171
|
+
reviewRequired: Boolean(config.types?.[fm.type]?.review_required),
|
|
172
|
+
});
|
|
173
|
+
text = setStatus(text, 'in-progress');
|
|
174
|
+
return appendLog(text, nowUtc(), `status: done → in-progress (human reopened): ${reason}`);
|
|
175
|
+
});
|
|
176
|
+
return file;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
140
180
|
// name '-' clears the owner.
|
|
141
181
|
export function owner(id, name, cwd = process.cwd()) {
|
|
142
182
|
const { file } = locate(cwd, id);
|
package/src/commands/context.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const STATUS_CONTEXT = {
|
|
|
26
26
|
discarded: { mode: 'discarded', fragments: ['discarded'] },
|
|
27
27
|
};
|
|
28
28
|
const INCREMENTAL_NOTICE = `This incremental context extends the complete core context already read.
|
|
29
|
-
|
|
29
|
+
Its one-pass full-capture rule applies here; a partial view is invalid.`;
|
|
30
30
|
|
|
31
31
|
function fragment(name) {
|
|
32
32
|
return fs.readFileSync(path.join(contractTemplatesDir, `${name}.md`), 'utf8').trim();
|
|
@@ -73,7 +73,7 @@ function changePolicyBlock(config, type) {
|
|
|
73
73
|
|
|
74
74
|
// One line per local dependency (id, title, status); external `project:id`
|
|
75
75
|
// references stay references, never pretending local resolution.
|
|
76
|
-
function dependencyBlock(
|
|
76
|
+
function dependencyBlock(dependsOn, cwd) {
|
|
77
77
|
if (!Array.isArray(dependsOn) || dependsOn.length === 0) return undefined;
|
|
78
78
|
const lines = dependsOn.map((raw) => {
|
|
79
79
|
const dep = String(raw);
|
|
@@ -140,7 +140,7 @@ export function buildContext(input, cwd = process.cwd()) {
|
|
|
140
140
|
changeText: text,
|
|
141
141
|
changeId: id,
|
|
142
142
|
policy: changePolicyBlock(config, type),
|
|
143
|
-
dependencies: dependencyBlock(
|
|
143
|
+
dependencies: dependencyBlock(dependsOn, cwd),
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -6,6 +6,7 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { mutateFileAtomic, writeFileAtomic } from '../atomic-write.mjs';
|
|
8
8
|
import { parseChange } from '../change.mjs';
|
|
9
|
+
import { assertChangeTextValid } from '../check.mjs';
|
|
9
10
|
import { findChangeledgerDir, loadConfig, resolveRepoPath, resolveSpecsDir } from '../config.mjs';
|
|
10
11
|
import { nowUtc } from '../paths.mjs';
|
|
11
12
|
import { resolveChange } from '../repo.mjs';
|
|
@@ -30,11 +31,23 @@ function requireDone(changeText) {
|
|
|
30
31
|
return change;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function requireGraduationReady(config, changeFile, changeText) {
|
|
35
|
+
const change = requireDone(changeText);
|
|
36
|
+
assertChangeTextValid(config, path.basename(changeFile), changeText);
|
|
37
|
+
return change;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
export function scaffoldSpec(id, slug, cwd = process.cwd()) {
|
|
34
|
-
const {
|
|
41
|
+
const {
|
|
42
|
+
config,
|
|
43
|
+
file: changeFile,
|
|
44
|
+
specsDir,
|
|
45
|
+
specName,
|
|
46
|
+
specFile,
|
|
47
|
+
} = graduationTarget(id, slug, cwd);
|
|
48
|
+
const change = requireGraduationReady(config, changeFile, fs.readFileSync(changeFile, 'utf8'));
|
|
35
49
|
if (fs.existsSync(specFile)) throw new Error(`Spec "${specName}" already exists`);
|
|
36
50
|
|
|
37
|
-
const change = requireDone(fs.readFileSync(changeFile, 'utf8'));
|
|
38
51
|
const seedStage =
|
|
39
52
|
change.stages.find((stage) => stage.key === 'specification') ??
|
|
40
53
|
change.stages.find((stage) => stage.key === 'proposal');
|
|
@@ -65,14 +78,15 @@ export function graduate(id, slug, cwd = process.cwd(), { into = false } = {}) {
|
|
|
65
78
|
if (!into) {
|
|
66
79
|
throw new Error('graduation mode required: use --new, --into, or --skip');
|
|
67
80
|
}
|
|
68
|
-
const { file: changeFile, specName, specFile } = graduationTarget(id, slug, cwd);
|
|
81
|
+
const { config, file: changeFile, specName, specFile } = graduationTarget(id, slug, cwd);
|
|
82
|
+
requireGraduationReady(config, changeFile, fs.readFileSync(changeFile, 'utf8'));
|
|
69
83
|
|
|
70
84
|
if (!fs.existsSync(specFile)) {
|
|
71
85
|
throw new Error(`Spec "${specName}" does not exist — use --new to create a scaffold`);
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
mutateFileAtomic(changeFile, (changeText) => {
|
|
75
|
-
|
|
89
|
+
requireGraduationReady(config, changeFile, changeText);
|
|
76
90
|
const specText = fs.readFileSync(specFile, 'utf8');
|
|
77
91
|
if (specText.includes(SPEC_SCAFFOLD_MARKER)) {
|
|
78
92
|
throw new Error(
|
|
@@ -91,12 +105,10 @@ export function graduate(id, slug, cwd = process.cwd(), { into = false } = {}) {
|
|
|
91
105
|
// Marks a done change's graduation as reviewed without creating a spec (e.g. a
|
|
92
106
|
// bug/chore with no persistent truth). Records the reason in the Log.
|
|
93
107
|
export function skipGraduation(id, reason, cwd = process.cwd()) {
|
|
94
|
-
const { file: changeFile } = resolveChange(cwd, id);
|
|
108
|
+
const { config, file: changeFile } = resolveChange(cwd, id);
|
|
95
109
|
const message = reason ? `graduation skipped: ${reason}` : 'graduation skipped';
|
|
96
110
|
mutateFileAtomic(changeFile, (text) => {
|
|
97
|
-
|
|
98
|
-
if (change.frontmatter.status !== 'done')
|
|
99
|
-
throw new Error('only done changes can be graduated/skipped');
|
|
111
|
+
requireGraduationReady(config, changeFile, text);
|
|
100
112
|
|
|
101
113
|
text = appendLog(text, nowUtc(), message);
|
|
102
114
|
return setReviewed(text, true);
|
package/src/commands/view.mjs
CHANGED
|
@@ -41,7 +41,11 @@ function parseViewArgs(args) {
|
|
|
41
41
|
return { localOnly, port };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export async function view(
|
|
44
|
+
export async function view(
|
|
45
|
+
args = [],
|
|
46
|
+
cwd = process.cwd(),
|
|
47
|
+
{ openBrowser: shouldOpen = true } = {},
|
|
48
|
+
) {
|
|
45
49
|
const { localOnly, port: requestedPort } = parseViewArgs(args);
|
|
46
50
|
resolveProjects(cwd, localOnly); // fail fast if local mode outside a repo
|
|
47
51
|
|
|
@@ -54,7 +58,7 @@ export async function view(args = [], cwd = process.cwd()) {
|
|
|
54
58
|
const port = await listen(server, host, requestedPort);
|
|
55
59
|
const url = `http://${host}:${port}`;
|
|
56
60
|
console.log(`ChangeLedger viewer → ${url} (Ctrl+C to stop)`);
|
|
57
|
-
openBrowser(url);
|
|
61
|
+
if (shouldOpen) openBrowser(url);
|
|
58
62
|
return server;
|
|
59
63
|
}
|
|
60
64
|
|
package/src/contract.mjs
CHANGED
|
@@ -37,11 +37,13 @@ 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
|
-
>
|
|
42
|
-
>
|
|
43
|
-
>
|
|
44
|
-
>
|
|
40
|
+
> planning, investigating, or acting — run \`changeledger context\` directly.
|
|
41
|
+
> On the first invocation, retain complete stdout through the \`CHANGELEDGER CONTEXT END\` line:
|
|
42
|
+
> no pipes, filters, summaries, previews or voluntary output limits. If the tool
|
|
43
|
+
> exposes an output budget, reserve enough for the whole response. A missing END
|
|
44
|
+
> after that is exceptional recovery: stop and re-run with a larger capture. If
|
|
45
|
+
> the command is unavailable, stop and restore/install ChangeLedger; do not
|
|
46
|
+
> proceed from memory.
|
|
45
47
|
>
|
|
46
48
|
> Do not create or modify files without an authorized change; the core context
|
|
47
49
|
> defines the workflow, the task contexts, and the narrow operational exception.
|
package/src/lifecycle.mjs
CHANGED
|
@@ -20,7 +20,8 @@ export const CANONICAL_STATUSES = [
|
|
|
20
20
|
// absent and therefore rejected. `in-review` is optional by type;
|
|
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
|
-
// terminal tombstone reachable only before either closing gate.
|
|
23
|
+
// terminal tombstone reachable only before either closing gate. `done` has one
|
|
24
|
+
// policy-gated human reopen edge; generic agent commands do not own it.
|
|
24
25
|
const TRANSITIONS = {
|
|
25
26
|
draft: ['approved', 'discarded'],
|
|
26
27
|
approved: ['in-progress', 'discarded'],
|
|
@@ -28,7 +29,7 @@ const TRANSITIONS = {
|
|
|
28
29
|
'in-review': ['in-validation', 'in-progress', 'blocked'],
|
|
29
30
|
'in-validation': ['done', 'in-progress'],
|
|
30
31
|
blocked: ['in-progress', 'discarded'],
|
|
31
|
-
done: [],
|
|
32
|
+
done: ['in-progress'],
|
|
32
33
|
discarded: [],
|
|
33
34
|
};
|
|
34
35
|
|
package/src/metrics.mjs
CHANGED
|
@@ -42,8 +42,8 @@ function transitions(change) {
|
|
|
42
42
|
return events;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Time spent in each state: [{ state, ms }].
|
|
46
|
-
//
|
|
45
|
+
// Time spent in each state: [{ state, ms }]. A provisional `done` interval is
|
|
46
|
+
// retained in the event stream but excluded from active-state duration.
|
|
47
47
|
export function statusTimeline(change, now) {
|
|
48
48
|
const events = transitions(change);
|
|
49
49
|
if (!events.length) return [];
|
|
@@ -53,7 +53,7 @@ export function statusTimeline(change, now) {
|
|
|
53
53
|
const endIso = events[i + 1]?.at ?? now;
|
|
54
54
|
const end = Date.parse(endIso);
|
|
55
55
|
if (Number.isNaN(start) || Number.isNaN(end)) continue;
|
|
56
|
-
// `done` is
|
|
56
|
+
// `done` is outside active work, including a provisional interval before reopen.
|
|
57
57
|
if (events[i].state === 'done') continue;
|
|
58
58
|
segs.push({ state: events[i].state, ms: Math.max(0, end - start) });
|
|
59
59
|
}
|
package/src/viewer/domain.mjs
CHANGED
|
@@ -3,8 +3,13 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { parseDocument } from 'yaml';
|
|
5
5
|
import { mutateFileAtomic } from '../atomic-write.mjs';
|
|
6
|
+
import { parseChange } from '../change.mjs';
|
|
6
7
|
import { checkRepo } from '../check.mjs';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
reopen as applyReopen,
|
|
10
|
+
status as applyStatusCmd,
|
|
11
|
+
validation as applyValidation,
|
|
12
|
+
} from '../commands/agent.mjs';
|
|
8
13
|
import { findChangeledgerDir, loadConfig, resolveRepoPath, resolveSpecsDir } from '../config.mjs';
|
|
9
14
|
import {
|
|
10
15
|
buildMigration,
|
|
@@ -14,7 +19,7 @@ import {
|
|
|
14
19
|
import { computeMetrics } from '../metrics.mjs';
|
|
15
20
|
import { nowUtc } from '../paths.mjs';
|
|
16
21
|
import { listProjects, remove, update } from '../registry.mjs';
|
|
17
|
-
import { loadRepo, loadRepoWithConfig } from '../repo.mjs';
|
|
22
|
+
import { loadRepo, loadRepoWithConfig, resolveChange } from '../repo.mjs';
|
|
18
23
|
import { parseYaml } from '../yaml.mjs';
|
|
19
24
|
|
|
20
25
|
// Serializes a loaded repo into the flat shape the UI consumes.
|
|
@@ -115,10 +120,12 @@ export function changeStatus(projects, { project, id, status, reason }) {
|
|
|
115
120
|
// the UI is bypassable.
|
|
116
121
|
let current;
|
|
117
122
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
current = change.frontmatter.status;
|
|
123
|
+
const { file } = resolveChange(proj.path, id);
|
|
124
|
+
current = parseChange(fs.readFileSync(file, 'utf8')).frontmatter.status;
|
|
121
125
|
} catch (e) {
|
|
126
|
+
if (/^No change with id /.test(e.message)) {
|
|
127
|
+
return { code: 404, body: { error: `no change with id "${id}"` } };
|
|
128
|
+
}
|
|
122
129
|
return { code: 400, body: { error: e.message } };
|
|
123
130
|
}
|
|
124
131
|
try {
|
|
@@ -128,11 +135,14 @@ export function changeStatus(projects, { project, id, status, reason }) {
|
|
|
128
135
|
applyValidation(id, 'pass', {}, proj.path);
|
|
129
136
|
} else if (current === 'in-validation' && status === 'in-progress') {
|
|
130
137
|
applyValidation(id, 'fail', { reason }, proj.path);
|
|
138
|
+
} else if (current === 'done' && status === 'in-progress') {
|
|
139
|
+
applyReopen(id, reason, proj.path);
|
|
131
140
|
} else {
|
|
132
141
|
return {
|
|
133
142
|
code: 403,
|
|
134
143
|
body: {
|
|
135
|
-
error:
|
|
144
|
+
error:
|
|
145
|
+
'the viewer only allows draft → approved, in-validation → done|in-progress, and eligible done → in-progress',
|
|
136
146
|
},
|
|
137
147
|
};
|
|
138
148
|
}
|
|
@@ -2,6 +2,8 @@ export const VIEWER_STATE_KEY = 'changeledger.viewer-state.v1';
|
|
|
2
2
|
|
|
3
3
|
const VALID_VIEWS = new Set(['board', 'table', 'graph', 'specs', 'metrics', 'projects']);
|
|
4
4
|
const VALID_SORT_KEYS = new Set(['id', 'title', 'type', 'status', 'progress', 'deps']);
|
|
5
|
+
const VALID_DETAIL_MODES = new Set(['side', 'floating']);
|
|
6
|
+
const VALID_DETAIL_SIZES = new Set(['compact', 'wide', 'full']);
|
|
5
7
|
let storage = null;
|
|
6
8
|
|
|
7
9
|
const emptyProjectFilters = () => ({
|
|
@@ -31,6 +33,8 @@ export const state = {
|
|
|
31
33
|
projectsList: [],
|
|
32
34
|
localOnly: false,
|
|
33
35
|
globalMode: false,
|
|
36
|
+
detailMode: 'side',
|
|
37
|
+
detailSize: 'wide',
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
function currentProjectFilters() {
|
|
@@ -71,6 +75,8 @@ export function serializeViewerState() {
|
|
|
71
75
|
sortKey: state.sortKey,
|
|
72
76
|
sortDir: state.sortDir,
|
|
73
77
|
projects: state.projectFilters,
|
|
78
|
+
detailMode: state.detailMode,
|
|
79
|
+
detailSize: state.detailSize,
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
|
|
@@ -99,6 +105,8 @@ export function restoreViewerState(storageLike) {
|
|
|
99
105
|
if (typeof snapshot.text === 'string') state.filters.text = snapshot.text;
|
|
100
106
|
if (typeof snapshot.sortKey === 'string') state.sortKey = snapshot.sortKey;
|
|
101
107
|
if (snapshot.sortDir === 1 || snapshot.sortDir === -1) state.sortDir = snapshot.sortDir;
|
|
108
|
+
state.detailMode = VALID_DETAIL_MODES.has(snapshot.detailMode) ? snapshot.detailMode : 'side';
|
|
109
|
+
state.detailSize = VALID_DETAIL_SIZES.has(snapshot.detailSize) ? snapshot.detailSize : 'wide';
|
|
102
110
|
if (
|
|
103
111
|
snapshot.projects &&
|
|
104
112
|
typeof snapshot.projects === 'object' &&
|
|
@@ -224,3 +232,10 @@ export function toggleGlobalMode() {
|
|
|
224
232
|
persistViewerState();
|
|
225
233
|
return state.globalMode;
|
|
226
234
|
}
|
|
235
|
+
|
|
236
|
+
export function setDetailPresentation(mode, size) {
|
|
237
|
+
if (VALID_DETAIL_MODES.has(mode)) state.detailMode = mode;
|
|
238
|
+
if (VALID_DETAIL_SIZES.has(size)) state.detailSize = size;
|
|
239
|
+
persistViewerState();
|
|
240
|
+
return { mode: state.detailMode, size: state.detailSize };
|
|
241
|
+
}
|
package/src/viewer/public/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
normalizeRepoState,
|
|
20
20
|
restoreViewerState,
|
|
21
21
|
selectProject,
|
|
22
|
+
setDetailPresentation,
|
|
22
23
|
setOwnerFilter,
|
|
23
24
|
setRepo,
|
|
24
25
|
setSortKey,
|
|
@@ -36,7 +37,7 @@ import { boardStatuses, isVisible, passesTombstones } from './state.js';
|
|
|
36
37
|
import { html, render as litRender, nothing } from './templates.js';
|
|
37
38
|
import {
|
|
38
39
|
card,
|
|
39
|
-
|
|
40
|
+
detailToolbar,
|
|
40
41
|
sortIndicator,
|
|
41
42
|
specBody,
|
|
42
43
|
stageBlock,
|
|
@@ -50,6 +51,8 @@ export { cssIdent, esc, makeMermaidExpandable, safeHtml } from './security.js';
|
|
|
50
51
|
export { boardStatuses, isVisible, passesTombstones } from './state.js';
|
|
51
52
|
export {
|
|
52
53
|
card,
|
|
54
|
+
detailPresentationControls,
|
|
55
|
+
detailToolbar,
|
|
53
56
|
sortIndicator,
|
|
54
57
|
stageBlock,
|
|
55
58
|
statusSummary,
|
|
@@ -312,6 +315,73 @@ export async function runValidationSubmission({ root, request, onSuccess }) {
|
|
|
312
315
|
return true;
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
export function reopenPanel(status) {
|
|
319
|
+
if (status !== 'done') return nothing;
|
|
320
|
+
return html`<section class="validation-actions" aria-labelledby="reopen-title">
|
|
321
|
+
<div class="validation-copy">
|
|
322
|
+
<span class="validation-kicker">Lifecycle correction</span>
|
|
323
|
+
<h2 id="reopen-title">Reopen completed change</h2>
|
|
324
|
+
<p>Return this change to active work while preserving why its completion was reconsidered.</p>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="reopen-controls">
|
|
327
|
+
<div class="rejection-field">
|
|
328
|
+
<label for="reopen-reason">Reason for reopening</label>
|
|
329
|
+
<input
|
|
330
|
+
id="reopen-reason"
|
|
331
|
+
data-reopen-reason
|
|
332
|
+
type="text"
|
|
333
|
+
placeholder="What requires more work?"
|
|
334
|
+
/>
|
|
335
|
+
<p class="validation-error" role="alert" hidden></p>
|
|
336
|
+
</div>
|
|
337
|
+
<button type="button" class="button button-danger" data-reopen>Reopen change</button>
|
|
338
|
+
</div>
|
|
339
|
+
</section>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function bindReopenAction({ root, request, onSuccess }) {
|
|
343
|
+
const button = root.querySelector('[data-reopen]');
|
|
344
|
+
if (!button) return;
|
|
345
|
+
button.onclick = async () => {
|
|
346
|
+
const input = root.querySelector('[data-reopen-reason]');
|
|
347
|
+
const reason = input?.value.trim();
|
|
348
|
+
if (!reason) {
|
|
349
|
+
showValidationError(root, 'A reopening reason is required.');
|
|
350
|
+
input?.focus();
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return runValidationSubmission({
|
|
354
|
+
root,
|
|
355
|
+
request: () => request(reason),
|
|
356
|
+
onSuccess,
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function applyDetailPresentation(root = document) {
|
|
362
|
+
const overlay = root.querySelector('#overlay');
|
|
363
|
+
const detail = root.querySelector('#detail');
|
|
364
|
+
if (!overlay || !detail) return;
|
|
365
|
+
overlay.dataset.detailMode = state.detailMode;
|
|
366
|
+
detail.dataset.detailSize = state.detailSize;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function bindDetailPresentation(root = document) {
|
|
370
|
+
root.querySelectorAll('[data-detail-setting]').forEach((button) => {
|
|
371
|
+
button.onclick = () => {
|
|
372
|
+
const setting = button.dataset.detailSetting;
|
|
373
|
+
setDetailPresentation(
|
|
374
|
+
setting === 'mode' ? button.dataset.detailValue : state.detailMode,
|
|
375
|
+
setting === 'size' ? button.dataset.detailValue : state.detailSize,
|
|
376
|
+
);
|
|
377
|
+
applyDetailPresentation(root);
|
|
378
|
+
root.querySelectorAll(`[data-detail-setting="${setting}"]`).forEach((option) => {
|
|
379
|
+
option.setAttribute('aria-pressed', String(option === button));
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
315
385
|
async function submitValidation(id, status, reason) {
|
|
316
386
|
const root = $('#detail');
|
|
317
387
|
await runValidationSubmission({
|
|
@@ -325,6 +395,28 @@ async function submitValidation(id, status, reason) {
|
|
|
325
395
|
});
|
|
326
396
|
}
|
|
327
397
|
|
|
398
|
+
export function resetDetailScroll(
|
|
399
|
+
detail,
|
|
400
|
+
schedule = globalThis.requestAnimationFrame?.bind(globalThis),
|
|
401
|
+
) {
|
|
402
|
+
const reset = () => {
|
|
403
|
+
detail.scrollTo?.({ top: 0, left: 0, behavior: 'instant' });
|
|
404
|
+
detail.scrollTop = 0;
|
|
405
|
+
};
|
|
406
|
+
reset();
|
|
407
|
+
schedule?.(reset);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function scrollToStage(stage) {
|
|
411
|
+
stage.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function renderOpenedDetail(content) {
|
|
415
|
+
const detail = $('#detail');
|
|
416
|
+
litRender(content, detail);
|
|
417
|
+
resetDetailScroll(detail);
|
|
418
|
+
}
|
|
419
|
+
|
|
328
420
|
function openDetail(id) {
|
|
329
421
|
const c = state.repo.changes.find((x) => String(x.id) === String(id));
|
|
330
422
|
if (!c) return;
|
|
@@ -334,14 +426,11 @@ function openDetail(id) {
|
|
|
334
426
|
? html`<span class="pill ext" data-extdep=${d} style="cursor:pointer">depends on ${d}</span>`
|
|
335
427
|
: html`<span class="pill" data-dep=${d} style="cursor:pointer">depends on #${d}</span>`;
|
|
336
428
|
});
|
|
337
|
-
const pipeline = c.stages.map(
|
|
338
|
-
(s) => html`<span class="stage-chip" data-go=${`stage-${s.key}`}>${s.heading}</span>`,
|
|
339
|
-
);
|
|
340
429
|
const stages = c.stages.map((s) => stageBlock(c, s));
|
|
341
430
|
|
|
342
|
-
|
|
431
|
+
renderOpenedDetail(
|
|
343
432
|
html`
|
|
344
|
-
${
|
|
433
|
+
${detailToolbar(state.detailMode, state.detailSize, c.stages)}
|
|
345
434
|
<h1>${c.title}</h1>
|
|
346
435
|
<div class="detail-meta">
|
|
347
436
|
<span class="pill">#${c.id}</span>
|
|
@@ -352,16 +441,18 @@ function openDetail(id) {
|
|
|
352
441
|
${deps}
|
|
353
442
|
</div>
|
|
354
443
|
${c.status === 'in-validation' ? validationPanel() : nothing}
|
|
355
|
-
|
|
444
|
+
${reopenPanel(c.status)}
|
|
356
445
|
${stages}
|
|
357
446
|
<div id="git-section"></div>`,
|
|
358
|
-
$('#detail'),
|
|
359
447
|
);
|
|
360
448
|
|
|
361
449
|
resetValidationState($('#detail'));
|
|
362
450
|
|
|
363
451
|
const overlay = $('#overlay');
|
|
364
452
|
overlay.classList.remove('hidden');
|
|
453
|
+
document.documentElement.classList.add('detail-open');
|
|
454
|
+
applyDetailPresentation();
|
|
455
|
+
bindDetailPresentation();
|
|
365
456
|
$('#detail').querySelector('.close').onclick = closeDetail;
|
|
366
457
|
const accept = $('#detail').querySelector('[data-validation="pass"]');
|
|
367
458
|
if (accept) accept.onclick = () => submitValidation(c.id, 'done');
|
|
@@ -378,14 +469,22 @@ function openDetail(id) {
|
|
|
378
469
|
submitValidation(c.id, 'in-progress', reason);
|
|
379
470
|
};
|
|
380
471
|
}
|
|
472
|
+
bindReopenAction({
|
|
473
|
+
root: $('#detail'),
|
|
474
|
+
request: (reason) => postStatus(state.currentProject, c.id, 'in-progress', reason),
|
|
475
|
+
onSuccess: async () => {
|
|
476
|
+
invalidateCache();
|
|
477
|
+
await load();
|
|
478
|
+
openDetail(c.id);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
381
481
|
overlay.onclick = (e) => {
|
|
382
482
|
if (e.target === overlay) closeDetail();
|
|
383
483
|
};
|
|
384
484
|
$('#detail')
|
|
385
485
|
.querySelectorAll('[data-go]')
|
|
386
486
|
.forEach((el) => {
|
|
387
|
-
el.onclick = () =>
|
|
388
|
-
$(`#${el.dataset.go}`).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
487
|
+
el.onclick = () => scrollToStage($(`#${el.dataset.go}`));
|
|
389
488
|
});
|
|
390
489
|
$('#detail')
|
|
391
490
|
.querySelectorAll('[data-dep]')
|
|
@@ -441,6 +540,7 @@ async function loadGitRefs(id) {
|
|
|
441
540
|
|
|
442
541
|
function closeDetail() {
|
|
443
542
|
$('#overlay').classList.add('hidden');
|
|
543
|
+
document.documentElement.classList.remove('detail-open');
|
|
444
544
|
}
|
|
445
545
|
|
|
446
546
|
let diagramLightbox = null;
|
|
@@ -584,9 +684,9 @@ function renderSpecs() {
|
|
|
584
684
|
}
|
|
585
685
|
|
|
586
686
|
function openSpec(s) {
|
|
587
|
-
|
|
687
|
+
renderOpenedDetail(
|
|
588
688
|
html`
|
|
589
|
-
${
|
|
689
|
+
${detailToolbar(state.detailMode, state.detailSize)}
|
|
590
690
|
<h1>${s.title}</h1>
|
|
591
691
|
<div class="detail-meta">
|
|
592
692
|
<span class="pill">spec</span>
|
|
@@ -594,10 +694,12 @@ function openSpec(s) {
|
|
|
594
694
|
${(s.tags || []).map((t) => html`<span class="pill">${t}</span>`)}
|
|
595
695
|
</div>
|
|
596
696
|
${specBody(s.body)}`,
|
|
597
|
-
$('#detail'),
|
|
598
697
|
);
|
|
599
698
|
const overlay = $('#overlay');
|
|
600
699
|
overlay.classList.remove('hidden');
|
|
700
|
+
document.documentElement.classList.add('detail-open');
|
|
701
|
+
applyDetailPresentation();
|
|
702
|
+
bindDetailPresentation();
|
|
601
703
|
const detail = $('#detail');
|
|
602
704
|
detail.querySelector('.close').onclick = closeDetail;
|
|
603
705
|
overlay.onclick = (e) => {
|
|
@@ -23,12 +23,34 @@
|
|
|
23
23
|
--status-discarded: #9aa0aa;
|
|
24
24
|
--status-muted: #8b929e;
|
|
25
25
|
font-family: "Avenir Next", Avenir, ui-sans-serif, sans-serif;
|
|
26
|
+
color-scheme: dark;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
* {
|
|
29
30
|
box-sizing: border-box;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
html,
|
|
34
|
+
body {
|
|
35
|
+
scrollbar-width: thin;
|
|
36
|
+
scrollbar-color: #3a414e transparent;
|
|
37
|
+
}
|
|
38
|
+
html::-webkit-scrollbar,
|
|
39
|
+
body::-webkit-scrollbar {
|
|
40
|
+
width: 8px;
|
|
41
|
+
height: 8px;
|
|
42
|
+
}
|
|
43
|
+
html::-webkit-scrollbar-track,
|
|
44
|
+
body::-webkit-scrollbar-track {
|
|
45
|
+
background: transparent;
|
|
46
|
+
}
|
|
47
|
+
html::-webkit-scrollbar-thumb,
|
|
48
|
+
body::-webkit-scrollbar-thumb {
|
|
49
|
+
background: #3a414e;
|
|
50
|
+
border: 2px solid var(--bg);
|
|
51
|
+
border-radius: 999px;
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
body {
|
|
33
55
|
margin: 0;
|
|
34
56
|
background: var(--bg);
|
|
@@ -527,16 +549,31 @@ body {
|
|
|
527
549
|
padding: 16px;
|
|
528
550
|
overflow-x: auto;
|
|
529
551
|
align-items: flex-start;
|
|
552
|
+
}
|
|
553
|
+
.board,
|
|
554
|
+
.detail,
|
|
555
|
+
.detail pre,
|
|
556
|
+
.detail table {
|
|
530
557
|
scrollbar-width: thin;
|
|
531
558
|
scrollbar-color: #3a414e transparent;
|
|
532
559
|
}
|
|
533
|
-
.board::-webkit-scrollbar
|
|
560
|
+
.board::-webkit-scrollbar,
|
|
561
|
+
.detail::-webkit-scrollbar,
|
|
562
|
+
.detail pre::-webkit-scrollbar,
|
|
563
|
+
.detail table::-webkit-scrollbar {
|
|
564
|
+
width: 8px;
|
|
534
565
|
height: 8px;
|
|
535
566
|
}
|
|
536
|
-
.board::-webkit-scrollbar-track
|
|
567
|
+
.board::-webkit-scrollbar-track,
|
|
568
|
+
.detail::-webkit-scrollbar-track,
|
|
569
|
+
.detail pre::-webkit-scrollbar-track,
|
|
570
|
+
.detail table::-webkit-scrollbar-track {
|
|
537
571
|
background: transparent;
|
|
538
572
|
}
|
|
539
|
-
.board::-webkit-scrollbar-thumb
|
|
573
|
+
.board::-webkit-scrollbar-thumb,
|
|
574
|
+
.detail::-webkit-scrollbar-thumb,
|
|
575
|
+
.detail pre::-webkit-scrollbar-thumb,
|
|
576
|
+
.detail table::-webkit-scrollbar-thumb {
|
|
540
577
|
background: #3a414e;
|
|
541
578
|
border: 2px solid var(--bg);
|
|
542
579
|
border-radius: 999px;
|
|
@@ -545,10 +582,10 @@ body {
|
|
|
545
582
|
background: var(--panel);
|
|
546
583
|
border: 1px solid var(--line);
|
|
547
584
|
border-radius: 12px;
|
|
548
|
-
width: clamp(
|
|
549
|
-
min-width:
|
|
550
|
-
max-width:
|
|
551
|
-
flex: 0 0 clamp(
|
|
585
|
+
width: clamp(190px, calc((100vw - 140px) / 6), 400px);
|
|
586
|
+
min-width: 190px;
|
|
587
|
+
max-width: 400px;
|
|
588
|
+
flex: 0 0 clamp(190px, calc((100vw - 140px) / 6), 400px);
|
|
552
589
|
}
|
|
553
590
|
.column-head {
|
|
554
591
|
display: flex;
|
|
@@ -593,6 +630,8 @@ body {
|
|
|
593
630
|
font-family: ui-monospace, monospace;
|
|
594
631
|
color: var(--muted);
|
|
595
632
|
font-size: 12px;
|
|
633
|
+
min-width: 0;
|
|
634
|
+
overflow-wrap: anywhere;
|
|
596
635
|
}
|
|
597
636
|
.type-tag {
|
|
598
637
|
font-size: 10px;
|
|
@@ -625,6 +664,7 @@ body {
|
|
|
625
664
|
.card-title {
|
|
626
665
|
font-weight: 600;
|
|
627
666
|
line-height: 1.3;
|
|
667
|
+
overflow-wrap: anywhere;
|
|
628
668
|
}
|
|
629
669
|
.progress {
|
|
630
670
|
margin-top: 9px;
|
|
@@ -640,6 +680,7 @@ body {
|
|
|
640
680
|
}
|
|
641
681
|
.card-meta {
|
|
642
682
|
display: flex;
|
|
683
|
+
flex-wrap: wrap;
|
|
643
684
|
gap: 10px;
|
|
644
685
|
margin-top: 7px;
|
|
645
686
|
font-size: 11px;
|
|
@@ -689,6 +730,13 @@ body {
|
|
|
689
730
|
gap: 10px;
|
|
690
731
|
margin-top: 15px;
|
|
691
732
|
}
|
|
733
|
+
.reopen-controls {
|
|
734
|
+
display: grid;
|
|
735
|
+
grid-template-columns: minmax(190px, 1fr) auto;
|
|
736
|
+
align-items: end;
|
|
737
|
+
gap: 10px;
|
|
738
|
+
margin-top: 15px;
|
|
739
|
+
}
|
|
692
740
|
.rejection-field {
|
|
693
741
|
display: flex;
|
|
694
742
|
flex-direction: column;
|
|
@@ -750,6 +798,7 @@ body {
|
|
|
750
798
|
}
|
|
751
799
|
.owner {
|
|
752
800
|
font-weight: 600;
|
|
801
|
+
overflow-wrap: anywhere;
|
|
753
802
|
}
|
|
754
803
|
.card.archived {
|
|
755
804
|
opacity: 0.55;
|
|
@@ -847,21 +896,107 @@ body {
|
|
|
847
896
|
}
|
|
848
897
|
|
|
849
898
|
/* Overlay + detail */
|
|
899
|
+
.detail-open {
|
|
900
|
+
overflow: hidden;
|
|
901
|
+
}
|
|
850
902
|
.overlay {
|
|
851
903
|
position: fixed;
|
|
852
904
|
inset: 0;
|
|
853
905
|
background: rgba(0, 0, 0, 0.55);
|
|
854
906
|
display: flex;
|
|
855
907
|
justify-content: flex-end;
|
|
908
|
+
align-items: stretch;
|
|
856
909
|
z-index: 20;
|
|
857
910
|
}
|
|
858
911
|
.detail {
|
|
859
|
-
width:
|
|
912
|
+
--detail-width: 960px;
|
|
913
|
+
container-type: inline-size;
|
|
914
|
+
position: relative;
|
|
915
|
+
width: min(var(--detail-width), calc(100vw - 48px));
|
|
860
916
|
height: 100%;
|
|
861
917
|
background: var(--bg);
|
|
862
918
|
border-left: 1px solid var(--line);
|
|
863
919
|
overflow-y: auto;
|
|
920
|
+
overflow-anchor: none;
|
|
864
921
|
padding: 22px 26px 60px;
|
|
922
|
+
box-shadow: -24px 0 70px rgba(0, 0, 0, 0.28);
|
|
923
|
+
}
|
|
924
|
+
.detail[data-detail-size="compact"] {
|
|
925
|
+
--detail-width: 720px;
|
|
926
|
+
}
|
|
927
|
+
.detail[data-detail-size="wide"] {
|
|
928
|
+
--detail-width: 960px;
|
|
929
|
+
}
|
|
930
|
+
.detail[data-detail-size="full"] {
|
|
931
|
+
--detail-width: 1280px;
|
|
932
|
+
}
|
|
933
|
+
.overlay[data-detail-mode="floating"] {
|
|
934
|
+
align-items: center;
|
|
935
|
+
justify-content: center;
|
|
936
|
+
padding: 28px;
|
|
937
|
+
}
|
|
938
|
+
.overlay[data-detail-mode="floating"] .detail {
|
|
939
|
+
width: min(var(--detail-width), calc(100vw - 56px));
|
|
940
|
+
height: min(92vh, calc(100vh - 56px));
|
|
941
|
+
border: 1px solid var(--line);
|
|
942
|
+
border-radius: 16px;
|
|
943
|
+
box-shadow: 0 28px 90px rgba(0, 0, 0, 0.48);
|
|
944
|
+
}
|
|
945
|
+
.detail-toolbar {
|
|
946
|
+
position: sticky;
|
|
947
|
+
top: -22px;
|
|
948
|
+
z-index: 3;
|
|
949
|
+
display: grid;
|
|
950
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
951
|
+
align-items: center;
|
|
952
|
+
gap: 12px;
|
|
953
|
+
margin: -22px -26px 20px;
|
|
954
|
+
padding: 14px 26px 10px;
|
|
955
|
+
border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent);
|
|
956
|
+
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
|
957
|
+
backdrop-filter: blur(14px);
|
|
958
|
+
}
|
|
959
|
+
.detail-presentation {
|
|
960
|
+
display: flex;
|
|
961
|
+
flex-wrap: wrap;
|
|
962
|
+
gap: 8px;
|
|
963
|
+
width: fit-content;
|
|
964
|
+
}
|
|
965
|
+
.detail-choice {
|
|
966
|
+
display: inline-flex;
|
|
967
|
+
padding: 3px;
|
|
968
|
+
border: 1px solid var(--line);
|
|
969
|
+
border-radius: 9px;
|
|
970
|
+
background: var(--panel);
|
|
971
|
+
}
|
|
972
|
+
.detail-option {
|
|
973
|
+
min-height: 28px;
|
|
974
|
+
padding: 4px 9px;
|
|
975
|
+
border: 0;
|
|
976
|
+
border-radius: 6px;
|
|
977
|
+
background: transparent;
|
|
978
|
+
color: var(--muted);
|
|
979
|
+
font: inherit;
|
|
980
|
+
font-size: 11px;
|
|
981
|
+
cursor: pointer;
|
|
982
|
+
}
|
|
983
|
+
.detail-option[aria-pressed="true"] {
|
|
984
|
+
background: var(--panel-2);
|
|
985
|
+
color: var(--text);
|
|
986
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 42%, var(--line));
|
|
987
|
+
}
|
|
988
|
+
.detail-option:focus-visible {
|
|
989
|
+
outline: 2px solid var(--accent);
|
|
990
|
+
outline-offset: 2px;
|
|
991
|
+
}
|
|
992
|
+
.detail pre {
|
|
993
|
+
max-width: 100%;
|
|
994
|
+
overflow-x: auto;
|
|
995
|
+
}
|
|
996
|
+
.detail table {
|
|
997
|
+
display: block;
|
|
998
|
+
max-width: 100%;
|
|
999
|
+
overflow-x: auto;
|
|
865
1000
|
}
|
|
866
1001
|
.detail h1 {
|
|
867
1002
|
font-size: 22px;
|
|
@@ -909,19 +1044,27 @@ body {
|
|
|
909
1044
|
outline-offset: 3px;
|
|
910
1045
|
}
|
|
911
1046
|
.close {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1047
|
+
justify-self: end;
|
|
1048
|
+
}
|
|
1049
|
+
.detail-toolbar-close {
|
|
1050
|
+
grid-column: 3;
|
|
915
1051
|
}
|
|
916
1052
|
|
|
917
1053
|
.pipeline {
|
|
918
1054
|
display: flex;
|
|
919
|
-
|
|
1055
|
+
min-width: 0;
|
|
1056
|
+
flex-wrap: nowrap;
|
|
920
1057
|
gap: 6px;
|
|
921
|
-
margin:
|
|
1058
|
+
margin: 0;
|
|
1059
|
+
padding: 3px 2px;
|
|
1060
|
+
overflow-x: auto;
|
|
1061
|
+
overscroll-behavior-x: contain;
|
|
1062
|
+
scrollbar-width: thin;
|
|
922
1063
|
}
|
|
923
1064
|
.stage-chip {
|
|
1065
|
+
flex: 0 0 auto;
|
|
924
1066
|
font-size: 12px;
|
|
1067
|
+
font-family: inherit;
|
|
925
1068
|
padding: 5px 11px;
|
|
926
1069
|
border-radius: 999px;
|
|
927
1070
|
background: var(--panel);
|
|
@@ -937,6 +1080,7 @@ body {
|
|
|
937
1080
|
.stage {
|
|
938
1081
|
border-top: 1px solid var(--line);
|
|
939
1082
|
padding-top: 14px;
|
|
1083
|
+
scroll-margin-top: 72px;
|
|
940
1084
|
margin-top: 18px;
|
|
941
1085
|
}
|
|
942
1086
|
.stage h2 {
|
|
@@ -1235,7 +1379,8 @@ body {
|
|
|
1235
1379
|
width: 100%;
|
|
1236
1380
|
flex: 0 0 auto;
|
|
1237
1381
|
}
|
|
1238
|
-
.validation-controls
|
|
1382
|
+
.validation-controls,
|
|
1383
|
+
.reopen-controls {
|
|
1239
1384
|
grid-template-columns: 1fr;
|
|
1240
1385
|
align-items: stretch;
|
|
1241
1386
|
}
|
|
@@ -1255,7 +1400,32 @@ body {
|
|
|
1255
1400
|
}
|
|
1256
1401
|
.detail {
|
|
1257
1402
|
width: 100vw;
|
|
1403
|
+
max-width: none;
|
|
1404
|
+
height: 100vh;
|
|
1405
|
+
max-height: none;
|
|
1258
1406
|
padding: 18px 16px 60px;
|
|
1407
|
+
border: 0;
|
|
1408
|
+
border-radius: 0;
|
|
1409
|
+
}
|
|
1410
|
+
.overlay[data-detail-mode] {
|
|
1411
|
+
align-items: stretch;
|
|
1412
|
+
justify-content: stretch;
|
|
1413
|
+
padding: 0;
|
|
1414
|
+
}
|
|
1415
|
+
.overlay[data-detail-mode] .detail {
|
|
1416
|
+
width: 100vw;
|
|
1417
|
+
height: 100vh;
|
|
1418
|
+
max-height: none;
|
|
1419
|
+
border: 0;
|
|
1420
|
+
border-radius: 0;
|
|
1421
|
+
}
|
|
1422
|
+
.detail-toolbar {
|
|
1423
|
+
top: -18px;
|
|
1424
|
+
margin: -18px -16px 16px;
|
|
1425
|
+
padding: 10px 16px 8px;
|
|
1426
|
+
}
|
|
1427
|
+
.detail-option {
|
|
1428
|
+
padding-inline: 7px;
|
|
1259
1429
|
}
|
|
1260
1430
|
.grid th,
|
|
1261
1431
|
.grid td {
|
|
@@ -1263,6 +1433,27 @@ body {
|
|
|
1263
1433
|
}
|
|
1264
1434
|
}
|
|
1265
1435
|
|
|
1436
|
+
/* Detail toolbar stacks its stage navigation onto its own row whenever the
|
|
1437
|
+
detail panel itself lacks room, independent of the surrounding viewport —
|
|
1438
|
+
this also covers the "Compact" width preset, not just narrow screens. */
|
|
1439
|
+
@container (max-width: 680px) {
|
|
1440
|
+
.detail-toolbar {
|
|
1441
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1442
|
+
gap: 8px;
|
|
1443
|
+
}
|
|
1444
|
+
.detail-toolbar-close {
|
|
1445
|
+
grid-column: 2;
|
|
1446
|
+
grid-row: 1;
|
|
1447
|
+
}
|
|
1448
|
+
.pipeline {
|
|
1449
|
+
grid-column: 1 / -1;
|
|
1450
|
+
grid-row: 2;
|
|
1451
|
+
}
|
|
1452
|
+
.stage {
|
|
1453
|
+
scroll-margin-top: 126px;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1266
1457
|
/* config editor tabs and form (113924) */
|
|
1267
1458
|
.config-section {
|
|
1268
1459
|
border-bottom: 1px solid var(--line);
|
|
@@ -36,6 +36,58 @@ export function closeButton(label = 'Close detail', extraClass = '') {
|
|
|
36
36
|
</button>`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function detailPresentationControls(mode = 'side', size = 'wide') {
|
|
40
|
+
const options = (values, selected, attr) =>
|
|
41
|
+
values.map(
|
|
42
|
+
([value, label]) => html`<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="detail-option"
|
|
45
|
+
data-detail-setting=${attr}
|
|
46
|
+
data-detail-value=${value}
|
|
47
|
+
aria-pressed=${String(selected === value)}
|
|
48
|
+
>${label}</button>`,
|
|
49
|
+
);
|
|
50
|
+
return html`<div class="detail-presentation" aria-label="Detail presentation">
|
|
51
|
+
<div class="detail-choice" role="group" aria-label="Layout">
|
|
52
|
+
${options(
|
|
53
|
+
[
|
|
54
|
+
['side', 'Side panel'],
|
|
55
|
+
['floating', 'Floating modal'],
|
|
56
|
+
],
|
|
57
|
+
mode,
|
|
58
|
+
'mode',
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
<div class="detail-choice" role="group" aria-label="Width">
|
|
62
|
+
${options(
|
|
63
|
+
[
|
|
64
|
+
['compact', 'Compact'],
|
|
65
|
+
['wide', 'Wide'],
|
|
66
|
+
['full', 'Full'],
|
|
67
|
+
],
|
|
68
|
+
size,
|
|
69
|
+
'size',
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function detailToolbar(mode = 'side', size = 'wide', stages = []) {
|
|
76
|
+
const navigation = stages.length
|
|
77
|
+
? html`<nav class="pipeline" aria-label="Change sections">
|
|
78
|
+
${stages.map(
|
|
79
|
+
(stage) =>
|
|
80
|
+
html`<button type="button" class="stage-chip" data-go=${`stage-${stage.key}`}>${stage.heading}</button>`,
|
|
81
|
+
)}
|
|
82
|
+
</nav>`
|
|
83
|
+
: nothing;
|
|
84
|
+
return html`<div class="detail-toolbar" aria-label="Detail tools">
|
|
85
|
+
${detailPresentationControls(mode, size)}
|
|
86
|
+
${navigation}
|
|
87
|
+
${closeButton('Close detail', 'detail-toolbar-close')}
|
|
88
|
+
</div>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
export function validationPanel() {
|
|
40
92
|
return html`<section class="validation-actions" aria-labelledby="validation-title">
|
|
41
93
|
<div class="validation-copy">
|
|
@@ -39,6 +39,12 @@ not necessarily that a spec was created.
|
|
|
39
39
|
The graduation link remains derivable from the Log marker `graduado a spec`,
|
|
40
40
|
which carries the spec link, rather than from the boolean flag.
|
|
41
41
|
|
|
42
|
+
After `--into` or `--skip`, create one final closure commit that coalesces any
|
|
43
|
+
pending `in-review → in-validation → done` ledger updates with the graduation
|
|
44
|
+
decision and durable spec edit. Do not create separate commits whose only
|
|
45
|
+
content is one of those transitions. If no lifecycle state is pending, the
|
|
46
|
+
graduation or skip itself remains the meaningful closure evidence.
|
|
47
|
+
|
|
42
48
|
Operational inspection and visibility:
|
|
43
49
|
|
|
44
50
|
- `changeledger list [--status S] [--type T] [--json]`
|
|
@@ -47,4 +53,4 @@ Operational inspection and visibility:
|
|
|
47
53
|
|
|
48
54
|
Use Mermaid where it communicates persistent relationships better than prose.
|
|
49
55
|
After closure, share a brief retrospective. New work needs a newly authorized
|
|
50
|
-
change;
|
|
56
|
+
change; graduated, skipped, archived or released work never reopens.
|
|
@@ -5,11 +5,16 @@ reflection. Work is planned and documented before code is written.
|
|
|
5
5
|
|
|
6
6
|
## Read complete context before acting
|
|
7
7
|
|
|
8
|
-
Running `changeledger context` is discovery, not compliance by itself.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
the
|
|
12
|
-
|
|
8
|
+
Running `changeledger context` is discovery, not compliance by itself. Capture the first invocation completely in one pass
|
|
9
|
+
and read through the `CHANGELEDGER CONTEXT END` line, then follow the current mode. Never request a preview, summary
|
|
10
|
+
or voluntary line, byte or token cap; if the tool exposes an output budget,
|
|
11
|
+
reserve enough for the whole response. A missing END after this deliberate full
|
|
12
|
+
capture is exceptional recovery: stop and re-run with a larger capture before
|
|
13
|
+
planning or acting on the partial output.
|
|
14
|
+
|
|
15
|
+
While the complete core remains available in the active conversation, a new
|
|
16
|
+
human message alone does not trigger a reload. Load only the specialized mode or
|
|
17
|
+
change-id context required by a real task or lifecycle transition.
|
|
13
18
|
|
|
14
19
|
1. Work starts with conversation. Read-only investigation may clarify a request,
|
|
15
20
|
but create no change or implementation artifact until there is enough clarity
|
|
@@ -19,12 +24,12 @@ files.
|
|
|
19
24
|
agent decides how to divide and execute work within that authorized scope.
|
|
20
25
|
3. Capture every authorized change in `.changeledger/changes/`. The document
|
|
21
26
|
wins when code and documentation disagree.
|
|
22
|
-
4. Never implement a `draft`. After approval,
|
|
23
|
-
non-main branch and commit the approved change document before code.
|
|
27
|
+
4. Never implement a `draft`. After approval, implement one change at a time on
|
|
28
|
+
a non-main branch and commit the approved change document before code.
|
|
24
29
|
5. Keep lifecycle, tasks, ownership and Log current while working.
|
|
25
30
|
6. For types that require review, use a fresh clean-context reviewer before
|
|
26
31
|
human validation.
|
|
27
|
-
7.
|
|
32
|
+
7. `in-validation` stops only that change; the agent never accepts on the human's behalf, but may start another approved change unless it or its `depends_on` chain (direct or transitive) reaches an `in-validation` change.
|
|
28
33
|
8. After human acceptance, reload `changeledger context <id>` for the `done`
|
|
29
34
|
change, then graduate persistent truth (a new spec is a two-step `--new`
|
|
30
35
|
then `--into`) or run `changeledger graduate <id> --skip [reason]`; archive
|
|
@@ -59,6 +64,7 @@ in-progress → in-validation → done [no review required]
|
|
|
59
64
|
in-review → in-progress [review retry]
|
|
60
65
|
in-review → blocked → in-progress [review escalation]
|
|
61
66
|
in-validation → in-progress [human rejection]
|
|
67
|
+
done → in-progress [human reopen before durable closure]
|
|
62
68
|
(draft | approved | in-progress | blocked) → discarded
|
|
63
69
|
```
|
|
64
70
|
|
|
@@ -68,15 +74,16 @@ in-validation → in-progress [human rejection]
|
|
|
68
74
|
- `in-review`: independent review required.
|
|
69
75
|
- `in-validation`: stop and wait for human acceptance or rejection.
|
|
70
76
|
- `blocked`: an impediment or decision needs resolution.
|
|
71
|
-
- `done`:
|
|
77
|
+
- `done`: the human accepted the complete result; provisional until durable closure.
|
|
72
78
|
- `discarded`: terminal tombstone; never reopen it.
|
|
73
79
|
|
|
74
80
|
`changeledger status <id> <status>` enforces agent-owned transitions and does not accept `done` or `discarded`.
|
|
75
|
-
The viewer owns `draft → approved` and `in-validation → done|in-progress
|
|
76
|
-
agent performs
|
|
81
|
+
The viewer owns `draft → approved` and `in-validation → done|in-progress`, plus
|
|
82
|
+
eligible `done → in-progress` with a reason; the agent performs other moves. Use
|
|
77
83
|
`changeledger discard <id> "<reason>"`: the discard reason is required and
|
|
78
|
-
logged, and dependencies remain resolvable. `
|
|
79
|
-
|
|
84
|
+
logged, and dependencies remain resolvable. `discarded` never reopens. A `done`
|
|
85
|
+
change can reopen only to finish its original scope before graduation/skip,
|
|
86
|
+
archive or release; after durable closure, later work needs a new change.
|
|
80
87
|
|
|
81
88
|
## Context modes
|
|
82
89
|
|
|
@@ -21,7 +21,8 @@ owns, what it returns and how the result integrates.
|
|
|
21
21
|
- Verification may be delegated when it catches risk without merely repeating
|
|
22
22
|
the implementer's work.
|
|
23
23
|
- Configured review is special: a fresh clean-context subagent is a correctness
|
|
24
|
-
requirement, not an optimization
|
|
24
|
+
requirement, not an optimization — and read-only: it reports, the
|
|
25
|
+
orchestrator alone records the verdict.
|
|
25
26
|
|
|
26
27
|
## Do not over-shard
|
|
27
28
|
|
|
@@ -17,10 +17,17 @@ to a work branch or ask the human before continuing. Inspect the worktree first.
|
|
|
17
17
|
unrelated changes exist, do not include them silently; ask the human whether to
|
|
18
18
|
stash, commit, ignore or include them before changing the worktree.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
Implement one change at a time
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
After `approved → in-progress`, create a baseline commit of the approved change
|
|
21
|
+
document before code. Implement one change at a time, even while another
|
|
22
|
+
already-delivered change waits in `in-validation`.
|
|
23
|
+
|
|
24
|
+
Commit completed units with their tasks and Log when later work could obscure
|
|
25
|
+
attribution. Do not create a dedicated commit for a lifecycle-only transition.
|
|
26
|
+
Coalesce it with the nearest meaningful commit; for example, include
|
|
27
|
+
`in-progress → in-review` with the final implementation unit. Do not wait until
|
|
28
|
+
the end to reconstruct mixed diffs. If a handoff precedes the next
|
|
29
|
+
meaningful commit, one consolidated checkpoint persists pending state; record
|
|
30
|
+
why and never create one per transition.
|
|
24
31
|
|
|
25
32
|
Commit messages use the canonical shape:
|
|
26
33
|
|
|
@@ -44,25 +51,32 @@ Useful mutation commands:
|
|
|
44
51
|
- `changeledger task <id> done|block <n> [reason]`
|
|
45
52
|
- `changeledger log <id> "<message>"`
|
|
46
53
|
- `changeledger owner <id> <name|->`
|
|
54
|
+
- `changeledger review <id> pass|fail`
|
|
47
55
|
- `changeledger check [id]`
|
|
48
56
|
|
|
49
57
|
When implementation and every task are complete, move to `in-review` if the
|
|
50
|
-
type requires independent review
|
|
58
|
+
type requires independent review, loading `changeledger context review`
|
|
59
|
+
once first — it stays valid for the rest of the cycle, do not reload it just
|
|
60
|
+
to record the verdict unless context was lost (compaction, a new session) —
|
|
61
|
+
and recording the delegate's verdict yourself with `review <id> pass|fail`
|
|
62
|
+
(never `log`+`status`). Otherwise move to `in-validation` and stop — its
|
|
63
|
+
closing has no CLI, only the human does it.
|
|
51
64
|
|
|
52
65
|
## Correction isolation
|
|
53
66
|
|
|
54
67
|
After review `fail --retry`, keep the candidate correction uncommitted while a
|
|
55
68
|
fresh clean-context reviewer checks it. If it fails again, iterate on that same
|
|
56
69
|
diff. Do not start another task or change while a correction waits: the
|
|
57
|
-
worktree is its isolation boundary. After `pass`, commit the confirmed correction
|
|
58
|
-
|
|
70
|
+
worktree is its isolation boundary. After `pass`, commit the confirmed correction,
|
|
71
|
+
tests and ledger before human validation; this is meaningful correction evidence,
|
|
72
|
+
not a status-only commit.
|
|
59
73
|
|
|
60
74
|
After human rejection (`in-validation → in-progress`), run
|
|
61
75
|
`changeledger context <id>` before modifying implementation; keep the correction
|
|
62
76
|
uncommitted until the human confirms it fixes the reported failure. Do not start
|
|
63
77
|
another task or change while a correction waits; iterate on
|
|
64
|
-
the same diff if it does not. After human acceptance, graduate or record a skip
|
|
65
|
-
|
|
78
|
+
the same diff if it does not. After human acceptance, graduate or record a skip
|
|
79
|
+
and include correction plus ledger in the final closure commit.
|
|
66
80
|
|
|
67
81
|
These exceptions prevent false fix attempts from becoming permanent history;
|
|
68
82
|
they do not relax intermediate commits for already verified units.
|
|
@@ -21,6 +21,11 @@ Record exactly one verdict:
|
|
|
21
21
|
- `changeledger review <id> fail --block "<reason>"` — correction requires scope
|
|
22
22
|
or product judgment; move to `blocked` for the human.
|
|
23
23
|
|
|
24
|
+
A review verdict alone needs no commit. A pass leaves `in-validation` for
|
|
25
|
+
closure unless it confirms uncommitted correction; then correction, tests and
|
|
26
|
+
ledger form a commit. Retry keeps the diff isolated. Handoff may use the
|
|
27
|
+
implementation contract's checkpoint.
|
|
28
|
+
|
|
24
29
|
After `fail --retry`, the correction remains uncommitted until another fresh
|
|
25
30
|
reviewer passes it. After the transition, run `changeledger context <id>` before
|
|
26
31
|
modifying implementation. After pass, commit correction + ledger before asking
|
|
@@ -3,8 +3,22 @@
|
|
|
3
3
|
Implementation and required review are complete. Do not modify the result or
|
|
4
4
|
mark it done. Ask the human to test the whole change in the viewer.
|
|
5
5
|
|
|
6
|
+
This stop is scoped to this change: the agent may start the next approved
|
|
7
|
+
change unless its `depends_on` chain, direct or transitive, reaches this or
|
|
8
|
+
another `in-validation` change. If every remaining approved change is
|
|
9
|
+
blocked, it stops entirely and does not invent work or touch delivered
|
|
10
|
+
results.
|
|
11
|
+
|
|
6
12
|
Acceptance reaches `done`. Rejection requires a reason and returns the same
|
|
7
13
|
change to `in-progress`; run `changeledger context <id>` before modifying
|
|
8
14
|
implementation, update Specification/Plan as needed and repeat review when
|
|
9
|
-
configured. The agent never accepts on the human's behalf.
|
|
10
|
-
|
|
15
|
+
configured. The agent never accepts on the human's behalf. Before graduation,
|
|
16
|
+
skip, archive or release, the human may reopen `done` with a reason only to
|
|
17
|
+
complete the original authorized scope; broader behavior needs a new change.
|
|
18
|
+
`discarded` never reopens.
|
|
19
|
+
|
|
20
|
+
The validation transition alone does not require a dedicated commit. After
|
|
21
|
+
acceptance, resolve graduation or skip first; the close overlay then requires one
|
|
22
|
+
final closure commit containing the pending lifecycle Log and graduation truth.
|
|
23
|
+
After rejection, follow correction isolation instead of committing an
|
|
24
|
+
unconfirmed attempt.
|