@warnyin/sdlc 0.14.0 → 0.14.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.2 (2026-09-19)
4
+
5
+ - **Fix (init)**: `init` no longer disowns the tools it did not install that run. It rebuilt the
6
+ ownership manifest from scratch and wrote it wholesale, so running `init` a second time for a
7
+ different tool dropped every entry belonging to the first — the files stayed on disk, but
8
+ `update` could no longer recognise them. The damage showed up later: such a file was **never
9
+ refreshed again** (refresh needs the recorded hash to match what is on disk) and was reported
10
+ as `kept (user-modified)`, blaming you for an edit you never made. `init` now carries forward
11
+ every entry it does not rewrite. It still never prunes — only `update --tool <list>` removes
12
+ a tool, and now that a tool installed by its own `init` run is properly owned, deselecting it
13
+ there prunes its files as it always should have.
14
+ - **If a project already hit this**, the fix cannot recover a hash nothing recorded. A disowned
15
+ file that still matches the current payload is re-claimed silently on your next `update`; one
16
+ that has already drifted stays frozen and keeps reporting `kept (user-modified)`. To recover
17
+ it, delete that file and run `update` — it is rewritten from the payload and owned again.
18
+
19
+ ## 0.14.1 (2026-09-19)
20
+
21
+ - **Fix (init)**: adding a tool to a project that already has `sdlc/` no longer loses it on the
22
+ next update. `init --tool <newtool>` installed the tool's files but never recorded it in
23
+ `sdlc/config.yaml`'s `tools:` line, so the next plain `update` read that stale list and
24
+ **silently pruned the files it had just installed**. `init` now records what it installs,
25
+ added to whatever was already listed — never removing an entry, because `init` has no prune
26
+ capability and this does not give it one. Only `update --tool <list>` still replaces the list
27
+ wholesale and prunes what is left out. The install summary says `(recorded <tools>)` when the
28
+ list actually grew, and only then. A `config.yaml` carrying no `tools:` line at all is still
29
+ left alone, exactly as `update` already leaves it.
30
+ - **Fix (config)**: the `tools:` line rewrite — now shared by `init` and `update` instead of
31
+ living inline in one of them — passes its replacement as a function rather than a string, so
32
+ a tool name containing `$&`, `` $` `` or `$'` in a hand-edited config is written literally
33
+ instead of being interpreted by `String.replace` and mangling the surrounding file.
34
+
3
35
  ## 0.14.0 (2026-09-19)
4
36
 
5
37
  - **Feature (init)**: `kimi` (Kimi Code CLI) is now a supported tool. Selecting or detecting it
package/bin/cli.mjs CHANGED
@@ -312,7 +312,7 @@ export async function resolveTools(args, {
312
312
  return picked;
313
313
  }
314
314
 
315
- function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
315
+ function printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded }) {
316
316
  const s = summarizeInstall(ctx.manifest.keys(), tools);
317
317
  const stats = ctx.stats;
318
318
  const line = (text) => console.log(` ${text}`);
@@ -329,7 +329,10 @@ function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
329
329
  }
330
330
  line(`${s.hooks} hooks in sdlc/.hooks/`);
331
331
  line(`Playbook: sdlc/.playbook/ (${s.playbook} stages + ${s.templates} templates)`);
332
- line(`Config: sdlc/config.yaml${configExisted ? ' (kept)' : ''}`);
332
+ const configNote = toolsAdded?.length
333
+ ? ` (recorded ${toolsAdded.map(toolName).join(', ')})`
334
+ : configExisted ? ' (kept)' : '';
335
+ line(`Config: sdlc/config.yaml${configNote}`);
333
336
  line(style.dim(`Files: ${stats.written} written · ${stats.current} unchanged · ${stats.updated} refreshed · ${stats.kept} kept (yours)`));
334
337
  console.log('');
335
338
  console.log(` ${style.bold('Getting started:')}`);
