@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.
- package/CHANGELOG.md +22 -0
- package/README.md +23 -9
- package/docs/ARCHITECTURE.md +8 -12
- package/docs/INSTALLATION.md +2 -1
- package/docs/SAFETY_MODEL.md +1 -1
- package/docs/SELF_AUDIT.md +37 -0
- package/docs/WORKFLOWS.md +7 -3
- package/package.json +2 -2
- package/schemas/documentation-audit-profile.schema.json +9 -1
- package/schemas/product-shaping-config.schema.json +63 -0
- package/schemas/product-shaping-contract.schema.json +204 -0
- package/schemas/product-shaping-profile.schema.json +39 -0
- package/schemas/product-shaping-run-manifest.schema.json +103 -0
- package/schemas/wefter.config.schema.json +46 -15
- package/schemas/work-unit-config.schema.json +12 -5
- package/schemas/workflow-manifest.schema.json +10 -0
- package/src/cli/main.js +857 -19
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-audit-orchestrator.md.tmpl +14 -10
- package/src/workflows/documentation-audit/templates/opencode/agent/wefter-doc-auditor.md.tmpl +3 -0
- package/src/workflows/documentation-audit/templates/opencode/skills/documentation-audit/SKILL.md.tmpl +1 -0
- package/src/workflows/documentation-audit/templates/prompts/auditor-prompt.md +1 -0
- package/src/workflows/product-shaping/README.md +1241 -3
- package/src/workflows/product-shaping/compatibility.md +33 -0
- package/src/workflows/product-shaping/contracts/product-spec-contract.json +250 -0
- package/src/workflows/product-shaping/templates/default-config.json +34 -0
- package/src/workflows/product-shaping/templates/default-profile.json +48 -0
- package/src/workflows/product-shaping/templates/documentation-audit/workflow-self-audit-auditor-prompt.md +117 -0
- package/src/workflows/product-shaping/templates/documentation-audit-profile.json +80 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-auditor.md.tmpl +22 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-domain-modeler.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-intake-analyst.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-orchestrator.md.tmpl +48 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-reference-researcher.md.tmpl +29 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-release-planner.md.tmpl +34 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-repairer.md.tmpl +25 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-shaper.md.tmpl +31 -0
- package/src/workflows/product-shaping/templates/opencode/agent/wefter-product-validator.md.tmpl +23 -0
- package/src/workflows/product-shaping/templates/opencode/skills/product-shaping/SKILL.md.tmpl +45 -0
- package/src/workflows/product-shaping/templates/prompts/domain-modeler-prompt.md +27 -0
- package/src/workflows/product-shaping/templates/prompts/intake-prompt.md +30 -0
- package/src/workflows/product-shaping/templates/prompts/product-auditor-prompt.md +53 -0
- package/src/workflows/product-shaping/templates/prompts/product-repairer-prompt.md +25 -0
- package/src/workflows/product-shaping/templates/prompts/product-shaper-prompt.md +26 -0
- package/src/workflows/product-shaping/templates/prompts/product-validator-prompt.md +55 -0
- package/src/workflows/product-shaping/templates/prompts/reference-research-prompt.md +25 -0
- package/src/workflows/product-shaping/templates/prompts/release-planner-prompt.md +34 -0
- package/src/workflows/product-shaping/workflow.json +26 -3
- package/src/workflows/technical-shaping/README.md +2 -0
- 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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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/.\
|
|
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;
|