@storewright/cli 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +19 -0
  2. package/VERSION +1 -0
  3. package/bin/storewright.mjs +62 -0
  4. package/contracts/action-registry.json +175 -0
  5. package/contracts/capability-registry.json +63 -0
  6. package/contracts/workflow-manifest.json +207 -0
  7. package/lib/cli/storewright-cli.mjs +259 -0
  8. package/lib/internal/launch-envelope.mjs +223 -0
  9. package/lib/internal/multi-agent-contracts.mjs +137 -0
  10. package/lib/internal/operation-ledger.mjs +190 -0
  11. package/lib/internal/pricing/default-preview-pricing.mjs +181 -0
  12. package/lib/internal/run-state-helpers.mjs +313 -0
  13. package/lib/internal/shopify-operation-adapter.mjs +456 -0
  14. package/package.json +38 -0
  15. package/schemas/action-registry.schema.json +11 -0
  16. package/schemas/agent-report.schema.json +14 -0
  17. package/schemas/approval-grant.schema.json +16 -0
  18. package/schemas/base-theme-report.schema.json +25 -0
  19. package/schemas/brand-identity.schema.json +142 -0
  20. package/schemas/capability-registry.schema.json +11 -0
  21. package/schemas/competitor-audit.schema.json +38 -0
  22. package/schemas/design-direction.schema.json +64 -0
  23. package/schemas/external-operation.schema.json +34 -0
  24. package/schemas/intake-blocked-report.schema.json +76 -0
  25. package/schemas/launch-envelope.schema.json +25 -0
  26. package/schemas/launch-readiness.schema.json +73 -0
  27. package/schemas/media-file-inspection-report.schema.json +223 -0
  28. package/schemas/media-manifest.schema.json +84 -0
  29. package/schemas/merchandising-brief.schema.json +27 -0
  30. package/schemas/normalized-product-catalog.schema.json +42 -0
  31. package/schemas/product-content-generation-input.schema.json +40 -0
  32. package/schemas/product-content-generation-output.schema.json +43 -0
  33. package/schemas/raw-product-candidates.schema.json +32 -0
  34. package/schemas/shopify-access-preflight-report.schema.json +213 -0
  35. package/schemas/shopify-content-sync-report.schema.json +190 -0
  36. package/schemas/shopify-media-map.schema.json +87 -0
  37. package/schemas/shopify-media-upload-report.schema.json +96 -0
  38. package/schemas/shopify-operation-request.schema.json +81 -0
  39. package/schemas/shopify-preflight-report.schema.json +187 -0
  40. package/schemas/store-blueprint.schema.json +112 -0
  41. package/schemas/store-content-generation-output.schema.json +102 -0
  42. package/schemas/store-intake.schema.json +205 -0
  43. package/schemas/store-ops-plan.schema.json +82 -0
  44. package/schemas/storefront-preview-review.schema.json +227 -0
  45. package/schemas/supplier-access-report.schema.json +36 -0
  46. package/schemas/supplier-extraction-report.schema.json +185 -0
  47. package/schemas/theme-build-report.schema.json +43 -0
  48. package/schemas/theme-code-change-summary.schema.json +65 -0
  49. package/schemas/theme-plan.schema.json +26 -0
  50. package/schemas/theme-push-report.schema.json +151 -0
  51. package/schemas/theme-workspace-validation-report.schema.json +61 -0
  52. package/schemas/workflow-manifest.schema.json +29 -0
  53. package/scripts/audit-run-state.mjs +472 -0
  54. package/scripts/execute-shopify-operation.mjs +190 -0
  55. package/scripts/generate-image-assets-openai.mjs +342 -0
  56. package/scripts/generate-media-assets.mjs +121 -0
  57. package/scripts/init-run-state.mjs +69 -0
  58. package/scripts/inspect-media-files.mjs +334 -0
  59. package/scripts/prepare-launch-envelope.mjs +47 -0
  60. package/scripts/shopify-access-preflight.mjs +432 -0
  61. package/scripts/upload-shopify-media.mjs +831 -0
  62. package/scripts/validate-agent-report.mjs +46 -0
  63. package/scripts/validate-artifact.mjs +196 -0
  64. package/scripts/validate-launch-envelope.mjs +50 -0
  65. package/scripts/validate-registries.mjs +50 -0
  66. package/scripts/validate-workflow-manifest.mjs +38 -0
  67. package/scripts/version.mjs +192 -0
