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 +149 -130
- package/bin/wdi-method.js +204 -81
- package/kit/.constitution/language-guide.md +5 -0
- package/kit-overlay/AGENTS.md +30 -14
- package/lib/identity.mjs +117 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,179 +1,207 @@
|
|
|
1
1
|
# WDI Method
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
21
|
+
npx wdi-method
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
41
|
+
Non-interactive, for CI:
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
|
-
npx
|
|
44
|
+
npx wdi-method install --yes --agents claude,codex --product "Your Product" \
|
|
45
|
+
--doc-language "Bahasa Indonesia"
|
|
37
46
|
```
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
It is a trace, not a lockfile.
|
|
51
|
+
---
|
|
50
52
|
|
|
51
|
-
##
|
|
53
|
+
## The gap this fills
|
|
52
54
|
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
is
|
|
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
|
-
|
|
69
|
+
WDI Method's whole contribution is that place, plus the discipline that keeps it honest:
|
|
66
70
|
|
|
67
|
-
|
|
|
71
|
+
| | |
|
|
68
72
|
|---|---|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| |
|
|
73
|
-
| |
|
|
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
|
-
|
|
80
|
+
---
|
|
76
81
|
|
|
77
|
-
##
|
|
82
|
+
## Two knobs, never merged
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
133
|
+
A `DEC-` freezes when it is applied. A change of mind produces a new one; it never edits the old.
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
---
|
|
131
136
|
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
148
|
-
|
|
|
149
|
-
|
|
|
150
|
-
|
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
BMad does not mandate it either. A repo that ignores the payload reinstalls with `update`.
|
|
170
|
+
### Language
|
|
160
171
|
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
97
|
-
--doc-filename-language <
|
|
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 (!
|
|
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
|
|
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
|
-
`
|
|
305
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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 ||
|
|
465
|
-
docFilenameLanguage:
|
|
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 ? "
|
|
642
|
+
console.log(first ? "After install:" : "After update:");
|
|
528
643
|
if (first) {
|
|
529
644
|
if (!productSet) {
|
|
530
|
-
console.log(" 1.
|
|
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
|
|
647
|
+
console.log(" 1. product.name is set. G1 confirms it in the brief.");
|
|
533
648
|
}
|
|
534
|
-
console.log(" 2.
|
|
535
|
-
console.log("
|
|
536
|
-
console.log(" 3.
|
|
537
|
-
console.log(" 4.
|
|
538
|
-
console.log(" 5.
|
|
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("
|
|
541
|
-
console.log(" npx
|
|
542
|
-
console.log(" (TUI
|
|
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.
|
|
545
|
-
console.log(" 2.
|
|
546
|
-
console.log(" 3.
|
|
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,
|
|
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
|
|
555
|
-
note(`skills ${
|
|
556
|
-
const
|
|
557
|
-
note(`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
|
-
|
|
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("
|
|
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: "
|
|
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}
|
|
835
|
+
await p.confirm({ message: `${target} does not exist. Create it?`, initialValue: true }),
|
|
719
836
|
);
|
|
720
837
|
if (!create) {
|
|
721
|
-
p.cancel("
|
|
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
|
|
742
|
-
p.outro("
|
|
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
|
|
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("
|
|
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: `
|
|
879
|
+
message: `Install WDI Method into ${target}?`,
|
|
763
880
|
initialValue: true,
|
|
764
881
|
}),
|
|
765
882
|
);
|
|
766
883
|
if (!go) {
|
|
767
|
-
p.cancel("
|
|
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: "
|
|
776
|
-
placeholder:
|
|
777
|
-
defaultValue:
|
|
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: "
|
|
784
|
-
placeholder: existing.client || "(
|
|
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.
|
|
918
|
+
(cancelIf(
|
|
919
|
+
await p.text({
|
|
795
920
|
message,
|
|
796
|
-
|
|
797
|
-
|
|
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 =
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const docFilenameLanguage =
|
|
807
|
-
|
|
808
|
-
|
|
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: "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 (
|
|
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
|
-
"
|
|
956
|
+
"Write targets",
|
|
836
957
|
);
|
|
837
958
|
|
|
838
|
-
const okGo = cancelIf(await p.confirm({ message: first ? "
|
|
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 ? "
|
|
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
|
package/kit-overlay/AGENTS.md
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
npx
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|