@warnyin/sdlc 0.1.2 → 0.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +66 -19
  2. package/LICENSE +21 -21
  3. package/README.md +83 -74
  4. package/bin/cli.mjs +103 -30
  5. package/bin/detect.mjs +51 -0
  6. package/bin/multiselect.mjs +147 -0
  7. package/bin/ui.mjs +78 -0
  8. package/lib/caps.mjs +45 -45
  9. package/lib/config.mjs +41 -41
  10. package/lib/delta.mjs +160 -160
  11. package/lib/frontmatter.mjs +59 -59
  12. package/lib/glob.mjs +29 -29
  13. package/lib/manifest.mjs +99 -99
  14. package/lib/observe.mjs +174 -174
  15. package/lib/settings-merge.mjs +63 -63
  16. package/lib/usage.mjs +46 -46
  17. package/lib/validate.mjs +186 -186
  18. package/package.json +42 -42
  19. package/payload/adapters/agents-md.md +8 -8
  20. package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
  21. package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
  22. package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
  23. package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
  24. package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
  25. package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
  26. package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
  27. package/payload/adapters/claude/agents/sdlc-security.md +12 -12
  28. package/payload/adapters/claude/commands/sdlc/auto.md +5 -5
  29. package/payload/adapters/claude/commands/sdlc/build.md +5 -5
  30. package/payload/adapters/claude/commands/sdlc/contract.md +5 -5
  31. package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
  32. package/payload/adapters/claude/commands/sdlc/design.md +5 -5
  33. package/payload/adapters/claude/commands/sdlc/init.md +4 -4
  34. package/payload/adapters/claude/commands/sdlc/new.md +5 -5
  35. package/payload/adapters/claude/commands/sdlc/next.md +4 -4
  36. package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
  37. package/payload/adapters/claude/commands/sdlc/review.md +5 -5
  38. package/payload/adapters/claude/commands/sdlc/ship.md +5 -5
  39. package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
  40. package/payload/adapters/claude/commands/sdlc/verify.md +5 -5
  41. package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
  42. package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +33 -33
  43. package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +26 -26
  44. package/payload/adapters/cline.md +8 -8
  45. package/payload/adapters/copilot.md +8 -8
  46. package/payload/adapters/cursor.mdc +7 -7
  47. package/payload/adapters/gemini.md +8 -8
  48. package/payload/adapters/windsurf.md +4 -4
  49. package/payload/hooks/_shared.mjs +154 -154
  50. package/payload/hooks/guard-writes.mjs +83 -83
  51. package/payload/hooks/inject-context.mjs +55 -55
  52. package/payload/hooks/journal.mjs +58 -58
  53. package/payload/hooks/session-summary.mjs +50 -50
  54. package/payload/hooks/validate-artifact.mjs +80 -80
  55. package/payload/playbook/README.md +30 -30
  56. package/payload/playbook/auto.md +21 -21
  57. package/payload/playbook/build.md +23 -23
  58. package/payload/playbook/context.md +26 -26
  59. package/payload/playbook/contract.md +23 -23
  60. package/payload/playbook/converge.md +19 -19
  61. package/payload/playbook/design.md +20 -20
  62. package/payload/playbook/init.md +22 -22
  63. package/payload/playbook/new.md +22 -22
  64. package/payload/playbook/next.md +12 -12
  65. package/payload/playbook/observe.md +20 -20
  66. package/payload/playbook/principles.md +28 -28
  67. package/payload/playbook/review.md +17 -17
  68. package/payload/playbook/routing.md +19 -19
  69. package/payload/playbook/rules-card.md +16 -16
  70. package/payload/playbook/ship.md +24 -24
  71. package/payload/playbook/steer.md +21 -21
  72. package/payload/playbook/verify.md +24 -24
  73. package/payload/templates/change-deep.md +29 -29
  74. package/payload/templates/change-standard.md +28 -28
  75. package/payload/templates/change-vibe.md +19 -19
  76. package/payload/templates/config.yaml +8 -8
  77. package/payload/templates/constitution.md +14 -14
  78. package/payload/templates/contract-evals.md +9 -9
  79. package/payload/templates/contract-tests.md +9 -9
  80. package/payload/templates/harness.md +33 -33
  81. package/payload/templates/spec.md +14 -14
  82. package/payload/templates/steering.md +9 -9
  83. package/scripts/validate.mjs +47 -47
