@wefter/opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +112 -0
  4. package/bin/wefter.js +8 -0
  5. package/docs/ARCHITECTURE.md +79 -0
  6. package/docs/INSTALLATION.md +46 -0
  7. package/docs/SAFETY_MODEL.md +17 -0
  8. package/docs/WORKFLOWS.md +13 -0
  9. package/package.json +45 -0
  10. package/schemas/documentation-audit-profile.schema.json +47 -0
  11. package/schemas/run-manifest.schema.json +14 -0
  12. package/schemas/wefter.config.schema.json +31 -0
  13. package/schemas/work-unit-config.schema.json +44 -0
  14. package/schemas/work-unit-profile.schema.json +38 -0
  15. package/schemas/work-unit-review-result.schema.json +13 -0
  16. package/schemas/workflow-manifest.schema.json +21 -0
  17. package/src/cli/main.js +1858 -0
  18. package/src/workflows/documentation-audit/README.md +37 -0
  19. package/src/workflows/documentation-audit/templates/README.md.tmpl +47 -0
  20. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-consolidator.md.tmpl +27 -0
  21. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-orchestrator.md.tmpl +65 -0
  22. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-profile-builder.md.tmpl +58 -0
  23. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-validator.md.tmpl +26 -0
  24. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-auditor.md.tmpl +28 -0
  25. package/src/workflows/documentation-audit/templates/opencode/skills/documentation-audit/SKILL.md.tmpl +38 -0
  26. package/src/workflows/documentation-audit/templates/prompts/auditor-prompt.md +97 -0
  27. package/src/workflows/documentation-audit/templates/prompts/consolidator-prompt.md +84 -0
  28. package/src/workflows/documentation-audit/templates/prompts/validator-prompt.md +92 -0
  29. package/src/workflows/documentation-audit/workflow.json +24 -0
  30. package/src/workflows/documentation-repair/README.md +11 -0
  31. package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-orchestrator.md.tmpl +33 -0
  32. package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-planner.md.tmpl +17 -0
  33. package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repair-reviewer.md.tmpl +17 -0
  34. package/src/workflows/documentation-repair/templates/opencode/agent/wefter-doc-repairer.md.tmpl +14 -0
  35. package/src/workflows/documentation-repair/templates/opencode/skills/documentation-repair/SKILL.md.tmpl +17 -0
  36. package/src/workflows/documentation-repair/templates/prompts/repair-apply-prompt.md +43 -0
  37. package/src/workflows/documentation-repair/templates/prompts/repair-plan-prompt.md +73 -0
  38. package/src/workflows/documentation-repair/templates/prompts/repair-review-prompt.md +47 -0
  39. package/src/workflows/documentation-repair/workflow.json +10 -0
  40. package/src/workflows/product-shaping/README.md +7 -0
  41. package/src/workflows/product-shaping/workflow.json +10 -0
  42. package/src/workflows/technical-shaping/README.md +5 -0
  43. package/src/workflows/technical-shaping/workflow.json +10 -0
  44. package/src/workflows/work-unit-implementation/README.md +71 -0
  45. package/src/workflows/work-unit-implementation/templates/default-config.json +46 -0
  46. package/src/workflows/work-unit-implementation/templates/default-profile.json +57 -0
  47. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-orchestrator.md.tmpl +62 -0
  48. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-auditor.md.tmpl +26 -0
  49. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-consolidator.md.tmpl +26 -0
  50. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-repairer.md.tmpl +25 -0
  51. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-plan-validator.md.tmpl +25 -0
  52. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-planner.md.tmpl +27 -0
  53. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-task-implementer.md.tmpl +30 -0
  54. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-task-reviewer.md.tmpl +28 -0
  55. package/src/workflows/work-unit-implementation/templates/opencode/agent/wefter-work-unit-validator.md.tmpl +26 -0
  56. package/src/workflows/work-unit-implementation/templates/opencode/skills/work-unit-implementation/SKILL.md.tmpl +25 -0
  57. package/src/workflows/work-unit-implementation/templates/prompts/plan-auditor-prompt.md +89 -0
  58. package/src/workflows/work-unit-implementation/templates/prompts/plan-consolidator-prompt.md +64 -0
  59. package/src/workflows/work-unit-implementation/templates/prompts/plan-repairer-prompt.md +42 -0
  60. package/src/workflows/work-unit-implementation/templates/prompts/plan-validator-prompt.md +84 -0
  61. package/src/workflows/work-unit-implementation/templates/prompts/planner-prompt.md +150 -0
  62. package/src/workflows/work-unit-implementation/templates/prompts/task-implementation-prompt.md +57 -0
  63. package/src/workflows/work-unit-implementation/templates/prompts/task-review-prompt.md +69 -0
  64. package/src/workflows/work-unit-implementation/templates/prompts/work-unit-validator-prompt.md +50 -0
  65. package/src/workflows/work-unit-implementation/workflow.json +14 -0
