@warnyin/sdlc 0.13.0 → 0.14.1

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.1 (2026-09-19)
4
+
5
+ - **Fix (init)**: adding a tool to a project that already has `sdlc/` no longer loses it on the
6
+ next update. `init --tool <newtool>` installed the tool's files but never recorded it in
7
+ `sdlc/config.yaml`'s `tools:` line, so the next plain `update` read that stale list and
8
+ **silently pruned the files it had just installed**. `init` now records what it installs,
9
+ added to whatever was already listed — never removing an entry, because `init` has no prune
10
+ capability and this does not give it one. Only `update --tool <list>` still replaces the list
11
+ wholesale and prunes what is left out. The install summary says `(recorded <tools>)` when the
12
+ list actually grew, and only then. A `config.yaml` carrying no `tools:` line at all is still
13
+ left alone, exactly as `update` already leaves it.
14
+ - **Fix (config)**: the `tools:` line rewrite — now shared by `init` and `update` instead of
15
+ living inline in one of them — passes its replacement as a function rather than a string, so
16
+ a tool name containing `$&`, `` $` `` or `$'` in a hand-edited config is written literally
17
+ instead of being interpreted by `String.replace` and mangling the surrounding file.
18
+
19
+ ## 0.14.0 (2026-09-19)
20
+
21
+ - **Feature (init)**: `kimi` (Kimi Code CLI) is now a supported tool. Selecting or detecting it
22
+ installs a dedicated `.kimi-code/AGENTS.md` — the shared rules-card plus a pointer to
23
+ `sdlc/.playbook/` — the same lite-tier treatment every non-Claude tool gets: no hooks, no
24
+ skills, no agents, just the rules-card and the validator as the enforcement floor. Detection
25
+ looks for an existing `.kimi-code/` directory, mirroring `claude`/`cursor`/`windsurf`; its
26
+ absence never blocks picking `kimi` explicitly with `--tool kimi` or `--tool all`. The file
27
+ is manifest-owned like `.cursor/rules/sdlc.mdc`: a hand-written file at that path is left
28
+ untouched and never claimed, and it is correctly pruned if a project later deselects the tool.
29
+
3
30
  ## 0.13.0 (2026-09-19)
4
31
 
5
32
  - **Feature (update notice)**: the notice that a newer version exists now **asks** instead of
package/bin/cli.mjs CHANGED
@@ -34,7 +34,7 @@ const MARKER = '<!-- sdlc:start -->';
34
34
  const OWN_PACKAGE_NAME = '@warnyin/sdlc';
35
35
 
36
36
  export const TOOLS = Object.freeze([
37
- 'claude', 'cursor', 'windsurf', 'copilot', 'cline', 'gemini', 'agents-md',
37
+ 'claude', 'cursor', 'windsurf', 'copilot', 'cline', 'gemini', 'agents-md', 'kimi',
38
38
  ]);
39
39
 
40
40
  const TEXT_EXT = new Set(['.md', '.mdc', '.mjs', '.json', '.yaml', '.yml', '.txt']);
@@ -203,6 +203,9 @@ function installToolAdapters(projectRoot, tools, ctx) {
203
203
  if (tools.includes('agents-md')) {
204
204
  appendWithMarker(projectRoot, 'AGENTS.md', renderAdapter('adapters/agents-md.md'));
205
205
  }
206
+ if (tools.includes('kimi')) {
207
+ installFile(projectRoot, path.join('.kimi-code', 'AGENTS.md'), renderAdapter('adapters/kimi.md'), ctx);
208
+ }
206
209
  }
207
210
 
208
211
  // ---------- scaffold ----------
@@ -309,7 +312,7 @@ export async function resolveTools(args, {
309
312
  return picked;
310
313
  }
311
314
 
312
- function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
315
+ function printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded }) {
313
316
  const s = summarizeInstall(ctx.manifest.keys(), tools);
314
317
  const stats = ctx.stats;
315
318
  const line = (text) => console.log(` ${text}`);
@@ -326,7 +329,10 @@ function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
326
329
  }
327
330
  line(`${s.hooks} hooks in sdlc/.hooks/`);
328
331
  line(`Playbook: sdlc/.playbook/ (${s.playbook} stages + ${s.templates} templates)`);
