@wefter/opencode 0.1.0 → 0.2.1

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +23 -9
  3. package/docs/ARCHITECTURE.md +8 -12
  4. package/docs/INSTALLATION.md +2 -1
  5. package/docs/SAFETY_MODEL.md +1 -1
  6. package/docs/SELF_AUDIT.md +37 -0
  7. package/docs/WORKFLOWS.md +7 -3
  8. package/package.json +2 -2
  9. package/schemas/documentation-audit-profile.schema.json +9 -1
  10. package/schemas/product-shaping-config.schema.json +63 -0
  11. package/schemas/product-shaping-contract.schema.json +204 -0
  12. package/schemas/product-shaping-profile.schema.json +39 -0
  13. package/schemas/product-shaping-run-manifest.schema.json +103 -0
  14. package/schemas/wefter.config.schema.json +46 -15
  15. package/schemas/work-unit-config.schema.json +12 -5
  16. package/schemas/workflow-manifest.schema.json +10 -0
  17. package/src/cli/main.js +857 -19
  18. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-orchestrator.md.tmpl +14 -10
  19. package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-auditor.md.tmpl +3 -0
  20. package/src/workflows/documentation-audit/templates/opencode/skills/documentation-audit/SKILL.md.tmpl +1 -0
  21. package/src/workflows/documentation-audit/templates/prompts/auditor-prompt.md +1 -0
  22. package/src/workflows/product-shaping/README.md +1241 -3
  23. package/src/workflows/product-shaping/compatibility.md +33 -0
  24. package/src/workflows/product-shaping/contracts/product-spec-contract.json +250 -0
  25. package/src/workflows/product-shaping/templates/default-config.json +34 -0
  26. package/src/workflows/product-shaping/templates/default-profile.json +48 -0
  27. package/src/workflows/product-shaping/templates/documentation-audit/workflow-self-audit-auditor-prompt.md +117 -0
  28. package/src/workflows/product-shaping/templates/documentation-audit-profile.json +80 -0
  29. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-auditor.md.tmpl +22 -0
  30. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-domain-modeler.md.tmpl +31 -0
  31. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-intake-analyst.md.tmpl +31 -0
  32. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-orchestrator.md.tmpl +48 -0
  33. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-reference-researcher.md.tmpl +29 -0
  34. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-release-planner.md.tmpl +34 -0
  35. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-repairer.md.tmpl +25 -0
  36. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-shaper.md.tmpl +31 -0
  37. package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-validator.md.tmpl +23 -0
  38. package/src/workflows/product-shaping/templates/opencode/skills/product-shaping/SKILL.md.tmpl +45 -0
  39. package/src/workflows/product-shaping/templates/prompts/domain-modeler-prompt.md +27 -0
  40. package/src/workflows/product-shaping/templates/prompts/intake-prompt.md +30 -0
  41. package/src/workflows/product-shaping/templates/prompts/product-auditor-prompt.md +53 -0
  42. package/src/workflows/product-shaping/templates/prompts/product-repairer-prompt.md +25 -0
  43. package/src/workflows/product-shaping/templates/prompts/product-shaper-prompt.md +26 -0
  44. package/src/workflows/product-shaping/templates/prompts/product-validator-prompt.md +55 -0
  45. package/src/workflows/product-shaping/templates/prompts/reference-research-prompt.md +25 -0
  46. package/src/workflows/product-shaping/templates/prompts/release-planner-prompt.md +34 -0
  47. package/src/workflows/product-shaping/workflow.json +26 -3
  48. package/src/workflows/technical-shaping/README.md +2 -0
  49. package/src/workflows/technical-shaping/workflow.json +4 -0
package/src/cli/main.js CHANGED
@@ -5,8 +5,9 @@ import readline from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- const VERSION = "0.1.0";
8
+ const VERSION = "0.2.1";
9
9
  const CONFIG_FILE = "wefter.config.json";
10
+ const PRODUCT_SHAPING_WORKFLOW_ID = "product-shaping";
10
11
  const DOCUMENTATION_REPAIR_WORKFLOW_ID = "documentation-repair";
11
12
  const WORK_UNIT_WORKFLOW_ID = "work-unit-implementation";