@@ -341,7 +344,26 @@ export async function cmdInit(projectRoot, args) {
341
344
  const style = createStyle(colorEnabled());
342
345
  const symbols = symbolsFor();
343
346
  const tools = await resolveTools(args, { projectRoot, style });
344
- const configExisted = fs.existsSync(path.join(projectRoot, 'sdlc', 'config.yaml'));
347
+ const configPath = path.join(projectRoot, 'sdlc', 'config.yaml');
348
+ const configExisted = fs.existsSync(configPath);
349
+
350
+ // config.yaml is a seed — written once, never rewritten wholesale. But its `tools:` line
351
+ // is the one field this command promises to keep current ("filled by `warnyin-sdlc init`"
352
+ // in the template), so a second init adding a tool must not leave it stale: union what's
353
+ // already recorded with what this run installs, never removing an entry (init has no prune
354
+ // capability, and this doesn't give it one — only `update --tool <list>` can shrink the set).
355
+ let toolsAdded = [];
356
+ if (configExisted) {
357
+ const recorded = parseConfig(fs.readFileSync(configPath, 'utf8')).tools;
358
+ const newlySelected = tools.filter((t) => !recorded.includes(t));
359
+ // Only claim "recorded" in the summary when a write actually happened — a config
360
+ // predating the `tools:` key makes persistToolsLine a no-op, and the message must not
361
+ // say otherwise.
362
+ if (newlySelected.length && persistToolsLine(configPath, [...recorded, ...newlySelected])) {
363
+ toolsAdded = newlySelected;
364
+ }
365
+ }
366
+
345
367
  const ctx = {
346
368
  mode: 'install',
347
369
  manifest: new Map(),
@@ -351,10 +373,19 @@ export async function cmdInit(projectRoot, args) {
351
373
  };
352
374
  scaffoldSdlc(projectRoot, tools, ctx);
353
375
  installToolAdapters(projectRoot, tools, ctx);
354
- writeManifestFile(projectRoot, ctx.manifest);
376
+ // Carry forward every entry this run did not rewrite. `init` installs only the tools it was
377
+ // given, so writing `ctx.manifest` alone would disown the files of every OTHER tool already
378
+ // installed here — they stay on disk, but `update` then sees files it has no record of,
379
+ // refuses to refresh them (its refresh branch needs disk hash === recorded hash), freezes
380
+ // them at their old payload version and reports them as user-modified. Same failure
381
+ // `installFile` documents fixing per-file, reached instead by never visiting the entry.
382
+ // The merge happens HERE, not by seeding `ctx.manifest`: that Map is also what the install
383
+ // summary counts, and it must keep describing only what this run installed. `cmdUpdate`
384
+ // deliberately does NOT do this — its wholesale replace is what tells prune a tool is gone.
385
+ writeManifestFile(projectRoot, new Map([...ctx.oldManifest, ...ctx.manifest]));
355
386
  ensureGitignore(projectRoot);
356
387
  for (const w of ctx.warnings) console.warn(` ${style.yellow(symbols.warn)} ${w}`);
357
- printInitSummary(tools, ctx, style, symbols, { configExisted });
388
+ printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded });
358
389
  return { tools };
359
390
  }
360
391
 
@@ -395,6 +426,25 @@ export function forceNeedsAPerson(args, env = process.env, stdin = process.stdin
395
426
  + ' Run it yourself, or set WARNYIN_SDLC_FORCE=1 if this really is automation that meant it.';
396
427
  }
397
428
 
429
+ // Rewrites just the `tools:` line of `config.yaml` in place, leaving every other line
430
+ // untouched — a no-op when the file carries no `tools:` line at all (a config predating the
431
+ // key; matched, not fixed). Shared by `cmdInit` and `cmdUpdate` so the two paths can't drift.
432
+ // Reproduces the existing behavior exactly, trailing inline comment included: the line is
433
+ // replaced wholesale, so a hand-added comment after `tools: [...]` does not survive a rewrite.
434
+ // Returns whether it actually wrote — callers that report "recorded X" to the user must not
435
+ // claim it when this was a no-op (no `tools:` line to rewrite).
436
+ export function persistToolsLine(configPath, tools) {
437
+ const raw = fs.readFileSync(configPath, 'utf8');
438
+ if (!/^tools:/m.test(raw)) return false;
439
+ // A replacer FUNCTION, not a string: `recorded` (unioned in by cmdInit) is free text read
440
+ // back from the project's own config.yaml, not validated against the TOOLS registry the way
441
+ // CLI-sourced tool names are — a string replacement would let `$&`/`$'`/`` $` `` in a
442
+ // hand-edited or corrupted config get interpreted as replacement patterns instead of literal
443
+ // text, silently duplicating or mangling the surrounding file.
444
+ fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, () => `tools: [${tools.join(', ')}]`));
445
+ return true;
446
+ }
447
+
398
448
  export function cmdUpdate(projectRoot, args) {
399
449
  const refusal = refuseSelfUpdate(projectRoot, PKG_ROOT);
400
450
  if (refusal) throw new Error(refusal);
@@ -414,9 +464,7 @@ export function cmdUpdate(projectRoot, args) {
414
464
  // Persist an explicit --tool override so declared and installed state never
415
465
  // diverge (otherwise pruning tool-specific files leaves config.yaml stale).
416
466
  if (args.toolProvided) {
417
- const configPath = path.join(sdlcRoot, 'config.yaml');
418
- const raw = fs.readFileSync(configPath, 'utf8');
419
- fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, `tools: [${tools.join(', ')}]`));
467
+ persistToolsLine(path.join(sdlcRoot, 'config.yaml'), tools);
420
468
  }
421
469
 
422
470
  // Read before scaffolding: recordPayloadVersion overwrites version.json with our own.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warnyin/sdlc",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Spec-driven, AI-driven SDLC framework — token-lean specs, contract-first changes, autonomous pipeline with managed hooks. Operationalizes the Day-1 'New SDLC with Vibe Coding' work process.",
5
5
  "type": "module",
6
6
  "bin": {