changeledger 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,9 +96,11 @@ changeledger discard <id> <reason>
96
96
  ### Preserve completed truth
97
97
 
98
98
  ```sh
99
- changeledger graduate <id> <spec-slug> # create a persistent spec
100
- changeledger graduate <id> <spec-slug> --into # update an existing spec's provenance
99
+ changeledger graduate <id> <spec-slug> --new # create a marked scaffold; remains pending
100
+ # refine the scaffold and remove its marker
101
+ changeledger graduate <id> <spec-slug> --into # finalize an existing refined spec
101
102
  changeledger graduate <id> --skip [reason] # record that no spec is needed
103
+ changeledger graduate --pending # list unresolved graduation decisions
102
104
  changeledger archive --graduated [--dry-run] # hide resolved changes from the board
103
105
  ```
104
106
 
@@ -15,7 +15,12 @@ import {
15
15
  } from '../src/commands/agent.mjs';
16
16
  import { check } from '../src/commands/check.mjs';
17
17
  import { context } from '../src/commands/context.mjs';
18
- import { graduate, pendingGraduation, skipGraduation } from '../src/commands/graduate.mjs';
18
+ import {
19
+ graduate,
20
+ pendingGraduation,
21
+ scaffoldSpec,
22
+ skipGraduation,
23
+ } from '../src/commands/graduate.mjs';
19
24
  import { init } from '../src/commands/init.mjs';
20
25
  import { newChange } from '../src/commands/new.mjs';
21
26
  import { registerRepo } from '../src/commands/register.mjs';
@@ -46,7 +51,7 @@ const USAGE = `ChangeLedger (changeledger)
46
51
  changeledger task <id> done|block <n> [reason] mark a Plan task
47
52
  changeledger list [--status S] [--type T] [--json] list changes
48
53
  changeledger show <id> [--json] print a change
49
- changeledger graduate <change-id> <spec-slug> graduate a change to a new spec
54
+ changeledger graduate <change-id> <spec-slug> --new create a spec scaffold to refine
50
55
  changeledger graduate <change-id> <spec-slug> --into graduate into an existing spec
51
56
  changeledger graduate <change-id> --skip [reason] mark graduation reviewed, no spec
52
57
  changeledger graduate --pending list done changes not yet reviewed
@@ -298,7 +303,8 @@ program
298
303
  .argument('[change-id]')
299
304
  .argument('[spec-slug]')
300
305
  .argument('[reason...]')
301
- .option('--into', 'graduate into an existing spec')
306
+ .option('--new', 'create a spec scaffold without resolving graduation')
307
+ .option('--into', 'finalize graduation into an existing refined spec')
302
308
  .option('--skip', 'mark graduation reviewed without a spec')
303
309
  .option('--pending', 'list done changes not yet reviewed')
304
310
  .addHelpText(
@@ -306,7 +312,7 @@ program
306
312
  [
307
313
  '',
308
314
  'Examples:',
309
- ' changeledger graduate <change-id> <spec-slug>',
315
+ ' changeledger graduate <change-id> <spec-slug> --new',
310
316
  ' changeledger graduate <change-id> <spec-slug> --into',
311
317
  ' changeledger graduate <change-id> --skip [reason]',
312
318
  ' changeledger graduate --pending',
@@ -314,7 +320,15 @@ program
314
320
  )
315
321
  .action(
316
322
  action((id, slug, reasonParts, options) => {
323
+ const modeCount = [options.new, options.into, options.skip, options.pending].filter(
324
+ Boolean,
325
+ ).length;
326
+ const modeUsage =
327
+ 'Usage: changeledger graduate requires exactly one mode: --new, --into, --skip, or --pending';
328
+ if (modeCount !== 1) throw new Error(modeUsage);
329
+
317
330
  if (options.pending) {
331
+ if (id || slug || reasonParts.length) throw new Error(modeUsage);
318
332
  const items = pendingGraduation();
319
333
  if (!items.length) console.log('No changes pending graduation.');
320
334
  for (const c of items) console.log(`#${c.id} ${c.title}`);
@@ -327,7 +341,15 @@ program
327
341
  console.log(`#${id} graduation skipped`);
328
342
  return;
329
343
  }
330
- if (!id || !slug) throw new Error('Usage: changeledger graduate <change-id> <spec-slug>');
344
+
345
+ if (!id || !slug || reasonParts.length) throw new Error(modeUsage);
346
+ if (options.new) {
347
+ const file = scaffoldSpec(id, slug);
348
+ console.log(
349
+ `Created spec scaffold ${file}. Refine it, then run: changeledger graduate ${id} ${slug} --into`,
350
+ );
351
+ return;
352
+ }
331
353
  const file = graduate(id, slug, process.cwd(), { into: options.into });