12
13
  const DEFAULTS = Object.freeze({
@@ -16,6 +17,12 @@ const DEFAULTS = Object.freeze({
16
17
  templateRoot: ".wefter/workflows/documentation-audit/templates",
17
18
  processDocPath: ".wefter/workflows/documentation-audit/README.md"
18
19
  });
20
+ const PRODUCT_SHAPING_DEFAULTS = Object.freeze({
21
+ specRoot: ".wefter/specs",
22
+ runRoot: ".wefter/runs/product-shaping",
23
+ configPath: ".wefter/workflows/product-shaping/config.json",
24
+ profilePath: ".wefter/workflows/product-shaping/profile.json"
25
+ });
19
26
 
20
27
  const ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
21
28
 
@@ -25,14 +32,46 @@ const REQUIRED_TEMPLATE_FILES = Object.freeze([
25
32
  "validator-prompt.md"
26
33
  ]);
27
34
 
35
+ const PRODUCT_SHAPING_PROMPT_FILES = Object.freeze([
36
+ "intake-prompt.md",
37
+ "reference-research-prompt.md",
38
+ "product-shaper-prompt.md",
39
+ "domain-modeler-prompt.md",
40
+ "release-planner-prompt.md",
41
+ "product-auditor-prompt.md",
42
+ "product-validator-prompt.md",
43
+ "product-repairer-prompt.md"
44
+ ]);
45
+ const PRODUCT_SHAPING_REQUIRED_FILES = Object.freeze([
46
+ "README.md",
47
+ "discovery/SOURCE_MATERIALS.md",
48
+ "discovery/IDEA_BRIEF.md",
49
+ "discovery/OPEN_QUESTIONS.md",
50
+ "references/README.md",
51
+ "PRODUCT_VISION.md",
52
+ "product/FEATURE_CATALOG.md",
53
+ "product/MODULE_ROADMAP.md",
54
+ "product/DOMAIN_MODEL.md",
55
+ "product/OPERATIONAL_FLOW.md",
56
+ "product/PRODUCT_DECISIONS.md",
57
+ "releases/README.md",
58
+ "releases/<release-id>/README.md",
59
+ "releases/<release-id>/SCOPE.md",
60
+ "releases/<release-id>/DOMAIN_SPEC.md",
61
+ "releases/<release-id>/ACCEPTANCE_CRITERIA.md",
62
+ "releases/<release-id>/DELIVERABLES.md"
63
+ ]);
64
+
28
65
  function printHelp() {
29
66
  console.log(`wefter ${VERSION}
30
67
 
31
68
  Usage:
32
69
  wefter init [--yes] [--force] [--target <path>] [--profile-path <path>] [--artifact-root <path>] [--template-root <path>] [--process-doc-path <path>] [--runner-command <command>]
70
+ wefter product shape [--target <path>] [--release-id <id>] [--run-name <name>] [--spec-root <path>] [--run-root <path>] [--config-path <path>] [--profile-path <path>] [--dry-run]
71
+ wefter product validate [--target <path>] [--release-id <id>] [--run-id <id> | --run-root <path>] [--config-path <path>] [--json]
33
72
  wefter docs audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
34
73
  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]
74
+ wefter work-unit run [--target <path>] [--work-unit-id <id>] [--work-units-document <path>] [--release-id <id>] [--product-run-id <id> | --product-run-root <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--config-path <path>] [--profile-path <path>] [--dry-run]
36
75
  wefter work-unit guard [--target <path>] [--run-id <id> | --run-root <path>] [--task-id <id>] [--mode Status|ReadyForReview|ReadyForNextTask|ReadyForFinalValidation] [--config-path <path>] [--json]
37
76
  wefter new-run documentation-audit [--target <path>] [--profile-path <path>] [--run-name <name>] [--passes-per-lens <n>] [--max-audits <n>] [--dry-run]
38
77
  wefter profile scaffold [--target <path>] [--force]
@@ -41,6 +80,8 @@ Usage:
41
80
 
42
81
  Commands:
43
82
  init Install opencode agents, skill, commands, templates and local config.
83
+ product shape Generate one product-shaping run skeleton.
84
+ product validate Validate product-shaping specs against the completion gate.
44
85
  docs audit Generate one documentation audit run from the configured profile.
45
86
  docs repair Generate one documentation repair run from a final audit report.
46
87
  work-unit run Generate one work-unit implementation run.
@@ -80,6 +121,52 @@ function parseArgs(argv) {
80
121
  return { positional, flags };
81
122
  }
82
123
 
124
+ function allowedFlagsForCommand(command, subcommand) {
125
+ if (command === "init") {
126
+ return ["yes", "force", "target", "profile-path", "artifact-root", "template-root", "process-doc-path", "runner-command"];
127
+ }
128
+ if (command === "new-run") {
129
+ return ["target", "profile-path", "run-name", "passes-per-lens", "max-audits", "dry-run"];
130
+ }
131
+ if (command === "docs" && subcommand === "audit") {
132
+ return ["target", "profile-path", "run-name", "passes-per-lens", "max-audits", "dry-run"];
133
+ }
134
+ if (command === "docs" && subcommand === "repair") {
135
+ return ["target", "audit-report", "run-name", "dry-run"];
136
+ }
137
+ if (command === "product" && subcommand === "shape") {
138
+ return ["target", "release-id", "run-name", "spec-root", "run-root", "config-path", "profile-path", "dry-run"];
139
+ }
140
+ if (command === "product" && subcommand === "validate") {
141
+ return ["target", "release-id", "run-id", "run-root", "config-path", "json"];
142
+ }
143
+ if (command === "work-unit" && subcommand === "run") {
144
+ return ["target", "work-unit-id", "work-units-document", "release-id", "product-run-id", "product-run-root", "run-name", "passes-per-lens", "max-audits", "config-path", "profile-path", "dry-run"];
145
+ }
146
+ if (command === "work-unit" && subcommand === "guard") {
147
+ return ["target", "run-id", "run-root", "task-id", "mode", "config-path", "json"];
148
+ }
149
+ if (command === "profile" && subcommand === "scaffold") {
150
+ return ["target", "force"];
151
+ }
152
+ if (command === "profile" && subcommand === "import") {
153
+ return ["target", "source", "force"];
154
+ }
155
+ if (command === "doctor") {
156
+ return ["target"];
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function assertKnownFlags(flags, allowedFlags) {
162
+ const allowed = new Set(["help", "version", ...allowedFlags]);
163
+ for (const key of Object.keys(flags)) {
164
+ if (!allowed.has(key)) {
165
+ throw new Error(`Unsupported option --${key} for this command.`);
166
+ }
167
+ }
168
+ }
169
+
83
170
  function packageRoot() {
84
171
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
85
172
  }
@@ -96,6 +183,10 @@ function documentationRepairTemplateRoot() {
96
183
  return path.join(workflowPackageRoot(DOCUMENTATION_REPAIR_WORKFLOW_ID), "templates");
97
184
  }
98
185
 
186
+ function productShapingWorkflowPackageRoot() {
187
+ return workflowPackageRoot(PRODUCT_SHAPING_WORKFLOW_ID);
188
+ }
189
+
99
190
  function workUnitWorkflowPackageRoot() {
100
191
  return workflowPackageRoot(WORK_UNIT_WORKFLOW_ID);
101
192
  }
@@ -141,7 +232,7 @@ function normalizeRelativePath(value, label) {
141
232
  throw new Error(`${label} must be relative to the target repository.`);
142
233
  }
143
234
 
144
- const parts = trimmed.split("/").filter(Boolean);
235
+ const parts = trimmed.split("/").filter((part) => part && part !== ".");
145
236
  if (parts.length === 0 || parts.includes("..")) {
146
237
  throw new Error(`${label} must not be empty or contain '..'.`);
147
238
  }
@@ -358,7 +449,14 @@ function normalizeConfig(config) {
358
449
 
359
450
  function defaultWorkflowRegistry() {
360
451
  return {
361
- "product-shaping": { status: "planned", enabled: false },
452
+ [PRODUCT_SHAPING_WORKFLOW_ID]: {
453
+ status: "available",
454
+ enabled: true,
455
+ specRoot: PRODUCT_SHAPING_DEFAULTS.specRoot,
456
+ runRoot: PRODUCT_SHAPING_DEFAULTS.runRoot,
457
+ configPath: PRODUCT_SHAPING_DEFAULTS.configPath,
458
+ profilePath: PRODUCT_SHAPING_DEFAULTS.profilePath
459
+ },
362
460
  "documentation-audit": { status: "available", enabled: true },
363
461
  "documentation-repair": { status: "available", enabled: true },
364
462
  "technical-shaping": { status: "planned", enabled: false },
@@ -379,6 +477,14 @@ function workflowSettings(config, workflowId) {
379
477
  return settings;
380
478
  }
381
479
 
480
+ function assertWorkflowEnabled(config, workflowId) {
481
+ const settings = workflowSettings(config, workflowId);
482
+ if (!settings.enabled) {
483
+ throw new Error(`Workflow '${workflowId}' is disabled in ${CONFIG_FILE}.`);
484
+ }
485
+ return settings;
486
+ }
487
+
382
488
  function workUnitConfigPath(config, flags = {}) {
383
489
  const settings = workflowSettings(config, WORK_UNIT_WORKFLOW_ID);
384
490
  return normalizeRelativePath(flags["config-path"] || settings.configPath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/config.json`, "work-unit config path");
@@ -389,19 +495,39 @@ function workUnitProfilePath(config, flags = {}) {
389
495
  return normalizeRelativePath(flags["profile-path"] || settings.profilePath || `${config.workflowRoot}/${WORK_UNIT_WORKFLOW_ID}/profile.json`, "work-unit profile path");
390
496
  }
391
497
 
498
+ function productShapingSpecRoot(config, flags = {}) {
499
+ const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
500
+ return normalizeRelativePath(flags["spec-root"] || settings.specRoot || PRODUCT_SHAPING_DEFAULTS.specRoot, "product-shaping spec root");
501
+ }
502
+
503
+ function productShapingRunRoot(config, flags = {}) {
504
+ const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
505
+ return normalizeRelativePath(flags["run-root"] || settings.runRoot || PRODUCT_SHAPING_DEFAULTS.runRoot, "product-shaping run root");
506
+ }
507
+
508
+ function productShapingConfigPath(config, flags = {}) {
509
+ const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
510
+ return normalizeRelativePath(flags["config-path"] || settings.configPath || `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/config.json`, "product-shaping config path");
511
+ }
512
+
513
+ function productShapingProfilePath(config, flags = {}) {
514
+ const settings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
515
+ return normalizeRelativePath(flags["profile-path"] || settings.profilePath || `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/profile.json`, "product-shaping profile path");
516
+ }
517
+
392
518
  function normalizeWorkflowRegistry(workflows) {
393
519
  assertObject(workflows, "workflows");
394
520
  for (const [workflowId, workflow] of Object.entries(workflows)) {
395
521
  requireId(workflowId, `workflows.${workflowId}`);
396
522
  assertObject(workflow, `workflows.${workflowId}`);
397
- assertAllowedKeys(workflow, `workflows.${workflowId}`, ["status", "enabled", "profilePath", "configPath"]);
523
+ assertAllowedKeys(workflow, `workflows.${workflowId}`, ["status", "enabled", "profilePath", "configPath", "specRoot", "runRoot"]);
398
524
  if (!["available", "planned"].includes(workflow.status)) {
399
525
  throw new Error(`workflows.${workflowId}.status must be available or planned.`);
400
526
  }
401
527
  if (typeof workflow.enabled !== "boolean") {
402
528
  throw new Error(`workflows.${workflowId}.enabled must be boolean.`);
403
529
  }
404
- for (const key of ["profilePath", "configPath"]) {
530
+ for (const key of ["profilePath", "configPath", "specRoot", "runRoot"]) {
405
531
  if (workflow[key] !== undefined) {
406
532
  normalizeRelativePath(workflow[key], `workflows.${workflowId}.${key}`);
407
533
  }
@@ -445,11 +571,12 @@ function mergeOpencodeConfig(targetRoot, config, force) {
445
571
  const opencodePath = path.join(targetRoot, "opencode.json");
446
572
  const existing = fs.existsSync(opencodePath) ? readJson(opencodePath, "opencode.json") : { "$schema": "https://opencode.ai/config.json" };
447
573
  const workUnitConfig = readJsonIfExists(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
574
+ const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
448
575
 
449
576
  existing["$schema"] = existing["$schema"] || "https://opencode.ai/config.json";
450
577
  existing.watcher = existing.watcher || {};
451
578
  existing.watcher.ignore = Array.isArray(existing.watcher.ignore) ? existing.watcher.ignore : [];
452
- for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot]) {
579
+ for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig?.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
453
580
  if (!ignored) {
454
581
  continue;
455
582
  }
@@ -481,18 +608,30 @@ function mergeOpencodeConfig(targetRoot, config, force) {
481
608
  agent: "wefter-work-unit-orchestrator",
482
609
  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
610
  };
611
+ const productShapeCommand = {
612
+ description: "Run the Wefter product-shaping workflow from idea to validated product specs and DELIVERABLES.md handoff.",
613
+ agent: "wefter-product-orchestrator",
614
+ template: `Run or resume the Wefter product-shaping workflow. Read ${CONFIG_FILE} first. If the user provided an existing ${productShapingRunRoot(config)}/<run-id> path, resume it. Otherwise create a run with ${config.runnerCommand} product shape. Shape source materials into product specs under ${productShapingSpecRoot(config)}, stop for human product decisions when needed, validate the completion gate, and do not create task specs or implementation plans.`
615
+ };
484
616
  const repairDocsCommand = {
485
617
  description: "Run the Wefter documentation repair workflow from a validated audit report.",
486
618
  agent: "wefter-doc-repair-orchestrator",
487
619
  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
620
  };
489
621
 
490
- for (const [name, nextValue] of Object.entries({
622
+ const commands = {
491
623
  "wefter-audit-docs": fullRunCommand,
492
624
  "wefter-generate-doc-audit-profile": generateProfileCommand,
493
625
  "wefter-repair-docs": repairDocsCommand,
494
626
  "wefter-run-work-unit": workUnitCommand
495
- })) {
627
+ };
628
+ if (productSettings.enabled) {
629
+ commands["wefter-shape-product"] = productShapeCommand;
630
+ } else {
631
+ delete existing.command["wefter-shape-product"];
632
+ }
633
+
634
+ for (const [name, nextValue] of Object.entries(commands)) {
496
635
  if (existing.command[name] && JSON.stringify(existing.command[name]) !== JSON.stringify(nextValue) && !force) {
497
636
  throw new Error(`Refusing to overwrite existing opencode command '${name}'. Use --force to replace it.`);
498
637
  }
@@ -565,11 +704,14 @@ function defaultProfile(config = DEFAULTS) {
565
704
 
566
705
  function validateProfile(profile) {
567
706
  assertObject(profile, "Profile");
568
- assertAllowedKeys(profile, "Profile", ["version", "corpus", "variants", "lenses"]);
707
+ assertAllowedKeys(profile, "Profile", ["version", "auditorPromptPath", "corpus", "variants", "lenses"]);
569
708
 
570
709
  if (profile.version !== 1) {
571
710
  throw new Error("Profile must have version: 1.");
572
711
  }
712
+ if (profile.auditorPromptPath !== undefined) {
713
+ normalizeRelativePath(profile.auditorPromptPath, "Profile auditorPromptPath");
714
+ }
573
715
 
574
716
  assertObject(profile.corpus, "Profile corpus");
575
717
  assertAllowedKeys(profile.corpus, "Profile corpus", ["include", "exclude"]);
@@ -601,6 +743,14 @@ function validateProfile(profile) {
601
743
  assertUniqueIds(profile.lenses, "Profile lenses");
602
744
  }
603
745
 
746
+ function documentationAuditAuditorPrompt(targetRoot, config, profile) {
747
+ const relativePath = profile.auditorPromptPath
748
+ ? normalizeRelativePath(profile.auditorPromptPath, "Profile auditorPromptPath")
749
+ : toPosix(path.join(config.templateRoot, "auditor-prompt.md"));
750
+ const fullPath = resolveInsideTarget(targetRoot, relativePath, "auditor prompt");
751
+ return { relativePath, fullPath };
752
+ }
753
+
604
754
  function validateWorkUnitConfig(config) {
605
755
  assertObject(config, "Work-unit config");
606
756
  assertAllowedKeys(config, "Work-unit config", ["version", "workflowName", "releaseId", "workUnitsDocument", "sourceDocs", "runArtifactsRoot", "versionedArtifacts", "defaultWorkUnitId", "defaultPlanAuditPassesPerLens", "gatePolicy"]);
@@ -639,6 +789,80 @@ function validateWorkUnitConfig(config) {
639
789
  requireStringArray(config.gatePolicy.alwaysPauseOn, "Work-unit config.gatePolicy.alwaysPauseOn");
640
790
  }
641
791
 
792
+ function validateProductShapingConfig(config) {
793
+ assertObject(config, "Product-shaping config");
794
+ assertAllowedKeys(config, "Product-shaping config", ["version", "workflowName", "releaseId", "specRoot", "runRoot", "contractPath", "processDocPath", "requiredFiles", "completionGate"]);
795
+
796
+ if (config.version !== 1) {
797
+ throw new Error("Product-shaping config must have version: 1.");
798
+ }
799
+ if (config.workflowName !== PRODUCT_SHAPING_WORKFLOW_ID) {
800
+ throw new Error(`Product-shaping config.workflowName must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
801
+ }
802
+ requireString(config.releaseId, "Product-shaping config.releaseId");
803
+ normalizeRelativePath(config.specRoot, "Product-shaping config.specRoot");
804
+ normalizeRelativePath(config.runRoot, "Product-shaping config.runRoot");
805
+ normalizeRelativePath(config.contractPath, "Product-shaping config.contractPath");
806
+ normalizeRelativePath(config.processDocPath, "Product-shaping config.processDocPath");
807
+ requireStringArray(config.requiredFiles, "Product-shaping config.requiredFiles");
808
+ if (JSON.stringify(config.requiredFiles) !== JSON.stringify(PRODUCT_SHAPING_REQUIRED_FILES)) {
809
+ throw new Error("Product-shaping config.requiredFiles must match the canonical product spec file set and order.");
810
+ }
811
+
812
+ assertObject(config.completionGate, "Product-shaping config.completionGate");
813
+ assertAllowedKeys(config.completionGate, "Product-shaping config.completionGate", ["requireNoReleaseBlockingQuestions", "requireAdversarialReview", "requireFinalValidation", "readyDeliverableStatuses"]);
814
+ for (const key of ["requireNoReleaseBlockingQuestions", "requireAdversarialReview", "requireFinalValidation"]) {
815
+ if (typeof config.completionGate[key] !== "boolean") {
816
+ throw new Error(`Product-shaping config.completionGate.${key} must be boolean.`);
817
+ }
818
+ }
819
+ for (const key of ["requireAdversarialReview", "requireFinalValidation"]) {
820
+ if (config.completionGate[key] !== true) {
821
+ throw new Error(`Product-shaping config.completionGate.${key} must be true for product-shaping completion.`);
822
+ }
823
+ }
824
+ requireStringArray(config.completionGate.readyDeliverableStatuses, "Product-shaping config.completionGate.readyDeliverableStatuses");
825
+ if (JSON.stringify(config.completionGate.readyDeliverableStatuses) !== JSON.stringify(["ready"])) {
826
+ throw new Error("Product-shaping config.completionGate.readyDeliverableStatuses must be exactly ['ready'].");
827
+ }
828
+ }
829
+
830
+ function validateProductShapingProfile(profile) {
831
+ assertObject(profile, "Product-shaping profile");
832
+ assertAllowedKeys(profile, "Product-shaping profile", ["version", "workflowName", "variants", "lenses"]);
833
+
834
+ if (profile.version !== 1) {
835
+ throw new Error("Product-shaping profile must have version: 1.");
836
+ }
837
+ if (profile.workflowName !== PRODUCT_SHAPING_WORKFLOW_ID) {
838
+ throw new Error(`Product-shaping profile.workflowName must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
839
+ }
840
+
841
+ if (!Array.isArray(profile.variants) || profile.variants.length === 0) {
842
+ throw new Error("Product-shaping profile must define at least one variant.");
843
+ }
844
+ profile.variants.forEach((variant, index) => {
845
+ assertObject(variant, `Product-shaping profile variants[${index}]`);
846
+ assertAllowedKeys(variant, `Product-shaping profile variants[${index}]`, ["id", "title", "instruction"]);
847
+ requireId(variant.id, `Product-shaping profile variants[${index}].id`);
848
+ requireString(variant.title, `Product-shaping profile variants[${index}].title`);
849
+ requireString(variant.instruction, `Product-shaping profile variants[${index}].instruction`);
850
+ });
851
+ assertUniqueIds(profile.variants, "Product-shaping profile variants");
852
+
853
+ if (!Array.isArray(profile.lenses) || profile.lenses.length === 0) {
854
+ throw new Error("Product-shaping profile must define at least one lens.");
855
+ }
856
+ profile.lenses.forEach((lens, index) => {
857
+ assertObject(lens, `Product-shaping profile lenses[${index}]`);
858
+ assertAllowedKeys(lens, `Product-shaping profile lenses[${index}]`, ["id", "title", "focus"]);
859
+ requireId(lens.id, `Product-shaping profile lenses[${index}].id`);
860
+ requireString(lens.title, `Product-shaping profile lenses[${index}].title`);
861
+ requireString(lens.focus, `Product-shaping profile lenses[${index}].focus`);
862
+ });
863
+ assertUniqueIds(profile.lenses, "Product-shaping profile lenses");
864
+ }
865
+
642
866
  function validateWorkUnitProfile(profile) {
643
867
  assertObject(profile, "Work-unit profile");
644
868
  assertAllowedKeys(profile, "Work-unit profile", ["version", "variants", "lenses"]);
@@ -745,8 +969,17 @@ async function commandInit(flags) {
745
969
  CONFIG_FILE,
746
970
  RUNNER_COMMAND_NEW_RUN_PATTERN: yamlSingleQuoted(`${config.runnerCommand}*`),
747
971
  RUNNER_COMMAND_DOCS_REPAIR_PATTERN: yamlSingleQuoted(`${config.runnerCommand} docs repair*`),
972
+ RUNNER_COMMAND_PRODUCT_SHAPE_PATTERN: yamlSingleQuoted(`${config.runnerCommand} product shape*`),
973
+ RUNNER_COMMAND_PRODUCT_VALIDATE_PATTERN: yamlSingleQuoted(`${config.runnerCommand} product validate*`),
748
974
  DOCUMENTATION_REPAIR_ARTIFACT_ROOT: documentationRepairArtifactRoot(),
749
975
  DOCUMENTATION_REPAIR_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(documentationRepairArtifactRoot()),
976
+ PRODUCT_SHAPING_SPEC_ROOT: productShapingSpecRoot(config),
977
+ PRODUCT_SHAPING_SPEC_ROOT_WINDOWS: windowsPermissionPath(productShapingSpecRoot(config)),
978
+ PRODUCT_SHAPING_RUN_ROOT: productShapingRunRoot(config),
979
+ PRODUCT_SHAPING_RUN_ROOT_WINDOWS: windowsPermissionPath(productShapingRunRoot(config)),
980
+ PRODUCT_SHAPING_CONFIG_PATH: productShapingConfigPath(config),
981
+ PRODUCT_SHAPING_PROFILE_PATH: productShapingProfilePath(config),
982
+ PRODUCT_SHAPING_PROCESS_DOC_PATH: `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/README.md`,
750
983
  RUNNER_COMMAND_WORK_UNIT_PATTERN: yamlSingleQuoted(`${config.runnerCommand} work-unit*`),
751
984
  WORK_UNIT_ARTIFACT_ROOT: ".audit/wefter/work-unit-implementation",
752
985
  WORK_UNIT_ARTIFACT_ROOT_WINDOWS: windowsPermissionPath(".audit/wefter/work-unit-implementation"),
@@ -761,12 +994,23 @@ async function commandInit(flags) {
761
994
 
762
995
  const root = packageRoot();
763
996
  const auditTemplates = documentationAuditTemplateRoot();
997
+ const productShapingPackageRoot = productShapingWorkflowPackageRoot();
764
998
  const workUnitPackageRoot = workUnitWorkflowPackageRoot();
765
999
  copyRenderedTemplate(path.join(root, "src/workflows/documentation-audit/workflow.json"), path.join(targetRoot, config.workflowRoot, "documentation-audit/workflow.json"), values, flags.force);
766
1000
  for (const workflowId of ["product-shaping", "documentation-repair", "technical-shaping", "work-unit-implementation"]) {
767
1001
  copyDirectory(path.join(root, "src/workflows", workflowId), path.join(targetRoot, config.workflowRoot, workflowId), flags.force);
768
1002
  }
769
1003
  copyDirectory(path.join(workUnitPackageRoot, "templates/prompts"), path.join(targetRoot, config.workflowRoot, WORK_UNIT_WORKFLOW_ID, "templates/prompts"), flags.force);
1004
+ const productShapingConfig = readJson(path.join(productShapingPackageRoot, "templates/default-config.json"), "default product-shaping config");
1005
+ productShapingConfig.specRoot = productShapingSpecRoot(config);
1006
+ productShapingConfig.runRoot = productShapingRunRoot(config);
1007
+ productShapingConfig.contractPath = `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/contracts/product-spec-contract.json`;
1008
+ productShapingConfig.processDocPath = `${config.workflowRoot}/${PRODUCT_SHAPING_WORKFLOW_ID}/README.md`;
1009
+ validateProductShapingConfig(productShapingConfig);
1010
+ writeJsonIfSafe(path.join(targetRoot, productShapingConfigPath(config)), productShapingConfig, flags.force);
1011
+ const productShapingProfile = readJson(path.join(productShapingPackageRoot, "templates/default-profile.json"), "default product-shaping profile");
1012
+ validateProductShapingProfile(productShapingProfile);
1013
+ writeJsonIfSafe(path.join(targetRoot, productShapingProfilePath(config)), productShapingProfile, flags.force);
770
1014
  writeJsonIfSafe(path.join(targetRoot, workUnitConfigPath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-config.json"), "default work-unit config"), flags.force);
771
1015
  writeJsonIfSafe(path.join(targetRoot, workUnitProfilePath(config)), readJson(path.join(workUnitPackageRoot, "templates/default-profile.json"), "default work-unit profile"), flags.force);
772
1016
  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);
@@ -780,6 +1024,10 @@ async function commandInit(flags) {
780
1024
  copyRenderedTemplate(path.join(repairTemplates, "opencode/agent", `${agentFile}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `${agentFile}.md`), values, flags.force);
781
1025
  }
782
1026
  copyRenderedTemplate(path.join(repairTemplates, "opencode/skills/documentation-repair/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md"), values, flags.force);
1027
+ for (const agent of ["orchestrator", "intake-analyst", "reference-researcher", "shaper", "domain-modeler", "release-planner", "auditor", "validator", "repairer"]) {
1028
+ copyRenderedTemplate(path.join(productShapingPackageRoot, "templates/opencode/agent", `wefter-product-${agent}.md.tmpl`), path.join(targetRoot, ".opencode/agent", `wefter-product-${agent}.md`), values, flags.force);
1029
+ }
1030
+ copyRenderedTemplate(path.join(productShapingPackageRoot, "templates/opencode/skills/product-shaping/SKILL.md.tmpl"), path.join(targetRoot, ".opencode/skills/product-shaping/SKILL.md"), values, flags.force);
783
1031
  for (const agent of ["orchestrator", "planner", "plan-auditor", "plan-consolidator", "plan-validator", "plan-repairer", "task-implementer", "task-reviewer", "validator"]) {
784
1032
  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
1033
  }
@@ -798,7 +1046,7 @@ async function commandInit(flags) {
798
1046
  console.log(`Artifacts: ${config.artifactRoot}`);
799
1047
  console.log(`Runner command: ${config.runnerCommand}`);
800
1048
  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.");
1049
+ console.log("Restart opencode before using /wefter-shape-product, /wefter-audit-docs, /wefter-generate-doc-audit-profile, /wefter-repair-docs, or /wefter-run-work-unit.");
802
1050
  }
803
1051
 
804
1052
  function readTextRequired(filePath) {
@@ -842,6 +1090,7 @@ function commandNewRun(flags) {
842
1090
  const runName = flags["run-name"] || timestampRunName();
843
1091
  assertSafeRunName(runName);
844
1092
  const combinations = buildCombinations(profile, passesPerLens, maxAudits);
1093
+ const auditorPrompt = documentationAuditAuditorPrompt(targetRoot, config, profile);
845
1094
 
846
1095
  const artifactRoot = path.join(targetRoot, config.artifactRoot);
847
1096
  const tempRoot = path.join(artifactRoot, ".tmp");
@@ -879,7 +1128,7 @@ function commandNewRun(flags) {
879
1128
  }
880
1129
 
881
1130
  const templateRoot = path.join(targetRoot, config.templateRoot);
882
- const auditorTemplate = fs.readFileSync(path.join(templateRoot, "auditor-prompt.md"), "utf8");
1131
+ const auditorTemplate = readTextRequired(auditorPrompt.fullPath);
883
1132
  const consolidatorTemplate = fs.readFileSync(path.join(templateRoot, "consolidator-prompt.md"), "utf8");
884
1133
  const validatorTemplate = fs.readFileSync(path.join(templateRoot, "validator-prompt.md"), "utf8");
885
1134
  const promptRecords = [];
@@ -939,6 +1188,7 @@ function commandNewRun(flags) {
939
1188
  passesPerLens,
940
1189
  maxAudits,
941
1190
  profilePath: profilePathRelative,
1191
+ auditorPromptPath: auditorPrompt.relativePath,
942
1192
  corpus: profile.corpus,
943
1193
  counts: {
944
1194
  lenses: profile.lenses.length,
@@ -956,7 +1206,7 @@ function commandNewRun(flags) {
956
1206
  prompts: promptRecords
957
1207
  });
958
1208
 
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");
1209
+ 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. Do not execute this run in plan mode or any read-only runtime. Audit execution must be able to write artifacts under this run directory.\n2. Read manifest.json and execute auditor prompts using the exact prompt and output paths listed there. Do not locate prompts with wildcard or glob patterns.\n3. Execute auditor prompts from prompts/auditors/ and write outputs to raw/.\n4. After each batch, compare manifest outputs against raw/ files and retry missing outputs once before continuing.\n5. Execute prompts/consolidate.md only after every expected raw output exists.\n6. Execute prompts/validate.md after consolidation exists.\n7. 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
1210
 
961
1211
  if (fs.existsSync(runRoot)) {
962
1212
  throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
@@ -1078,15 +1328,530 @@ function commandDocsRepair(flags) {
1078
1328
  console.log(`Next prompt: ${path.join(runRoot, "prompts", "plan-repair.md")}`);
1079
1329
  }
1080
1330
 
1331
+ function productSpecPath(specRoot, releaseId, relativePath) {
1332
+ return toPosix(path.join(specRoot, relativePath.replaceAll("<release-id>", releaseId)));
1333
+ }
1334
+
1335
+ function readTextIfExists(filePath) {
1336
+ if (!fs.existsSync(filePath)) {
1337
+ return null;
1338
+ }
1339
+ return fs.readFileSync(filePath, "utf8");
1340
+ }
1341
+
1342
+ function productRunRootFromValidationFlags(targetRoot, productConfig, flags = {}) {
1343
+ if (flags["run-id"] && flags["run-root"]) {
1344
+ throw new Error("Use either --run-id or --run-root, not both.");
1345
+ }
1346
+ if (flags["run-id"]) {
1347
+ assertSafeRunName(flags["run-id"]);
1348
+ return resolveInsideTarget(targetRoot, path.join(productConfig.runRoot, flags["run-id"]), "product-shaping run root");
1349
+ }
1350
+ if (flags["run-root"]) {
1351
+ return resolveInsideTarget(targetRoot, flags["run-root"], "product-shaping run root");
1352
+ }
1353
+ return null;
1354
+ }
1355
+
1356
+ function hasPassingAdversarialReview(content) {
1357
+ return /(?:status|result):\s*pass/i.test(content) && /blocking findings:\s*(?:none|0)/i.test(content);
1358
+ }
1359
+
1360
+ function hasPassingFinalValidation(content) {
1361
+ return /(?:status|result):\s*pass/i.test(content) && /ready for delivery implementation:\s*yes/i.test(content);
1362
+ }
1363
+
1364
+ function escapeRegExp(value) {
1365
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1366
+ }
1367
+
1368
+ function isInsideDirectory(parent, candidate) {
1369
+ const relative = path.relative(parent, candidate);
1370
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
1371
+ }
1372
+
1373
+ function deliverableSections(deliverables) {
1374
+ const headingPattern = /^##\s+(Deliverable\s+([A-Za-z0-9][A-Za-z0-9_-]*):\s+\S.*)$/gm;
1375
+ const headings = [];
1376
+ let match;
1377
+ while ((match = headingPattern.exec(deliverables)) !== null) {
1378
+ headings.push({ title: match[1].trim(), id: match[2], index: match.index });
1379
+ }
1380
+
1381
+ return headings.map((start, index) => {
1382
+ const next = headings[index + 1];
1383
+ const end = next ? next.index : deliverables.length;
1384
+ return {
1385
+ title: start.title,
1386
+ id: start.id,
1387
+ index: start.index,
1388
+ end,
1389
+ body: deliverables.slice(start.index, end)
1390
+ };
1391
+ });
1392
+ }
1393
+
1394
+ function isProductDeliverablesHandoff(value) {
1395
+ return /(?:^|\/)releases\/[^/]+\/DELIVERABLES\.md$/i.test(value) || /(?:^|\/)DELIVERABLES\.md$/i.test(value);
1396
+ }
1397
+
1398
+ function assertConfiguredProductHandoff(workUnitsDocument, expectedHandoff) {
1399
+ if (workUnitsDocument === expectedHandoff) {
1400
+ return true;
1401
+ }
1402
+ if (isProductDeliverablesHandoff(workUnitsDocument)) {
1403
+ throw new Error(`Product-shaping handoff must use the configured DELIVERABLES.md path '${expectedHandoff}', but got '${workUnitsDocument}'.`);
1404
+ }
1405
+ return false;
1406
+ }
1407
+
1408
+ function validateDeliverableFieldCoverage(deliverables, readyStatuses, errors) {
1409
+ const allowed = new Set(["candidate", "ready", "blocked", "deferred", "done"]);
1410
+ const requiredLabels = [
1411
+ "Goal",
1412
+ "Scope",
1413
+ "Out of scope",
1414
+ "Dependencies",
1415
+ "Source docs",
1416
+ "Acceptance criteria",
1417
+ "Risk areas",
1418
+ "Human gate triggers",
1419
+ "Expected verification"
1420
+ ];
1421
+ const sections = deliverableSections(deliverables);
1422
+ const allStatuses = [...deliverables.matchAll(/^Status:\s*([A-Za-z-]+)/gim)];
1423
+ if (sections.length === 0) {
1424
+ if (allStatuses.length > 0) {
1425
+ errors.push("DELIVERABLES.md contains Status lines but no deliverable sections with stable ids. Use headings like '## Deliverable 00: <title>'.");
1426
+ } else {
1427
+ errors.push("DELIVERABLES.md contains no deliverable sections with stable ids. Use headings like '## Deliverable 00: <title>'.");
1428
+ }
1429
+ return;
1430
+ }
1431
+ const seenIds = new Set();
1432
+ for (const section of sections) {
1433
+ const normalizedId = section.id.toLowerCase();
1434
+ if (seenIds.has(normalizedId)) {
1435
+ errors.push(`DELIVERABLES.md deliverable id '${section.id}' must be unique.`);
1436
+ }
1437
+ seenIds.add(normalizedId);
1438
+ }
1439
+ for (const status of allStatuses) {
1440
+ const inSection = sections.some((section) => status.index >= section.index && status.index < section.end);
1441
+ if (!inSection) {
1442
+ errors.push("DELIVERABLES.md contains a Status line outside a deliverable section with a stable id.");
1443
+ }
1444
+ }
1445
+ sections.forEach((section, index) => {
1446
+ const title = section.title || `deliverable ${index + 1}`;
1447
+ const statuses = [...section.body.matchAll(/^Status:\s*([A-Za-z-]+)/gim)];
1448
+ if (statuses.length !== 1) {
1449
+ errors.push(`DELIVERABLES.md deliverable '${title}' must contain exactly one Status line.`);
1450
+ } else {
1451
+ const status = statuses[0][1].toLowerCase();
1452
+ if (!allowed.has(status)) {
1453
+ errors.push(`DELIVERABLES.md deliverable '${title}' contains invalid status '${status}'.`);
1454
+ } else if (!readyStatuses.has(status)) {
1455
+ errors.push(`DELIVERABLES.md deliverable '${title}' contains non-ready status '${status}'. Ready statuses: ${[...readyStatuses].join(", ")}.`);
1456
+ }
1457
+ }
1458
+ for (const label of requiredLabels) {
1459
+ const labelPattern = new RegExp(`^${escapeRegExp(label)}:\\s*\\S`, "im");
1460
+ const headingPattern = new RegExp(`^#{2,6}\\s+${escapeRegExp(label)}\\s*$`, "im");
1461
+ if (!labelPattern.test(section.body) && !headingPattern.test(section.body)) {
1462
+ errors.push(`DELIVERABLES.md deliverable '${title}' is missing required field '${label}:'.`);
1463
+ }
1464
+ }
1465
+ });
1466
+ }
1467
+
1468
+ function hasUnresolvedReleaseBlockingQuestion(openQuestions) {
1469
+ const blockStarts = [...openQuestions.matchAll(/^(?:##\s+.+|Question id:\s*.+)$/gim)].map((match) => match.index);
1470
+ const blocks = blockStarts.length > 0
1471
+ ? blockStarts.map((start, index) => openQuestions.slice(start, blockStarts[index + 1] || openQuestions.length))
1472
+ : [openQuestions];
1473
+ return blocks.some((block) => /blocks target release:\s*yes/i.test(block) && /status:\s*(?:open|deferred)/i.test(block));
1474
+ }
1475
+
1476
+ function validateReferenceCollection(specRoot, errors) {
1477
+ const referencesRoot = path.join(specRoot, "references");
1478
+ const referencesIndexPath = path.join(referencesRoot, "README.md");
1479
+ const referencesIndex = readTextIfExists(referencesIndexPath);
1480
+ if (!referencesIndex) {
1481
+ return;
1482
+ }
1483
+
1484
+ const referenceFiles = fs.existsSync(referencesRoot)
1485
+ ? fs.readdirSync(referencesRoot, { withFileTypes: true })
1486
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
1487
+ .map((entry) => entry.name)
1488
+ : [];
1489
+ const listedReferences = [...referencesIndex.matchAll(/\breferences\/(?!README\.md\b)[A-Za-z0-9_.-]+\.md\b/gi)]
1490
+ .map((match) => toPosix(match[0]).replace(/^\.\//, ""));
1491
+ const listedReferenceSet = new Set(listedReferences.map((reference) => reference.toLowerCase()));
1492
+
1493
+ if (referenceFiles.length === 0 && !/no (?:external )?references (?:used|researched|required)|zero references/i.test(referencesIndex)) {
1494
+ errors.push("references/README.md must explicitly state when no individual reference files are used.");
1495
+ }
1496
+ for (const listedReference of listedReferences) {
1497
+ const fullPath = path.join(specRoot, listedReference);
1498
+ ensureInside(specRoot, fullPath, `listed reference ${listedReference}`);
1499
+ if (!fs.existsSync(fullPath)) {
1500
+ errors.push(`references/README.md lists missing reference file '${listedReference}'.`);
1501
+ }
1502
+ }
1503
+ for (const referenceFile of referenceFiles) {
1504
+ const relativePath = `references/${referenceFile}`;
1505
+ if (!listedReferenceSet.has(relativePath.toLowerCase())) {
1506
+ errors.push(`references/README.md must list individual reference file '${relativePath}'.`);
1507
+ }
1508
+ }
1509
+ }
1510
+
1511
+ function validateNoTaskLevelImplementationDetail(deliverables, errors) {
1512
+ const blockedPatterns = [
1513
+ /task-specs\//i,
1514
+ /(?:^|\n)#{1,6}\s+Task\b/i,
1515
+ /(?:^|\n)#\s+T\d{2}(?:[-:]|\b)/i,
1516
+ /(?:^|\n)#{1,6}\s+T\d{2}[-:]\d+/i,
1517
+ /(?:^|\n)```(?:js|javascript|ts|typescript|tsx|jsx|python|py|sql|bash|sh)\b/i,
1518
+ /\b(?:implementation|task-logs|task-reviews)\//i,
1519
+ /\bRed-Green-Refactor\b/i,
1520
+ /\bwrite (?:a )?failing test\b/i,
1521
+ /\bmake the test pass\b/i
1522
+ ];
1523
+ if (blockedPatterns.some((pattern) => pattern.test(deliverables))) {
1524
+ errors.push("DELIVERABLES.md appears to contain task-level implementation detail.");
1525
+ }
1526
+ }
1527
+
1528
+ function validateRunEvidencePath(targetRoot, runRoot, actualRelative, expectedRelative, label, errors) {
1529
+ if (!actualRelative) {
1530
+ errors.push(`Missing ${label} path in product-shaping run manifest.`);
1531
+ return null;
1532
+ }
1533
+ const actualPath = resolveInsideTarget(targetRoot, actualRelative, label);
1534
+ const expectedPath = path.join(runRoot, expectedRelative);
1535
+ if (!isInsideDirectory(runRoot, actualPath) || path.resolve(actualPath) !== path.resolve(expectedPath)) {
1536
+ errors.push(`${label} must stay inside the selected product-shaping run at ${toDisplayPath(targetRoot, expectedPath)}.`);
1537
+ return null;
1538
+ }
1539
+ return actualPath;
1540
+ }
1541
+
1542
+ function validateProductSpecs(targetRoot, productConfig, releaseId, flags = {}) {
1543
+ const errors = [];
1544
+ const warnings = [];
1545
+ const specRootRelative = normalizeRelativePath(productConfig.specRoot, "Product-shaping config.specRoot");
1546
+ const specRoot = path.join(targetRoot, specRootRelative);
1547
+ ensureInside(targetRoot, specRoot, "product-shaping spec root");
1548
+
1549
+ for (const file of productConfig.requiredFiles) {
1550
+ const relativePath = file.replaceAll("<release-id>", releaseId);
1551
+ const fullPath = path.join(specRoot, relativePath);
1552
+ ensureInside(targetRoot, fullPath, `product spec ${relativePath}`);
1553
+ if (!fs.existsSync(fullPath)) {
1554
+ errors.push(`Missing required product spec: ${toPosix(path.join(specRootRelative, relativePath))}`);
1555
+ }
1556
+ }
1557
+ validateReferenceCollection(specRoot, errors);
1558
+
1559
+ const openQuestionsPath = path.join(specRoot, "discovery/OPEN_QUESTIONS.md");
1560
+ const openQuestions = readTextIfExists(openQuestionsPath);
1561
+ if (openQuestions && hasUnresolvedReleaseBlockingQuestion(openQuestions)) {
1562
+ errors.push("OPEN_QUESTIONS.md contains an unresolved release-blocking question.");
1563
+ }
1564
+
1565
+ const deliverablesPath = path.join(specRoot, "releases", releaseId, "DELIVERABLES.md");
1566
+ const deliverables = readTextIfExists(deliverablesPath);
1567
+ if (deliverables) {
1568
+ const readyStatuses = new Set(productConfig.completionGate.readyDeliverableStatuses.map((status) => status.toLowerCase()));
1569
+ validateDeliverableFieldCoverage(deliverables, readyStatuses, errors);
1570
+ validateNoTaskLevelImplementationDetail(deliverables, errors);
1571
+ }
1572
+
1573
+ if (productConfig.completionGate.requireAdversarialReview || productConfig.completionGate.requireFinalValidation) {
1574
+ const runRoot = productRunRootFromValidationFlags(targetRoot, productConfig, flags);
1575
+ if (!runRoot) {
1576
+ errors.push("Product-shaping validation requires --run-id or --run-root to verify adversarial review and final validation evidence.");
1577
+ } else {
1578
+ const manifestPath = path.join(runRoot, "manifest.json");
1579
+ if (!fs.existsSync(manifestPath)) {
1580
+ errors.push(`Missing product-shaping run manifest: ${toDisplayPath(targetRoot, manifestPath)}.`);
1581
+ } else {
1582
+ const manifest = readJson(manifestPath, "product-shaping run manifest");
1583
+ if (manifest.workflowId !== PRODUCT_SHAPING_WORKFLOW_ID) {
1584
+ errors.push(`Run manifest workflowId must be ${PRODUCT_SHAPING_WORKFLOW_ID}.`);
1585
+ }
1586
+ if (manifest.releaseId !== releaseId) {
1587
+ errors.push(`Run manifest releaseId '${manifest.releaseId}' does not match requested release '${releaseId}'.`);
1588
+ }
1589
+ if (manifest.paths?.runRoot) {
1590
+ const manifestRunRoot = resolveInsideTarget(targetRoot, manifest.paths.runRoot, "product-shaping manifest run root");
1591
+ if (path.resolve(manifestRunRoot) !== path.resolve(runRoot)) {
1592
+ errors.push("Run manifest paths.runRoot does not match the selected product-shaping run root.");
1593
+ }
1594
+ }
1595
+ const outputs = manifest.outputs || {};
1596
+ if (productConfig.completionGate.requireAdversarialReview) {
1597
+ const reviewPath = validateRunEvidencePath(targetRoot, runRoot, outputs.adversarialReview, path.join("review", "adversarial-review.md"), "adversarial review evidence", errors);
1598
+ if (!reviewPath || !fs.existsSync(reviewPath)) {
1599
+ errors.push("Missing adversarial review evidence for product-shaping completion gate.");
1600
+ } else if (!hasPassingAdversarialReview(fs.readFileSync(reviewPath, "utf8"))) {
1601
+ errors.push("Adversarial review evidence must include 'Status: pass' or 'Result: pass' and 'Blocking findings: none'.");
1602
+ }
1603
+ }
1604
+ if (productConfig.completionGate.requireFinalValidation) {
1605
+ const finalValidationPath = validateRunEvidencePath(targetRoot, runRoot, outputs.finalValidation, path.join("final", "product-shaping-validation.md"), "final validation evidence", errors);
1606
+ if (!finalValidationPath || !fs.existsSync(finalValidationPath)) {
1607
+ errors.push("Missing final validation evidence for product-shaping completion gate.");
1608
+ } else if (!hasPassingFinalValidation(fs.readFileSync(finalValidationPath, "utf8"))) {
1609
+ errors.push("Final validation evidence must include 'Status: pass' or 'Result: pass' and 'Ready for delivery implementation: yes'.");
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ return { errors, warnings };
1617
+ }
1618
+
1619
+ function commandProductValidate(flags) {
1620
+ const targetRoot = resolveTarget(flags);
1621
+ const wefterConfig = readConfig(targetRoot);
1622
+ assertWorkflowEnabled(wefterConfig, PRODUCT_SHAPING_WORKFLOW_ID);
1623
+ const configPath = productShapingConfigPath(wefterConfig, flags);
1624
+ const productConfig = readJson(path.join(targetRoot, configPath), "product-shaping config");
1625
+ validateProductShapingConfig(productConfig);
1626
+
1627
+ const releaseId = flags["release-id"] || productConfig.releaseId;
1628
+ assertSafeRunName(releaseId);
1629
+ const result = validateProductSpecs(targetRoot, productConfig, releaseId, flags);
1630
+ const ok = result.errors.length === 0;
1631
+
1632
+ if (flags.json) {
1633
+ console.log(JSON.stringify({ ok, releaseId, errors: result.errors, warnings: result.warnings }, null, 2));
1634
+ } else {
1635
+ console.log(`Product shaping validation: ${ok ? "pass" : "fail"}`);
1636
+ console.log(`Release: ${releaseId}`);
1637
+ for (const warning of result.warnings) {
1638
+ console.log(`WARNING ${warning}`);
1639
+ }
1640
+ for (const error of result.errors) {
1641
+ console.error(`ERROR ${error}`);
1642
+ }
1643
+ }
1644
+
1645
+ if (!ok) {
1646
+ process.exitCode = 1;
1647
+ }
1648
+ }
1649
+
1650
+ function commandProductShape(flags) {
1651
+ const targetRoot = resolveTarget(flags);
1652
+ const wefterConfig = readConfig(targetRoot);
1653
+ assertWorkflowEnabled(wefterConfig, PRODUCT_SHAPING_WORKFLOW_ID);
1654
+ const configPath = productShapingConfigPath(wefterConfig, flags);
1655
+ const profilePath = productShapingProfilePath(wefterConfig, flags);
1656
+ const productConfig = readJson(path.join(targetRoot, configPath), "product-shaping config");
1657
+ const productProfile = readJson(path.join(targetRoot, profilePath), "product-shaping profile");
1658
+ validateProductShapingConfig(productConfig);
1659
+ validateProductShapingProfile(productProfile);
1660
+
1661
+ const releaseId = flags["release-id"] || productConfig.releaseId;
1662
+ assertSafeRunName(releaseId);
1663
+ const specRootRelative = normalizeRelativePath(flags["spec-root"] || productConfig.specRoot, "product-shaping spec root");
1664
+ const runArtifactsRootRelative = normalizeRelativePath(flags["run-root"] || productConfig.runRoot, "product-shaping run root");
1665
+ const runName = flags["run-name"] || `${timestampRunName()}__${releaseId}`;
1666
+ assertSafeRunName(runName);
1667
+
1668
+ const specRoot = path.join(targetRoot, specRootRelative);
1669
+ const artifactRoot = path.join(targetRoot, runArtifactsRootRelative);
1670
+ const tempRoot = path.join(artifactRoot, ".tmp");
1671
+ const runRoot = path.join(artifactRoot, runName);
1672
+ const stagingRunRoot = path.join(tempRoot, runName);
1673
+ ensureInside(targetRoot, specRoot, "product-shaping spec root");
1674
+ ensureInside(targetRoot, artifactRoot, "product-shaping run root");
1675
+ ensureInside(targetRoot, runRoot, "product-shaping run");
1676
+ ensureInside(targetRoot, stagingRunRoot, "product-shaping staging run");
1677
+
1678
+ const requiredFiles = productConfig.requiredFiles.map((file) => ({
1679
+ templatePath: file,
1680
+ path: file.replaceAll("<release-id>", releaseId),
1681
+ fullPath: productSpecPath(specRootRelative, releaseId, file)
1682
+ }));
1683
+
1684
+ if (flags["dry-run"]) {
1685
+ console.log(`Run name: ${runName}`);
1686
+ console.log(`Release: ${releaseId}`);
1687
+ console.log(`Spec root: ${specRootRelative}`);
1688
+ console.log(`Output root: ${toPosix(path.join(runArtifactsRootRelative, runName))}`);
1689
+ console.log(`Required product spec files: ${requiredFiles.length}`);
1690
+ return;
1691
+ }
1692
+
1693
+ if (fs.existsSync(runRoot)) {
1694
+ throw new Error(`Run directory already exists: ${runRoot}. Use a different --run-name or resume the existing run.`);
1695
+ }
1696
+ if (fs.existsSync(stagingRunRoot)) {
1697
+ throw new Error(`Staging directory already exists: ${stagingRunRoot}. Remove it manually after verifying no product-shaping run is in progress, or use a different --run-name.`);
1698
+ }
1699
+
1700
+ const runRootRelative = toPosix(path.join(runArtifactsRootRelative, runName));
1701
+ const adversarialReviewPath = toPosix(path.join(runRootRelative, "review", "adversarial-review.md"));
1702
+ const finalValidationPath = toPosix(path.join(runRootRelative, "final", "product-shaping-validation.md"));
1703
+ const handoffDeliverablesPath = productSpecPath(specRootRelative, releaseId, "releases/<release-id>/DELIVERABLES.md");
1704
+ const promptsRoot = path.join(stagingRunRoot, "prompts");
1705
+ const draftRoot = path.join(stagingRunRoot, "draft");
1706
+ const reviewRoot = path.join(stagingRunRoot, "review");
1707
+ const validationRoot = path.join(stagingRunRoot, "validation");
1708
+ const finalRoot = path.join(stagingRunRoot, "final");
1709
+ for (const directory of [artifactRoot, tempRoot, stagingRunRoot, promptsRoot, draftRoot, reviewRoot, validationRoot, finalRoot]) {
1710
+ fs.mkdirSync(directory, { recursive: true });
1711
+ }
1712
+
1713
+ const promptTemplateRoot = path.join(targetRoot, wefterConfig.workflowRoot, PRODUCT_SHAPING_WORKFLOW_ID, "templates", "prompts");
1714
+ const promptValues = {
1715
+ RUN_ID: runName,
1716
+ RELEASE_ID: releaseId,
1717
+ CONFIG_PATH: configPath,
1718
+ PROFILE_PATH: profilePath,
1719
+ CONTRACT_PATH: productConfig.contractPath,
1720
+ PROCESS_DOC_PATH: productConfig.processDocPath,
1721
+ SPEC_ROOT: specRootRelative,
1722
+ RUN_ROOT: runRootRelative,
1723
+ REQUIRED_FILES: markdownList(requiredFiles.map((file) => file.fullPath)),
1724
+ DELIVERABLES_PATH: handoffDeliverablesPath,
1725
+ ADVERSARIAL_REVIEW_PATH: adversarialReviewPath,
1726
+ FINAL_VALIDATION_PATH: finalValidationPath,
1727
+ SCOPE_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/SCOPE.md"),
1728
+ DOMAIN_SPEC_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/DOMAIN_SPEC.md"),
1729
+ ACCEPTANCE_CRITERIA_PATH: productSpecPath(specRootRelative, releaseId, "releases/<release-id>/ACCEPTANCE_CRITERIA.md"),
1730
+ OPEN_QUESTIONS_PATH: productSpecPath(specRootRelative, releaseId, "discovery/OPEN_QUESTIONS.md"),
1731
+ PRODUCT_DECISIONS_PATH: productSpecPath(specRootRelative, releaseId, "product/PRODUCT_DECISIONS.md")
1732
+ };
1733
+
1734
+ const promptRecords = [];
1735
+ for (const file of PRODUCT_SHAPING_PROMPT_FILES) {
1736
+ const source = path.join(promptTemplateRoot, file);
1737
+ const template = readTextRequired(source);
1738
+ const rendered = renderTemplate(template, promptValues);
1739
+ const destination = path.join(promptsRoot, file);
1740
+ assertNoPlaceholders(destination, rendered);
1741
+ fs.writeFileSync(destination, rendered, "utf8");
1742
+ promptRecords.push(toPosix(path.join(runRootRelative, "prompts", file)));
1743
+ }
1744
+
1745
+ writeJson(path.join(stagingRunRoot, "manifest.json"), {
1746
+ version: 1,
1747
+ workflowId: PRODUCT_SHAPING_WORKFLOW_ID,
1748
+ runId: runName,
1749
+ releaseId,
1750
+ generatedAt: new Date().toISOString(),
1751
+ configPath,
1752
+ profilePath,
1753
+ contractPath: productConfig.contractPath,
1754
+ processDocPath: productConfig.processDocPath,
1755
+ specRoot: specRootRelative,
1756
+ counts: {
1757
+ requiredFiles: requiredFiles.length,
1758
+ variants: productProfile.variants.length,
1759
+ lenses: productProfile.lenses.length
1760
+ },
1761
+ paths: {
1762
+ runRoot: runRootRelative,
1763
+ prompts: toPosix(path.join(runRootRelative, "prompts")),
1764
+ draft: toPosix(path.join(runRootRelative, "draft")),
1765
+ review: toPosix(path.join(runRootRelative, "review")),
1766
+ validation: toPosix(path.join(runRootRelative, "validation")),
1767
+ final: toPosix(path.join(runRootRelative, "final")),
1768
+ specRoot: specRootRelative,
1769
+ releaseRoot: toPosix(path.join(specRootRelative, "releases", releaseId))
1770
+ },
1771
+ outputs: {
1772
+ adversarialReview: adversarialReviewPath,
1773
+ finalValidation: finalValidationPath
1774
+ },
1775
+ handoff: {
1776
+ deliverables: handoffDeliverablesPath
1777
+ },
1778
+ gate: {
1779
+ status: "pending",
1780
+ requireNoReleaseBlockingQuestions: productConfig.completionGate.requireNoReleaseBlockingQuestions,
1781
+ requireAdversarialReview: productConfig.completionGate.requireAdversarialReview,
1782
+ requireFinalValidation: productConfig.completionGate.requireFinalValidation,
1783
+ readyDeliverableStatuses: productConfig.completionGate.readyDeliverableStatuses
1784
+ },
1785
+ prompts: promptRecords,
1786
+ requiredFiles
1787
+ });
1788
+
1789
+ fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Product Shaping Run\n\nRun: ${runName}\nRelease: ${releaseId}\n\n## Roots\n\n- Specs: ${specRootRelative}\n- Run: ${runRootRelative}\n- Config: ${configPath}\n- Profile: ${profilePath}\n- Contract: ${productConfig.contractPath}\n\n## Execution Order\n\n1. Read ${productConfig.processDocPath}.\n2. Read ${productConfig.contractPath}.\n3. Create or repair product specs under ${specRootRelative}.\n4. Keep runtime notes and draft artifacts in this run directory.\n5. Write adversarial review evidence to ${adversarialReviewPath}.\n6. Write final validation evidence to ${finalValidationPath}.\n7. Validate that release-blocking questions, scope, domain spec, acceptance criteria and deliverables satisfy the completion gate.\n8. Hand off only ${handoffDeliverablesPath} to delivery implementation.\n\n## Passing Evidence Format\n\n- Adversarial review must include \`Status: pass\` or \`Result: pass\` and \`Blocking findings: none\`.\n- Final validation must include \`Status: pass\` or \`Result: pass\` and \`Ready for delivery implementation: yes\`.\n\n## Required Product Spec Files\n\n${requiredFiles.map((file) => `- ${file.fullPath}`).join("\n")}\n`, "utf8");
1790
+
1791
+ if (fs.existsSync(runRoot)) {
1792
+ throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
1793
+ }
1794
+ fs.renameSync(stagingRunRoot, runRoot);
1795
+
1796
+ console.log(`Created product-shaping run: ${runRoot}`);
1797
+ console.log(`Release: ${releaseId}`);
1798
+ console.log(`Spec root: ${specRootRelative}`);
1799
+ console.log(`Required product spec files: ${requiredFiles.length}`);
1800
+ }
1801
+
1802
+ function productValidationFlagsFromWorkUnitFlags(flags) {
1803
+ const validationFlags = {};
1804
+ if (flags["product-run-id"] && flags["product-run-root"]) {
1805
+ throw new Error("Use either --product-run-id or --product-run-root, not both.");
1806
+ }
1807
+ if (flags["product-run-id"]) {
1808
+ validationFlags["run-id"] = flags["product-run-id"];
1809
+ }
1810
+ if (flags["product-run-root"]) {
1811
+ validationFlags["run-root"] = flags["product-run-root"];
1812
+ }
1813
+ return validationFlags;
1814
+ }
1815
+
1816
+ function enforceProductHandoffGate(targetRoot, wefterConfig, workUnitConfig, flags) {
1817
+ if (!isProductDeliverablesHandoff(workUnitConfig.workUnitsDocument)) {
1818
+ return;
1819
+ }
1820
+
1821
+ const productConfigPath = productShapingConfigPath(wefterConfig);
1822
+ const productConfig = readJson(path.join(targetRoot, productConfigPath), "product-shaping config");
1823
+ validateProductShapingConfig(productConfig);
1824
+ const expectedHandoff = productSpecPath(productConfig.specRoot, workUnitConfig.releaseId, "releases/<release-id>/DELIVERABLES.md");
1825
+ if (!assertConfiguredProductHandoff(workUnitConfig.workUnitsDocument, expectedHandoff)) {
1826
+ return;
1827
+ }
1828
+
1829
+ if (!flags["product-run-id"] && !flags["product-run-root"]) {
1830
+ throw new Error(`Using product-shaping DELIVERABLES.md as a work-unit handoff requires --product-run-id or --product-run-root so the product completion gate can be verified.`);
1831
+ }
1832
+
1833
+ const result = validateProductSpecs(targetRoot, productConfig, workUnitConfig.releaseId, productValidationFlagsFromWorkUnitFlags(flags));
1834
+ if (result.errors.length > 0) {
1835
+ throw new Error(`Product-shaping handoff is not valid for delivery implementation:\n- ${result.errors.join("\n- ")}`);
1836
+ }
1837
+ }
1838
+
1081
1839
  function commandWorkUnitRun(flags) {
1082
1840
  const targetRoot = resolveTarget(flags);
1083
1841
  const wefterConfig = readConfig(targetRoot);
1084
1842
  const configPath = workUnitConfigPath(wefterConfig, flags);
1085
1843
  const profilePath = workUnitProfilePath(wefterConfig, flags);
1086
1844
  const workUnitConfig = readJson(path.join(targetRoot, configPath), "work-unit config");
1845
+ if (flags["work-units-document"]) {
1846
+ workUnitConfig.workUnitsDocument = normalizeRelativePath(flags["work-units-document"], "work-units document override");
1847
+ }
1848
+ if (flags["release-id"]) {
1849
+ workUnitConfig.releaseId = requireString(flags["release-id"], "release id override");
1850
+ }
1087
1851
  const profile = readJson(path.join(targetRoot, profilePath), "work-unit profile");
1088
1852
  validateWorkUnitConfig(workUnitConfig);
1089
1853
  validateWorkUnitProfile(profile);
1854
+ enforceProductHandoffGate(targetRoot, wefterConfig, workUnitConfig, flags);
1090
1855
 
1091
1856
  const workUnitId = flags["work-unit-id"] || workUnitConfig.defaultWorkUnitId;
1092
1857
  const workUnitKey = getSafeWorkUnitKey(workUnitId);
@@ -1279,6 +2044,7 @@ function commandWorkUnitRun(flags) {
1279
2044
  generatedAt: new Date().toISOString(),
1280
2045
  configPath,
1281
2046
  profilePath,
2047
+ workUnitsDocument: workUnitConfig.workUnitsDocument,
1282
2048
  passesPerLens,
1283
2049
  maxAudits,
1284
2050
  gatePolicy: workUnitConfig.gatePolicy,
@@ -1312,7 +2078,7 @@ function commandWorkUnitRun(flags) {
1312
2078
  }
1313
2079
  });
1314
2080
 
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");
2081
+ fs.writeFileSync(path.join(stagingRunRoot, "README.md"), `# Work Unit Implementation Run\n\nRun: ${runName}\nWork unit: ${workUnitKey}\nRelease: ${workUnitConfig.releaseId}\n\n## Source Handoff\n\n- Work units document: ${workUnitConfig.workUnitsDocument}\n\n## Counts\n\n- Lenses: ${profile.lenses.length}\n- Variants: ${profile.variants.length}\n- Passes per lens/variant: ${passesPerLens}\n- Plan auditor prompts: ${combinations.length}\n\n## Execution Order\n\n1. Execute prompts/plan.md with the work-unit planner.\n2. Execute prompts/plan-auditors/${workUnitKey}/*.md with plan auditors.\n3. Execute prompts/consolidate-plan.md.\n4. Execute prompts/validate-plan.md.\n5. Execute prompts/repair-plan.md.\n6. Review final/approved-artifacts/${workUnitKey}/ and apply gate policy.\n7. Publish approved artifacts to ${versionedWorkUnitDir} and ${versionedDecisionLog}.\n8. Execute prompts/implement-tasks.md task by task only after approval.\n9. After each implementation or correction, run \`wefter work-unit guard --run-id ${runName} --task-id <task-id> --mode ReadyForReview\`.\n10. Review the task with prompts/review-task.md.\n11. After each task review, run \`wefter work-unit guard --run-id ${runName} --task-id <task-id> --mode ReadyForNextTask\`.\n12. If the guard reports Needs Fix, correct the same task and repeat implementation guard -> review -> next-task guard.\n13. If the guard reports Blocked, pause the work unit for human decision or specification repair.\n14. Before final validation, run \`wefter work-unit guard --run-id ${runName} --mode ReadyForFinalValidation\`.\n15. Execute prompts/validate-work-unit.md only when all tasks pass review and the final-validation guard passes.\n`, "utf8");
1316
2082
 
1317
2083
  if (fs.existsSync(runRoot)) {
1318
2084
  throw new Error(`Run directory was created before finalizing the staging move: ${runRoot}`);
@@ -1669,6 +2435,12 @@ function commandDoctor(flags) {
1669
2435
  validateWorkUnitConfig(readJson(configPath, "work-unit config"));
1670
2436
  validateWorkUnitProfile(readJson(profilePath, "work-unit profile"));
1671
2437
  });
2438
+ check("product-shaping workflow config", () => {
2439
+ const configPath = path.join(targetRoot, productShapingConfigPath(config));
2440
+ const profilePath = path.join(targetRoot, productShapingProfilePath(config));
2441
+ validateProductShapingConfig(readJson(configPath, "product-shaping config"));
2442
+ validateProductShapingProfile(readJson(profilePath, "product-shaping profile"));
2443
+ });
1672
2444
  check("opencode agents", () => {
1673
2445
  const agentFiles = [
1674
2446
  "wefter-doc-audit-orchestrator.md",
@@ -1725,6 +2497,41 @@ function commandDoctor(flags) {
1725
2497
  assertIncludes(orchestrator, workUnitProfilePath(config), "work-unit orchestrator workflow profile path");
1726
2498
  assertIncludes(orchestrator, config.runnerCommand, "work-unit orchestrator runner command");
1727
2499
  });
2500
+ check("product-shaping opencode agents", () => {
2501
+ const agentFiles = [
2502
+ "wefter-product-orchestrator.md",
2503
+ "wefter-product-intake-analyst.md",
2504
+ "wefter-product-reference-researcher.md",
2505
+ "wefter-product-shaper.md",
2506
+ "wefter-product-domain-modeler.md",
2507
+ "wefter-product-release-planner.md",
2508
+ "wefter-product-auditor.md",
2509
+ "wefter-product-validator.md",
2510
+ "wefter-product-repairer.md"
2511
+ ];
2512
+ const specGlob = `${productShapingSpecRoot(config)}/**`;
2513
+ const runGlob = `${productShapingRunRoot(config)}/**`;
2514
+ const specWindowsGlob = windowsPermissionGlob(productShapingSpecRoot(config));
2515
+ const runWindowsGlob = windowsPermissionGlob(productShapingRunRoot(config));
2516
+
2517
+ for (const file of agentFiles) {
2518
+ const fullPath = path.join(targetRoot, ".opencode/agent", file);
2519
+ const content = readTextRequired(fullPath);
2520
+ assertNoPlaceholders(fullPath, content);
2521
+ if (!file.includes("auditor") && !file.includes("validator")) {
2522
+ assertIncludes(content, specGlob, `${file} POSIX spec permission`);
2523
+ assertIncludes(content, specWindowsGlob, `${file} Windows spec permission`);
2524
+ }
2525
+ assertIncludes(content, runGlob, `${file} POSIX run permission`);
2526
+ assertIncludes(content, runWindowsGlob, `${file} Windows run permission`);
2527
+ }
2528
+
2529
+ const orchestrator = readTextRequired(path.join(targetRoot, ".opencode/agent/wefter-product-orchestrator.md"));
2530
+ assertIncludes(orchestrator, CONFIG_FILE, "product orchestrator config reference");
2531
+ assertIncludes(orchestrator, productShapingConfigPath(config), "product orchestrator workflow config path");
2532
+ assertIncludes(orchestrator, productShapingProfilePath(config), "product orchestrator workflow profile path");
2533
+ assertIncludes(orchestrator, config.runnerCommand, "product orchestrator runner command");
2534
+ });
1728
2535
  check("documentation repair opencode agents", () => {
1729
2536
  const agentFiles = [
1730
2537
  "wefter-doc-repair-orchestrator.md",
@@ -1765,6 +2572,15 @@ function commandDoctor(flags) {
1765
2572
  assertIncludes(content, workUnitConfigPath(config), "work-unit skill config path");
1766
2573
  assertIncludes(content, workUnitProfilePath(config), "work-unit skill profile path");
1767
2574
  });
2575
+ check("product-shaping opencode skill", () => {
2576
+ const skillPath = path.join(targetRoot, ".opencode/skills/product-shaping/SKILL.md");
2577
+ const content = readTextRequired(skillPath);
2578
+ assertNoPlaceholders(skillPath, content);
2579
+ assertIncludes(content, productShapingConfigPath(config), "product skill config path");
2580
+ assertIncludes(content, productShapingProfilePath(config), "product skill profile path");
2581
+ assertIncludes(content, productShapingSpecRoot(config), "product skill spec root");
2582
+ assertIncludes(content, productShapingRunRoot(config), "product skill run root");
2583
+ });
1768
2584
  check("documentation repair opencode skill", () => {
1769
2585
  const skillPath = path.join(targetRoot, ".opencode/skills/documentation-repair/SKILL.md");
1770
2586
  const content = readTextRequired(skillPath);
@@ -1776,12 +2592,22 @@ function commandDoctor(flags) {
1776
2592
  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
2593
  throw new Error("Missing Wefter opencode commands.");
1778
2594
  }
2595
+ const productSettings = workflowSettings(config, PRODUCT_SHAPING_WORKFLOW_ID);
2596
+ if (productSettings.enabled && opencode.command?.["wefter-shape-product"]?.agent !== "wefter-product-orchestrator") {
2597
+ throw new Error("Missing enabled product-shaping opencode command.");
2598
+ }
2599
+ if (!productSettings.enabled && opencode.command?.["wefter-shape-product"]) {
2600
+ throw new Error("Disabled product-shaping workflow must not install a runnable opencode command.");
2601
+ }
1779
2602
  if (!Array.isArray(opencode.skills?.paths) || !opencode.skills.paths.includes(".opencode/skills")) {
1780
2603
  throw new Error("Missing .opencode/skills in opencode skills.paths.");
1781
2604
  }
1782
2605
  const watcherIgnore = Array.isArray(opencode.watcher?.ignore) ? opencode.watcher.ignore : [];
1783
2606
  const workUnitConfig = readJson(path.join(targetRoot, workUnitConfigPath(config)), "work-unit config");
1784
- for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig.runArtifactsRoot]) {
2607
+ for (const ignored of [config.artifactRoot, config.templateRoot, documentationRepairArtifactRoot(), workUnitConfig.runArtifactsRoot, productSettings.enabled ? productShapingRunRoot(config) : null]) {
2608
+ if (!ignored) {
2609
+ continue;
2610
+ }
1785
2611
  const pattern = `${ignored.replace(/\/$/, "")}/**`;
1786
2612
  if (!watcherIgnore.includes(pattern)) {
1787
2613
  throw new Error(`Missing opencode watcher ignore '${pattern}'.`);
@@ -1804,16 +2630,20 @@ function commandDoctor(flags) {
1804
2630
 
1805
2631
  export async function main(argv = process.argv.slice(2)) {
1806
2632
  const { positional, flags } = parseArgs(argv);
1807
- if (flags.help || positional.length === 0) {
1808
- printHelp();
1809
- return;
1810
- }
1811
2633
  if (flags.version) {
1812
2634
  console.log(VERSION);
1813
2635
  return;
1814
2636
  }
2637
+ if (flags.help || positional.length === 0) {
2638
+ printHelp();
2639
+ return;
2640
+ }
1815
2641
 
1816
2642
  const [command, subcommand] = positional;
2643
+ const allowedFlags = allowedFlagsForCommand(command, subcommand);
2644
+ if (allowedFlags) {
2645
+ assertKnownFlags(flags, allowedFlags);
2646
+ }
1817
2647
  if (command === "init") {
1818
2648
  await commandInit(flags);
1819
2649
  return;
@@ -1833,6 +2663,14 @@ export async function main(argv = process.argv.slice(2)) {
1833
2663
  commandDocsRepair(flags);
1834
2664
  return;
1835
2665
  }
2666
+ if (command === "product" && subcommand === "shape") {
2667
+ commandProductShape(flags);
2668
+ return;
2669
+ }
2670
+ if (command === "product" && subcommand === "validate") {
2671
+ commandProductValidate(flags);
2672
+ return;
2673
+ }
1836
2674
  if (command === "work-unit" && subcommand === "run") {
1837
2675
  commandWorkUnitRun(flags);
1838
2676
  return;