changeledger 0.4.0 → 0.5.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 +13 -7
- package/README.md +22 -13
- package/bin/changeledger.mjs +10 -2
- package/package.json +1 -1
- package/src/commands/agent.mjs +1 -1
- package/src/commands/check.mjs +1 -1
- package/src/commands/context.mjs +65 -0
- package/src/commands/init.mjs +3 -6
- package/src/commands/new.mjs +1 -1
- package/src/commands/register.mjs +10 -6
- package/src/contract.mjs +86 -50
- package/src/paths.mjs +1 -1
- package/src/viewer/public/app.js +32 -2
- package/templates/config.yml +4 -3
- package/templates/contract/blocked.md +6 -0
- package/templates/contract/close.md +18 -0
- package/templates/contract/core.md +64 -0
- package/templates/contract/discarded.md +5 -0
- package/templates/contract/implement.md +52 -0
- package/templates/contract/readiness.md +19 -0
- package/templates/contract/release.md +15 -0
- package/templates/contract/review.md +19 -0
- package/templates/contract/spec.md +94 -0
- package/templates/contract/validation.md +9 -0
- package/templates/AGENTS.md +0 -540
package/AGENTS.md
CHANGED
|
@@ -5,15 +5,21 @@ under `.changeledger/changes/`, persistent truth under `.changeledger/specs/`.
|
|
|
5
5
|
|
|
6
6
|
<!-- changeledger -->
|
|
7
7
|
> [!IMPORTANT]
|
|
8
|
-
> This repo uses **ChangeLedger**.
|
|
9
|
-
>
|
|
8
|
+
> This repo uses **ChangeLedger**. Before creating or modifying files, run
|
|
9
|
+
> `changeledger context` (or `changeledger context <change-id>`) and follow its output.
|
|
10
|
+
> If the command is unavailable, stop and restore/install ChangeLedger; do not proceed from memory.
|
|
10
11
|
|
|
11
|
-
The
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
The canonical ChangeLedger contract is split into task-focused fragments under
|
|
13
|
+
[`templates/contract/`](templates/contract/). The deterministic
|
|
14
|
+
`changeledger context` command composes them; consuming repos keep only the
|
|
15
|
+
bootstrap above, not a linked or copied contract.
|
|
14
16
|
|
|
15
17
|
## Project-specific notes
|
|
16
18
|
|
|
19
|
+
- Product evolution follows the complexity budget and no-goals in
|
|
20
|
+
[`INTENT.md`](INTENT.md): prefer observed problems and a small deterministic,
|
|
21
|
+
local-first core; AI orchestration, memory and cloud services belong only in
|
|
22
|
+
optional integrations.
|
|
17
23
|
- Managed with **pnpm**; lint/format via **Biome**. Runtime dependencies are
|
|
18
24
|
allowed only when they are mature and justified: the CLI uses `yaml` for
|
|
19
25
|
config/frontmatter parsing, and the viewer uses `lit-html`, `marked`,
|
|
@@ -21,5 +27,5 @@ into each repo as `.changeledger/AGENTS.md`. Edit the convention there, not here
|
|
|
21
27
|
- `pnpm verify` (lint + test + `changeledger check`) is the full quality gate. The
|
|
22
28
|
versioned `hooks/pre-commit` runs `lint-staged`, `pnpm test` and `changeledger check`
|
|
23
29
|
so staged formatting stays compatible with partial commits.
|
|
24
|
-
-
|
|
25
|
-
|
|
30
|
+
- Edit the convention in `templates/contract/`; keep each rule in one fragment
|
|
31
|
+
so task contexts compose without duplicated truth.
|
package/README.md
CHANGED
|
@@ -27,9 +27,10 @@ changeledger init
|
|
|
27
27
|
changeledger view
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
`changeledger init` creates `.changeledger/`, gives the project a stable identity
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
`changeledger init` creates `.changeledger/`, gives the project a stable identity
|
|
31
|
+
and installs a small context bootstrap in the project-owned `AGENTS.md`. The
|
|
32
|
+
repository keeps only its configuration and documents; the CLI, viewer and
|
|
33
|
+
canonical contract fragments remain in the global package.
|
|
33
34
|
|
|
34
35
|
## The workflow
|
|
35
36
|
|
|
@@ -62,8 +63,8 @@ other tools discover it through the repository's `AGENTS.md` reference.
|
|
|
62
63
|
|
|
63
64
|
With the default Definition of Ready policy, `changeledger check` verifies that acceptance
|
|
64
65
|
criteria are test-grade and mapped to actionable tasks. Repositories doing
|
|
65
|
-
exploratory work can set `tdd: false`;
|
|
66
|
-
|
|
66
|
+
exploratory work can set `tdd: false`; run `changeledger context spec` or
|
|
67
|
+
`changeledger context implement` for the complete task-specific rules.
|
|
67
68
|
|
|
68
69
|
## Essential commands
|
|
69
70
|
|
|
@@ -71,7 +72,7 @@ exploratory work can set `tdd: false`; the complete rules live in
|
|
|
71
72
|
|
|
72
73
|
```sh
|
|
73
74
|
changeledger init # initialize and register the current repository
|
|
74
|
-
changeledger register #
|
|
75
|
+
changeledger register # refresh registration and the context bootstrap
|
|
75
76
|
changeledger view # view every registered project
|
|
76
77
|
changeledger view . # view only the current project
|
|
77
78
|
changeledger check [id] # validate the repository or one change
|
|
@@ -128,17 +129,25 @@ CI:
|
|
|
128
129
|
changeledger check || exit 1
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
The contract
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
The contract ships as task-focused fragments and is compiled on demand:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
changeledger context # minimal non-negotiable core
|
|
136
|
+
changeledger context <change-id> # lifecycle-aware rules + selected change
|
|
137
|
+
changeledger context review # explicit task mode
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`init` places a small fail-closed bootstrap in the project-owned `AGENTS.md`;
|
|
141
|
+
there is no linked or copied contract under `.changeledger/`. Run
|
|
142
|
+
`changeledger register` after upgrading to refresh that bootstrap and migrate
|
|
143
|
+
legacy repositories.
|
|
135
144
|
|
|
136
145
|
## Compatibility and security
|
|
137
146
|
|
|
138
147
|
- Node.js **24+**.
|
|
139
148
|
- Tested on Linux, macOS and Windows.
|
|
140
|
-
-
|
|
141
|
-
|
|
149
|
+
- No symlink privileges are required on Windows; context fragments remain in the
|
|
150
|
+
installed package and the CLI prints them on demand.
|
|
142
151
|
- The viewer binds to loopback and treats repository content as untrusted input.
|
|
143
152
|
See [`SECURITY.md`](SECURITY.md) for the threat model and private reporting
|
|
144
153
|
instructions.
|
|
@@ -151,7 +160,7 @@ latest `0.x` release to receive fixes.
|
|
|
151
160
|
|
|
152
161
|
Contributions are welcome. Development setup, quality gates and repository
|
|
153
162
|
conventions live in [`CONTRIBUTING.md`](CONTRIBUTING.md). The canonical agent
|
|
154
|
-
contract is [`
|
|
163
|
+
contract is composed from [`templates/contract/`](templates/contract/).
|
|
155
164
|
|
|
156
165
|
## Language policy
|
|
157
166
|
|
package/bin/changeledger.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
task,
|
|
14
14
|
} from '../src/commands/agent.mjs';
|
|
15
15
|
import { check } from '../src/commands/check.mjs';
|
|
16
|
+
import { context } from '../src/commands/context.mjs';
|
|
16
17
|
import { graduate, pendingGraduation, skipGraduation } from '../src/commands/graduate.mjs';
|
|
17
18
|
import { init } from '../src/commands/init.mjs';
|
|
18
19
|
import { newChange } from '../src/commands/new.mjs';
|
|
@@ -24,10 +25,11 @@ import { nowUtc } from '../src/paths.mjs';
|
|
|
24
25
|
const USAGE = `ChangeLedger (changeledger)
|
|
25
26
|
|
|
26
27
|
changeledger init set up .changeledger/ in the current repo (+ register it)
|
|
27
|
-
changeledger register
|
|
28
|
+
changeledger register refresh registration and context bootstrap
|
|
28
29
|
changeledger new <type> <slug> <title> scaffold a new change (slug is the English filename)
|
|
29
30
|
changeledger view [port] launch the local viewer (default port 4040)
|
|
30
31
|
changeledger check [id] [--json] validate the repo or one change
|
|
32
|
+
changeledger context [mode|change-id] print deterministic task context
|
|
31
33
|
changeledger status <id> <status> move a change's lifecycle status
|
|
32
34
|
changeledger discard <id> "<reason>" discard a change (terminal; keeps the record)
|
|
33
35
|
changeledger review <id> pass independent review passed → in-validation
|
|
@@ -78,7 +80,7 @@ program
|
|
|
78
80
|
|
|
79
81
|
program
|
|
80
82
|
.command('register')
|
|
81
|
-
.description(
|
|
83
|
+
.description('refresh registration and context bootstrap')
|
|
82
84
|
.action(
|
|
83
85
|
action(() => {
|
|
84
86
|
const { id, path: p } = registerRepo();
|
|
@@ -122,6 +124,12 @@ program
|
|
|
122
124
|
}
|
|
123
125
|
});
|
|
124
126
|
|
|
127
|
+
program
|
|
128
|
+
.command('context')
|
|
129
|
+
.description('print deterministic task context')
|
|
130
|
+
.argument('[mode-or-change-id]')
|
|
131
|
+
.action(action((input) => context(input)));
|
|
132
|
+
|
|
125
133
|
program
|
|
126
134
|
.command('status')
|
|
127
135
|
.description("move a change's lifecycle status")
|
package/package.json
CHANGED
package/src/commands/agent.mjs
CHANGED
|
@@ -59,7 +59,7 @@ export function status(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// Records the verdict of the independent review (run by a delegated subagent
|
|
62
|
-
// with clean context — see
|
|
62
|
+
// with clean context — see `changeledger context review`). `pass` advances to human validation;
|
|
63
63
|
// `fail` routes it back: `retry` for a defect inside the contract (the
|
|
64
64
|
// implementer fixes), `block` for one that escalates to a human. Requires the
|
|
65
65
|
// change to be in-review.
|
package/src/commands/check.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export function check(args = [], cwd = process.cwd(), output = console) {
|
|
|
22
22
|
|
|
23
23
|
const { errors, warnings } = checkRepo(repo, { id });
|
|
24
24
|
|
|
25
|
-
// Discovery validation needs the filesystem (root contract
|
|
25
|
+
// Discovery validation needs the filesystem (root contract bootstrap), so it
|
|
26
26
|
// lives here, not in the pure validator. Repo-wide only.
|
|
27
27
|
if (!id) {
|
|
28
28
|
for (const message of checkContract(repo.repoRoot, repo.changeledgerDir)) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseChange } from '../change.mjs';
|
|
4
|
+
import { findChangeledgerDir } from '../config.mjs';
|
|
5
|
+
import { contractTemplatesDir } from '../paths.mjs';
|
|
6
|
+
import { resolveChange } from '../repo.mjs';
|
|
7
|
+
|
|
8
|
+
const MODES = ['implement', 'review', 'spec', 'release'];
|
|
9
|
+
const MODE_CONTEXT = {
|
|
10
|
+
implement: ['core', 'implement', 'readiness'],
|
|
11
|
+
review: ['core', 'review'],
|
|
12
|
+
spec: ['core', 'spec', 'readiness'],
|
|
13
|
+
release: ['core', 'release'],
|
|
14
|
+
};
|
|
15
|
+
const STATUS_CONTEXT = {
|
|
16
|
+
draft: { mode: 'spec', fragments: MODE_CONTEXT.spec },
|
|
17
|
+
approved: { mode: 'implement', fragments: MODE_CONTEXT.implement },
|
|
18
|
+
'in-progress': { mode: 'implement', fragments: MODE_CONTEXT.implement },
|
|
19
|
+
'in-review': { mode: 'review', fragments: ['core', 'review'] },
|
|
20
|
+
blocked: { mode: 'blocked', fragments: ['core', 'blocked'] },
|
|
21
|
+
'in-validation': { mode: 'validation', fragments: ['core', 'validation'] },
|
|
22
|
+
done: { mode: 'close', fragments: ['core', 'close'] },
|
|
23
|
+
discarded: { mode: 'discarded', fragments: ['core', 'discarded'] },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function fragment(name) {
|
|
27
|
+
return fs.readFileSync(path.join(contractTemplatesDir, `${name}.md`), 'utf8').trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compose(mode, fragments, changeText) {
|
|
31
|
+
const sections = [`Mode: ${mode}`, ...fragments.map(fragment)];
|
|
32
|
+
if (changeText) sections.push('---\n\n# Selected change\n', changeText.trim());
|
|
33
|
+
return `${sections.join('\n\n')}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function requireRepo(cwd) {
|
|
37
|
+
if (!findChangeledgerDir(cwd)) {
|
|
38
|
+
throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildContext(input, cwd = process.cwd()) {
|
|
43
|
+
requireRepo(cwd);
|
|
44
|
+
if (!input) return compose('core', ['core']);
|
|
45
|
+
if (MODES.includes(input)) return compose(input, MODE_CONTEXT[input]);
|
|
46
|
+
|
|
47
|
+
let resolved;
|
|
48
|
+
try {
|
|
49
|
+
resolved = resolveChange(cwd, input);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Unknown context "${input}" — valid modes: ${MODES.join(', ')} (or pass a change id)`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const text = fs.readFileSync(resolved.file, 'utf8');
|
|
57
|
+
const status = parseChange(text).frontmatter.status;
|
|
58
|
+
const selected = STATUS_CONTEXT[status];
|
|
59
|
+
if (!selected) throw new Error(`No context mapping for change status "${status}"`);
|
|
60
|
+
return compose(selected.mode, selected.fragments, text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function context(input, cwd = process.cwd(), output = console.log) {
|
|
64
|
+
output(buildContext(input, cwd).trimEnd());
|
|
65
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -2,20 +2,19 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { writeFileAtomic } from '../atomic-write.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { ensureReference, rootContract } from '../contract.mjs';
|
|
6
6
|
import { templatesDir } from '../paths.mjs';
|
|
7
7
|
import { register } from '../registry.mjs';
|
|
8
8
|
import { serializeScalar } from '../yaml.mjs';
|
|
9
9
|
|
|
10
|
-
// Sets up `.changeledger
|
|
11
|
-
// AGENTS.md contract into `.changeledger/`, references it from the project's root
|
|
10
|
+
// Sets up `.changeledger/`, installs the context bootstrap in the project-owned
|
|
12
11
|
// AGENTS.md, and registers the repo in the global registry.
|
|
13
12
|
export function init(cwd = process.cwd()) {
|
|
14
13
|
const repoRoot = path.resolve(cwd);
|
|
15
14
|
const changeledgerDir = path.join(repoRoot, '.changeledger');
|
|
16
15
|
if (fs.existsSync(changeledgerDir)) {
|
|
17
16
|
throw new Error(
|
|
18
|
-
'.changeledger/ already exists. Use `changeledger register` to
|
|
17
|
+
'.changeledger/ already exists. Use `changeledger register` to refresh this repo.',
|
|
19
18
|
);
|
|
20
19
|
}
|
|
21
20
|
// The root AGENTS.md is the project's own contract; we reference the tool's
|
|
@@ -34,9 +33,7 @@ export function init(cwd = process.cwd()) {
|
|
|
34
33
|
`${fs.readFileSync(path.join(templatesDir, 'config.yml'), 'utf8')}\n# Project identity (stable; the global registry maps it to a path)\nproject_id: "${id}"\nproject_name: ${serializeScalar(name)}\n`,
|
|
35
34
|
);
|
|
36
35
|
|
|
37
|
-
linkContract(changeledgerDir);
|
|
38
36
|
ensureReference(repoRoot);
|
|
39
|
-
ensureGitignore(repoRoot);
|
|
40
37
|
|
|
41
38
|
register({ id, name, path: repoRoot });
|
|
42
39
|
return changeledgerDir;
|
package/src/commands/new.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const LOCK_MTIME_STALE_MS = 30_000;
|
|
|
11
11
|
|
|
12
12
|
// Scaffolds a new change file with the active stages for its type.
|
|
13
13
|
// `slug` is the English filename slug (structure); `title` is the content title
|
|
14
|
-
// (repo language). See
|
|
14
|
+
// (repo language). See `changeledger context spec`.
|
|
15
15
|
export function newChange({ type, slug, title, owner, now }, cwd = process.cwd()) {
|
|
16
16
|
const changeledgerDir = findChangeledgerDir(cwd);
|
|
17
17
|
if (!changeledgerDir) throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { findChangeledgerDir, loadConfig } from '../config.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ensureReference,
|
|
6
|
+
removeLegacyContract,
|
|
7
|
+
removeLegacyGitignore,
|
|
8
|
+
rootContract,
|
|
9
|
+
} from '../contract.mjs';
|
|
5
10
|
import { register } from '../registry.mjs';
|
|
6
11
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// after moving the repo or cloning it on another machine.
|
|
12
|
+
// Refreshes the repo bootstrap and registry path. Also migrates the per-machine
|
|
13
|
+
// contract artifact left by legacy versions.
|
|
10
14
|
export function registerRepo(cwd = process.cwd()) {
|
|
11
15
|
const changeledgerDir = findChangeledgerDir(cwd);
|
|
12
16
|
if (!changeledgerDir) throw new Error('Not a ChangeLedger repo. Run `changeledger init` first.');
|
|
@@ -19,8 +23,8 @@ export function registerRepo(cwd = process.cwd()) {
|
|
|
19
23
|
const repoRoot = path.dirname(changeledgerDir);
|
|
20
24
|
const name = config.project_name || path.basename(repoRoot);
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
removeLegacyContract(changeledgerDir);
|
|
27
|
+
removeLegacyGitignore(repoRoot);
|
|
24
28
|
if (fs.existsSync(rootContract(repoRoot))) ensureReference(repoRoot);
|
|
25
29
|
|
|
26
30
|
register({ id: config.project_id, name, path: repoRoot });
|
package/src/contract.mjs
CHANGED
|
@@ -1,29 +1,49 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { writeFileAtomic } from './atomic-write.mjs';
|
|
4
|
-
import { agentsTemplate } from './paths.mjs';
|
|
5
5
|
|
|
6
|
-
// The tool's contract (templates/AGENTS.md) is a tool artifact, not a project
|
|
7
|
-
// artifact. Each repo links to the installed copy via `.changeledger/AGENTS.md` (a
|
|
8
|
-
// per-machine, gitignored symlink) and points its own contract files at that
|
|
9
|
-
// link. We never copy the contract (it would drift) nor commit the symlink (it
|
|
10
|
-
// would dangle on another machine). See change #20260614-151759.
|
|
11
|
-
|
|
12
|
-
// Project-owned contract files that should reference the linked contract. The
|
|
13
|
-
// root AGENTS.md is required by `init`; CLAUDE.md is referenced when present.
|
|
14
6
|
const CONTRACT_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
15
|
-
|
|
16
7
|
const MARKER = '<!-- changeledger -->';
|
|
17
|
-
const
|
|
8
|
+
const LEGACY_ENTRY = '.changeledger/AGENTS.md';
|
|
9
|
+
// Exact SHA-256 digests of every historical templates/AGENTS.md payload. A
|
|
10
|
+
// regular file is removed only when it is byte-identical to a contract version
|
|
11
|
+
// ChangeLedger actually shipped; headings or other heuristics are not enough.
|
|
12
|
+
const LEGACY_CONTRACT_HASHES = new Set([
|
|
13
|
+
'92a6d22c9985dce2d15724d20e2d5c0f1178d82dc56d28e339b72c7a27574c6f',
|
|
14
|
+
'4701b260d0cc39098f3b942ed2b1969f00a84e7b77a4e107070fedef0fda7d0d',
|
|
15
|
+
'30b3ce1768655ebee2be2b88328ba860293d8bbbe55e12a5aacc8177d7d02c9b',
|
|
16
|
+
'd36d17f99505f2b47a7ceea8f078618f652807463d5d60a83ca95f940d5a9c35',
|
|
17
|
+
'1175cc4bcc884d792cdde2338b3c72b336bcb66b47555d6e5938d756acdea635',
|
|
18
|
+
'f605adad9192286bc2cda45aebb5a12dab5b3c199924789398684b79b55811',
|
|
19
|
+
'05e06c8e9dc45ac41ff38b47075b22492c30f3f1a7019371293be10351aada77',
|
|
20
|
+
'9816e4d14f70dad268f9b89ad49094665997bf62f0b773a05fe8eed17eebc9de',
|
|
21
|
+
'1b5439055e52a1bb72f72b99cbd2842b2118776fb80e262175d584a335e07e5f',
|
|
22
|
+
'6fe7a03f699ffa875c1ef4bc2d59b451058d6ab4c3c8824cc220ae031d068480',
|
|
23
|
+
'd56ec9ef5c1bd71464d9d9bc2a772c42cce103f336ebba61106855c8011bca6f',
|
|
24
|
+
'071ed83e79589ec0362e4e99ee80cb4f7a839f2a810cf59dbf056c46e342a09c',
|
|
25
|
+
'1148a1b69ce035a695c64f0df1aaaedcbeeed669b5b774c52f7b136e9f5fe4f9',
|
|
26
|
+
'25d4508b0b60aa939e81d1e83383f90e7a41feacb6cb2f12fab6548cebb77ca1',
|
|
27
|
+
'37e1ffe059c0d26b07566597607c41d42df3e9548830d42413dd8d2afd62aa86',
|
|
28
|
+
'60519091c785977299725e4f2f3207b83828bfc56d94a989adf13bd0fc9bb174',
|
|
29
|
+
'6e201bb60337c9b0869c6c769272c82abc466330f82c662bb023a74ad0b8fa92',
|
|
30
|
+
'80e2560e01cfae87455177b54f77845b657764338d3bf3507119e3e1ff289bcd',
|
|
31
|
+
'7a126595a0b5de0b58eec9db38ee6d1654caf0b42df29d181692c382abe213c6',
|
|
32
|
+
'2e839be3ad9458feebcfd8ca979f9173b1f63b514939903ea8f9f03517528f88',
|
|
33
|
+
'f10407e809fc679d11e442d57c92a4b82d0647f06946bb42c7daccedf76873ec',
|
|
34
|
+
'99a42380f3835e1f706b986b86e58e33f12d68de4a69d94eb50845e790e05f71',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const REFERENCE = `${MARKER}
|
|
18
38
|
> [!IMPORTANT]
|
|
19
|
-
> This repo uses **ChangeLedger**.
|
|
20
|
-
>
|
|
39
|
+
> This repo uses **ChangeLedger**. Before creating or modifying files, run
|
|
40
|
+
> \`changeledger context\` (or \`changeledger context <change-id>\`) and follow its output.
|
|
41
|
+
> If the command is unavailable, stop and restore/install ChangeLedger; do not proceed from memory.
|
|
21
42
|
`;
|
|
22
43
|
|
|
23
44
|
export const contractLink = (changeledgerDir) => path.join(changeledgerDir, 'AGENTS.md');
|
|
24
45
|
export const rootContract = (repoRoot) => path.join(repoRoot, 'AGENTS.md');
|
|
25
46
|
|
|
26
|
-
// A real, non-symlink file we may safely append to.
|
|
27
47
|
function isPlainFile(file) {
|
|
28
48
|
try {
|
|
29
49
|
return fs.lstatSync(file).isFile();
|
|
@@ -32,69 +52,85 @@ function isPlainFile(file) {
|
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
54
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
fs.symlinkSync(agentsTemplate, link);
|
|
47
|
-
} catch {
|
|
48
|
-
// Windows without Developer Mode/admin cannot create symlinks. Fall back to
|
|
49
|
-
// a copy so the contract is still present; `changeledger register` refreshes it if the
|
|
50
|
-
// installed contract changes.
|
|
51
|
-
writeFileAtomic(link, fs.readFileSync(agentsTemplate, 'utf8'));
|
|
52
|
-
}
|
|
53
|
-
return link;
|
|
55
|
+
function replaceReference(text) {
|
|
56
|
+
const start = text.indexOf(MARKER);
|
|
57
|
+
if (start === -1) return `${text}${text.endsWith('\n') ? '' : '\n'}\n${REFERENCE}`;
|
|
58
|
+
const tail = text.slice(start).split('\n');
|
|
59
|
+
let consumed = 1;
|
|
60
|
+
while (consumed < tail.length && tail[consumed].startsWith('>')) consumed += 1;
|
|
61
|
+
const before = text.slice(0, start);
|
|
62
|
+
const after = tail.slice(consumed).join('\n').replace(/^\n+/, '');
|
|
63
|
+
return `${before}${REFERENCE}${after ? `\n${after}` : ''}`;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
//
|
|
57
|
-
// already there. Symlinks are skipped (appending would write into their target).
|
|
66
|
+
// Add or replace the managed bootstrap block in project-owned agent files.
|
|
58
67
|
export function ensureReference(repoRoot) {
|
|
59
68
|
const touched = [];
|
|
60
69
|
for (const name of CONTRACT_FILES) {
|
|
61
70
|
const file = path.join(repoRoot, name);
|
|
62
71
|
if (!isPlainFile(file)) continue;
|
|
63
72
|
const text = fs.readFileSync(file, 'utf8');
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
const updated = replaceReference(text);
|
|
74
|
+
if (updated === text) continue;
|
|
75
|
+
writeFileAtomic(file, updated);
|
|
66
76
|
touched.push(name);
|
|
67
77
|
}
|
|
68
78
|
return touched;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
//
|
|
72
|
-
|
|
81
|
+
// Remove only artifacts known to be managed by legacy ChangeLedger versions.
|
|
82
|
+
// Unknown regular files fail closed instead of being deleted.
|
|
83
|
+
export function removeLegacyContract(changeledgerDir, knownHashes = LEGACY_CONTRACT_HASHES) {
|
|
84
|
+
const file = contractLink(changeledgerDir);
|
|
85
|
+
let stat;
|
|
86
|
+
try {
|
|
87
|
+
stat = fs.lstatSync(file);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error.code === 'ENOENT') return false;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
if (stat.isSymbolicLink()) {
|
|
93
|
+
fs.unlinkSync(file);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (stat.isFile()) {
|
|
97
|
+
const digest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
98
|
+
if (knownHashes.has(digest)) {
|
|
99
|
+
fs.unlinkSync(file);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new Error(
|
|
104
|
+
'`.changeledger/AGENTS.md` is not a recognized legacy ChangeLedger contract; move or remove it manually, then run `changeledger register` again',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function removeLegacyGitignore(repoRoot) {
|
|
73
109
|
const file = path.join(repoRoot, '.gitignore');
|
|
74
|
-
|
|
75
|
-
const text = fs.
|
|
76
|
-
|
|
77
|
-
|
|
110
|
+
if (!fs.existsSync(file)) return false;
|
|
111
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
112
|
+
const lines = text.split('\n');
|
|
113
|
+
const kept = lines.filter((line) => line !== LEGACY_ENTRY);
|
|
114
|
+
if (kept.length === lines.length) return false;
|
|
115
|
+
writeFileAtomic(file, kept.join('\n'));
|
|
78
116
|
return true;
|
|
79
117
|
}
|
|
80
118
|
|
|
81
|
-
|
|
82
|
-
export function checkContract(repoRoot, changeledgerDir) {
|
|
119
|
+
export function checkContract(repoRoot) {
|
|
83
120
|
const errors = [];
|
|
84
121
|
const root = rootContract(repoRoot);
|
|
85
122
|
if (!fs.existsSync(root)) {
|
|
86
123
|
errors.push('missing AGENTS.md at the repo root (ChangeLedger contract reference lives here)');
|
|
87
124
|
}
|
|
88
|
-
// Every present, non-symlink contract file must carry the reference.
|
|
89
125
|
for (const name of CONTRACT_FILES) {
|
|
90
126
|
const file = path.join(repoRoot, name);
|
|
91
127
|
if (!isPlainFile(file)) continue;
|
|
92
|
-
|
|
128
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
129
|
+
if (!text.includes(MARKER)) {
|
|
93
130
|
errors.push(`${name} has no ChangeLedger reference — run \`changeledger register\``);
|
|
131
|
+
} else if (!text.includes(REFERENCE.trim())) {
|
|
132
|
+
errors.push(`${name} has an outdated ChangeLedger reference — run \`changeledger register\``);
|
|
94
133
|
}
|
|
95
134
|
}
|
|
96
|
-
if (!fs.existsSync(contractLink(changeledgerDir))) {
|
|
97
|
-
errors.push('`.changeledger/AGENTS.md` is missing or dangling — run `changeledger register`');
|
|
98
|
-
}
|
|
99
135
|
return errors;
|
|
100
136
|
}
|
package/src/paths.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
|
|
4
4
|
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
5
5
|
export const templatesDir = path.join(packageRoot, 'templates');
|
|
6
|
-
export const
|
|
6
|
+
export const contractTemplatesDir = path.join(templatesDir, 'contract');
|
|
7
7
|
export const publicDir = path.join(packageRoot, 'src', 'viewer', 'public');
|
|
8
8
|
|
|
9
9
|
// ISO 8601 UTC at second precision, matching the change `created`/task convention.
|
package/src/viewer/public/app.js
CHANGED
|
@@ -595,11 +595,41 @@ function openSpec(s) {
|
|
|
595
595
|
);
|
|
596
596
|
const overlay = $('#overlay');
|
|
597
597
|
overlay.classList.remove('hidden');
|
|
598
|
-
$('#detail')
|
|
598
|
+
const detail = $('#detail');
|
|
599
|
+
detail.querySelector('.close').onclick = closeDetail;
|
|
599
600
|
overlay.onclick = (e) => {
|
|
600
601
|
if (e.target === overlay) closeDetail();
|
|
601
602
|
};
|
|
602
|
-
|
|
603
|
+
detail.onclick = (e) => handleSpecBodyClick(e, (href) => openSpecByName(href, state, openSpec));
|
|
604
|
+
renderExpandableMermaid(detail);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Normalizes a spec href and opens the matching spec.
|
|
609
|
+
* Exported for testing: accepts `repoState` and `_openSpec` to avoid DOM coupling.
|
|
610
|
+
*/
|
|
611
|
+
export function openSpecByName(href, repoState = state, _openSpec = openSpec) {
|
|
612
|
+
const name = href.replace(/^\.\//, '').replace(/\.md$/, '');
|
|
613
|
+
const found = (repoState.repo?.specs ?? []).find((s) => s.name.replace(/\.md$/, '') === name);
|
|
614
|
+
if (found) _openSpec(found);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Click handler for the spec body container.
|
|
619
|
+
* Exported for testing: accepts `_openSpecByName` callback to avoid DOM coupling.
|
|
620
|
+
* Intercepts relative *.md links only; lets external links through unchanged.
|
|
621
|
+
*/
|
|
622
|
+
export function handleSpecBodyClick(event, _openSpecByName) {
|
|
623
|
+
const anchor = event.target.closest('a');
|
|
624
|
+
if (!anchor) return;
|
|
625
|
+
const href = anchor.getAttribute('href');
|
|
626
|
+
if (!href) return;
|
|
627
|
+
// Let external links (with scheme or absolute path) through unchanged.
|
|
628
|
+
if (/^[a-z][a-z\d+\-.]*:/i.test(href) || href.startsWith('/')) return;
|
|
629
|
+
// Only intercept relative *.md links.
|
|
630
|
+
if (!href.endsWith('.md')) return;
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
_openSpecByName(href);
|
|
603
633
|
}
|
|
604
634
|
|
|
605
635
|
const VIEWS = ['board', 'table', 'graph', 'specs', 'metrics', 'projects'];
|
package/templates/config.yml
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# Repo specifics: documentation language, change types and active stages per type.
|
|
3
3
|
|
|
4
4
|
# Language for generated documentation CONTENT. Structure (headings, keys, enums)
|
|
5
|
-
# is always English. See
|
|
5
|
+
# is always English. See `changeledger context spec`.
|
|
6
6
|
language: en
|
|
7
7
|
|
|
8
8
|
# Definition of Ready policy. When true (default), changes are documented
|
|
9
9
|
# test-grade and implemented via TDD; `changeledger check` warns on criteria ↔ task
|
|
10
|
-
# coverage gaps. Set false for exploratory repos. See
|
|
10
|
+
# coverage gaps. Set false for exploratory repos. See `changeledger context spec`.
|
|
11
11
|
tdd: true
|
|
12
12
|
|
|
13
13
|
# Default SemVer impact for a completed change. A change may override its type
|
|
@@ -38,7 +38,8 @@ stages: [request, investigation, proposal, specification, plan, log]
|
|
|
38
38
|
|
|
39
39
|
# Active stages per type. The viewer shows only these.
|
|
40
40
|
# `review_required: true` makes the change pass through `in-review` (an
|
|
41
|
-
# independent subagent review) before universal human validation. See
|
|
41
|
+
# independent subagent review) before universal human validation. See
|
|
42
|
+
# `changeledger context implement` and `changeledger context review`.
|
|
42
43
|
types:
|
|
43
44
|
feature:
|
|
44
45
|
stages: [request, investigation, proposal, specification, plan, log]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Blocked — Resolve Before Implementing
|
|
2
|
+
|
|
3
|
+
Do not resume implementation merely because context was requested. Inspect the
|
|
4
|
+
blocked task and Log. If the impediment is resolved within authorized scope,
|
|
5
|
+
record the decision, restore the task and move `blocked → in-progress`. If it
|
|
6
|
+
requires scope or product judgment, ask the human. Never bypass the block.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Closing Accepted Work
|
|
2
|
+
|
|
3
|
+
The human accepted this change. Graduate persistent truth before archiving:
|
|
4
|
+
|
|
5
|
+
- `changeledger graduate <id> <spec-slug>` creates a new spec.
|
|
6
|
+
- `changeledger graduate <id> <spec-slug> --into` links an existing spec and
|
|
7
|
+
refreshes `updated` without overwriting the spec body (the agent edits the body manually).
|
|
8
|
+
- `changeledger graduate <id> --skip [reason]` records that no persistent truth
|
|
9
|
+
changed.
|
|
10
|
+
- `changeledger graduate --pending` lists unresolved graduation decisions.
|
|
11
|
+
- `changeledger archive <id>` hides the completed record from the default board.
|
|
12
|
+
|
|
13
|
+
Specs have no lifecycle: minimal frontmatter (`title`, `updated`, `tags`) plus
|
|
14
|
+
free Markdown describing current capabilities, architecture or domain truth.
|
|
15
|
+
The change is the journey; the spec is the destination.
|
|
16
|
+
|
|
17
|
+
After closure, share a brief retrospective. New work needs a new authorized
|
|
18
|
+
change; `done` never reopens.
|