329
- 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}`);
330
336
  line(style.dim(`Files: ${stats.written} written · ${stats.current} unchanged · ${stats.updated} refreshed · ${stats.kept} kept (yours)`));
331
337
  console.log('');
332
338
  console.log(` ${style.bold('Getting started:')}`);
@@ -338,7 +344,26 @@ export async function cmdInit(projectRoot, args) {
338
344
  const style = createStyle(colorEnabled());
339
345
  const symbols = symbolsFor();
340
346
  const tools = await resolveTools(args, { projectRoot, style });
341
- 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
+
342
367
  const ctx = {
343
368
  mode: 'install',
344
369
  manifest: new Map(),
@@ -351,7 +376,7 @@ export async function cmdInit(projectRoot, args) {
351
376
  writeManifestFile(projectRoot, ctx.manifest);
352
377
  ensureGitignore(projectRoot);
353
378
  for (const w of ctx.warnings) console.warn(` ${style.yellow(symbols.warn)} ${w}`);
354
- printInitSummary(tools, ctx, style, symbols, { configExisted });
379
+ printInitSummary(tools, ctx, style, symbols, { configExisted, toolsAdded });
355
380
  return { tools };
356
381
  }
357
382
 
@@ -392,6 +417,25 @@ export function forceNeedsAPerson(args, env = process.env, stdin = process.stdin
392
417
  + ' Run it yourself, or set WARNYIN_SDLC_FORCE=1 if this really is automation that meant it.';
393
418
  }
394
419
 
420
+ // Rewrites just the `tools:` line of `config.yaml` in place, leaving every other line
421
+ // untouched — a no-op when the file carries no `tools:` line at all (a config predating the
422
+ // key; matched, not fixed). Shared by `cmdInit` and `cmdUpdate` so the two paths can't drift.
423
+ // Reproduces the existing behavior exactly, trailing inline comment included: the line is
424
+ // replaced wholesale, so a hand-added comment after `tools: [...]` does not survive a rewrite.
425
+ // Returns whether it actually wrote — callers that report "recorded X" to the user must not
426
+ // claim it when this was a no-op (no `tools:` line to rewrite).
427
+ export function persistToolsLine(configPath, tools) {
428
+ const raw = fs.readFileSync(configPath, 'utf8');
429
+ if (!/^tools:/m.test(raw)) return false;
430
+ // A replacer FUNCTION, not a string: `recorded` (unioned in by cmdInit) is free text read
431
+ // back from the project's own config.yaml, not validated against the TOOLS registry the way
432
+ // CLI-sourced tool names are — a string replacement would let `$&`/`$'`/`` $` `` in a
433
+ // hand-edited or corrupted config get interpreted as replacement patterns instead of literal
434
+ // text, silently duplicating or mangling the surrounding file.
435
+ fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, () => `tools: [${tools.join(', ')}]`));
436
+ return true;
437
+ }
438
+
395
439
  export function cmdUpdate(projectRoot, args) {
396
440
  const refusal = refuseSelfUpdate(projectRoot, PKG_ROOT);
397
441
  if (refusal) throw new Error(refusal);
@@ -411,9 +455,7 @@ export function cmdUpdate(projectRoot, args) {
411
455
  // Persist an explicit --tool override so declared and installed state never
412
456
  // diverge (otherwise pruning tool-specific files leaves config.yaml stale).
413
457
  if (args.toolProvided) {
414
- const configPath = path.join(sdlcRoot, 'config.yaml');
415
- const raw = fs.readFileSync(configPath, 'utf8');
416
- fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, `tools: [${tools.join(', ')}]`));
458
+ persistToolsLine(path.join(sdlcRoot, 'config.yaml'), tools);
417
459
  }
418
460
 
419
461
  // Read before scaffolding: recordPayloadVersion overwrites version.json with our own.
package/bin/detect.mjs CHANGED
@@ -15,6 +15,7 @@ export const ADAPTER_PATHS = Object.freeze({
15
15
  cline: '.clinerules',
16
16
  gemini: 'GEMINI.md',
17
17
  'agents-md': 'AGENTS.md',
18
+ kimi: '.kimi-code/AGENTS.md',
18
19
  });
19
20
 
20
21
  export const TOOL_NAMES = Object.freeze({
@@ -25,6 +26,7 @@ export const TOOL_NAMES = Object.freeze({
25
26
  cline: 'Cline',
26
27
  gemini: 'Gemini CLI',
27
28
  'agents-md': 'AGENTS.md',
29
+ kimi: 'Kimi Code',
28
30
  });
29
31
 
30
32
  // A tool counts as present when the project already carries its home directory
@@ -38,6 +40,7 @@ const MARKERS = Object.freeze({
38
40
  cline: ['.clinerules'],
39
41
  gemini: ['GEMINI.md', '.gemini'],
40
42
  'agents-md': ['AGENTS.md'],
43
+ kimi: ['.kimi-code'],
41
44
  });
42
45
 
43
46
  export function detectTools(projectRoot) {
package/lib/manifest.mjs CHANGED
@@ -19,6 +19,7 @@ const SKILL_ALLOW_RE = /^\.claude\/skills\/(delta-spec-format|contract-writing|s
19
19
  const ADAPTER_ALLOW = new Set([
20
20
  '.cursor/rules/sdlc.mdc',
21
21
  '.windsurf/rules/sdlc.md',
22
+ '.kimi-code/AGENTS.md',
22
23
  ]);
23
24
 
24
25
  export function parseManifest(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warnyin/sdlc",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
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": {
@@ -0,0 +1,4 @@
1
+ # Warnyin SDLC workflow rules
2
+ Stage playbooks: `sdlc/.playbook/` (start at README.md) — read the stage playbook before acting.
3
+
4
+ {{RULES_CARD}}