@@ -0,0 +1,190 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile, appendFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+
5
+ const allowedOperationStatuses = new Set([
6
+ "prepared",
7
+ "dispatched",
8
+ "succeeded",
9
+ "failed",
10
+ "indeterminate",
11
+ "reconciled",
12
+ "compensated"
13
+ ]);
14
+
15
+ function stableJson(value) {
16
+ return `${JSON.stringify(value, null, 2)}\n`;
17
+ }
18
+
19
+ function sha256Text(value) {
20
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
21
+ }
22
+
23
+ function ledgerPath({ runDir, stageId, attemptId }) {
24
+ return join(runDir, "stages", stageId, "attempts", attemptId, "work", "external-operations.jsonl");
25
+ }
26
+
27
+ function approvalPath({ runDir, approvalId }) {
28
+ return join(runDir, "approvals", `${approvalId}.json`);
29
+ }
30
+
31
+ function includesAll(values, required = []) {
32
+ const valueSet = new Set(values ?? []);
33
+ return required.every((value) => valueSet.has(value));
34
+ }
35
+
36
+ function isInsideDirectory(parentDir, childPath) {
37
+ const relativePath = relative(resolve(parentDir), resolve(childPath));
38
+ return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
39
+ }
40
+
41
+ export async function createApprovalGrant({
42
+ runDir,
43
+ approvalId,
44
+ runId,
45
+ actionId,
46
+ impactClass,
47
+ approvalRequirement,
48
+ approvedBy,
49
+ source
50
+ }) {
51
+ const artifact = {
52
+ schemaVersion: "1.0.0",
53
+ approvalId,
54
+ runId,
55
+ actionId,
56
+ impactClass,
57
+ approvalRequirement,
58
+ approvedBy,
59
+ source,
60
+ grantedAt: new Date().toISOString()
61
+ };
62
+ const path = approvalPath({ runDir, approvalId });
63
+ await mkdir(dirname(path), { recursive: true });
64
+ await writeFile(path, stableJson(artifact));
65
+ return { path, artifact, sha256: sha256Text(JSON.stringify(artifact)) };
66
+ }
67
+
68
+ export async function validateApprovalForAction({
69
+ runDir,
70
+ actionRegistry,
71
+ actionId,
72
+ approvalGrantPath
73
+ }) {
74
+ const errors = [];
75
+ const action = actionRegistry.actions?.[actionId];
76
+ if (!action) {
77
+ return { ok: false, errors: [`Unknown action: ${actionId}`] };
78
+ }
79
+ if (action.approvalRequirement !== "explicit-user") {
80
+ return { ok: true, errors };
81
+ }
82
+ if (!approvalGrantPath) {
83
+ return { ok: false, errors: [`${actionId} requires explicit-user approval`] };
84
+ }
85
+ if (!isInsideDirectory(join(runDir, "approvals"), approvalGrantPath)) {
86
+ return { ok: false, errors: ["approval grant must live under the run approvals directory"] };
87
+ }
88
+
89
+ const grant = JSON.parse(await readFile(approvalGrantPath, "utf8"));
90
+ if (grant.actionId !== actionId) errors.push("approval grant actionId does not match action");
91
+ if (grant.impactClass !== action.impactClass) errors.push("approval grant impactClass does not match action");
92
+ if (grant.approvalRequirement !== "explicit-user") errors.push("approval grant must be explicit-user");
93
+ if (!grant.approvedBy) errors.push("approval grant approvedBy is required");
94
+ return { ok: errors.length === 0, errors };
95
+ }
96
+
97
+ function operationIdFromKey(operationKey) {
98
+ return createHash("sha256").update(operationKey).digest("hex").slice(0, 20);
99
+ }
100
+
101
+ export async function prepareExternalOperation({
102
+ runDir,
103
+ stageId,
104
+ attemptId,
105
+ envelope,
106
+ actionRegistry,
107
+ actionId,
108
+ launchEnvelopeSha256,
109
+ grantedScopes = [],
110
+ approvalGrantPath = null,
111
+ operationKey,
112
+ targetStore,
113
+ apiVersion,
114
+ requestFingerprint,
115
+ naturalResourceKey,
116
+ providerIdempotencySupported,
117
+ resourceType
118
+ }) {
119
+ const action = actionRegistry.actions?.[actionId];
120
+ if (!action) {
121
+ throw new Error(`Unknown action: ${actionId}`);
122
+ }
123
+ if (!envelope.effectiveAllowedActions?.includes(actionId)) {
124
+ throw new Error(`${actionId} is not present in effectiveAllowedActions`);
125
+ }
126
+ if (launchEnvelopeSha256 !== envelope.launchEnvelopeSha256) {
127
+ throw new Error("launchEnvelopeSha256 does not match envelope");
128
+ }
129
+ if (!includesAll(grantedScopes, action.requiredScopes)) {
130
+ throw new Error(`${actionId} missing required scopes`);
131
+ }
132
+ const approval = await validateApprovalForAction({ runDir, actionRegistry, actionId, approvalGrantPath });
133
+ if (!approval.ok) {
134
+ throw new Error(approval.errors.join("\n"));
135
+ }
136
+
137
+ const operation = {
138
+ schemaVersion: "1.0.0",
139
+ operationId: operationIdFromKey(operationKey),
140
+ operationKey,
141
+ actionId,
142
+ launchEnvelopeSha256,
143
+ targetStore,
144
+ apiVersion,
145
+ requestFingerprint,
146
+ naturalResourceKey,
147
+ providerIdempotencySupported,
148
+ resourceType,
149
+ resourceId: null,
150
+ resourceOwnership: "created-by-this-run",
151
+ status: "prepared",
152
+ reconcileStrategy: "lookup-by-natural-resource-key-and-compare",
153
+ compensation: null,
154
+ recordedAt: new Date().toISOString()
155
+ };
156
+
157
+ const path = ledgerPath({ runDir, stageId, attemptId });
158
+ await mkdir(dirname(path), { recursive: true });
159
+ await appendFile(path, `${JSON.stringify(operation)}\n`);
160
+ return { status: "prepared", operation, ledgerPath: path };
161
+ }
162
+
163
+ export async function transitionExternalOperation({
164
+ runDir,
165
+ stageId,
166
+ attemptId,
167
+ operationId,
168
+ status,
169
+ patch = {}
170
+ }) {
171
+ if (!allowedOperationStatuses.has(status)) {
172
+ throw new Error(`Unsupported operation status: ${status}`);
173
+ }
174
+ const reservedPatchKeys = ["schemaVersion", "operationId", "status", "recordedAt"];
175
+ const reservedKey = reservedPatchKeys.find((key) => Object.hasOwn(patch, key));
176
+ if (reservedKey) {
177
+ throw new Error(`Patch contains reserved ledger field: ${reservedKey}`);
178
+ }
179
+ const record = {
180
+ schemaVersion: "1.0.0",
181
+ ...patch,
182
+ operationId,
183
+ status,
184
+ recordedAt: new Date().toISOString()
185
+ };
186
+ const path = ledgerPath({ runDir, stageId, attemptId });
187
+ await mkdir(dirname(path), { recursive: true });
188
+ await appendFile(path, `${JSON.stringify(record)}\n`);
189
+ return record;
190
+ }
@@ -0,0 +1,181 @@
1
+ export const DEFAULT_PREVIEW_PRICING_STRATEGY = Object.freeze({
2
+ id: "default-preview-pricing-v1",
3
+ currencyRates: Object.freeze({
4
+ CNY: Object.freeze({ USD: 0.14 }),
5
+ USD: Object.freeze({ USD: 1 })
6
+ }),
7
+ landedCostMultiplier: 2.8,
8
+ compareAtMultiplier: 1.25,
9
+ rounding: ".00"
10
+ });
11
+
12
+ function normalizeCurrency(value, fallback = "") {
13
+ return String(value || fallback).trim().toUpperCase();
14
+ }
15
+
16
+ function parsePositiveAmount(value) {
17
+ const amount = Number.parseFloat(String(value ?? ""));
18
+ return Number.isFinite(amount) && amount > 0 ? amount : undefined;
19
+ }
20
+
21
+ function roundAmount(value, rounding = ".00") {
22
+ if (rounding === ".00") {
23
+ return Math.round(value).toFixed(2);
24
+ }
25
+ return value.toFixed(2);
26
+ }
27
+
28
+ function formatSourceAmount(value) {
29
+ const amount = parsePositiveAmount(value);
30
+ return amount === undefined ? "" : amount.toFixed(2);
31
+ }
32
+
33
+ function strategyRate({ sourceCurrency, targetCurrency, strategy }) {
34
+ if (sourceCurrency === targetCurrency) {
35
+ return 1;
36
+ }
37
+ if (Number.isFinite(strategy.fxRate) && strategy.fxRate > 0) {
38
+ return strategy.fxRate;
39
+ }
40
+ const directRate = strategy.currencyRates?.[sourceCurrency]?.[targetCurrency];
41
+ return Number.isFinite(directRate) && directRate > 0 ? directRate : undefined;
42
+ }
43
+
44
+ function reviewResult({ sourceAmount, sourceCurrency, targetCurrency, status, strategy }) {
45
+ return {
46
+ amount: "",
47
+ currency: targetCurrency,
48
+ compareAtAmount: "",
49
+ compareAtCurrency: targetCurrency,
50
+ status,
51
+ merchantApprovalRequired: true,
52
+ warningTag: "price-review-required",
53
+ draftWriteBlocked: false,
54
+ provenance: {
55
+ sourceAmount: formatSourceAmount(sourceAmount),
56
+ sourceCurrency,
57
+ targetAmount: "",
58
+ targetCurrency,
59
+ strategyId: strategy.id,
60
+ fxRate: "",
61
+ landedCostMultiplier: strategy.landedCostMultiplier,
62
+ compareAtMultiplier: strategy.compareAtMultiplier,
63
+ rounding: strategy.rounding,
64
+ status,
65
+ merchantApprovalRequired: true
66
+ }
67
+ };
68
+ }
69
+
70
+ function userFormulaStrategy(userFormula) {
71
+ if (!userFormula) {
72
+ return DEFAULT_PREVIEW_PRICING_STRATEGY;
73
+ }
74
+ return {
75
+ id: userFormula.id || "user-provided-pricing-formula",
76
+ fxRate: userFormula.fxRate,
77
+ currencyRates: userFormula.currencyRates ?? DEFAULT_PREVIEW_PRICING_STRATEGY.currencyRates,
78
+ landedCostMultiplier: userFormula.landedCostMultiplier ?? DEFAULT_PREVIEW_PRICING_STRATEGY.landedCostMultiplier,
79
+ compareAtMultiplier: userFormula.compareAtMultiplier ?? DEFAULT_PREVIEW_PRICING_STRATEGY.compareAtMultiplier,
80
+ rounding: userFormula.rounding ?? DEFAULT_PREVIEW_PRICING_STRATEGY.rounding
81
+ };
82
+ }
83
+
84
+ export function derivePreviewPricing({
85
+ sourceAmount,
86
+ sourceCurrency,
87
+ targetCurrency = "USD",
88
+ userPrice,
89
+ userFormula
90
+ } = {}) {
91
+ const normalizedTargetCurrency = normalizeCurrency(userPrice?.currency, normalizeCurrency(targetCurrency, "USD"));
92
+ const normalizedSourceCurrency = normalizeCurrency(sourceCurrency);
93
+ const approvedAmount = parsePositiveAmount(userPrice?.amount);
94
+ if (approvedAmount !== undefined) {
95
+ const amount = approvedAmount.toFixed(2);
96
+ return {
97
+ amount,
98
+ currency: normalizedTargetCurrency,
99
+ compareAtAmount: "",
100
+ compareAtCurrency: normalizedTargetCurrency,
101
+ status: "user-approved",
102
+ merchantApprovalRequired: false,
103
+ warningTag: "",
104
+ draftWriteBlocked: false,
105
+ provenance: {
106
+ sourceAmount: formatSourceAmount(sourceAmount),
107
+ sourceCurrency: normalizedSourceCurrency,
108
+ targetAmount: amount,
109
+ targetCurrency: normalizedTargetCurrency,
110
+ strategyId: "user-approved-price",
111
+ fxRate: "",
112
+ landedCostMultiplier: "",
113
+ compareAtMultiplier: "",
114
+ rounding: "",
115
+ status: "user-approved",
116
+ merchantApprovalRequired: false
117
+ }
118
+ };
119
+ }
120
+
121
+ const strategy = userFormulaStrategy(userFormula);
122
+ const amount = parsePositiveAmount(sourceAmount);
123
+ if (amount === undefined) {
124
+ return reviewResult({
125
+ sourceAmount,
126
+ sourceCurrency: normalizedSourceCurrency,
127
+ targetCurrency: normalizedTargetCurrency,
128
+ status: "missing",
129
+ strategy
130
+ });
131
+ }
132
+ if (!normalizedSourceCurrency) {
133
+ return reviewResult({
134
+ sourceAmount,
135
+ sourceCurrency: "",
136
+ targetCurrency: normalizedTargetCurrency,
137
+ status: "missing-source-currency",
138
+ strategy
139
+ });
140
+ }
141
+ const fxRate = strategyRate({
142
+ sourceCurrency: normalizedSourceCurrency,
143
+ targetCurrency: normalizedTargetCurrency,
144
+ strategy
145
+ });
146
+ if (!Number.isFinite(fxRate) || fxRate <= 0) {
147
+ return reviewResult({
148
+ sourceAmount,
149
+ sourceCurrency: normalizedSourceCurrency,
150
+ targetCurrency: normalizedTargetCurrency,
151
+ status: "missing-currency-rate",
152
+ strategy
153
+ });
154
+ }
155
+
156
+ const targetAmount = roundAmount(amount * fxRate * strategy.landedCostMultiplier, strategy.rounding);
157
+ const compareAtAmount = roundAmount(Number.parseFloat(targetAmount) * strategy.compareAtMultiplier, strategy.rounding);
158
+ return {
159
+ amount: targetAmount,
160
+ currency: normalizedTargetCurrency,
161
+ compareAtAmount,
162
+ compareAtCurrency: normalizedTargetCurrency,
163
+ status: "derived-preview",
164
+ merchantApprovalRequired: true,
165
+ warningTag: "price-review-required",
166
+ draftWriteBlocked: false,
167
+ provenance: {
168
+ sourceAmount: amount.toFixed(2),
169
+ sourceCurrency: normalizedSourceCurrency,
170
+ targetAmount,
171
+ targetCurrency: normalizedTargetCurrency,
172
+ strategyId: strategy.id,
173
+ fxRate,
174
+ landedCostMultiplier: strategy.landedCostMultiplier,
175
+ compareAtMultiplier: strategy.compareAtMultiplier,
176
+ rounding: strategy.rounding,
177
+ status: "derived-preview",
178
+ merchantApprovalRequired: true
179
+ }
180
+ };
181
+ }
@@ -0,0 +1,313 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const runtimeRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
7
+ const pluginRoot = resolve(runtimeRoot, "..");
8
+
9
+ const stageOrder = [
10
+ "1 Intake",
11
+ "1A Shopify Access Preflight",
12
+ "2 Product Source Normalize",
13
+ "3 Supplier Extraction",
14
+ "4 Product Content",
15
+ "5 Merchandising Strategy",
16
+ "6 Brand Identity",
17
+ "7 Store Content",
18
+ "8 Design Direction",
19
+ "9 Store Blueprint",
20
+ "9A Media Production",
21
+ "9B Shopify Media Upload",
22
+ "10 Theme Build And Apply",
23
+ "11A Shopify Preflight Auth Scope",
24
+ "11B Shopify Products Collections",
25
+ "11C Shopify Content Navigation",
26
+ "11D Shopify Theme Push",
27
+ "11E Shopify Store Ops Consolidation",
28
+ "12 Launch Readiness"
29
+ ];
30
+
31
+ const stageResumeContract = new Map([
32
+ ["1 Intake", { packet: "workflows/stages/01-intake.md", workerProfile: "intake-worker" }],
33
+ ["1A Shopify Access Preflight", { packet: "workflows/stages/01A-shopify-access-preflight.md", workerProfile: "shopify-access-preflight-worker" }],
34
+ ["2 Product Source Normalize", { packet: "workflows/stages/02-product-source-normalize.md", workerProfile: "product-source-normalize-worker" }],
35
+ ["3 Supplier Extraction", { packet: "workflows/stages/03-supplier-extraction.md", workerProfile: "supplier-extraction-worker" }],
36
+ ["4 Product Content", { packet: "workflows/stages/04-product-content.md", workerProfile: "product-content-worker" }],
37
+ ["5 Merchandising Strategy", { packet: "workflows/stages/05-merchandising-strategy.md", workerProfile: "merchandising-strategy-worker" }],
38
+ ["6 Brand Identity", { packet: "workflows/stages/06-brand-identity.md", workerProfile: "brand-identity-worker" }],
39
+ ["7 Store Content", { packet: "workflows/stages/07-store-content.md", workerProfile: "store-content-worker" }],
40
+ ["8 Design Direction", { packet: "workflows/stages/08-design-direction.md", workerProfile: "design-direction-worker" }],
41
+ ["9 Store Blueprint", { packet: "workflows/stages/09-store-blueprint.md", workerProfile: "store-blueprint-worker" }],
42
+ ["9A Media Production", { packet: "workflows/stages/09A-media-production.md", workerProfile: "media-production-worker" }],
43
+ ["9B Shopify Media Upload", { packet: "workflows/stages/09B-shopify-media-upload.md", workerProfile: "shopify-media-upload-worker" }],
44
+ ["10 Theme Build And Apply", { packet: "workflows/stages/10-theme-build-and-apply.md", workerProfile: "theme-build-worker" }],
45
+ ["11A Shopify Preflight Auth Scope", { packet: "workflows/stages/11A-shopify-preflight-auth-scope.md", workerProfile: "store-ops-preflight-worker" }],
46
+ ["11B Shopify Products Collections", { packet: "workflows/stages/11B-shopify-products-collections.md", workerProfile: "store-ops-products-worker" }],
47
+ ["11C Shopify Content Navigation", { packet: "workflows/stages/11C-shopify-content-navigation.md", workerProfile: "store-ops-content-worker" }],
48
+ ["11D Shopify Theme Push", { packet: "workflows/stages/11D-shopify-theme-push.md", workerProfile: "store-ops-theme-worker" }],
49
+ ["11E Shopify Store Ops Consolidation", { packet: "workflows/stages/11E-shopify-store-ops-consolidation.md", workerProfile: "store-ops-consolidation-worker" }],
50
+ ["12 Launch Readiness", { packet: "workflows/stages/12-launch-readiness.md", workerProfile: "launch-readiness-worker" }]
51
+ ]);
52
+
53
+ async function readVersionText(path) {
54
+ return (await readFile(path, "utf8")).trim();
55
+ }
56
+
57
+ async function currentPluginVersionInfo() {
58
+ const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
59
+ if (!existsSync(manifestPath)) {
60
+ const pkg = JSON.parse(await readFile(join(runtimeRoot, "package.json"), "utf8"));
61
+ return {
62
+ name: pkg.name,
63
+ manifestVersion: "",
64
+ pluginVersion: await readVersionText(join(runtimeRoot, "VERSION")),
65
+ skillVersion: ""
66
+ };
67
+ }
68
+
69
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
70
+ return {
71
+ name: manifest.name,
72
+ manifestVersion: manifest.version,
73
+ pluginVersion: await readVersionText(join(pluginRoot, "VERSION")),
74
+ skillVersion: await readVersionText(join(pluginRoot, "storewright", "VERSION"))
75
+ };
76
+ }
77
+
78
+ function pluginVersionLines(info) {
79
+ return [
80
+ `Plugin: ${info.name}`,
81
+ `Manifest version: ${info.manifestVersion}`,
82
+ `Plugin version: ${info.pluginVersion}`,
83
+ `Skill version: ${info.skillVersion}`
84
+ ];
85
+ }
86
+
87
+ function stageRow({
88
+ stage,
89
+ workerProfile,
90
+ executionMode = "subagent",
91
+ status,
92
+ gate,
93
+ outputs,
94
+ notes
95
+ }) {
96
+ return `| ${stage} | ${workerProfile} | ${executionMode} | ${status} | ${gate} | ${outputs} | ${notes} |`;
97
+ }
98
+
99
+ function nextStageAfter(completedStage, status) {
100
+ if (["blocked", "fatal", "repairable"].includes(status)) {
101
+ return completedStage;
102
+ }
103
+ const index = stageOrder.indexOf(completedStage);
104
+ return stageOrder[index + 1] ?? completedStage;
105
+ }
106
+
107
+ function stageResumeInstructions(stage) {
108
+ const contract = stageResumeContract.get(stage) ?? {
109
+ packet: "See the current stage packet.",
110
+ workerProfile: "stage-worker"
111
+ };
112
+
113
+ return [
114
+ "Next action:",
115
+ `Lead Agent must discover a real spawn/delegation tool and spawn ${contract.workerProfile} as the stage owner for ${stage}.`,
116
+ "Launch envelope:",
117
+ "Lead Agent must prepare the attempt control/launch-envelope.json before spawn, including resolved inputs, effectiveAllowedActions, and work/outputs path constraints.",
118
+ "Stage packet:",
119
+ contract.packet,
120
+ "Required input artifacts:",
121
+ "See the stage packet.",
122
+ "Worker trace:",
123
+ "Worker writes the trace artifact declared in the matching stage spec.",
124
+ "Expected worker handoff:",
125
+ `${contract.workerProfile} returns declared outputs under work/outputs, work/agent-report.json, and a concise handoff summary to the Lead Agent.`,
126
+ "After worker handoff:",
127
+ "Lead Agent applies the Lead-facing gate spec, writes the gate artifact, updates PROGRESS.md, and selects the next stage."
128
+ ].join("\n");
129
+ }
130
+
131
+ function workerRunningResumeInstructions(stage, activeWorker) {
132
+ const contract = stageResumeContract.get(stage) ?? {
133
+ workerProfile: activeWorker.profile ?? "stage-worker"
134
+ };
135
+ const workerProfile = activeWorker.profile ?? contract.workerProfile;
136
+
137
+ return [
138
+ "Next action:",
139
+ `Active worker: ${activeWorker.id}`,
140
+ `Wait for subagent_notification from ${workerProfile}.`,
141
+ "Do not interrupt or mark the run blocked because a wait_agent call reaches its timeout; that timeout is non-terminal while the worker is still running.",
142
+ "After worker handoff:",
143
+ "Lead Agent reads the worker handoff, work/agent-report.json, worker trace, and declared artifacts, then applies the Lead-facing gate spec."
144
+ ].join("\n");
145
+ }
146
+
147
+ function isCanonicalLaunchEnvelopePath(path) {
148
+ return /^stages\/[A-Za-z0-9_-]+\/attempts\/[A-Za-z0-9_-]+\/control\/launch-envelope\.json$/.test(path);
149
+ }
150
+
151
+ export async function writeWorkerRunningProgress({
152
+ runDir,
153
+ runId,
154
+ currentStage,
155
+ completedStages = [],
156
+ activeWorker,
157
+ pluginVersionInfo
158
+ }) {
159
+ if (!activeWorker?.id) {
160
+ throw new Error("activeWorker.id is required");
161
+ }
162
+ if (!activeWorker.launchEnvelope) {
163
+ throw new Error("activeWorker.launchEnvelope is required");
164
+ }
165
+ if (!isCanonicalLaunchEnvelopePath(activeWorker.launchEnvelope)) {
166
+ throw new Error("activeWorker.launchEnvelope must be stages/<stage-id>/attempts/<attempt-id>/control/launch-envelope.json");
167
+ }
168
+ const contract = stageResumeContract.get(currentStage) ?? {
169
+ workerProfile: activeWorker.profile ?? "stage-worker"
170
+ };
171
+ const workerProfile = activeWorker.profile ?? contract.workerProfile;
172
+ const launchEnvelope = activeWorker.launchEnvelope;
173
+ const stageRows = [
174
+ ...completedStages.map(stageRow),
175
+ stageRow({
176
+ stage: currentStage,
177
+ workerProfile,
178
+ executionMode: "worker-running",
179
+ status: "in-progress",
180
+ gate: "-",
181
+ outputs: launchEnvelope,
182
+ notes: `Active worker ${activeWorker.id}; waiting for subagent notification.`
183
+ })
184
+ ].join("\n");
185
+ const versionInfo = pluginVersionInfo ?? await currentPluginVersionInfo();
186
+
187
+ const progress = [
188
+ "# Storewright Progress",
189
+ "",
190
+ `Run ID: ${runId}`,
191
+ ...pluginVersionLines(versionInfo),
192
+ "Workflow: workflows/official-build.md",
193
+ `Current stage: ${currentStage}`,
194
+ "Status: in-progress",
195
+ `Last updated: ${new Date().toISOString()}`,
196
+ "",
197
+ "## Stage Table",
198
+ "",
199
+ "| Stage | Worker profile | Execution mode | Status | Gate | Outputs | Notes |",
200
+ "| --- | --- | --- | --- | --- | --- | --- |",
201
+ stageRows,
202
+ "",
203
+ "## Current Blocker",
204
+ "",
205
+ "None",
206
+ "",
207
+ "## Resume Instructions",
208
+ "",
209
+ workerRunningResumeInstructions(currentStage, { ...activeWorker, profile: workerProfile }),
210
+ ""
211
+ ].join("\n");
212
+
213
+ const progressPath = join(runDir, "PROGRESS.md");
214
+ await writeFile(progressPath, progress);
215
+ return progressPath;
216
+ }
217
+
218
+ export async function writeProgress({
219
+ runDir,
220
+ runId,
221
+ completedStages,
222
+ latestStage,
223
+ latestStatus,
224
+ blocker = "None",
225
+ resumeInstructions,
226
+ pluginVersionInfo
227
+ }) {
228
+ const currentStage = nextStageAfter(latestStage, latestStatus);
229
+ const progressStatus = latestStatus === "pass" || latestStatus === "warning" ? "in-progress" : latestStatus;
230
+ const stageRows = completedStages.map(stageRow).join("\n");
231
+ const versionInfo = pluginVersionInfo ?? await currentPluginVersionInfo();
232
+
233
+ const progress = [
234
+ "# Storewright Progress",
235
+ "",
236
+ `Run ID: ${runId}`,
237
+ ...pluginVersionLines(versionInfo),
238
+ "Workflow: workflows/official-build.md",
239
+ `Current stage: ${currentStage}`,
240
+ `Status: ${progressStatus}`,
241
+ `Last updated: ${new Date().toISOString()}`,
242
+ "",
243
+ "## Stage Table",
244
+ "",
245
+ "| Stage | Worker profile | Execution mode | Status | Gate | Outputs | Notes |",
246
+ "| --- | --- | --- | --- | --- | --- | --- |",
247
+ stageRows,
248
+ "",
249
+ "## Current Blocker",
250
+ "",
251
+ blocker,
252
+ "",
253
+ "## Resume Instructions",
254
+ "",
255
+ resumeInstructions ?? stageResumeInstructions(currentStage),
256
+ ""
257
+ ].join("\n");
258
+
259
+ const progressPath = join(runDir, "PROGRESS.md");
260
+ await writeFile(progressPath, progress);
261
+ return progressPath;
262
+ }
263
+
264
+ export function progressExists(runDir) {
265
+ return existsSync(join(runDir, "PROGRESS.md"));
266
+ }
267
+
268
+ export async function writeInitialProgress({ runDir, runId, pluginVersionInfo }) {
269
+ const versionInfo = pluginVersionInfo ?? await currentPluginVersionInfo();
270
+ const progress = [
271
+ "# Storewright Progress",
272
+ "",
273
+ `Run ID: ${runId}`,
274
+ ...pluginVersionLines(versionInfo),
275
+ "Workflow: workflows/official-build.md",
276
+ "Current stage: 1 Intake",
277
+ "Status: in-progress",
278
+ `Last updated: ${new Date().toISOString()}`,
279
+ "",
280
+ "## Stage Table",
281
+ "",
282
+ "| Stage | Worker profile | Execution mode | Status | Gate | Outputs | Notes |",
283
+ "| --- | --- | --- | --- | --- | --- | --- |",
284
+ "",
285
+ "## Current Blocker",
286
+ "",
287
+ "None",
288
+ "",
289
+ "## Resume Instructions",
290
+ "",
291
+ stageResumeInstructions("1 Intake"),
292
+ ""
293
+ ].join("\n");
294
+
295
+ const progressPath = join(runDir, "PROGRESS.md");
296
+ await writeFile(progressPath, progress);
297
+ return progressPath;
298
+ }
299
+
300
+ export function qualityGate({ runId, stage, status, checks, reasons = [], nextActions = [] }) {
301
+ return {
302
+ schemaVersion: "1.0.0",
303
+ runId,
304
+ qualityGate: {
305
+ stage,
306
+ status,
307
+ checks,
308
+ reasons,
309
+ nextActions,
310
+ retryCount: 0
311
+ }
312
+ };
313
+ }