@@ -0,0 +1,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.1.0";
9
+ const CONFIG_FILE = "wefter.config.json";
10
+ const DOCUMENTATION_REPAIR_WORKFLOW_ID = "documentation-repair";
11
+ const WORK_UNIT_WORKFLOW_ID = "work-unit-implementation";
12
+ const DEFAULTS = Object.freeze({
13
+ workflowRoot: ".wefter/workflows",
14
+ profilePath: ".wefter/workflows/documentation-audit/profile.json",
15
+ artifactRoot: ".audit/wefter/documentation-audit",
16
+ templateRoot: ".wefter/workflows/documentation-audit/templates",
17
+ processDocPath: ".wefter/workflows/documentation-audit/README.md"
18
+ });
19
+
20
+ const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
21
+
22
+ const REQUIRED_TEMPLATE_FILES = Object.freeze([
23
+ "auditor-prompt.md",
24
+ "consolidator-prompt.md",
25
+ "validator-prompt.md"
26
+ ]);
27
+
28
+ function printHelp() {
29
+ console.log(`wefter ${VERSION}
30
+
31
+ Usage:
32
+ wefter init [--yes] [--force] [--target <path>] [--profile-path <path>] [--artifact-root <path>] [--template-root <path>] [--process-doc-path <path>] [--runner-command <command>]
33
+ wefter docs audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
34
+ wefter docs repair [--target <path>] --audit-report <path> [--run-name <name>] [--dry-run]
35
+ wefter work-unit run [--target <path>] [--work-unit-id <id>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--config-path <path>] [--profile-path <path>] [--dry-run]
36
+ wefter work-unit guard [--target <path>] [--run-id <id> | --run-root <path>] [--task-id <id>] [--mode Status|ReadyForReview|ReadyForNextTask|ReadyForFinalValidation] [--config-path <path>] [--json]
37
+ wefter new-run documentation-audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
38
+ wefter profile scaffold [--target <path>] [--force]
39
+ wefter profile import [--target <path>] --source <path> [--force]
40
+ wefter doctor [--target <path>]
41
+
42
+ Commands:
43
+ init Install opencode agents, skill, commands, templates and local config.
44
+ docs audit Generate one documentation audit run from the configured profile.
45
+ docs repair Generate one documentation repair run from a final audit report.
46
+ work-unit run Generate one work-unit implementation run.
47
+ work-unit guard Validate task/review loop state for a work-unit run.
48
+ new-run Generate one workflow run. Currently supports documentation-audit.
49
+ profile scaffold Create a heuristic starter audit profile for the current repository.
50
+ profile import Import a repository-relative documentation audit profile into the configured profile path.
51
+ doctor Validate local installation and configuration.
52
+ `);
53
+ }
54
+
55
+ function parseArgs(argv) {
56
+ const positional = [];
57
+ const flags = {};
58
+
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const arg = argv[i];
61
+ if (!arg.startsWith("--")) {
62
+ positional.push(arg);
63
+ continue;
64
+ }
65
+
66
+ const key = arg.slice(2);
67
+ if (["yes", "force", "dry-run", "help", "version", "json"].includes(key)) {
68
+ flags[key] = true;
69
+ continue;
70
+ }
71
+
72
+ const next = argv[i + 1];
73
+ if (!next || next.startsWith("--")) {
74
+ throw new Error(`Missing value for --${key}`);
75
+ }
76
+ flags[key] = next;
77
+ i++;
78
+ }
79
+
80
+ return { positional, flags };
81
+ }
82
+
83
+ function packageRoot() {
84
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
85
+ }
86
+
87
+ function workflowPackageRoot(workflowId) {
88
+ return path.join(packageRoot(), "src/workflows", workflowId);
89
+ }
90
+
91
+ function documentationAuditTemplateRoot() {
92
+ return path.join(workflowPackageRoot("documentation-audit"), "templates");
93
+ }
94
+
95
+ function documentationRepairTemplateRoot() {
96
+ return path.join(workflowPackageRoot(DOCUMENTATION_REPAIR_WORKFLOW_ID), "templates");
97
+ }
98
+
99
+ function workUnitWorkflowPackageRoot() {
100
+ return workflowPackageRoot(WORK_UNIT_WORKFLOW_ID);
101
+ }
102
+
103
+ function documentationRepairArtifactRoot() {
104
+ return ".audit/wefter/documentation-repair";
105
+ }
106
+
107
+ function resolveTarget(flags) {
108
+ return path.resolve(flags.target || process.cwd());
109
+ }
110
+
111
+ function toPosix(value) {
112
+ return value.split(path.sep).join("/");
113
+ }
114
+
115
+ function quoteCommandArg(value) {
116
+ const normalized = toPosix(path.resolve(value));
117
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(normalized)) {
118
+ return normalized;
119
+ }
120
+ return `"${normalized.replaceAll('"', '\\"')}"`;
121
+ }
122
+
123
+ function defaultRunnerCommand() {
124
+ return `node ${quoteCommandArg(path.join(packageRoot(), "bin/wefter.js"))}`;
125
+ }
126
+
127
+ function yamlSingleQuoted(value) {
128
+ return `'${String(value).replaceAll("'", "''")}'`;
129
+ }
130
+
131
+ function normalizeRelativePath(value, label) {
132
+ if (typeof value !== "string" || value.trim() === "") {
133
+ throw new Error(`${label} must be a non-empty relative path.`);
134
+ }
135
+
136
+ const trimmed = value.trim().replaceAll("\\", "/");
137
+ if (trimmed.includes("\n") || trimmed.includes("\r")) {
138
+ throw new Error(`${label} must not contain line breaks.`);
139
+ }
140
+ if (path.isAbsolute(trimmed)) {
141
+ throw new Error(`${label} must be relative to the target repository.`);
142
+ }
143
+
144
+ const parts = trimmed.split("/").filter(Boolean);
145
+ if (parts.length === 0 || parts.includes("..")) {
146
+ throw new Error(`${label} must not be empty or contain '..'.`);
147
+ }
148
+
149
+ return parts.join("/");
150
+ }
151
+
152
+ function normalizeRunnerCommand(value, label) {
153
+ if (typeof value !== "string" || value.trim() === "") {
154
+ throw new Error(`${label} must be a non-empty command string.`);
155
+ }
156
+ if (value !== value.trim()) {
157
+ throw new Error(`${label} must not contain leading or trailing whitespace.`);
158
+ }
159
+ if (value.includes("\n") || value.includes("\r")) {
160
+ throw new Error(`${label} must not contain line breaks.`);
161
+ }
162
+ if (value.includes("{{")) {
163
+ throw new Error(`${label} must not contain unresolved template placeholders.`);
164
+ }
165
+ return value;
166
+ }
167
+
168
+ function assertObject(value, label) {
169
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
170
+ throw new Error(`${label} must be an object.`);
171
+ }
172
+ }
173
+
174
+ function assertAllowedKeys(value, label, allowedKeys) {
175
+ const allowed = new Set(allowedKeys);
176
+ for (const key of Object.keys(value)) {
177
+ if (!allowed.has(key)) {
178
+ throw new Error(`${label} has unsupported property '${key}'.`);
179
+ }
180
+ }
181
+ }
182
+
183
+ function requireString(value, label) {
184
+ if (typeof value !== "string" || value.trim() === "") {
185
+ throw new Error(`${label} must be a non-empty string.`);
186
+ }
187
+ if (value.includes("\n") || value.includes("\r")) {
188
+ throw new Error(`${label} must not contain line breaks.`);
189
+ }
190
+ return value;
191
+ }
192
+
193
+ function requireId(value, label) {
194
+ requireString(value, label);
195
+ if (!ID_PATTERN.test(value)) {
196
+ throw new Error(`${label} must match ${ID_PATTERN}.`);
197
+ }
198
+ }
199
+
200
+ function requireStringArray(value, label) {
201
+ if (!Array.isArray(value)) {
202
+ throw new Error(`${label} must be an array.`);
203
+ }
204
+ value.forEach((item, index) => requireString(item, `${label}[${index}]`));
205
+ }
206
+
207
+ function assertUniqueIds(items, label) {
208
+ const seen = new Set();
209
+ for (const item of items) {
210
+ if (seen.has(item.id)) {
211
+ throw new Error(`${label} contains duplicate id '${item.id}'.`);
212
+ }
213
+ seen.add(item.id);
214
+ }
215
+ }
216
+
217
+ function windowsPermissionPath(relativePath) {
218
+ return relativePath.replaceAll("/", "\\\\");
219
+ }
220
+
221
+ function windowsPermissionGlob(relativePath) {
222
+ return `${windowsPermissionPath(relativePath)}\\\\**`;
223
+ }
224
+
225
+ function assertSafeRunName(value) {
226
+ if (typeof value !== "string" || value.trim() === "") {
227
+ throw new Error("Run name must not be empty.");
228
+ }
229
+ if (value !== value.trim()) {
230
+ throw new Error("Run name must not contain leading or trailing whitespace.");
231
+ }
232
+ if (path.isAbsolute(value) || value.includes("/") || value.includes("\\")) {
233
+ throw new Error("Run name must be a plain directory name, not a path.");
234
+ }
235
+ if (value.includes("..")) {
236
+ throw new Error("Run name must not contain '..'.");
237
+ }
238
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value)) {
239
+ throw new Error("Run name may contain only letters, numbers, dot, underscore and hyphen, and must start with a letter or number.");
240
+ }
241
+ }
242
+
243
+ function assertPlainRunId(value) {
244
+ if (typeof value !== "string" || value.trim() === "") {
245
+ throw new Error("Run id must not be empty when --run-root is not provided.");
246
+ }
247
+ assertSafeRunName(value);
248
+ }
249
+
250
+ function getSafeWorkUnitKey(value) {
251
+ if (typeof value !== "string" || value.trim() === "") {
252
+ throw new Error("Work unit id must not be empty.");
253
+ }
254
+
255
+ const trimmed = value.trim();
256
+ if (/^work-unit-[A-Za-z0-9_.-]+$/.test(trimmed)) {
257
+ return trimmed.toLowerCase();
258
+ }
259
+ if (/^\d+$/.test(trimmed)) {
260
+ return `work-unit-${String(Number.parseInt(trimmed, 10)).padStart(2, "0")}`;
261
+ }
262
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
263
+ throw new Error("Work unit id may contain only letters, numbers, dot, underscore and hyphen, and must start with a letter or number.");
264
+ }
265
+ return `work-unit-${trimmed.toLowerCase()}`;
266
+ }
267
+
268
+ function ensureInside(targetRoot, candidate, label) {
269
+ const relative = path.relative(targetRoot, candidate);
270
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
271
+ return;
272
+ }
273
+ throw new Error(`${label} resolves outside the target repository.`);
274
+ }
275
+
276
+ function resolveInsideTarget(targetRoot, candidatePath, label) {
277
+ const resolved = path.isAbsolute(candidatePath) ? path.resolve(candidatePath) : path.resolve(targetRoot, candidatePath);
278
+ ensureInside(targetRoot, resolved, label);
279
+ return resolved;
280
+ }
281
+
282
+ function toDisplayPath(targetRoot, fullPath) {
283
+ const relative = path.relative(targetRoot, fullPath);
284
+ if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
285
+ return toPosix(relative || ".");
286
+ }
287
+ return toPosix(fullPath);
288
+ }
289
+
290
+ function readJson(filePath, label) {
291
+ try {
292
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
293
+ } catch (error) {
294
+ throw new Error(`Failed to read ${label} at ${filePath}: ${error.message}`);
295
+ }
296
+ }
297
+
298
+ function readJsonIfExists(filePath, label) {
299
+ if (!fs.existsSync(filePath)) {
300
+ return null;
301
+ }
302
+ return readJson(filePath, label);
303
+ }
304
+
305
+ function writeJson(filePath, value) {
306
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
307
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
308
+ }
309
+
310
+ function writeTextIfSafe(filePath, content, force) {
311
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
312
+ if (fs.existsSync(filePath)) {
313
+ const current = fs.readFileSync(filePath, "utf8");
314
+ if (current !== content && !force) {
315
+ throw new Error(`Refusing to overwrite existing file: ${filePath}. Use --force to replace it.`);
316
+ }
317
+ }
318
+ fs.writeFileSync(filePath, content, "utf8");
319
+ }
320
+
321
+ function writeJsonIfSafe(filePath, value, force) {
322
+ writeTextIfSafe(filePath, `${JSON.stringify(value, null, 2)}\n`, force);
323
+ }
324
+
325
+ function readConfig(targetRoot) {
326
+ const configPath = path.join(targetRoot, CONFIG_FILE);
327
+ if (!fs.existsSync(configPath)) {
328
+ throw new Error(`Missing ${CONFIG_FILE}. Run wefter init first.`);
329
+ }
330
+
331
+ const config = readJson(configPath, CONFIG_FILE);
332
+ return normalizeConfig(config);
333
+ }
334
+
335
+ function normalizeConfig(config) {
336
+ assertObject(config, CONFIG_FILE);
337
+ assertAllowedKeys(config, CONFIG_FILE, ["$schema", "version", "workflowRoot", "profilePath", "artifactRoot", "templateRoot", "processDocPath", "runnerCommand", "workflows"]);
338
+
339
+ if (config.version !== 1) {
340
+ throw new Error(`${CONFIG_FILE} must have version: 1.`);
341
+ }
342
+
343
+ const workflowRoot = normalizeRelativePath(config.workflowRoot || DEFAULTS.workflowRoot, "workflowRoot");
344
+ const workflows = config.workflows || defaultWorkflowRegistry();
345
+ normalizeWorkflowRegistry(workflows);
346
+
347
+ return {
348
+ version: 1,
349
+ workflowRoot,
350
+ profilePath: normalizeRelativePath(config.profilePath, "profilePath"),
351
+ artifactRoot: normalizeRelativePath(config.artifactRoot, "artifactRoot"),
352
+ templateRoot: normalizeRelativePath(config.templateRoot, "templateRoot"),
353
+ processDocPath: normalizeRelativePath(config.processDocPath, "processDocPath"),
354
+ runnerCommand: normalizeRunnerCommand(config.runnerCommand, "runnerCommand"),
355
+ workflows
356
+ };
357
+ }
358
+
359
+ function defaultWorkflowRegistry() {
360
+ return {
361
+ "product-shaping": { status: "planned", enabled: false },
362
+ "documentation-audit": { status: "available", enabled: true },
363
+ "documentation-repair": { status: "available", enabled: true },
364
+ "technical-shaping": { status: "planned", enabled: false },
365
+ "work-unit-implementation": {
366
+ status: "available",
367
+ enabled: true,
368
+ configPath: ".wefter/workflows/work-unit-implementation/config.json",
369
+ profilePath: ".wefter/workflows/work-unit-implementation/profile.json"
370
+ }
371
+ };
372
+ }
373
+
374
+ function workflowSettings(config, workflowId) {
375
+ const settings = config.workflows?.[workflowId];
376
+ if (!settings) {
377
+ throw new Error(`Missing workflow settings for ${workflowId}.`);
378
+ }
379
+ return settings;
380
+ }
381
+
382
+ function workUnitConfigPath(config, flags = {}) {
383
+ const settings = workflowSettings(config, WORK_UNIT_WORKFLOW_ID);
384
+ return normalizeRelativePath(flags["config-path"] || settings.configPath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/config.json`, "work-unit config path");
385
+ }
386
+
387
+ function workUnitProfilePath(config, flags = {}) {
388
+ const settings = workflowSettings(config, WORK_UNIT_WORKFLOW_ID);
389
+ return normalizeRelativePath(flags["profile-path"] || settings.profilePath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/profile.json`, "work-unit profile path");
390
+ }
391
+
392
+ function normalizeWorkflowRegistry(workflows) {
393
+ assertObject(workflows, "workflows");
394
+ for (const [workflowId, workflow] of Object.entries(workflows)) {
395
+ requireId(workflowId, `workflows.${workflowId}`);
396
+ assertObject(workflow, `workflows.${workflowId}`);
397
+ assertAllowedKeys(workflow, `workflows.${workflowId}`, ["status", "enabled", "profilePath", "configPath"]);
398
+ if (!["available", "planned"].includes(workflow.status)) {
399
+ throw new Error(`workflows.${workflowId}.status must be available or planned.`);
400
+ }
401
+ if (typeof workflow.enabled !== "boolean") {
402
+ throw new Error(`workflows.${workflowId}.enabled must be boolean.`);
403
+ }
404
+ for (const key of ["profilePath", "configPath"]) {
405
+ if (workflow[key] !== undefined) {
406
+ normalizeRelativePath(workflow[key], `workflows.${workflowId}.${key}`);
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ async function promptForValue(rl, label, defaultValue) {
413
+ const answer = await rl.question(`${label} (${defaultValue}): `);
414
+ return answer.trim() === "" ? defaultValue : answer.trim();
415
+ }
416
+
417
+ function renderTemplate(content, values) {
418
+ let result = content;
419
+ for (const [key, value] of Object.entries(values)) {
420
+ result = result.replaceAll(`{{${key}}}`, String(value));
421
+ }
422
+ return result;
423
+ }
424
+
425
+ function copyRenderedTemplate(source, destination, values, force) {
426
+ const content = fs.readFileSync(source, "utf8");
427
+ const rendered = renderTemplate(content, values);
428
+ writeTextIfSafe(destination, rendered, force);
429
+ }
430
+
431
+ function copyDirectory(sourceRoot, destinationRoot, force) {
432
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
433
+ const source = path.join(sourceRoot, entry.name);
434
+ const destination = path.join(destinationRoot, entry.name);
435
+ if (entry.isDirectory()) {
436
+ copyDirectory(source, destination, force);
437
+ continue;
438
+ }
439
+ const content = fs.readFileSync(source, "utf8");
440
+ writeTextIfSafe(destination, content, force);
441
+ }
442
+ }
443
+
444
+ function mergeOpencodeConfig(targetRoot, config, force) {
445
+ const opencodePath = path.join(targetRoot, "opencode.json");
446
+ const existing = fs.existsSync(opencodePath) ? readJson(opencodePath, "opencode.json") : { "$schema": "https://opencode.ai/config.json" };
447
+ const workUnitConfig = readJsonIfExists(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
448
+
449
+ existing["$schema"] = existing["$schema"] || "https://opencode.ai/config.json";
450
+ existing.watcher = existing.watcher || {};
451
+ existing.watcher.ignore = Array.isArray(existing.watcher.ignore) ? existing.watcher.ignore : [];
452
+ for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot]) {
453
+ if (!ignored) {
454
+ continue;
455
+ }
456
+ const pattern = `${ignored.replace(/\/$/, "")}/**`;
457
+ if (!existing.watcher.ignore.includes(pattern)) {
458
+ existing.watcher.ignore.push(pattern);
459
+ }
460
+ }
461
+
462
+ existing.skills = existing.skills || {};
463
+ existing.skills.paths = Array.isArray(existing.skills.paths) ? existing.skills.paths : [];
464
+ if (!existing.skills.paths.includes(".opencode/skills")) {
465
+ existing.skills.paths.push(".opencode/skills");
466
+ }
467
+
468
+ existing.command = existing.command || {};
469
+ const fullRunCommand = {
470
+ description: "Run the Wefter documentation audit workflow end-to-end.",
471
+ agent: "wefter-doc-audit-orchestrator",
472
+ 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.`
473
+ };
474
+ const generateProfileCommand = {
475
+ description: "Inspect the repository and create or update the Wefter documentation audit profile.",
476
+ agent: "wefter-doc-audit-profile-builder",
477
+ 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.`
478
+ };
479
+ const workUnitCommand = {
480
+ description: "Run the Wefter work-unit implementation workflow: plan, review, gate, implement tasks, review tasks, and validate.",
481
+ agent: "wefter-work-unit-orchestrator",
482
+ 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.`
483
+ };
484
+ const repairDocsCommand = {
485
+ description: "Run the Wefter documentation repair workflow from a validated audit report.",
486
+ agent: "wefter-doc-repair-orchestrator",
487
+ 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.`
488
+ };
489
+
490
+ for (const [name, nextValue] of Object.entries({
491
+ "wefter-audit-docs": fullRunCommand,
492
+ "wefter-generate-doc-audit-profile": generateProfileCommand,
493
+ "wefter-repair-docs": repairDocsCommand,
494
+ "wefter-run-work-unit": workUnitCommand
495
+ })) {
496
+ if (existing.command[name] && JSON.stringify(existing.command[name]) !== JSON.stringify(nextValue) && !force) {
497
+ throw new Error(`Refusing to overwrite existing opencode command '${name}'. Use --force to replace it.`);
498
+ }
499
+ existing.command[name] = nextValue;
500
+ }
501
+
502
+ writeJson(opencodePath, existing);
503
+ }
504
+
505
+ function unique(values) {
506
+ return [...new Set(values.filter(Boolean))];
507
+ }
508
+
509
+ function defaultProfile(config = DEFAULTS) {
510
+ return {
511
+ version: 1,
512
+ corpus: {
513
+ include: ["*.md", "docs/**/*.md"],
514
+ exclude: unique([
515
+ "node_modules/**",
516
+ ".git/**",
517
+ ".opencode/**",
518
+ `${config.artifactRoot}/**`,
519
+ `${documentationRepairArtifactRoot()}/**`,
520
+ `${config.templateRoot}/**`,
521
+ config.processDocPath
522
+ ])
523
+ },
524
+ variants: [
525
+ {
526
+ id: "explicit-contradictions",
527
+ title: "Explicit contradictions",
528
+ 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."
529
+ },
530
+ {
531
+ id: "implicit-incompatibilities",
532
+ title: "Implicit incompatibilities",
533
+ instruction: "Find statements that look compatible in isolation but conflict when combined. Evaluate preconditions, consequences, dependencies and operational impact."
534
+ },
535
+ {
536
+ id: "staleness-and-drift",
537
+ title: "Staleness and drift",
538
+ instruction: "Find signs that a document is outdated compared with newer decisions, terminology, status or implementation direction."
539
+ },
540
+ {
541
+ id: "adversarial-edge-cases",
542
+ title: "Adversarial edge cases",
543
+ instruction: "Look for gaps in error states, validation, concurrency, permissions, traceability, security, incomplete data and documented limits."
544
+ }
545
+ ],
546
+ lenses: [
547
+ {
548
+ id: "documentation-map-consistency",
549
+ title: "Documentation map consistency",
550
+ focus: "Compare index, overview, roadmap, technical and domain documents. Look for missing cross-references, duplicated responsibilities and contradictions between high-level and detailed documents."
551
+ },
552
+ {
553
+ id: "requirements-vs-technical-guidance",
554
+ title: "Requirements vs technical guidance",
555
+ 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."
556
+ },
557
+ {
558
+ id: "terminology-and-responsibility",
559
+ title: "Terminology and responsibility",
560
+ focus: "Compare glossary, naming, roles, permissions, ownership and document responsibility. Look for inconsistent terms or rules described in the wrong place."
561
+ }
562
+ ]
563
+ };
564
+ }
565
+
566
+ function validateProfile(profile) {
567
+ assertObject(profile, "Profile");
568
+ assertAllowedKeys(profile, "Profile", ["version", "corpus", "variants", "lenses"]);
569
+
570
+ if (profile.version !== 1) {
571
+ throw new Error("Profile must have version: 1.");
572
+ }
573
+
574
+ assertObject(profile.corpus, "Profile corpus");
575
+ assertAllowedKeys(profile.corpus, "Profile corpus", ["include", "exclude"]);
576
+ requireStringArray(profile.corpus.include, "Profile corpus.include");
577
+ requireStringArray(profile.corpus.exclude, "Profile corpus.exclude");
578
+
579
+ if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
580
+ throw new Error("Profile must define at least one variant.");
581
+ }
582
+ profile.variants.forEach((variant, index) => {
583
+ assertObject(variant, `Profile variants[${index}]`);
584
+ assertAllowedKeys(variant, `Profile variants[${index}]`, ["id", "title", "instruction"]);
585
+ requireId(variant.id, `Profile variants[${index}].id`);
586
+ requireString(variant.title, `Profile variants[${index}].title`);
587
+ requireString(variant.instruction, `Profile variants[${index}].instruction`);
588
+ });
589
+ assertUniqueIds(profile.variants, "Profile variants");
590
+
591
+ if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
592
+ throw new Error("Profile must define at least one lens.");
593
+ }
594
+ profile.lenses.forEach((lens, index) => {
595
+ assertObject(lens, `Profile lenses[${index}]`);
596
+ assertAllowedKeys(lens, `Profile lenses[${index}]`, ["id", "title", "focus"]);
597
+ requireId(lens.id, `Profile lenses[${index}].id`);
598
+ requireString(lens.title, `Profile lenses[${index}].title`);
599
+ requireString(lens.focus, `Profile lenses[${index}].focus`);
600
+ });
601
+ assertUniqueIds(profile.lenses, "Profile lenses");
602
+ }
603
+
604
+ function validateWorkUnitConfig(config) {
605
+ assertObject(config, "Work-unit config");
606
+ assertAllowedKeys(config, "Work-unit config", ["version", "workflowName", "releaseId", "workUnitsDocument", "sourceDocs", "runArtifactsRoot", "versionedArtifacts", "defaultWorkUnitId", "defaultPlanAuditPassesPerLens", "gatePolicy"]);
607
+
608
+ if (config.version !== 1) {
609
+ throw new Error("Work-unit config must have version: 1.");
610
+ }
611
+ requireString(config.workflowName, "Work-unit config.workflowName");
612
+ if (config.workflowName !== WORK_UNIT_WORKFLOW_ID) {
613
+ throw new Error(`Work-unit config.workflowName must be ${WORK_UNIT_WORKFLOW_ID}.`);
614
+ }
615
+ requireString(config.releaseId, "Work-unit config.releaseId");
616
+ normalizeRelativePath(config.workUnitsDocument, "Work-unit config.workUnitsDocument");
617
+ normalizeRelativePath(config.runArtifactsRoot, "Work-unit config.runArtifactsRoot");
618
+ requireString(config.defaultWorkUnitId, "Work-unit config.defaultWorkUnitId");
619
+
620
+ const passes = Number.parseInt(String(config.defaultPlanAuditPassesPerLens), 10);
621
+ if (!Number.isInteger(passes) || passes < 1) {
622
+ throw new Error("Work-unit config.defaultPlanAuditPassesPerLens must be an integer greater than 0.");
623
+ }
624
+
625
+ assertObject(config.sourceDocs, "Work-unit config.sourceDocs");
626
+ assertAllowedKeys(config.sourceDocs, "Work-unit config.sourceDocs", ["include", "exclude"]);
627
+ requireStringArray(config.sourceDocs.include, "Work-unit config.sourceDocs.include");
628
+ requireStringArray(config.sourceDocs.exclude, "Work-unit config.sourceDocs.exclude");
629
+
630
+ assertObject(config.versionedArtifacts, "Work-unit config.versionedArtifacts");
631
+ assertAllowedKeys(config.versionedArtifacts, "Work-unit config.versionedArtifacts", ["executionRoot", "decisionLogRoot"]);
632
+ normalizeRelativePath(config.versionedArtifacts.executionRoot, "Work-unit config.versionedArtifacts.executionRoot");
633
+ normalizeRelativePath(config.versionedArtifacts.decisionLogRoot, "Work-unit config.versionedArtifacts.decisionLogRoot");
634
+
635
+ assertObject(config.gatePolicy, "Work-unit config.gatePolicy");
636
+ assertAllowedKeys(config.gatePolicy, "Work-unit config.gatePolicy", ["mode", "structuralWorkUnits", "alwaysPauseOn"]);
637
+ requireString(config.gatePolicy.mode, "Work-unit config.gatePolicy.mode");
638
+ requireStringArray(config.gatePolicy.structuralWorkUnits, "Work-unit config.gatePolicy.structuralWorkUnits");
639
+ requireStringArray(config.gatePolicy.alwaysPauseOn, "Work-unit config.gatePolicy.alwaysPauseOn");
640
+ }
641
+
642
+ function validateWorkUnitProfile(profile) {
643
+ assertObject(profile, "Work-unit profile");
644
+ assertAllowedKeys(profile, "Work-unit profile", ["version", "variants", "lenses"]);
645
+
646
+ if (profile.version !== 1) {
647
+ throw new Error("Work-unit profile must have version: 1.");
648
+ }
649
+
650
+ if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
651
+ throw new Error("Work-unit profile must define at least one variant.");
652
+ }
653
+ profile.variants.forEach((variant, index) => {
654
+ assertObject(variant, `Work-unit profile variants[${index}]`);
655
+ assertAllowedKeys(variant, `Work-unit profile variants[${index}]`, ["id", "title", "instruction"]);
656
+ requireId(variant.id, `Work-unit profile variants[${index}].id`);
657
+ requireString(variant.title, `Work-unit profile variants[${index}].title`);
658
+ requireString(variant.instruction, `Work-unit profile variants[${index}].instruction`);
659
+ });
660
+ assertUniqueIds(profile.variants, "Work-unit profile variants");
661
+
662
+ if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
663
+ throw new Error("Work-unit profile must define at least one lens.");
664
+ }
665
+ profile.lenses.forEach((lens, index) => {
666
+ assertObject(lens, `Work-unit profile lenses[${index}]`);
667
+ assertAllowedKeys(lens, `Work-unit profile lenses[${index}]`, ["id", "title", "focus"]);
668
+ requireId(lens.id, `Work-unit profile lenses[${index}].id`);
669
+ requireString(lens.title, `Work-unit profile lenses[${index}].title`);
670
+ requireString(lens.focus, `Work-unit profile lenses[${index}].focus`);
671
+ });
672
+ assertUniqueIds(profile.lenses, "Work-unit profile lenses");
673
+ }
674
+
675
+ function markdownList(items) {
676
+ if (!items || items.length === 0) {
677
+ return "- <none>";
678
+ }
679
+ return items.map((item) => `- \`${item}\``).join("\n");
680
+ }
681
+
682
+ function timestampRunName() {
683
+ const now = new Date();
684
+ const pad = (n) => String(n).padStart(2, "0");
685
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
686
+ }
687
+
688
+ function buildCombinations(profile, passesPerLens, maxAudits) {
689
+ const combinations = [];
690
+ let auditIndex = 1;
691
+
692
+ for (const lens of profile.lenses) {
693
+ for (const variant of profile.variants) {
694
+ for (let pass = 1; pass <= passesPerLens; pass++) {
695
+ if (maxAudits > 0 && combinations.length >= maxAudits) {
696
+ return combinations;
697
+ }
698
+ const auditId = `A${String(auditIndex).padStart(4, "0")}__${lens.id}__${variant.id}__p${String(pass).padStart(2, "0")}`;
699
+ combinations.push({ auditId, lens, variant, pass });
700
+ auditIndex++;
701
+ }
702
+ }
703
+ }
704
+
705
+ return combinations;
706
+ }
707
+
708
+ async function commandInit(flags) {
709
+ const targetRoot = resolveTarget(flags);
710
+ fs.mkdirSync(targetRoot, { recursive: true });
711
+ ensureInside(path.dirname(targetRoot), targetRoot, "target");
712
+
713
+ let profilePath = flags["profile-path"] || DEFAULTS.profilePath;
714
+ let artifactRoot = flags["artifact-root"] || DEFAULTS.artifactRoot;
715
+ const workflowRoot = DEFAULTS.workflowRoot;
716
+ const templateRoot = flags["template-root"] || DEFAULTS.templateRoot;
717
+ const processDocPath = flags["process-doc-path"] || DEFAULTS.processDocPath;
718
+ const runnerCommand = flags["runner-command"] || defaultRunnerCommand();
719
+
720
+ if (!flags.yes && process.stdin.isTTY) {
721
+ const rl = readline.createInterface({ input, output });
722
+ profilePath = await promptForValue(rl, "Audit profile path", profilePath);
723
+ artifactRoot = await promptForValue(rl, "Audit artifact root", artifactRoot);
724
+ rl.close();
725
+ }
726
+
727
+ const config = normalizeConfig({
728
+ version: 1,
729
+ workflowRoot,
730
+ profilePath,
731
+ artifactRoot,
732
+ templateRoot,
733
+ processDocPath,
734
+ runnerCommand,
735
+ workflows: defaultWorkflowRegistry()
736
+ });
737
+
738
+ const values = {
739
+ PROFILE_PATH: config.profilePath,
740
+ ARTIFACT_ROOT: config.artifactRoot,
741
+ ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(config.artifactRoot),
742
+ TEMPLATE_ROOT: config.templateRoot,
743
+ PROCESS_DOC_PATH: config.processDocPath,
744
+ RUNNER_COMMAND: config.runnerCommand,
745
+ CONFIG_FILE,
746
+ RUNNER_COMMAND_NEW_RUN_PATTERN: yamlSingleQuoted(`${config.runnerCommand}*`),
747
+ RUNNER_COMMAND_DOCS_REPAIR_PATTERN: yamlSingleQuoted(`${config.runnerCommand} docs repair*`),
748
+ DOCUMENTATION_REPAIR_ARTIFACT_ROOT: documentationRepairArtifactRoot(),
749
+ DOCUMENTATION_REPAIR_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(documentationRepairArtifactRoot()),
750
+ RUNNER_COMMAND_WORK_UNIT_PATTERN: yamlSingleQuoted(`${config.runnerCommand} work-unit*`),
751
+ WORK_UNIT_ARTIFACT_ROOT: ".audit/wefter/work-unit-implementation",
752
+ WORK_UNIT_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(".audit/wefter/work-unit-implementation"),
753
+ WORK_UNIT_CONFIG_PATH: ".wefter/workflows/work-unit-implementation/config.json",
754
+ WORK_UNIT_PROFILE_PATH: ".wefter/workflows/work-unit-implementation/profile.json"
755
+ };
756
+
757
+ writeJsonIfSafe(path.join(targetRoot, CONFIG_FILE), {
758
+ "$schema": "./node_modules/@wefter/opencode/schemas/wefter.config.schema.json",
759
+ ...config
760
+ }, flags.force);
761
+
762
+ const root = packageRoot();
763
+ const auditTemplates = documentationAuditTemplateRoot();
764
+ const workUnitPackageRoot = workUnitWorkflowPackageRoot();
765
+ copyRenderedTemplate(path.join(root, "src/workflows/documentation-audit/workflow.json"), path.join(targetRoot, config.workflowRoot, "documentation-audit/workflow.json"), values, flags.force);
766
+ for (const workflowId of ["product-shaping", "documentation-repair", "technical-shaping", "work-unit-implementation"]) {
767
+ copyDirectory(path.join(root, "src/workflows", workflowId), path.join(targetRoot, config.workflowRoot, workflowId), flags.force);
768
+ }
769
+ copyDirectory(path.join(workUnitPackageRoot, "templates/prompts"), path.join(targetRoot, config.workflowRoot, WORK_UNIT_WORKFLOW_ID, "templates/prompts"), flags.force);
770
+ writeJsonIfSafe(path.join(targetRoot, workUnitConfigPath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-config.json"), "default work-unit config"), flags.force);
771
+ writeJsonIfSafe(path.join(targetRoot, workUnitProfilePath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-profile.json"), "default work-unit profile"), flags.force);
772
+ 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);
773
+ copyRenderedTemplate(path.join(auditTemplates, "opencode/agent/wefter-doc-auditor.md.tmpl"), path.join(targetRoot, ".opencode/agent/wefter-doc-auditor.md"), values, flags.force);
774
+ 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);
775
+ 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);
776
+ 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);
777
+ copyRenderedTemplate(path.join(auditTemplates, "opencode/skills/documentation-audit/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/documentation-audit/SKILL.md"), values, flags.force);
778
+ const repairTemplates = documentationRepairTemplateRoot();
779
+ for (const agentFile of ["wefter-doc-repair-orchestrator", "wefter-doc-repair-planner", "wefter-doc-repairer", "wefter-doc-repair-reviewer"]) {
780
+ copyRenderedTemplate(path.join(repairTemplates, "opencode/agent", `${agentFile}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `${agentFile}.md`), values, flags.force);
781
+ }
782
+ copyRenderedTemplate(path.join(repairTemplates, "opencode/skills/documentation-repair/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md"), values, flags.force);
783
+ for (const agent of ["orchestrator", "planner", "plan-auditor", "plan-consolidator", "plan-validator", "plan-repairer", "task-implementer", "task-reviewer", "validator"]) {
784
+ 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);
785
+ }
786
+ 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);
787
+ copyDirectory(path.join(auditTemplates, "prompts"), path.join(targetRoot, config.templateRoot), flags.force);
788
+ copyRenderedTemplate(path.join(auditTemplates, "README.md.tmpl"), path.join(targetRoot, config.processDocPath), values, flags.force);
789
+ mergeOpencodeConfig(targetRoot, config, flags.force);
790
+
791
+ const profileFullPath = path.join(targetRoot, config.profilePath);
792
+ if (!fs.existsSync(profileFullPath)) {
793
+ writeJson(profileFullPath, defaultProfile(config));
794
+ }
795
+
796
+ console.log(`Installed Wefter for OpenCode into ${targetRoot}`);
797
+ console.log(`Profile: ${config.profilePath}`);
798
+ console.log(`Artifacts: ${config.artifactRoot}`);
799
+ console.log(`Runner command: ${config.runnerCommand}`);
800
+ console.log(`Tip: add ${config.artifactRoot}/ to .gitignore if you do not want to track generated audit runs.`);
801
+ console.log("Restart opencode before using /wefter-audit-docs, /wefter-generate-doc-audit-profile, /wefter-repair-docs, or /wefter-run-work-unit.");
802
+ }
803
+
804
+ function readTextRequired(filePath) {
805
+ if (!fs.existsSync(filePath)) {
806
+ throw new Error(`Missing ${filePath}`);
807
+ }
808
+ return fs.readFileSync(filePath, "utf8");
809
+ }
810
+
811
+ function assertNoPlaceholders(filePath, content) {
812
+ const match = content.match(/{{[^}]+}}/);
813
+ if (match) {
814
+ throw new Error(`${filePath} contains unresolved placeholder ${match[0]}.`);
815
+ }
816
+ }
817
+
818
+ function assertIncludes(content, expected, label) {
819
+ if (!content.includes(expected)) {
820
+ throw new Error(`Missing ${label}: ${expected}`);
821
+ }
822
+ }
823
+
824
+ function commandNewRun(flags) {
825
+ const targetRoot = resolveTarget(flags);
826
+ const config = readConfig(targetRoot);
827
+ const profilePathRelative = normalizeRelativePath(flags["profile-path"] || config.profilePath, "profilePath");
828
+ const profilePath = path.join(targetRoot, profilePathRelative);
829
+ ensureInside(targetRoot, profilePath, "profilePath");
830
+ const profile = readJson(profilePath, "audit profile");
831
+ validateProfile(profile);
832
+
833
+ const passesPerLens = Number.parseInt(flags["passes-per-lens"] || "3", 10);
834
+ const maxAudits = Number.parseInt(flags["max-audits"] || "0", 10);
835
+ if (!Number.isInteger(passesPerLens) || passesPerLens < 1) {
836
+ throw new Error("--passes-per-lens must be an integer greater than 0.");
837
+ }
838
+ if (!Number.isInteger(maxAudits) || maxAudits < 0) {
839
+ throw new Error("--max-audits must be an integer greater than or equal to 0.");
840
+ }
841
+
842
+ const runName = flags["run-name"] || timestampRunName();
843
+ assertSafeRunName(runName);
844
+ const combinations = buildCombinations(profile, passesPerLens, maxAudits);
845
+
846
+ const artifactRoot = path.join(targetRoot, config.artifactRoot);
847
+ const tempRoot = path.join(artifactRoot, ".tmp");
848
+ const runRoot = path.join(artifactRoot, runName);
849
+ const stagingRunRoot = path.join(tempRoot, runName);
850
+ ensureInside(targetRoot, artifactRoot, "artifactRoot");
851
+ ensureInside(targetRoot, runRoot, "runRoot");
852
+ ensureInside(targetRoot, stagingRunRoot, "stagingRunRoot");
853
+
854
+ if (flags["dry-run"]) {
855
+ console.log(`Run name: ${runName}`);
856
+ console.log(`Lenses: ${profile.lenses.length}`);
857
+ console.log(`Variants: ${profile.variants.length}`);
858
+ console.log(`Passes per lens/variant: ${passesPerLens}`);
859
+ console.log(`Auditor prompts to generate: ${combinations.length}`);
860
+ console.log(`Output root: ${runRoot}`);
861
+ return;
862
+ }
863
+
864
+ if (fs.existsSync(runRoot)) {
865
+ throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name to avoid mixing stale prompts or outputs.`);
866
+ }
867
+ if (fs.existsSync(stagingRunRoot)) {
868
+ 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.`);
869
+ }
870
+
871
+ const promptsRoot = path.join(stagingRunRoot, "prompts");
872
+ const auditorPromptsRoot = path.join(promptsRoot, "auditors");
873
+ const rawRoot = path.join(stagingRunRoot, "raw");
874
+ const consolidationRoot = path.join(stagingRunRoot, "consolidation");
875
+ const validationRoot = path.join(stagingRunRoot, "validation");
876
+ const finalRoot = path.join(stagingRunRoot, "final");
877
+ for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, auditorPromptsRoot, rawRoot, consolidationRoot, validationRoot, finalRoot]) {
878
+ fs.mkdirSync(directory, { recursive: true });
879
+ }
880
+
881
+ const templateRoot = path.join(targetRoot, config.templateRoot);
882
+ const auditorTemplate = fs.readFileSync(path.join(templateRoot, "auditor-prompt.md"), "utf8");
883
+ const consolidatorTemplate = fs.readFileSync(path.join(templateRoot, "consolidator-prompt.md"), "utf8");
884
+ const validatorTemplate = fs.readFileSync(path.join(templateRoot, "validator-prompt.md"), "utf8");
885
+ const promptRecords = [];
886
+
887
+ for (const combo of combinations) {
888
+ const outputRelative = toPosix(path.join(config.artifactRoot, runName, "raw", `${combo.auditId}.md`));
889
+ const promptRelative = toPosix(path.join(config.artifactRoot, runName, "prompts", "auditors", `${combo.auditId}.md`));
890
+ const prompt = renderTemplate(auditorTemplate, {
891
+ RUN_ID: runName,
892
+ AUDIT_ID: combo.auditId,
893
+ LENS_ID: combo.lens.id,
894
+ LENS_TITLE: combo.lens.title,
895
+ LENS_FOCUS: combo.lens.focus,
896
+ VARIANT_ID: combo.variant.id,
897
+ VARIANT_TITLE: combo.variant.title,
898
+ VARIANT_INSTRUCTION: combo.variant.instruction,
899
+ PASS_NUMBER: combo.pass,
900
+ OUTPUT_FILE: outputRelative,
901
+ CORPUS_INCLUDE: markdownList(profile.corpus.include),
902
+ CORPUS_EXCLUDE: markdownList(profile.corpus.exclude)
903
+ });
904
+ fs.writeFileSync(path.join(auditorPromptsRoot, `${combo.auditId}.md`), prompt, "utf8");
905
+ promptRecords.push({
906
+ auditId: combo.auditId,
907
+ lensId: combo.lens.id,
908
+ variantId: combo.variant.id,
909
+ pass: combo.pass,
910
+ prompt: promptRelative,
911
+ output: outputRelative
912
+ });
913
+ }
914
+
915
+ const consolidatedRelative = toPosix(path.join(config.artifactRoot, runName, "consolidation", "consolidated-candidates.md"));
916
+ const discardedRelative = toPosix(path.join(config.artifactRoot, runName, "consolidation", "discarded-raw-findings.md"));
917
+ const validationRelative = toPosix(path.join(config.artifactRoot, runName, "validation", "adversarial-validation-log.md"));
918
+ const finalRelative = toPosix(path.join(config.artifactRoot, runName, "final", "final-documentation-audit-report.md"));
919
+ const rawRelative = toPosix(path.join(config.artifactRoot, runName, "raw"));
920
+
921
+ fs.writeFileSync(path.join(promptsRoot, "consolidate.md"), renderTemplate(consolidatorTemplate, {
922
+ RUN_ID: runName,
923
+ RAW_DIR: rawRelative,
924
+ CONSOLIDATED_OUTPUT: consolidatedRelative,
925
+ DISCARDED_OUTPUT: discardedRelative
926
+ }), "utf8");
927
+ fs.writeFileSync(path.join(promptsRoot, "validate.md"), renderTemplate(validatorTemplate, {
928
+ RUN_ID: runName,
929
+ CONSOLIDATED_OUTPUT: consolidatedRelative,
930
+ VALIDATION_OUTPUT: validationRelative,
931
+ FINAL_OUTPUT: finalRelative
932
+ }), "utf8");
933
+
934
+ writeJson(path.join(stagingRunRoot, "manifest.json"), {
935
+ version: 1,
936
+ workflowId: "documentation-audit",
937
+ runId: runName,
938
+ generatedAt: new Date().toISOString(),
939
+ passesPerLens,
940
+ maxAudits,
941
+ profilePath: profilePathRelative,
942
+ corpus: profile.corpus,
943
+ counts: {
944
+ lenses: profile.lenses.length,
945
+ variants: profile.variants.length,
946
+ auditorPrompts: combinations.length
947
+ },
948
+ paths: {
949
+ runRoot: toPosix(path.join(config.artifactRoot, runName)),
950
+ prompts: toPosix(path.join(config.artifactRoot, runName, "prompts")),
951
+ raw: rawRelative,
952
+ consolidation: toPosix(path.join(config.artifactRoot, runName, "consolidation")),
953
+ validation: toPosix(path.join(config.artifactRoot, runName, "validation")),
954
+ final: toPosix(path.join(config.artifactRoot, runName, "final"))
955
+ },
956
+ prompts: promptRecords
957
+ });
958
+
959
+ 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");
960
+
961
+ if (fs.existsSync(runRoot)) {
962
+ throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
963
+ }
964
+ fs.renameSync(stagingRunRoot, runRoot);
965
+
966
+ console.log(`Created documentation audit run: ${runRoot}`);
967
+ console.log(`Auditor prompts generated: ${combinations.length}`);
968
+ console.log(`Next prompt directory: ${path.join(runRoot, "prompts", "auditors")}`);
969
+ }
970
+
971
+ function commandDocsRepair(flags) {
972
+ const targetRoot = resolveTarget(flags);
973
+ const config = readConfig(targetRoot);
974
+ if (!flags["audit-report"]) {
975
+ throw new Error("--audit-report is required for docs repair.");
976
+ }
977
+ const auditReportPath = normalizeRelativePath(flags["audit-report"], "audit-report");
978
+ const auditReportFullPath = path.join(targetRoot, auditReportPath);
979
+ ensureInside(targetRoot, auditReportFullPath, "audit report");
980
+ if (!fs.existsSync(auditReportFullPath)) {
981
+ throw new Error(`Audit report not found: ${auditReportFullPath}`);
982
+ }
983
+
984
+ const runName = flags["run-name"] || timestampRunName();
985
+ assertSafeRunName(runName);
986
+
987
+ const artifactRootRelative = documentationRepairArtifactRoot();
988
+ const artifactRoot = path.join(targetRoot, artifactRootRelative);
989
+ const tempRoot = path.join(artifactRoot, ".tmp");
990
+ const runRoot = path.join(artifactRoot, runName);
991
+ const stagingRunRoot = path.join(tempRoot, runName);
992
+ ensureInside(targetRoot, artifactRoot, "documentation repair artifact root");
993
+ ensureInside(targetRoot, runRoot, "documentation repair run root");
994
+ ensureInside(targetRoot, stagingRunRoot, "documentation repair staging run root");
995
+
996
+ if (flags["dry-run"]) {
997
+ console.log(`Run name: ${runName}`);
998
+ console.log(`Audit report: ${auditReportPath}`);
999
+ console.log(`Output root: ${runRoot}`);
1000
+ return;
1001
+ }
1002
+
1003
+ if (fs.existsSync(runRoot)) {
1004
+ throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
1005
+ }
1006
+ if (fs.existsSync(stagingRunRoot)) {
1007
+ 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.`);
1008
+ }
1009
+
1010
+ const runRootRelative = toPosix(path.join(artifactRootRelative, runName));
1011
+ const promptsRoot = path.join(stagingRunRoot, "prompts");
1012
+ const planningRoot = path.join(stagingRunRoot, "planning");
1013
+ const repairRoot = path.join(stagingRunRoot, "repair");
1014
+ const reviewRoot = path.join(stagingRunRoot, "review");
1015
+ const finalRoot = path.join(stagingRunRoot, "final");
1016
+ for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, planningRoot, repairRoot, reviewRoot, finalRoot]) {
1017
+ fs.mkdirSync(directory, { recursive: true });
1018
+ }
1019
+
1020
+ const templateRoot = path.join(documentationRepairTemplateRoot(), "prompts");
1021
+ const planTemplate = fs.readFileSync(path.join(templateRoot, "repair-plan-prompt.md"), "utf8");
1022
+ const applyTemplate = fs.readFileSync(path.join(templateRoot, "repair-apply-prompt.md"), "utf8");
1023
+ const reviewTemplate = fs.readFileSync(path.join(templateRoot, "repair-review-prompt.md"), "utf8");
1024
+ const repairPlan = toPosix(path.join(runRootRelative, "planning", "documentation-repair-plan.md"));
1025
+ const humanDecisions = toPosix(path.join(runRootRelative, "planning", "human-decisions.md"));
1026
+ const repairLog = toPosix(path.join(runRootRelative, "repair", "repair-log.md"));
1027
+ const reviewOutput = toPosix(path.join(runRootRelative, "review", "repair-review.md"));
1028
+ const finalSummary = toPosix(path.join(runRootRelative, "final", "documentation-repair-summary.md"));
1029
+ const profile = readJsonIfExists(path.join(targetRoot, config.profilePath), "audit profile");
1030
+ const baseValues = {
1031
+ RUN_ID: runName,
1032
+ RUN_ROOT: runRootRelative,
1033
+ AUDIT_REPORT: auditReportPath,
1034
+ REPAIR_PLAN_OUTPUT: repairPlan,
1035
+ HUMAN_DECISIONS_OUTPUT: humanDecisions,
1036
+ REPAIR_LOG_OUTPUT: repairLog,
1037
+ REVIEW_OUTPUT: reviewOutput,
1038
+ FINAL_SUMMARY_OUTPUT: finalSummary,
1039
+ CORPUS_INCLUDE: markdownList(profile?.corpus?.include || ["*.md", "docs/**/*.md"]),
1040
+ CORPUS_EXCLUDE: markdownList(profile?.corpus?.exclude || ["node_modules/**", ".git/**", ".audit/**", ".opencode/**"])
1041
+ };
1042
+
1043
+ fs.writeFileSync(path.join(promptsRoot, "plan-repair.md"), renderTemplate(planTemplate, baseValues), "utf8");
1044
+ fs.writeFileSync(path.join(promptsRoot, "apply-repair.md"), renderTemplate(applyTemplate, baseValues), "utf8");
1045
+ fs.writeFileSync(path.join(promptsRoot, "review-repair.md"), renderTemplate(reviewTemplate, baseValues), "utf8");
1046
+
1047
+ writeJson(path.join(stagingRunRoot, "manifest.json"), {
1048
+ version: 1,
1049
+ workflowId: DOCUMENTATION_REPAIR_WORKFLOW_ID,
1050
+ runId: runName,
1051
+ generatedAt: new Date().toISOString(),
1052
+ auditReport: auditReportPath,
1053
+ paths: {
1054
+ runRoot: runRootRelative,
1055
+ prompts: toPosix(path.join(runRootRelative, "prompts")),
1056
+ repairPlan,
1057
+ humanDecisions,
1058
+ repairLog,
1059
+ reviewOutput,
1060
+ finalSummary
1061
+ },
1062
+ prompts: {
1063
+ planRepair: toPosix(path.join(runRootRelative, "prompts", "plan-repair.md")),
1064
+ applyRepair: toPosix(path.join(runRootRelative, "prompts", "apply-repair.md")),
1065
+ reviewRepair: toPosix(path.join(runRootRelative, "prompts", "review-repair.md"))
1066
+ }
1067
+ });
1068
+
1069
+ 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");
1070
+
1071
+ if (fs.existsSync(runRoot)) {
1072
+ throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
1073
+ }
1074
+ fs.renameSync(stagingRunRoot, runRoot);
1075
+
1076
+ console.log(`Created documentation repair run: ${runRoot}`);
1077
+ console.log(`Audit report: ${auditReportPath}`);
1078
+ console.log(`Next prompt: ${path.join(runRoot, "prompts", "plan-repair.md")}`);
1079
+ }
1080
+
1081
+ function commandWorkUnitRun(flags) {
1082
+ const targetRoot = resolveTarget(flags);
1083
+ const wefterConfig = readConfig(targetRoot);
1084
+ const configPath = workUnitConfigPath(wefterConfig, flags);
1085
+ const profilePath = workUnitProfilePath(wefterConfig, flags);
1086
+ const workUnitConfig = readJson(path.join(targetRoot, configPath), "work-unit config");
1087
+ const profile = readJson(path.join(targetRoot, profilePath), "work-unit profile");
1088
+ validateWorkUnitConfig(workUnitConfig);
1089
+ validateWorkUnitProfile(profile);
1090
+
1091
+ const workUnitId = flags["work-unit-id"] || workUnitConfig.defaultWorkUnitId;
1092
+ const workUnitKey = getSafeWorkUnitKey(workUnitId);
1093
+ const passesPerLens = Number.parseInt(flags["passes-per-lens"] || String(workUnitConfig.defaultPlanAuditPassesPerLens), 10);
1094
+ const maxAudits = Number.parseInt(flags["max-audits"] || "0", 10);
1095
+ if (!Number.isInteger(passesPerLens) || passesPerLens < 1) {
1096
+ throw new Error("--passes-per-lens must be an integer greater than 0.");
1097
+ }
1098
+ if (!Number.isInteger(maxAudits) || maxAudits < 0) {
1099
+ throw new Error("--max-audits must be an integer greater than or equal to 0.");
1100
+ }
1101
+
1102
+ const runName = flags["run-name"] || `${timestampRunName()}__${workUnitKey}`;
1103
+ assertSafeRunName(runName);
1104
+ const combinations = buildCombinations(profile, passesPerLens, maxAudits).map((combo, index) => ({
1105
+ ...combo,
1106
+ auditId: `P${String(index + 1).padStart(4, "0")}__${combo.lens.id}__${combo.variant.id}__p${String(combo.pass).padStart(2, "0")}`
1107
+ }));
1108
+
1109
+ const artifactRoot = path.join(targetRoot, workUnitConfig.runArtifactsRoot);
1110
+ const tempRoot = path.join(artifactRoot, ".tmp");
1111
+ const runRoot = path.join(artifactRoot, runName);
1112
+ const stagingRunRoot = path.join(tempRoot, runName);
1113
+ ensureInside(targetRoot, artifactRoot, "work-unit runArtifactsRoot");
1114
+ ensureInside(targetRoot, runRoot, "work-unit runRoot");
1115
+ ensureInside(targetRoot, stagingRunRoot, "work-unit stagingRunRoot");
1116
+
1117
+ const runRootRelative = toPosix(path.join(workUnitConfig.runArtifactsRoot, runName));
1118
+ const versionedWorkUnitDir = toPosix(path.join(workUnitConfig.versionedArtifacts.executionRoot, workUnitKey));
1119
+ const versionedTaskSpecsDir = toPosix(path.join(versionedWorkUnitDir, "task-specs"));
1120
+ const versionedDecisionLog = toPosix(path.join(workUnitConfig.versionedArtifacts.decisionLogRoot, `${workUnitKey}-decisions.md`));
1121
+
1122
+ if (flags["dry-run"]) {
1123
+ console.log(`Run name: ${runName}`);
1124
+ console.log(`Work unit: ${workUnitKey}`);
1125
+ console.log(`Lenses: ${profile.lenses.length}`);
1126
+ console.log(`Variants: ${profile.variants.length}`);
1127
+ console.log(`Passes per lens/variant: ${passesPerLens}`);
1128
+ console.log(`Plan auditor prompts to generate: ${combinations.length}`);
1129
+ console.log(`Output root: ${runRoot}`);
1130
+ console.log(`Versioned work-unit dir: ${versionedWorkUnitDir}`);
1131
+ return;
1132
+ }
1133
+
1134
+ if (fs.existsSync(runRoot)) {
1135
+ throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
1136
+ }
1137
+ if (fs.existsSync(stagingRunRoot)) {
1138
+ throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no run is in progress, or use a different --run-name.`);
1139
+ }
1140
+
1141
+ const promptsRoot = path.join(stagingRunRoot, "prompts");
1142
+ const planAuditorPromptsRoot = path.join(promptsRoot, "plan-auditors", workUnitKey);
1143
+ const planningRoot = path.join(stagingRunRoot, "planning");
1144
+ const draftRoot = path.join(planningRoot, "draft");
1145
+ const draftTaskSpecsRoot = path.join(draftRoot, "task-specs");
1146
+ const finalRoot = path.join(stagingRunRoot, "final");
1147
+ const candidateRoot = path.join(finalRoot, "approved-artifacts");
1148
+ const candidateWorkUnitRoot = path.join(candidateRoot, workUnitKey);
1149
+ const candidateTaskSpecsRoot = path.join(candidateWorkUnitRoot, "task-specs");
1150
+ const rawPlanAuditsRoot = path.join(stagingRunRoot, "raw", "plan-audits");
1151
+ const consolidationRoot = path.join(stagingRunRoot, "consolidation");
1152
+ const validationRoot = path.join(stagingRunRoot, "validation");
1153
+ const implementationRoot = path.join(stagingRunRoot, "implementation");
1154
+ const taskLogRoot = path.join(implementationRoot, "task-logs");
1155
+ const taskReviewRoot = path.join(implementationRoot, "task-reviews");
1156
+ for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, planAuditorPromptsRoot, planningRoot, draftRoot, draftTaskSpecsRoot, finalRoot, candidateRoot, candidateWorkUnitRoot, candidateTaskSpecsRoot, rawPlanAuditsRoot, consolidationRoot, validationRoot, implementationRoot, taskLogRoot, taskReviewRoot]) {
1157
+ fs.mkdirSync(directory, { recursive: true });
1158
+ }
1159
+
1160
+ const templateRoot = path.join(targetRoot, wefterConfig.workflowRoot, WORK_UNIT_WORKFLOW_ID, "templates", "prompts");
1161
+ const templates = {
1162
+ planner: fs.readFileSync(path.join(templateRoot, "planner-prompt.md"), "utf8"),
1163
+ planAuditor: fs.readFileSync(path.join(templateRoot, "plan-auditor-prompt.md"), "utf8"),
1164
+ consolidator: fs.readFileSync(path.join(templateRoot, "plan-consolidator-prompt.md"), "utf8"),
1165
+ validator: fs.readFileSync(path.join(templateRoot, "plan-validator-prompt.md"), "utf8"),
1166
+ repairer: fs.readFileSync(path.join(templateRoot, "plan-repairer-prompt.md"), "utf8"),
1167
+ taskImplementation: fs.readFileSync(path.join(templateRoot, "task-implementation-prompt.md"), "utf8"),
1168
+ taskReview: fs.readFileSync(path.join(templateRoot, "task-review-prompt.md"), "utf8"),
1169
+ workUnitValidator: fs.readFileSync(path.join(templateRoot, "work-unit-validator-prompt.md"), "utf8")
1170
+ };
1171
+
1172
+ const draftPlan = toPosix(path.join(runRootRelative, "planning", "draft", "work-unit-plan.md"));
1173
+ const draftTraceability = toPosix(path.join(runRootRelative, "planning", "draft", "traceability-matrix.md"));
1174
+ const draftVerification = toPosix(path.join(runRootRelative, "planning", "draft", "verification-plan.md"));
1175
+ const draftGate = toPosix(path.join(runRootRelative, "planning", "draft", "gate-assessment.md"));
1176
+ const draftDecisions = toPosix(path.join(runRootRelative, "planning", "draft", "decisions-draft.md"));
1177
+ const draftTaskSpecs = toPosix(path.join(runRootRelative, "planning", "draft", "task-specs"));
1178
+ const candidatePlan = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "work-unit-plan.md"));
1179
+ const candidateTraceability = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "traceability-matrix.md"));
1180
+ const candidateVerification = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "verification-plan.md"));
1181
+ const candidateGate = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "gate-assessment.md"));
1182
+ const candidateDecisions = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, `${workUnitKey}-decisions.md`));
1183
+ const candidateTaskSpecs = toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey, "task-specs"));
1184
+ const repairSummary = toPosix(path.join(runRootRelative, "final", "plan-repair-summary.md"));
1185
+ const rawPlanAudits = toPosix(path.join(runRootRelative, "raw", "plan-audits"));
1186
+ const consolidatedOutput = toPosix(path.join(runRootRelative, "consolidation", "consolidated-plan-candidates.md"));
1187
+ const discardedOutput = toPosix(path.join(runRootRelative, "consolidation", "discarded-plan-findings.md"));
1188
+ const validationOutput = toPosix(path.join(runRootRelative, "validation", "plan-adversarial-validation-log.md"));
1189
+ const finalPlanReview = toPosix(path.join(runRootRelative, "final", "final-plan-review-report.md"));
1190
+ const workUnitValidation = toPosix(path.join(runRootRelative, "final", "work-unit-validation.md"));
1191
+ const taskLogDir = toPosix(path.join(runRootRelative, "implementation", "task-logs"));
1192
+ const taskReviewDir = toPosix(path.join(runRootRelative, "implementation", "task-reviews"));
1193
+ const versionedWorkUnitPlan = toPosix(path.join(versionedWorkUnitDir, "work-unit-plan.md"));
1194
+ const versionedTraceability = toPosix(path.join(versionedWorkUnitDir, "traceability-matrix.md"));
1195
+ const versionedVerification = toPosix(path.join(versionedWorkUnitDir, "verification-plan.md"));
1196
+
1197
+ const baseValues = {
1198
+ RUN_ID: runName,
1199
+ WORK_UNIT_ID: workUnitId,
1200
+ WORK_UNIT_KEY: workUnitKey,
1201
+ RELEASE_ID: workUnitConfig.releaseId,
1202
+ CONFIG_PATH: configPath,
1203
+ PROFILE_PATH: profilePath,
1204
+ RUN_ROOT: runRootRelative,
1205
+ WORK_UNITS_DOCUMENT: workUnitConfig.workUnitsDocument,
1206
+ SOURCE_INCLUDE: markdownList(workUnitConfig.sourceDocs.include),
1207
+ SOURCE_EXCLUDE: markdownList(workUnitConfig.sourceDocs.exclude),
1208
+ DRAFT_PLAN_OUTPUT: draftPlan,
1209
+ DRAFT_TRACEABILITY_OUTPUT: draftTraceability,
1210
+ DRAFT_VERIFICATION_OUTPUT: draftVerification,
1211
+ DRAFT_GATE_OUTPUT: draftGate,
1212
+ DRAFT_DECISIONS_OUTPUT: draftDecisions,
1213
+ DRAFT_TASK_SPECS_DIR: draftTaskSpecs,
1214
+ CANDIDATE_PLAN_OUTPUT: candidatePlan,
1215
+ CANDIDATE_TRACEABILITY_OUTPUT: candidateTraceability,
1216
+ CANDIDATE_VERIFICATION_OUTPUT: candidateVerification,
1217
+ CANDIDATE_GATE_OUTPUT: candidateGate,
1218
+ CANDIDATE_DECISIONS_OUTPUT: candidateDecisions,
1219
+ CANDIDATE_TASK_SPECS_DIR: candidateTaskSpecs,
1220
+ REPAIR_SUMMARY_OUTPUT: repairSummary,
1221
+ RAW_PLAN_AUDITS_DIR: rawPlanAudits,
1222
+ CONSOLIDATED_OUTPUT: consolidatedOutput,
1223
+ DISCARDED_OUTPUT: discardedOutput,
1224
+ VALIDATION_OUTPUT: validationOutput,
1225
+ FINAL_PLAN_REVIEW_OUTPUT: finalPlanReview,
1226
+ VERSIONED_WORK_UNIT_DIR: versionedWorkUnitDir,
1227
+ VERSIONED_TASK_SPECS_DIR: versionedTaskSpecsDir,
1228
+ VERSIONED_WORK_UNIT_PLAN: versionedWorkUnitPlan,
1229
+ VERSIONED_TRACEABILITY_MATRIX: versionedTraceability,
1230
+ VERSIONED_VERIFICATION_PLAN: versionedVerification,
1231
+ VERSIONED_DECISION_LOG: versionedDecisionLog,
1232
+ TASK_LOG_DIR: taskLogDir,
1233
+ TASK_REVIEW_DIR: taskReviewDir,
1234
+ WORK_UNIT_VALIDATION_OUTPUT: workUnitValidation
1235
+ };
1236
+
1237
+ fs.writeFileSync(path.join(promptsRoot, "plan.md"), renderTemplate(templates.planner, baseValues), "utf8");
1238
+ fs.writeFileSync(path.join(promptsRoot, "consolidate-plan.md"), renderTemplate(templates.consolidator, baseValues), "utf8");
1239
+ fs.writeFileSync(path.join(promptsRoot, "validate-plan.md"), renderTemplate(templates.validator, baseValues), "utf8");
1240
+ fs.writeFileSync(path.join(promptsRoot, "repair-plan.md"), renderTemplate(templates.repairer, baseValues), "utf8");
1241
+ fs.writeFileSync(path.join(promptsRoot, "implement-tasks.md"), renderTemplate(templates.taskImplementation, baseValues), "utf8");
1242
+ fs.writeFileSync(path.join(promptsRoot, "review-task.md"), renderTemplate(templates.taskReview, baseValues), "utf8");
1243
+ fs.writeFileSync(path.join(promptsRoot, "validate-work-unit.md"), renderTemplate(templates.workUnitValidator, baseValues), "utf8");
1244
+
1245
+ const promptRecords = [];
1246
+ for (const combo of combinations) {
1247
+ const outputRelative = toPosix(path.join(runRootRelative, "raw", "plan-audits", `${combo.auditId}.md`));
1248
+ const promptRelative = toPosix(path.join(runRootRelative, "prompts", "plan-auditors", workUnitKey, `${combo.auditId}.md`));
1249
+ const prompt = renderTemplate(templates.planAuditor, {
1250
+ ...baseValues,
1251
+ AUDIT_ID: combo.auditId,
1252
+ LENS_ID: combo.lens.id,
1253
+ LENS_TITLE: combo.lens.title,
1254
+ LENS_FOCUS: combo.lens.focus,
1255
+ VARIANT_ID: combo.variant.id,
1256
+ VARIANT_TITLE: combo.variant.title,
1257
+ VARIANT_INSTRUCTION: combo.variant.instruction,
1258
+ PASS_NUMBER: combo.pass,
1259
+ OUTPUT_FILE: outputRelative
1260
+ });
1261
+ fs.writeFileSync(path.join(planAuditorPromptsRoot, `${combo.auditId}.md`), prompt, "utf8");
1262
+ promptRecords.push({
1263
+ auditId: combo.auditId,
1264
+ lensId: combo.lens.id,
1265
+ variantId: combo.variant.id,
1266
+ pass: combo.pass,
1267
+ prompt: promptRelative,
1268
+ output: outputRelative
1269
+ });
1270
+ }
1271
+
1272
+ writeJson(path.join(stagingRunRoot, "manifest.json"), {
1273
+ version: 1,
1274
+ workflowId: WORK_UNIT_WORKFLOW_ID,
1275
+ runId: runName,
1276
+ workUnitId,
1277
+ workUnitKey,
1278
+ releaseId: workUnitConfig.releaseId,
1279
+ generatedAt: new Date().toISOString(),
1280
+ configPath,
1281
+ profilePath,
1282
+ passesPerLens,
1283
+ maxAudits,
1284
+ gatePolicy: workUnitConfig.gatePolicy,
1285
+ counts: {
1286
+ lenses: profile.lenses.length,
1287
+ variants: profile.variants.length,
1288
+ planAuditorPrompts: combinations.length
1289
+ },
1290
+ paths: {
1291
+ runRoot: runRootRelative,
1292
+ prompts: toPosix(path.join(runRootRelative, "prompts")),
1293
+ planPrompt: toPosix(path.join(runRootRelative, "prompts", "plan.md")),
1294
+ planAuditorPrompts: toPosix(path.join(runRootRelative, "prompts", "plan-auditors", workUnitKey)),
1295
+ rawPlanAudits,
1296
+ consolidation: toPosix(path.join(runRootRelative, "consolidation")),
1297
+ validation: toPosix(path.join(runRootRelative, "validation")),
1298
+ final: toPosix(path.join(runRootRelative, "final")),
1299
+ candidateArtifacts: toPosix(path.join(runRootRelative, "final", "approved-artifacts", workUnitKey)),
1300
+ versionedWorkUnitDir,
1301
+ versionedDecisionLog
1302
+ },
1303
+ prompts: {
1304
+ plan: toPosix(path.join(runRootRelative, "prompts", "plan.md")),
1305
+ planAudits: promptRecords,
1306
+ consolidatePlan: toPosix(path.join(runRootRelative, "prompts", "consolidate-plan.md")),
1307
+ validatePlan: toPosix(path.join(runRootRelative, "prompts", "validate-plan.md")),
1308
+ repairPlan: toPosix(path.join(runRootRelative, "prompts", "repair-plan.md")),
1309
+ implementTasks: toPosix(path.join(runRootRelative, "prompts", "implement-tasks.md")),
1310
+ reviewTask: toPosix(path.join(runRootRelative, "prompts", "review-task.md")),
1311
+ validateWorkUnit: toPosix(path.join(runRootRelative, "prompts", "validate-work-unit.md"))
1312
+ }
1313
+ });
1314
+
1315
+ fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Work Unit Implementation Run\n\nRun: ${runName}\nWork unit: ${workUnitKey}\nRelease: ${workUnitConfig.releaseId}\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");
1316
+
1317
+ if (fs.existsSync(runRoot)) {
1318
+ throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
1319
+ }
1320
+ fs.renameSync(stagingRunRoot, runRoot);
1321
+
1322
+ console.log(`Created work-unit implementation run: ${runRoot}`);
1323
+ console.log(`Work unit: ${workUnitKey}`);
1324
+ console.log(`Plan auditor prompts generated: ${combinations.length}`);
1325
+ console.log(`Next prompt: ${path.join(runRoot, "prompts", "plan.md")}`);
1326
+ }
1327
+
1328
+ function requireProperty(object, name, context) {
1329
+ if (!object || typeof object !== "object" || Array.isArray(object) || !(name in object)) {
1330
+ throw new Error(`${context} is missing required property '${name}'.`);
1331
+ }
1332
+ return object[name];
1333
+ }
1334
+
1335
+ function getReviewMachineResult(reviewPath, expectedTaskId) {
1336
+ const content = fs.readFileSync(reviewPath, "utf8");
1337
+ const match = content.match(/##\s+Machine Result\s*```json\s*(\{[\s\S]*?\})\s*```/i);
1338
+ if (!match) {
1339
+ throw new Error(`Review '${reviewPath}' must contain a '## Machine Result' section with a fenced json object.`);
1340
+ }
1341
+
1342
+ let machine;
1343
+ try {
1344
+ machine = JSON.parse(match[1]);
1345
+ } catch (error) {
1346
+ throw new Error(`Review '${reviewPath}' contains invalid Machine Result JSON: ${error.message}`);
1347
+ }
1348
+
1349
+ const taskId = String(requireProperty(machine, "taskId", `Machine Result in '${reviewPath}'`));
1350
+ const result = String(requireProperty(machine, "result", `Machine Result in '${reviewPath}'`));
1351
+ const reviewIteration = requireProperty(machine, "reviewIteration", `Machine Result in '${reviewPath}'`);
1352
+ const blockingFindings = requireProperty(machine, "blockingFindings", `Machine Result in '${reviewPath}'`);
1353
+
1354
+ if (taskId !== expectedTaskId) {
1355
+ throw new Error(`Review '${reviewPath}' Machine Result taskId '${taskId}' does not match expected task '${expectedTaskId}'.`);
1356
+ }
1357
+ if (!["Pass", "Needs Fix", "Blocked"].includes(result)) {
1358
+ throw new Error(`Review '${reviewPath}' Machine Result result must be one of: Pass, Needs Fix, Blocked.`);
1359
+ }
1360
+ const iterationNumber = Number.parseInt(String(reviewIteration), 10);
1361
+ if (!Number.isInteger(iterationNumber) || iterationNumber < 1) {
1362
+ throw new Error(`Review '${reviewPath}' Machine Result reviewIteration must be an integer >= 1.`);
1363
+ }
1364
+ if (!Array.isArray(blockingFindings)) {
1365
+ throw new Error(`Review '${reviewPath}' Machine Result blockingFindings must be an array.`);
1366
+ }
1367
+ if ((result === "Needs Fix" || result === "Blocked") && blockingFindings.length === 0) {
1368
+ throw new Error(`Review '${reviewPath}' Machine Result must list blockingFindings when result is '${result}'.`);
1369
+ }
1370
+
1371
+ return { taskId, result, reviewIteration: iterationNumber, blockingFindings };
1372
+ }
1373
+
1374
+ function getTaskIds(taskSpecsDir) {
1375
+ if (!fs.existsSync(taskSpecsDir)) {
1376
+ throw new Error(`Task specs directory not found: ${taskSpecsDir}`);
1377
+ }
1378
+ const entries = fs.readdirSync(taskSpecsDir, { withFileTypes: true })
1379
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
1380
+ .map((entry) => entry.name)
1381
+ .sort();
1382
+ if (entries.length === 0) {
1383
+ throw new Error(`No task spec files found in: ${taskSpecsDir}`);
1384
+ }
1385
+ return entries.map((name) => {
1386
+ const taskId = path.basename(name, ".md");
1387
+ if (!/^T\d{2}-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(taskId)) {
1388
+ throw new Error(`Task spec file '${path.join(taskSpecsDir, name)}' does not use the required task id format TXX-YYY.`);
1389
+ }
1390
+ return taskId;
1391
+ });
1392
+ }
1393
+
1394
+ function getTaskState(targetRoot, taskId, taskLogDir, taskReviewDir) {
1395
+ const logPath = path.join(taskLogDir, `${taskId}.md`);
1396
+ const reviewPath = path.join(taskReviewDir, `${taskId}.md`);
1397
+ const logExists = fs.existsSync(logPath);
1398
+ const reviewExists = fs.existsSync(reviewPath);
1399
+ let reviewResult = null;
1400
+ let reviewIteration = null;
1401
+ let blockingFindings = [];
1402
+ let state = "NotImplemented";
1403
+ let reason = "Task implementation log is missing.";
1404
+
1405
+ if (logExists && !reviewExists) {
1406
+ state = "AwaitingReview";
1407
+ reason = "Task implementation log exists, but review is missing.";
1408
+ }
1409
+
1410
+ if (logExists && reviewExists) {
1411
+ const logStat = fs.statSync(logPath);
1412
+ const reviewStat = fs.statSync(reviewPath);
1413
+ const machine = getReviewMachineResult(reviewPath, taskId);
1414
+ reviewResult = machine.result;
1415
+ reviewIteration = machine.reviewIteration;
1416
+ blockingFindings = machine.blockingFindings;
1417
+
1418
+ if (reviewStat.mtimeMs < logStat.mtimeMs) {
1419
+ state = "AwaitingReview";
1420
+ reason = "Task implementation log is newer than the review; review is stale.";
1421
+ } else if (reviewResult === "Pass") {
1422
+ state = "Passed";
1423
+ reason = "Task review passed.";
1424
+ } else if (reviewResult === "Needs Fix") {
1425
+ state = "NeedsFix";
1426
+ reason = "Task review requires correction before another task can start.";
1427
+ } else if (reviewResult === "Blocked") {
1428
+ state = "Blocked";
1429
+ reason = "Task review is blocked.";
1430
+ }
1431
+ }
1432
+
1433
+ return {
1434
+ taskId,
1435
+ state,
1436
+ reason,
1437
+ logExists,
1438
+ reviewExists,
1439
+ reviewResult,
1440
+ reviewIteration,
1441
+ blockingFindings,
1442
+ logPath: toDisplayPath(targetRoot, logPath),
1443
+ reviewPath: toDisplayPath(targetRoot, reviewPath)
1444
+ };
1445
+ }
1446
+
1447
+ function assertKnownTask(taskIds, taskId, mode) {
1448
+ if (typeof taskId !== "string" || taskId.trim() === "") {
1449
+ throw new Error(`--task-id is required for mode '${mode}'.`);
1450
+ }
1451
+ if (!taskIds.includes(taskId)) {
1452
+ throw new Error(`Task id '${taskId}' is not present in the approved task specs.`);
1453
+ }
1454
+ }
1455
+
1456
+ function assertPreviousTasksPassed(taskIds, states, taskId) {
1457
+ const targetIndex = taskIds.indexOf(taskId);
1458
+ if (targetIndex < 0) {
1459
+ throw new Error(`Task id '${taskId}' is not present in the approved task specs.`);
1460
+ }
1461
+ for (let index = 0; index < targetIndex; index++) {
1462
+ const previousTaskId = taskIds[index];
1463
+ const previousState = states.find((state) => state.taskId === previousTaskId);
1464
+ if (previousState?.state !== "Passed") {
1465
+ throw new Error(`Task '${taskId}' cannot proceed because previous task '${previousTaskId}' is '${previousState?.state}'. Reason: ${previousState?.reason}`);
1466
+ }
1467
+ }
1468
+ }
1469
+
1470
+ function getLoopStatus(states) {
1471
+ for (const state of states) {
1472
+ if (state.state === "Blocked") {
1473
+ return { result: "Blocked", action: "StopForHumanOrSpecDecision", taskId: state.taskId, reason: state.reason };
1474
+ }
1475
+ if (state.state === "NeedsFix") {
1476
+ return { result: "NeedsAction", action: "FixTask", taskId: state.taskId, reason: state.reason };
1477
+ }
1478
+ if (state.state === "NotImplemented") {
1479
+ return { result: "NeedsAction", action: "ImplementTask", taskId: state.taskId, reason: state.reason };
1480
+ }
1481
+ if (state.state === "AwaitingReview") {
1482
+ return { result: "NeedsAction", action: "ReviewTask", taskId: state.taskId, reason: state.reason };
1483
+ }
1484
+ }
1485
+ return { result: "Ready", action: "RunFinalWorkUnitValidation", taskId: null, reason: "All approved tasks have passing, non-stale reviews." };
1486
+ }
1487
+
1488
+ function writeGuardResult(status, states, json) {
1489
+ if (json) {
1490
+ console.log(JSON.stringify({ status, tasks: states }, null, 2));
1491
+ return;
1492
+ }
1493
+ console.log(`Result: ${status.result}`);
1494
+ console.log(`Action: ${status.action}`);
1495
+ if (status.taskId) {
1496
+ console.log(`Task: ${status.taskId}`);
1497
+ }
1498
+ console.log(`Reason: ${status.reason}`);
1499
+ }
1500
+
1501
+ function commandWorkUnitGuard(flags) {
1502
+ const targetRoot = resolveTarget(flags);
1503
+ const wefterConfig = readConfig(targetRoot);
1504
+ const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(wefterConfig, flags)), "work-unit config");
1505
+ validateWorkUnitConfig(workUnitConfig);
1506
+
1507
+ const mode = flags.mode || "Status";
1508
+ if (!["Status", "ReadyForReview", "ReadyForNextTask", "ReadyForFinalValidation"].includes(mode)) {
1509
+ throw new Error("--mode must be one of: Status, ReadyForReview, ReadyForNextTask, ReadyForFinalValidation.");
1510
+ }
1511
+
1512
+ let runRoot;
1513
+ if (flags["run-root"]) {
1514
+ runRoot = resolveInsideTarget(targetRoot, flags["run-root"], "run root");
1515
+ } else {
1516
+ assertPlainRunId(flags["run-id"]);
1517
+ runRoot = resolveInsideTarget(targetRoot, path.join(workUnitConfig.runArtifactsRoot, flags["run-id"]), "run root");
1518
+ }
1519
+ if (!fs.existsSync(runRoot)) {
1520
+ throw new Error(`Run root not found: ${runRoot}`);
1521
+ }
1522
+
1523
+ const manifestPath = path.join(runRoot, "manifest.json");
1524
+ const manifest = readJson(manifestPath, "work-unit run manifest");
1525
+ if (manifest.workflowId !== WORK_UNIT_WORKFLOW_ID) {
1526
+ throw new Error(`Run manifest workflowId must be ${WORK_UNIT_WORKFLOW_ID}.`);
1527
+ }
1528
+ const versionedWorkUnitDir = requireProperty(requireProperty(manifest, "paths", "manifest"), "versionedWorkUnitDir", "manifest.paths");
1529
+ const taskSpecsDir = resolveInsideTarget(targetRoot, path.join(versionedWorkUnitDir, "task-specs"), "task specs directory");
1530
+ const taskLogDir = path.join(runRoot, "implementation", "task-logs");
1531
+ const taskReviewDir = path.join(runRoot, "implementation", "task-reviews");
1532
+ if (!fs.existsSync(taskLogDir)) {
1533
+ throw new Error(`Task log directory not found: ${taskLogDir}`);
1534
+ }
1535
+ if (!fs.existsSync(taskReviewDir)) {
1536
+ throw new Error(`Task review directory not found: ${taskReviewDir}`);
1537
+ }
1538
+
1539
+ const taskIds = getTaskIds(taskSpecsDir);
1540
+ const states = taskIds.map((taskId) => getTaskState(targetRoot, taskId, taskLogDir, taskReviewDir));
1541
+ const status = getLoopStatus(states);
1542
+
1543
+ if (mode === "Status") {
1544
+ writeGuardResult(status, states, flags.json);
1545
+ return;
1546
+ }
1547
+
1548
+ const taskId = flags["task-id"];
1549
+ if (mode === "ReadyForReview") {
1550
+ assertKnownTask(taskIds, taskId, mode);
1551
+ assertPreviousTasksPassed(taskIds, states, taskId);
1552
+ const state = states.find((item) => item.taskId === taskId);
1553
+ if (!state.logExists) {
1554
+ throw new Error(`Task '${taskId}' is not ready for review because its implementation log is missing.`);
1555
+ }
1556
+ if (state.state === "NeedsFix") {
1557
+ throw new Error(`Task '${taskId}' still needs a correction. Update its implementation log after the correction before requesting another review.`);
1558
+ }
1559
+ if (state.state === "Blocked") {
1560
+ throw new Error(`Task '${taskId}' is blocked and cannot be reviewed as ready.`);
1561
+ }
1562
+ writeGuardResult({ result: "Ready", action: "ReviewTask", taskId, reason: `Task '${taskId}' has an implementation log and can be reviewed.` }, states, flags.json);
1563
+ return;
1564
+ }
1565
+
1566
+ if (mode === "ReadyForNextTask") {
1567
+ assertKnownTask(taskIds, taskId, mode);
1568
+ assertPreviousTasksPassed(taskIds, states, taskId);
1569
+ const state = states.find((item) => item.taskId === taskId);
1570
+ if (state.state !== "Passed") {
1571
+ throw new Error(`Task '${taskId}' cannot release the next task. Current state: ${state.state}. Reason: ${state.reason}`);
1572
+ }
1573
+ writeGuardResult({ result: "Ready", action: "AdvanceToNextTaskOrFinalValidation", taskId, reason: `Task '${taskId}' has a passing, non-stale review.` }, states, flags.json);
1574
+ return;
1575
+ }
1576
+
1577
+ if (mode === "ReadyForFinalValidation") {
1578
+ if (status.result !== "Ready") {
1579
+ throw new Error(`Work unit is not ready for final validation. Next required action: ${status.action} for task '${status.taskId}'. Reason: ${status.reason}`);
1580
+ }
1581
+ writeGuardResult(status, states, flags.json);
1582
+ }
1583
+ }
1584
+
1585
+ function commandProfileScaffold(flags) {
1586
+ const targetRoot = resolveTarget(flags);
1587
+ const config = readConfig(targetRoot);
1588
+ const profilePath = path.join(targetRoot, config.profilePath);
1589
+ if (fs.existsSync(profilePath) && !flags.force) {
1590
+ throw new Error(`Profile already exists: ${profilePath}. Use --force to overwrite.`);
1591
+ }
1592
+ writeJson(profilePath, defaultProfile(config));
1593
+ console.log(`Wrote starter audit profile: ${profilePath}`);
1594
+ }
1595
+
1596
+ function commandProfileImport(flags) {
1597
+ const targetRoot = resolveTarget(flags);
1598
+ const config = readConfig(targetRoot);
1599
+ if (!flags.source) {
1600
+ throw new Error("--source is required for profile import.");
1601
+ }
1602
+
1603
+ const sourceRelative = normalizeRelativePath(flags.source, "source");
1604
+ const sourcePath = path.join(targetRoot, sourceRelative);
1605
+ const destinationPath = path.join(targetRoot, config.profilePath);
1606
+ ensureInside(targetRoot, sourcePath, "source");
1607
+ ensureInside(targetRoot, destinationPath, "profilePath");
1608
+
1609
+ if (!fs.existsSync(sourcePath)) {
1610
+ throw new Error(`Source profile not found: ${sourcePath}`);
1611
+ }
1612
+ if (fs.existsSync(destinationPath) && !flags.force) {
1613
+ throw new Error(`Profile already exists: ${destinationPath}. Use --force to overwrite.`);
1614
+ }
1615
+
1616
+ const profile = readJson(sourcePath, "source audit profile");
1617
+ validateProfile(profile);
1618
+ writeJson(destinationPath, profile);
1619
+ console.log(`Imported audit profile from ${sourceRelative} to ${config.profilePath}`);
1620
+ }
1621
+
1622
+ function commandDoctor(flags) {
1623
+ const targetRoot = resolveTarget(flags);
1624
+ const config = readConfig(targetRoot);
1625
+ const errors = [];
1626
+ const checks = [];
1627
+
1628
+ function check(label, fn) {
1629
+ try {
1630
+ fn();
1631
+ checks.push(`OK ${label}`);
1632
+ } catch (error) {
1633
+ errors.push(`${label}: ${error.message}`);
1634
+ }
1635
+ }
1636
+
1637
+ check(CONFIG_FILE, () => normalizeConfig(config));
1638
+ check("profile", () => validateProfile(readJson(path.join(targetRoot, config.profilePath), "audit profile")));
1639
+ check("configured paths", () => {
1640
+ for (const [label, relativePath] of Object.entries({
1641
+ profilePath: config.profilePath,
1642
+ workflowRoot: config.workflowRoot,
1643
+ artifactRoot: config.artifactRoot,
1644
+ templateRoot: config.templateRoot,
1645
+ processDocPath: config.processDocPath
1646
+ })) {
1647
+ ensureInside(targetRoot, path.join(targetRoot, relativePath), label);
1648
+ }
1649
+ });
1650
+ check("templates", () => {
1651
+ for (const file of REQUIRED_TEMPLATE_FILES) {
1652
+ const fullPath = path.join(targetRoot, config.templateRoot, file);
1653
+ if (!fs.existsSync(fullPath)) {
1654
+ throw new Error(`Missing ${fullPath}`);
1655
+ }
1656
+ }
1657
+ });
1658
+ check("workflow manifests", () => {
1659
+ for (const workflowId of Object.keys(config.workflows)) {
1660
+ const fullPath = path.join(targetRoot, config.workflowRoot, workflowId, "workflow.json");
1661
+ if (!fs.existsSync(fullPath)) {
1662
+ throw new Error(`Missing ${fullPath}`);
1663
+ }
1664
+ }
1665
+ });
1666
+ check("work-unit workflow config", () => {
1667
+ const configPath = path.join(targetRoot, workUnitConfigPath(config));
1668
+ const profilePath = path.join(targetRoot, workUnitProfilePath(config));
1669
+ validateWorkUnitConfig(readJson(configPath, "work-unit config"));
1670
+ validateWorkUnitProfile(readJson(profilePath, "work-unit profile"));
1671
+ });
1672
+ check("opencode agents", () => {
1673
+ const agentFiles = [
1674
+ "wefter-doc-audit-orchestrator.md",
1675
+ "wefter-doc-auditor.md",
1676
+ "wefter-doc-audit-consolidator.md",
1677
+ "wefter-doc-audit-validator.md",
1678
+ "wefter-doc-audit-profile-builder.md"
1679
+ ];
1680
+ const posixGlob = `${config.artifactRoot}/**`;
1681
+ const windowsGlob = windowsPermissionGlob(config.artifactRoot);
1682
+
1683
+ for (const file of agentFiles) {
1684
+ const fullPath = path.join(targetRoot, ".opencode/agent", file);
1685
+ const content = readTextRequired(fullPath);
1686
+ assertNoPlaceholders(fullPath, content);
1687
+ assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
1688
+ assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
1689
+ }
1690
+
1691
+ const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-doc-audit-orchestrator.md"));
1692
+ assertIncludes(orchestrator, CONFIG_FILE, "orchestrator config reference");
1693
+ assertIncludes(orchestrator, config.profilePath, "orchestrator profile path");
1694
+ assertIncludes(orchestrator, config.runnerCommand, "orchestrator runner command");
1695
+ });
1696
+ check("work-unit opencode agents", () => {
1697
+ const agentFiles = [
1698
+ "wefter-work-unit-orchestrator.md",
1699
+ "wefter-work-unit-planner.md",
1700
+ "wefter-work-unit-plan-auditor.md",
1701
+ "wefter-work-unit-plan-consolidator.md",
1702
+ "wefter-work-unit-plan-validator.md",
1703
+ "wefter-work-unit-plan-repairer.md",
1704
+ "wefter-work-unit-task-implementer.md",
1705
+ "wefter-work-unit-task-reviewer.md",
1706
+ "wefter-work-unit-validator.md"
1707
+ ];
1708
+ const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
1709
+ const posixGlob = `${workUnitConfig.runArtifactsRoot}/**`;
1710
+ const windowsGlob = windowsPermissionGlob(workUnitConfig.runArtifactsRoot);
1711
+
1712
+ for (const file of agentFiles) {
1713
+ const fullPath = path.join(targetRoot, ".opencode/agent", file);
1714
+ const content = readTextRequired(fullPath);
1715
+ assertNoPlaceholders(fullPath, content);
1716
+ if (!file.includes("task-implementer") && !file.includes("orchestrator")) {
1717
+ assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
1718
+ assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
1719
+ }
1720
+ }
1721
+
1722
+ const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-work-unit-orchestrator.md"));
1723
+ assertIncludes(orchestrator, CONFIG_FILE, "work-unit orchestrator config reference");
1724
+ assertIncludes(orchestrator, workUnitConfigPath(config), "work-unit orchestrator workflow config path");
1725
+ assertIncludes(orchestrator, workUnitProfilePath(config), "work-unit orchestrator workflow profile path");
1726
+ assertIncludes(orchestrator, config.runnerCommand, "work-unit orchestrator runner command");
1727
+ });
1728
+ check("documentation repair opencode agents", () => {
1729
+ const agentFiles = [
1730
+ "wefter-doc-repair-orchestrator.md",
1731
+ "wefter-doc-repair-planner.md",
1732
+ "wefter-doc-repairer.md",
1733
+ "wefter-doc-repair-reviewer.md"
1734
+ ];
1735
+ const posixGlob = `${documentationRepairArtifactRoot()}/**`;
1736
+ const windowsGlob = windowsPermissionGlob(documentationRepairArtifactRoot());
1737
+
1738
+ for (const file of agentFiles) {
1739
+ const fullPath = path.join(targetRoot, ".opencode/agent", file);
1740
+ const content = readTextRequired(fullPath);
1741
+ assertNoPlaceholders(fullPath, content);
1742
+ if (file.includes("planner") || file.includes("reviewer")) {
1743
+ assertIncludes(content, posixGlob, `${file} POSIX artifact permission`);
1744
+ assertIncludes(content, windowsGlob, `${file} Windows artifact permission`);
1745
+ }
1746
+ }
1747
+
1748
+ const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-doc-repair-orchestrator.md"));
1749
+ assertIncludes(orchestrator, CONFIG_FILE, "documentation repair orchestrator config reference");
1750
+ assertIncludes(orchestrator, documentationRepairArtifactRoot(), "documentation repair orchestrator artifact root");
1751
+ assertIncludes(orchestrator, config.runnerCommand, "documentation repair orchestrator runner command");
1752
+ });
1753
+ check("opencode skill", () => {
1754
+ const skillPath = path.join(targetRoot, ".opencode/skills/documentation-audit/SKILL.md");
1755
+ const content = readTextRequired(skillPath);
1756
+ assertNoPlaceholders(skillPath, content);
1757
+ assertIncludes(content, config.profilePath, "skill profile path");
1758
+ assertIncludes(content, config.templateRoot, "skill template root");
1759
+ assertIncludes(content, config.processDocPath, "skill process doc path");
1760
+ });
1761
+ check("work-unit opencode skill", () => {
1762
+ const skillPath = path.join(targetRoot, ".opencode/skills/work-unit-implementation/SKILL.md");
1763
+ const content = readTextRequired(skillPath);
1764
+ assertNoPlaceholders(skillPath, content);
1765
+ assertIncludes(content, workUnitConfigPath(config), "work-unit skill config path");
1766
+ assertIncludes(content, workUnitProfilePath(config), "work-unit skill profile path");
1767
+ });
1768
+ check("documentation repair opencode skill", () => {
1769
+ const skillPath = path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md");
1770
+ const content = readTextRequired(skillPath);
1771
+ assertNoPlaceholders(skillPath, content);
1772
+ assertIncludes(content, "/wefter-repair-docs", "documentation repair skill command reference");
1773
+ });
1774
+ check("opencode commands", () => {
1775
+ const opencode = readJson(path.join(targetRoot, "opencode.json"), "opencode.json");
1776
+ 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") {
1777
+ throw new Error("Missing Wefter opencode commands.");
1778
+ }
1779
+ if (!Array.isArray(opencode.skills?.paths) || !opencode.skills.paths.includes(".opencode/skills")) {
1780
+ throw new Error("Missing .opencode/skills in opencode skills.paths.");
1781
+ }
1782
+ const watcherIgnore = Array.isArray(opencode.watcher?.ignore) ? opencode.watcher.ignore : [];
1783
+ const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
1784
+ for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig.runArtifactsRoot]) {
1785
+ const pattern = `${ignored.replace(/\/$/, "")}/**`;
1786
+ if (!watcherIgnore.includes(pattern)) {
1787
+ throw new Error(`Missing opencode watcher ignore '${pattern}'.`);
1788
+ }
1789
+ }
1790
+ });
1791
+
1792
+ for (const item of checks) {
1793
+ console.log(item);
1794
+ }
1795
+ if (errors.length > 0) {
1796
+ for (const error of errors) {
1797
+ console.error(`ERROR ${error}`);
1798
+ }
1799
+ process.exitCode = 1;
1800
+ return;
1801
+ }
1802
+ console.log("Wefter installation looks healthy.");
1803
+ }
1804
+
1805
+ export async function main(argv = process.argv.slice(2)) {
1806
+ const { positional, flags } = parseArgs(argv);
1807
+ if (flags.help || positional.length === 0) {
1808
+ printHelp();
1809
+ return;
1810
+ }
1811
+ if (flags.version) {
1812
+ console.log(VERSION);
1813
+ return;
1814
+ }
1815
+
1816
+ const [command, subcommand] = positional;
1817
+ if (command === "init") {
1818
+ await commandInit(flags);
1819
+ return;
1820
+ }
1821
+ if (command === "new-run") {
1822
+ if (subcommand && subcommand !== "documentation-audit") {
1823
+ throw new Error(`Unsupported workflow for new-run: ${subcommand}`);
1824
+ }
1825
+ commandNewRun(flags);
1826
+ return;
1827
+ }
1828
+ if (command === "docs" && subcommand === "audit") {
1829
+ commandNewRun(flags);
1830
+ return;
1831
+ }
1832
+ if (command === "docs" && subcommand === "repair") {
1833
+ commandDocsRepair(flags);
1834
+ return;
1835
+ }
1836
+ if (command === "work-unit" && subcommand === "run") {
1837
+ commandWorkUnitRun(flags);
1838
+ return;
1839
+ }
1840
+ if (command === "work-unit" && subcommand === "guard") {
1841
+ commandWorkUnitGuard(flags);
1842
+ return;
1843
+ }
1844
+ if (command === "profile" && subcommand === "scaffold") {
1845
+ commandProfileScaffold(flags);
1846
+ return;
1847
+ }
1848
+ if (command === "profile" && subcommand === "import") {
1849
+ commandProfileImport(flags);
1850
+ return;
1851
+ }
1852
+ if (command === "doctor") {
1853
+ commandDoctor(flags);
1854
+ return;
1855
+ }
1856
+
1857
+ throw new Error(`Unknown command: ${positional.join(" ")}`);
1858
+ }