this-is-enough 0.1.0
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/AGENTS.md +546 -0
- package/README.md +126 -0
- package/bin/cli.js +461 -0
- package/package.json +16 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
# AGENTS.md - Project Agent Operating Rules
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Operate this repository with a strict single-active-requirement workflow.
|
|
5
|
+
Enforce one active lane at a time while still allowing controlled pause/switch/resume.
|
|
6
|
+
Keep architecture, decisions, and validation evidence consistent.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Single Source of Truth (SSOT)
|
|
11
|
+
|
|
12
|
+
### 1) Active Requirement Lock (SSOT)
|
|
13
|
+
- `reqs/ACTIVE.md` is the only lock + pointer for the active requirement.
|
|
14
|
+
- If `reqs/ACTIVE.md` exists:
|
|
15
|
+
- One requirement is active (`DRAFT` or `ACTIVE`).
|
|
16
|
+
- Creating a new `reqs/REQ-*` directory is forbidden.
|
|
17
|
+
- If `reqs/ACTIVE.md` does not exist:
|
|
18
|
+
- No requirement is active.
|
|
19
|
+
- Creating a new requirement is allowed.
|
|
20
|
+
|
|
21
|
+
Note:
|
|
22
|
+
- `reqs/ACTIVE.md` exists only while a requirement is active.
|
|
23
|
+
- Inactive states (`PAUSED`, `BLOCKED`, `DONE`) are recorded in each requirement's `STATUS.md`.
|
|
24
|
+
|
|
25
|
+
### 2) Current Architecture (SSOT for current design)
|
|
26
|
+
- `ARCHITECTURE.md` represents the current system design and core logic.
|
|
27
|
+
- Update `ARCHITECTURE.md` only when a validated phase introduces architecture-impacting changes.
|
|
28
|
+
|
|
29
|
+
### 3) Decisions (SSOT for why)
|
|
30
|
+
- ADRs live in `docs/adr/`.
|
|
31
|
+
- Add or update an ADR only when a validated phase introduces architecture-impacting changes.
|
|
32
|
+
|
|
33
|
+
### 4) Backlog Intake (SSOT for independent incoming requests)
|
|
34
|
+
- `reqs/INBOX.md` is the global intake/backlog for independent requests.
|
|
35
|
+
- While `reqs/ACTIVE.md` exists:
|
|
36
|
+
- independent new requests must be appended to `reqs/INBOX.md`
|
|
37
|
+
- they must not be converted into a new `REQ-*` until unlocked (or after explicit pause/switch)
|
|
38
|
+
|
|
39
|
+
### 5) Interrupt Exception State (SSOT for urgent exception lane)
|
|
40
|
+
- `reqs/INTERRUPT.md` is the only active record for an urgent interrupt.
|
|
41
|
+
- At most one urgent interrupt may be active at a time.
|
|
42
|
+
- After completion, move `reqs/INTERRUPT.md` to `reqs/interrupts/INT-*.md` and remove the active interrupt file.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Repository Layout (Required)
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
ARCHITECTURE.md
|
|
50
|
+
AGENTS.md
|
|
51
|
+
docs/
|
|
52
|
+
adr/
|
|
53
|
+
reqs/
|
|
54
|
+
INBOX.md # global intake for independent requests
|
|
55
|
+
ACTIVE.md # present only while a requirement is active
|
|
56
|
+
INTERRUPT.md # present only while an urgent interrupt is active
|
|
57
|
+
interrupts/ # archive of completed interrupts
|
|
58
|
+
INT-YYYYMMDD-HHMM-<slug>.md
|
|
59
|
+
REQ-0001-<slug>/
|
|
60
|
+
REQUEST.md # requirement draft/spec
|
|
61
|
+
ROADMAP.md # full roadmap (required unless Mini Roadmap criteria are fully met)
|
|
62
|
+
STATUS.md # status + phase state + inactive-state record
|
|
63
|
+
DEFERRED.md # req-local parking lot for derived non-mandatory ideas
|
|
64
|
+
phases/
|
|
65
|
+
PHASE-01/
|
|
66
|
+
PLAN.md
|
|
67
|
+
VALIDATION_LOG.md
|
|
68
|
+
PHASE-02/
|
|
69
|
+
PLAN.md
|
|
70
|
+
VALIDATION_LOG.md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Day-0 Bootstrap (MUST)
|
|
76
|
+
|
|
77
|
+
Run once when adopting this workflow in a new repository.
|
|
78
|
+
|
|
79
|
+
1) Create required directories
|
|
80
|
+
- `docs/adr/`
|
|
81
|
+
- `reqs/`
|
|
82
|
+
- `reqs/interrupts/`
|
|
83
|
+
|
|
84
|
+
2) Create required baseline files
|
|
85
|
+
- `ARCHITECTURE.md` (initial architecture baseline; can start minimal)
|
|
86
|
+
- `reqs/INBOX.md` (initialize with header and item schema)
|
|
87
|
+
|
|
88
|
+
3) Initial lock/interrupt state must be clean
|
|
89
|
+
- `reqs/ACTIVE.md` must not exist at bootstrap completion
|
|
90
|
+
- `reqs/INTERRUPT.md` must not exist at bootstrap completion
|
|
91
|
+
|
|
92
|
+
4) First requirement starts only after bootstrap
|
|
93
|
+
- create `REQ-*` through lifecycle section `A) Create New Requirement`
|
|
94
|
+
- confirm via section `B) Confirm Requirement` (`full` or `mini` mode)
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Status Model (Requirement-Level)
|
|
99
|
+
|
|
100
|
+
Allowed statuses:
|
|
101
|
+
- `DRAFT`: requirement exists and is being drafted (lock is active)
|
|
102
|
+
- `ACTIVE`: confirmed and currently being executed (lock is active)
|
|
103
|
+
- `PAUSED`: intentionally stopped to work on another requirement (lock not held)
|
|
104
|
+
- `BLOCKED`: cannot progress due to external dependency (lock not held)
|
|
105
|
+
- `DONE`: fully completed (lock not held)
|
|
106
|
+
|
|
107
|
+
Rules:
|
|
108
|
+
- Only one requirement may be in `DRAFT` or `ACTIVE` at a time.
|
|
109
|
+
- `PAUSED`/`BLOCKED` requirements must include resume metadata in `STATUS.md`.
|
|
110
|
+
|
|
111
|
+
Definitions:
|
|
112
|
+
- `activate requirement`: set target requirement `STATUS.md` to `ACTIVE` and set `reqs/ACTIVE.md` to that target.
|
|
113
|
+
- `switch execution`: perform switch steps atomically (one continuous operation) without per-step user confirmation; report the final switch result. Ask for user input only if target selection or priority is ambiguous.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Hard Constraints (MUST)
|
|
118
|
+
|
|
119
|
+
1) Single active requirement
|
|
120
|
+
- Only one requirement lane may be active at a time (`DRAFT` or `ACTIVE`).
|
|
121
|
+
- If `reqs/ACTIVE.md` exists, no new `REQ-*` directory may be created unless pause/switch flow is completed.
|
|
122
|
+
|
|
123
|
+
2) Single urgent interrupt exception
|
|
124
|
+
- While `reqs/ACTIVE.md` exists, one temporary interrupt lane may be opened through `reqs/INTERRUPT.md`.
|
|
125
|
+
- Interrupt eligibility uses a strict 3-question gate (all `yes` required):
|
|
126
|
+
- Q1 (`impact_now`): Is there immediate production/customer/business impact that cannot wait? (`yes` for outage, data risk, active security issue, release-blocking incident)
|
|
127
|
+
- Q2 (`timebox_fit`): Can a safe minimal fix be completed within <= 8 hours?
|
|
128
|
+
- Q3 (`scope_safe`): Can it be fixed without architecture-impacting changes (no external contract change, no schema/migration, no boundary/topology/security model change)?
|
|
129
|
+
- Priority requirement:
|
|
130
|
+
- only `P0` or `P1` may use interrupt flow
|
|
131
|
+
- During an interrupt, do not create a new `REQ-*`; execute only the interrupt scope.
|
|
132
|
+
- At most one `reqs/INTERRUPT.md` may exist at a time.
|
|
133
|
+
- If any gate answer is `no`, do not open interrupt flow; route to normal pause/switch + `REQ-*` flow.
|
|
134
|
+
|
|
135
|
+
3) No new requirement creation while locked
|
|
136
|
+
- While `reqs/ACTIVE.md` exists:
|
|
137
|
+
- forbidden: creating a new `REQUEST.md` for another requirement
|
|
138
|
+
- forbidden: creating a new `REQ-*` folder
|
|
139
|
+
- allowed alternatives:
|
|
140
|
+
- append req-derived/non-mandatory ideas to active requirement `DEFERRED.md`
|
|
141
|
+
- append independent new requests to `reqs/INBOX.md`
|
|
142
|
+
- use `reqs/INTERRUPT.md` flow for qualifying urgent interrupts
|
|
143
|
+
|
|
144
|
+
4) Agent must announce lock/interrupt state
|
|
145
|
+
- At session start and before new planning:
|
|
146
|
+
- check `reqs/INTERRUPT.md` and `reqs/ACTIVE.md`
|
|
147
|
+
- if `reqs/INTERRUPT.md` exists, announce interrupt `id`, `status`, `reason`, `related_active`
|
|
148
|
+
- else if `reqs/ACTIVE.md` exists, announce active requirement `id`, `path`, `status`, `current_phase`
|
|
149
|
+
- else announce no active requirement and that new creation is allowed
|
|
150
|
+
|
|
151
|
+
5) Requirement confirmation rule
|
|
152
|
+
- Requirement is confirmed by one of:
|
|
153
|
+
- Full mode: `ROADMAP.md` exists.
|
|
154
|
+
- Mini mode: `REQUEST.md` contains a `Mini Roadmap` section and all criteria below are `yes/no`-exactly satisfied:
|
|
155
|
+
- `single_phase: yes`
|
|
156
|
+
- `architecture_impact: no`
|
|
157
|
+
- `external_contract_change: no`
|
|
158
|
+
- `data_model_change: no`
|
|
159
|
+
- `security_or_topology_change: no`
|
|
160
|
+
- If any Mini mode criterion becomes false during execution, create `ROADMAP.md` immediately and continue in Full mode.
|
|
161
|
+
|
|
162
|
+
6) Architecture-impact gated updates
|
|
163
|
+
- After each phase validation:
|
|
164
|
+
- `VALIDATION_LOG.md` must record:
|
|
165
|
+
- `architecture_impact: yes|no`
|
|
166
|
+
- `architecture_impact_reason: <one-line reason>`
|
|
167
|
+
- if `architecture_impact: yes`:
|
|
168
|
+
- write/update ADR in `docs/adr/`
|
|
169
|
+
- update `ARCHITECTURE.md`
|
|
170
|
+
- if `architecture_impact: no`:
|
|
171
|
+
- ADR/`ARCHITECTURE.md` updates are optional for that phase
|
|
172
|
+
|
|
173
|
+
Architecture-impacting change criteria (MUST):
|
|
174
|
+
- classify a phase as architecture-impacting when at least one is true:
|
|
175
|
+
1. external interface contract changed (API route/schema/status/event/topic/CLI contract)
|
|
176
|
+
2. data model or persistence changed (schema/index/migration/storage model)
|
|
177
|
+
3. system boundary/topology changed (service split/merge, sync<->async flow, queue/broker introduction)
|
|
178
|
+
4. security/trust model changed (auth/authz, secret handling, tenancy/isolation, compliance control)
|
|
179
|
+
5. reliability/performance design policy changed (caching strategy, consistency model, retry/idempotency policy, SLO-critical path)
|
|
180
|
+
- non-architecture-impacting examples:
|
|
181
|
+
- bug fixes or refactors without external contract/data model/boundary changes
|
|
182
|
+
- test-only changes, docs-only changes, copy/style/UI-only changes
|
|
183
|
+
- tie-breaker:
|
|
184
|
+
- if uncertain, classify as `architecture_impact: yes` and update ADR + `ARCHITECTURE.md`.
|
|
185
|
+
|
|
186
|
+
7) Inactive-state record rule
|
|
187
|
+
- When moving to `PAUSED` or `BLOCKED`, `STATUS.md` must record:
|
|
188
|
+
- `changed_at`
|
|
189
|
+
- `reason`
|
|
190
|
+
- `resume_condition`
|
|
191
|
+
- `next_action`
|
|
192
|
+
|
|
193
|
+
8) Safe switch rule
|
|
194
|
+
- Switching to a different requirement is allowed only by:
|
|
195
|
+
1. recording a checkpoint in current phase `VALIDATION_LOG.md`
|
|
196
|
+
2. setting current requirement `STATUS.md` to `PAUSED` or `BLOCKED`
|
|
197
|
+
3. removing/updating `reqs/ACTIVE.md`
|
|
198
|
+
4. activating the target requirement by rule:
|
|
199
|
+
- if target status is `DRAFT`, apply lifecycle section `B) Confirm Requirement`
|
|
200
|
+
- if target status is `PAUSED` or `BLOCKED`, apply lifecycle section `G) Resume Paused/Blocked Requirement`
|
|
201
|
+
5. execute switch atomically and report result summary
|
|
202
|
+
|
|
203
|
+
9) Backlog separation rule
|
|
204
|
+
- `DEFERRED.md` is only for ideas derived from the currently active requirement.
|
|
205
|
+
- `reqs/INBOX.md` is for independent/external requests.
|
|
206
|
+
- Do not mix them.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Session Start Checklist (MUST)
|
|
211
|
+
|
|
212
|
+
1) Check `reqs/INTERRUPT.md` and `reqs/ACTIVE.md`
|
|
213
|
+
- If `reqs/INTERRUPT.md` exists:
|
|
214
|
+
- announce active interrupt context
|
|
215
|
+
- proceed only within interrupt scope until closed
|
|
216
|
+
- Else if `reqs/ACTIVE.md` exists:
|
|
217
|
+
- announce active requirement context
|
|
218
|
+
- proceed within that requirement
|
|
219
|
+
- Else:
|
|
220
|
+
- announce unlocked state and allow creating a new requirement
|
|
221
|
+
|
|
222
|
+
2) If interrupt is active, read in this order
|
|
223
|
+
1. `reqs/INTERRUPT.md`
|
|
224
|
+
2. `reqs/ACTIVE.md`
|
|
225
|
+
3. `<active_req_path>/STATUS.md`
|
|
226
|
+
4. current phase `PLAN.md` + `VALIDATION_LOG.md` (if exists)
|
|
227
|
+
5. conditionally read `ARCHITECTURE.md` only when at least one is true:
|
|
228
|
+
- current interrupt scope may hit any architecture-impacting criterion
|
|
229
|
+
- latest validation context indicates `architecture_impact: yes`
|
|
230
|
+
- this session just switched/resumed active requirement
|
|
231
|
+
6. conditionally read related ADRs using `ADR selective read protocol` below
|
|
232
|
+
|
|
233
|
+
3) If active requirement only (no interrupt), read in this order
|
|
234
|
+
1. `reqs/ACTIVE.md`
|
|
235
|
+
2. `<active_req_path>/STATUS.md`
|
|
236
|
+
3. `<active_req_path>/REQUEST.md`
|
|
237
|
+
4. `<active_req_path>/ROADMAP.md` (if exists)
|
|
238
|
+
5. current phase `PLAN.md` + `VALIDATION_LOG.md` (if exists)
|
|
239
|
+
6. conditionally read `ARCHITECTURE.md` only when at least one is true:
|
|
240
|
+
- current planned change may hit any architecture-impacting criterion
|
|
241
|
+
- latest validation context indicates `architecture_impact: yes`
|
|
242
|
+
- this session just switched/resumed active requirement
|
|
243
|
+
7. conditionally read related ADRs using `ADR selective read protocol` below
|
|
244
|
+
|
|
245
|
+
4) If unlocked and `reqs/INBOX.md` exists
|
|
246
|
+
- review top `open` backlog items before creating a new requirement
|
|
247
|
+
|
|
248
|
+
ADR selective read protocol (MUST):
|
|
249
|
+
1. scan ADR filenames/titles in `docs/adr/` first (do not read all ADR bodies by default)
|
|
250
|
+
2. for candidate ADRs, read top metadata first
|
|
251
|
+
3. fully read only ADRs that match the current change scope
|
|
252
|
+
4. if matched ADR has `supersedes`, read the superseded ADR one hop for context
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Requirement Lifecycle (MUST)
|
|
257
|
+
|
|
258
|
+
### Intake Rule (Always)
|
|
259
|
+
When a new item appears during work:
|
|
260
|
+
1. if it is required to complete current requirement acceptance, include it in current requirement plan/scope
|
|
261
|
+
2. if it is derived from current requirement but not required now, append to `<active_req>/DEFERRED.md`
|
|
262
|
+
3. if it is an independent/external request, append to `reqs/INBOX.md`
|
|
263
|
+
|
|
264
|
+
### A) Create New Requirement (Only when unlocked)
|
|
265
|
+
Precondition:
|
|
266
|
+
- `reqs/ACTIVE.md` does not exist
|
|
267
|
+
|
|
268
|
+
Steps:
|
|
269
|
+
1. create `reqs/REQ-XXXX-<slug>/`
|
|
270
|
+
2. create `REQUEST.md`
|
|
271
|
+
3. create `STATUS.md` with status `DRAFT`
|
|
272
|
+
4. create `reqs/ACTIVE.md` pointing to this requirement with status `DRAFT`
|
|
273
|
+
5. if sourced from `reqs/INBOX.md`, update that item status to `promoted`
|
|
274
|
+
|
|
275
|
+
### B) Confirm Requirement (`DRAFT` -> `ACTIVE`)
|
|
276
|
+
Trigger:
|
|
277
|
+
- one of:
|
|
278
|
+
- Full mode: `ROADMAP.md` created in active requirement
|
|
279
|
+
- Mini mode: `REQUEST.md` includes `Mini Roadmap` and meets all Mini criteria
|
|
280
|
+
|
|
281
|
+
Steps:
|
|
282
|
+
1. choose confirmation mode (`full` or `mini`)
|
|
283
|
+
2. if `full`: create `ROADMAP.md` (phases, goals, acceptance)
|
|
284
|
+
3. if `mini`: add `Mini Roadmap` in `REQUEST.md` (tasks + acceptance + criteria flags)
|
|
285
|
+
4. update `STATUS.md` to `ACTIVE` and record `roadmap_mode: full|mini`
|
|
286
|
+
5. update `reqs/ACTIVE.md` to `ACTIVE`
|
|
287
|
+
|
|
288
|
+
### C) Execute Phase (loop)
|
|
289
|
+
For each `PHASE-XX`:
|
|
290
|
+
1. write `phases/PHASE-XX/PLAN.md` with ordered tasks, validation checklist, exit criteria
|
|
291
|
+
2. implement according to plan
|
|
292
|
+
3. append evidence to `phases/PHASE-XX/VALIDATION_LOG.md`
|
|
293
|
+
|
|
294
|
+
### D) Complete Phase (2 required + 2 conditional)
|
|
295
|
+
A phase is `DONE` only when all required checks pass:
|
|
296
|
+
1. `VALIDATION_LOG.md` has acceptance conclusion
|
|
297
|
+
2. `STATUS.md` and `reqs/ACTIVE.md` are updated
|
|
298
|
+
3. if architecture-impacting changes occurred, ADR is added/updated in `docs/adr/`
|
|
299
|
+
4. if architecture-impacting changes occurred, `ARCHITECTURE.md` reflects the current design
|
|
300
|
+
|
|
301
|
+
### E) Pause or Block Current Requirement
|
|
302
|
+
Use when scope split or dependency wait occurs.
|
|
303
|
+
|
|
304
|
+
Steps:
|
|
305
|
+
1. write checkpoint summary in current `VALIDATION_LOG.md`
|
|
306
|
+
2. update `STATUS.md` to `PAUSED` or `BLOCKED`
|
|
307
|
+
3. fill inactive-state record fields (`changed_at`, `reason`, `resume_condition`, `next_action`)
|
|
308
|
+
4. remove `reqs/ACTIVE.md` to unlock
|
|
309
|
+
|
|
310
|
+
### F) Switch to Another Requirement
|
|
311
|
+
Precondition:
|
|
312
|
+
- previous requirement is set to `PAUSED` or `BLOCKED`
|
|
313
|
+
- lock is removed
|
|
314
|
+
|
|
315
|
+
Steps:
|
|
316
|
+
1. execute switch as one atomic operation (no per-step user confirmation)
|
|
317
|
+
2. activate target by its current status:
|
|
318
|
+
- target `DRAFT`: follow section `B) Confirm Requirement`
|
|
319
|
+
- target `PAUSED`/`BLOCKED`: follow section `G) Resume Paused/Blocked Requirement`
|
|
320
|
+
3. continue work only on newly active requirement
|
|
321
|
+
|
|
322
|
+
### G) Resume Paused/Blocked Requirement
|
|
323
|
+
Precondition:
|
|
324
|
+
- `reqs/ACTIVE.md` does not exist
|
|
325
|
+
|
|
326
|
+
Steps:
|
|
327
|
+
1. verify `resume_condition` is met
|
|
328
|
+
2. create `reqs/ACTIVE.md` pointing to paused/blocked requirement
|
|
329
|
+
3. update status to `ACTIVE`
|
|
330
|
+
4. continue from recorded `next_action`
|
|
331
|
+
|
|
332
|
+
### H) Finish Requirement (`DONE` + unlock)
|
|
333
|
+
Precondition:
|
|
334
|
+
- all phases are `DONE` and validated
|
|
335
|
+
|
|
336
|
+
Steps:
|
|
337
|
+
1. update `<active_req_path>/STATUS.md` to `DONE` with date
|
|
338
|
+
2. remove `reqs/ACTIVE.md`
|
|
339
|
+
3. optionally append completion summary
|
|
340
|
+
|
|
341
|
+
### I) Open Urgent Interrupt (Exception Path)
|
|
342
|
+
Precondition:
|
|
343
|
+
- `reqs/ACTIVE.md` exists
|
|
344
|
+
- `reqs/INTERRUPT.md` does not exist
|
|
345
|
+
- interrupt eligibility is satisfied (see Hard Constraints rule 2)
|
|
346
|
+
|
|
347
|
+
Steps:
|
|
348
|
+
1. append a short checkpoint in current phase `VALIDATION_LOG.md`
|
|
349
|
+
2. create `reqs/INTERRUPT.md` with interrupt metadata and narrow scope
|
|
350
|
+
3. execute only interrupt scope and record validation evidence in `reqs/INTERRUPT.md`
|
|
351
|
+
4. if scope exceeds timebox or becomes non-urgent, close interrupt and move work to normal pause/switch + `REQ-*` flow
|
|
352
|
+
|
|
353
|
+
### J) Close Urgent Interrupt (Archive + Resume)
|
|
354
|
+
Precondition:
|
|
355
|
+
- `reqs/INTERRUPT.md` exists
|
|
356
|
+
|
|
357
|
+
Steps:
|
|
358
|
+
1. update `reqs/INTERRUPT.md` with resolution summary and validation outcome
|
|
359
|
+
2. move `reqs/INTERRUPT.md` to `reqs/interrupts/INT-YYYYMMDD-HHMM-<slug>.md`
|
|
360
|
+
3. append a single-line resume note in current phase `VALIDATION_LOG.md` with archived interrupt path only
|
|
361
|
+
4. continue work on requirement referenced by `reqs/ACTIVE.md`
|
|
362
|
+
|
|
363
|
+
Interrupt logging source-of-truth rule:
|
|
364
|
+
- interrupt details (root cause, changes, validation evidence) must live in interrupt record (`reqs/INTERRUPT.md` while active, `reqs/interrupts/INT-*.md` after close)
|
|
365
|
+
- requirement `VALIDATION_LOG.md` must keep only one-line interrupt close/resume note with pointer to archived interrupt record
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Backlog Rules (`reqs/INBOX.md` and `DEFERRED.md`)
|
|
370
|
+
|
|
371
|
+
Purpose:
|
|
372
|
+
- capture incoming work without breaking single-active workflow
|
|
373
|
+
- separate independent requests from requirement-local deferred ideas
|
|
374
|
+
|
|
375
|
+
While locked:
|
|
376
|
+
- do not create new requirement docs for side requests
|
|
377
|
+
- append independent/external requests to `reqs/INBOX.md`
|
|
378
|
+
- append requirement-derived/non-mandatory ideas to active requirement `DEFERRED.md`
|
|
379
|
+
- if a request is urgent and satisfies interrupt criteria, handle it via `reqs/INTERRUPT.md` flow
|
|
380
|
+
- capture-time rule: fill all fields from the selected item schema at capture time; if unknown, use explicit placeholder (`unknown` or `-`) and update later
|
|
381
|
+
|
|
382
|
+
`reqs/INBOX.md` item schema (recommended):
|
|
383
|
+
|
|
384
|
+
```md
|
|
385
|
+
- id: INB-0001
|
|
386
|
+
title: <short request>
|
|
387
|
+
captured_at: 2026-02-14
|
|
388
|
+
source: <who/where>
|
|
389
|
+
priority: P2 # P0 | P1 | P2 | P3
|
|
390
|
+
status: open # open | promoted | dropped
|
|
391
|
+
promoted_to: - # e.g., REQ-0008
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Deferred item schema (recommended):
|
|
395
|
+
|
|
396
|
+
```md
|
|
397
|
+
- id: DEF-0001
|
|
398
|
+
title: <short idea>
|
|
399
|
+
captured_at: 2026-02-14
|
|
400
|
+
reason_deferred: <why not now>
|
|
401
|
+
target: same-req-later # same-req-later | next-req | unknown
|
|
402
|
+
status: open # open | promoted | dropped
|
|
403
|
+
promoted_to: - # e.g., REQ-0008
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
At requirement close:
|
|
407
|
+
- review all deferred items
|
|
408
|
+
- mark each as `promoted` or `dropped`
|
|
409
|
+
- for `next-req` deferred items, create/update matching `reqs/INBOX.md` item(s)
|
|
410
|
+
|
|
411
|
+
When to execute deferred items:
|
|
412
|
+
1. at phase planning refresh (check if a deferred item should be pulled into current phase)
|
|
413
|
+
2. before requirement `DONE` (resolve each deferred item to `promoted` or `dropped`)
|
|
414
|
+
3. after unlock, when creating next requirement from promoted backlog
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## File Formats (Machine-Readable)
|
|
419
|
+
|
|
420
|
+
### `reqs/INBOX.md` (global independent backlog)
|
|
421
|
+
|
|
422
|
+
```md
|
|
423
|
+
# REQUIREMENT INBOX
|
|
424
|
+
|
|
425
|
+
- id: INB-0001
|
|
426
|
+
title: Add 2FA to login
|
|
427
|
+
captured_at: 2026-02-14
|
|
428
|
+
source: product-review
|
|
429
|
+
priority: P2
|
|
430
|
+
status: open # open | promoted | dropped
|
|
431
|
+
promoted_to: -
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### `reqs/ACTIVE.md` (lock + pointer)
|
|
435
|
+
|
|
436
|
+
```md
|
|
437
|
+
# ACTIVE REQUIREMENT (LOCK)
|
|
438
|
+
|
|
439
|
+
id: REQ-0007
|
|
440
|
+
path: reqs/REQ-0007-some-feature
|
|
441
|
+
status: ACTIVE # DRAFT | ACTIVE
|
|
442
|
+
current_phase: PHASE-01
|
|
443
|
+
started_at: 2026-02-12
|
|
444
|
+
|
|
445
|
+
links:
|
|
446
|
+
- status: ./REQ-0007-some-feature/STATUS.md
|
|
447
|
+
- request: ./REQ-0007-some-feature/REQUEST.md
|
|
448
|
+
- roadmap: ./REQ-0007-some-feature/ROADMAP.md
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### `reqs/INTERRUPT.md` (active urgent exception)
|
|
452
|
+
|
|
453
|
+
```md
|
|
454
|
+
# ACTIVE INTERRUPT (EXCEPTION)
|
|
455
|
+
|
|
456
|
+
id: INT-20260214-2300-login-hotfix
|
|
457
|
+
type: HOTFIX # HOTFIX | BLOCKER
|
|
458
|
+
priority: P0 # P0 | P1
|
|
459
|
+
status: ACTIVE # ACTIVE | DONE
|
|
460
|
+
reason: login outage after deployment
|
|
461
|
+
related_active: REQ-0007
|
|
462
|
+
started_at: 2026-02-14T23:00:00Z
|
|
463
|
+
timebox_hours: 4
|
|
464
|
+
|
|
465
|
+
eligibility_check:
|
|
466
|
+
- impact_now: yes
|
|
467
|
+
- timebox_fit: yes
|
|
468
|
+
- scope_safe: yes
|
|
469
|
+
|
|
470
|
+
scope:
|
|
471
|
+
- restore login path
|
|
472
|
+
- add regression test for the failing case
|
|
473
|
+
|
|
474
|
+
validation:
|
|
475
|
+
- <what was checked>
|
|
476
|
+
- <result>
|
|
477
|
+
|
|
478
|
+
exit_rule:
|
|
479
|
+
- on close, move to `reqs/interrupts/INT-*.md` and clear active `reqs/INTERRUPT.md`
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `STATUS.md` (requirement status)
|
|
483
|
+
Must include:
|
|
484
|
+
- requirement id
|
|
485
|
+
- current status (`DRAFT|ACTIVE|PAUSED|BLOCKED|DONE`)
|
|
486
|
+
- roadmap mode (`full|mini`)
|
|
487
|
+
- phase list (`TODO|ACTIVE|DONE`)
|
|
488
|
+
- pointers to roadmap/current phase plan/validation
|
|
489
|
+
- inactive-state record when status is `PAUSED` or `BLOCKED`
|
|
490
|
+
|
|
491
|
+
### `REQUEST.md` Mini Roadmap section (when `roadmap_mode: mini`)
|
|
492
|
+
|
|
493
|
+
```md
|
|
494
|
+
## Mini Roadmap
|
|
495
|
+
single_phase: yes
|
|
496
|
+
architecture_impact: no
|
|
497
|
+
external_contract_change: no
|
|
498
|
+
data_model_change: no
|
|
499
|
+
security_or_topology_change: no
|
|
500
|
+
|
|
501
|
+
tasks:
|
|
502
|
+
- <ordered task 1>
|
|
503
|
+
- <ordered task 2>
|
|
504
|
+
|
|
505
|
+
acceptance:
|
|
506
|
+
- <validation check 1>
|
|
507
|
+
- <validation check 2>
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### ADR naming
|
|
511
|
+
Use one format:
|
|
512
|
+
- `docs/adr/ADR-0001-<short-title>.md`
|
|
513
|
+
- `docs/adr/ADR-YYYYMMDD-<short-title>.md`
|
|
514
|
+
|
|
515
|
+
Each ADR must reference:
|
|
516
|
+
- related requirement id
|
|
517
|
+
- related phase
|
|
518
|
+
- decision, alternatives, consequences
|
|
519
|
+
|
|
520
|
+
ADR metadata for selective lookup (required for new ADRs):
|
|
521
|
+
- components: `<component-a>, <component-b>, ...`
|
|
522
|
+
- status: `active|superseded`
|
|
523
|
+
- supersedes: `<ADR-id>` (optional)
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Prohibitions (MUST NOT)
|
|
528
|
+
- do not create a new requirement while `reqs/ACTIVE.md` exists (except the explicit interrupt exception path)
|
|
529
|
+
- do not treat requirement as confirmed without either:
|
|
530
|
+
- `ROADMAP.md` (full mode), or
|
|
531
|
+
- valid `REQUEST.md` Mini Roadmap that satisfies all Mini criteria (mini mode)
|
|
532
|
+
- do not keep `roadmap_mode: mini` if any Mini criterion becomes false; switch to full mode immediately
|
|
533
|
+
- do not store independent/external requests only in a requirement `DEFERRED.md`
|
|
534
|
+
- do not promote `reqs/INBOX.md` item to a new `REQ-*` while locked, unless pause/switch procedure is completed
|
|
535
|
+
- do not mark a phase `DONE` without:
|
|
536
|
+
- `VALIDATION_LOG` acceptance
|
|
537
|
+
- `STATUS.md` and `ACTIVE.md` update
|
|
538
|
+
- if architecture-impacting changes occurred: ADR update
|
|
539
|
+
- if architecture-impacting changes occurred: `ARCHITECTURE.md` update
|
|
540
|
+
- do not record `architecture_impact: no` when any architecture-impacting criterion is met
|
|
541
|
+
- do not open interrupt work as `reqs/INTERRUPT.md` unless all interrupt eligibility criteria are met
|
|
542
|
+
- do not open interrupt work with any missing gate answers (`impact_now`, `timebox_fit`, `scope_safe`)
|
|
543
|
+
- do not keep more than one active interrupt record
|
|
544
|
+
- do not leave `reqs/INTERRUPT.md` after interrupt close; archive it to `reqs/interrupts/`
|
|
545
|
+
- do not duplicate interrupt detail logs in requirement `VALIDATION_LOG.md`; store details only in interrupt record
|
|
546
|
+
- do not switch requirements without writing pause/block checkpoint + inactive-state record
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ThisIsEnough
|
|
2
|
+
|
|
3
|
+
가볍지만 실행 가능한 에이전트 작업 규칙을 프로젝트에 설치하는 CLI입니다.
|
|
4
|
+
|
|
5
|
+
핵심 목표:
|
|
6
|
+
- 요청 수신
|
|
7
|
+
- 로드맵/플랜 정리
|
|
8
|
+
- 페이즈 실행
|
|
9
|
+
- 검증/기록 유지
|
|
10
|
+
|
|
11
|
+
기본 규칙 문서는 루트 `AGENTS.md`를 사용합니다.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
빠른 시작:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx this-is-enough@latest init --mode existing --dry-run
|
|
19
|
+
npx this-is-enough@latest init --mode existing
|
|
20
|
+
npx this-is-enough@latest doctor
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
도움말:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx this-is-enough@latest --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
로컬 개발 실행:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node /path/to/this-is-enough/bin/cli.js --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### `init`
|
|
38
|
+
|
|
39
|
+
Day-0 구조를 생성/보강합니다.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx this-is-enough@latest init --mode new --cwd /path/to/repo
|
|
43
|
+
npx this-is-enough@latest init --mode existing --cwd /path/to/repo --dry-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
로컬 실행:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node /path/to/this-is-enough/bin/cli.js init --mode existing --cwd /path/to/repo
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
옵션:
|
|
53
|
+
- `--mode new|existing`
|
|
54
|
+
- `--cwd <path>` (기본값: 현재 디렉토리)
|
|
55
|
+
- `--dry-run` (변경 예정만 출력)
|
|
56
|
+
- `--force` (기존 파일 덮어쓰기)
|
|
57
|
+
- `--yes` (호환용, 기본이 이미 non-interactive)
|
|
58
|
+
|
|
59
|
+
### `doctor`
|
|
60
|
+
|
|
61
|
+
규칙 준수 상태를 점검합니다.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx this-is-enough@latest doctor --cwd /path/to/repo
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
로컬 실행:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
node /path/to/this-is-enough/bin/cli.js doctor --cwd /path/to/repo
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
출력:
|
|
74
|
+
- `OK`: 정상
|
|
75
|
+
- `WARN`: 주의
|
|
76
|
+
- `FAIL`: 필수 항목 누락/불일치 (exit code 1)
|
|
77
|
+
|
|
78
|
+
### `upgrade`
|
|
79
|
+
|
|
80
|
+
템플릿/구조를 안전하게 업데이트합니다.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx this-is-enough@latest upgrade --cwd /path/to/repo --dry-run
|
|
84
|
+
npx this-is-enough@latest upgrade --cwd /path/to/repo
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
로컬 실행:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
node /path/to/this-is-enough/bin/cli.js upgrade --cwd /path/to/repo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
동작:
|
|
94
|
+
- 누락된 `docs/adr`, `reqs`, `reqs/interrupts` 생성
|
|
95
|
+
- 누락된 `ARCHITECTURE.md`, `reqs/INBOX.md` 생성
|
|
96
|
+
- `AGENTS.md`가 다르면 백업(`AGENTS.md.bak.<timestamp>`) 후 갱신
|
|
97
|
+
|
|
98
|
+
## Installed Structure
|
|
99
|
+
|
|
100
|
+
`init`/`upgrade`는 아래 구조를 기준으로 맞춥니다.
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
ARCHITECTURE.md
|
|
104
|
+
AGENTS.md
|
|
105
|
+
docs/
|
|
106
|
+
adr/
|
|
107
|
+
reqs/
|
|
108
|
+
INBOX.md
|
|
109
|
+
interrupts/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
작업 진행 중에는 규칙에 따라 아래 파일이 생길 수 있습니다.
|
|
113
|
+
- `reqs/ACTIVE.md`
|
|
114
|
+
- `reqs/INTERRUPT.md`
|
|
115
|
+
- `reqs/REQ-XXXX-*/...`
|
|
116
|
+
|
|
117
|
+
## Workflow Modes
|
|
118
|
+
|
|
119
|
+
- `new`: 빈 프로젝트/초기 프로젝트 설치
|
|
120
|
+
- clean state 전제 (`reqs/ACTIVE.md`, `reqs/INTERRUPT.md` 없음)
|
|
121
|
+
- `existing`: 진행 중 프로젝트 중간 도입
|
|
122
|
+
- 기존 상태 보존 중심으로 설치
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
ThisIsEnough CLI
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
this-is-enough init --mode <new|existing> [--cwd <path>] [--dry-run] [--force] [--yes]
|
|
11
|
+
this-is-enough doctor [--cwd <path>]
|
|
12
|
+
this-is-enough upgrade [--cwd <path>] [--dry-run] [--yes]
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
- commands are non-interactive by default (no mid-run prompts).
|
|
16
|
+
- --yes is accepted for compatibility but not required.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const ARCHITECTURE_TEMPLATE = `# ARCHITECTURE.md
|
|
20
|
+
|
|
21
|
+
## Metadata
|
|
22
|
+
- last_updated:
|
|
23
|
+
- scope: as-is baseline
|
|
24
|
+
|
|
25
|
+
## System Boundary
|
|
26
|
+
- TODO
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const INBOX_TEMPLATE = `# REQUIREMENT INBOX
|
|
30
|
+
|
|
31
|
+
- id: INB-0001
|
|
32
|
+
title: <short request>
|
|
33
|
+
captured_at: YYYY-MM-DD
|
|
34
|
+
source: <who/where>
|
|
35
|
+
priority: P2
|
|
36
|
+
status: open
|
|
37
|
+
promoted_to: -
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
function resolveProjectPaths(cwd) {
|
|
41
|
+
return {
|
|
42
|
+
agentsPath: path.join(cwd, "AGENTS.md"),
|
|
43
|
+
architecturePath: path.join(cwd, "ARCHITECTURE.md"),
|
|
44
|
+
inboxPath: path.join(cwd, "reqs", "INBOX.md"),
|
|
45
|
+
activePath: path.join(cwd, "reqs", "ACTIVE.md"),
|
|
46
|
+
interruptPath: path.join(cwd, "reqs", "INTERRUPT.md"),
|
|
47
|
+
adrDir: path.join(cwd, "docs", "adr"),
|
|
48
|
+
reqsDir: path.join(cwd, "reqs"),
|
|
49
|
+
interruptsDir: path.join(cwd, "reqs", "interrupts")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fail(message, code = 1) {
|
|
54
|
+
console.error(`ERROR: ${message}`);
|
|
55
|
+
process.exit(code);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function info(message) {
|
|
59
|
+
console.log(message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const args = {
|
|
64
|
+
command: null,
|
|
65
|
+
mode: null,
|
|
66
|
+
cwd: process.cwd(),
|
|
67
|
+
dryRun: false,
|
|
68
|
+
force: false,
|
|
69
|
+
yes: false,
|
|
70
|
+
help: false
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const tokens = argv.slice(2);
|
|
74
|
+
if (tokens.length === 0) {
|
|
75
|
+
args.help = true;
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (tokens[0] === "--help" || tokens[0] === "-h") {
|
|
80
|
+
args.help = true;
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
args.command = tokens[0];
|
|
85
|
+
|
|
86
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
87
|
+
const t = tokens[i];
|
|
88
|
+
if (t === "--help" || t === "-h") {
|
|
89
|
+
args.help = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (t === "--dry-run") {
|
|
93
|
+
args.dryRun = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (t === "--force") {
|
|
97
|
+
args.force = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (t === "--yes") {
|
|
101
|
+
args.yes = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (t === "--mode") {
|
|
105
|
+
const value = tokens[i + 1];
|
|
106
|
+
if (!value) fail("--mode requires a value (new|existing)");
|
|
107
|
+
args.mode = value;
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (t === "--cwd") {
|
|
112
|
+
const value = tokens[i + 1];
|
|
113
|
+
if (!value) fail("--cwd requires a path");
|
|
114
|
+
args.cwd = path.resolve(value);
|
|
115
|
+
i += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
fail(`Unknown option: ${t}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return args;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureDir(dirPath, options, actions) {
|
|
125
|
+
if (fs.existsSync(dirPath)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
actions.push(`mkdir -p ${dirPath}`);
|
|
129
|
+
if (!options.dryRun) {
|
|
130
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeFileIfNeeded(filePath, content, options, actions) {
|
|
135
|
+
if (fs.existsSync(filePath) && !options.force) {
|
|
136
|
+
actions.push(`skip existing ${filePath}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (fs.existsSync(filePath) && options.force) {
|
|
140
|
+
actions.push(`overwrite ${filePath}`);
|
|
141
|
+
} else {
|
|
142
|
+
actions.push(`create ${filePath}`);
|
|
143
|
+
}
|
|
144
|
+
if (!options.dryRun) {
|
|
145
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function createFileIfMissing(filePath, content, options, actions) {
|
|
150
|
+
if (fs.existsSync(filePath)) {
|
|
151
|
+
actions.push(`skip existing ${filePath}`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
actions.push(`create ${filePath}`);
|
|
155
|
+
if (!options.dryRun) {
|
|
156
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function timestampForBackup() {
|
|
161
|
+
const now = new Date();
|
|
162
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
163
|
+
return `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readTemplateAgents() {
|
|
167
|
+
const templatePath = path.resolve(__dirname, "..", "AGENTS.md");
|
|
168
|
+
if (!fs.existsSync(templatePath)) {
|
|
169
|
+
fail(`Template AGENTS.md not found at ${templatePath}`);
|
|
170
|
+
}
|
|
171
|
+
return fs.readFileSync(templatePath, "utf8");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function initProject(options) {
|
|
175
|
+
const mode = options.mode || "existing";
|
|
176
|
+
if (mode !== "new" && mode !== "existing") {
|
|
177
|
+
fail(`Invalid mode "${mode}". Use --mode new or --mode existing.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(options.cwd)) {
|
|
181
|
+
fail(`Target directory does not exist: ${options.cwd}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const actions = [];
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const paths = resolveProjectPaths(options.cwd);
|
|
187
|
+
|
|
188
|
+
const dirs = [
|
|
189
|
+
paths.adrDir,
|
|
190
|
+
paths.reqsDir,
|
|
191
|
+
paths.interruptsDir
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
for (const dirPath of dirs) {
|
|
195
|
+
ensureDir(dirPath, options, actions);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
writeFileIfNeeded(paths.agentsPath, readTemplateAgents(), options, actions);
|
|
199
|
+
writeFileIfNeeded(paths.architecturePath, ARCHITECTURE_TEMPLATE, options, actions);
|
|
200
|
+
writeFileIfNeeded(paths.inboxPath, INBOX_TEMPLATE, options, actions);
|
|
201
|
+
|
|
202
|
+
if (mode === "new") {
|
|
203
|
+
if (fs.existsSync(paths.activePath)) {
|
|
204
|
+
fail(`Mode "new" expects clean state, but found ${paths.activePath}`);
|
|
205
|
+
}
|
|
206
|
+
if (fs.existsSync(paths.interruptPath)) {
|
|
207
|
+
fail(`Mode "new" expects clean state, but found ${paths.interruptPath}`);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
if (fs.existsSync(paths.activePath)) {
|
|
211
|
+
warnings.push(`existing mode: found active lock ${paths.activePath} (kept as-is)`);
|
|
212
|
+
}
|
|
213
|
+
if (fs.existsSync(paths.interruptPath)) {
|
|
214
|
+
warnings.push(`existing mode: found interrupt lock ${paths.interruptPath} (kept as-is)`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
info(`Mode: ${mode}`);
|
|
219
|
+
info(`Target: ${options.cwd}`);
|
|
220
|
+
if (!options.mode) {
|
|
221
|
+
info(`Note: --mode not provided, defaulted to "existing".`);
|
|
222
|
+
}
|
|
223
|
+
if (options.dryRun) {
|
|
224
|
+
info(`Dry run: no files were modified.`);
|
|
225
|
+
}
|
|
226
|
+
if (options.yes) {
|
|
227
|
+
info(`Note: --yes provided (non-interactive mode already default).`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (actions.length === 0) {
|
|
231
|
+
info(`No changes needed.`);
|
|
232
|
+
} else {
|
|
233
|
+
info(`Planned actions:`);
|
|
234
|
+
for (const action of actions) {
|
|
235
|
+
info(`- ${action}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (warnings.length > 0) {
|
|
240
|
+
info(`Warnings:`);
|
|
241
|
+
for (const warning of warnings) {
|
|
242
|
+
info(`- ${warning}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
info(options.dryRun ? "Init check complete." : "Init complete.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function doctorProject(options) {
|
|
250
|
+
if (!fs.existsSync(options.cwd)) {
|
|
251
|
+
fail(`Target directory does not exist: ${options.cwd}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const paths = resolveProjectPaths(options.cwd);
|
|
255
|
+
const results = [];
|
|
256
|
+
|
|
257
|
+
const addResult = (level, message) => {
|
|
258
|
+
results.push({ level, message });
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const checkFile = (filePath, label) => {
|
|
262
|
+
if (!fs.existsSync(filePath)) {
|
|
263
|
+
addResult("FAIL", `${label} missing: ${filePath}`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (!fs.statSync(filePath).isFile()) {
|
|
267
|
+
addResult("FAIL", `${label} is not a file: ${filePath}`);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
addResult("OK", `${label} exists`);
|
|
271
|
+
return true;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const checkDir = (dirPath, label) => {
|
|
275
|
+
if (!fs.existsSync(dirPath)) {
|
|
276
|
+
addResult("FAIL", `${label} missing: ${dirPath}`);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (!fs.statSync(dirPath).isDirectory()) {
|
|
280
|
+
addResult("FAIL", `${label} is not a directory: ${dirPath}`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
addResult("OK", `${label} exists`);
|
|
284
|
+
return true;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const hasAgents = checkFile(paths.agentsPath, "AGENTS.md");
|
|
288
|
+
checkFile(paths.architecturePath, "ARCHITECTURE.md");
|
|
289
|
+
checkFile(paths.inboxPath, "reqs/INBOX.md");
|
|
290
|
+
checkDir(paths.adrDir, "docs/adr");
|
|
291
|
+
checkDir(paths.reqsDir, "reqs");
|
|
292
|
+
checkDir(paths.interruptsDir, "reqs/interrupts");
|
|
293
|
+
|
|
294
|
+
if (hasAgents) {
|
|
295
|
+
const agentsText = fs.readFileSync(paths.agentsPath, "utf8");
|
|
296
|
+
if (!agentsText.includes("## Day-0 Bootstrap (MUST)")) {
|
|
297
|
+
addResult("WARN", "AGENTS.md missing Day-0 Bootstrap section");
|
|
298
|
+
} else {
|
|
299
|
+
addResult("OK", "AGENTS.md includes Day-0 Bootstrap section");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fs.existsSync(paths.activePath)) {
|
|
304
|
+
const activeText = fs.readFileSync(paths.activePath, "utf8");
|
|
305
|
+
addResult("OK", "reqs/ACTIVE.md exists");
|
|
306
|
+
|
|
307
|
+
const statusMatch = activeText.match(/^status:\s*([A-Z]+)\s*$/m);
|
|
308
|
+
if (!statusMatch || (statusMatch[1] !== "DRAFT" && statusMatch[1] !== "ACTIVE")) {
|
|
309
|
+
addResult("FAIL", "reqs/ACTIVE.md status must be DRAFT or ACTIVE");
|
|
310
|
+
} else {
|
|
311
|
+
addResult("OK", `reqs/ACTIVE.md status is ${statusMatch[1]}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const reqPathMatch = activeText.match(/^path:\s*(.+)\s*$/m);
|
|
315
|
+
if (!reqPathMatch) {
|
|
316
|
+
addResult("FAIL", "reqs/ACTIVE.md missing path field");
|
|
317
|
+
} else {
|
|
318
|
+
const reqPath = path.resolve(options.cwd, reqPathMatch[1].trim());
|
|
319
|
+
if (!fs.existsSync(reqPath)) {
|
|
320
|
+
addResult("FAIL", `active requirement path not found: ${reqPath}`);
|
|
321
|
+
} else {
|
|
322
|
+
addResult("OK", "active requirement path exists");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
addResult("OK", "reqs/ACTIVE.md not present (unlocked)");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (fs.existsSync(paths.interruptPath)) {
|
|
330
|
+
const interruptText = fs.readFileSync(paths.interruptPath, "utf8");
|
|
331
|
+
addResult("OK", "reqs/INTERRUPT.md exists");
|
|
332
|
+
|
|
333
|
+
if (!fs.existsSync(paths.activePath)) {
|
|
334
|
+
addResult("FAIL", "reqs/INTERRUPT.md exists without reqs/ACTIVE.md");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const requiredGateKeys = ["impact_now", "timebox_fit", "scope_safe"];
|
|
338
|
+
for (const key of requiredGateKeys) {
|
|
339
|
+
const gateMatch = interruptText.match(new RegExp(`^-\\s*${key}:\\s*(yes|no)\\s*$`, "m"));
|
|
340
|
+
if (!gateMatch) {
|
|
341
|
+
addResult("FAIL", `reqs/INTERRUPT.md missing gate answer: ${key}`);
|
|
342
|
+
} else {
|
|
343
|
+
addResult("OK", `interrupt gate present: ${key}=${gateMatch[1]}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
addResult("OK", "reqs/INTERRUPT.md not present");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
info(`Target: ${options.cwd}`);
|
|
351
|
+
info("Doctor report:");
|
|
352
|
+
for (const result of results) {
|
|
353
|
+
info(`- ${result.level}: ${result.message}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const failCount = results.filter((r) => r.level === "FAIL").length;
|
|
357
|
+
const warnCount = results.filter((r) => r.level === "WARN").length;
|
|
358
|
+
const okCount = results.filter((r) => r.level === "OK").length;
|
|
359
|
+
info(`Summary: OK=${okCount} WARN=${warnCount} FAIL=${failCount}`);
|
|
360
|
+
|
|
361
|
+
return failCount === 0 ? 0 : 1;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function upgradeProject(options) {
|
|
365
|
+
if (!fs.existsSync(options.cwd)) {
|
|
366
|
+
fail(`Target directory does not exist: ${options.cwd}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const paths = resolveProjectPaths(options.cwd);
|
|
370
|
+
const actions = [];
|
|
371
|
+
const warnings = [];
|
|
372
|
+
|
|
373
|
+
ensureDir(paths.adrDir, options, actions);
|
|
374
|
+
ensureDir(paths.reqsDir, options, actions);
|
|
375
|
+
ensureDir(paths.interruptsDir, options, actions);
|
|
376
|
+
|
|
377
|
+
createFileIfMissing(paths.architecturePath, ARCHITECTURE_TEMPLATE, options, actions);
|
|
378
|
+
createFileIfMissing(paths.inboxPath, INBOX_TEMPLATE, options, actions);
|
|
379
|
+
|
|
380
|
+
const templateAgents = readTemplateAgents();
|
|
381
|
+
if (!fs.existsSync(paths.agentsPath)) {
|
|
382
|
+
actions.push(`create ${paths.agentsPath}`);
|
|
383
|
+
if (!options.dryRun) {
|
|
384
|
+
fs.writeFileSync(paths.agentsPath, templateAgents, "utf8");
|
|
385
|
+
}
|
|
386
|
+
warnings.push("AGENTS.md did not exist; created from template.");
|
|
387
|
+
} else {
|
|
388
|
+
const currentAgents = fs.readFileSync(paths.agentsPath, "utf8");
|
|
389
|
+
if (currentAgents === templateAgents) {
|
|
390
|
+
actions.push(`skip up-to-date ${paths.agentsPath}`);
|
|
391
|
+
} else {
|
|
392
|
+
const backupPath = `${paths.agentsPath}.bak.${timestampForBackup()}`;
|
|
393
|
+
actions.push(`backup ${paths.agentsPath} -> ${backupPath}`);
|
|
394
|
+
actions.push(`overwrite ${paths.agentsPath}`);
|
|
395
|
+
if (!options.dryRun) {
|
|
396
|
+
fs.copyFileSync(paths.agentsPath, backupPath);
|
|
397
|
+
fs.writeFileSync(paths.agentsPath, templateAgents, "utf8");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (fs.existsSync(paths.activePath)) {
|
|
403
|
+
warnings.push(`active lock detected and kept as-is: ${paths.activePath}`);
|
|
404
|
+
}
|
|
405
|
+
if (fs.existsSync(paths.interruptPath)) {
|
|
406
|
+
warnings.push(`interrupt lock detected and kept as-is: ${paths.interruptPath}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
info(`Target: ${options.cwd}`);
|
|
410
|
+
if (options.dryRun) {
|
|
411
|
+
info("Dry run: no files were modified.");
|
|
412
|
+
}
|
|
413
|
+
if (options.yes) {
|
|
414
|
+
info("Note: --yes provided (non-interactive mode already default).");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (actions.length === 0) {
|
|
418
|
+
info("No changes needed.");
|
|
419
|
+
} else {
|
|
420
|
+
info("Planned actions:");
|
|
421
|
+
for (const action of actions) {
|
|
422
|
+
info(`- ${action}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (warnings.length > 0) {
|
|
427
|
+
info("Warnings:");
|
|
428
|
+
for (const warning of warnings) {
|
|
429
|
+
info(`- ${warning}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
info(options.dryRun ? "Upgrade check complete." : "Upgrade complete.");
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function main() {
|
|
438
|
+
const args = parseArgs(process.argv);
|
|
439
|
+
|
|
440
|
+
if (args.help || !args.command) {
|
|
441
|
+
console.log(HELP.trim());
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (args.command === "init") {
|
|
446
|
+
initProject(args);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (args.command === "doctor") {
|
|
451
|
+
process.exit(doctorProject(args));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (args.command === "upgrade") {
|
|
455
|
+
process.exit(upgradeProject(args));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fail(`Unknown command "${args.command}". Supported commands: init, doctor, upgrade.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "this-is-enough",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install and bootstrap the ThisIsEnough AGENTS.md workflow.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"this-is-enough": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"AGENTS.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
}
|
|
16
|
+
}
|