332
354
  console.log(`Graduated #${id} → ${file}`);
333
355
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "changeledger",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Turn conversations into buildable changes.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
- // Graduates a change to a spec: scaffolds the spec file (frontmatter + a body
2
- // seeded from the change's Specification/Proposal) and links it back in the
3
- // change's Log. The final wording stays a manual/agent task.
1
+ // Graduation is intentionally two-phase for a new spec. `scaffoldSpec` creates
2
+ // an editable seed without resolving the change; `graduate --into` links only
3
+ // after the durable wording has been reviewed.
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
@@ -13,55 +13,73 @@ import { slugify } from '../slug.mjs';
13
13
  import { appendLog, setReviewed, setSpecUpdated } from '../writer.mjs';
14
14
  import { serializeScalar } from '../yaml.mjs';
15
15
 
16
- // `into: true` graduates into an EXISTING spec — it refreshes the spec's
17
- // `updated` and links it back, but leaves the body to the agent (who knows what
18
- // to refine). Without `into`, a new spec is scaffolded and an existing one is an
19
- // error. Both routes share the same change-side record (marker + reviewed).
20
- export function graduate(id, slug, cwd = process.cwd(), { into = false } = {}) {
21
- const { config, repoRoot, file: changeFile } = resolveChange(cwd, id);
16
+ const SPEC_SCAFFOLD_MARKER = '<!-- changeledger:spec-scaffold -->';
22
17
 
23
- const specsDir = resolveSpecsDir(repoRoot, config);
18
+ function graduationTarget(id, slug, cwd) {
19
+ const resolved = resolveChange(cwd, id);
20
+ const specsDir = resolveSpecsDir(resolved.repoRoot, resolved.config);
24
21
  const specName = `${slugify(slug)}.md`;
25
- const specFile = path.join(specsDir, specName);
22
+ return { ...resolved, specsDir, specName, specFile: path.join(specsDir, specName) };
23
+ }
26
24
 
27
- // Validate preconditions before acquiring the change file lock — ensures no
28
- // write happens at all when the existence check fails (CR1).
29
- const exists = fs.existsSync(specFile);
30
- if (into && !exists)
31
- throw new Error(`Spec "${specName}" does not exist — drop --into to create it`);
32
- if (!into && exists) throw new Error(`Spec "${specName}" already exists`);
25
+ function requireDone(changeText) {
26
+ const change = parseChange(changeText);
27
+ if (change.frontmatter.status !== 'done') {
28
+ throw new Error('only done changes can be graduated/skipped');
29
+ }
30
+ return change;
31
+ }
33
32
 
34
- mutateFileAtomic(changeFile, (changeText) => {
35
- const change = parseChange(changeText);
36
- if (change.frontmatter.status !== 'done') {
37
- throw new Error('only done changes can be graduated/skipped');
38
- }
33
+ export function scaffoldSpec(id, slug, cwd = process.cwd()) {
34
+ const { file: changeFile, specsDir, specName, specFile } = graduationTarget(id, slug, cwd);
35
+ if (fs.existsSync(specFile)) throw new Error(`Spec "${specName}" already exists`);
39
36
 
40
- if (into) {
41
- // Refresh the spec's updated; the body stays the agent's to edit.
42
- writeFileAtomic(specFile, setSpecUpdated(fs.readFileSync(specFile, 'utf8'), nowUtc()));
43
- } else {
44
- const seedStage =
45
- change.stages.find((s) => s.key === 'specification') ??
46
- change.stages.find((s) => s.key === 'proposal');
47
- const seed = seedStage ? seedStage.body : '';
48
- const title = change.frontmatter.title;
49
-
50
- const content = `---
51
- title: ${serializeScalar(title)}
37
+ const change = requireDone(fs.readFileSync(changeFile, 'utf8'));
38
+ const seedStage =
39
+ change.stages.find((stage) => stage.key === 'specification') ??
40
+ change.stages.find((stage) => stage.key === 'proposal');
41
+ const seed = seedStage ? seedStage.body : '';
42
+ const content = `---
43
+ title: ${serializeScalar(change.frontmatter.title)}
52
44
  updated: ${nowUtc()}
53
45
  tags: [${change.frontmatter.type}]
54
46
  ---
55
47
 
56
- # ${title}
48
+ # ${change.frontmatter.title}
57
49
 
58
- > Graduado del change ${id}.
50
+ ${SPEC_SCAFFOLD_MARKER}
51
+
52
+ > Scaffold from change ${id}; replace this seed with durable current truth before --into.
59
53
 
60
54
  ${seed}
61
55
  `;
62
- fs.mkdirSync(specsDir, { recursive: true });
63
- writeFileAtomic(specFile, content);
56
+
57
+ fs.mkdirSync(specsDir, { recursive: true });
58
+ writeFileAtomic(specFile, content);
59
+ return specFile;
60
+ }
61
+
62
+ // Finalizes graduation into an EXISTING, manually refined spec. The command
63
+ // refreshes `updated` and links it back, but never overwrites the body.
64
+ export function graduate(id, slug, cwd = process.cwd(), { into = false } = {}) {
65
+ if (!into) {
66
+ throw new Error('graduation mode required: use --new, --into, or --skip');
67
+ }
68
+ const { file: changeFile, specName, specFile } = graduationTarget(id, slug, cwd);
69
+
70
+ if (!fs.existsSync(specFile)) {
71
+ throw new Error(`Spec "${specName}" does not exist — use --new to create a scaffold`);
72
+ }
73
+
74
+ mutateFileAtomic(changeFile, (changeText) => {
75
+ requireDone(changeText);
76
+ const specText = fs.readFileSync(specFile, 'utf8');
77
+ if (specText.includes(SPEC_SCAFFOLD_MARKER)) {
78
+ throw new Error(
79
+ `Spec "${specName}" still contains the scaffold marker — refine it and remove the marker before --into`,
80
+ );
64
81
  }
82
+ writeFileAtomic(specFile, setSpecUpdated(specText, nowUtc()));
65
83
 
66
84
  let text = appendLog(changeText, nowUtc(), `graduado a spec \`${specName}\``);
67
85
  text = setReviewed(text, true);
@@ -1,6 +1,8 @@
1
1
  # Closing Accepted Work
2
2
 
3
- The human accepted this change. Resolve persistent truth before archiving.
3
+ The human accepted this change. Resolve persistent truth before archiving. Run
4
+ `changeledger context <id>` after acceptance even if the base context was loaded
5
+ earlier; this lifecycle-specific close overlay is not part of the base context.
4
6
  Changes describe a journey; `.changeledger/specs/*.md` describe the current
5
7
  capability, architecture or domain truth that code reflects.
6
8
 
@@ -15,19 +17,25 @@ tags: []
15
17
  ---
16
18
  ```
17
19
 
18
- Choose one graduation outcome:
19
-
20
- - `changeledger graduate <id> <spec-slug>` creates a new spec seeded from the
21
- change's Specification and Proposal; refine its wording manually.
22
- - `changeledger graduate <id> <spec-slug> --into` links an existing spec and
23
- refreshes `updated` without overwriting the spec body (the agent edits the body manually).
20
+ Choose exactly one explicit graduation mode. A positional slug without a mode
21
+ is an error, so words such as `skip` or `skip-*` can never silently become specs.
22
+
23
+ - For a new spec, run `changeledger graduate <id> <spec-slug> --new`. This creates
24
+ a seed from the change's Specification or Proposal but leaves graduation
25
+ pending. Rewrite it as concise durable current truth and remove the explicit
26
+ scaffold marker. Then run `changeledger graduate <id> <spec-slug> --into` to
27
+ finalize it. `--into` refuses an unrefined marked scaffold.
28
+ - For an existing spec, the agent edits its body first, then runs
29
+ `changeledger graduate <id> <spec-slug> --into`. It refreshes `updated`, records
30
+ the link and does not overwrite the body.
24
31
  - `changeledger graduate <id> --skip [reason]` records that no persistent truth
25
32
  changed.
26
33
  - `changeledger graduate --pending` lists accepted changes whose decision is
27
34
  unresolved.
28
35
 
29
- Graduation and skip both set `reviewed: true` on the change: it means the
30
- persistent-truth question was settled, not necessarily that a spec was created.
36
+ Finalization with `--into` and skip both set `reviewed: true` on the change;
37
+ `--new` does not. The boolean means the persistent-truth question was settled,
38
+ not necessarily that a spec was created.
31
39
  The graduation link remains derivable from the Log marker `graduado a spec`,
32
40
  which carries the spec link, rather than from the boolean flag.
33
41
 
@@ -26,8 +26,9 @@ modifying files.
26
26
  6. For types that require review, use a fresh clean-context reviewer before
27
27
  human validation.
28
28
  7. Stop at `in-validation`. The agent never accepts on the human's behalf.
29
- 8. After human acceptance, graduate persistent truth or record an explicit skip,
30
- then archive the done change.
29
+ 8. After human acceptance, reload `changeledger context <id>` for the `done`
30
+ change, then graduate persistent truth or run
31
+ `changeledger graduate <id> --skip [reason]`; archive only after that decision.
31
32
 
32
33
  If no approved or in-progress change applies, do not silently edit repository
33
34
  files. Create or update a change, or ask the human whether a purely operational,