@wefter/opencode 0.1.0 → 0.2.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/CHANGELOG.md +25 -14
- package/LICENSE +21 -21
- package/README.md +125 -112
- package/bin/wefter.js +8 -8
- package/docs/ARCHITECTURE.md +75 -79
- package/docs/INSTALLATION.md +47 -46
- package/docs/SAFETY_MODEL.md +17 -17
- package/docs/SELF_AUDIT.md +35 -0
- package/docs/WORKFLOWS.md +15 -13
- package/package.json +45 -45
- package/schemas/documentation-audit-profile.schema.json +55 -47
- package/schemas/product-shaping-config.schema.json +63 -0
- package/schemas/product-shaping-contract.schema.json +204 -0
- package/schemas/product-shaping-profile.schema.json +39 -0
- package/schemas/product-shaping-run-manifest.schema.json +103 -0
- package/schemas/run-manifest.schema.json +14 -14
- package/schemas/wefter.config.schema.json +62 -31
- package/schemas/work-unit-config.schema.json +44 -44
- package/schemas/work-unit-profile.schema.json +38 -38
- package/schemas/work-unit-review-result.schema.json +13 -13
- package/schemas/workflow-manifest.schema.json +21 -21
- package/src/cli/main.js +2646 -1858
- package/src/workflows/documentation-audit/README.md +37 -37
- package/src/workflows/documentation-audit/templates/README.md.tmpl +47 -47
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-consolidator.md.tmpl +27 -27
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-orchestrator.md.tmpl +65 -65
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-profile-builder.md.tmpl +58 -58
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-validator.md.tmpl +26 -26
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-auditor.md.tmpl +28 -28
- package/src/workflows/documentation-audit/templates/opencode/skills/documentation-audit/SKILL.md.tmpl +38 -38
- package/src/workflows/documentation-audit/templates/prompts/auditor-prompt.md +97 -97
- package/src/workflows/documentation-audit/templates/prompts/consolidator-prompt.md +84 -84
- package/src/workflows/documentation-audit/templates/prompts/validator-prompt.md +92 -92
- package/src/workflows/documentation-audit/workflow.json +24 -24
- package/src/workflows/documentation-repair/README.md +11 -11
- package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-orchestrator.md.tmpl +33 -33
- package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-planner.md.tmpl +17 -17
- package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-reviewer.md.tmpl +17 -17
- package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repairer.md.tmpl +14 -14
- package/src/workflows/documentation-repair/templates/opencode/skills/documentation-repair/SKILL.md.tmpl +17 -17
- package/src/workflows/documentation-repair/templates/prompts/repair-apply-prompt.md +43 -43
- package/src/workflows/documentation-repair/templates/prompts/repair-plan-prompt.md +73 -73
- package/src/workflows/documentation-repair/templates/prompts/repair-review-prompt.md +47 -47
- package/src/workflows/documentation-repair/workflow.json +10 -10
- package/src/workflows/product-shaping/README.md +1245 -7
- package/src/workflows/product-shaping/compatibility.md +33 -0
- package/src/workflows/product-shaping/contracts/product-spec-contract.json +250 -0
- package/src/workflows/product-shaping/templates/default-config.json +34 -0
- package/src/workflows/product-shaping/templates/default-profile.json +48 -0
- package/src/workflows/product-shaping/templates/documentation-audit/workflow-self-audit-auditor-prompt.md +116 -0
- package/src/workflows/product-shaping/templates/documentation-audit-profile.json +80 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-auditor.md.tmpl +22 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-domain-modeler.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-intake-analyst.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-orchestrator.md.tmpl +48 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-reference-researcher.md.tmpl +29 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-release-planner.md.tmpl +34 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-repairer.md.tmpl +25 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-shaper.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-validator.md.tmpl +23 -0
- package/src/workflows/product-shaping/templates/opencode/skills/product-shaping/SKILL.md.tmpl +45 -0
- package/src/workflows/product-shaping/templates/prompts/domain-modeler-prompt.md +27 -0
- package/src/workflows/product-shaping/templates/prompts/intake-prompt.md +30 -0
- package/src/workflows/product-shaping/templates/prompts/product-auditor-prompt.md +53 -0
- package/src/workflows/product-shaping/templates/prompts/product-repairer-prompt.md +25 -0
- package/src/workflows/product-shaping/templates/prompts/product-shaper-prompt.md +26 -0
- package/src/workflows/product-shaping/templates/prompts/product-validator-prompt.md +55 -0
- package/src/workflows/product-shaping/templates/prompts/reference-research-prompt.md +25 -0
- package/src/workflows/product-shaping/templates/prompts/release-planner-prompt.md +34 -0
- package/src/workflows/product-shaping/workflow.json +33 -10
- package/src/workflows/technical-shaping/README.md +5 -5
- package/src/workflows/technical-shaping/workflow.json +10 -10
- package/src/workflows/work-unit-implementation/README.md +71 -71
- package/src/workflows/work-unit-implementation/templates/default-config.json +46 -46
- package/src/workflows/work-unit-implementation/templates/default-profile.json +57 -57
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-orchestrator.md.tmpl +62 -62
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-auditor.md.tmpl +26 -26
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-consolidator.md.tmpl +26 -26
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-repairer.md.tmpl +25 -25
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-validator.md.tmpl +25 -25
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-planner.md.tmpl +27 -27
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-task-implementer.md.tmpl +30 -30
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-task-reviewer.md.tmpl +28 -28
- package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-validator.md.tmpl +26 -26
- package/src/workflows/work-unit-implementation/templates/opencode/skills/work-unit-implementation/SKILL.md.tmpl +25 -25
- package/src/workflows/work-unit-implementation/templates/prompts/plan-auditor-prompt.md +89 -89
- package/src/workflows/work-unit-implementation/templates/prompts/plan-consolidator-prompt.md +64 -64
- package/src/workflows/work-unit-implementation/templates/prompts/plan-repairer-prompt.md +42 -42
- package/src/workflows/work-unit-implementation/templates/prompts/plan-validator-prompt.md +84 -84
- package/src/workflows/work-unit-implementation/templates/prompts/planner-prompt.md +150 -150
- package/src/workflows/work-unit-implementation/templates/prompts/task-implementation-prompt.md +57 -57
- package/src/workflows/work-unit-implementation/templates/prompts/task-review-prompt.md +69 -69
- package/src/workflows/work-unit-implementation/templates/prompts/work-unit-validator-prompt.md +50 -50
- package/src/workflows/work-unit-implementation/workflow.json +14 -14
package/src/cli/main.js
CHANGED
|
@@ -1,1858 +1,2646 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import readline from "node:readline/promises";
|
|
5
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const VERSION = "0.
|
|
9
|
-
const CONFIG_FILE = "wefter.config.json";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
throw new Error(`${label} must
|
|
203
|
-
}
|
|
204
|
-
value.
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (value !== value.trim()) {
|
|
230
|
-
throw new Error(
|
|
231
|
-
}
|
|
232
|
-
if (
|
|
233
|
-
throw new Error(
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
return
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
}
|
|
1062
|
-
prompts: {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
const
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
const
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
if (!
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
"
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
});
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const VERSION = "0.2.0";
|
|
9
|
+
const CONFIG_FILE = "wefter.config.json";
|
|
10
|
+
const PRODUCT_SHAPING_WORKFLOW_ID = "product-shaping";
|
|
11
|
+
const DOCUMENTATION_REPAIR_WORKFLOW_ID = "documentation-repair";
|
|
12
|
+
const WORK_UNIT_WORKFLOW_ID = "work-unit-implementation";
|
|
13
|
+
const DEFAULTS = Object.freeze({
|
|
14
|
+
workflowRoot: ".wefter/workflows",
|
|
15
|
+
profilePath: ".wefter/workflows/documentation-audit/profile.json",
|
|
16
|
+
artifactRoot: ".audit/wefter/documentation-audit",
|
|
17
|
+
templateRoot: ".wefter/workflows/documentation-audit/templates",
|
|
18
|
+
processDocPath: ".wefter/workflows/documentation-audit/README.md"
|
|
19
|
+
});
|
|
20
|
+
const PRODUCT_SHAPING_DEFAULTS = Object.freeze({
|
|
21
|
+
specRoot: ".wefter/specs",
|
|
22
|
+
runRoot: ".wefter/runs/product-shaping",
|
|
23
|
+
configPath: ".wefter/workflows/product-shaping/config.json",
|
|
24
|
+
profilePath: ".wefter/workflows/product-shaping/profile.json"
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
28
|
+
|
|
29
|
+
const REQUIRED_TEMPLATE_FILES = Object.freeze([
|
|
30
|
+
"auditor-prompt.md",
|
|
31
|
+
"consolidator-prompt.md",
|
|
32
|
+
"validator-prompt.md"
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const PRODUCT_SHAPING_PROMPT_FILES = Object.freeze([
|
|
36
|
+
"intake-prompt.md",
|
|
37
|
+
"reference-research-prompt.md",
|
|
38
|
+
"product-shaper-prompt.md",
|
|
39
|
+
"domain-modeler-prompt.md",
|
|
40
|
+
"release-planner-prompt.md",
|
|
41
|
+
"product-auditor-prompt.md",
|
|
42
|
+
"product-validator-prompt.md",
|
|
43
|
+
"product-repairer-prompt.md"
|
|
44
|
+
]);
|
|
45
|
+
const PRODUCT_SHAPING_REQUIRED_FILES = Object.freeze([
|
|
46
|
+
"README.md",
|
|
47
|
+
"discovery/SOURCE_MATERIALS.md",
|
|
48
|
+
"discovery/IDEA_BRIEF.md",
|
|
49
|
+
"discovery/OPEN_QUESTIONS.md",
|
|
50
|
+
"references/README.md",
|
|
51
|
+
"PRODUCT_VISION.md",
|
|
52
|
+
"product/FEATURE_CATALOG.md",
|
|
53
|
+
"product/MODULE_ROADMAP.md",
|
|
54
|
+
"product/DOMAIN_MODEL.md",
|
|
55
|
+
"product/OPERATIONAL_FLOW.md",
|
|
56
|
+
"product/PRODUCT_DECISIONS.md",
|
|
57
|
+
"releases/README.md",
|
|
58
|
+
"releases/<release-id>/README.md",
|
|
59
|
+
"releases/<release-id>/SCOPE.md",
|
|
60
|
+
"releases/<release-id>/DOMAIN_SPEC.md",
|
|
61
|
+
"releases/<release-id>/ACCEPTANCE_CRITERIA.md",
|
|
62
|
+
"releases/<release-id>/DELIVERABLES.md"
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function printHelp() {
|
|
66
|
+
console.log(`wefter ${VERSION}
|
|
67
|
+
|
|
68
|
+
Usage:
|
|
69
|
+
wefter init [--yes] [--force] [--target <path>] [--profile-path <path>] [--artifact-root <path>] [--template-root <path>] [--process-doc-path <path>] [--runner-command <command>]
|
|
70
|
+
wefter product shape [--target <path>] [--release-id <id>] [--run-name <name>] [--spec-root <path>] [--run-root <path>] [--config-path <path>] [--profile-path <path>] [--dry-run]
|
|
71
|
+
wefter product validate [--target <path>] [--release-id <id>] [--run-id <id> | --run-root <path>] [--config-path <path>] [--json]
|
|
72
|
+
wefter docs audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
|
|
73
|
+
wefter docs repair [--target <path>] --audit-report <path> [--run-name <name>] [--dry-run]
|
|
74
|
+
wefter work-unit run [--target <path>] [--work-unit-id <id>] [--work-units-document <path>] [--release-id <id>] [--product-run-id <id> | --product-run-root <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--config-path <path>] [--profile-path <path>] [--dry-run]
|
|
75
|
+
wefter work-unit guard [--target <path>] [--run-id <id> | --run-root <path>] [--task-id <id>] [--mode Status|ReadyForReview|ReadyForNextTask|ReadyForFinalValidation] [--config-path <path>] [--json]
|
|
76
|
+
wefter new-run documentation-audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
|
|
77
|
+
wefter profile scaffold [--target <path>] [--force]
|
|
78
|
+
wefter profile import [--target <path>] --source <path> [--force]
|
|
79
|
+
wefter doctor [--target <path>]
|
|
80
|
+
|
|
81
|
+
Commands:
|
|
82
|
+
init Install opencode agents, skill, commands, templates and local config.
|
|
83
|
+
product shape Generate one product-shaping run skeleton.
|
|
84
|
+
product validate Validate product-shaping specs against the completion gate.
|
|
85
|
+
docs audit Generate one documentation audit run from the configured profile.
|
|
86
|
+
docs repair Generate one documentation repair run from a final audit report.
|
|
87
|
+
work-unit run Generate one work-unit implementation run.
|
|
88
|
+
work-unit guard Validate task/review loop state for a work-unit run.
|
|
89
|
+
new-run Generate one workflow run. Currently supports documentation-audit.
|
|
90
|
+
profile scaffold Create a heuristic starter audit profile for the current repository.
|
|
91
|
+
profile import Import a repository-relative documentation audit profile into the configured profile path.
|
|
92
|
+
doctor Validate local installation and configuration.
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const positional = [];
|
|
98
|
+
const flags = {};
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < argv.length; i++) {
|
|
101
|
+
const arg = argv[i];
|
|
102
|
+
if (!arg.startsWith("--")) {
|
|
103
|
+
positional.push(arg);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = arg.slice(2);
|
|
108
|
+
if (["yes", "force", "dry-run", "help", "version", "json"].includes(key)) {
|
|
109
|
+
flags[key] = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const next = argv[i + 1];
|
|
114
|
+
if (!next || next.startsWith("--")) {
|
|
115
|
+
throw new Error(`Missing value for --${key}`);
|
|
116
|
+
}
|
|
117
|
+
flags[key] = next;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { positional, flags };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function packageRoot() {
|
|
125
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function workflowPackageRoot(workflowId) {
|
|
129
|
+
return path.join(packageRoot(), "src/workflows", workflowId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function documentationAuditTemplateRoot() {
|
|
133
|
+
return path.join(workflowPackageRoot("documentation-audit"), "templates");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function documentationRepairTemplateRoot() {
|
|
137
|
+
return path.join(workflowPackageRoot(DOCUMENTATION_REPAIR_WORKFLOW_ID), "templates");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function productShapingWorkflowPackageRoot() {
|
|
141
|
+
return workflowPackageRoot(PRODUCT_SHAPING_WORKFLOW_ID);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function workUnitWorkflowPackageRoot() {
|
|
145
|
+
return workflowPackageRoot(WORK_UNIT_WORKFLOW_ID);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function documentationRepairArtifactRoot() {
|
|
149
|
+
return ".audit/wefter/documentation-repair";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveTarget(flags) {
|
|
153
|
+
return path.resolve(flags.target || process.cwd());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toPosix(value) {
|
|
157
|
+
return value.split(path.sep).join("/");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function quoteCommandArg(value) {
|
|
161
|
+
const normalized = toPosix(path.resolve(value));
|
|
162
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(normalized)) {
|
|
163
|
+
return normalized;
|
|
164
|
+
}
|
|
165
|
+
return `"${normalized.replaceAll('"', '\\"')}"`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function defaultRunnerCommand() {
|
|
169
|
+
return `node ${quoteCommandArg(path.join(packageRoot(), "bin/wefter.js"))}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function yamlSingleQuoted(value) {
|
|
173
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeRelativePath(value, label) {
|
|
177
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
178
|
+
throw new Error(`${label} must be a non-empty relative path.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const trimmed = value.trim().replaceAll("\\", "/");
|
|
182
|
+
if (trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
183
|
+
throw new Error(`${label} must not contain line breaks.`);
|
|
184
|
+
}
|
|
185
|
+
if (path.isAbsolute(trimmed)) {
|
|
186
|
+
throw new Error(`${label} must be relative to the target repository.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const parts = trimmed.split("/").filter((part) => part && part !== ".");
|
|
190
|
+
if (parts.length === 0 || parts.includes("..")) {
|
|
191
|
+
throw new Error(`${label} must not be empty or contain '..'.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return parts.join("/");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeRunnerCommand(value, label) {
|
|
198
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
199
|
+
throw new Error(`${label} must be a non-empty command string.`);
|
|
200
|
+
}
|
|
201
|
+
if (value !== value.trim()) {
|
|
202
|
+
throw new Error(`${label} must not contain leading or trailing whitespace.`);
|
|
203
|
+
}
|
|
204
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
205
|
+
throw new Error(`${label} must not contain line breaks.`);
|
|
206
|
+
}
|
|
207
|
+
if (value.includes("{{")) {
|
|
208
|
+
throw new Error(`${label} must not contain unresolved template placeholders.`);
|
|
209
|
+
}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function assertObject(value, label) {
|
|
214
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
215
|
+
throw new Error(`${label} must be an object.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function assertAllowedKeys(value, label, allowedKeys) {
|
|
220
|
+
const allowed = new Set(allowedKeys);
|
|
221
|
+
for (const key of Object.keys(value)) {
|
|
222
|
+
if (!allowed.has(key)) {
|
|
223
|
+
throw new Error(`${label} has unsupported property '${key}'.`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function requireString(value, label) {
|
|
229
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
230
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
231
|
+
}
|
|
232
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
233
|
+
throw new Error(`${label} must not contain line breaks.`);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function requireId(value, label) {
|
|
239
|
+
requireString(value, label);
|
|
240
|
+
if (!ID_PATTERN.test(value)) {
|
|
241
|
+
throw new Error(`${label} must match ${ID_PATTERN}.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function requireStringArray(value, label) {
|
|
246
|
+
if (!Array.isArray(value)) {
|
|
247
|
+
throw new Error(`${label} must be an array.`);
|
|
248
|
+
}
|
|
249
|
+
value.forEach((item, index) => requireString(item, `${label}[${index}]`));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertUniqueIds(items, label) {
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
for (const item of items) {
|
|
255
|
+
if (seen.has(item.id)) {
|
|
256
|
+
throw new Error(`${label} contains duplicate id '${item.id}'.`);
|
|
257
|
+
}
|
|
258
|
+
seen.add(item.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function windowsPermissionPath(relativePath) {
|
|
263
|
+
return relativePath.replaceAll("/", "\\\\");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function windowsPermissionGlob(relativePath) {
|
|
267
|
+
return `${windowsPermissionPath(relativePath)}\\\\**`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function assertSafeRunName(value) {
|
|
271
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
272
|
+
throw new Error("Run name must not be empty.");
|
|
273
|
+
}
|
|
274
|
+
if (value !== value.trim()) {
|
|
275
|
+
throw new Error("Run name must not contain leading or trailing whitespace.");
|
|
276
|
+
}
|
|
277
|
+
if (path.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
|
|
278
|
+
throw new Error("Run name must be a plain directory name, not a path.");
|
|
279
|
+
}
|
|
280
|
+
if (value.includes("..")) {
|
|
281
|
+
throw new Error("Run name must not contain '..'.");
|
|
282
|
+
}
|
|
283
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value)) {
|
|
284
|
+
throw new Error("Run name may contain only letters, numbers, dot, underscore and hyphen, and must start with a letter or number.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function assertPlainRunId(value) {
|
|
289
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
290
|
+
throw new Error("Run id must not be empty when --run-root is not provided.");
|
|
291
|
+
}
|
|
292
|
+
assertSafeRunName(value);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getSafeWorkUnitKey(value) {
|
|
296
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
297
|
+
throw new Error("Work unit id must not be empty.");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const trimmed = value.trim();
|
|
301
|
+
if (/^work-unit-[A-Za-z0-9_.-]+$/.test(trimmed)) {
|
|
302
|
+
return trimmed.toLowerCase();
|
|
303
|
+
}
|
|
304
|
+
if (/^\d+$/.test(trimmed)) {
|
|
305
|
+
return `work-unit-${String(Number.parseInt(trimmed, 10)).padStart(2, "0")}`;
|
|
306
|
+
}
|
|
307
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
|
|
308
|
+
throw new Error("Work unit id may contain only letters, numbers, dot, underscore and hyphen, and must start with a letter or number.");
|
|
309
|
+
}
|
|
310
|
+
return `work-unit-${trimmed.toLowerCase()}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ensureInside(targetRoot, candidate, label) {
|
|
314
|
+
const relative = path.relative(targetRoot, candidate);
|
|
315
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`${label} resolves outside the target repository.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolveInsideTarget(targetRoot, candidatePath, label) {
|
|
322
|
+
const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(targetRoot, candidatePath);
|
|
323
|
+
ensureInside(targetRoot, resolved, label);
|
|
324
|
+
return resolved;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function toDisplayPath(targetRoot, fullPath) {
|
|
328
|
+
const relative = path.relative(targetRoot, fullPath);
|
|
329
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
330
|
+
return toPosix(relative || ".");
|
|
331
|
+
}
|
|
332
|
+
return toPosix(fullPath);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readJson(filePath, label) {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
throw new Error(`Failed to read ${label} at ${filePath}: ${error.message}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readJsonIfExists(filePath, label) {
|
|
344
|
+
if (!fs.existsSync(filePath)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return readJson(filePath, label);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function writeJson(filePath, value) {
|
|
351
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
352
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function writeTextIfSafe(filePath, content, force) {
|
|
356
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
357
|
+
if (fs.existsSync(filePath)) {
|
|
358
|
+
const current = fs.readFileSync(filePath, "utf8");
|
|
359
|
+
if (current !== content && !force) {
|
|
360
|
+
throw new Error(`Refusing to overwrite existing file: ${filePath}. Use --force to replace it.`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function writeJsonIfSafe(filePath, value, force) {
|
|
367
|
+
writeTextIfSafe(filePath, `${JSON.stringify(value, null, 2)}\n`, force);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readConfig(targetRoot) {
|
|
371
|
+
const configPath = path.join(targetRoot, CONFIG_FILE);
|
|
372
|
+
if (!fs.existsSync(configPath)) {
|
|
373
|
+
throw new Error(`Missing ${CONFIG_FILE}. Run wefter init first.`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const config = readJson(configPath, CONFIG_FILE);
|
|
377
|
+
return normalizeConfig(config);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function normalizeConfig(config) {
|
|
381
|
+
assertObject(config, CONFIG_FILE);
|
|
382
|
+
assertAllowedKeys(config, CONFIG_FILE, ["$schema", "version", "workflowRoot", "profilePath", "artifactRoot", "templateRoot", "processDocPath", "runnerCommand", "workflows"]);
|
|
383
|
+
|
|
384
|
+
if (config.version !== 1) {
|
|
385
|
+
throw new Error(`${CONFIG_FILE} must have version: 1.`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const workflowRoot = normalizeRelativePath(config.workflowRoot || DEFAULTS.workflowRoot, "workflowRoot");
|
|
389
|
+
const workflows = config.workflows || defaultWorkflowRegistry();
|
|
390
|
+
normalizeWorkflowRegistry(workflows);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
version: 1,
|
|
394
|
+
workflowRoot,
|
|
395
|
+
profilePath: normalizeRelativePath(config.profilePath, "profilePath"),
|
|
396
|
+
artifactRoot: normalizeRelativePath(config.artifactRoot, "artifactRoot"),
|
|
397
|
+
templateRoot: normalizeRelativePath(config.templateRoot, "templateRoot"),
|
|
398
|
+
processDocPath: normalizeRelativePath(config.processDocPath, "processDocPath"),
|
|
399
|
+
runnerCommand: normalizeRunnerCommand(config.runnerCommand, "runnerCommand"),
|
|
400
|
+
workflows
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function defaultWorkflowRegistry() {
|
|
405
|
+
return {
|
|
406
|
+
[PRODUCT_SHAPING_WORKFLOW_ID]: {
|
|
407
|
+
status: "available",
|
|
408
|
+
enabled: true,
|
|
409
|
+
specRoot: PRODUCT_SHAPING_DEFAULTS.specRoot,
|
|
410
|
+
runRoot: PRODUCT_SHAPING_DEFAULTS.runRoot,
|
|
411
|
+
configPath: PRODUCT_SHAPING_DEFAULTS.configPath,
|
|
412
|
+
profilePath: PRODUCT_SHAPING_DEFAULTS.profilePath
|
|
413
|
+
},
|
|
414
|
+
"documentation-audit": { status: "available", enabled: true },
|
|
415
|
+
"documentation-repair": { status: "available", enabled: true },
|
|
416
|
+
"technical-shaping": { status: "planned", enabled: false },
|
|
417
|
+
"work-unit-implementation": {
|
|
418
|
+
status: "available",
|
|
419
|
+
enabled: true,
|
|
420
|
+
configPath: ".wefter/workflows/work-unit-implementation/config.json",
|
|
421
|
+
profilePath: ".wefter/workflows/work-unit-implementation/profile.json"
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function workflowSettings(config, workflowId) {
|
|
427
|
+
const settings = config.workflows?.[workflowId];
|
|
428
|
+
if (!settings) {
|
|
429
|
+
throw new Error(`Missing workflow settings for ${workflowId}.`);
|
|
430
|
+
}
|
|
431
|
+
return settings;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function assertWorkflowEnabled(config, workflowId) {
|
|
435
|
+
const settings = workflowSettings(config, workflowId);
|
|
436
|
+
if (!settings.enabled) {
|
|
437
|
+
throw new Error(`Workflow '${workflowId}' is disabled in ${CONFIG_FILE}.`);
|
|
438
|
+
}
|
|
439
|
+
return settings;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function workUnitConfigPath(config, flags = {}) {
|
|
443
|
+
const settings = workflowSettings(config, WORK_UNIT_WORKFLOW_ID);
|
|
444
|
+
return normalizeRelativePath(flags["config-path"] || settings.configPath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/config.json`, "work-unit config path");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function workUnitProfilePath(config, flags = {}) {
|
|
448
|
+
const settings = workflowSettings(config, WORK_UNIT_WORKFLOW_ID);
|
|
449
|
+
return normalizeRelativePath(flags["profile-path"] || settings.profilePath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/profile.json`, "work-unit profile path");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function productShapingSpecRoot(config, flags = {}) {
|
|
453
|
+
const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
454
|
+
return normalizeRelativePath(flags["spec-root"] || settings.specRoot || PRODUCT_SHAPING_DEFAULTS.specRoot, "product-shaping spec root");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function productShapingRunRoot(config, flags = {}) {
|
|
458
|
+
const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
459
|
+
return normalizeRelativePath(flags["run-root"] || settings.runRoot || PRODUCT_SHAPING_DEFAULTS.runRoot, "product-shaping run root");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function productShapingConfigPath(config, flags = {}) {
|
|
463
|
+
const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
464
|
+
return normalizeRelativePath(flags["config-path"] || settings.configPath || `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/config.json`, "product-shaping config path");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function productShapingProfilePath(config, flags = {}) {
|
|
468
|
+
const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
469
|
+
return normalizeRelativePath(flags["profile-path"] || settings.profilePath || `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/profile.json`, "product-shaping profile path");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function normalizeWorkflowRegistry(workflows) {
|
|
473
|
+
assertObject(workflows, "workflows");
|
|
474
|
+
for (const [workflowId, workflow] of Object.entries(workflows)) {
|
|
475
|
+
requireId(workflowId, `workflows.${workflowId}`);
|
|
476
|
+
assertObject(workflow, `workflows.${workflowId}`);
|
|
477
|
+
assertAllowedKeys(workflow, `workflows.${workflowId}`, ["status", "enabled", "profilePath", "configPath", "specRoot", "runRoot"]);
|
|
478
|
+
if (!["available", "planned"].includes(workflow.status)) {
|
|
479
|
+
throw new Error(`workflows.${workflowId}.status must be available or planned.`);
|
|
480
|
+
}
|
|
481
|
+
if (typeof workflow.enabled !== "boolean") {
|
|
482
|
+
throw new Error(`workflows.${workflowId}.enabled must be boolean.`);
|
|
483
|
+
}
|
|
484
|
+
for (const key of ["profilePath", "configPath", "specRoot", "runRoot"]) {
|
|
485
|
+
if (workflow[key] !== undefined) {
|
|
486
|
+
normalizeRelativePath(workflow[key], `workflows.${workflowId}.${key}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function promptForValue(rl, label, defaultValue) {
|
|
493
|
+
const answer = await rl.question(`${label} (${defaultValue}): `);
|
|
494
|
+
return answer.trim() === "" ? defaultValue : answer.trim();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function renderTemplate(content, values) {
|
|
498
|
+
let result = content;
|
|
499
|
+
for (const [key, value] of Object.entries(values)) {
|
|
500
|
+
result = result.replaceAll(`{{${key}}}`, String(value));
|
|
501
|
+
}
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function copyRenderedTemplate(source, destination, values, force) {
|
|
506
|
+
const content = fs.readFileSync(source, "utf8");
|
|
507
|
+
const rendered = renderTemplate(content, values);
|
|
508
|
+
writeTextIfSafe(destination, rendered, force);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function copyDirectory(sourceRoot, destinationRoot, force) {
|
|
512
|
+
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
513
|
+
const source = path.join(sourceRoot, entry.name);
|
|
514
|
+
const destination = path.join(destinationRoot, entry.name);
|
|
515
|
+
if (entry.isDirectory()) {
|
|
516
|
+
copyDirectory(source, destination, force);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const content = fs.readFileSync(source, "utf8");
|
|
520
|
+
writeTextIfSafe(destination, content, force);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function mergeOpencodeConfig(targetRoot, config, force) {
|
|
525
|
+
const opencodePath = path.join(targetRoot, "opencode.json");
|
|
526
|
+
const existing = fs.existsSync(opencodePath) ? readJson(opencodePath, "opencode.json") : { "$schema": "https://opencode.ai/config.json" };
|
|
527
|
+
const workUnitConfig = readJsonIfExists(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
|
|
528
|
+
const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
529
|
+
|
|
530
|
+
existing["$schema"] = existing["$schema"] || "https://opencode.ai/config.json";
|
|
531
|
+
existing.watcher = existing.watcher || {};
|
|
532
|
+
existing.watcher.ignore = Array.isArray(existing.watcher.ignore) ? existing.watcher.ignore : [];
|
|
533
|
+
for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
|
|
534
|
+
if (!ignored) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const pattern = `${ignored.replace(/\/$/, "")}/**`;
|
|
538
|
+
if (!existing.watcher.ignore.includes(pattern)) {
|
|
539
|
+
existing.watcher.ignore.push(pattern);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
existing.skills = existing.skills || {};
|
|
544
|
+
existing.skills.paths = Array.isArray(existing.skills.paths) ? existing.skills.paths : [];
|
|
545
|
+
if (!existing.skills.paths.includes(".opencode/skills")) {
|
|
546
|
+
existing.skills.paths.push(".opencode/skills");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
existing.command = existing.command || {};
|
|
550
|
+
const fullRunCommand = {
|
|
551
|
+
description: "Run the Wefter documentation audit workflow end-to-end.",
|
|
552
|
+
agent: "wefter-doc-audit-orchestrator",
|
|
553
|
+
template: `Run the complete Wefter documentation audit workflow end-to-end. Read ${CONFIG_FILE} first. If the user provided an existing run path, resume it. Otherwise create a new run with ${config.runnerCommand} docs audit. Unless the user provided different sizing, use --passes-per-lens 3. Execute all auditor prompts in parallel batches, consolidate, validate adversarially, and report the final output path. Do not edit source documentation.`
|
|
554
|
+
};
|
|
555
|
+
const generateProfileCommand = {
|
|
556
|
+
description: "Inspect the repository and create or update the Wefter documentation audit profile.",
|
|
557
|
+
agent: "wefter-doc-audit-profile-builder",
|
|
558
|
+
template: `Inspect this repository and create or update the documentation audit profile defined by ${CONFIG_FILE}. If the profile already exists, write a proposal under the configured artifact root instead of overwriting it unless the user explicitly asked to replace it.`
|
|
559
|
+
};
|
|
560
|
+
const workUnitCommand = {
|
|
561
|
+
description: "Run the Wefter work-unit implementation workflow: plan, review, gate, implement tasks, review tasks, and validate.",
|
|
562
|
+
agent: "wefter-work-unit-orchestrator",
|
|
563
|
+
template: `Run or resume the Wefter work-unit implementation workflow. Read ${CONFIG_FILE} first. If the user provided an existing .audit/wefter/work-unit-implementation/<run-id> path, resume it. Otherwise create a run with ${config.runnerCommand} work-unit run. Use the work unit id supplied by the user, or ask if unclear. Generate the work-unit plan, run adversarial plan reviews, consolidate, validate, repair candidate artifacts, apply the configured gate policy, and only execute code tasks after approval.`
|
|
564
|
+
};
|
|
565
|
+
const productShapeCommand = {
|
|
566
|
+
description: "Run the Wefter product-shaping workflow from idea to validated product specs and DELIVERABLES.md handoff.",
|
|
567
|
+
agent: "wefter-product-orchestrator",
|
|
568
|
+
template: `Run or resume the Wefter product-shaping workflow. Read ${CONFIG_FILE} first. If the user provided an existing ${productShapingRunRoot(config)}/<run-id> path, resume it. Otherwise create a run with ${config.runnerCommand} product shape. Shape source materials into product specs under ${productShapingSpecRoot(config)}, stop for human product decisions when needed, validate the completion gate, and do not create task specs or implementation plans.`
|
|
569
|
+
};
|
|
570
|
+
const repairDocsCommand = {
|
|
571
|
+
description: "Run the Wefter documentation repair workflow from a validated audit report.",
|
|
572
|
+
agent: "wefter-doc-repair-orchestrator",
|
|
573
|
+
template: `Run or resume the Wefter documentation repair workflow. Read ${CONFIG_FILE} first. If the user provided an existing .audit/wefter/documentation-repair/<run-id> path, resume it. Otherwise create a run with ${config.runnerCommand} docs repair using the final audit report path supplied by the user. If the report path is missing, ask for it. Plan repairs first, pause on human-decision items, apply approved documentation edits, review the result, and recommend a follow-up documentation audit.`
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const commands = {
|
|
577
|
+
"wefter-audit-docs": fullRunCommand,
|
|
578
|
+
"wefter-generate-doc-audit-profile": generateProfileCommand,
|
|
579
|
+
"wefter-repair-docs": repairDocsCommand,
|
|
580
|
+
"wefter-run-work-unit": workUnitCommand
|
|
581
|
+
};
|
|
582
|
+
if (productSettings.enabled) {
|
|
583
|
+
commands["wefter-shape-product"] = productShapeCommand;
|
|
584
|
+
} else {
|
|
585
|
+
delete existing.command["wefter-shape-product"];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
for (const [name, nextValue] of Object.entries(commands)) {
|
|
589
|
+
if (existing.command[name] && JSON.stringify(existing.command[name]) !== JSON.stringify(nextValue) && !force) {
|
|
590
|
+
throw new Error(`Refusing to overwrite existing opencode command '${name}'. Use --force to replace it.`);
|
|
591
|
+
}
|
|
592
|
+
existing.command[name] = nextValue;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
writeJson(opencodePath, existing);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function unique(values) {
|
|
599
|
+
return [...new Set(values.filter(Boolean))];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function defaultProfile(config = DEFAULTS) {
|
|
603
|
+
return {
|
|
604
|
+
version: 1,
|
|
605
|
+
corpus: {
|
|
606
|
+
include: ["*.md", "docs/**/*.md"],
|
|
607
|
+
exclude: unique([
|
|
608
|
+
"node_modules/**",
|
|
609
|
+
".git/**",
|
|
610
|
+
".opencode/**",
|
|
611
|
+
`${config.artifactRoot}/**`,
|
|
612
|
+
`${documentationRepairArtifactRoot()}/**`,
|
|
613
|
+
`${config.templateRoot}/**`,
|
|
614
|
+
config.processDocPath
|
|
615
|
+
])
|
|
616
|
+
},
|
|
617
|
+
variants: [
|
|
618
|
+
{
|
|
619
|
+
id: "explicit-contradictions",
|
|
620
|
+
title: "Explicit contradictions",
|
|
621
|
+
instruction: "Find statements that cannot all be true at the same time. Prioritize direct conflicts in scope, state, permission, obligation, technology, workflow, data or acceptance criteria."
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "implicit-incompatibilities",
|
|
625
|
+
title: "Implicit incompatibilities",
|
|
626
|
+
instruction: "Find statements that look compatible in isolation but conflict when combined. Evaluate preconditions, consequences, dependencies and operational impact."
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: "staleness-and-drift",
|
|
630
|
+
title: "Staleness and drift",
|
|
631
|
+
instruction: "Find signs that a document is outdated compared with newer decisions, terminology, status or implementation direction."
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
id: "adversarial-edge-cases",
|
|
635
|
+
title: "Adversarial edge cases",
|
|
636
|
+
instruction: "Look for gaps in error states, validation, concurrency, permissions, traceability, security, incomplete data and documented limits."
|
|
637
|
+
}
|
|
638
|
+
],
|
|
639
|
+
lenses: [
|
|
640
|
+
{
|
|
641
|
+
id: "documentation-map-consistency",
|
|
642
|
+
title: "Documentation map consistency",
|
|
643
|
+
focus: "Compare index, overview, roadmap, technical and domain documents. Look for missing cross-references, duplicated responsibilities and contradictions between high-level and detailed documents."
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "requirements-vs-technical-guidance",
|
|
647
|
+
title: "Requirements vs technical guidance",
|
|
648
|
+
focus: "Compare product requirements, technical decisions, setup instructions, architecture notes and delivery guidance. Look for required behavior without technical support and technical guidance that contradicts requirements."
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: "terminology-and-responsibility",
|
|
652
|
+
title: "Terminology and responsibility",
|
|
653
|
+
focus: "Compare glossary, naming, roles, permissions, ownership and document responsibility. Look for inconsistent terms or rules described in the wrong place."
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function validateProfile(profile) {
|
|
660
|
+
assertObject(profile, "Profile");
|
|
661
|
+
assertAllowedKeys(profile, "Profile", ["version", "auditorPromptPath", "corpus", "variants", "lenses"]);
|
|
662
|
+
|
|
663
|
+
if (profile.version !== 1) {
|
|
664
|
+
throw new Error("Profile must have version: 1.");
|
|
665
|
+
}
|
|
666
|
+
if (profile.auditorPromptPath !== undefined) {
|
|
667
|
+
normalizeRelativePath(profile.auditorPromptPath, "Profile auditorPromptPath");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
assertObject(profile.corpus, "Profile corpus");
|
|
671
|
+
assertAllowedKeys(profile.corpus, "Profile corpus", ["include", "exclude"]);
|
|
672
|
+
requireStringArray(profile.corpus.include, "Profile corpus.include");
|
|
673
|
+
requireStringArray(profile.corpus.exclude, "Profile corpus.exclude");
|
|
674
|
+
|
|
675
|
+
if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
|
|
676
|
+
throw new Error("Profile must define at least one variant.");
|
|
677
|
+
}
|
|
678
|
+
profile.variants.forEach((variant, index) => {
|
|
679
|
+
assertObject(variant, `Profile variants[${index}]`);
|
|
680
|
+
assertAllowedKeys(variant, `Profile variants[${index}]`, ["id", "title", "instruction"]);
|
|
681
|
+
requireId(variant.id, `Profile variants[${index}].id`);
|
|
682
|
+
requireString(variant.title, `Profile variants[${index}].title`);
|
|
683
|
+
requireString(variant.instruction, `Profile variants[${index}].instruction`);
|
|
684
|
+
});
|
|
685
|
+
assertUniqueIds(profile.variants, "Profile variants");
|
|
686
|
+
|
|
687
|
+
if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
|
|
688
|
+
throw new Error("Profile must define at least one lens.");
|
|
689
|
+
}
|
|
690
|
+
profile.lenses.forEach((lens, index) => {
|
|
691
|
+
assertObject(lens, `Profile lenses[${index}]`);
|
|
692
|
+
assertAllowedKeys(lens, `Profile lenses[${index}]`, ["id", "title", "focus"]);
|
|
693
|
+
requireId(lens.id, `Profile lenses[${index}].id`);
|
|
694
|
+
requireString(lens.title, `Profile lenses[${index}].title`);
|
|
695
|
+
requireString(lens.focus, `Profile lenses[${index}].focus`);
|
|
696
|
+
});
|
|
697
|
+
assertUniqueIds(profile.lenses, "Profile lenses");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function documentationAuditAuditorPrompt(targetRoot, config, profile) {
|
|
701
|
+
const relativePath = profile.auditorPromptPath
|
|
702
|
+
? normalizeRelativePath(profile.auditorPromptPath, "Profile auditorPromptPath")
|
|
703
|
+
: toPosix(path.join(config.templateRoot, "auditor-prompt.md"));
|
|
704
|
+
const fullPath = resolveInsideTarget(targetRoot, relativePath, "auditor prompt");
|
|
705
|
+
return { relativePath, fullPath };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function validateWorkUnitConfig(config) {
|
|
709
|
+
assertObject(config, "Work-unit config");
|
|
710
|
+
assertAllowedKeys(config, "Work-unit config", ["version", "workflowName", "releaseId", "workUnitsDocument", "sourceDocs", "runArtifactsRoot", "versionedArtifacts", "defaultWorkUnitId", "defaultPlanAuditPassesPerLens", "gatePolicy"]);
|
|
711
|
+
|
|
712
|
+
if (config.version !== 1) {
|
|
713
|
+
throw new Error("Work-unit config must have version: 1.");
|
|
714
|
+
}
|
|
715
|
+
requireString(config.workflowName, "Work-unit config.workflowName");
|
|
716
|
+
if (config.workflowName !== WORK_UNIT_WORKFLOW_ID) {
|
|
717
|
+
throw new Error(`Work-unit config.workflowName must be ${WORK_UNIT_WORKFLOW_ID}.`);
|
|
718
|
+
}
|
|
719
|
+
requireString(config.releaseId, "Work-unit config.releaseId");
|
|
720
|
+
normalizeRelativePath(config.workUnitsDocument, "Work-unit config.workUnitsDocument");
|
|
721
|
+
normalizeRelativePath(config.runArtifactsRoot, "Work-unit config.runArtifactsRoot");
|
|
722
|
+
requireString(config.defaultWorkUnitId, "Work-unit config.defaultWorkUnitId");
|
|
723
|
+
|
|
724
|
+
const passes = Number.parseInt(String(config.defaultPlanAuditPassesPerLens), 10);
|
|
725
|
+
if (!Number.isInteger(passes) || passes < 1) {
|
|
726
|
+
throw new Error("Work-unit config.defaultPlanAuditPassesPerLens must be an integer greater than 0.");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
assertObject(config.sourceDocs, "Work-unit config.sourceDocs");
|
|
730
|
+
assertAllowedKeys(config.sourceDocs, "Work-unit config.sourceDocs", ["include", "exclude"]);
|
|
731
|
+
requireStringArray(config.sourceDocs.include, "Work-unit config.sourceDocs.include");
|
|
732
|
+
requireStringArray(config.sourceDocs.exclude, "Work-unit config.sourceDocs.exclude");
|
|
733
|
+
|
|
734
|
+
assertObject(config.versionedArtifacts, "Work-unit config.versionedArtifacts");
|
|
735
|
+
assertAllowedKeys(config.versionedArtifacts, "Work-unit config.versionedArtifacts", ["executionRoot", "decisionLogRoot"]);
|
|
736
|
+
normalizeRelativePath(config.versionedArtifacts.executionRoot, "Work-unit config.versionedArtifacts.executionRoot");
|
|
737
|
+
normalizeRelativePath(config.versionedArtifacts.decisionLogRoot, "Work-unit config.versionedArtifacts.decisionLogRoot");
|
|
738
|
+
|
|
739
|
+
assertObject(config.gatePolicy, "Work-unit config.gatePolicy");
|
|
740
|
+
assertAllowedKeys(config.gatePolicy, "Work-unit config.gatePolicy", ["mode", "structuralWorkUnits", "alwaysPauseOn"]);
|
|
741
|
+
requireString(config.gatePolicy.mode, "Work-unit config.gatePolicy.mode");
|
|
742
|
+
requireStringArray(config.gatePolicy.structuralWorkUnits, "Work-unit config.gatePolicy.structuralWorkUnits");
|
|
743
|
+
requireStringArray(config.gatePolicy.alwaysPauseOn, "Work-unit config.gatePolicy.alwaysPauseOn");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function validateProductShapingConfig(config) {
|
|
747
|
+
assertObject(config, "Product-shaping config");
|
|
748
|
+
assertAllowedKeys(config, "Product-shaping config", ["version", "workflowName", "releaseId", "specRoot", "runRoot", "contractPath", "processDocPath", "requiredFiles", "completionGate"]);
|
|
749
|
+
|
|
750
|
+
if (config.version !== 1) {
|
|
751
|
+
throw new Error("Product-shaping config must have version: 1.");
|
|
752
|
+
}
|
|
753
|
+
if (config.workflowName !== PRODUCT_SHAPING_WORKFLOW_ID) {
|
|
754
|
+
throw new Error(`Product-shaping config.workflowName must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
|
|
755
|
+
}
|
|
756
|
+
requireString(config.releaseId, "Product-shaping config.releaseId");
|
|
757
|
+
normalizeRelativePath(config.specRoot, "Product-shaping config.specRoot");
|
|
758
|
+
normalizeRelativePath(config.runRoot, "Product-shaping config.runRoot");
|
|
759
|
+
normalizeRelativePath(config.contractPath, "Product-shaping config.contractPath");
|
|
760
|
+
normalizeRelativePath(config.processDocPath, "Product-shaping config.processDocPath");
|
|
761
|
+
requireStringArray(config.requiredFiles, "Product-shaping config.requiredFiles");
|
|
762
|
+
if (JSON.stringify(config.requiredFiles) !== JSON.stringify(PRODUCT_SHAPING_REQUIRED_FILES)) {
|
|
763
|
+
throw new Error("Product-shaping config.requiredFiles must match the canonical product spec file set and order.");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
assertObject(config.completionGate, "Product-shaping config.completionGate");
|
|
767
|
+
assertAllowedKeys(config.completionGate, "Product-shaping config.completionGate", ["requireNoReleaseBlockingQuestions", "requireAdversarialReview", "requireFinalValidation", "readyDeliverableStatuses"]);
|
|
768
|
+
for (const key of ["requireNoReleaseBlockingQuestions", "requireAdversarialReview", "requireFinalValidation"]) {
|
|
769
|
+
if (typeof config.completionGate[key] !== "boolean") {
|
|
770
|
+
throw new Error(`Product-shaping config.completionGate.${key} must be boolean.`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const key of ["requireAdversarialReview", "requireFinalValidation"]) {
|
|
774
|
+
if (config.completionGate[key] !== true) {
|
|
775
|
+
throw new Error(`Product-shaping config.completionGate.${key} must be true for product-shaping completion.`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
requireStringArray(config.completionGate.readyDeliverableStatuses, "Product-shaping config.completionGate.readyDeliverableStatuses");
|
|
779
|
+
if (JSON.stringify(config.completionGate.readyDeliverableStatuses) !== JSON.stringify(["ready"])) {
|
|
780
|
+
throw new Error("Product-shaping config.completionGate.readyDeliverableStatuses must be exactly ['ready'].");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function validateProductShapingProfile(profile) {
|
|
785
|
+
assertObject(profile, "Product-shaping profile");
|
|
786
|
+
assertAllowedKeys(profile, "Product-shaping profile", ["version", "workflowName", "variants", "lenses"]);
|
|
787
|
+
|
|
788
|
+
if (profile.version !== 1) {
|
|
789
|
+
throw new Error("Product-shaping profile must have version: 1.");
|
|
790
|
+
}
|
|
791
|
+
if (profile.workflowName !== PRODUCT_SHAPING_WORKFLOW_ID) {
|
|
792
|
+
throw new Error(`Product-shaping profile.workflowName must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
|
|
796
|
+
throw new Error("Product-shaping profile must define at least one variant.");
|
|
797
|
+
}
|
|
798
|
+
profile.variants.forEach((variant, index) => {
|
|
799
|
+
assertObject(variant, `Product-shaping profile variants[${index}]`);
|
|
800
|
+
assertAllowedKeys(variant, `Product-shaping profile variants[${index}]`, ["id", "title", "instruction"]);
|
|
801
|
+
requireId(variant.id, `Product-shaping profile variants[${index}].id`);
|
|
802
|
+
requireString(variant.title, `Product-shaping profile variants[${index}].title`);
|
|
803
|
+
requireString(variant.instruction, `Product-shaping profile variants[${index}].instruction`);
|
|
804
|
+
});
|
|
805
|
+
assertUniqueIds(profile.variants, "Product-shaping profile variants");
|
|
806
|
+
|
|
807
|
+
if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
|
|
808
|
+
throw new Error("Product-shaping profile must define at least one lens.");
|
|
809
|
+
}
|
|
810
|
+
profile.lenses.forEach((lens, index) => {
|
|
811
|
+
assertObject(lens, `Product-shaping profile lenses[${index}]`);
|
|
812
|
+
assertAllowedKeys(lens, `Product-shaping profile lenses[${index}]`, ["id", "title", "focus"]);
|
|
813
|
+
requireId(lens.id, `Product-shaping profile lenses[${index}].id`);
|
|
814
|
+
requireString(lens.title, `Product-shaping profile lenses[${index}].title`);
|
|
815
|
+
requireString(lens.focus, `Product-shaping profile lenses[${index}].focus`);
|
|
816
|
+
});
|
|
817
|
+
assertUniqueIds(profile.lenses, "Product-shaping profile lenses");
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function validateWorkUnitProfile(profile) {
|
|
821
|
+
assertObject(profile, "Work-unit profile");
|
|
822
|
+
assertAllowedKeys(profile, "Work-unit profile", ["version", "variants", "lenses"]);
|
|
823
|
+
|
|
824
|
+
if (profile.version !== 1) {
|
|
825
|
+
throw new Error("Work-unit profile must have version: 1.");
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
|
|
829
|
+
throw new Error("Work-unit profile must define at least one variant.");
|
|
830
|
+
}
|
|
831
|
+
profile.variants.forEach((variant, index) => {
|
|
832
|
+
assertObject(variant, `Work-unit profile variants[${index}]`);
|
|
833
|
+
assertAllowedKeys(variant, `Work-unit profile variants[${index}]`, ["id", "title", "instruction"]);
|
|
834
|
+
requireId(variant.id, `Work-unit profile variants[${index}].id`);
|
|
835
|
+
requireString(variant.title, `Work-unit profile variants[${index}].title`);
|
|
836
|
+
requireString(variant.instruction, `Work-unit profile variants[${index}].instruction`);
|
|
837
|
+
});
|
|
838
|
+
assertUniqueIds(profile.variants, "Work-unit profile variants");
|
|
839
|
+
|
|
840
|
+
if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
|
|
841
|
+
throw new Error("Work-unit profile must define at least one lens.");
|
|
842
|
+
}
|
|
843
|
+
profile.lenses.forEach((lens, index) => {
|
|
844
|
+
assertObject(lens, `Work-unit profile lenses[${index}]`);
|
|
845
|
+
assertAllowedKeys(lens, `Work-unit profile lenses[${index}]`, ["id", "title", "focus"]);
|
|
846
|
+
requireId(lens.id, `Work-unit profile lenses[${index}].id`);
|
|
847
|
+
requireString(lens.title, `Work-unit profile lenses[${index}].title`);
|
|
848
|
+
requireString(lens.focus, `Work-unit profile lenses[${index}].focus`);
|
|
849
|
+
});
|
|
850
|
+
assertUniqueIds(profile.lenses, "Work-unit profile lenses");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function markdownList(items) {
|
|
854
|
+
if (!items || items.length === 0) {
|
|
855
|
+
return "- <none>";
|
|
856
|
+
}
|
|
857
|
+
return items.map((item) => `- \`${item}\``).join("\n");
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function timestampRunName() {
|
|
861
|
+
const now = new Date();
|
|
862
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
863
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function buildCombinations(profile, passesPerLens, maxAudits) {
|
|
867
|
+
const combinations = [];
|
|
868
|
+
let auditIndex = 1;
|
|
869
|
+
|
|
870
|
+
for (const lens of profile.lenses) {
|
|
871
|
+
for (const variant of profile.variants) {
|
|
872
|
+
for (let pass = 1; pass <= passesPerLens; pass++) {
|
|
873
|
+
if (maxAudits > 0 && combinations.length >= maxAudits) {
|
|
874
|
+
return combinations;
|
|
875
|
+
}
|
|
876
|
+
const auditId = `A${String(auditIndex).padStart(4, "0")}__${lens.id}__${variant.id}__p${String(pass).padStart(2, "0")}`;
|
|
877
|
+
combinations.push({ auditId, lens, variant, pass });
|
|
878
|
+
auditIndex++;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return combinations;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function commandInit(flags) {
|
|
887
|
+
const targetRoot = resolveTarget(flags);
|
|
888
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
889
|
+
ensureInside(path.dirname(targetRoot), targetRoot, "target");
|
|
890
|
+
|
|
891
|
+
let profilePath = flags["profile-path"] || DEFAULTS.profilePath;
|
|
892
|
+
let artifactRoot = flags["artifact-root"] || DEFAULTS.artifactRoot;
|
|
893
|
+
const workflowRoot = DEFAULTS.workflowRoot;
|
|
894
|
+
const templateRoot = flags["template-root"] || DEFAULTS.templateRoot;
|
|
895
|
+
const processDocPath = flags["process-doc-path"] || DEFAULTS.processDocPath;
|
|
896
|
+
const runnerCommand = flags["runner-command"] || defaultRunnerCommand();
|
|
897
|
+
|
|
898
|
+
if (!flags.yes && process.stdin.isTTY) {
|
|
899
|
+
const rl = readline.createInterface({ input, output });
|
|
900
|
+
profilePath = await promptForValue(rl, "Audit profile path", profilePath);
|
|
901
|
+
artifactRoot = await promptForValue(rl, "Audit artifact root", artifactRoot);
|
|
902
|
+
rl.close();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const config = normalizeConfig({
|
|
906
|
+
version: 1,
|
|
907
|
+
workflowRoot,
|
|
908
|
+
profilePath,
|
|
909
|
+
artifactRoot,
|
|
910
|
+
templateRoot,
|
|
911
|
+
processDocPath,
|
|
912
|
+
runnerCommand,
|
|
913
|
+
workflows: defaultWorkflowRegistry()
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const values = {
|
|
917
|
+
PROFILE_PATH: config.profilePath,
|
|
918
|
+
ARTIFACT_ROOT: config.artifactRoot,
|
|
919
|
+
ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(config.artifactRoot),
|
|
920
|
+
TEMPLATE_ROOT: config.templateRoot,
|
|
921
|
+
PROCESS_DOC_PATH: config.processDocPath,
|
|
922
|
+
RUNNER_COMMAND: config.runnerCommand,
|
|
923
|
+
CONFIG_FILE,
|
|
924
|
+
RUNNER_COMMAND_NEW_RUN_PATTERN: yamlSingleQuoted(`${config.runnerCommand}*`),
|
|
925
|
+
RUNNER_COMMAND_DOCS_REPAIR_PATTERN: yamlSingleQuoted(`${config.runnerCommand} docs repair*`),
|
|
926
|
+
RUNNER_COMMAND_PRODUCT_SHAPE_PATTERN: yamlSingleQuoted(`${config.runnerCommand} product shape*`),
|
|
927
|
+
RUNNER_COMMAND_PRODUCT_VALIDATE_PATTERN: yamlSingleQuoted(`${config.runnerCommand} product validate*`),
|
|
928
|
+
DOCUMENTATION_REPAIR_ARTIFACT_ROOT: documentationRepairArtifactRoot(),
|
|
929
|
+
DOCUMENTATION_REPAIR_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(documentationRepairArtifactRoot()),
|
|
930
|
+
PRODUCT_SHAPING_SPEC_ROOT: productShapingSpecRoot(config),
|
|
931
|
+
PRODUCT_SHAPING_SPEC_ROOT_WINDOWS: windowsPermissionPath(productShapingSpecRoot(config)),
|
|
932
|
+
PRODUCT_SHAPING_RUN_ROOT: productShapingRunRoot(config),
|
|
933
|
+
PRODUCT_SHAPING_RUN_ROOT_WINDOWS: windowsPermissionPath(productShapingRunRoot(config)),
|
|
934
|
+
PRODUCT_SHAPING_CONFIG_PATH: productShapingConfigPath(config),
|
|
935
|
+
PRODUCT_SHAPING_PROFILE_PATH: productShapingProfilePath(config),
|
|
936
|
+
PRODUCT_SHAPING_PROCESS_DOC_PATH: `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/README.md`,
|
|
937
|
+
RUNNER_COMMAND_WORK_UNIT_PATTERN: yamlSingleQuoted(`${config.runnerCommand} work-unit*`),
|
|
938
|
+
WORK_UNIT_ARTIFACT_ROOT: ".audit/wefter/work-unit-implementation",
|
|
939
|
+
WORK_UNIT_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(".audit/wefter/work-unit-implementation"),
|
|
940
|
+
WORK_UNIT_CONFIG_PATH: ".wefter/workflows/work-unit-implementation/config.json",
|
|
941
|
+
WORK_UNIT_PROFILE_PATH: ".wefter/workflows/work-unit-implementation/profile.json"
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
writeJsonIfSafe(path.join(targetRoot, CONFIG_FILE), {
|
|
945
|
+
"$schema": "./node_modules/@wefter/opencode/schemas/wefter.config.schema.json",
|
|
946
|
+
...config
|
|
947
|
+
}, flags.force);
|
|
948
|
+
|
|
949
|
+
const root = packageRoot();
|
|
950
|
+
const auditTemplates = documentationAuditTemplateRoot();
|
|
951
|
+
const productShapingPackageRoot = productShapingWorkflowPackageRoot();
|
|
952
|
+
const workUnitPackageRoot = workUnitWorkflowPackageRoot();
|
|
953
|
+
copyRenderedTemplate(path.join(root, "src/workflows/documentation-audit/workflow.json"), path.join(targetRoot, config.workflowRoot, "documentation-audit/workflow.json"), values, flags.force);
|
|
954
|
+
for (const workflowId of ["product-shaping", "documentation-repair", "technical-shaping", "work-unit-implementation"]) {
|
|
955
|
+
copyDirectory(path.join(root, "src/workflows", workflowId), path.join(targetRoot, config.workflowRoot, workflowId), flags.force);
|
|
956
|
+
}
|
|
957
|
+
copyDirectory(path.join(workUnitPackageRoot, "templates/prompts"), path.join(targetRoot, config.workflowRoot, WORK_UNIT_WORKFLOW_ID, "templates/prompts"), flags.force);
|
|
958
|
+
const productShapingConfig = readJson(path.join(productShapingPackageRoot, "templates/default-config.json"), "default product-shaping config");
|
|
959
|
+
productShapingConfig.specRoot = productShapingSpecRoot(config);
|
|
960
|
+
productShapingConfig.runRoot = productShapingRunRoot(config);
|
|
961
|
+
productShapingConfig.contractPath = `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/contracts/product-spec-contract.json`;
|
|
962
|
+
productShapingConfig.processDocPath = `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/README.md`;
|
|
963
|
+
validateProductShapingConfig(productShapingConfig);
|
|
964
|
+
writeJsonIfSafe(path.join(targetRoot, productShapingConfigPath(config)), productShapingConfig, flags.force);
|
|
965
|
+
const productShapingProfile = readJson(path.join(productShapingPackageRoot, "templates/default-profile.json"), "default product-shaping profile");
|
|
966
|
+
validateProductShapingProfile(productShapingProfile);
|
|
967
|
+
writeJsonIfSafe(path.join(targetRoot, productShapingProfilePath(config)), productShapingProfile, flags.force);
|
|
968
|
+
writeJsonIfSafe(path.join(targetRoot, workUnitConfigPath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-config.json"), "default work-unit config"), flags.force);
|
|
969
|
+
writeJsonIfSafe(path.join(targetRoot, workUnitProfilePath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-profile.json"), "default work-unit profile"), flags.force);
|
|
970
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-audit-orchestrator.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-audit-orchestrator.md"), values, flags.force);
|
|
971
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-auditor.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-auditor.md"), values, flags.force);
|
|
972
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-audit-consolidator.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-audit-consolidator.md"), values, flags.force);
|
|
973
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-audit-validator.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-audit-validator.md"), values, flags.force);
|
|
974
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-audit-profile-builder.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-audit-profile-builder.md"), values, flags.force);
|
|
975
|
+
copyRenderedTemplate(path.join(auditTemplates, "opencode/skills/documentation-audit/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/documentation-audit/SKILL.md"), values, flags.force);
|
|
976
|
+
const repairTemplates = documentationRepairTemplateRoot();
|
|
977
|
+
for (const agentFile of ["wefter-doc-repair-orchestrator", "wefter-doc-repair-planner", "wefter-doc-repairer", "wefter-doc-repair-reviewer"]) {
|
|
978
|
+
copyRenderedTemplate(path.join(repairTemplates, "opencode/agent", `${agentFile}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `${agentFile}.md`), values, flags.force);
|
|
979
|
+
}
|
|
980
|
+
copyRenderedTemplate(path.join(repairTemplates, "opencode/skills/documentation-repair/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md"), values, flags.force);
|
|
981
|
+
for (const agent of ["orchestrator", "intake-analyst", "reference-researcher", "shaper", "domain-modeler", "release-planner", "auditor", "validator", "repairer"]) {
|
|
982
|
+
copyRenderedTemplate(path.join(productShapingPackageRoot, "templates/opencode/agent", `wefter-product-${agent}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `wefter-product-${agent}.md`), values, flags.force);
|
|
983
|
+
}
|
|
984
|
+
copyRenderedTemplate(path.join(productShapingPackageRoot, "templates/opencode/skills/product-shaping/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/product-shaping/SKILL.md"), values, flags.force);
|
|
985
|
+
for (const agent of ["orchestrator", "planner", "plan-auditor", "plan-consolidator", "plan-validator", "plan-repairer", "task-implementer", "task-reviewer", "validator"]) {
|
|
986
|
+
copyRenderedTemplate(path.join(workUnitPackageRoot, "templates/opencode/agent", `wefter-work-unit-${agent}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `wefter-work-unit-${agent}.md`), values, flags.force);
|
|
987
|
+
}
|
|
988
|
+
copyRenderedTemplate(path.join(workUnitPackageRoot, "templates/opencode/skills/work-unit-implementation/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/work-unit-implementation/SKILL.md"), values, flags.force);
|
|
989
|
+
copyDirectory(path.join(auditTemplates, "prompts"), path.join(targetRoot, config.templateRoot), flags.force);
|
|
990
|
+
copyRenderedTemplate(path.join(auditTemplates, "README.md.tmpl"), path.join(targetRoot, config.processDocPath), values, flags.force);
|
|
991
|
+
mergeOpencodeConfig(targetRoot, config, flags.force);
|
|
992
|
+
|
|
993
|
+
const profileFullPath = path.join(targetRoot, config.profilePath);
|
|
994
|
+
if (!fs.existsSync(profileFullPath)) {
|
|
995
|
+
writeJson(profileFullPath, defaultProfile(config));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
console.log(`Installed Wefter for OpenCode into ${targetRoot}`);
|
|
999
|
+
console.log(`Profile: ${config.profilePath}`);
|
|
1000
|
+
console.log(`Artifacts: ${config.artifactRoot}`);
|
|
1001
|
+
console.log(`Runner command: ${config.runnerCommand}`);
|
|
1002
|
+
console.log(`Tip: add ${config.artifactRoot}/ to .gitignore if you do not want to track generated audit runs.`);
|
|
1003
|
+
console.log("Restart opencode before using /wefter-shape-product, /wefter-audit-docs, /wefter-generate-doc-audit-profile, /wefter-repair-docs, or /wefter-run-work-unit.");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function readTextRequired(filePath) {
|
|
1007
|
+
if (!fs.existsSync(filePath)) {
|
|
1008
|
+
throw new Error(`Missing ${filePath}`);
|
|
1009
|
+
}
|
|
1010
|
+
return fs.readFileSync(filePath, "utf8");
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function assertNoPlaceholders(filePath, content) {
|
|
1014
|
+
const match = content.match(/{{[^}]+}}/);
|
|
1015
|
+
if (match) {
|
|
1016
|
+
throw new Error(`${filePath} contains unresolved placeholder ${match[0]}.`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function assertIncludes(content, expected, label) {
|
|
1021
|
+
if (!content.includes(expected)) {
|
|
1022
|
+
throw new Error(`Missing ${label}: ${expected}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function commandNewRun(flags) {
|
|
1027
|
+
const targetRoot = resolveTarget(flags);
|
|
1028
|
+
const config = readConfig(targetRoot);
|
|
1029
|
+
const profilePathRelative = normalizeRelativePath(flags["profile-path"] || config.profilePath, "profilePath");
|
|
1030
|
+
const profilePath = path.join(targetRoot, profilePathRelative);
|
|
1031
|
+
ensureInside(targetRoot, profilePath, "profilePath");
|
|
1032
|
+
const profile = readJson(profilePath, "audit profile");
|
|
1033
|
+
validateProfile(profile);
|
|
1034
|
+
|
|
1035
|
+
const passesPerLens = Number.parseInt(flags["passes-per-lens"] || "3", 10);
|
|
1036
|
+
const maxAudits = Number.parseInt(flags["max-audits"] || "0", 10);
|
|
1037
|
+
if (!Number.isInteger(passesPerLens) || passesPerLens < 1) {
|
|
1038
|
+
throw new Error("--passes-per-lens must be an integer greater than 0.");
|
|
1039
|
+
}
|
|
1040
|
+
if (!Number.isInteger(maxAudits) || maxAudits < 0) {
|
|
1041
|
+
throw new Error("--max-audits must be an integer greater than or equal to 0.");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const runName = flags["run-name"] || timestampRunName();
|
|
1045
|
+
assertSafeRunName(runName);
|
|
1046
|
+
const combinations = buildCombinations(profile, passesPerLens, maxAudits);
|
|
1047
|
+
const auditorPrompt = documentationAuditAuditorPrompt(targetRoot, config, profile);
|
|
1048
|
+
|
|
1049
|
+
const artifactRoot = path.join(targetRoot, config.artifactRoot);
|
|
1050
|
+
const tempRoot = path.join(artifactRoot, ".tmp");
|
|
1051
|
+
const runRoot = path.join(artifactRoot, runName);
|
|
1052
|
+
const stagingRunRoot = path.join(tempRoot, runName);
|
|
1053
|
+
ensureInside(targetRoot, artifactRoot, "artifactRoot");
|
|
1054
|
+
ensureInside(targetRoot, runRoot, "runRoot");
|
|
1055
|
+
ensureInside(targetRoot, stagingRunRoot, "stagingRunRoot");
|
|
1056
|
+
|
|
1057
|
+
if (flags["dry-run"]) {
|
|
1058
|
+
console.log(`Run name: ${runName}`);
|
|
1059
|
+
console.log(`Lenses: ${profile.lenses.length}`);
|
|
1060
|
+
console.log(`Variants: ${profile.variants.length}`);
|
|
1061
|
+
console.log(`Passes per lens/variant: ${passesPerLens}`);
|
|
1062
|
+
console.log(`Auditor prompts to generate: ${combinations.length}`);
|
|
1063
|
+
console.log(`Output root: ${runRoot}`);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (fs.existsSync(runRoot)) {
|
|
1068
|
+
throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name to avoid mixing stale prompts or outputs.`);
|
|
1069
|
+
}
|
|
1070
|
+
if (fs.existsSync(stagingRunRoot)) {
|
|
1071
|
+
throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no audit run is in progress, or use a different --run-name.`);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const promptsRoot = path.join(stagingRunRoot, "prompts");
|
|
1075
|
+
const auditorPromptsRoot = path.join(promptsRoot, "auditors");
|
|
1076
|
+
const rawRoot = path.join(stagingRunRoot, "raw");
|
|
1077
|
+
const consolidationRoot = path.join(stagingRunRoot, "consolidation");
|
|
1078
|
+
const validationRoot = path.join(stagingRunRoot, "validation");
|
|
1079
|
+
const finalRoot = path.join(stagingRunRoot, "final");
|
|
1080
|
+
for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, auditorPromptsRoot, rawRoot, consolidationRoot, validationRoot, finalRoot]) {
|
|
1081
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const templateRoot = path.join(targetRoot, config.templateRoot);
|
|
1085
|
+
const auditorTemplate = readTextRequired(auditorPrompt.fullPath);
|
|
1086
|
+
const consolidatorTemplate = fs.readFileSync(path.join(templateRoot, "consolidator-prompt.md"), "utf8");
|
|
1087
|
+
const validatorTemplate = fs.readFileSync(path.join(templateRoot, "validator-prompt.md"), "utf8");
|
|
1088
|
+
const promptRecords = [];
|
|
1089
|
+
|
|
1090
|
+
for (const combo of combinations) {
|
|
1091
|
+
const outputRelative = toPosix(path.join(config.artifactRoot, runName, "raw", `${combo.auditId}.md`));
|
|
1092
|
+
const promptRelative = toPosix(path.join(config.artifactRoot, runName, "prompts", "auditors", `${combo.auditId}.md`));
|
|
1093
|
+
const prompt = renderTemplate(auditorTemplate, {
|
|
1094
|
+
RUN_ID: runName,
|
|
1095
|
+
AUDIT_ID: combo.auditId,
|
|
1096
|
+
LENS_ID: combo.lens.id,
|
|
1097
|
+
LENS_TITLE: combo.lens.title,
|
|
1098
|
+
LENS_FOCUS: combo.lens.focus,
|
|
1099
|
+
VARIANT_ID: combo.variant.id,
|
|
1100
|
+
VARIANT_TITLE: combo.variant.title,
|
|
1101
|
+
VARIANT_INSTRUCTION: combo.variant.instruction,
|
|
1102
|
+
PASS_NUMBER: combo.pass,
|
|
1103
|
+
OUTPUT_FILE: outputRelative,
|
|
1104
|
+
CORPUS_INCLUDE: markdownList(profile.corpus.include),
|
|
1105
|
+
CORPUS_EXCLUDE: markdownList(profile.corpus.exclude)
|
|
1106
|
+
});
|
|
1107
|
+
fs.writeFileSync(path.join(auditorPromptsRoot, `${combo.auditId}.md`), prompt, "utf8");
|
|
1108
|
+
promptRecords.push({
|
|
1109
|
+
auditId: combo.auditId,
|
|
1110
|
+
lensId: combo.lens.id,
|
|
1111
|
+
variantId: combo.variant.id,
|
|
1112
|
+
pass: combo.pass,
|
|
1113
|
+
prompt: promptRelative,
|
|
1114
|
+
output: outputRelative
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const consolidatedRelative = toPosix(path.join(config.artifactRoot, runName, "consolidation", "consolidated-candidates.md"));
|
|
1119
|
+
const discardedRelative = toPosix(path.join(config.artifactRoot, runName, "consolidation", "discarded-raw-findings.md"));
|
|
1120
|
+
const validationRelative = toPosix(path.join(config.artifactRoot, runName, "validation", "adversarial-validation-log.md"));
|
|
1121
|
+
const finalRelative = toPosix(path.join(config.artifactRoot, runName, "final", "final-documentation-audit-report.md"));
|
|
1122
|
+
const rawRelative = toPosix(path.join(config.artifactRoot, runName, "raw"));
|
|
1123
|
+
|
|
1124
|
+
fs.writeFileSync(path.join(promptsRoot, "consolidate.md"), renderTemplate(consolidatorTemplate, {
|
|
1125
|
+
RUN_ID: runName,
|
|
1126
|
+
RAW_DIR: rawRelative,
|
|
1127
|
+
CONSOLIDATED_OUTPUT: consolidatedRelative,
|
|
1128
|
+
DISCARDED_OUTPUT: discardedRelative
|
|
1129
|
+
}), "utf8");
|
|
1130
|
+
fs.writeFileSync(path.join(promptsRoot, "validate.md"), renderTemplate(validatorTemplate, {
|
|
1131
|
+
RUN_ID: runName,
|
|
1132
|
+
CONSOLIDATED_OUTPUT: consolidatedRelative,
|
|
1133
|
+
VALIDATION_OUTPUT: validationRelative,
|
|
1134
|
+
FINAL_OUTPUT: finalRelative
|
|
1135
|
+
}), "utf8");
|
|
1136
|
+
|
|
1137
|
+
writeJson(path.join(stagingRunRoot, "manifest.json"), {
|
|
1138
|
+
version: 1,
|
|
1139
|
+
workflowId: "documentation-audit",
|
|
1140
|
+
runId: runName,
|
|
1141
|
+
generatedAt: new Date().toISOString(),
|
|
1142
|
+
passesPerLens,
|
|
1143
|
+
maxAudits,
|
|
1144
|
+
profilePath: profilePathRelative,
|
|
1145
|
+
auditorPromptPath: auditorPrompt.relativePath,
|
|
1146
|
+
corpus: profile.corpus,
|
|
1147
|
+
counts: {
|
|
1148
|
+
lenses: profile.lenses.length,
|
|
1149
|
+
variants: profile.variants.length,
|
|
1150
|
+
auditorPrompts: combinations.length
|
|
1151
|
+
},
|
|
1152
|
+
paths: {
|
|
1153
|
+
runRoot: toPosix(path.join(config.artifactRoot, runName)),
|
|
1154
|
+
prompts: toPosix(path.join(config.artifactRoot, runName, "prompts")),
|
|
1155
|
+
raw: rawRelative,
|
|
1156
|
+
consolidation: toPosix(path.join(config.artifactRoot, runName, "consolidation")),
|
|
1157
|
+
validation: toPosix(path.join(config.artifactRoot, runName, "validation")),
|
|
1158
|
+
final: toPosix(path.join(config.artifactRoot, runName, "final"))
|
|
1159
|
+
},
|
|
1160
|
+
prompts: promptRecords
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Documentation Audit Run\n\nRun: ${runName}\n\n## Counts\n\n- Lenses: ${profile.lenses.length}\n- Variants: ${profile.variants.length}\n- Passes per lens/variant: ${passesPerLens}\n- Auditor prompts: ${combinations.length}\n\n## Execution Order\n\n1. Execute auditor prompts from prompts/auditors/ and write outputs to raw/.\n2. Execute prompts/consolidate.md after raw outputs exist.\n3. Execute prompts/validate.md after consolidation exists.\n4. Review final/final-documentation-audit-report.md.\n\n## opencode Command\n\n- Use /wefter-audit-docs with this run path to execute or resume the end-to-end audit.\n`, "utf8");
|
|
1164
|
+
|
|
1165
|
+
if (fs.existsSync(runRoot)) {
|
|
1166
|
+
throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
|
|
1167
|
+
}
|
|
1168
|
+
fs.renameSync(stagingRunRoot, runRoot);
|
|
1169
|
+
|
|
1170
|
+
console.log(`Created documentation audit run: ${runRoot}`);
|
|
1171
|
+
console.log(`Auditor prompts generated: ${combinations.length}`);
|
|
1172
|
+
console.log(`Next prompt directory: ${path.join(runRoot, "prompts", "auditors")}`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function commandDocsRepair(flags) {
|
|
1176
|
+
const targetRoot = resolveTarget(flags);
|
|
1177
|
+
const config = readConfig(targetRoot);
|
|
1178
|
+
if (!flags["audit-report"]) {
|
|
1179
|
+
throw new Error("--audit-report is required for docs repair.");
|
|
1180
|
+
}
|
|
1181
|
+
const auditReportPath = normalizeRelativePath(flags["audit-report"], "audit-report");
|
|
1182
|
+
const auditReportFullPath = path.join(targetRoot, auditReportPath);
|
|
1183
|
+
ensureInside(targetRoot, auditReportFullPath, "audit report");
|
|
1184
|
+
if (!fs.existsSync(auditReportFullPath)) {
|
|
1185
|
+
throw new Error(`Audit report not found: ${auditReportFullPath}`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const runName = flags["run-name"] || timestampRunName();
|
|
1189
|
+
assertSafeRunName(runName);
|
|
1190
|
+
|
|
1191
|
+
const artifactRootRelative = documentationRepairArtifactRoot();
|
|
1192
|
+
const artifactRoot = path.join(targetRoot, artifactRootRelative);
|
|
1193
|
+
const tempRoot = path.join(artifactRoot, ".tmp");
|
|
1194
|
+
const runRoot = path.join(artifactRoot, runName);
|
|
1195
|
+
const stagingRunRoot = path.join(tempRoot, runName);
|
|
1196
|
+
ensureInside(targetRoot, artifactRoot, "documentation repair artifact root");
|
|
1197
|
+
ensureInside(targetRoot, runRoot, "documentation repair run root");
|
|
1198
|
+
ensureInside(targetRoot, stagingRunRoot, "documentation repair staging run root");
|
|
1199
|
+
|
|
1200
|
+
if (flags["dry-run"]) {
|
|
1201
|
+
console.log(`Run name: ${runName}`);
|
|
1202
|
+
console.log(`Audit report: ${auditReportPath}`);
|
|
1203
|
+
console.log(`Output root: ${runRoot}`);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (fs.existsSync(runRoot)) {
|
|
1208
|
+
throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
|
|
1209
|
+
}
|
|
1210
|
+
if (fs.existsSync(stagingRunRoot)) {
|
|
1211
|
+
throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no repair run is in progress, or use a different --run-name.`);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const runRootRelative = toPosix(path.join(artifactRootRelative, runName));
|
|
1215
|
+
const promptsRoot = path.join(stagingRunRoot, "prompts");
|
|
1216
|
+
const planningRoot = path.join(stagingRunRoot, "planning");
|
|
1217
|
+
const repairRoot = path.join(stagingRunRoot, "repair");
|
|
1218
|
+
const reviewRoot = path.join(stagingRunRoot, "review");
|
|
1219
|
+
const finalRoot = path.join(stagingRunRoot, "final");
|
|
1220
|
+
for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, planningRoot, repairRoot, reviewRoot, finalRoot]) {
|
|
1221
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const templateRoot = path.join(documentationRepairTemplateRoot(), "prompts");
|
|
1225
|
+
const planTemplate = fs.readFileSync(path.join(templateRoot, "repair-plan-prompt.md"), "utf8");
|
|
1226
|
+
const applyTemplate = fs.readFileSync(path.join(templateRoot, "repair-apply-prompt.md"), "utf8");
|
|
1227
|
+
const reviewTemplate = fs.readFileSync(path.join(templateRoot, "repair-review-prompt.md"), "utf8");
|
|
1228
|
+
const repairPlan = toPosix(path.join(runRootRelative, "planning", "documentation-repair-plan.md"));
|
|
1229
|
+
const humanDecisions = toPosix(path.join(runRootRelative, "planning", "human-decisions.md"));
|
|
1230
|
+
const repairLog = toPosix(path.join(runRootRelative, "repair", "repair-log.md"));
|
|
1231
|
+
const reviewOutput = toPosix(path.join(runRootRelative, "review", "repair-review.md"));
|
|
1232
|
+
const finalSummary = toPosix(path.join(runRootRelative, "final", "documentation-repair-summary.md"));
|
|
1233
|
+
const profile = readJsonIfExists(path.join(targetRoot, config.profilePath), "audit profile");
|
|
1234
|
+
const baseValues = {
|
|
1235
|
+
RUN_ID: runName,
|
|
1236
|
+
RUN_ROOT: runRootRelative,
|
|
1237
|
+
AUDIT_REPORT: auditReportPath,
|
|
1238
|
+
REPAIR_PLAN_OUTPUT: repairPlan,
|
|
1239
|
+
HUMAN_DECISIONS_OUTPUT: humanDecisions,
|
|
1240
|
+
REPAIR_LOG_OUTPUT: repairLog,
|
|
1241
|
+
REVIEW_OUTPUT: reviewOutput,
|
|
1242
|
+
FINAL_SUMMARY_OUTPUT: finalSummary,
|
|
1243
|
+
CORPUS_INCLUDE: markdownList(profile?.corpus?.include || ["*.md", "docs/**/*.md"]),
|
|
1244
|
+
CORPUS_EXCLUDE: markdownList(profile?.corpus?.exclude || ["node_modules/**", ".git/**", ".audit/**", ".opencode/**"])
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
fs.writeFileSync(path.join(promptsRoot, "plan-repair.md"), renderTemplate(planTemplate, baseValues), "utf8");
|
|
1248
|
+
fs.writeFileSync(path.join(promptsRoot, "apply-repair.md"), renderTemplate(applyTemplate, baseValues), "utf8");
|
|
1249
|
+
fs.writeFileSync(path.join(promptsRoot, "review-repair.md"), renderTemplate(reviewTemplate, baseValues), "utf8");
|
|
1250
|
+
|
|
1251
|
+
writeJson(path.join(stagingRunRoot, "manifest.json"), {
|
|
1252
|
+
version: 1,
|
|
1253
|
+
workflowId: DOCUMENTATION_REPAIR_WORKFLOW_ID,
|
|
1254
|
+
runId: runName,
|
|
1255
|
+
generatedAt: new Date().toISOString(),
|
|
1256
|
+
auditReport: auditReportPath,
|
|
1257
|
+
paths: {
|
|
1258
|
+
runRoot: runRootRelative,
|
|
1259
|
+
prompts: toPosix(path.join(runRootRelative, "prompts")),
|
|
1260
|
+
repairPlan,
|
|
1261
|
+
humanDecisions,
|
|
1262
|
+
repairLog,
|
|
1263
|
+
reviewOutput,
|
|
1264
|
+
finalSummary
|
|
1265
|
+
},
|
|
1266
|
+
prompts: {
|
|
1267
|
+
planRepair: toPosix(path.join(runRootRelative, "prompts", "plan-repair.md")),
|
|
1268
|
+
applyRepair: toPosix(path.join(runRootRelative, "prompts", "apply-repair.md")),
|
|
1269
|
+
reviewRepair: toPosix(path.join(runRootRelative, "prompts", "review-repair.md"))
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Documentation Repair Run\n\nRun: ${runName}\nAudit report: ${auditReportPath}\n\n## Execution Order\n\n1. Execute prompts/plan-repair.md.\n2. If planning records human decisions, pause until they are resolved.\n3. Execute prompts/apply-repair.md after approval.\n4. Execute prompts/review-repair.md after repair edits.\n5. Run a follow-up documentation audit.\n`, "utf8");
|
|
1274
|
+
|
|
1275
|
+
if (fs.existsSync(runRoot)) {
|
|
1276
|
+
throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
|
|
1277
|
+
}
|
|
1278
|
+
fs.renameSync(stagingRunRoot, runRoot);
|
|
1279
|
+
|
|
1280
|
+
console.log(`Created documentation repair run: ${runRoot}`);
|
|
1281
|
+
console.log(`Audit report: ${auditReportPath}`);
|
|
1282
|
+
console.log(`Next prompt: ${path.join(runRoot, "prompts", "plan-repair.md")}`);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function productSpecPath(specRoot, releaseId, relativePath) {
|
|
1286
|
+
return toPosix(path.join(specRoot, relativePath.replaceAll("<release-id>", releaseId)));
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function readTextIfExists(filePath) {
|
|
1290
|
+
if (!fs.existsSync(filePath)) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
return fs.readFileSync(filePath, "utf8");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function productRunRootFromValidationFlags(targetRoot, productConfig, flags = {}) {
|
|
1297
|
+
if (flags["run-id"] && flags["run-root"]) {
|
|
1298
|
+
throw new Error("Use either --run-id or --run-root, not both.");
|
|
1299
|
+
}
|
|
1300
|
+
if (flags["run-id"]) {
|
|
1301
|
+
assertSafeRunName(flags["run-id"]);
|
|
1302
|
+
return resolveInsideTarget(targetRoot, path.join(productConfig.runRoot, flags["run-id"]), "product-shaping run root");
|
|
1303
|
+
}
|
|
1304
|
+
if (flags["run-root"]) {
|
|
1305
|
+
return resolveInsideTarget(targetRoot, flags["run-root"], "product-shaping run root");
|
|
1306
|
+
}
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function hasPassingAdversarialReview(content) {
|
|
1311
|
+
return /(?:status|result):\s*pass/i.test(content) && /blocking findings:\s*(?:none|0)/i.test(content);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function hasPassingFinalValidation(content) {
|
|
1315
|
+
return /(?:status|result):\s*pass/i.test(content) && /ready for delivery implementation:\s*yes/i.test(content);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function escapeRegExp(value) {
|
|
1319
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function isInsideDirectory(parent, candidate) {
|
|
1323
|
+
const relative = path.relative(parent, candidate);
|
|
1324
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function deliverableSections(deliverables) {
|
|
1328
|
+
const headingPattern = /^##\s+(Deliverable\s+([A-Za-z0-9][A-Za-z0-9_-]*):\s+\S.*)$/gm;
|
|
1329
|
+
const headings = [];
|
|
1330
|
+
let match;
|
|
1331
|
+
while ((match = headingPattern.exec(deliverables)) !== null) {
|
|
1332
|
+
headings.push({ title: match[1].trim(), id: match[2], index: match.index });
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return headings.map((start, index) => {
|
|
1336
|
+
const next = headings[index + 1];
|
|
1337
|
+
const end = next ? next.index : deliverables.length;
|
|
1338
|
+
return {
|
|
1339
|
+
title: start.title,
|
|
1340
|
+
id: start.id,
|
|
1341
|
+
index: start.index,
|
|
1342
|
+
end,
|
|
1343
|
+
body: deliverables.slice(start.index, end)
|
|
1344
|
+
};
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function isProductDeliverablesHandoff(value) {
|
|
1349
|
+
return /(?:^|\/)releases\/[^/]+\/DELIVERABLES\.md$/i.test(value) || /(?:^|\/)DELIVERABLES\.md$/i.test(value);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function assertConfiguredProductHandoff(workUnitsDocument, expectedHandoff) {
|
|
1353
|
+
if (workUnitsDocument === expectedHandoff) {
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
if (isProductDeliverablesHandoff(workUnitsDocument)) {
|
|
1357
|
+
throw new Error(`Product-shaping handoff must use the configured DELIVERABLES.md path '${expectedHandoff}', but got '${workUnitsDocument}'.`);
|
|
1358
|
+
}
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function validateDeliverableFieldCoverage(deliverables, readyStatuses, errors) {
|
|
1363
|
+
const allowed = new Set(["candidate", "ready", "blocked", "deferred", "done"]);
|
|
1364
|
+
const requiredLabels = [
|
|
1365
|
+
"Goal",
|
|
1366
|
+
"Scope",
|
|
1367
|
+
"Out of scope",
|
|
1368
|
+
"Dependencies",
|
|
1369
|
+
"Source docs",
|
|
1370
|
+
"Acceptance criteria",
|
|
1371
|
+
"Risk areas",
|
|
1372
|
+
"Human gate triggers",
|
|
1373
|
+
"Expected verification"
|
|
1374
|
+
];
|
|
1375
|
+
const sections = deliverableSections(deliverables);
|
|
1376
|
+
const allStatuses = [...deliverables.matchAll(/^Status:\s*([A-Za-z-]+)/gim)];
|
|
1377
|
+
if (sections.length === 0) {
|
|
1378
|
+
if (allStatuses.length > 0) {
|
|
1379
|
+
errors.push("DELIVERABLES.md contains Status lines but no deliverable sections with stable ids. Use headings like '## Deliverable 00: <title>'.");
|
|
1380
|
+
} else {
|
|
1381
|
+
errors.push("DELIVERABLES.md contains no deliverable sections with stable ids. Use headings like '## Deliverable 00: <title>'.");
|
|
1382
|
+
}
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const seenIds = new Set();
|
|
1386
|
+
for (const section of sections) {
|
|
1387
|
+
const normalizedId = section.id.toLowerCase();
|
|
1388
|
+
if (seenIds.has(normalizedId)) {
|
|
1389
|
+
errors.push(`DELIVERABLES.md deliverable id '${section.id}' must be unique.`);
|
|
1390
|
+
}
|
|
1391
|
+
seenIds.add(normalizedId);
|
|
1392
|
+
}
|
|
1393
|
+
for (const status of allStatuses) {
|
|
1394
|
+
const inSection = sections.some((section) => status.index >= section.index && status.index < section.end);
|
|
1395
|
+
if (!inSection) {
|
|
1396
|
+
errors.push("DELIVERABLES.md contains a Status line outside a deliverable section with a stable id.");
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
sections.forEach((section, index) => {
|
|
1400
|
+
const title = section.title || `deliverable ${index + 1}`;
|
|
1401
|
+
const statuses = [...section.body.matchAll(/^Status:\s*([A-Za-z-]+)/gim)];
|
|
1402
|
+
if (statuses.length !== 1) {
|
|
1403
|
+
errors.push(`DELIVERABLES.md deliverable '${title}' must contain exactly one Status line.`);
|
|
1404
|
+
} else {
|
|
1405
|
+
const status = statuses[0][1].toLowerCase();
|
|
1406
|
+
if (!allowed.has(status)) {
|
|
1407
|
+
errors.push(`DELIVERABLES.md deliverable '${title}' contains invalid status '${status}'.`);
|
|
1408
|
+
} else if (!readyStatuses.has(status)) {
|
|
1409
|
+
errors.push(`DELIVERABLES.md deliverable '${title}' contains non-ready status '${status}'. Ready statuses: ${[...readyStatuses].join(", ")}.`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
for (const label of requiredLabels) {
|
|
1413
|
+
const labelPattern = new RegExp(`^${escapeRegExp(label)}:\\s*\\S`, "im");
|
|
1414
|
+
const headingPattern = new RegExp(`^#{2,6}\\s+${escapeRegExp(label)}\\s*$`, "im");
|
|
1415
|
+
if (!labelPattern.test(section.body) && !headingPattern.test(section.body)) {
|
|
1416
|
+
errors.push(`DELIVERABLES.md deliverable '${title}' is missing required field '${label}:'.`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function hasUnresolvedReleaseBlockingQuestion(openQuestions) {
|
|
1423
|
+
const blockStarts = [...openQuestions.matchAll(/^(?:##\s+.+|Question id:\s*.+)$/gim)].map((match) => match.index);
|
|
1424
|
+
const blocks = blockStarts.length > 0
|
|
1425
|
+
? blockStarts.map((start, index) => openQuestions.slice(start, blockStarts[index + 1] || openQuestions.length))
|
|
1426
|
+
: [openQuestions];
|
|
1427
|
+
return blocks.some((block) => /blocks target release:\s*yes/i.test(block) && /status:\s*(?:open|deferred)/i.test(block));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function validateReferenceCollection(specRoot, errors) {
|
|
1431
|
+
const referencesRoot = path.join(specRoot, "references");
|
|
1432
|
+
const referencesIndexPath = path.join(referencesRoot, "README.md");
|
|
1433
|
+
const referencesIndex = readTextIfExists(referencesIndexPath);
|
|
1434
|
+
if (!referencesIndex) {
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const referenceFiles = fs.existsSync(referencesRoot)
|
|
1439
|
+
? fs.readdirSync(referencesRoot, { withFileTypes: true })
|
|
1440
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
|
|
1441
|
+
.map((entry) => entry.name)
|
|
1442
|
+
: [];
|
|
1443
|
+
const listedReferences = [...referencesIndex.matchAll(/\breferences\/(?!README\.md\b)[A-Za-z0-9_.-]+\.md\b/gi)]
|
|
1444
|
+
.map((match) => toPosix(match[0]).replace(/^\.\//, ""));
|
|
1445
|
+
const listedReferenceSet = new Set(listedReferences.map((reference) => reference.toLowerCase()));
|
|
1446
|
+
|
|
1447
|
+
if (referenceFiles.length === 0 && !/no (?:external )?references (?:used|researched|required)|zero references/i.test(referencesIndex)) {
|
|
1448
|
+
errors.push("references/README.md must explicitly state when no individual reference files are used.");
|
|
1449
|
+
}
|
|
1450
|
+
for (const listedReference of listedReferences) {
|
|
1451
|
+
const fullPath = path.join(specRoot, listedReference);
|
|
1452
|
+
ensureInside(specRoot, fullPath, `listed reference ${listedReference}`);
|
|
1453
|
+
if (!fs.existsSync(fullPath)) {
|
|
1454
|
+
errors.push(`references/README.md lists missing reference file '${listedReference}'.`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
for (const referenceFile of referenceFiles) {
|
|
1458
|
+
const relativePath = `references/${referenceFile}`;
|
|
1459
|
+
if (!listedReferenceSet.has(relativePath.toLowerCase())) {
|
|
1460
|
+
errors.push(`references/README.md must list individual reference file '${relativePath}'.`);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function validateNoTaskLevelImplementationDetail(deliverables, errors) {
|
|
1466
|
+
const blockedPatterns = [
|
|
1467
|
+
/task-specs\//i,
|
|
1468
|
+
/(?:^|\n)#{1,6}\s+Task\b/i,
|
|
1469
|
+
/(?:^|\n)#\s+T\d{2}(?:[-:]|\b)/i,
|
|
1470
|
+
/(?:^|\n)#{1,6}\s+T\d{2}[-:]\d+/i,
|
|
1471
|
+
/(?:^|\n)```(?:js|javascript|ts|typescript|tsx|jsx|python|py|sql|bash|sh)\b/i,
|
|
1472
|
+
/\b(?:implementation|task-logs|task-reviews)\//i,
|
|
1473
|
+
/\bRed-Green-Refactor\b/i,
|
|
1474
|
+
/\bwrite (?:a )?failing test\b/i,
|
|
1475
|
+
/\bmake the test pass\b/i
|
|
1476
|
+
];
|
|
1477
|
+
if (blockedPatterns.some((pattern) => pattern.test(deliverables))) {
|
|
1478
|
+
errors.push("DELIVERABLES.md appears to contain task-level implementation detail.");
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function validateRunEvidencePath(targetRoot, runRoot, actualRelative, expectedRelative, label, errors) {
|
|
1483
|
+
if (!actualRelative) {
|
|
1484
|
+
errors.push(`Missing ${label} path in product-shaping run manifest.`);
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
const actualPath = resolveInsideTarget(targetRoot, actualRelative, label);
|
|
1488
|
+
const expectedPath = path.join(runRoot, expectedRelative);
|
|
1489
|
+
if (!isInsideDirectory(runRoot, actualPath) || path.resolve(actualPath) !== path.resolve(expectedPath)) {
|
|
1490
|
+
errors.push(`${label} must stay inside the selected product-shaping run at ${toDisplayPath(targetRoot, expectedPath)}.`);
|
|
1491
|
+
return null;
|
|
1492
|
+
}
|
|
1493
|
+
return actualPath;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function validateProductSpecs(targetRoot, productConfig, releaseId, flags = {}) {
|
|
1497
|
+
const errors = [];
|
|
1498
|
+
const warnings = [];
|
|
1499
|
+
const specRootRelative = normalizeRelativePath(productConfig.specRoot, "Product-shaping config.specRoot");
|
|
1500
|
+
const specRoot = path.join(targetRoot, specRootRelative);
|
|
1501
|
+
ensureInside(targetRoot, specRoot, "product-shaping spec root");
|
|
1502
|
+
|
|
1503
|
+
for (const file of productConfig.requiredFiles) {
|
|
1504
|
+
const relativePath = file.replaceAll("<release-id>", releaseId);
|
|
1505
|
+
const fullPath = path.join(specRoot, relativePath);
|
|
1506
|
+
ensureInside(targetRoot, fullPath, `product spec ${relativePath}`);
|
|
1507
|
+
if (!fs.existsSync(fullPath)) {
|
|
1508
|
+
errors.push(`Missing required product spec: ${toPosix(path.join(specRootRelative, relativePath))}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
validateReferenceCollection(specRoot, errors);
|
|
1512
|
+
|
|
1513
|
+
const openQuestionsPath = path.join(specRoot, "discovery/OPEN_QUESTIONS.md");
|
|
1514
|
+
const openQuestions = readTextIfExists(openQuestionsPath);
|
|
1515
|
+
if (openQuestions && hasUnresolvedReleaseBlockingQuestion(openQuestions)) {
|
|
1516
|
+
errors.push("OPEN_QUESTIONS.md contains an unresolved release-blocking question.");
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const deliverablesPath = path.join(specRoot, "releases", releaseId, "DELIVERABLES.md");
|
|
1520
|
+
const deliverables = readTextIfExists(deliverablesPath);
|
|
1521
|
+
if (deliverables) {
|
|
1522
|
+
const readyStatuses = new Set(productConfig.completionGate.readyDeliverableStatuses.map((status) => status.toLowerCase()));
|
|
1523
|
+
validateDeliverableFieldCoverage(deliverables, readyStatuses, errors);
|
|
1524
|
+
validateNoTaskLevelImplementationDetail(deliverables, errors);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (productConfig.completionGate.requireAdversarialReview || productConfig.completionGate.requireFinalValidation) {
|
|
1528
|
+
const runRoot = productRunRootFromValidationFlags(targetRoot, productConfig, flags);
|
|
1529
|
+
if (!runRoot) {
|
|
1530
|
+
errors.push("Product-shaping validation requires --run-id or --run-root to verify adversarial review and final validation evidence.");
|
|
1531
|
+
} else {
|
|
1532
|
+
const manifestPath = path.join(runRoot, "manifest.json");
|
|
1533
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1534
|
+
errors.push(`Missing product-shaping run manifest: ${toDisplayPath(targetRoot, manifestPath)}.`);
|
|
1535
|
+
} else {
|
|
1536
|
+
const manifest = readJson(manifestPath, "product-shaping run manifest");
|
|
1537
|
+
if (manifest.workflowId !== PRODUCT_SHAPING_WORKFLOW_ID) {
|
|
1538
|
+
errors.push(`Run manifest workflowId must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
|
|
1539
|
+
}
|
|
1540
|
+
if (manifest.releaseId !== releaseId) {
|
|
1541
|
+
errors.push(`Run manifest releaseId '${manifest.releaseId}' does not match requested release '${releaseId}'.`);
|
|
1542
|
+
}
|
|
1543
|
+
if (manifest.paths?.runRoot) {
|
|
1544
|
+
const manifestRunRoot = resolveInsideTarget(targetRoot, manifest.paths.runRoot, "product-shaping manifest run root");
|
|
1545
|
+
if (path.resolve(manifestRunRoot) !== path.resolve(runRoot)) {
|
|
1546
|
+
errors.push("Run manifest paths.runRoot does not match the selected product-shaping run root.");
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const outputs = manifest.outputs || {};
|
|
1550
|
+
if (productConfig.completionGate.requireAdversarialReview) {
|
|
1551
|
+
const reviewPath = validateRunEvidencePath(targetRoot, runRoot, outputs.adversarialReview, path.join("review", "adversarial-review.md"), "adversarial review evidence", errors);
|
|
1552
|
+
if (!reviewPath || !fs.existsSync(reviewPath)) {
|
|
1553
|
+
errors.push("Missing adversarial review evidence for product-shaping completion gate.");
|
|
1554
|
+
} else if (!hasPassingAdversarialReview(fs.readFileSync(reviewPath, "utf8"))) {
|
|
1555
|
+
errors.push("Adversarial review evidence must include 'Status: pass' or 'Result: pass' and 'Blocking findings: none'.");
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (productConfig.completionGate.requireFinalValidation) {
|
|
1559
|
+
const finalValidationPath = validateRunEvidencePath(targetRoot, runRoot, outputs.finalValidation, path.join("final", "product-shaping-validation.md"), "final validation evidence", errors);
|
|
1560
|
+
if (!finalValidationPath || !fs.existsSync(finalValidationPath)) {
|
|
1561
|
+
errors.push("Missing final validation evidence for product-shaping completion gate.");
|
|
1562
|
+
} else if (!hasPassingFinalValidation(fs.readFileSync(finalValidationPath, "utf8"))) {
|
|
1563
|
+
errors.push("Final validation evidence must include 'Status: pass' or 'Result: pass' and 'Ready for delivery implementation: yes'.");
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
return { errors, warnings };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function commandProductValidate(flags) {
|
|
1574
|
+
const targetRoot = resolveTarget(flags);
|
|
1575
|
+
const wefterConfig = readConfig(targetRoot);
|
|
1576
|
+
assertWorkflowEnabled(wefterConfig, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
1577
|
+
const configPath = productShapingConfigPath(wefterConfig, flags);
|
|
1578
|
+
const productConfig = readJson(path.join(targetRoot, configPath), "product-shaping config");
|
|
1579
|
+
validateProductShapingConfig(productConfig);
|
|
1580
|
+
|
|
1581
|
+
const releaseId = flags["release-id"] || productConfig.releaseId;
|
|
1582
|
+
assertSafeRunName(releaseId);
|
|
1583
|
+
const result = validateProductSpecs(targetRoot, productConfig, releaseId, flags);
|
|
1584
|
+
const ok = result.errors.length === 0;
|
|
1585
|
+
|
|
1586
|
+
if (flags.json) {
|
|
1587
|
+
console.log(JSON.stringify({ ok, releaseId, errors: result.errors, warnings: result.warnings }, null, 2));
|
|
1588
|
+
} else {
|
|
1589
|
+
console.log(`Product shaping validation: ${ok ? "pass" : "fail"}`);
|
|
1590
|
+
console.log(`Release: ${releaseId}`);
|
|
1591
|
+
for (const warning of result.warnings) {
|
|
1592
|
+
console.log(`WARNING ${warning}`);
|
|
1593
|
+
}
|
|
1594
|
+
for (const error of result.errors) {
|
|
1595
|
+
console.error(`ERROR ${error}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
if (!ok) {
|
|
1600
|
+
process.exitCode = 1;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function commandProductShape(flags) {
|
|
1605
|
+
const targetRoot = resolveTarget(flags);
|
|
1606
|
+
const wefterConfig = readConfig(targetRoot);
|
|
1607
|
+
assertWorkflowEnabled(wefterConfig, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
1608
|
+
const configPath = productShapingConfigPath(wefterConfig, flags);
|
|
1609
|
+
const profilePath = productShapingProfilePath(wefterConfig, flags);
|
|
1610
|
+
const productConfig = readJson(path.join(targetRoot, configPath), "product-shaping config");
|
|
1611
|
+
const productProfile = readJson(path.join(targetRoot, profilePath), "product-shaping profile");
|
|
1612
|
+
validateProductShapingConfig(productConfig);
|
|
1613
|
+
validateProductShapingProfile(productProfile);
|
|
1614
|
+
|
|
1615
|
+
const releaseId = flags["release-id"] || productConfig.releaseId;
|
|
1616
|
+
assertSafeRunName(releaseId);
|
|
1617
|
+
const specRootRelative = normalizeRelativePath(flags["spec-root"] || productConfig.specRoot, "product-shaping spec root");
|
|
1618
|
+
const runArtifactsRootRelative = normalizeRelativePath(flags["run-root"] || productConfig.runRoot, "product-shaping run root");
|
|
1619
|
+
const runName = flags["run-name"] || `${timestampRunName()}__${releaseId}`;
|
|
1620
|
+
assertSafeRunName(runName);
|
|
1621
|
+
|
|
1622
|
+
const specRoot = path.join(targetRoot, specRootRelative);
|
|
1623
|
+
const artifactRoot = path.join(targetRoot, runArtifactsRootRelative);
|
|
1624
|
+
const tempRoot = path.join(artifactRoot, ".tmp");
|
|
1625
|
+
const runRoot = path.join(artifactRoot, runName);
|
|
1626
|
+
const stagingRunRoot = path.join(tempRoot, runName);
|
|
1627
|
+
ensureInside(targetRoot, specRoot, "product-shaping spec root");
|
|
1628
|
+
ensureInside(targetRoot, artifactRoot, "product-shaping run root");
|
|
1629
|
+
ensureInside(targetRoot, runRoot, "product-shaping run");
|
|
1630
|
+
ensureInside(targetRoot, stagingRunRoot, "product-shaping staging run");
|
|
1631
|
+
|
|
1632
|
+
const requiredFiles = productConfig.requiredFiles.map((file) => ({
|
|
1633
|
+
templatePath: file,
|
|
1634
|
+
path: file.replaceAll("<release-id>", releaseId),
|
|
1635
|
+
fullPath: productSpecPath(specRootRelative, releaseId, file)
|
|
1636
|
+
}));
|
|
1637
|
+
|
|
1638
|
+
if (flags["dry-run"]) {
|
|
1639
|
+
console.log(`Run name: ${runName}`);
|
|
1640
|
+
console.log(`Release: ${releaseId}`);
|
|
1641
|
+
console.log(`Spec root: ${specRootRelative}`);
|
|
1642
|
+
console.log(`Output root: ${toPosix(path.join(runArtifactsRootRelative, runName))}`);
|
|
1643
|
+
console.log(`Required product spec files: ${requiredFiles.length}`);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (fs.existsSync(runRoot)) {
|
|
1648
|
+
throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
|
|
1649
|
+
}
|
|
1650
|
+
if (fs.existsSync(stagingRunRoot)) {
|
|
1651
|
+
throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no product-shaping run is in progress, or use a different --run-name.`);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const runRootRelative = toPosix(path.join(runArtifactsRootRelative, runName));
|
|
1655
|
+
const adversarialReviewPath = toPosix(path.join(runRootRelative, "review", "adversarial-review.md"));
|
|
1656
|
+
const finalValidationPath = toPosix(path.join(runRootRelative, "final", "product-shaping-validation.md"));
|
|
1657
|
+
const handoffDeliverablesPath = productSpecPath(specRootRelative, releaseId, "releases/<release-id>/DELIVERABLES.md");
|
|
1658
|
+
const promptsRoot = path.join(stagingRunRoot, "prompts");
|
|
1659
|
+
const draftRoot = path.join(stagingRunRoot, "draft");
|
|
1660
|
+
const reviewRoot = path.join(stagingRunRoot, "review");
|
|
1661
|
+
const validationRoot = path.join(stagingRunRoot, "validation");
|
|
1662
|
+
const finalRoot = path.join(stagingRunRoot, "final");
|
|
1663
|
+
for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, draftRoot, reviewRoot, validationRoot, finalRoot]) {
|
|
1664
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
const promptTemplateRoot = path.join(targetRoot, wefterConfig.workflowRoot, PRODUCT_SHAPING_WORKFLOW_ID, "templates", "prompts");
|
|
1668
|
+
const promptValues = {
|
|
1669
|
+
RUN_ID: runName,
|
|
1670
|
+
RELEASE_ID: releaseId,
|
|
1671
|
+
CONFIG_PATH: configPath,
|
|
1672
|
+
PROFILE_PATH: profilePath,
|
|
1673
|
+
CONTRACT_PATH: productConfig.contractPath,
|
|
1674
|
+
PROCESS_DOC_PATH: productConfig.processDocPath,
|
|
1675
|
+
SPEC_ROOT: specRootRelative,
|
|
1676
|
+
RUN_ROOT: runRootRelative,
|
|
1677
|
+
REQUIRED_FILES: markdownList(requiredFiles.map((file) => file.fullPath)),
|
|
1678
|
+
DELIVERABLES_PATH: handoffDeliverablesPath,
|
|
1679
|
+
ADVERSARIAL_REVIEW_PATH: adversarialReviewPath,
|
|
1680
|
+
FINAL_VALIDATION_PATH: finalValidationPath,
|
|
1681
|
+
SCOPE_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/SCOPE.md"),
|
|
1682
|
+
DOMAIN_SPEC_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/DOMAIN_SPEC.md"),
|
|
1683
|
+
ACCEPTANCE_CRITERIA_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/ACCEPTANCE_CRITERIA.md"),
|
|
1684
|
+
OPEN_QUESTIONS_PATH: productSpecPath(specRootRelative, releaseId, "discovery/OPEN_QUESTIONS.md"),
|
|
1685
|
+
PRODUCT_DECISIONS_PATH: productSpecPath(specRootRelative, releaseId, "product/PRODUCT_DECISIONS.md")
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
const promptRecords = [];
|
|
1689
|
+
for (const file of PRODUCT_SHAPING_PROMPT_FILES) {
|
|
1690
|
+
const source = path.join(promptTemplateRoot, file);
|
|
1691
|
+
const template = readTextRequired(source);
|
|
1692
|
+
const rendered = renderTemplate(template, promptValues);
|
|
1693
|
+
const destination = path.join(promptsRoot, file);
|
|
1694
|
+
assertNoPlaceholders(destination, rendered);
|
|
1695
|
+
fs.writeFileSync(destination, rendered, "utf8");
|
|
1696
|
+
promptRecords.push(toPosix(path.join(runRootRelative, "prompts", file)));
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
writeJson(path.join(stagingRunRoot, "manifest.json"), {
|
|
1700
|
+
version: 1,
|
|
1701
|
+
workflowId: PRODUCT_SHAPING_WORKFLOW_ID,
|
|
1702
|
+
runId: runName,
|
|
1703
|
+
releaseId,
|
|
1704
|
+
generatedAt: new Date().toISOString(),
|
|
1705
|
+
configPath,
|
|
1706
|
+
profilePath,
|
|
1707
|
+
contractPath: productConfig.contractPath,
|
|
1708
|
+
processDocPath: productConfig.processDocPath,
|
|
1709
|
+
specRoot: specRootRelative,
|
|
1710
|
+
counts: {
|
|
1711
|
+
requiredFiles: requiredFiles.length,
|
|
1712
|
+
variants: productProfile.variants.length,
|
|
1713
|
+
lenses: productProfile.lenses.length
|
|
1714
|
+
},
|
|
1715
|
+
paths: {
|
|
1716
|
+
runRoot: runRootRelative,
|
|
1717
|
+
prompts: toPosix(path.join(runRootRelative, "prompts")),
|
|
1718
|
+
draft: toPosix(path.join(runRootRelative, "draft")),
|
|
1719
|
+
review: toPosix(path.join(runRootRelative, "review")),
|
|
1720
|
+
validation: toPosix(path.join(runRootRelative, "validation")),
|
|
1721
|
+
final: toPosix(path.join(runRootRelative, "final")),
|
|
1722
|
+
specRoot: specRootRelative,
|
|
1723
|
+
releaseRoot: toPosix(path.join(specRootRelative, "releases", releaseId))
|
|
1724
|
+
},
|
|
1725
|
+
outputs: {
|
|
1726
|
+
adversarialReview: adversarialReviewPath,
|
|
1727
|
+
finalValidation: finalValidationPath
|
|
1728
|
+
},
|
|
1729
|
+
handoff: {
|
|
1730
|
+
deliverables: handoffDeliverablesPath
|
|
1731
|
+
},
|
|
1732
|
+
gate: {
|
|
1733
|
+
status: "pending",
|
|
1734
|
+
requireNoReleaseBlockingQuestions: productConfig.completionGate.requireNoReleaseBlockingQuestions,
|
|
1735
|
+
requireAdversarialReview: productConfig.completionGate.requireAdversarialReview,
|
|
1736
|
+
requireFinalValidation: productConfig.completionGate.requireFinalValidation,
|
|
1737
|
+
readyDeliverableStatuses: productConfig.completionGate.readyDeliverableStatuses
|
|
1738
|
+
},
|
|
1739
|
+
prompts: promptRecords,
|
|
1740
|
+
requiredFiles
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Product Shaping Run\n\nRun: ${runName}\nRelease: ${releaseId}\n\n## Roots\n\n- Specs: ${specRootRelative}\n- Run: ${runRootRelative}\n- Config: ${configPath}\n- Profile: ${profilePath}\n- Contract: ${productConfig.contractPath}\n\n## Execution Order\n\n1. Read ${productConfig.processDocPath}.\n2. Read ${productConfig.contractPath}.\n3. Create or repair product specs under ${specRootRelative}.\n4. Keep runtime notes and draft artifacts in this run directory.\n5. Write adversarial review evidence to ${adversarialReviewPath}.\n6. Write final validation evidence to ${finalValidationPath}.\n7. Validate that release-blocking questions, scope, domain spec, acceptance criteria and deliverables satisfy the completion gate.\n8. Hand off only ${handoffDeliverablesPath} to delivery implementation.\n\n## Passing Evidence Format\n\n- Adversarial review must include \`Status: pass\` or \`Result: pass\` and \`Blocking findings: none\`.\n- Final validation must include \`Status: pass\` or \`Result: pass\` and \`Ready for delivery implementation: yes\`.\n\n## Required Product Spec Files\n\n${requiredFiles.map((file) => `- ${file.fullPath}`).join("\n")}\n`, "utf8");
|
|
1744
|
+
|
|
1745
|
+
if (fs.existsSync(runRoot)) {
|
|
1746
|
+
throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
|
|
1747
|
+
}
|
|
1748
|
+
fs.renameSync(stagingRunRoot, runRoot);
|
|
1749
|
+
|
|
1750
|
+
console.log(`Created product-shaping run: ${runRoot}`);
|
|
1751
|
+
console.log(`Release: ${releaseId}`);
|
|
1752
|
+
console.log(`Spec root: ${specRootRelative}`);
|
|
1753
|
+
console.log(`Required product spec files: ${requiredFiles.length}`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function productValidationFlagsFromWorkUnitFlags(flags) {
|
|
1757
|
+
const validationFlags = {};
|
|
1758
|
+
if (flags["product-run-id"] && flags["product-run-root"]) {
|
|
1759
|
+
throw new Error("Use either --product-run-id or --product-run-root, not both.");
|
|
1760
|
+
}
|
|
1761
|
+
if (flags["product-run-id"]) {
|
|
1762
|
+
validationFlags["run-id"] = flags["product-run-id"];
|
|
1763
|
+
}
|
|
1764
|
+
if (flags["product-run-root"]) {
|
|
1765
|
+
validationFlags["run-root"] = flags["product-run-root"];
|
|
1766
|
+
}
|
|
1767
|
+
return validationFlags;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function enforceProductHandoffGate(targetRoot, wefterConfig, workUnitConfig, flags) {
|
|
1771
|
+
if (!isProductDeliverablesHandoff(workUnitConfig.workUnitsDocument)) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const productConfigPath = productShapingConfigPath(wefterConfig);
|
|
1776
|
+
const productConfig = readJson(path.join(targetRoot, productConfigPath), "product-shaping config");
|
|
1777
|
+
validateProductShapingConfig(productConfig);
|
|
1778
|
+
const expectedHandoff = productSpecPath(productConfig.specRoot, workUnitConfig.releaseId, "releases/<release-id>/DELIVERABLES.md");
|
|
1779
|
+
if (!assertConfiguredProductHandoff(workUnitConfig.workUnitsDocument, expectedHandoff)) {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (!flags["product-run-id"] && !flags["product-run-root"]) {
|
|
1784
|
+
throw new Error(`Using product-shaping DELIVERABLES.md as a work-unit handoff requires --product-run-id or --product-run-root so the product completion gate can be verified.`);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
const result = validateProductSpecs(targetRoot, productConfig, workUnitConfig.releaseId, productValidationFlagsFromWorkUnitFlags(flags));
|
|
1788
|
+
if (result.errors.length > 0) {
|
|
1789
|
+
throw new Error(`Product-shaping handoff is not valid for delivery implementation:\n- ${result.errors.join("\n- ")}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function commandWorkUnitRun(flags) {
|
|
1794
|
+
const targetRoot = resolveTarget(flags);
|
|
1795
|
+
const wefterConfig = readConfig(targetRoot);
|
|
1796
|
+
const configPath = workUnitConfigPath(wefterConfig, flags);
|
|
1797
|
+
const profilePath = workUnitProfilePath(wefterConfig, flags);
|
|
1798
|
+
const workUnitConfig = readJson(path.join(targetRoot, configPath), "work-unit config");
|
|
1799
|
+
if (flags["work-units-document"]) {
|
|
1800
|
+
workUnitConfig.workUnitsDocument = normalizeRelativePath(flags["work-units-document"], "work-units document override");
|
|
1801
|
+
}
|
|
1802
|
+
if (flags["release-id"]) {
|
|
1803
|
+
workUnitConfig.releaseId = requireString(flags["release-id"], "release id override");
|
|
1804
|
+
}
|
|
1805
|
+
const profile = readJson(path.join(targetRoot, profilePath), "work-unit profile");
|
|
1806
|
+
validateWorkUnitConfig(workUnitConfig);
|
|
1807
|
+
validateWorkUnitProfile(profile);
|
|
1808
|
+
enforceProductHandoffGate(targetRoot, wefterConfig, workUnitConfig, flags);
|
|
1809
|
+
|
|
1810
|
+
const workUnitId = flags["work-unit-id"] || workUnitConfig.defaultWorkUnitId;
|
|
1811
|
+
const workUnitKey = getSafeWorkUnitKey(workUnitId);
|
|
1812
|
+
const passesPerLens = Number.parseInt(flags["passes-per-lens"] || String(workUnitConfig.defaultPlanAuditPassesPerLens), 10);
|
|
1813
|
+
const maxAudits = Number.parseInt(flags["max-audits"] || "0", 10);
|
|
1814
|
+
if (!Number.isInteger(passesPerLens) || passesPerLens < 1) {
|
|
1815
|
+
throw new Error("--passes-per-lens must be an integer greater than 0.");
|
|
1816
|
+
}
|
|
1817
|
+
if (!Number.isInteger(maxAudits) || maxAudits < 0) {
|
|
1818
|
+
throw new Error("--max-audits must be an integer greater than or equal to 0.");
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const runName = flags["run-name"] || `${timestampRunName()}__${workUnitKey}`;
|
|
1822
|
+
assertSafeRunName(runName);
|
|
1823
|
+
const combinations = buildCombinations(profile, passesPerLens, maxAudits).map((combo, index) => ({
|
|
1824
|
+
...combo,
|
|
1825
|
+
auditId: `P${String(index + 1).padStart(4, "0")}__${combo.lens.id}__${combo.variant.id}__p${String(combo.pass).padStart(2, "0")}`
|
|
1826
|
+
}));
|
|
1827
|
+
|
|
1828
|
+
const artifactRoot = path.join(targetRoot, workUnitConfig.runArtifactsRoot);
|
|
1829
|
+
const tempRoot = path.join(artifactRoot, ".tmp");
|
|
1830
|
+
const runRoot = path.join(artifactRoot, runName);
|
|
1831
|
+
const stagingRunRoot = path.join(tempRoot, runName);
|
|
1832
|
+
ensureInside(targetRoot, artifactRoot, "work-unit runArtifactsRoot");
|
|
1833
|
+
ensureInside(targetRoot, runRoot, "work-unit runRoot");
|
|
1834
|
+
ensureInside(targetRoot, stagingRunRoot, "work-unit stagingRunRoot");
|
|
1835
|
+
|
|
1836
|
+
const runRootRelative = toPosix(path.join(workUnitConfig.runArtifactsRoot, runName));
|
|
1837
|
+
const versionedWorkUnitDir = toPosix(path.join(workUnitConfig.versionedArtifacts.executionRoot, workUnitKey));
|
|
1838
|
+
const versionedTaskSpecsDir = toPosix(path.join(versionedWorkUnitDir, "task-specs"));
|
|
1839
|
+
const versionedDecisionLog = toPosix(path.join(workUnitConfig.versionedArtifacts.decisionLogRoot, `${workUnitKey}-decisions.md`));
|
|
1840
|
+
|
|
1841
|
+
if (flags["dry-run"]) {
|
|
1842
|
+
console.log(`Run name: ${runName}`);
|
|
1843
|
+
console.log(`Work unit: ${workUnitKey}`);
|
|
1844
|
+
console.log(`Lenses: ${profile.lenses.length}`);
|
|
1845
|
+
console.log(`Variants: ${profile.variants.length}`);
|
|
1846
|
+
console.log(`Passes per lens/variant: ${passesPerLens}`);
|
|
1847
|
+
console.log(`Plan auditor prompts to generate: ${combinations.length}`);
|
|
1848
|
+
console.log(`Output root: ${runRoot}`);
|
|
1849
|
+
console.log(`Versioned work-unit dir: ${versionedWorkUnitDir}`);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
if (fs.existsSync(runRoot)) {
|
|
1854
|
+
throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
|
|
1855
|
+
}
|
|
1856
|
+
if (fs.existsSync(stagingRunRoot)) {
|
|
1857
|
+
throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no run is in progress, or use a different --run-name.`);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const promptsRoot = path.join(stagingRunRoot, "prompts");
|
|
1861
|
+
const planAuditorPromptsRoot = path.join(promptsRoot, "plan-auditors", workUnitKey);
|
|
1862
|
+
const planningRoot = path.join(stagingRunRoot, "planning");
|
|
1863
|
+
const draftRoot = path.join(planningRoot, "draft");
|
|
1864
|
+
const draftTaskSpecsRoot = path.join(draftRoot, "task-specs");
|
|
1865
|
+
const finalRoot = path.join(stagingRunRoot, "final");
|
|
1866
|
+
const candidateRoot = path.join(finalRoot, "approved-artifacts");
|
|
1867
|
+
const candidateWorkUnitRoot = path.join(candidateRoot, workUnitKey);
|
|
1868
|
+
const candidateTaskSpecsRoot = path.join(candidateWorkUnitRoot, "task-specs");
|
|
1869
|
+
const rawPlanAuditsRoot = path.join(stagingRunRoot, "raw", "plan-audits");
|
|
1870
|
+
const consolidationRoot = path.join(stagingRunRoot, "consolidation");
|
|
1871
|
+
const validationRoot = path.join(stagingRunRoot, "validation");
|
|
1872
|
+
const implementationRoot = path.join(stagingRunRoot, "implementation");
|
|
1873
|
+
const taskLogRoot = path.join(implementationRoot, "task-logs");
|
|
1874
|
+
const taskReviewRoot = path.join(implementationRoot, "task-reviews");
|
|
1875
|
+
for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, planAuditorPromptsRoot, planningRoot, draftRoot, draftTaskSpecsRoot, finalRoot, candidateRoot, candidateWorkUnitRoot, candidateTaskSpecsRoot, rawPlanAuditsRoot, consolidationRoot, validationRoot, implementationRoot, taskLogRoot, taskReviewRoot]) {
|
|
1876
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const templateRoot = path.join(targetRoot, wefterConfig.workflowRoot, WORK_UNIT_WORKFLOW_ID, "templates", "prompts");
|
|
1880
|
+
const templates = {
|
|
1881
|
+
planner: fs.readFileSync(path.join(templateRoot, "planner-prompt.md"), "utf8"),
|
|
1882
|
+
planAuditor: fs.readFileSync(path.join(templateRoot, "plan-auditor-prompt.md"), "utf8"),
|
|
1883
|
+
consolidator: fs.readFileSync(path.join(templateRoot, "plan-consolidator-prompt.md"), "utf8"),
|
|
1884
|
+
validator: fs.readFileSync(path.join(templateRoot, "plan-validator-prompt.md"), "utf8"),
|
|
1885
|
+
repairer: fs.readFileSync(path.join(templateRoot, "plan-repairer-prompt.md"), "utf8"),
|
|
1886
|
+
taskImplementation: fs.readFileSync(path.join(templateRoot, "task-implementation-prompt.md"), "utf8"),
|
|
1887
|
+
taskReview: fs.readFileSync(path.join(templateRoot, "task-review-prompt.md"), "utf8"),
|
|
1888
|
+
workUnitValidator: fs.readFileSync(path.join(templateRoot, "work-unit-validator-prompt.md"), "utf8")
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
const draftPlan = toPosix(path.join(runRootRelative, "planning", "draft", "work-unit-plan.md"));
|
|
1892
|
+
const draftTraceability = toPosix(path.join(runRootRelative, "planning", "draft", "traceability-matrix.md"));
|
|
1893
|
+
const draftVerification = toPosix(path.join(runRootRelative, "planning", "draft", "verification-plan.md"));
|
|
1894
|
+
const draftGate = toPosix(path.join(runRootRelative, "planning", "draft", "gate-assessment.md"));
|
|
1895
|
+
const draftDecisions = toPosix(path.join(runRootRelative, "planning", "draft", "decisions-draft.md"));
|
|
1896
|
+
const draftTaskSpecs = toPosix(path.join(runRootRelative, "planning", "draft", "task-specs"));
|
|
1897
|
+
const candidatePlan = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "work-unit-plan.md"));
|
|
1898
|
+
const candidateTraceability = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "traceability-matrix.md"));
|
|
1899
|
+
const candidateVerification = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "verification-plan.md"));
|
|
1900
|
+
const candidateGate = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "gate-assessment.md"));
|
|
1901
|
+
const candidateDecisions = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, `${workUnitKey}-decisions.md`));
|
|
1902
|
+
const candidateTaskSpecs = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "task-specs"));
|
|
1903
|
+
const repairSummary = toPosix(path.join(runRootRelative, "final", "plan-repair-summary.md"));
|
|
1904
|
+
const rawPlanAudits = toPosix(path.join(runRootRelative, "raw", "plan-audits"));
|
|
1905
|
+
const consolidatedOutput = toPosix(path.join(runRootRelative, "consolidation", "consolidated-plan-candidates.md"));
|
|
1906
|
+
const discardedOutput = toPosix(path.join(runRootRelative, "consolidation", "discarded-plan-findings.md"));
|
|
1907
|
+
const validationOutput = toPosix(path.join(runRootRelative, "validation", "plan-adversarial-validation-log.md"));
|
|
1908
|
+
const finalPlanReview = toPosix(path.join(runRootRelative, "final", "final-plan-review-report.md"));
|
|
1909
|
+
const workUnitValidation = toPosix(path.join(runRootRelative, "final", "work-unit-validation.md"));
|
|
1910
|
+
const taskLogDir = toPosix(path.join(runRootRelative, "implementation", "task-logs"));
|
|
1911
|
+
const taskReviewDir = toPosix(path.join(runRootRelative, "implementation", "task-reviews"));
|
|
1912
|
+
const versionedWorkUnitPlan = toPosix(path.join(versionedWorkUnitDir, "work-unit-plan.md"));
|
|
1913
|
+
const versionedTraceability = toPosix(path.join(versionedWorkUnitDir, "traceability-matrix.md"));
|
|
1914
|
+
const versionedVerification = toPosix(path.join(versionedWorkUnitDir, "verification-plan.md"));
|
|
1915
|
+
|
|
1916
|
+
const baseValues = {
|
|
1917
|
+
RUN_ID: runName,
|
|
1918
|
+
WORK_UNIT_ID: workUnitId,
|
|
1919
|
+
WORK_UNIT_KEY: workUnitKey,
|
|
1920
|
+
RELEASE_ID: workUnitConfig.releaseId,
|
|
1921
|
+
CONFIG_PATH: configPath,
|
|
1922
|
+
PROFILE_PATH: profilePath,
|
|
1923
|
+
RUN_ROOT: runRootRelative,
|
|
1924
|
+
WORK_UNITS_DOCUMENT: workUnitConfig.workUnitsDocument,
|
|
1925
|
+
SOURCE_INCLUDE: markdownList(workUnitConfig.sourceDocs.include),
|
|
1926
|
+
SOURCE_EXCLUDE: markdownList(workUnitConfig.sourceDocs.exclude),
|
|
1927
|
+
DRAFT_PLAN_OUTPUT: draftPlan,
|
|
1928
|
+
DRAFT_TRACEABILITY_OUTPUT: draftTraceability,
|
|
1929
|
+
DRAFT_VERIFICATION_OUTPUT: draftVerification,
|
|
1930
|
+
DRAFT_GATE_OUTPUT: draftGate,
|
|
1931
|
+
DRAFT_DECISIONS_OUTPUT: draftDecisions,
|
|
1932
|
+
DRAFT_TASK_SPECS_DIR: draftTaskSpecs,
|
|
1933
|
+
CANDIDATE_PLAN_OUTPUT: candidatePlan,
|
|
1934
|
+
CANDIDATE_TRACEABILITY_OUTPUT: candidateTraceability,
|
|
1935
|
+
CANDIDATE_VERIFICATION_OUTPUT: candidateVerification,
|
|
1936
|
+
CANDIDATE_GATE_OUTPUT: candidateGate,
|
|
1937
|
+
CANDIDATE_DECISIONS_OUTPUT: candidateDecisions,
|
|
1938
|
+
CANDIDATE_TASK_SPECS_DIR: candidateTaskSpecs,
|
|
1939
|
+
REPAIR_SUMMARY_OUTPUT: repairSummary,
|
|
1940
|
+
RAW_PLAN_AUDITS_DIR: rawPlanAudits,
|
|
1941
|
+
CONSOLIDATED_OUTPUT: consolidatedOutput,
|
|
1942
|
+
DISCARDED_OUTPUT: discardedOutput,
|
|
1943
|
+
VALIDATION_OUTPUT: validationOutput,
|
|
1944
|
+
FINAL_PLAN_REVIEW_OUTPUT: finalPlanReview,
|
|
1945
|
+
VERSIONED_WORK_UNIT_DIR: versionedWorkUnitDir,
|
|
1946
|
+
VERSIONED_TASK_SPECS_DIR: versionedTaskSpecsDir,
|
|
1947
|
+
VERSIONED_WORK_UNIT_PLAN: versionedWorkUnitPlan,
|
|
1948
|
+
VERSIONED_TRACEABILITY_MATRIX: versionedTraceability,
|
|
1949
|
+
VERSIONED_VERIFICATION_PLAN: versionedVerification,
|
|
1950
|
+
VERSIONED_DECISION_LOG: versionedDecisionLog,
|
|
1951
|
+
TASK_LOG_DIR: taskLogDir,
|
|
1952
|
+
TASK_REVIEW_DIR: taskReviewDir,
|
|
1953
|
+
WORK_UNIT_VALIDATION_OUTPUT: workUnitValidation
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
fs.writeFileSync(path.join(promptsRoot, "plan.md"), renderTemplate(templates.planner, baseValues), "utf8");
|
|
1957
|
+
fs.writeFileSync(path.join(promptsRoot, "consolidate-plan.md"), renderTemplate(templates.consolidator, baseValues), "utf8");
|
|
1958
|
+
fs.writeFileSync(path.join(promptsRoot, "validate-plan.md"), renderTemplate(templates.validator, baseValues), "utf8");
|
|
1959
|
+
fs.writeFileSync(path.join(promptsRoot, "repair-plan.md"), renderTemplate(templates.repairer, baseValues), "utf8");
|
|
1960
|
+
fs.writeFileSync(path.join(promptsRoot, "implement-tasks.md"), renderTemplate(templates.taskImplementation, baseValues), "utf8");
|
|
1961
|
+
fs.writeFileSync(path.join(promptsRoot, "review-task.md"), renderTemplate(templates.taskReview, baseValues), "utf8");
|
|
1962
|
+
fs.writeFileSync(path.join(promptsRoot, "validate-work-unit.md"), renderTemplate(templates.workUnitValidator, baseValues), "utf8");
|
|
1963
|
+
|
|
1964
|
+
const promptRecords = [];
|
|
1965
|
+
for (const combo of combinations) {
|
|
1966
|
+
const outputRelative = toPosix(path.join(runRootRelative, "raw", "plan-audits", `${combo.auditId}.md`));
|
|
1967
|
+
const promptRelative = toPosix(path.join(runRootRelative, "prompts", "plan-auditors", workUnitKey, `${combo.auditId}.md`));
|
|
1968
|
+
const prompt = renderTemplate(templates.planAuditor, {
|
|
1969
|
+
...baseValues,
|
|
1970
|
+
AUDIT_ID: combo.auditId,
|
|
1971
|
+
LENS_ID: combo.lens.id,
|
|
1972
|
+
LENS_TITLE: combo.lens.title,
|
|
1973
|
+
LENS_FOCUS: combo.lens.focus,
|
|
1974
|
+
VARIANT_ID: combo.variant.id,
|
|
1975
|
+
VARIANT_TITLE: combo.variant.title,
|
|
1976
|
+
VARIANT_INSTRUCTION: combo.variant.instruction,
|
|
1977
|
+
PASS_NUMBER: combo.pass,
|
|
1978
|
+
OUTPUT_FILE: outputRelative
|
|
1979
|
+
});
|
|
1980
|
+
fs.writeFileSync(path.join(planAuditorPromptsRoot, `${combo.auditId}.md`), prompt, "utf8");
|
|
1981
|
+
promptRecords.push({
|
|
1982
|
+
auditId: combo.auditId,
|
|
1983
|
+
lensId: combo.lens.id,
|
|
1984
|
+
variantId: combo.variant.id,
|
|
1985
|
+
pass: combo.pass,
|
|
1986
|
+
prompt: promptRelative,
|
|
1987
|
+
output: outputRelative
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
writeJson(path.join(stagingRunRoot, "manifest.json"), {
|
|
1992
|
+
version: 1,
|
|
1993
|
+
workflowId: WORK_UNIT_WORKFLOW_ID,
|
|
1994
|
+
runId: runName,
|
|
1995
|
+
workUnitId,
|
|
1996
|
+
workUnitKey,
|
|
1997
|
+
releaseId: workUnitConfig.releaseId,
|
|
1998
|
+
generatedAt: new Date().toISOString(),
|
|
1999
|
+
configPath,
|
|
2000
|
+
profilePath,
|
|
2001
|
+
workUnitsDocument: workUnitConfig.workUnitsDocument,
|
|
2002
|
+
passesPerLens,
|
|
2003
|
+
maxAudits,
|
|
2004
|
+
gatePolicy: workUnitConfig.gatePolicy,
|
|
2005
|
+
counts: {
|
|
2006
|
+
lenses: profile.lenses.length,
|
|
2007
|
+
variants: profile.variants.length,
|
|
2008
|
+
planAuditorPrompts: combinations.length
|
|
2009
|
+
},
|
|
2010
|
+
paths: {
|
|
2011
|
+
runRoot: runRootRelative,
|
|
2012
|
+
prompts: toPosix(path.join(runRootRelative, "prompts")),
|
|
2013
|
+
planPrompt: toPosix(path.join(runRootRelative, "prompts", "plan.md")),
|
|
2014
|
+
planAuditorPrompts: toPosix(path.join(runRootRelative, "prompts", "plan-auditors", workUnitKey)),
|
|
2015
|
+
rawPlanAudits,
|
|
2016
|
+
consolidation: toPosix(path.join(runRootRelative, "consolidation")),
|
|
2017
|
+
validation: toPosix(path.join(runRootRelative, "validation")),
|
|
2018
|
+
final: toPosix(path.join(runRootRelative, "final")),
|
|
2019
|
+
candidateArtifacts: toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey)),
|
|
2020
|
+
versionedWorkUnitDir,
|
|
2021
|
+
versionedDecisionLog
|
|
2022
|
+
},
|
|
2023
|
+
prompts: {
|
|
2024
|
+
plan: toPosix(path.join(runRootRelative, "prompts", "plan.md")),
|
|
2025
|
+
planAudits: promptRecords,
|
|
2026
|
+
consolidatePlan: toPosix(path.join(runRootRelative, "prompts", "consolidate-plan.md")),
|
|
2027
|
+
validatePlan: toPosix(path.join(runRootRelative, "prompts", "validate-plan.md")),
|
|
2028
|
+
repairPlan: toPosix(path.join(runRootRelative, "prompts", "repair-plan.md")),
|
|
2029
|
+
implementTasks: toPosix(path.join(runRootRelative, "prompts", "implement-tasks.md")),
|
|
2030
|
+
reviewTask: toPosix(path.join(runRootRelative, "prompts", "review-task.md")),
|
|
2031
|
+
validateWorkUnit: toPosix(path.join(runRootRelative, "prompts", "validate-work-unit.md"))
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Work Unit Implementation Run\n\nRun: ${runName}\nWork unit: ${workUnitKey}\nRelease: ${workUnitConfig.releaseId}\n\n## Source Handoff\n\n- Work units document: ${workUnitConfig.workUnitsDocument}\n\n## Counts\n\n- Lenses: ${profile.lenses.length}\n- Variants: ${profile.variants.length}\n- Passes per lens/variant: ${passesPerLens}\n- Plan auditor prompts: ${combinations.length}\n\n## Execution Order\n\n1. Execute prompts/plan.md with the work-unit planner.\n2. Execute prompts/plan-auditors/${workUnitKey}/*.md with plan auditors.\n3. Execute prompts/consolidate-plan.md.\n4. Execute prompts/validate-plan.md.\n5. Execute prompts/repair-plan.md.\n6. Review final/approved-artifacts/${workUnitKey}/ and apply gate policy.\n7. Publish approved artifacts to ${versionedWorkUnitDir} and ${versionedDecisionLog}.\n8. Execute prompts/implement-tasks.md task by task only after approval.\n9. After each implementation or correction, run \`wefter work-unit guard --run-id ${runName} --task-id <task-id> --mode ReadyForReview\`.\n10. Review the task with prompts/review-task.md.\n11. After each task review, run \`wefter work-unit guard --run-id ${runName} --task-id <task-id> --mode ReadyForNextTask\`.\n12. If the guard reports Needs Fix, correct the same task and repeat implementation guard -> review -> next-task guard.\n13. If the guard reports Blocked, pause the work unit for human decision or specification repair.\n14. Before final validation, run \`wefter work-unit guard --run-id ${runName} --mode ReadyForFinalValidation\`.\n15. Execute prompts/validate-work-unit.md only when all tasks pass review and the final-validation guard passes.\n`, "utf8");
|
|
2036
|
+
|
|
2037
|
+
if (fs.existsSync(runRoot)) {
|
|
2038
|
+
throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
|
|
2039
|
+
}
|
|
2040
|
+
fs.renameSync(stagingRunRoot, runRoot);
|
|
2041
|
+
|
|
2042
|
+
console.log(`Created work-unit implementation run: ${runRoot}`);
|
|
2043
|
+
console.log(`Work unit: ${workUnitKey}`);
|
|
2044
|
+
console.log(`Plan auditor prompts generated: ${combinations.length}`);
|
|
2045
|
+
console.log(`Next prompt: ${path.join(runRoot, "prompts", "plan.md")}`);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function requireProperty(object, name, context) {
|
|
2049
|
+
if (!object || typeof object !== "object" || Array.isArray(object) || !(name in object)) {
|
|
2050
|
+
throw new Error(`${context} is missing required property '${name}'.`);
|
|
2051
|
+
}
|
|
2052
|
+
return object[name];
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function getReviewMachineResult(reviewPath, expectedTaskId) {
|
|
2056
|
+
const content = fs.readFileSync(reviewPath, "utf8");
|
|
2057
|
+
const match = content.match(/##\s+Machine Result\s*```json\s*(\{[\s\S]*?\})\s*```/i);
|
|
2058
|
+
if (!match) {
|
|
2059
|
+
throw new Error(`Review '${reviewPath}' must contain a '## Machine Result' section with a fenced json object.`);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
let machine;
|
|
2063
|
+
try {
|
|
2064
|
+
machine = JSON.parse(match[1]);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
throw new Error(`Review '${reviewPath}' contains invalid Machine Result JSON: ${error.message}`);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const taskId = String(requireProperty(machine, "taskId", `Machine Result in '${reviewPath}'`));
|
|
2070
|
+
const result = String(requireProperty(machine, "result", `Machine Result in '${reviewPath}'`));
|
|
2071
|
+
const reviewIteration = requireProperty(machine, "reviewIteration", `Machine Result in '${reviewPath}'`);
|
|
2072
|
+
const blockingFindings = requireProperty(machine, "blockingFindings", `Machine Result in '${reviewPath}'`);
|
|
2073
|
+
|
|
2074
|
+
if (taskId !== expectedTaskId) {
|
|
2075
|
+
throw new Error(`Review '${reviewPath}' Machine Result taskId '${taskId}' does not match expected task '${expectedTaskId}'.`);
|
|
2076
|
+
}
|
|
2077
|
+
if (!["Pass", "Needs Fix", "Blocked"].includes(result)) {
|
|
2078
|
+
throw new Error(`Review '${reviewPath}' Machine Result result must be one of: Pass, Needs Fix, Blocked.`);
|
|
2079
|
+
}
|
|
2080
|
+
const iterationNumber = Number.parseInt(String(reviewIteration), 10);
|
|
2081
|
+
if (!Number.isInteger(iterationNumber) || iterationNumber < 1) {
|
|
2082
|
+
throw new Error(`Review '${reviewPath}' Machine Result reviewIteration must be an integer >= 1.`);
|
|
2083
|
+
}
|
|
2084
|
+
if (!Array.isArray(blockingFindings)) {
|
|
2085
|
+
throw new Error(`Review '${reviewPath}' Machine Result blockingFindings must be an array.`);
|
|
2086
|
+
}
|
|
2087
|
+
if ((result === "Needs Fix" || result === "Blocked") && blockingFindings.length === 0) {
|
|
2088
|
+
throw new Error(`Review '${reviewPath}' Machine Result must list blockingFindings when result is '${result}'.`);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
return { taskId, result, reviewIteration: iterationNumber, blockingFindings };
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function getTaskIds(taskSpecsDir) {
|
|
2095
|
+
if (!fs.existsSync(taskSpecsDir)) {
|
|
2096
|
+
throw new Error(`Task specs directory not found: ${taskSpecsDir}`);
|
|
2097
|
+
}
|
|
2098
|
+
const entries = fs.readdirSync(taskSpecsDir, { withFileTypes: true })
|
|
2099
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
2100
|
+
.map((entry) => entry.name)
|
|
2101
|
+
.sort();
|
|
2102
|
+
if (entries.length === 0) {
|
|
2103
|
+
throw new Error(`No task spec files found in: ${taskSpecsDir}`);
|
|
2104
|
+
}
|
|
2105
|
+
return entries.map((name) => {
|
|
2106
|
+
const taskId = path.basename(name, ".md");
|
|
2107
|
+
if (!/^T\d{2}-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(taskId)) {
|
|
2108
|
+
throw new Error(`Task spec file '${path.join(taskSpecsDir, name)}' does not use the required task id format TXX-YYY.`);
|
|
2109
|
+
}
|
|
2110
|
+
return taskId;
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function getTaskState(targetRoot, taskId, taskLogDir, taskReviewDir) {
|
|
2115
|
+
const logPath = path.join(taskLogDir, `${taskId}.md`);
|
|
2116
|
+
const reviewPath = path.join(taskReviewDir, `${taskId}.md`);
|
|
2117
|
+
const logExists = fs.existsSync(logPath);
|
|
2118
|
+
const reviewExists = fs.existsSync(reviewPath);
|
|
2119
|
+
let reviewResult = null;
|
|
2120
|
+
let reviewIteration = null;
|
|
2121
|
+
let blockingFindings = [];
|
|
2122
|
+
let state = "NotImplemented";
|
|
2123
|
+
let reason = "Task implementation log is missing.";
|
|
2124
|
+
|
|
2125
|
+
if (logExists && !reviewExists) {
|
|
2126
|
+
state = "AwaitingReview";
|
|
2127
|
+
reason = "Task implementation log exists, but review is missing.";
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (logExists && reviewExists) {
|
|
2131
|
+
const logStat = fs.statSync(logPath);
|
|
2132
|
+
const reviewStat = fs.statSync(reviewPath);
|
|
2133
|
+
const machine = getReviewMachineResult(reviewPath, taskId);
|
|
2134
|
+
reviewResult = machine.result;
|
|
2135
|
+
reviewIteration = machine.reviewIteration;
|
|
2136
|
+
blockingFindings = machine.blockingFindings;
|
|
2137
|
+
|
|
2138
|
+
if (reviewStat.mtimeMs < logStat.mtimeMs) {
|
|
2139
|
+
state = "AwaitingReview";
|
|
2140
|
+
reason = "Task implementation log is newer than the review; review is stale.";
|
|
2141
|
+
} else if (reviewResult === "Pass") {
|
|
2142
|
+
state = "Passed";
|
|
2143
|
+
reason = "Task review passed.";
|
|
2144
|
+
} else if (reviewResult === "Needs Fix") {
|
|
2145
|
+
state = "NeedsFix";
|
|
2146
|
+
reason = "Task review requires correction before another task can start.";
|
|
2147
|
+
} else if (reviewResult === "Blocked") {
|
|
2148
|
+
state = "Blocked";
|
|
2149
|
+
reason = "Task review is blocked.";
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
return {
|
|
2154
|
+
taskId,
|
|
2155
|
+
state,
|
|
2156
|
+
reason,
|
|
2157
|
+
logExists,
|
|
2158
|
+
reviewExists,
|
|
2159
|
+
reviewResult,
|
|
2160
|
+
reviewIteration,
|
|
2161
|
+
blockingFindings,
|
|
2162
|
+
logPath: toDisplayPath(targetRoot, logPath),
|
|
2163
|
+
reviewPath: toDisplayPath(targetRoot, reviewPath)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function assertKnownTask(taskIds, taskId, mode) {
|
|
2168
|
+
if (typeof taskId !== "string" || taskId.trim() === "") {
|
|
2169
|
+
throw new Error(`--task-id is required for mode '${mode}'.`);
|
|
2170
|
+
}
|
|
2171
|
+
if (!taskIds.includes(taskId)) {
|
|
2172
|
+
throw new Error(`Task id '${taskId}' is not present in the approved task specs.`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function assertPreviousTasksPassed(taskIds, states, taskId) {
|
|
2177
|
+
const targetIndex = taskIds.indexOf(taskId);
|
|
2178
|
+
if (targetIndex < 0) {
|
|
2179
|
+
throw new Error(`Task id '${taskId}' is not present in the approved task specs.`);
|
|
2180
|
+
}
|
|
2181
|
+
for (let index = 0; index < targetIndex; index++) {
|
|
2182
|
+
const previousTaskId = taskIds[index];
|
|
2183
|
+
const previousState = states.find((state) => state.taskId === previousTaskId);
|
|
2184
|
+
if (previousState?.state !== "Passed") {
|
|
2185
|
+
throw new Error(`Task '${taskId}' cannot proceed because previous task '${previousTaskId}' is '${previousState?.state}'. Reason: ${previousState?.reason}`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
function getLoopStatus(states) {
|
|
2191
|
+
for (const state of states) {
|
|
2192
|
+
if (state.state === "Blocked") {
|
|
2193
|
+
return { result: "Blocked", action: "StopForHumanOrSpecDecision", taskId: state.taskId, reason: state.reason };
|
|
2194
|
+
}
|
|
2195
|
+
if (state.state === "NeedsFix") {
|
|
2196
|
+
return { result: "NeedsAction", action: "FixTask", taskId: state.taskId, reason: state.reason };
|
|
2197
|
+
}
|
|
2198
|
+
if (state.state === "NotImplemented") {
|
|
2199
|
+
return { result: "NeedsAction", action: "ImplementTask", taskId: state.taskId, reason: state.reason };
|
|
2200
|
+
}
|
|
2201
|
+
if (state.state === "AwaitingReview") {
|
|
2202
|
+
return { result: "NeedsAction", action: "ReviewTask", taskId: state.taskId, reason: state.reason };
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
return { result: "Ready", action: "RunFinalWorkUnitValidation", taskId: null, reason: "All approved tasks have passing, non-stale reviews." };
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function writeGuardResult(status, states, json) {
|
|
2209
|
+
if (json) {
|
|
2210
|
+
console.log(JSON.stringify({ status, tasks: states }, null, 2));
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
console.log(`Result: ${status.result}`);
|
|
2214
|
+
console.log(`Action: ${status.action}`);
|
|
2215
|
+
if (status.taskId) {
|
|
2216
|
+
console.log(`Task: ${status.taskId}`);
|
|
2217
|
+
}
|
|
2218
|
+
console.log(`Reason: ${status.reason}`);
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function commandWorkUnitGuard(flags) {
|
|
2222
|
+
const targetRoot = resolveTarget(flags);
|
|
2223
|
+
const wefterConfig = readConfig(targetRoot);
|
|
2224
|
+
const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(wefterConfig, flags)), "work-unit config");
|
|
2225
|
+
validateWorkUnitConfig(workUnitConfig);
|
|
2226
|
+
|
|
2227
|
+
const mode = flags.mode || "Status";
|
|
2228
|
+
if (!["Status", "ReadyForReview", "ReadyForNextTask", "ReadyForFinalValidation"].includes(mode)) {
|
|
2229
|
+
throw new Error("--mode must be one of: Status, ReadyForReview, ReadyForNextTask, ReadyForFinalValidation.");
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
let runRoot;
|
|
2233
|
+
if (flags["run-root"]) {
|
|
2234
|
+
runRoot = resolveInsideTarget(targetRoot, flags["run-root"], "run root");
|
|
2235
|
+
} else {
|
|
2236
|
+
assertPlainRunId(flags["run-id"]);
|
|
2237
|
+
runRoot = resolveInsideTarget(targetRoot, path.join(workUnitConfig.runArtifactsRoot, flags["run-id"]), "run root");
|
|
2238
|
+
}
|
|
2239
|
+
if (!fs.existsSync(runRoot)) {
|
|
2240
|
+
throw new Error(`Run root not found: ${runRoot}`);
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const manifestPath = path.join(runRoot, "manifest.json");
|
|
2244
|
+
const manifest = readJson(manifestPath, "work-unit run manifest");
|
|
2245
|
+
if (manifest.workflowId !== WORK_UNIT_WORKFLOW_ID) {
|
|
2246
|
+
throw new Error(`Run manifest workflowId must be ${WORK_UNIT_WORKFLOW_ID}.`);
|
|
2247
|
+
}
|
|
2248
|
+
const versionedWorkUnitDir = requireProperty(requireProperty(manifest, "paths", "manifest"), "versionedWorkUnitDir", "manifest.paths");
|
|
2249
|
+
const taskSpecsDir = resolveInsideTarget(targetRoot, path.join(versionedWorkUnitDir, "task-specs"), "task specs directory");
|
|
2250
|
+
const taskLogDir = path.join(runRoot, "implementation", "task-logs");
|
|
2251
|
+
const taskReviewDir = path.join(runRoot, "implementation", "task-reviews");
|
|
2252
|
+
if (!fs.existsSync(taskLogDir)) {
|
|
2253
|
+
throw new Error(`Task log directory not found: ${taskLogDir}`);
|
|
2254
|
+
}
|
|
2255
|
+
if (!fs.existsSync(taskReviewDir)) {
|
|
2256
|
+
throw new Error(`Task review directory not found: ${taskReviewDir}`);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const taskIds = getTaskIds(taskSpecsDir);
|
|
2260
|
+
const states = taskIds.map((taskId) => getTaskState(targetRoot, taskId, taskLogDir, taskReviewDir));
|
|
2261
|
+
const status = getLoopStatus(states);
|
|
2262
|
+
|
|
2263
|
+
if (mode === "Status") {
|
|
2264
|
+
writeGuardResult(status, states, flags.json);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const taskId = flags["task-id"];
|
|
2269
|
+
if (mode === "ReadyForReview") {
|
|
2270
|
+
assertKnownTask(taskIds, taskId, mode);
|
|
2271
|
+
assertPreviousTasksPassed(taskIds, states, taskId);
|
|
2272
|
+
const state = states.find((item) => item.taskId === taskId);
|
|
2273
|
+
if (!state.logExists) {
|
|
2274
|
+
throw new Error(`Task '${taskId}' is not ready for review because its implementation log is missing.`);
|
|
2275
|
+
}
|
|
2276
|
+
if (state.state === "NeedsFix") {
|
|
2277
|
+
throw new Error(`Task '${taskId}' still needs a correction. Update its implementation log after the correction before requesting another review.`);
|
|
2278
|
+
}
|
|
2279
|
+
if (state.state === "Blocked") {
|
|
2280
|
+
throw new Error(`Task '${taskId}' is blocked and cannot be reviewed as ready.`);
|
|
2281
|
+
}
|
|
2282
|
+
writeGuardResult({ result: "Ready", action: "ReviewTask", taskId, reason: `Task '${taskId}' has an implementation log and can be reviewed.` }, states, flags.json);
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (mode === "ReadyForNextTask") {
|
|
2287
|
+
assertKnownTask(taskIds, taskId, mode);
|
|
2288
|
+
assertPreviousTasksPassed(taskIds, states, taskId);
|
|
2289
|
+
const state = states.find((item) => item.taskId === taskId);
|
|
2290
|
+
if (state.state !== "Passed") {
|
|
2291
|
+
throw new Error(`Task '${taskId}' cannot release the next task. Current state: ${state.state}. Reason: ${state.reason}`);
|
|
2292
|
+
}
|
|
2293
|
+
writeGuardResult({ result: "Ready", action: "AdvanceToNextTaskOrFinalValidation", taskId, reason: `Task '${taskId}' has a passing, non-stale review.` }, states, flags.json);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (mode === "ReadyForFinalValidation") {
|
|
2298
|
+
if (status.result !== "Ready") {
|
|
2299
|
+
throw new Error(`Work unit is not ready for final validation. Next required action: ${status.action} for task '${status.taskId}'. Reason: ${status.reason}`);
|
|
2300
|
+
}
|
|
2301
|
+
writeGuardResult(status, states, flags.json);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
function commandProfileScaffold(flags) {
|
|
2306
|
+
const targetRoot = resolveTarget(flags);
|
|
2307
|
+
const config = readConfig(targetRoot);
|
|
2308
|
+
const profilePath = path.join(targetRoot, config.profilePath);
|
|
2309
|
+
if (fs.existsSync(profilePath) && !flags.force) {
|
|
2310
|
+
throw new Error(`Profile already exists: ${profilePath}. Use --force to overwrite.`);
|
|
2311
|
+
}
|
|
2312
|
+
writeJson(profilePath, defaultProfile(config));
|
|
2313
|
+
console.log(`Wrote starter audit profile: ${profilePath}`);
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
function commandProfileImport(flags) {
|
|
2317
|
+
const targetRoot = resolveTarget(flags);
|
|
2318
|
+
const config = readConfig(targetRoot);
|
|
2319
|
+
if (!flags.source) {
|
|
2320
|
+
throw new Error("--source is required for profile import.");
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
const sourceRelative = normalizeRelativePath(flags.source, "source");
|
|
2324
|
+
const sourcePath = path.join(targetRoot, sourceRelative);
|
|
2325
|
+
const destinationPath = path.join(targetRoot, config.profilePath);
|
|
2326
|
+
ensureInside(targetRoot, sourcePath, "source");
|
|
2327
|
+
ensureInside(targetRoot, destinationPath, "profilePath");
|
|
2328
|
+
|
|
2329
|
+
if (!fs.existsSync(sourcePath)) {
|
|
2330
|
+
throw new Error(`Source profile not found: ${sourcePath}`);
|
|
2331
|
+
}
|
|
2332
|
+
if (fs.existsSync(destinationPath) && !flags.force) {
|
|
2333
|
+
throw new Error(`Profile already exists: ${destinationPath}. Use --force to overwrite.`);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
const profile = readJson(sourcePath, "source audit profile");
|
|
2337
|
+
validateProfile(profile);
|
|
2338
|
+
writeJson(destinationPath, profile);
|
|
2339
|
+
console.log(`Imported audit profile from ${sourceRelative} to ${config.profilePath}`);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function commandDoctor(flags) {
|
|
2343
|
+
const targetRoot = resolveTarget(flags);
|
|
2344
|
+
const config = readConfig(targetRoot);
|
|
2345
|
+
const errors = [];
|
|
2346
|
+
const checks = [];
|
|
2347
|
+
|
|
2348
|
+
function check(label, fn) {
|
|
2349
|
+
try {
|
|
2350
|
+
fn();
|
|
2351
|
+
checks.push(`OK ${label}`);
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
errors.push(`${label}: ${error.message}`);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
check(CONFIG_FILE, () => normalizeConfig(config));
|
|
2358
|
+
check("profile", () => validateProfile(readJson(path.join(targetRoot, config.profilePath), "audit profile")));
|
|
2359
|
+
check("configured paths", () => {
|
|
2360
|
+
for (const [label, relativePath] of Object.entries({
|
|
2361
|
+
profilePath: config.profilePath,
|
|
2362
|
+
workflowRoot: config.workflowRoot,
|
|
2363
|
+
artifactRoot: config.artifactRoot,
|
|
2364
|
+
templateRoot: config.templateRoot,
|
|
2365
|
+
processDocPath: config.processDocPath
|
|
2366
|
+
})) {
|
|
2367
|
+
ensureInside(targetRoot, path.join(targetRoot, relativePath), label);
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
check("templates", () => {
|
|
2371
|
+
for (const file of REQUIRED_TEMPLATE_FILES) {
|
|
2372
|
+
const fullPath = path.join(targetRoot, config.templateRoot, file);
|
|
2373
|
+
if (!fs.existsSync(fullPath)) {
|
|
2374
|
+
throw new Error(`Missing ${fullPath}`);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2378
|
+
check("workflow manifests", () => {
|
|
2379
|
+
for (const workflowId of Object.keys(config.workflows)) {
|
|
2380
|
+
const fullPath = path.join(targetRoot, config.workflowRoot, workflowId, "workflow.json");
|
|
2381
|
+
if (!fs.existsSync(fullPath)) {
|
|
2382
|
+
throw new Error(`Missing ${fullPath}`);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
check("work-unit workflow config", () => {
|
|
2387
|
+
const configPath = path.join(targetRoot, workUnitConfigPath(config));
|
|
2388
|
+
const profilePath = path.join(targetRoot, workUnitProfilePath(config));
|
|
2389
|
+
validateWorkUnitConfig(readJson(configPath, "work-unit config"));
|
|
2390
|
+
validateWorkUnitProfile(readJson(profilePath, "work-unit profile"));
|
|
2391
|
+
});
|
|
2392
|
+
check("product-shaping workflow config", () => {
|
|
2393
|
+
const configPath = path.join(targetRoot, productShapingConfigPath(config));
|
|
2394
|
+
const profilePath = path.join(targetRoot, productShapingProfilePath(config));
|
|
2395
|
+
validateProductShapingConfig(readJson(configPath, "product-shaping config"));
|
|
2396
|
+
validateProductShapingProfile(readJson(profilePath, "product-shaping profile"));
|
|
2397
|
+
});
|
|
2398
|
+
check("opencode agents", () => {
|
|
2399
|
+
const agentFiles = [
|
|
2400
|
+
"wefter-doc-audit-orchestrator.md",
|
|
2401
|
+
"wefter-doc-auditor.md",
|
|
2402
|
+
"wefter-doc-audit-consolidator.md",
|
|
2403
|
+
"wefter-doc-audit-validator.md",
|
|
2404
|
+
"wefter-doc-audit-profile-builder.md"
|
|
2405
|
+
];
|
|
2406
|
+
const posixGlob = `${config.artifactRoot}/**`;
|
|
2407
|
+
const windowsGlob = windowsPermissionGlob(config.artifactRoot);
|
|
2408
|
+
|
|
2409
|
+
for (const file of agentFiles) {
|
|
2410
|
+
const fullPath = path.join(targetRoot, ".opencode/agent", file);
|
|
2411
|
+
const content = readTextRequired(fullPath);
|
|
2412
|
+
assertNoPlaceholders(fullPath, content);
|
|
2413
|
+
assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
|
|
2414
|
+
assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-doc-audit-orchestrator.md"));
|
|
2418
|
+
assertIncludes(orchestrator, CONFIG_FILE, "orchestrator config reference");
|
|
2419
|
+
assertIncludes(orchestrator, config.profilePath, "orchestrator profile path");
|
|
2420
|
+
assertIncludes(orchestrator, config.runnerCommand, "orchestrator runner command");
|
|
2421
|
+
});
|
|
2422
|
+
check("work-unit opencode agents", () => {
|
|
2423
|
+
const agentFiles = [
|
|
2424
|
+
"wefter-work-unit-orchestrator.md",
|
|
2425
|
+
"wefter-work-unit-planner.md",
|
|
2426
|
+
"wefter-work-unit-plan-auditor.md",
|
|
2427
|
+
"wefter-work-unit-plan-consolidator.md",
|
|
2428
|
+
"wefter-work-unit-plan-validator.md",
|
|
2429
|
+
"wefter-work-unit-plan-repairer.md",
|
|
2430
|
+
"wefter-work-unit-task-implementer.md",
|
|
2431
|
+
"wefter-work-unit-task-reviewer.md",
|
|
2432
|
+
"wefter-work-unit-validator.md"
|
|
2433
|
+
];
|
|
2434
|
+
const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
|
|
2435
|
+
const posixGlob = `${workUnitConfig.runArtifactsRoot}/**`;
|
|
2436
|
+
const windowsGlob = windowsPermissionGlob(workUnitConfig.runArtifactsRoot);
|
|
2437
|
+
|
|
2438
|
+
for (const file of agentFiles) {
|
|
2439
|
+
const fullPath = path.join(targetRoot, ".opencode/agent", file);
|
|
2440
|
+
const content = readTextRequired(fullPath);
|
|
2441
|
+
assertNoPlaceholders(fullPath, content);
|
|
2442
|
+
if (!file.includes("task-implementer") && !file.includes("orchestrator")) {
|
|
2443
|
+
assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
|
|
2444
|
+
assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-work-unit-orchestrator.md"));
|
|
2449
|
+
assertIncludes(orchestrator, CONFIG_FILE, "work-unit orchestrator config reference");
|
|
2450
|
+
assertIncludes(orchestrator, workUnitConfigPath(config), "work-unit orchestrator workflow config path");
|
|
2451
|
+
assertIncludes(orchestrator, workUnitProfilePath(config), "work-unit orchestrator workflow profile path");
|
|
2452
|
+
assertIncludes(orchestrator, config.runnerCommand, "work-unit orchestrator runner command");
|
|
2453
|
+
});
|
|
2454
|
+
check("product-shaping opencode agents", () => {
|
|
2455
|
+
const agentFiles = [
|
|
2456
|
+
"wefter-product-orchestrator.md",
|
|
2457
|
+
"wefter-product-intake-analyst.md",
|
|
2458
|
+
"wefter-product-reference-researcher.md",
|
|
2459
|
+
"wefter-product-shaper.md",
|
|
2460
|
+
"wefter-product-domain-modeler.md",
|
|
2461
|
+
"wefter-product-release-planner.md",
|
|
2462
|
+
"wefter-product-auditor.md",
|
|
2463
|
+
"wefter-product-validator.md",
|
|
2464
|
+
"wefter-product-repairer.md"
|
|
2465
|
+
];
|
|
2466
|
+
const specGlob = `${productShapingSpecRoot(config)}/**`;
|
|
2467
|
+
const runGlob = `${productShapingRunRoot(config)}/**`;
|
|
2468
|
+
const specWindowsGlob = windowsPermissionGlob(productShapingSpecRoot(config));
|
|
2469
|
+
const runWindowsGlob = windowsPermissionGlob(productShapingRunRoot(config));
|
|
2470
|
+
|
|
2471
|
+
for (const file of agentFiles) {
|
|
2472
|
+
const fullPath = path.join(targetRoot, ".opencode/agent", file);
|
|
2473
|
+
const content = readTextRequired(fullPath);
|
|
2474
|
+
assertNoPlaceholders(fullPath, content);
|
|
2475
|
+
if (!file.includes("auditor") && !file.includes("validator")) {
|
|
2476
|
+
assertIncludes(content, specGlob, `${file} POSIX spec permission`);
|
|
2477
|
+
assertIncludes(content, specWindowsGlob, `${file} Windows spec permission`);
|
|
2478
|
+
}
|
|
2479
|
+
assertIncludes(content, runGlob, `${file} POSIX run permission`);
|
|
2480
|
+
assertIncludes(content, runWindowsGlob, `${file} Windows run permission`);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-product-orchestrator.md"));
|
|
2484
|
+
assertIncludes(orchestrator, CONFIG_FILE, "product orchestrator config reference");
|
|
2485
|
+
assertIncludes(orchestrator, productShapingConfigPath(config), "product orchestrator workflow config path");
|
|
2486
|
+
assertIncludes(orchestrator, productShapingProfilePath(config), "product orchestrator workflow profile path");
|
|
2487
|
+
assertIncludes(orchestrator, config.runnerCommand, "product orchestrator runner command");
|
|
2488
|
+
});
|
|
2489
|
+
check("documentation repair opencode agents", () => {
|
|
2490
|
+
const agentFiles = [
|
|
2491
|
+
"wefter-doc-repair-orchestrator.md",
|
|
2492
|
+
"wefter-doc-repair-planner.md",
|
|
2493
|
+
"wefter-doc-repairer.md",
|
|
2494
|
+
"wefter-doc-repair-reviewer.md"
|
|
2495
|
+
];
|
|
2496
|
+
const posixGlob = `${documentationRepairArtifactRoot()}/**`;
|
|
2497
|
+
const windowsGlob = windowsPermissionGlob(documentationRepairArtifactRoot());
|
|
2498
|
+
|
|
2499
|
+
for (const file of agentFiles) {
|
|
2500
|
+
const fullPath = path.join(targetRoot, ".opencode/agent", file);
|
|
2501
|
+
const content = readTextRequired(fullPath);
|
|
2502
|
+
assertNoPlaceholders(fullPath, content);
|
|
2503
|
+
if (file.includes("planner") || file.includes("reviewer")) {
|
|
2504
|
+
assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
|
|
2505
|
+
assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-doc-repair-orchestrator.md"));
|
|
2510
|
+
assertIncludes(orchestrator, CONFIG_FILE, "documentation repair orchestrator config reference");
|
|
2511
|
+
assertIncludes(orchestrator, documentationRepairArtifactRoot(), "documentation repair orchestrator artifact root");
|
|
2512
|
+
assertIncludes(orchestrator, config.runnerCommand, "documentation repair orchestrator runner command");
|
|
2513
|
+
});
|
|
2514
|
+
check("opencode skill", () => {
|
|
2515
|
+
const skillPath = path.join(targetRoot, ".opencode/skills/documentation-audit/SKILL.md");
|
|
2516
|
+
const content = readTextRequired(skillPath);
|
|
2517
|
+
assertNoPlaceholders(skillPath, content);
|
|
2518
|
+
assertIncludes(content, config.profilePath, "skill profile path");
|
|
2519
|
+
assertIncludes(content, config.templateRoot, "skill template root");
|
|
2520
|
+
assertIncludes(content, config.processDocPath, "skill process doc path");
|
|
2521
|
+
});
|
|
2522
|
+
check("work-unit opencode skill", () => {
|
|
2523
|
+
const skillPath = path.join(targetRoot, ".opencode/skills/work-unit-implementation/SKILL.md");
|
|
2524
|
+
const content = readTextRequired(skillPath);
|
|
2525
|
+
assertNoPlaceholders(skillPath, content);
|
|
2526
|
+
assertIncludes(content, workUnitConfigPath(config), "work-unit skill config path");
|
|
2527
|
+
assertIncludes(content, workUnitProfilePath(config), "work-unit skill profile path");
|
|
2528
|
+
});
|
|
2529
|
+
check("product-shaping opencode skill", () => {
|
|
2530
|
+
const skillPath = path.join(targetRoot, ".opencode/skills/product-shaping/SKILL.md");
|
|
2531
|
+
const content = readTextRequired(skillPath);
|
|
2532
|
+
assertNoPlaceholders(skillPath, content);
|
|
2533
|
+
assertIncludes(content, productShapingConfigPath(config), "product skill config path");
|
|
2534
|
+
assertIncludes(content, productShapingProfilePath(config), "product skill profile path");
|
|
2535
|
+
assertIncludes(content, productShapingSpecRoot(config), "product skill spec root");
|
|
2536
|
+
assertIncludes(content, productShapingRunRoot(config), "product skill run root");
|
|
2537
|
+
});
|
|
2538
|
+
check("documentation repair opencode skill", () => {
|
|
2539
|
+
const skillPath = path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md");
|
|
2540
|
+
const content = readTextRequired(skillPath);
|
|
2541
|
+
assertNoPlaceholders(skillPath, content);
|
|
2542
|
+
assertIncludes(content, "/wefter-repair-docs", "documentation repair skill command reference");
|
|
2543
|
+
});
|
|
2544
|
+
check("opencode commands", () => {
|
|
2545
|
+
const opencode = readJson(path.join(targetRoot, "opencode.json"), "opencode.json");
|
|
2546
|
+
if (opencode.command?.["wefter-audit-docs"]?.agent !== "wefter-doc-audit-orchestrator" || opencode.command?.["wefter-generate-doc-audit-profile"]?.agent !== "wefter-doc-audit-profile-builder" || opencode.command?.["wefter-repair-docs"]?.agent !== "wefter-doc-repair-orchestrator" || opencode.command?.["wefter-run-work-unit"]?.agent !== "wefter-work-unit-orchestrator") {
|
|
2547
|
+
throw new Error("Missing Wefter opencode commands.");
|
|
2548
|
+
}
|
|
2549
|
+
const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
|
|
2550
|
+
if (productSettings.enabled && opencode.command?.["wefter-shape-product"]?.agent !== "wefter-product-orchestrator") {
|
|
2551
|
+
throw new Error("Missing enabled product-shaping opencode command.");
|
|
2552
|
+
}
|
|
2553
|
+
if (!productSettings.enabled && opencode.command?.["wefter-shape-product"]) {
|
|
2554
|
+
throw new Error("Disabled product-shaping workflow must not install a runnable opencode command.");
|
|
2555
|
+
}
|
|
2556
|
+
if (!Array.isArray(opencode.skills?.paths) || !opencode.skills.paths.includes(".opencode/skills")) {
|
|
2557
|
+
throw new Error("Missing .opencode/skills in opencode skills.paths.");
|
|
2558
|
+
}
|
|
2559
|
+
const watcherIgnore = Array.isArray(opencode.watcher?.ignore) ? opencode.watcher.ignore : [];
|
|
2560
|
+
const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
|
|
2561
|
+
for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
|
|
2562
|
+
if (!ignored) {
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
const pattern = `${ignored.replace(/\/$/, "")}/**`;
|
|
2566
|
+
if (!watcherIgnore.includes(pattern)) {
|
|
2567
|
+
throw new Error(`Missing opencode watcher ignore '${pattern}'.`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
for (const item of checks) {
|
|
2573
|
+
console.log(item);
|
|
2574
|
+
}
|
|
2575
|
+
if (errors.length > 0) {
|
|
2576
|
+
for (const error of errors) {
|
|
2577
|
+
console.error(`ERROR ${error}`);
|
|
2578
|
+
}
|
|
2579
|
+
process.exitCode = 1;
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
console.log("Wefter installation looks healthy.");
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
2586
|
+
const { positional, flags } = parseArgs(argv);
|
|
2587
|
+
if (flags.version) {
|
|
2588
|
+
console.log(VERSION);
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (flags.help || positional.length === 0) {
|
|
2592
|
+
printHelp();
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
const [command, subcommand] = positional;
|
|
2597
|
+
if (command === "init") {
|
|
2598
|
+
await commandInit(flags);
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
if (command === "new-run") {
|
|
2602
|
+
if (subcommand && subcommand !== "documentation-audit") {
|
|
2603
|
+
throw new Error(`Unsupported workflow for new-run: ${subcommand}`);
|
|
2604
|
+
}
|
|
2605
|
+
commandNewRun(flags);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (command === "docs" && subcommand === "audit") {
|
|
2609
|
+
commandNewRun(flags);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (command === "docs" && subcommand === "repair") {
|
|
2613
|
+
commandDocsRepair(flags);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
if (command === "product" && subcommand === "shape") {
|
|
2617
|
+
commandProductShape(flags);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (command === "product" && subcommand === "validate") {
|
|
2621
|
+
commandProductValidate(flags);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (command === "work-unit" && subcommand === "run") {
|
|
2625
|
+
commandWorkUnitRun(flags);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
if (command === "work-unit" && subcommand === "guard") {
|
|
2629
|
+
commandWorkUnitGuard(flags);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
if (command === "profile" && subcommand === "scaffold") {
|
|
2633
|
+
commandProfileScaffold(flags);
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (command === "profile" && subcommand === "import") {
|
|
2637
|
+
commandProfileImport(flags);
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
if (command === "doctor") {
|
|
2641
|
+
commandDoctor(flags);
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
throw new Error(`Unknown command: ${positional.join(" ")}`);
|
|
2646
|
+
}
|