wdi-method 0.3.0 → 0.4.3

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
@@ -1,179 +1,207 @@
1
1
  # WDI Method
2
2
 
3
- Metode pengiriman perangkat lunak WDI. Membungkus BMad; tidak menggantikannya.
3
+ A software delivery method for agent-driven work. It **wraps [BMad](https://github.com/bmad-code-org/BMAD-METHOD); it does not replace it.**
4
4
 
5
- WDI software delivery method. It wraps BMad; it does not replace it.
5
+ BMad decides *what to build* and *how to build it* well. What it leaves thin is the middle: the
6
+ documents a **human** reads to check that the decision is right before anybody writes code. WDI Method
7
+ adds that middle, and a way to choose how much of it you want.
6
8
 
7
- This repository is **public**. It MUST NOT contain a client name, a product name, or a
8
- link to any other private repository. Product identity lives in the consuming repo, at
9
- `.control/registry/index.yaml` (`product.name`, optional `product.client`), filled at G1.
9
+ This repository is **public** and generic. It MUST NOT contain a client name, a product name, or a link
10
+ to a private repository. Product identity lives in the consuming repo.
11
+
12
+ ---
10
13
 
11
14
  ## Install
12
15
 
13
- BMad first, then WDI Method. The wrappers call BMad skills; without BMad they cannot run.
16
+ BMad first, then this. The wrappers call BMad skills; without BMad they cannot run.
14
17
 
15
18
  ```bash
16
19
  cd /path/to/your/product-repo
17
20
  npx bmad-method install
18
- npx github:wiradigitalid/wdi-method
21
+ npx wdi-method
19
22
  ```
20
23
 
21
- Tanpa subcommand, installer membuka **TUI**: cek BMad, deteksi install vs update, tanya nama
22
- produk / klien, pilih agen, tampilkan folder yang akan ditulis, lalu langkah sesudahnya.
24
+ The second command opens a TUI: it checks BMad, detects install versus update, asks the product name and
25
+ the document language, lets you pick agents, shows what it will write, and prints what to do next.
23
26
 
24
- Folder korpus (`.constitution` `.control` `.what` `.how` `.work`) **bukan** opsi namanya
25
- identitas metode. Yang dipilih di TUI adalah repo tujuan dan agennya.
27
+ **Every field arrives with an answer already in it, and Enter accepts it.** On an update that answer is
28
+ what the repo already says; on a first install the product name is the folder name made readable —
29
+ `acme-billing-portal` offers `Acme Billing Portal`. Nothing is validated as required: a prompt that
30
+ refuses an empty submission while already holding a sensible default is asking you to retype something
31
+ the installer knows.
26
32
 
27
- Non-interactive (CI):
33
+ A value only changes when you actually answer. A run that does not mention language keeps the language the
34
+ repo already chose, and says so.
28
35
 
29
36
  ```bash
30
- npx github:wiradigitalid/wdi-method install --yes --agents cursor,claude --product "Nama Produk"
37
+ npx wdi-method update # later, to take a newer method
38
+ npx wdi-method verify # check the method files are all present
31
39
  ```
32
40
 
33
- ## Update
41
+ Non-interactive, for CI:
34
42
 
35
43
  ```bash
36
- npx github:wiradigitalid/wdi-method update
44
+ npx wdi-method install --yes --agents claude,codex --product "Your Product" \
45
+ --doc-language "Bahasa Indonesia"
37
46
  ```
38
47
 
39
- Update overwrites method files. It MUST NOT touch `.what/`, `.how/`, filled `.control/` state,
40
- existing `constitution.md` Articles 1–2 and 5, `codebase/*-guide.md` once `Accepted`, extra
41
- constitution files this repo added, or `_bmad/custom/*.user.toml`.
42
-
43
- `AGENTS.md` has a marked method block (`<!-- BEGIN:wdi-method -->` … `<!-- END:wdi-method -->`).
44
- Update replaces **that block only** — including how to install and update. Product sections
45
- outside it (`## Code`, extra boundaries) stay. A file without the markers gets the block
46
- injected before `## Code`; existing product prose above `## Language` is kept.
48
+ Then invoke the **`wdi-help`** skill and ask what to do next. It reads where the project actually is and
49
+ answers with the gate you are at, not with a menu.
47
50
 
48
- A stamp is written to `.control/wdi-method.yaml` (`wdi_method`, `bmad_method` if detectable).
49
- It is a trace, not a lockfile.
51
+ ---
50
52
 
51
- ## Agents
53
+ ## The gap this fills
52
54
 
53
- `--agents` chooses **where skills are copied**, not a different method. Default: all.
55
+ A gate is only as good as the artifact it reads. Between *"the architecture is decided"* and *"the code
56
+ is written"* there is a set of questions that decide whether a build goes straight or crooked, and they
57
+ are all **list-shaped**:
54
58
 
55
- | Flag | Writes |
56
- |---|---|
57
- | `claude` | `.claude/skills/wdi-*`, `CLAUDE.md` |
58
- | `cursor` | `.agents/skills/wdi-*`, `.cursorrules` |
59
- | `codex` | `AGENTS.md` |
60
- | `antigravity` | `.agents/skills/wdi-*`, `.agents/AGENTS.md` |
59
+ - Which use cases exist, and which of them touch money, personal data, or something irreversible?
60
+ - Which tables exist, and which component is allowed to **write** each one?
61
+ - Which endpoints exist, on which host, and which promise does each serve?
62
+ - Which screens exist, in which application?
63
+ - When a boundary fails halfway — the other side slow, absent, or lying — what does the user see?
61
64
 
62
- `AGENTS.md` is created on first install if missing. On update, only the marked method block
63
- is replaced.
65
+ Those questions have answers inside an architecture document and a build spec. What they usually do not
66
+ have is a **place where a person can read all of one kind at once** and notice the row that is missing,
67
+ the table with two owners, or the endpoint nobody promised.
64
68
 
65
- ## What travels, what does not
69
+ WDI Method's whole contribution is that place, plus the discipline that keeps it honest:
66
70
 
67
- | Travels (the method) | Stays in the product repo |
71
+ | | |
68
72
  |---|---|
69
- | `.constitution/` guides, templates, scripts, `method/` | `.control/` `.what/` `.how/` `_bmad-output/` `.work/` |
70
- | fifteen `wdi-*` skills | `constitution.md` that already exists (Articles 1, 2, 5) |
71
- | `_bmad/custom/*.toml` | `codebase/*-guide.md` once `Accepted` |
72
- | | **`.constitution/project/`** the product's custom room |
73
- | | `_bmad/custom/*.user.toml` |
73
+ | **Inventories** | Tables, endpoints, and screens as three flat lists — **derived from the code**, not hand-written, so the difference between plan and reality is a finding rather than an argument |
74
+ | **Use case catalogue** | One line per use case with its actor, the requirement it satisfies, and whether it is `critical` |
75
+ | **SRS / SDD** | What a component promises, and how it is built — one pair per component, in human language |
76
+ | **C4** | Context, containers, and one component view per container that carries more than one domain slice |
77
+ | **Robustness** | For the deepest mode: boundary, control, and entity objects per critical use case, before code |
78
+ | **Invariants** | A spine of `AD-N` rules that constrain every component, separate from the decisions that produced them |
74
79
 
75
- `.control/` empty stubs are written only when that folder is absent.
80
+ ---
76
81
 
77
- ## `.constitution/project/` the custom room
82
+ ## Two knobs, never merged
78
83
 
79
- Everything else in `.constitution/` belongs to the method and is **overwritten** on update. This one
80
- folder is not: `install` seeds it, `update` never writes over a file that exists in it, and `promote`
81
- **skips it entirely** — so a rule that names a client cannot reach this public repository.
84
+ The reason a method like this usually fails is that it asks for the same depth everywhere, so people
85
+ either drown in it or abandon it. WDI splits depth from scrutiny into **two independent fields**:
82
86
 
83
- | Goes there | Does not, and where it goes |
84
- |---|---|
85
- | A review policy a client requires | product / client name `index.yaml` `product:` |
86
- | A process rule that came from a contract | code conventions `codebase/*-guide.md` |
87
- | A policy that differs from the method default | scope, method ownership → `constitution.md` Art. 1, 2, 5 |
88
- | A prohibition specific to this domain | agent instructions → `AGENTS.md`, outside the marked block |
87
+ | Field | Controls | Values |
88
+ |---|---|---|
89
+ | `mode` | **Document depth**, and nothing else | `catalog` · `outline` · `guarded` · `deep` |
90
+ | `risk_accepted` | **Review intensity**, and nothing else | `low` · `medium` · `high` |
89
91
 
90
- **A generic rule MUST NOT be moved there.** If it holds in any project it belongs to the package — fix
91
- it there and `promote`. Using the room to bypass the package is how a method stops being generic with
92
- nobody deciding it, and **an empty room is a valid state**: filling it so it gets used is the very
93
- failure this rule prevents.
92
+ | `mode` | What is written per component | G4 |
93
+ |---|---|---|
94
+ | `catalog` | Nothing. Code is written from the use case catalogue, the three inventories, and C4 | **skipped** |
95
+ | `outline` | + a decision summary and the component list in the SDD, full flows for at most 3 use cases, local rules | 20 min |
96
+ | `guarded` | + **failure behaviour for every boundary**, inherited invariants quoted verbatim, integration documents | 20 min |
97
+ | `deep` | + robustness analysis, a contract per endpoint, data dictionary, flow diagrams, state machines | 30 min |
94
98
 
95
- Required frontmatter, checked by `V27` in `validate.py`:
99
+ Neither field is derived from the other, and that is the point: **a component MAY be thin on purpose and
100
+ reviewed the hardest.** A component at `catalog` skips the component gate entirely — which is what makes
101
+ a shallow default genuinely fast rather than nominally fast.
96
102
 
97
- ```yaml
98
- scope: project # exactly this
99
- purpose: "" # one line: what this rule protects
100
- overrides: null # optional: the kit file it narrows or contradicts
101
- decision: null # REQUIRED when `overrides:` is set — the DEC- that decided it
102
- ```
103
+ Depth is a preference and needs no defence. Accepting risk on something that touches money, personal
104
+ data, or an irreversible action is **not** free: it requires a recorded decision, and a validator checks
105
+ that the decision exists.
103
106
 
104
- A file there MAY narrow or add with none of the last two. To **contradict** a generic rule it MUST name
105
- it in `overrides:` and carry `decision:`; a method that can be contradicted without a decision stops
106
- being trustworthy in the next repo.
107
+ ---
107
108
 
108
- **Whole files, not marked blocks.** `AGENTS.md` uses a marked block because it is *one* file.
109
- `.constitution/` has fifty-odd, and blocks inside them would make `update` perform surgery in each —
110
- one broken marker and either the product's rule is erased or the generic rule freezes forever.
109
+ ## Five gates, fifteen skills
111
110
 
112
- The room's own `README.md` is authored in the package and `promote` never carries it home. Edit it if
113
- you like; the edit will not survive the next install elsewhere, so **your rules MUST be other files.**
111
+ | Gate | Decides | Skill |
112
+ |---|---|---|
113
+ | **G1 Problem** | What the problem is, whose it is, why it earns work | `wdi-problem` |
114
+ | **G2 Product** | What is built, and how it feels to use | `wdi-product` · optional `wdi-ux` |
115
+ | **G3 Blueprint** | The whole portrait, once per product | `wdi-blueprint` |
116
+ | **G4 Component** | How one component is built — **skipped at `catalog`** | `wdi-component` |
117
+ | **G5 Release** | Whether it is done and proven | `wdi-build` |
114
118
 
115
- ## Product name one room, filled at G1
119
+ Around them: `wdi-init` (scaffold, component birth, depth and risk settings, structure maps),
120
+ `wdi-decision`, `wdi-question`, `wdi-log`, `wdi-help`, `wdi-reconcile`, `wdi-review`, `wdi-report`, and
121
+ `wdi-systematic-debugging`.
116
122
 
117
- `.control/registry/index.yaml`:
123
+ **No BMad skill is invoked directly.** Each has a wrapper, and the wrapper is what checks position,
124
+ verifies the result, and records what happened.
118
125
 
119
- ```yaml
120
- product:
121
- name: "{product}" # set at G1; the brief title uses this value
122
- client: "" # empty when there is no client
123
- ```
126
+ ### Decisions, not ADRs
124
127
 
125
- The brief at `.what/_product-brief/brief.md` uses that name. `constitution.md` Article 1 cites
126
- the field. Neither document is a second source of the name.
128
+ A decision is a `DEC-`, and **recording one is not mandatory.** The test is one sentence: *if somebody
129
+ asks in three months why it is like this, is the answer readable from the code?* If yes, it MUST NOT be
130
+ recorded — a register nobody trusts is worse than no register. One case is mandatory: contradicting an
131
+ invariant on the spine.
127
132
 
128
- ## Language two settings, and nothing else is a choice
133
+ A `DEC-` freezes when it is applied. A change of mind produces a new one; it never edits the old.
129
134
 
130
- The TUI asks two questions; `install`/`update` write the answers to `.control/registry/index.yaml`:
135
+ ---
131
136
 
132
- ```yaml
133
- policy:
134
- doc_language: en # prose of working documents in .what/ .how/ .control/
135
- doc_filename_language: en # the slug part of a document filename
136
- ```
137
+ ## The mechanical half
137
138
 
138
- Non-interactive: `--doc-language <en|id> --doc-filename-language <en|id>`.
139
+ `validate.py` runs **V1–V27** over the registries and the corpus, and `inventory.py` derives the three
140
+ inventories from code and reports the difference against the plan without patching either side.
139
141
 
140
- **A setting that already exists is kept**, and the run says so. A language somebody already chose is
141
- not the installer's to change behind their back.
142
+ The validators exist because prose that nothing checks is prose that gets contradicted by the first
143
+ person in a hurry. Every one of them also states **the state in which it does not apply** — a rule that
144
+ demands a trace before the trace can exist is a rule that gets switched off, and a validator nobody
145
+ reads guards nothing.
142
146
 
143
- Always English, and a skill MUST NOT ask about them:
147
+ ---
144
148
 
145
- | | |
149
+ ## What is generic, and where your own rules live
150
+
151
+ `.constitution/` belongs to the method and is **overwritten** on every update. Four rooms are yours:
152
+
153
+ | Room | Yours because |
146
154
  |---|---|
147
- | Method terminology | `DEC` `SRS` `SDD` `UC` `FR` `AD`, the gate names, `mode` and `risk_accepted` values |
148
- | Document code prefixes | `UC-` `DEC-` `SRS-` only the slug after them follows the setting |
149
- | Machine-facing markers | `[NEEDS CONFIRMATION]` `[MISSING]` `[ASSUMED]` `[PARTIAL]`, `yes`/`no` |
150
- | Code identifiers, DB columns, config keys | `language-guide.md` owns this |
155
+ | `.control/registry/index.yaml` `product:` | The product and client name live in exactly one place |
156
+ | `constitution.md` Articles 1, 2, 5 | Scope, repo checklist, method ownership |
157
+ | `.constitution/codebase/*-guide.md` | Your stack and conventions, protected once `Accepted` |
158
+ | **`.constitution/project/`** | Any rule that binds **only this product** |
159
+ | `_bmad/custom/*.user.toml` | Your BMad overrides |
151
160
 
152
- **A corpus written before these settings existed is not migrated for them.** `validate.py` accepts both
153
- languages — `yes|ya`, and V23's keyword set is the union of both — so existing documents keep working
154
- and only new writing follows the setting.
161
+ `.control/` `.what/` `.how/` are never touched by an update at all they are your state, your promises,
162
+ and your design.
155
163
 
156
- ## Gitignore (optional)
164
+ The custom room takes whole files, not marked blocks inside generic ones: `AGENTS.md` can use a marked
165
+ block because it is *one* file, while `.constitution/` has fifty-odd, and blocks inside them would make
166
+ an update perform surgery in every file. A file there declares `scope: project` and a one-line
167
+ `purpose:`; to **contradict** a generic rule it must name that rule and carry the decision that allowed
168
+ it. **An empty room is a valid state** — filling it so that it gets used is the failure the rule prevents.
157
169
 
158
- This package does not require method files to be committed or ignored. Each product repo decides.
159
- BMad does not mandate it either. A repo that ignores the payload reinstalls with `update`.
170
+ ### Language
160
171
 
161
- Example, if you choose not to commit skills:
172
+ Two settings, both free text, both defaulting to English:
162
173
 
174
+ ```yaml
175
+ policy:
176
+ doc_language: "English" # prose of working documents
177
+ doc_filename_language: "English" # the slug part of a document filename
163
178
  ```
164
- .claude/skills/wdi-*/
165
- .agents/skills/wdi-*/
166
- ```
167
179
 
168
- Do not ignore all of `.constitution/` Articles 1, 2, 5 and extra product files belong to the
169
- product and MUST stay tracked.
180
+ Write whatever names the language`English`, `Bahasa Indonesia`, `id`. What reads the value is a model,
181
+ and a model does not need a lookup table.
182
+
183
+ Always English, and never asked: method terminology, document code prefixes (`UC-`, `DEC-`), machine
184
+ markers (`[NEEDS CONFIRMATION]`, `[MISSING]`), and code identifiers.
185
+
186
+ ---
187
+
188
+ ## What update does
170
189
 
171
- ## Carrying a method change into this package
190
+ | | |
191
+ |---|---|
192
+ | Overwrites | `.constitution/` guides, templates and scripts · the fifteen wrappers · `_bmad/custom/*.toml` · the marked block in `AGENTS.md` |
193
+ | Removes | Wrappers the method has retired — a `wdi-*` folder with a `SKILL.md` that is no longer one of the fifteen. Each removal is printed |
194
+ | Keeps | Everything in the table above, plus your initiative slug and your language choice. A setting somebody already chose is not the installer's to change behind their back |
195
+ | Never resurrects | A folder you retired. On update, absence is treated as a decision |
196
+
197
+ It prints the version it replaced, what it wrote, what it kept, and what to do next.
198
+
199
+ ---
172
200
 
173
- The published source is this repository. A product repo that still holds a newer working copy
174
- of the method MUST promote it here before the change is treated as published.
201
+ ## Carrying a change back into this package
175
202
 
176
- From a checkout of this repo:
203
+ The published source is this repository. A product repo that holds a newer working copy of the method
204
+ promotes it here before the change counts as published:
177
205
 
178
206
  ```bash
179
207
  npx wdi-method promote /path/to/the/product-repo
@@ -181,23 +209,14 @@ npm test
181
209
  git commit && git push
182
210
  ```
183
211
 
184
- `promote` copies the portable method, replaces product-named files with `kit-overlay/`, and
185
- scrubs initiative slugs to the placeholder `ISI-slug-inisiatif`.
212
+ `promote` copies the portable method, replaces product-named files with their generic versions, scrubs
213
+ initiative slugs, and **skips `.constitution/project/`** so a product's own rules can never be published.
186
214
 
187
- Do not run `update` against a repo you are about to promote from that would overwrite the
188
- newer working copy.
215
+ **Patch releases are routine; minor and major are the maintainer's call.** This package overwrites files
216
+ in repos that already hold months of work, and the version is the only signal a reader has for how
217
+ carefully to read the diff. [`CONTRIBUTING.md`](CONTRIBUTING.md) has the detail, and
218
+ [`AGENTS.md`](AGENTS.md) states it for agents working on the package.
189
219
 
190
- A product name, a client name, or a link to another private repository MUST NOT land in this
191
- tree. If the cleanliness test fails, fix the overlay or the source, never weaken the test.
192
-
193
- ## Commands
194
-
195
- | Command | Direction |
196
- |---|---|
197
- | `(no command)` | interactive TUI |
198
- | `install [dir]` | this package → product repo (first time) |
199
- | `update [dir]` | this package → product repo (again) |
200
- | `verify [dir]` | list missing method files |
201
- | `promote <dir>` | product repo → this package (maintainers) |
220
+ ---
202
221
 
203
- `install` and `update` share one copy path. `install` seeds empty `.control/` if it is missing.
222
+ MIT. Requires Node 20+ and [uv](https://docs.astral.sh/uv/) for the Python scripts.
package/bin/wdi-method.js CHANGED
@@ -10,9 +10,10 @@ import {
10
10
  } from "../lib/agents-block.mjs";
11
11
  import {
12
12
  identityIsPlaceholder,
13
+ humaniseFolderName,
13
14
  readLanguagePolicy,
14
15
  writeLanguagePolicy,
15
- DOC_LANGUAGES,
16
+ DEFAULT_DOC_LANGUAGE,
16
17
  readProductIdentity,
17
18
  writeProductIdentity,
18
19
  } from "../lib/identity.mjs";
@@ -59,6 +60,8 @@ const AGENT_LABELS = {
59
60
  };
60
61
 
61
62
  const BMAD_INSTALL = `npx bmad-method install`;
63
+ const REPO_URL = "https://github.com/wiradigitalid/wdi-method";
64
+ const HELP_SKILL = "wdi-help";
62
65
  const BMAD_REPO = "https://github.com/bmad-code-org/BMAD-METHOD";
63
66
  const WDI_REPO = "https://github.com/wiradigitalid/wdi-method";
64
67
 
@@ -93,8 +96,8 @@ function usage() {
93
96
  --agents a,b claude,cursor,codex,antigravity
94
97
  --product NAME written to index.yaml product.name
95
98
  --client NAME written to index.yaml product.client (optional)
96
- --doc-language <en|id> prose of working documents; default en
97
- --doc-filename-language <en|id> slug part of document filenames; default en
99
+ --doc-language <text> prose of working documents; free text, default English
100
+ --doc-filename-language <text> slug part of document filenames; free text, default English
98
101
  --skip-bmad-check
99
102
 
100
103
  BMad first, then this package. ${WDI_REPO}
@@ -145,10 +148,9 @@ function parseArgs(argv) {
145
148
  } else if (t === "--product") args.product = rest.shift();
146
149
  else if (t === "--client") args.client = rest.shift();
147
150
  else if (t === "--doc-language" || t === "--doc-filename-language") {
151
+ // Free text: "English", "Bahasa Indonesia", "id" — a model reads it, so no list to match.
148
152
  const raw = (rest.shift() || "").trim();
149
- if (!DOC_LANGUAGES.includes(raw)) {
150
- die(`${t} needs one of: ${DOC_LANGUAGES.join(", ")}`);
151
- }
153
+ if (!raw) die(`${t} needs a value, for example: English`);
152
154
  if (t === "--doc-language") args.docLanguage = raw;
153
155
  else args.docFilenameLanguage = raw;
154
156
  }
@@ -297,12 +299,12 @@ function skillDests(target, agents) {
297
299
 
298
300
  function bmadMissingMessage() {
299
301
  return [
300
- "BMad Method belum terpasang di repo ini. Pasang dulu, lalu jalankan installer ini lagi.",
302
+ "BMad Method is not installed in this repo. Install it first, then run this installer again.",
301
303
  "",
302
304
  ` ${BMAD_INSTALL}`,
303
305
  "",
304
- `Sumber: ${BMAD_REPO}`,
305
- "Di installer BMad, pilih agen yang sama (Claude Code, Cursor, …).",
306
+ `Source: ${BMAD_REPO}`,
307
+ "In the BMad installer, pick the same agents (Claude Code, Cursor, …).",
306
308
  ].join("\n");
307
309
  }
308
310
 
@@ -359,8 +361,8 @@ function syncSkills(target, agents) {
359
361
  n += copyTree(src, dest);
360
362
  }
361
363
  }
362
- pruneRetiredSkills(dests);
363
- return n;
364
+ const removed = pruneRetiredSkills(dests);
365
+ return { files: n, removed };
364
366
  }
365
367
 
366
368
  // A wrapper the method RETIRED is worse than a wrapper missing: the folder is still there, its
@@ -371,6 +373,7 @@ function syncSkills(target, agents) {
371
373
  // `wdi-` is the method's namespace, so a `wdi-*` folder carrying a SKILL.md and not in WDI_SKILLS is
372
374
  // ours and retired. Each removal is PRINTED: silent deletion in someone else's repo is not a fix.
373
375
  function pruneRetiredSkills(dests) {
376
+ let removed = 0;
374
377
  const keep = new Set(WDI_SKILLS);
375
378
  for (const root of dests) {
376
379
  if (!fs.existsSync(root)) continue;
@@ -383,8 +386,36 @@ function pruneRetiredSkills(dests) {
383
386
  }
384
387
  fs.rmSync(dir, { recursive: true, force: true });
385
388
  note(`removed retired skill ${entry.name}`);
389
+ removed += 1;
386
390
  }
387
391
  }
392
+ return removed;
393
+ }
394
+
395
+ // `promote` scrubs a product's initiative slug out of bmad-prd.toml before publishing, which is right.
396
+ // Writing the scrubbed PLACEHOLDER back into a product repo is not: the first real install replaced a
397
+ // live `run_folder_pattern = "toko-tanpa-akun"` with `ISI-slug-inisiatif`, and nothing said so. A value
398
+ // the product already chose is not the installer's to overwrite — same rule as the custom room and the
399
+ // language policy.
400
+ const PLACEHOLDER_SLUG = "ISI-slug-inisiatif";
401
+ const RUN_FOLDER_LINE = /^(\s*run_folder_pattern\s*=\s*)(".*?"|'.*?')/m;
402
+
403
+ // The slug appears MORE THAN ONCE — bmad-prd.toml carries it in `run_folder_pattern` and again inside a
404
+ // memlog path, and the file itself says the two lines MUST change together. The first version of this
405
+ // function restored only the first line and so produced exactly the inconsistency that file forbids.
406
+ // So: read the product's slug once, then put it back everywhere the placeholder appears.
407
+ function keepProductSlug(incoming, existing) {
408
+ const mineNow = existing.match(RUN_FOLDER_LINE);
409
+ if (!mineNow) return null;
410
+ const slug = mineNow[2].slice(1, -1);
411
+ if (!slug || slug === PLACEHOLDER_SLUG) return null;
412
+ if (!incoming.includes(PLACEHOLDER_SLUG)) return null;
413
+ // Only where the slug is a VALUE: the quoted setting, and the memlog path built from it. A bare
414
+ // mention inside a comment stays the placeholder — that sentence explains the pattern, and rewriting
415
+ // it would turn a generic explanation into a statement about one initiative.
416
+ return incoming
417
+ .replaceAll(`"${PLACEHOLDER_SLUG}"`, `"${slug}"`)
418
+ .replaceAll(`prd-${PLACEHOLDER_SLUG}`, `prd-${slug}`);
388
419
  }
389
420
 
390
421
  function syncTomls(target) {
@@ -392,12 +423,24 @@ function syncTomls(target) {
392
423
  const dest = path.join(target, "_bmad", "custom");
393
424
  fs.mkdirSync(dest, { recursive: true });
394
425
  let n = 0;
426
+ let slugsKept = 0;
395
427
  for (const file of walkFiles(src)) {
396
428
  if (!file.endsWith(".toml") || file.endsWith(".user.toml")) continue;
397
- copyFile(file, path.join(dest, path.basename(file)));
429
+ const to = path.join(dest, path.basename(file));
430
+ if (fs.existsSync(to)) {
431
+ const merged = keepProductSlug(fs.readFileSync(file, "utf8"), fs.readFileSync(to, "utf8"));
432
+ if (merged !== null) {
433
+ fs.writeFileSync(to, merged);
434
+ note(`kept run_folder_pattern in ${path.basename(file)}`);
435
+ slugsKept += 1;
436
+ n += 1;
437
+ continue;
438
+ }
439
+ }
440
+ copyFile(file, to);
398
441
  n += 1;
399
442
  }
400
- return n;
443
+ return { files: n, slugsKept };
401
444
  }
402
445
 
403
446
  function seedControlIfMissing(target) {
@@ -411,14 +454,27 @@ function seedControlIfMissing(target) {
411
454
  ok(`seeded empty .control/ (${n} files)`);
412
455
  }
413
456
 
414
- function seedEmptyLayers(target) {
415
- for (const rel of [".what", path.join(".how", "_platform"), ".work", path.join("_bmad-output", "prior-knowledge")]) {
457
+ // On a FIRST install these folders are the corpus taking shape. On an UPDATE their absence means
458
+ // somebody removed them on purpose `.work/` and `_bmad-output/prior-knowledge/` are exactly the two a
459
+ // product retires once its migration is done, and one repo retired them through an applied decision.
460
+ // Recreating them then is an installer overruling a decision it cannot read. Seed once, never resurrect.
461
+ function seedEmptyLayers(target, { first }) {
462
+ const always = [".what", path.join(".how", "_platform")];
463
+ const firstOnly = [".work", path.join("_bmad-output", "prior-knowledge")];
464
+ for (const rel of first ? [...always, ...firstOnly] : always) {
416
465
  const dest = path.join(target, rel);
417
466
  if (!fs.existsSync(dest)) {
418
467
  fs.mkdirSync(dest, { recursive: true });
419
468
  note(`created ${rel.replaceAll(path.sep, "/")}/`);
420
469
  }
421
470
  }
471
+ if (!first) {
472
+ for (const rel of firstOnly) {
473
+ if (!fs.existsSync(path.join(target, rel))) {
474
+ note(`left ${rel.replaceAll(path.sep, "/")}/ absent — a product retires it, not the installer`);
475
+ }
476
+ }
477
+ }
422
478
  }
423
479
 
424
480
  function writeStamp(target) {
@@ -450,19 +506,23 @@ function setProductIdentity(target, { name, client }) {
450
506
  // Bahasa dokumen milik PRODUK, jadi update MUST NOT menimpanya. Ia ditulis hanya ketika belum ada —
451
507
  // sama seperti kamar custom, dan dengan alasan yang sama: setelan yang pernah dipilih seseorang bukan
452
508
  // milik installer untuk diubah di belakangnya.
453
- function setLanguagePolicy(target, { docLanguage, docFilenameLanguage }) {
509
+ function setLanguagePolicy(target, { docLanguage, docFilenameLanguage, chosen }) {
454
510
  const file = path.join(target, ".control", "registry", "index.yaml");
455
511
  if (!fs.existsSync(file)) return;
456
512
  const text = fs.readFileSync(file, "utf8");
457
513
  const existing = readLanguagePolicy(text);
458
- if (existing.docLanguage && existing.docFilenameLanguage) {
514
+ // `chosen` berarti seseorang benar-benar menjawab — di TUI, atau lewat flag eksplisit. Maka
515
+ // jawabannya berlaku. Tanpa itu nilai yang masuk hanyalah default, dan default MUST NOT menimpa
516
+ // pilihan yang sudah pernah diambil seseorang.
517
+ if (!chosen && existing.docLanguage && existing.docFilenameLanguage) {
459
518
  note(`kept policy.doc_language = ${existing.docLanguage}, ` +
460
519
  `doc_filename_language = ${existing.docFilenameLanguage}`);
461
520
  return;
462
521
  }
463
522
  const next = writeLanguagePolicy(text, {
464
- docLanguage: docLanguage || existing.docLanguage || "en",
465
- docFilenameLanguage: docFilenameLanguage || existing.docFilenameLanguage || "en",
523
+ docLanguage: docLanguage || existing.docLanguage || DEFAULT_DOC_LANGUAGE,
524
+ docFilenameLanguage:
525
+ docFilenameLanguage || existing.docFilenameLanguage || DEFAULT_DOC_LANGUAGE,
466
526
  });
467
527
  fs.writeFileSync(file, next.endsWith("\n") ? next : `${next}\n`);
468
528
  const after = readLanguagePolicy(next);
@@ -470,6 +530,15 @@ function setLanguagePolicy(target, { docLanguage, docFilenameLanguage }) {
470
530
  `doc_filename_language = ${after.docFilenameLanguage}`);
471
531
  }
472
532
 
533
+ // Dibaca SEBELUM writeStamp menimpanya. Tanpa ini tidak ada transisi versi yang bisa dicetak, dan
534
+ // "updated" tanpa dari-ke tidak memberi tahu apa pun yang bisa dipakai.
535
+ function readStampVersion(target) {
536
+ const file = path.join(target, ".control", "wdi-method.yaml");
537
+ if (!fs.existsSync(file)) return "";
538
+ const m = fs.readFileSync(file, "utf8").match(/^wdi_method:\s*"?([^"\s]+)"?/m);
539
+ return m ? m[1] : "";
540
+ }
541
+
473
542
  function readIndexPolicy(target) {
474
543
  const file = path.join(target, ".control", "registry", "index.yaml");
475
544
  if (!fs.existsSync(file)) return { docLanguage: "", docFilenameLanguage: "" };
@@ -522,46 +591,94 @@ function upsertAgentFiles(target, agents, productName) {
522
591
  }
523
592
  }
524
593
 
594
+ // What a run MUST leave a reader able to answer: which version replaced which, what was written, what
595
+ // was KEPT, and what to do next. The third is the one usually missing, and it is the one that decides
596
+ // whether somebody trusts running this over a repo they have already put work into.
597
+ function summaryLine(label, value) {
598
+ console.log(` ${DIM}${label.padEnd(11)}${RESET}${value}`);
599
+ }
600
+
601
+ function printSummary(target, agents, { first, was, written, skipped, skills, tomls }) {
602
+ const now = PKG.version;
603
+ const version = first
604
+ ? `${now} — first install`
605
+ : was && was !== now
606
+ ? `${was} ${DIM}→${RESET} ${now}`
607
+ : `${now} ${DIM}(unchanged)${RESET}`;
608
+ const bmad = readBmadVersion(target);
609
+
610
+ const kept = [];
611
+ if (skipped) kept.push(`${skipped} constitution file${skipped === 1 ? "" : "s"}`);
612
+ if (tomls.slugsKept) kept.push(`${tomls.slugsKept} initiative slug${tomls.slugsKept === 1 ? "" : "s"}`);
613
+ // On a first install the language was just CHOSEN, not kept — saying "kept" there reads as if the
614
+ // installer had found something it decided to leave alone, which is the opposite of what happened.
615
+ const policy = readIndexPolicy(target);
616
+ if (policy.docLanguage && !first) kept.push(`language (${policy.docLanguage})`);
617
+ if (fs.existsSync(path.join(target, ".constitution", "project"))) kept.push(".constitution/project/");
618
+
619
+ console.log("");
620
+ console.log(`${DIM}────${RESET} WDI Method ${DIM}${"─".repeat(46)}${RESET}`);
621
+ summaryLine("version", version);
622
+ if (bmad) summaryLine("bmad", bmad);
623
+ summaryLine("target", target);
624
+ console.log("");
625
+ summaryLine("written", `${written} constitution · ${skills.files} skill files · ${tomls.files} bmad overrides`);
626
+ if (kept.length) summaryLine("kept", kept.join(" · "));
627
+ if (skills.removed) {
628
+ summaryLine("removed", `${skills.removed} retired wrapper${skills.removed === 1 ? "" : "s"}`);
629
+ }
630
+ if (first && policy.docLanguage) {
631
+ summaryLine("language", `${policy.docLanguage} · filenames ${policy.docFilenameLanguage}`);
632
+ }
633
+ summaryLine("agents", agents.join(", ") || "none");
634
+ console.log("");
635
+ summaryLine("next", `invoke the ${HELP_SKILL} skill and ask what to do`);
636
+ summaryLine("", REPO_URL);
637
+ console.log(`${DIM}${"─".repeat(62)}${RESET}`);
638
+ }
639
+
525
640
  function printNextSteps({ first, productSet }) {
526
641
  console.log("");
527
- console.log(first ? "Sesudah install:" : "Sesudah update:");
642
+ console.log(first ? "After install:" : "After update:");
528
643
  if (first) {
529
644
  if (!productSet) {
530
- console.log(" 1. Isi product.name (dan product.client bila ada) di .control/registry/index.yaml.");
645
+ console.log(" 1. Fill product.name (and product.client if there is one) in .control/registry/index.yaml.");
531
646
  } else {
532
- console.log(" 1. product.name sudah diisi. G1 nanti mengonfirmasinya di brief.");
647
+ console.log(" 1. product.name is set. G1 confirms it in the brief.");
533
648
  }
534
- console.log(" 2. Tulis ulang .constitution/constitution.md Pasal 2 dan 5 untuk produk ini.");
535
- console.log(" Pasal 1 mengutip index.yaml — jangan jadi sumber nama kedua.");
536
- console.log(" 3. Tulis ## Code di AGENTS.md (akar aplikasi). Blok BEGIN:wdi-method jangan diedit.");
537
- console.log(" 4. Jalankan skill wdi-init intent setup.");
538
- console.log(" 5. Pilah dokumen lama. Jangan dipindah di langkah ini.");
649
+ console.log(" 2. Rewrite .constitution/constitution.md Articles 2 and 5 for this product.");
650
+ console.log(" Article 1 cites index.yaml — do not become a second source for the name.");
651
+ console.log(" 3. Write ## Code in AGENTS.md (where the app lives). Leave the BEGIN:wdi-method block alone.");
652
+ console.log(" 4. Run the wdi-init skill, intent setup.");
653
+ console.log(" 5. Sort the documents you already have. Do not move any of them in this step.");
539
654
  console.log("");
540
- console.log("Update berikutnya:");
541
- console.log(" npx github:wiradigitalid/wdi-method");
542
- console.log(" (TUI akan menawarkan update) atau: npx github:wiradigitalid/wdi-method update --yes");
655
+ console.log("Next update:");
656
+ console.log(" npx wdi-method");
657
+ console.log(" (the TUI offers the update) or: npx wdi-method update --yes");
543
658
  } else {
544
- console.log(" 1. Blok <!-- BEGIN:wdi-method --> di AGENTS.md sudah diganti. Cek diff-nya.");
545
- console.log(" 2. Pasal 125 constitution.md, ## Code, dan *.user.toml tidak ditimpa.");
546
- console.log(" 3. Kalau ada skill BMad baru, pasang dulu lewat installer BMad, lalu update ini lagi.");
659
+ console.log(" 1. The <!-- BEGIN:wdi-method --> block in AGENTS.md was replaced. Read the diff.");
660
+ console.log(" 2. constitution.md Articles 1-2-5, ## Code, and *.user.toml were not overwritten.");
661
+ console.log(" 3. If BMad has new skills, install those first, then run this update again.");
547
662
  }
548
663
  }
549
664
 
550
- function apply(target, agents, { first, product, client, docLanguage, docFilenameLanguage }) {
665
+ function apply(target, agents,
666
+ { first, product, client, docLanguage, docFilenameLanguage, languageChosen }) {
551
667
  requireKit();
668
+ const was = readStampVersion(target);
552
669
  const { written, skipped } = syncConstitution(target);
553
670
  note(`constitution wrote ${written}, kept ${skipped}`);
554
- const nSkills = syncSkills(target, agents);
555
- note(`skills ${nSkills} files`);
556
- const nToml = syncTomls(target);
557
- note(`bmad custom ${nToml} toml → _bmad/custom/`);
671
+ const skills = syncSkills(target, agents);
672
+ note(`skills ${skills.files} files`);
673
+ const tomls = syncTomls(target);
674
+ note(`bmad custom ${tomls.files} toml → _bmad/custom/`);
558
675
  if (first) seedControlIfMissing(target);
559
- seedEmptyLayers(target);
676
+ seedEmptyLayers(target, { first });
560
677
  setProductIdentity(target, { name: product, client });
561
- setLanguagePolicy(target, { docLanguage, docFilenameLanguage });
678
+ setLanguagePolicy(target, { docLanguage, docFilenameLanguage, chosen: languageChosen });
562
679
  upsertAgentFiles(target, agents, product);
563
680
  writeStamp(target);
564
- ok(`${first ? "installed" : "updated"} into ${target}`);
681
+ printSummary(target, agents, { first, was, written, skipped, skills, tomls });
565
682
  printNextSteps({
566
683
  first,
567
684
  productSet: Boolean(product) && !identityIsPlaceholder(product),
@@ -695,7 +812,7 @@ function promote(live) {
695
812
 
696
813
  function cancelIf(value) {
697
814
  if (p.isCancel(value)) {
698
- p.cancel("Dibatalkan.");
815
+ p.cancel("Cancelled.");
699
816
  process.exit(0);
700
817
  }
701
818
  return value;
@@ -706,7 +823,7 @@ async function runWizard(pre) {
706
823
 
707
824
  const dirValue = cancelIf(
708
825
  await p.text({
709
- message: "Repo tujuan (folder produk)",
826
+ message: "Target repo (the product folder)",
710
827
  placeholder: process.cwd(),
711
828
  defaultValue: pre.dir || process.cwd(),
712
829
  }),
@@ -715,10 +832,10 @@ async function runWizard(pre) {
715
832
 
716
833
  if (!fs.existsSync(target)) {
717
834
  const create = cancelIf(
718
- await p.confirm({ message: `${target} belum ada. Buat folder?`, initialValue: true }),
835
+ await p.confirm({ message: `${target} does not exist. Create it?`, initialValue: true }),
719
836
  );
720
837
  if (!create) {
721
- p.cancel("Tidak ada folder tujuan.");
838
+ p.cancel("No target folder.");
722
839
  process.exit(1);
723
840
  }
724
841
  fs.mkdirSync(target, { recursive: true });
@@ -738,8 +855,8 @@ async function runWizard(pre) {
738
855
  p.note(facts, "Deteksi");
739
856
 
740
857
  if (!hasBmad && !pre.skipBmad) {
741
- p.note(bmadMissingMessage(), "BMad dulu");
742
- p.outro("Pasang BMad, lalu jalankan lagi: npx github:wiradigitalid/wdi-method");
858
+ p.note(bmadMissingMessage(), "BMad first");
859
+ p.outro("Install BMad, then run this again: npx wdi-method");
743
860
  process.exit(1);
744
861
  }
745
862
 
@@ -747,70 +864,74 @@ async function runWizard(pre) {
747
864
  if (hasWdi) {
748
865
  const update = cancelIf(
749
866
  await p.confirm({
750
- message: "WDI Method sudah terpasang. Update sekarang?",
867
+ message: "WDI Method is already installed. Update it now?",
751
868
  initialValue: true,
752
869
  }),
753
870
  );
754
871
  first = !update;
755
872
  if (first) {
756
- p.cancel("Tidak jadi meng-update.");
873
+ p.cancel("Update declined.");
757
874
  process.exit(0);
758
875
  }
759
876
  } else {
760
877
  const go = cancelIf(
761
878
  await p.confirm({
762
- message: `Pasang WDI Method ke ${target}?`,
879
+ message: `Install WDI Method into ${target}?`,
763
880
  initialValue: true,
764
881
  }),
765
882
  );
766
883
  if (!go) {
767
- p.cancel("Tidak jadi memasang.");
884
+ p.cancel("Install declined.");
768
885
  process.exit(0);
769
886
  }
770
887
  }
771
888
 
889
+ // Every field arrives with an answer already in it, and Enter accepts it. On an update that answer is
890
+ // what the repo already says; on a first install it is the folder name made readable. Nothing here is
891
+ // validated as required: a prompt that refuses an empty submission when it already holds a sensible
892
+ // default is asking the owner to retype something the installer knows.
772
893
  const existing = readIndexIdentity(target);
894
+ const suggestedName = identityIsPlaceholder(existing.name)
895
+ ? humaniseFolderName(path.basename(target))
896
+ : existing.name;
773
897
  const product = cancelIf(
774
898
  await p.text({
775
- message: "Nama produk (satu kamar: index.yaml product.name)",
776
- placeholder: existing.name && !identityIsPlaceholder(existing.name) ? existing.name : "contoh: Worship Presenter Web",
777
- defaultValue: existing.name && !identityIsPlaceholder(existing.name) ? existing.name : "",
778
- validate: (v) => (v && v.trim() && v.trim() !== "{product}" ? undefined : "Wajib. Ini diisi di G1 dan dipakai judul brief."),
899
+ message: "Product name (one room: index.yaml product.name)",
900
+ placeholder: suggestedName,
901
+ defaultValue: suggestedName,
779
902
  }),
780
- );
903
+ ).trim() || suggestedName;
781
904
  const client = cancelIf(
782
905
  await p.text({
783
- message: "Nama klien (kosongkan kalau tidak ada)",
784
- placeholder: existing.client || "(opsional)",
906
+ message: "Client name (Enter to leave it as it is)",
907
+ placeholder: existing.client || "(none)",
785
908
  defaultValue: existing.client || "",
786
909
  }),
787
- );
910
+ ).trim();
788
911
 
789
912
  // Dua pertanyaan, dan hanya dua. Istilah metodologi, kode di depan nama berkas, penanda
790
913
  // machine-facing, dan identifier kode selalu English — MUST NOT ditanyakan.
791
914
  const policy = readIndexPolicy(target);
915
+ // Teks bebas, bukan daftar. Tulis apa saja yang dimengerti sebuah model — "English",
916
+ // "Bahasa Indonesia", "id". Yang ditolak hanya kosong.
792
917
  const askLanguage = async (message, current) =>
793
- cancelIf(
794
- await p.select({
918
+ (cancelIf(
919
+ await p.text({
795
920
  message,
796
- options: [
797
- { value: "en", label: "English" },
798
- { value: "id", label: "Bahasa Indonesia" },
799
- ],
800
- initialValue: current || "en",
921
+ placeholder: current || DEFAULT_DOC_LANGUAGE,
922
+ defaultValue: current || DEFAULT_DOC_LANGUAGE,
801
923
  }),
802
- );
803
- const docLanguage = policy.docLanguage
804
- ? (note(`bahasa dokumen sudah disetel: ${policy.docLanguage}`), policy.docLanguage)
805
- : await askLanguage("Bahasa isi dokumen kerja (.what/ .how/ .control/)", pre.docLanguage);
806
- const docFilenameLanguage = policy.docFilenameLanguage
807
- ? policy.docFilenameLanguage
808
- : await askLanguage("Bahasa slug nama berkas dokumen — kode `UC-` `DEC-` tetap English",
809
- pre.docFilenameLanguage || docLanguage);
924
+ ) || DEFAULT_DOC_LANGUAGE).trim();
925
+ const docLanguage = await askLanguage(
926
+ "Language of working-document prose (.what/ .how/ .control/) — free text",
927
+ policy.docLanguage || pre.docLanguage);
928
+ const docFilenameLanguage = await askLanguage(
929
+ "Language of document filename slugs — the `UC-` `DEC-` codes stay English",
930
+ policy.docFilenameLanguage || pre.docFilenameLanguage || docLanguage);
810
931
 
811
932
  const selected = cancelIf(
812
933
  await p.multiselect({
813
- message: "Agen mana yang kebagian skill? (spasi untuk pilih)",
934
+ message: "Which agents get the skills? (space to select)",
814
935
  options: ALL_AGENTS.map((id) => ({ value: id, label: AGENT_LABELS[id] })),
815
936
  initialValues: pre.agents || detectAgents(target),
816
937
  required: true,
@@ -819,23 +940,23 @@ async function runWizard(pre) {
819
940
 
820
941
  p.note(
821
942
  [
822
- "Nama folder korpus tetapbukan opsi instal:",
943
+ "The corpus folder names are fixed they are not an install option:",
823
944
  " .constitution .control .what .how .work _bmad-output",
824
945
  "",
825
- "Yang ditulis untuk agen yang dipilih:",
946
+ "What gets written for the agents you picked:",
826
947
  selected.includes("claude") ? " .claude/skills/wdi-* CLAUDE.md" : "",
827
948
  selected.includes("cursor") ? " .agents/skills/wdi-* .cursorrules" : "",
828
949
  selected.includes("codex") || selected.includes("cursor") || selected.includes("antigravity")
829
- ? " AGENTS.md (blok BEGIN:wdi-method)"
950
+ ? " AGENTS.md (the BEGIN:wdi-method block)"
830
951
  : "",
831
952
  selected.includes("antigravity") ? " .agents/AGENTS.md" : "",
832
953
  ]
833
954
  .filter(Boolean)
834
955
  .join("\n"),
835
- "Tujuan tulis",
956
+ "Write targets",
836
957
  );
837
958
 
838
- const okGo = cancelIf(await p.confirm({ message: first ? "Jalankan install?" : "Jalankan update?", initialValue: true }));
959
+ const okGo = cancelIf(await p.confirm({ message: first ? "Run the install?" : "Run the update?", initialValue: true }));
839
960
  if (!okGo) {
840
961
  p.cancel("Dibatalkan.");
841
962
  process.exit(0);
@@ -846,12 +967,13 @@ async function runWizard(pre) {
846
967
  apply(target, selected, {
847
968
  docLanguage,
848
969
  docFilenameLanguage,
970
+ languageChosen: true,
849
971
  first,
850
972
  product: String(product).trim(),
851
973
  client: String(client).trim(),
852
974
  });
853
975
  spinner.stop(first ? "Terpasang" : "Ter-update");
854
- p.outro(first ? "Siap. Lanjut ke langkah sesudah install di atas." : "Siap. Cek diff blok metode di AGENTS.md.");
976
+ p.outro(first ? "Done. Take the after-install steps above." : "Done. Read the method-block diff in AGENTS.md.");
855
977
  }
856
978
 
857
979
  function runNonInteractive(args) {
@@ -874,6 +996,7 @@ function runNonInteractive(args) {
874
996
  client,
875
997
  docLanguage: args.docLanguage,
876
998
  docFilenameLanguage: args.docFilenameLanguage,
999
+ languageChosen: Boolean(args.docLanguage || args.docFilenameLanguage),
877
1000
  });
878
1001
  }
879
1002
 
@@ -40,6 +40,11 @@ defaulting to English:
40
40
  | `doc_language` | The **prose** of working documents in `.what/` · `.how/` · `.control/` |
41
41
  | `doc_filename_language` | The **slug** part of a document filename |
42
42
 
43
+ **Both are free text, not a list of codes.** `English`, `Bahasa Indonesia`, `id`, `Indonesia` — write
44
+ whatever names the language, because what reads the value is a **model**, and a model does not need a
45
+ lookup table. Fencing it into two codes would only make the owner translate their intent into the
46
+ installer's vocabulary first, and nothing is bought with that. The one value refused is an empty one.
47
+
43
48
  Nothing else about language is a setting, and a skill MUST NOT ask:
44
49
 
45
50
  - **Method terminology** — `DEC` `SRS` `SDD` `UC` `FR` `AD`, the gate names, the values of `mode` and
@@ -15,15 +15,15 @@ BMad first, then WDI Method. In the product repo:
15
15
 
16
16
  ```bash
17
17
  npx bmad-method install
18
- npx github:wiradigitalid/wdi-method
18
+ npx wdi-method
19
19
  ```
20
20
 
21
21
  No subcommand opens the installer TUI. It detects an existing install and offers
22
22
  **update**. Non-interactive:
23
23
 
24
24
  ```bash
25
- npx github:wiradigitalid/wdi-method install --yes
26
- npx github:wiradigitalid/wdi-method update --yes
25
+ npx wdi-method install --yes
26
+ npx wdi-method update --yes
27
27
  ```
28
28
 
29
29
  BMad: https://github.com/bmad-code-org/BMAD-METHOD
@@ -37,19 +37,34 @@ the task matches.
37
37
 
38
38
  ## Language
39
39
 
40
- Prose in this repo is Bahasa Indonesia; a technical term the industry writes in English MUST be left
41
- in English an Indonesian equivalent MUST NOT be invented. Which language a **name** is written in —
42
- code identifiers, files, database columns — is governed by `.constitution/language-guide.md`.
40
+ **Two settings decide this, and they live in `.control/registry/index.yaml` under `policy:`.** Both are
41
+ free text and both default to English:
43
42
 
44
- The agent-instruction files are the exception the Rule-Writing Standard already claims: `AGENTS.md`,
45
- `CLAUDE.md`, and everything under `.constitution/`. Nothing else in this repo is one — `.control/`,
46
- `.what/`, and `.how/` are product content and MUST stay Bahasa Indonesia, and translating them MUST
47
- NOT be proposed as tidying. A literal value written into an Indonesian document a marker such as
48
- `[TIDAK ADA]`, an answer option such as `ya / tidak / ubah` — stays Indonesian wherever it appears,
49
- including inside an English guide.
43
+ | Setting | Governs |
44
+ |---|---|
45
+ | `doc_language` | The prose of working documents in `.what/` · `.how/` · `.control/` |
46
+ | `doc_filename_language` | The slug part of a document filename |
47
+
48
+ Read those two before writing a document. A technical term the industry writes in English MUST be left in
49
+ English whatever the setting says — an equivalent MUST NOT be invented for it.
50
+
51
+ **These files are always English, whatever the settings say:** `AGENTS.md`, `CLAUDE.md`, and everything
52
+ under `.constitution/`. They are agent instructions, and they travel to every repo through the
53
+ `wdi-method` package. The one exception is `.constitution/project/`, which is this product's own room.
54
+
55
+ **Always English and never a setting**, because a script matches them:
56
+
57
+ - method terminology — `DEC` `SRS` `SDD` `UC` `FR` `AD`, the gate names, the values of `mode` and
58
+ `risk_accepted`
59
+ - document code prefixes — `UC-` `DEC-` `SRS-`; only the slug after them follows `doc_filename_language`
60
+ - markers — `[NEEDS CONFIRMATION]` `[MISSING]` `[ASSUMED]` `[PARTIAL]`, and `yes`/`no` in a `critical`
61
+ column
62
+ - registry values — `mode: catalog`, `status: applied`, `risk_accepted: low`. Used as written in prose
63
+ too: one thing, one name
64
+ - code identifiers, database columns, config keys — `language-guide.md` owns these
50
65
 
51
- A **registry value** is a machine-facing key and stays English: `mode: catalog`, `status: applied`,
52
- `risk_accepted: low`. Those names are used as written in prose too one thing, one name.
66
+ **A corpus written before these settings existed MUST NOT be migrated for them.** The readers accept more
67
+ than one language, so existing documents keep working and only new writing follows the setting.
53
68
 
54
69
  ## The thing in your hand → its folder
55
70
 
@@ -58,6 +73,7 @@ Read this instead of reasoning about what `.what/` and `.how/` mean.
58
73
  | The thing in your hand | Its folder |
59
74
  |---|---|
60
75
  | A rule, a guide, a template — how we work | `.constitution/` |
76
+ | A rule that binds **only this product** | `.constitution/project/` — `update` never writes over it, `promote` never publishes it |
61
77
  | The explanation of a rule, never a rule itself | `.constitution/method/` |
62
78
  | A decision, an open question, a registry, a structure map, minutes | `.control/` |
63
79
  | The brief, a PRD, a use case, a business rule — what is promised | `.what/` |
package/lib/identity.mjs CHANGED
@@ -1,65 +1,117 @@
1
- const PRODUCT_BLOCK = /(?:^|\n)product:\n(?: .*\n)*/;
2
-
3
- export function yamlQuote(value) {
4
- return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
5
- }
6
-
7
- export function readProductIdentity(text) {
8
- const name = text.match(/product:\n(?: .*\n)*? name:\s*"([^"]*)"/);
9
- const client = text.match(/product:\n(?: .*\n)*? client:\s*"([^"]*)"/);
10
- return {
11
- name: name ? name[1] : "",
12
- client: client ? client[1] : "",
13
- };
14
- }
15
-
16
- export function writeProductIdentity(text, { name, client }) {
17
- const block =
18
- `product:\n name: ${yamlQuote(name)}\n client: ${yamlQuote(client ?? "")}\n`;
19
- if (/^product:/m.test(text)) {
20
- return text.replace(PRODUCT_BLOCK, (m) => (m.startsWith("\n") ? `\n${block}` : block));
21
- }
22
- return `${block}\n${text.replace(/^/, "")}`;
23
- }
24
-
25
- export function identityIsPlaceholder(name) {
26
- return !name || name === "{product}";
27
- }
28
-
29
- // ---------------------------------------------------------------------- language policy
30
- //
31
- // Two settings, and only two. Everything else about language is NOT a choice: method terminology,
32
- // document code prefixes, machine-facing markers, and code identifiers are always English —
33
- // `language-guide.md` owns that, and a caller MUST NOT ask about them.
34
- //
35
- // doc_language the PROSE of working documents in .what/ .how/ .control/
36
- // doc_filename_language the SLUG part of a document filename
37
- //
38
- // Both default to `en`. A corpus written before these existed is NOT migrated for them: the readers
39
- // in validate.py accept both languages, so an existing document keeps working and only new writing
40
- // follows the setting.
41
- const POLICY_BLOCK = /(?:^|\n)policy:\n(?: .*\n)*/;
42
- export const DOC_LANGUAGES = ["en", "id"];
43
-
44
- export function readLanguagePolicy(text) {
45
- const doc = text.match(/policy:\n(?: .*\n)*? doc_language:\s*([A-Za-z-]+)/);
46
- const file = text.match(/policy:\n(?: .*\n)*? doc_filename_language:\s*([A-Za-z-]+)/);
47
- return {
48
- docLanguage: doc ? doc[1] : "",
49
- docFilenameLanguage: file ? file[1] : "",
50
- };
51
- }
52
-
53
- export function writeLanguagePolicy(text, { docLanguage, docFilenameLanguage }) {
54
- const block =
55
- `policy:\n doc_language: ${docLanguage}\n doc_filename_language: ${docFilenameLanguage}\n`;
56
- if (/^policy:/m.test(text)) {
57
- return text.replace(POLICY_BLOCK, (m) => (m.startsWith("\n") ? `\n${block}` : block));
58
- }
59
- // Sits right after `product:` when that block exists the two answer the same kind of question,
60
- // so a reader who finds one finds the other.
61
- if (/^product:/m.test(text)) {
62
- return text.replace(/^product:\n(?: .*\n)*/m, (m) => `${m}\n${block}`);
63
- }
64
- return `${block}\n${text.replace(/^/, "")}`;
65
- }
1
+ // index.yaml editing. Every regex here is anchored on `\n`, and a Windows checkout hands us `\r\n` —
2
+ // so every function normalises before matching and restores the file's own ending before writing.
3
+ //
4
+ // Skipping that is not a cosmetic bug. It silently returns "" for a name that IS set, which makes
5
+ // `identityIsPlaceholder` true, which makes `setProductIdentity` return early: the installer reports
6
+ // nothing and writes nothing, on every CRLF repo. Found 2026-08-18 on the first real install into a
7
+ // Windows product repo, where `policy.doc_language` printed empty while the file plainly said `id`.
8
+
9
+ const PRODUCT_BLOCK = /(?:^|\n)product:\n(?: .*\n)*/;
10
+ const POLICY_BLOCK = /(?:^|\n)policy:\n(?: .*\n)*/;
11
+
12
+ // FREE TEXT, not an enum. The consumer is a model, and a model does not need a list: "English",
13
+ // "Bahasa Indonesia", "id", "Indonesia" all read the same to it. Fencing this into two values would
14
+ // force the owner to translate their intent into the installer's vocabulary first, and nothing is
15
+ // bought with that.
16
+ export const DEFAULT_DOC_LANGUAGE = "English";
17
+
18
+ /** LF view of the text, plus how to put the original endings back. */
19
+ function lf(text) {
20
+ const crlf = text.includes("\r\n");
21
+ return { body: crlf ? text.replaceAll("\r\n", "\n") : text, crlf };
22
+ }
23
+
24
+ function restore(body, crlf) {
25
+ return crlf ? body.replaceAll("\n", "\r\n") : body;
26
+ }
27
+
28
+ export function yamlQuote(value) {
29
+ return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
30
+ }
31
+
32
+ export function readProductIdentity(text) {
33
+ const { body } = lf(text);
34
+ const name = body.match(/product:\n(?: .*\n)*? name:\s*"([^"]*)"/);
35
+ const client = body.match(/product:\n(?: .*\n)*? client:\s*"([^"]*)"/);
36
+ return {
37
+ name: name ? name[1] : "",
38
+ client: client ? client[1] : "",
39
+ };
40
+ }
41
+
42
+ export function writeProductIdentity(text, { name, client }) {
43
+ const { body, crlf } = lf(text);
44
+ const block =
45
+ `product:\n name: ${yamlQuote(name)}\n client: ${yamlQuote(client ?? "")}\n`;
46
+ if (/^product:/m.test(body)) {
47
+ return restore(
48
+ body.replace(PRODUCT_BLOCK, (m) => (m.startsWith("\n") ? `\n${block}` : block)), crlf);
49
+ }
50
+ return restore(`${block}\n${body.replace(/^/, "")}`, crlf);
51
+ }
52
+
53
+ export function identityIsPlaceholder(name) {
54
+ return !name || name === "{product}";
55
+ }
56
+
57
+ // ---------------------------------------------------------------------- language policy
58
+ //
59
+ // Two settings, and only two. Everything else about language is NOT a choice: method terminology,
60
+ // document code prefixes, machine-facing markers, and code identifiers are always English —
61
+ // `language-guide.md` owns that, and a caller MUST NOT ask about them.
62
+ //
63
+ // doc_language the PROSE of working documents in .what/ .how/ .control/
64
+ // doc_filename_language the SLUG part of a document filename
65
+ //
66
+ // A corpus written before these existed is NOT migrated for them: the readers in validate.py accept
67
+ // more than one language, so an existing document keeps working and only new writing follows the value.
68
+
69
+ function readPolicyValue(body, key) {
70
+ const quoted = body.match(new RegExp(`policy:\\n(?: .*\\n)*? ${key}:[ \\t]*"([^"]*)"`));
71
+ if (quoted) return quoted[1].trim();
72
+ // Bare scalars are accepted because early repos wrote `doc_language: id` unquoted. Stops at a
73
+ // comment so a trailing `# note` never becomes part of the value.
74
+ const bare = body.match(new RegExp(`policy:\\n(?: .*\\n)*? ${key}:[ \\t]*([^\\n#]*)`));
75
+ return bare ? bare[1].trim() : "";
76
+ }
77
+
78
+ export function readLanguagePolicy(text) {
79
+ const { body } = lf(text);
80
+ return {
81
+ docLanguage: readPolicyValue(body, "doc_language"),
82
+ docFilenameLanguage: readPolicyValue(body, "doc_filename_language"),
83
+ };
84
+ }
85
+
86
+ export function writeLanguagePolicy(text, { docLanguage, docFilenameLanguage }) {
87
+ const { body, crlf } = lf(text);
88
+ // Always quoted: free text MAY carry a space or a colon, and a bare YAML scalar breaks on both.
89
+ const block =
90
+ `policy:\n doc_language: ${yamlQuote(docLanguage)}\n` +
91
+ ` doc_filename_language: ${yamlQuote(docFilenameLanguage)}\n`;
92
+ if (/^policy:/m.test(body)) {
93
+ return restore(
94
+ body.replace(POLICY_BLOCK, (m) => (m.startsWith("\n") ? `\n${block}` : block)), crlf);
95
+ }
96
+ // Sits right after `product:` when that block exists — the two answer the same kind of question,
97
+ // so a reader who finds one finds the other.
98
+ if (/^product:/m.test(body)) {
99
+ return restore(body.replace(/^product:\n(?: .*\n)*/m, (m) => `${m}\n${block}`), crlf);
100
+ }
101
+ return restore(`${block}\n${body.replace(/^/, "")}`, crlf);
102
+ }
103
+
104
+ // A first install has nowhere to read a product name from, so the folder is the best guess available —
105
+ // and a guess the owner can accept with Enter beats a field they must type. `worship-presenter-web`
106
+ // becomes `Worship Presenter Web`; camelCase splits too. It is a SUGGESTION: G1 confirms the real name.
107
+ export function humaniseFolderName(name) {
108
+ return String(name || "")
109
+ .replace(/[._-]+/g, " ")
110
+ .replace(/([a-z\d])([A-Z])/g, "$1 $2")
111
+ .replace(/\s+/g, " ")
112
+ .trim()
113
+ .split(" ")
114
+ .filter(Boolean)
115
+ .map((w) => (w === w.toUpperCase() && w.length <= 4 ? w : w[0].toUpperCase() + w.slice(1)))
116
+ .join(" ");
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdi-method",
3
- "version": "0.3.0",
3
+ "version": "0.4.3",
4
4
  "description": "WDI Method — software delivery method that wraps BMad",
5
5
  "type": "module",
6
6
  "bin": {