package/CHANGELOG.md CHANGED
@@ -1,19 +1,66 @@
1
- # Changelog
2
-
3
- ## 0.1.0 (2026-08-20)
4
-
5
- Initial release the full Day-1 SDLC loop:
6
-
7
- - CLI: `init` (multi-tool: claude/cursor/windsurf/copilot/cline/gemini/agents-md),
8
- `update` (ownership-aware refresh + guarded prune), `validate`, `status`,
9
- `observe`, `archive`.
10
- - Artifact model: constitution (≤30) + steering with inclusion modes + harness
11
- (routing/triage/autonomy policy) + living specs + delta-based changes
12
- (vibe/standard/deep caps 40/100/150) + contracts (tests ≤60, evals ≤40).
13
- - Managed Claude Code hooks: static-context injector, spec write-lock with TTL
14
- gates, artifact validator + steering pointers, session token/cost journaling,
15
- compact-event tracking.
16
- - 13 stage playbooks + 13 `/sdlc:*` commands + 3 background skills + 8 agents
17
- with model routing (cheap/balanced/deepest).
18
- - Observability: per-change tokens/cost, first-pass rate, lead time, dead
19
- steering and residency flags; post-ship learner loop (distill, never bloat).
1
+ # Changelog
2
+
3
+ ## 0.2.1 (2026-08-20)
4
+
5
+ - **Fix**: `archive` failed with `ENOENT` on the first change a repo ever ships.
6
+ `init` scaffolds `sdlc/changes/archive/`, but git does not track empty
7
+ directories, so the folder is absent for everyone who clones before that first
8
+ ship. The rename is now preceded by a `mkdir -p` of the archive root.
9
+
10
+ The failure landed mid-phase-2, after the delta had been merged into the living
11
+ specs, evals promoted, `status: shipped` stamped and the ship event journalled —
12
+ a repo left half-shipped while the CLI reported total failure. The directory is
13
+ now prepared next to the other destination checks, before phase 1 computes a
14
+ single merge, so an unusable archive path aborts with the specs untouched.
15
+
16
+ ## 0.2.0 (2026-08-20)
17
+
18
+ `init` is now an installer you can actually see working — still zero dependencies.
19
+
20
+ - **Interactive tool picker**: a searchable checkbox list replaces the
21
+ comma-separated typing prompt. Arrows move, `space` toggles, typing filters,
22
+ `ctrl+a` selects everything on screen, `enter` confirms, `ctrl+c` cancels
23
+ without installing anything.
24
+ - **Tool detection**: tools the project already uses (`.claude/`, `.cursor/`,
25
+ `AGENTS.md`, …) come pre-selected; an empty project still defaults to claude.
26
+ - **Post-install summary**: artifact counts, the adapter path per tool, a
27
+ written/unchanged/refreshed/kept tally, and Getting-started hints that differ
28
+ per tool (slash commands for Claude Code, prose for the rest).
29
+ - **Colour** gated on `NO_COLOR` > `FORCE_COLOR` > TTY, with an ASCII glyph
30
+ fallback for legacy Windows consoles.
31
+ - `--tool` accepts `all` and `none`; `--tools` is an alias. An empty `--tool`,
32
+ an unknown tool, or `all` mixed with a named tool now fails loudly.
33
+ - **Fix**: `update` read an explicit `tools: []` (what `init --tool none` writes)
34
+ as "unset" and reinstalled claude. A missing key and a declared-empty one are
35
+ now distinguished.
36
+
37
+ ## 0.1.2 (2026-08-20)
38
+
39
+ - Fix: `readStdinJson()` grew a 1s grace timeout — journal and hook utilities no
40
+ longer hang when stdin is open but idle.
41
+ - Fix: hooks release stdin (pause + unref) so an open-idle stdin cannot keep a
42
+ hook process alive.
43
+
44
+ ## 0.1.1 (2026-08-20)
45
+
46
+ - Fix: the entrypoint guard must realpath `process.argv[1]` — npx invokes through
47
+ a `node_modules/.bin` symlink, which made the CLI a silent no-op.
48
+ - Fix (CI): `node --test` bare discovery; a quoted glob is not expanded on Node 20.
49
+
50
+ ## 0.1.0 (2026-08-20)
51
+
52
+ Initial release — the full Day-1 SDLC loop:
53
+
54
+ - CLI: `init` (multi-tool: claude/cursor/windsurf/copilot/cline/gemini/agents-md),
55
+ `update` (ownership-aware refresh + guarded prune), `validate`, `status`,
56
+ `observe`, `archive`.
57
+ - Artifact model: constitution (≤30) + steering with inclusion modes + harness
58
+ (routing/triage/autonomy policy) + living specs + delta-based changes
59
+ (vibe/standard/deep caps 40/100/150) + contracts (tests ≤60, evals ≤40).
60
+ - Managed Claude Code hooks: static-context injector, spec write-lock with TTL
61
+ gates, artifact validator + steering pointers, session token/cost journaling,
62
+ compact-event tracking.
63
+ - 13 stage playbooks + 13 `/sdlc:*` commands + 3 background skills + 8 agents
64
+ with model routing (cheap/balanced/deepest).
65
+ - Observability: per-change tokens/cost, first-pass rate, lead time, dead
66
+ steering and residency flags; post-ship learner loop (distill, never bloat).
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 warnyin
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 warnyin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,74 +1,83 @@
1
- # @warnyin/sdlc
2
-
3
- **Spec-driven, AI-driven SDLC for coding agents — token-lean by construction.**
4
-
5
- Operationalizes the *"New SDLC with Vibe Coding"* (Day-1) work process: the human configures
6
- the harness once; the AI drives changes end-to-end through contract-first gates; deterministic
7
- hooks and a validator enforce the rules; a journal prices every change in real tokens.
8
-
9
- Inspired by OpenSpec (delta specs, archive lifecycle), spec-kit (artifact grammar), and
10
- Kiro (steering + enforced hooks) — tuned for minimum context residency.
11
-
12
- ## Install
13
-
14
- ```bash
15
- cd your-project
16
- npx @warnyin/sdlc init # pick tools: claude, cursor, windsurf, copilot, cline, gemini, agents-md
17
- ```
18
-
19
- Then in your coding agent:
20
-
21
- ```
22
- /sdlc:init # interview → constitution + harness (the one human gate)
23
- /sdlc:auto Add rate limiting # AI runs new → contract → build → verify → ship
24
- ```
25
-
26
- ## How it works
27
-
28
- ```
29
- sdlc/
30
- ├── context/constitution.md ≤30 lines — the ONLY always-loaded prose (hook-injected)
31
- ├── context/steering/*.md scoped knowledge · inclusion: always|paths|manual|agent
32
- ├── harness.md tools, model routing, tier triage, autonomy policy
33
- ├── specs/<capability>/ living specs — WHEN/THEN SHALL, merged mechanically at ship
34
- ├── changes/<id>/ one change: change.md (Why+Delta+Tasks) + contract/ + journal
35
- └── changes/archive/ shipped changes + digests (the async human touchpoint)
36
- ```
37
-
38
- - **Contract-first**: tests + evals are written *before* code and gate everything after.
39
- - **AI-driven, exception-only humans**: the autonomy policy in `harness.md` decides what
40
- auto-ships and what escalates (hard-floor: security/payments/data-loss/irreversible).
41
- - **Managed hooks** (Claude Code): SessionStart injects ≤60 lines of static context;
42
- PreToolUse write-locks living specs; PostToolUse validates caps and points at steering;
43
- Stop journals real token usage. Other tools get the same rules as prose + the validator.
44
- - **Self-improving, leaner over time**: a post-ship learner proposes rules with evidence;
45
- the always-loaded budget is fixed, so learning must distill, not accumulate.
46
- - **Measured**: `npx @warnyin/sdlc observe` — tokens/cost per change, first-pass rate,
47
- lead time, dead steering, context-overflow flags.
48
-
49
- ## CLI
50
-
51
- ```
52
- warnyin-sdlc init [--tool ...] scaffold + adapters + hooks
53
- warnyin-sdlc update [--force] refresh payload, guarded prune of stale files
54
- warnyin-sdlc validate [id] [--strict]
55
- warnyin-sdlc status | observe [--json]
56
- warnyin-sdlc archive <id> merge deltas into living specs + archive
57
- ```
58
-
59
- ## Commands (in your agent)
60
-
61
- `/sdlc:init` · `/sdlc:auto` · `/sdlc:new` · `/sdlc:design` · `/sdlc:contract` · `/sdlc:build`
62
- · `/sdlc:verify` · `/sdlc:review` · `/sdlc:ship` · `/sdlc:observe` · `/sdlc:converge`
63
- · `/sdlc:steer` · `/sdlc:next`
64
-
65
- Playbooks live in `sdlc/.playbook/` behavior is defined once there; commands are thin pointers.
66
-
67
- ## Development
68
-
69
- Zero dependencies, Node ≥ 20. `npm test` runs the black-box suite (temp dirs, real CLI spawns).
70
- This repo self-hosts: its own development flows through `sdlc/changes/`. After cloning, run
71
- `npm run setup:dogfood` to regenerate the installer-owned mirrors (`sdlc/.playbook/`,
72
- `sdlc/.hooks/`, `.claude/`).
73
-
74
- MIT
1
+ # @warnyin/sdlc
2
+
3
+ **Spec-driven, AI-driven SDLC for coding agents — token-lean by construction.**
4
+
5
+ Operationalizes the *"New SDLC with Vibe Coding"* (Day-1) work process: the human configures
6
+ the harness once; the AI drives changes end-to-end through contract-first gates; deterministic
7
+ hooks and a validator enforce the rules; a journal prices every change in real tokens.
8
+
9
+ Inspired by OpenSpec (delta specs, archive lifecycle), spec-kit (artifact grammar), and
10
+ Kiro (steering + enforced hooks) — tuned for minimum context residency.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ cd your-project
16
+ npx @warnyin/sdlc init # interactive picker; tools already in the project are pre-selected
17
+ ```
18
+
19
+ The picker is a checkbox list — arrows move, `space` toggles, typing filters, `ctrl+a` selects
20
+ everything on screen, `enter` confirms. Skip it in CI or scripts:
21
+
22
+ ```bash
23
+ npx @warnyin/sdlc init --tool claude,cursor # explicit list
24
+ npx @warnyin/sdlc init --tool all # every supported tool
25
+ npx @warnyin/sdlc init --tool none # sdlc/ framework only, no agent adapters
26
+ ```
27
+
28
+ Then in your coding agent:
29
+
30
+ ```
31
+ /sdlc:init # interview constitution + harness (the one human gate)
32
+ /sdlc:auto Add rate limiting # AI runs new → contract → build → verify → ship
33
+ ```
34
+
35
+ ## How it works
36
+
37
+ ```
38
+ sdlc/
39
+ ├── context/constitution.md ≤30 lines the ONLY always-loaded prose (hook-injected)
40
+ ├── context/steering/*.md scoped knowledge · inclusion: always|paths|manual|agent
41
+ ├── harness.md tools, model routing, tier triage, autonomy policy
42
+ ├── specs/<capability>/ living specs WHEN/THEN SHALL, merged mechanically at ship
43
+ ├── changes/<id>/ one change: change.md (Why+Delta+Tasks) + contract/ + journal
44
+ └── changes/archive/ shipped changes + digests (the async human touchpoint)
45
+ ```
46
+
47
+ - **Contract-first**: tests + evals are written *before* code and gate everything after.
48
+ - **AI-driven, exception-only humans**: the autonomy policy in `harness.md` decides what
49
+ auto-ships and what escalates (hard-floor: security/payments/data-loss/irreversible).
50
+ - **Managed hooks** (Claude Code): SessionStart injects ≤60 lines of static context;
51
+ PreToolUse write-locks living specs; PostToolUse validates caps and points at steering;
52
+ Stop journals real token usage. Other tools get the same rules as prose + the validator.
53
+ - **Self-improving, leaner over time**: a post-ship learner proposes rules with evidence;
54
+ the always-loaded budget is fixed, so learning must distill, not accumulate.
55
+ - **Measured**: `npx @warnyin/sdlc observe` — tokens/cost per change, first-pass rate,
56
+ lead time, dead steering, context-overflow flags.
57
+
58
+ ## CLI
59
+
60
+ ```
61
+ warnyin-sdlc init [--tool all|none|a,b] scaffold + adapters + hooks (picker when omitted)
62
+ warnyin-sdlc update [--force] refresh payload, guarded prune of stale files
63
+ warnyin-sdlc validate [id] [--strict]
64
+ warnyin-sdlc status | observe [--json]
65
+ warnyin-sdlc archive <id> merge deltas into living specs + archive
66
+ ```
67
+
68
+ ## Commands (in your agent)
69
+
70
+ `/sdlc:init` · `/sdlc:auto` · `/sdlc:new` · `/sdlc:design` · `/sdlc:contract` · `/sdlc:build`
71
+ · `/sdlc:verify` · `/sdlc:review` · `/sdlc:ship` · `/sdlc:observe` · `/sdlc:converge`
72
+ · `/sdlc:steer` · `/sdlc:next`
73
+
74
+ Playbooks live in `sdlc/.playbook/` — behavior is defined once there; commands are thin pointers.
75
+
76
+ ## Development
77
+
78
+ Zero dependencies, Node ≥ 20. `npm test` runs the black-box suite (temp dirs, real CLI spawns).
79
+ This repo self-hosts: its own development flows through `sdlc/changes/`. After cloning, run
80
+ `npm run setup:dogfood` to regenerate the installer-owned mirrors (`sdlc/.playbook/`,
81
+ `sdlc/.hooks/`, `.claude/`).
82
+
83
+ MIT
package/bin/cli.mjs CHANGED
@@ -7,7 +7,6 @@ import fs from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import crypto from 'node:crypto';
9
9
  import process from 'node:process';
10
- import readline from 'node:readline';
11
10
  import { spawnSync } from 'node:child_process';
12
11
  import { fileURLToPath } from 'node:url';
13
12
  import { parseFrontmatter } from '../lib/frontmatter.mjs';
@@ -17,6 +16,9 @@ import { mergeHookSettings } from '../lib/settings-merge.mjs';
17
16
  import { buildReport, renderReport } from '../lib/observe.mjs';
18
17
  import { parseManifest, renderManifest, computeStale, containedIn, hasSymlinkSegment, PRUNE_BLAST_CAP } from '../lib/manifest.mjs';
19
18
  import { validateAll, formatIssues, listChangeDirs } from '../lib/validate.mjs';
19
+ import { detectTools, toolName } from './detect.mjs';
20
+ import { colorEnabled, createStyle, symbolsFor, summarizeInstall, startHints } from './ui.mjs';
21
+ import { multiSelect } from './multiselect.mjs';
20
22
 
21
23
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
24
  const PAYLOAD = path.join(PKG_ROOT, 'payload');
@@ -43,10 +45,13 @@ export function sha256(content) {
43
45
  }
44
46
 
45
47
  export function parseArgs(argv) {
46
- const args = { _: [], tool: null, strict: false, force: false, json: false, help: false };
48
+ const args = { _: [], tool: null, toolProvided: false, strict: false, force: false, json: false, help: false };
47
49
  for (let i = 0; i < argv.length; i++) {
48
50
  const a = argv[i];
49
- if (a === '--tool') args.tool = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
51
+ if (a === '--tool' || a === '--tools') {
52
+ args.toolProvided = true;
53
+ args.tool = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
54
+ }
50
55
  else if (a === '--strict') args.strict = true;
51
56
  else if (a === '--force') args.force = true;
52
57
  else if (a === '--json') args.json = true;
@@ -69,6 +74,11 @@ function writeFileNormalized(dest, content) {
69
74
  return out;
70
75
  }
71
76
 
77
+ function tally(ctx, outcome) {
78
+ if (ctx.stats) ctx.stats[outcome] = (ctx.stats[outcome] ?? 0) + 1;
79
+ return outcome;
80
+ }
81
+
72
82
  export function installFile(projectRoot, destRel, content, ctx) {
73
83
  const relPosix = toPosix(destRel);
74
84
  const dest = path.join(projectRoot, destRel);
@@ -78,21 +88,21 @@ export function installFile(projectRoot, destRel, content, ctx) {
78
88
  if (!fs.existsSync(dest)) {
79
89
  writeFileNormalized(dest, next);
80
90
  ctx.manifest.set(relPosix, nextHash);
81
- return 'written';
91
+ return tally(ctx, 'written');
82
92
  }
83
93
  const current = normalizeEol(fs.readFileSync(dest, 'utf8'));
84
94
  const currentHash = sha256(current);
85
95
  if (currentHash === nextHash) {
86
96
  ctx.manifest.set(relPosix, nextHash);
87
- return 'current';
97
+ return tally(ctx, 'current');
88
98
  }
89
99
  if (ctx.mode === 'update' && ctx.oldManifest?.get(relPosix) === currentHash) {
90
100
  writeFileNormalized(dest, next);
91
101
  ctx.manifest.set(relPosix, nextHash);
92
- return 'updated';
102
+ return tally(ctx, 'updated');
93
103
  }
94
104
  ctx.warnings.push(`kept (user-modified): ${relPosix}`);
95
- return 'kept';
105
+ return tally(ctx, 'kept');
96
106
  }
97
107
 
98
108
  function copyTree(srcDir, destDirRel, projectRoot, ctx) {
@@ -218,35 +228,88 @@ export function ensureGitignore(projectRoot) {
218
228
 
219
229
  // ---------- init ----------
220
230
 
221
- export async function resolveTools(args, { interactive = process.stdin.isTTY && process.stdout.isTTY } = {}) {
222
- if (args.tool?.length) {
223
- const bad = args.tool.filter((t) => !TOOLS.includes(t));
224
- if (bad.length) throw new Error(`unknown tool(s): ${bad.join(', ')} valid: ${TOOLS.join(', ')}`);
225
- return args.tool;
231
+ // `all` / `none` are reserved words, never combinable with a list mixing
232
+ // them would leave the caller guessing which one won.
233
+ export function resolveToolList(picked) {
234
+ const reserved = picked.filter((t) => t === 'all' || t === 'none');
235
+ if (reserved.length && picked.length > 1) {
236
+ throw new Error(`"${reserved[0]}" cannot be combined with other tools`);
226
237
  }
227
- if (!interactive) return ['claude'];
228
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
229
- const answer = await new Promise((resolve) => {
230
- rl.question(`Tools to set up [${TOOLS.join(', ')}] (comma-separated, default: claude): `, resolve);
231
- });
232
- rl.close();
233
- const picked = answer.split(',').map((s) => s.trim()).filter(Boolean);
234
- if (!picked.length) return ['claude'];
238
+ if (picked[0] === 'all') return [...TOOLS];
239
+ if (picked[0] === 'none') return [];
240
+ if (!picked.length) throw new Error(`--tool requires a value: all, none, or any of ${TOOLS.join(', ')}`);
235
241
  const bad = picked.filter((t) => !TOOLS.includes(t));
236
- if (bad.length) throw new Error(`unknown tool(s): ${bad.join(', ')}`);
242
+ if (bad.length) throw new Error(`unknown tool(s): ${bad.join(', ')} — valid: ${TOOLS.join(', ')}, all, none`);
243
+ return [...new Set(picked)];
244
+ }
245
+
246
+ export async function resolveTools(args, {
247
+ interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
248
+ projectRoot = process.cwd(),
249
+ style = createStyle(false),
250
+ } = {}) {
251
+ if (args.toolProvided) return resolveToolList(args.tool ?? []);
252
+ if (!interactive) return ['claude'];
253
+
254
+ const detected = detectTools(projectRoot);
255
+ if (detected.length) {
256
+ console.log(style.dim(`Detected in this project: ${detected.map(toolName).join(', ')} (pre-selected)`));
257
+ }
258
+ const choices = TOOLS.map((tool) => ({
259
+ value: tool,
260
+ name: toolName(tool),
261
+ note: detected.includes(tool) ? 'detected' : '',
262
+ // First-time setup with nothing detected still needs a sane default.
263
+ preSelected: detected.length ? detected.includes(tool) : tool === 'claude',
264
+ }));
265
+ const picked = await multiSelect({ choices, style, symbols: symbolsFor() });
266
+ if (picked === null) throw new Error('cancelled — nothing was installed');
237
267
  return picked;
238
268
  }
239
269
 
270
+ function printInitSummary(tools, ctx, style, symbols, { configExisted }) {
271
+ const s = summarizeInstall(ctx.manifest.keys(), tools);
272
+ const stats = ctx.stats;
273
+ const line = (text) => console.log(` ${text}`);
274
+
275
+ console.log('');
276
+ console.log(` ${style.green(symbols.tick)} ${style.bold('SDLC Setup Complete')}`);
277
+ console.log('');
278
+ line(`Tools: ${tools.length ? tools.map(toolName).join(', ') : style.dim('none (framework only)')}`);
279
+ if (s.commands || s.skills || s.agents) {
280
+ line(`${s.commands} commands, ${s.skills} skills and ${s.agents} agents in .claude/`);
281
+ }
282
+ for (const a of s.adapters.filter((a) => a.tool !== 'claude')) {
283
+ line(`Rules for ${toolName(a.tool)}: ${a.path}`);
284
+ }
285
+ line(`${s.hooks} hooks in sdlc/.hooks/`);
286
+ line(`Playbook: sdlc/.playbook/ (${s.playbook} stages + ${s.templates} templates)`);
287
+ line(`Config: sdlc/config.yaml${configExisted ? ' (kept)' : ''}`);
288
+ line(style.dim(`Files: ${stats.written} written · ${stats.current} unchanged · ${stats.updated} refreshed · ${stats.kept} kept (yours)`));
289
+ console.log('');
290
+ console.log(` ${style.bold('Getting started:')}`);
291
+ startHints(tools).forEach((hint, i) => line(` ${i + 1}. ${hint}`));
292
+ console.log('');
293
+ }
294
+
240
295
  export async function cmdInit(projectRoot, args) {
241
- const tools = await resolveTools(args);
242
- const ctx = { mode: 'install', manifest: new Map(), oldManifest: readManifestFile(projectRoot), warnings: [] };
296
+ const style = createStyle(colorEnabled());
297
+ const symbols = symbolsFor();
298
+ const tools = await resolveTools(args, { projectRoot, style });
299
+ const configExisted = fs.existsSync(path.join(projectRoot, 'sdlc', 'config.yaml'));
300
+ const ctx = {
301
+ mode: 'install',
302
+ manifest: new Map(),
303
+ oldManifest: readManifestFile(projectRoot),
304
+ warnings: [],
305
+ stats: { written: 0, current: 0, updated: 0, kept: 0 },
306
+ };
243
307
  scaffoldSdlc(projectRoot, tools, ctx);
244
308
  installToolAdapters(projectRoot, tools, ctx);
245
309
  writeManifestFile(projectRoot, ctx.manifest);
246
310
  ensureGitignore(projectRoot);
247
- for (const w of ctx.warnings) console.warn(` ${w}`);
248
- console.log(`sdlc/ initialized for: ${tools.join(', ')}`);
249
- console.log('Next: open your coding agent and run /sdlc:init to write your constitution and harness.');
311
+ for (const w of ctx.warnings) console.warn(` ${style.yellow(symbols.warn)} ${w}`);
312
+ printInitSummary(tools, ctx, style, symbols, { configExisted });
250
313
  return { tools };
251
314
  }
252
315
 
@@ -255,12 +318,17 @@ export async function cmdInit(projectRoot, args) {
255
318
  export function cmdUpdate(projectRoot, args) {
256
319
  const sdlcRoot = path.join(projectRoot, 'sdlc');
257
320
  requireSdlc(sdlcRoot);
258
- const config = parseConfig(fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8'));
259
- const tools = args.tool?.length ? args.tool : (config.tools.length ? config.tools : ['claude']);
321
+ const configRaw = fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8');
322
+ const config = parseConfig(configRaw);
323
+ // An explicit `tools: []` (from `init --tool none`) is a decision, not a gap:
324
+ // only a config that never declared the key at all falls back to claude.
325
+ const tools = args.toolProvided
326
+ ? resolveToolList(args.tool ?? [])
327
+ : (/^tools:/m.test(configRaw) ? config.tools : ['claude']);
260
328
 
261
329
  // Persist an explicit --tool override so declared and installed state never
262
330
  // diverge (otherwise pruning tool-specific files leaves config.yaml stale).
263
- if (args.tool?.length) {
331
+ if (args.toolProvided) {
264
332
  const configPath = path.join(sdlcRoot, 'config.yaml');
265
333
  const raw = fs.readFileSync(configPath, 'utf8');
266
334
  fs.writeFileSync(configPath, raw.replace(/^tools:.*$/m, `tools: [${tools.join(', ')}]`));
@@ -369,6 +437,11 @@ export function cmdArchive(projectRoot, changeId, { strict = true } = {}) {
369
437
  // while reporting failure.
370
438
  const date = new Date().toISOString().slice(0, 10);
371
439
  const destDir = path.join(sdlcRoot, 'changes', 'archive', `${date}-${changeId}`);
440
+ // `init` scaffolds changes/archive/, but git does not track empty directories:
441
+ // it is absent for anyone who cloned before the first change shipped. Prepare
442
+ // it here, with the other destination checks, so a bad archive path fails
443
+ // while the specs are still untouched instead of ENOENT-ing at the rename.
444
+ fs.mkdirSync(path.dirname(destDir), { recursive: true });
372
445
  if (fs.existsSync(destDir)) {
373
446
  throw new Error(`archive target already exists: ${toPosix(path.relative(projectRoot, destDir))} — nothing was merged`);
374
447
  }
@@ -438,7 +511,7 @@ const HELP = `@warnyin/sdlc — spec-driven AI-SDLC framework
438
511
 
439
512
  usage: warnyin-sdlc <command> [options]
440
513
 
441
- init [--tool claude,cursor,...] scaffold sdlc/ + adapters + hooks into this project
514
+ init [--tool all|none|a,b] scaffold sdlc/ + adapters + hooks (interactive picker when omitted)
442
515
  update [--tool ...] [--force] refresh payload-owned files, prune stale ones (guarded)
443
516
  validate [id] [--strict] structural validation (caps, delta grammar, gates)
444
517
  status [--json] list active changes and their stage
package/bin/detect.mjs ADDED
@@ -0,0 +1,51 @@
1
+ // Which agent tools a project already uses, and where each adapter lands.
2
+ // CLI-only: this must NOT move into lib/, which is copied verbatim into every
3
+ // installed project as sdlc/.hooks/lib/.
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ // The one user-visible path per tool — detection and the post-install summary
9
+ // both name the same file, so nobody has to guess what `init` touched.
10
+ export const ADAPTER_PATHS = Object.freeze({
11
+ claude: '.claude/',
12
+ cursor: '.cursor/rules/sdlc.mdc',
13
+ windsurf: '.windsurf/rules/sdlc.md',
14
+ copilot: '.github/copilot-instructions.md',
15
+ cline: '.clinerules',
16
+ gemini: 'GEMINI.md',
17
+ 'agents-md': 'AGENTS.md',
18
+ });
19
+
20
+ export const TOOL_NAMES = Object.freeze({
21
+ claude: 'Claude Code',
22
+ cursor: 'Cursor',
23
+ windsurf: 'Windsurf',
24
+ copilot: 'GitHub Copilot',
25
+ cline: 'Cline',
26
+ gemini: 'Gemini CLI',
27
+ 'agents-md': 'AGENTS.md',
28
+ });
29
+
30
+ // A tool counts as present when the project already carries its home directory
31
+ // or instruction file. Detection only pre-selects a checkbox — it never
32
+ // installs anything on its own.
33
+ const MARKERS = Object.freeze({
34
+ claude: ['.claude'],
35
+ cursor: ['.cursor'],
36
+ windsurf: ['.windsurf'],
37
+ copilot: ['.github/copilot-instructions.md'],
38
+ cline: ['.clinerules'],
39
+ gemini: ['GEMINI.md', '.gemini'],
40
+ 'agents-md': ['AGENTS.md'],
41
+ });
42
+
43
+ export function detectTools(projectRoot) {
44
+ return Object.entries(MARKERS)
45
+ .filter(([, markers]) => markers.some((m) => fs.existsSync(path.join(projectRoot, ...m.split('/')))))
46
+ .map(([tool]) => tool);
47
+ }
48
+
49
+ export function toolName(tool) {
50
+ return TOOL_NAMES[tool] ?? tool;
51
+ }