cool-workflow 0.1.78
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/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/mcp.json +10 -0
- package/.codex-plugin/plugin.json +38 -0
- package/.mcp.json +10 -0
- package/LICENSE +24 -0
- package/README.md +638 -0
- package/apps/architecture-review/app.json +51 -0
- package/apps/architecture-review/workflow.js +116 -0
- package/apps/end-to-end-golden-path/app.json +30 -0
- package/apps/end-to-end-golden-path/workflow.js +33 -0
- package/apps/pr-review-fix-ci/app.json +59 -0
- package/apps/pr-review-fix-ci/workflow.js +90 -0
- package/apps/release-cut/app.json +54 -0
- package/apps/release-cut/workflow.js +82 -0
- package/apps/research-synthesis/app.json +50 -0
- package/apps/research-synthesis/workflow.js +76 -0
- package/apps/workflow-app-framework-demo/app.json +29 -0
- package/apps/workflow-app-framework-demo/workflow.js +44 -0
- package/dist/agent-config.js +223 -0
- package/dist/candidate-scoring.js +715 -0
- package/dist/capability-core.js +630 -0
- package/dist/capability-dispatcher.js +86 -0
- package/dist/capability-registry.js +523 -0
- package/dist/cli.js +1276 -0
- package/dist/collaboration.js +727 -0
- package/dist/commit.js +570 -0
- package/dist/contract-migration.js +234 -0
- package/dist/coordinator.js +1163 -0
- package/dist/daemon.js +44 -0
- package/dist/dispatch.js +201 -0
- package/dist/drive.js +503 -0
- package/dist/error-feedback.js +415 -0
- package/dist/evidence-grounding.js +179 -0
- package/dist/evidence-reasoning.js +733 -0
- package/dist/execution-backend.js +1279 -0
- package/dist/harness.js +61 -0
- package/dist/mcp-server.js +1615 -0
- package/dist/multi-agent-eval.js +857 -0
- package/dist/multi-agent-host.js +764 -0
- package/dist/multi-agent-operator-ux.js +537 -0
- package/dist/multi-agent-trust.js +366 -0
- package/dist/multi-agent.js +1173 -0
- package/dist/node-snapshot.js +270 -0
- package/dist/observability.js +922 -0
- package/dist/operator-ux.js +971 -0
- package/dist/orchestrator/audit-operations.js +182 -0
- package/dist/orchestrator/candidate-operations.js +117 -0
- package/dist/orchestrator/cli-options.js +288 -0
- package/dist/orchestrator/collaboration-operations.js +86 -0
- package/dist/orchestrator/feedback-operations.js +81 -0
- package/dist/orchestrator/host-operations.js +78 -0
- package/dist/orchestrator/lifecycle-operations.js +462 -0
- package/dist/orchestrator/migration-operations.js +44 -0
- package/dist/orchestrator/multi-agent-operations.js +362 -0
- package/dist/orchestrator/report.js +369 -0
- package/dist/orchestrator/topology-operations.js +84 -0
- package/dist/orchestrator.js +874 -0
- package/dist/pipeline-contract.js +92 -0
- package/dist/pipeline-runner.js +285 -0
- package/dist/reclamation.js +882 -0
- package/dist/result-normalize.js +194 -0
- package/dist/run-export.js +64 -0
- package/dist/run-registry.js +1347 -0
- package/dist/run-state-schema.js +67 -0
- package/dist/sandbox-profile.js +471 -0
- package/dist/scheduler.js +266 -0
- package/dist/scheduling.js +184 -0
- package/dist/schema-validate.js +98 -0
- package/dist/state-explosion.js +1213 -0
- package/dist/state-migrations.js +463 -0
- package/dist/state-node.js +301 -0
- package/dist/state.js +308 -0
- package/dist/telemetry-attestation.js +156 -0
- package/dist/telemetry-ledger.js +145 -0
- package/dist/topology.js +527 -0
- package/dist/triggers.js +159 -0
- package/dist/trust-audit.js +475 -0
- package/dist/types/blackboard.js +2 -0
- package/dist/types/boundary.js +29 -0
- package/dist/types/candidate.js +2 -0
- package/dist/types/collaboration.js +2 -0
- package/dist/types/core.js +2 -0
- package/dist/types/drive.js +10 -0
- package/dist/types/error-feedback.js +2 -0
- package/dist/types/evidence-reasoning.js +2 -0
- package/dist/types/execution-backend.js +2 -0
- package/dist/types/multi-agent.js +2 -0
- package/dist/types/observability.js +2 -0
- package/dist/types/pipeline.js +2 -0
- package/dist/types/reclamation.js +8 -0
- package/dist/types/result.js +2 -0
- package/dist/types/run-registry.js +2 -0
- package/dist/types/run.js +2 -0
- package/dist/types/sandbox.js +2 -0
- package/dist/types/schedule.js +2 -0
- package/dist/types/state-node.js +2 -0
- package/dist/types/topology.js +2 -0
- package/dist/types/trust.js +2 -0
- package/dist/types/workbench.js +2 -0
- package/dist/types/worker.js +2 -0
- package/dist/types/workflow-app.js +2 -0
- package/dist/types.js +43 -0
- package/dist/verifier-registry.js +46 -0
- package/dist/verifier.js +78 -0
- package/dist/version.js +8 -0
- package/dist/workbench-host.js +172 -0
- package/dist/workbench.js +190 -0
- package/dist/worker-isolation.js +1028 -0
- package/dist/workflow-api.js +98 -0
- package/dist/workflow-app-framework.js +626 -0
- package/docs/agent-delegation-drive.7.md +190 -0
- package/docs/agent-framework.md +176 -0
- package/docs/candidate-scoring.7.md +106 -0
- package/docs/canonical-workflow-apps.7.md +137 -0
- package/docs/capability-topology-registry.7.md +168 -0
- package/docs/cli-mcp-parity.7.md +373 -0
- package/docs/contract-migration-tooling.7.md +123 -0
- package/docs/control-plane-scheduling.7.md +110 -0
- package/docs/coordinator-blackboard.7.md +183 -0
- package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
- package/docs/dogfood-one-real-repo.7.md +168 -0
- package/docs/durable-state-and-locking.7.md +107 -0
- package/docs/end-to-end-golden-path.7.md +117 -0
- package/docs/error-feedback.7.md +153 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
- package/docs/execution-backends.7.md +300 -0
- package/docs/getting-started.md +99 -0
- package/docs/index.md +41 -0
- package/docs/mcp-app-surface.7.md +235 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
- package/docs/multi-agent-eval-replay-harness.7.md +302 -0
- package/docs/multi-agent-operator-ux.7.md +314 -0
- package/docs/multi-agent-runtime-core.7.md +231 -0
- package/docs/multi-agent-topologies.7.md +103 -0
- package/docs/multi-agent-trust-policy-audit.7.md +154 -0
- package/docs/node-snapshot-diff-replay.7.md +135 -0
- package/docs/observability-cost-accounting.7.md +194 -0
- package/docs/operator-ux.7.md +180 -0
- package/docs/pipeline-runner.7.md +136 -0
- package/docs/project-index.md +261 -0
- package/docs/real-execution-backends.7.md +142 -0
- package/docs/release-and-migration.7.md +280 -0
- package/docs/release-tooling.7.md +159 -0
- package/docs/routines.md +48 -0
- package/docs/run-registry-control-plane.7.md +312 -0
- package/docs/run-retention-reclamation.7.md +191 -0
- package/docs/sandbox-profiles.7.md +137 -0
- package/docs/scheduled-tasks.md +80 -0
- package/docs/security-trust-hardening.7.md +117 -0
- package/docs/state-explosion-management.7.md +264 -0
- package/docs/state-node.7.md +96 -0
- package/docs/team-collaboration.7.md +207 -0
- package/docs/unix-principles.md +192 -0
- package/docs/verifier-gated-commit.7.md +140 -0
- package/docs/web-desktop-workbench.7.md +215 -0
- package/docs/worker-isolation.7.md +167 -0
- package/docs/workflow-app-framework.7.md +274 -0
- package/manifest/README.md +43 -0
- package/manifest/plugin.manifest.json +316 -0
- package/manifest/pricing.policy.json +14 -0
- package/package.json +79 -0
- package/scripts/agents/claude-p-agent.js +104 -0
- package/scripts/agents/claude-p-agent.sh +9 -0
- package/scripts/agents/cw-attest-keygen.js +55 -0
- package/scripts/agents/cw-attest-wrap.js +143 -0
- package/scripts/block-unapproved-tag.sh +39 -0
- package/scripts/bump-version.js +249 -0
- package/scripts/canonical-apps.js +171 -0
- package/scripts/cw.js +4 -0
- package/scripts/dist-drift-check.js +79 -0
- package/scripts/dogfood-architecture-review.js +237 -0
- package/scripts/dogfood-release.js +624 -0
- package/scripts/forward-ref-docs.js +73 -0
- package/scripts/gen-manifests.js +232 -0
- package/scripts/golden-path.js +300 -0
- package/scripts/mcp-server.js +4 -0
- package/scripts/new-feature.js +121 -0
- package/scripts/parity-check.js +213 -0
- package/scripts/release-check.js +118 -0
- package/scripts/release-flow.js +272 -0
- package/scripts/release-gate.sh +85 -0
- package/scripts/sync-project-index.js +387 -0
- package/scripts/validate-run-state-schema.js +126 -0
- package/scripts/verify-container-selfref.js +64 -0
- package/scripts/version-sync-check.js +237 -0
- package/skills/cool-workflow/SKILL.md +162 -0
- package/skills/cool-workflow/references/commands.md +282 -0
- package/tsconfig.json +16 -0
- package/ui/workbench/app.css +76 -0
- package/ui/workbench/app.js +159 -0
- package/ui/workbench/index.html +32 -0
- package/workflows/architecture-review.workflow.js +84 -0
- package/workflows/research-synthesis.workflow.js +47 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const TARGET_VERSION = "0.1.78";
|
|
9
|
+
const PREVIOUS_VERSION = "0.1.31";
|
|
10
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
11
|
+
const repoRoot = path.resolve(pluginRoot, "..", "..");
|
|
12
|
+
const cli = path.join(pluginRoot, "scripts", "cw.js");
|
|
13
|
+
const node = process.execPath;
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
const options = parseArgs(process.argv.slice(2));
|
|
17
|
+
const dryRun = !options.execute;
|
|
18
|
+
enforceReleaseActionGate(options, dryRun);
|
|
19
|
+
|
|
20
|
+
const plan = cwJson(
|
|
21
|
+
[
|
|
22
|
+
"plan",
|
|
23
|
+
"release-cut",
|
|
24
|
+
"--repo",
|
|
25
|
+
repoRoot,
|
|
26
|
+
"--version",
|
|
27
|
+
TARGET_VERSION,
|
|
28
|
+
"--previousVersion",
|
|
29
|
+
PREVIOUS_VERSION,
|
|
30
|
+
"--releaseBranch",
|
|
31
|
+
currentBranch(),
|
|
32
|
+
"--dryRun",
|
|
33
|
+
String(dryRun)
|
|
34
|
+
],
|
|
35
|
+
repoRoot
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const context = {
|
|
39
|
+
options,
|
|
40
|
+
dryRun,
|
|
41
|
+
runId: plan.runId,
|
|
42
|
+
reportPath: plan.reportPath,
|
|
43
|
+
statePath: plan.statePath,
|
|
44
|
+
commandResults: [],
|
|
45
|
+
workerIds: [],
|
|
46
|
+
taskWorkers: {},
|
|
47
|
+
externalActions: []
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const taskId of [
|
|
51
|
+
"preflight:repo-state",
|
|
52
|
+
"audit:versions",
|
|
53
|
+
"notes:update",
|
|
54
|
+
"package:artifacts",
|
|
55
|
+
"verify:package",
|
|
56
|
+
"verdict:release"
|
|
57
|
+
]) {
|
|
58
|
+
runTask(context, taskId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const verdictWorkerId = context.taskWorkers["verdict:release"];
|
|
62
|
+
const allPassed = context.commandResults.every((result) => result.status === 0);
|
|
63
|
+
const candidateId = `dogfood-release-${TARGET_VERSION}`;
|
|
64
|
+
const evidence = compactEvidence(context.commandResults.map((result) => result.locator));
|
|
65
|
+
|
|
66
|
+
const candidate = cwJson(
|
|
67
|
+
["candidate", "register", context.runId, "--worker", verdictWorkerId, "--id", candidateId, "--kind", "release"],
|
|
68
|
+
repoRoot
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const scoreArgs = [
|
|
72
|
+
"candidate",
|
|
73
|
+
"score",
|
|
74
|
+
context.runId,
|
|
75
|
+
candidateId,
|
|
76
|
+
"--criterion",
|
|
77
|
+
`correctness=${allPassed ? 10 : 0}`,
|
|
78
|
+
"--criterion",
|
|
79
|
+
`completeness=${requiredEvidencePresent(context) ? 10 : 0}`,
|
|
80
|
+
"--criterion",
|
|
81
|
+
`releaseSafety=${dryRun && context.externalActions.length === 0 ? 10 : 0}`,
|
|
82
|
+
"--criterion",
|
|
83
|
+
`auditability=${context.workerIds.length >= 6 ? 10 : 0}`,
|
|
84
|
+
"--criterion",
|
|
85
|
+
`reproducibility=${allPassed ? 10 : 4}`,
|
|
86
|
+
"--maxTotal",
|
|
87
|
+
"50",
|
|
88
|
+
"--verdict",
|
|
89
|
+
allPassed ? "pass" : "fail",
|
|
90
|
+
"--notes",
|
|
91
|
+
allPassed
|
|
92
|
+
? "Dogfood release candidate accepted: all real dry-run evidence commands passed."
|
|
93
|
+
: "Dogfood release candidate held: at least one real evidence command failed."
|
|
94
|
+
];
|
|
95
|
+
for (const locator of evidence.slice(0, 20)) scoreArgs.push("--evidence", locator);
|
|
96
|
+
const score = cwJson(scoreArgs, repoRoot);
|
|
97
|
+
|
|
98
|
+
let selection = null;
|
|
99
|
+
let commit = null;
|
|
100
|
+
let releaseVerdict = "hold";
|
|
101
|
+
if (allPassed) {
|
|
102
|
+
selection = cwJson(
|
|
103
|
+
[
|
|
104
|
+
"candidate",
|
|
105
|
+
"select",
|
|
106
|
+
context.runId,
|
|
107
|
+
candidateId,
|
|
108
|
+
"--reason",
|
|
109
|
+
`Dogfood release ${TARGET_VERSION} selected after verifier-backed dry-run evidence.`
|
|
110
|
+
],
|
|
111
|
+
repoRoot
|
|
112
|
+
);
|
|
113
|
+
const commitResult = cwJson(
|
|
114
|
+
[
|
|
115
|
+
"commit",
|
|
116
|
+
context.runId,
|
|
117
|
+
"--selection",
|
|
118
|
+
selection.id,
|
|
119
|
+
"--reason",
|
|
120
|
+
`Dogfood One Real Repo ${TARGET_VERSION} verifier-gated checkpoint`
|
|
121
|
+
],
|
|
122
|
+
repoRoot
|
|
123
|
+
);
|
|
124
|
+
commit = commitResult.commit;
|
|
125
|
+
releaseVerdict = "ready-dry-run";
|
|
126
|
+
} else {
|
|
127
|
+
const checkpoint = cwJson(
|
|
128
|
+
[
|
|
129
|
+
"commit",
|
|
130
|
+
context.runId,
|
|
131
|
+
"--allow-unverified-checkpoint",
|
|
132
|
+
"--reason",
|
|
133
|
+
`Dogfood One Real Repo ${TARGET_VERSION} held; evidence commands failed.`
|
|
134
|
+
],
|
|
135
|
+
repoRoot
|
|
136
|
+
);
|
|
137
|
+
commit = checkpoint.commit;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const auditSummary = cwJson(["audit", "summary", context.runId], repoRoot);
|
|
141
|
+
const provenanceArgs = ["audit", "provenance", context.runId];
|
|
142
|
+
if (commit && commit.id) provenanceArgs.push("--commit", commit.id);
|
|
143
|
+
const provenance = cwJson(provenanceArgs, repoRoot);
|
|
144
|
+
const reportPath = cwText(["report", context.runId], repoRoot).trim();
|
|
145
|
+
|
|
146
|
+
const summary = {
|
|
147
|
+
ok: allPassed,
|
|
148
|
+
mode: options.smoke ? "smoke" : "full",
|
|
149
|
+
dryRun,
|
|
150
|
+
runId: context.runId,
|
|
151
|
+
statePath: context.statePath,
|
|
152
|
+
reportPath,
|
|
153
|
+
auditSummaryPath: auditSummary.summaryPath,
|
|
154
|
+
auditEventLogPath: auditSummary.eventLogPath,
|
|
155
|
+
auditIndexPath: auditSummary.indexPath,
|
|
156
|
+
provenanceEvidenceCount: provenance.evidence.length,
|
|
157
|
+
provenanceEventCount: provenance.events.length,
|
|
158
|
+
workerIds: context.workerIds,
|
|
159
|
+
candidateId: candidate.id,
|
|
160
|
+
scoreId: score.id,
|
|
161
|
+
selectionId: selection ? selection.id : null,
|
|
162
|
+
commitId: commit ? commit.id : null,
|
|
163
|
+
checkpointId: commit && commit.checkpoint ? commit.id : null,
|
|
164
|
+
releaseVerdict,
|
|
165
|
+
commandResults: context.commandResults.map((result) => ({
|
|
166
|
+
id: result.id,
|
|
167
|
+
command: result.command,
|
|
168
|
+
cwd: result.cwd,
|
|
169
|
+
status: result.status,
|
|
170
|
+
logPath: result.logPath,
|
|
171
|
+
locator: result.locator
|
|
172
|
+
})),
|
|
173
|
+
releaseActions: {
|
|
174
|
+
tag: Boolean(options.tag),
|
|
175
|
+
push: Boolean(options.push),
|
|
176
|
+
publish: Boolean(options.publish),
|
|
177
|
+
skipped: dryRun || context.externalActions.length === 0,
|
|
178
|
+
reason: dryRun
|
|
179
|
+
? "dry-run mode never creates git tags, pushes, publishes, or mutates external state"
|
|
180
|
+
: "no release action flag requested"
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const summaryPath = path.join(path.dirname(context.statePath), "dogfood-summary.json");
|
|
184
|
+
writeJson(summaryPath, { ...summary, summaryPath });
|
|
185
|
+
summary.summaryPath = summaryPath;
|
|
186
|
+
|
|
187
|
+
if (options.json) process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
188
|
+
else {
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
[
|
|
191
|
+
`dogfood-release: ${allPassed ? "ok" : "hold"}`,
|
|
192
|
+
`run: ${summary.runId}`,
|
|
193
|
+
`report: ${summary.reportPath}`,
|
|
194
|
+
`audit: ${summary.auditSummaryPath}`,
|
|
195
|
+
`candidate: ${summary.candidateId}`,
|
|
196
|
+
`selection: ${summary.selectionId || "held"}`,
|
|
197
|
+
`commit/checkpoint: ${summary.commitId || summary.checkpointId}`,
|
|
198
|
+
`summary: ${summary.summaryPath}`,
|
|
199
|
+
""
|
|
200
|
+
].join("\n")
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!allPassed) process.exitCode = 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function runTask(context, expectedTaskId) {
|
|
208
|
+
const dispatch = cwJson(["dispatch", context.runId, "--limit", "1"], repoRoot);
|
|
209
|
+
if (!dispatch.tasks || dispatch.tasks.length !== 1) {
|
|
210
|
+
throw new Error(`Expected one dispatched task for ${expectedTaskId}`);
|
|
211
|
+
}
|
|
212
|
+
const task = dispatch.tasks[0];
|
|
213
|
+
if (task.id !== expectedTaskId) throw new Error(`Expected ${expectedTaskId}, got ${task.id}`);
|
|
214
|
+
const workerId = task.workerId;
|
|
215
|
+
context.workerIds.push(workerId);
|
|
216
|
+
context.taskWorkers[task.id] = workerId;
|
|
217
|
+
|
|
218
|
+
const manifest = cwJson(["worker", "manifest", context.runId, workerId], repoRoot);
|
|
219
|
+
const commandResults = executeCommandsForTask(context, task.id, manifest);
|
|
220
|
+
const taskEvidence = evidenceForTask(task.id, commandResults, context);
|
|
221
|
+
const findings = commandResults
|
|
222
|
+
.filter((result) => result.status !== 0)
|
|
223
|
+
.map((result) => ({
|
|
224
|
+
id: `${result.id}:failed`,
|
|
225
|
+
classification: "real",
|
|
226
|
+
severity: "P1",
|
|
227
|
+
evidence: [result.locator]
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
const resultMarkdown = renderWorkerResult({
|
|
231
|
+
task,
|
|
232
|
+
manifest,
|
|
233
|
+
commandResults,
|
|
234
|
+
findings,
|
|
235
|
+
evidence: taskEvidence,
|
|
236
|
+
dryRun: context.dryRun,
|
|
237
|
+
smoke: context.options.smoke
|
|
238
|
+
});
|
|
239
|
+
fs.writeFileSync(manifest.resultPath, resultMarkdown, "utf8");
|
|
240
|
+
cwJson(["worker", "output", context.runId, workerId, manifest.resultPath], repoRoot);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function executeCommandsForTask(context, taskId, manifest) {
|
|
244
|
+
const commands = commandsForTask(taskId, context);
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const command of commands) {
|
|
247
|
+
const result = runEvidenceCommand(command, manifest, context);
|
|
248
|
+
results.push(result);
|
|
249
|
+
context.commandResults.push(result);
|
|
250
|
+
cwJson(
|
|
251
|
+
[
|
|
252
|
+
"audit",
|
|
253
|
+
"attest",
|
|
254
|
+
context.runId,
|
|
255
|
+
"--worker",
|
|
256
|
+
manifest.id,
|
|
257
|
+
"--hostEnforced",
|
|
258
|
+
"true",
|
|
259
|
+
"--command",
|
|
260
|
+
result.command,
|
|
261
|
+
"--note",
|
|
262
|
+
`dogfood ${taskId} command status=${result.status}`
|
|
263
|
+
],
|
|
264
|
+
repoRoot
|
|
265
|
+
);
|
|
266
|
+
cwJson(["audit", "decision", context.runId, manifest.id, "--command", result.command], repoRoot);
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Read a release surface from the COMMIT being released (the blob at HEAD), not
|
|
272
|
+
// the mutable working tree. A release gate that reads the working tree can
|
|
273
|
+
// false-RED when a concurrent writer briefly mutates-then-reverts a surface
|
|
274
|
+
// (the read lands in that window) even though the committed tree is correct and
|
|
275
|
+
// `git status` is clean — and could false-GREEN on an uncommitted local edit.
|
|
276
|
+
// `git show HEAD:<path>` is immutable for the life of the commit, so the gate is
|
|
277
|
+
// deterministic. Falls back to the working tree only when the path is not
|
|
278
|
+
// tracked at HEAD or we are not in a git work tree. Emitted as a `node -e` body
|
|
279
|
+
// so each check stays a separate audited evidence command (cwd: repoRoot).
|
|
280
|
+
// node + git only — no ripgrep (CI portability rule).
|
|
281
|
+
function releaseSourceReaderSnippet() {
|
|
282
|
+
return [
|
|
283
|
+
"const cp=require('child_process');",
|
|
284
|
+
"const fs=require('fs');",
|
|
285
|
+
"function readHead(f){",
|
|
286
|
+
" const r=cp.spawnSync('git',['show','HEAD:'+f],{encoding:'utf8',maxBuffer:1024*1024*32});",
|
|
287
|
+
" if(r.status===0) return r.stdout;",
|
|
288
|
+
" return null;",
|
|
289
|
+
"}",
|
|
290
|
+
"function readSurface(f){",
|
|
291
|
+
" const h=readHead(f);",
|
|
292
|
+
" if(h!==null) return h;",
|
|
293
|
+
" return fs.existsSync(f)?fs.readFileSync(f,'utf8'):null;",
|
|
294
|
+
"}",
|
|
295
|
+
"function surfaceExists(f){",
|
|
296
|
+
" const r=cp.spawnSync('git',['cat-file','-e','HEAD:'+f],{encoding:'utf8'});",
|
|
297
|
+
" if(r.status===0) return true;",
|
|
298
|
+
" return fs.existsSync(f);",
|
|
299
|
+
"}"
|
|
300
|
+
].join("");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandsForTask(taskId, context) {
|
|
304
|
+
const smoke = context.options.smoke;
|
|
305
|
+
const versionCheck = { id: "version-sync", cwd: pluginRoot, command: [node, ["scripts/version-sync-check.js"]] };
|
|
306
|
+
switch (taskId) {
|
|
307
|
+
case "preflight:repo-state":
|
|
308
|
+
return [
|
|
309
|
+
{ id: "git-status", cwd: repoRoot, command: ["git", ["status", "--short", "--branch"]] },
|
|
310
|
+
{ id: "git-head", cwd: repoRoot, command: ["git", ["rev-parse", "HEAD"]] },
|
|
311
|
+
{ id: "node-version", cwd: repoRoot, command: [node, ["--version"]] },
|
|
312
|
+
{ id: "npm-version", cwd: pluginRoot, command: ["npm", ["--version"]] }
|
|
313
|
+
];
|
|
314
|
+
case "audit:versions":
|
|
315
|
+
return [
|
|
316
|
+
versionCheck,
|
|
317
|
+
{
|
|
318
|
+
id: "version-surfaces",
|
|
319
|
+
cwd: repoRoot,
|
|
320
|
+
command: [
|
|
321
|
+
node,
|
|
322
|
+
[
|
|
323
|
+
"-e",
|
|
324
|
+
[
|
|
325
|
+
releaseSourceReaderSnippet(),
|
|
326
|
+
"for (const f of ['plugins/cool-workflow/package.json','plugins/cool-workflow/src/version.ts','CHANGELOG.md','RELEASE.md']) {",
|
|
327
|
+
" const t=readSurface(f);",
|
|
328
|
+
` if (t===null) throw new Error(f+' missing from release commit');`,
|
|
329
|
+
` if (!t.includes('${TARGET_VERSION}')) throw new Error(f+' missing ${TARGET_VERSION}');`,
|
|
330
|
+
"}",
|
|
331
|
+
"console.log('version surfaces include target release (from release commit)');"
|
|
332
|
+
].join("")
|
|
333
|
+
]
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
];
|
|
337
|
+
case "notes:update":
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
id: "release-docs",
|
|
341
|
+
cwd: repoRoot,
|
|
342
|
+
command: [
|
|
343
|
+
node,
|
|
344
|
+
[
|
|
345
|
+
"-e",
|
|
346
|
+
[
|
|
347
|
+
releaseSourceReaderSnippet(),
|
|
348
|
+
"const files=['docs/dogfood-one-real-repo.7.md','plugins/cool-workflow/docs/dogfood-one-real-repo.7.md','README.md','plugins/cool-workflow/README.md','CHANGELOG.md','RELEASE.md'];",
|
|
349
|
+
"for (const f of files) { if (!surfaceExists(f)) throw new Error('missing '+f); }",
|
|
350
|
+
"const changelog=readSurface('CHANGELOG.md');",
|
|
351
|
+
"if (changelog===null) throw new Error('CHANGELOG.md missing from release commit');",
|
|
352
|
+
`if (!changelog.includes('## ${TARGET_VERSION}')) throw new Error('changelog missing target');`,
|
|
353
|
+
"console.log('dogfood release docs present (from release commit)');"
|
|
354
|
+
].join("")
|
|
355
|
+
]
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "docs-index",
|
|
360
|
+
cwd: repoRoot,
|
|
361
|
+
// Portable docs-index check: assert both files reference the dogfood
|
|
362
|
+
// proof without depending on ripgrep, which is not preinstalled on
|
|
363
|
+
// stock CI runners (an external `rg` would ENOENT and hold the verdict).
|
|
364
|
+
command: [
|
|
365
|
+
node,
|
|
366
|
+
[
|
|
367
|
+
"-e",
|
|
368
|
+
[
|
|
369
|
+
releaseSourceReaderSnippet(),
|
|
370
|
+
"const files=['plugins/cool-workflow/docs/index.md','README.md'];",
|
|
371
|
+
"for (const f of files) { const t=readSurface(f); if (t===null) throw new Error(f+' missing from release commit'); if (!t.toLowerCase().includes('dogfood')) throw new Error(f+' missing dogfood reference'); }",
|
|
372
|
+
"console.log('docs index references dogfood (from release commit)');"
|
|
373
|
+
].join("")
|
|
374
|
+
]
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
case "package:artifacts":
|
|
379
|
+
if (smoke) {
|
|
380
|
+
return [
|
|
381
|
+
{ id: "app-validate-release-cut", cwd: pluginRoot, command: [node, ["scripts/cw.js", "app", "validate", "release-cut"]] },
|
|
382
|
+
{ id: "npm-pack-dry-run", cwd: pluginRoot, command: packDryRunCommand() }
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
return [
|
|
386
|
+
{ id: "build", cwd: pluginRoot, command: ["npm", ["run", "build"]] },
|
|
387
|
+
{ id: "npm-pack-dry-run", cwd: pluginRoot, command: packDryRunCommand() }
|
|
388
|
+
];
|
|
389
|
+
case "verify:package":
|
|
390
|
+
if (smoke) {
|
|
391
|
+
return [
|
|
392
|
+
{ id: "canonical-apps", cwd: pluginRoot, command: ["npm", ["run", "canonical-apps"]] },
|
|
393
|
+
{ id: "golden-path", cwd: pluginRoot, command: ["npm", ["run", "golden-path"]] }
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
return [
|
|
397
|
+
{ id: "check", cwd: pluginRoot, command: ["npm", ["run", "check"]] },
|
|
398
|
+
{ id: "test", cwd: pluginRoot, command: ["npm", ["test"]] },
|
|
399
|
+
{ id: "fixture-compat", cwd: pluginRoot, command: ["npm", ["run", "fixture-compat"]] },
|
|
400
|
+
{ id: "canonical-apps", cwd: pluginRoot, command: ["npm", ["run", "canonical-apps"]] },
|
|
401
|
+
{ id: "golden-path", cwd: pluginRoot, command: ["npm", ["run", "golden-path"]] },
|
|
402
|
+
{ id: "release-check", cwd: pluginRoot, command: ["npm", ["run", "release:check"]] }
|
|
403
|
+
];
|
|
404
|
+
case "verdict:release":
|
|
405
|
+
return [
|
|
406
|
+
{ id: "cw-status", cwd: repoRoot, command: [node, [cli, "status", context.runId, "--json"]] },
|
|
407
|
+
{ id: "cw-graph", cwd: repoRoot, command: [node, [cli, "graph", context.runId, "--json"]] },
|
|
408
|
+
{ id: "cw-worker-summary", cwd: repoRoot, command: [node, [cli, "worker", "summary", context.runId, "--json"]] },
|
|
409
|
+
{ id: "cw-audit-summary", cwd: repoRoot, command: [node, [cli, "audit", "summary", context.runId]] },
|
|
410
|
+
{ id: "cw-audit-provenance", cwd: repoRoot, command: [node, [cli, "audit", "provenance", context.runId]] }
|
|
411
|
+
];
|
|
412
|
+
default:
|
|
413
|
+
throw new Error(`No dogfood command set for task ${taskId}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function runEvidenceCommand(spec, manifest, context) {
|
|
418
|
+
const started = new Date().toISOString();
|
|
419
|
+
const [bin, args] = spec.command;
|
|
420
|
+
const commandText = [bin, ...args].join(" ");
|
|
421
|
+
const result = spawnSync(bin, args, {
|
|
422
|
+
cwd: spec.cwd,
|
|
423
|
+
encoding: "utf8",
|
|
424
|
+
env: {
|
|
425
|
+
...process.env,
|
|
426
|
+
CW_DOGFOOD_RELEASE: "1",
|
|
427
|
+
CW_DOGFOOD_MODE: context.options.smoke ? "smoke" : "full"
|
|
428
|
+
},
|
|
429
|
+
maxBuffer: 1024 * 1024 * 20
|
|
430
|
+
});
|
|
431
|
+
const ended = new Date().toISOString();
|
|
432
|
+
const logPath = path.join(manifest.logsDir, `${safeName(spec.id)}.log`);
|
|
433
|
+
fs.mkdirSync(manifest.logsDir, { recursive: true });
|
|
434
|
+
fs.writeFileSync(
|
|
435
|
+
logPath,
|
|
436
|
+
[
|
|
437
|
+
`$ ${commandText}`,
|
|
438
|
+
`cwd: ${spec.cwd}`,
|
|
439
|
+
`started: ${started}`,
|
|
440
|
+
`ended: ${ended}`,
|
|
441
|
+
`status: ${result.status === null ? "signal:" + result.signal : result.status}`,
|
|
442
|
+
"",
|
|
443
|
+
"## stdout",
|
|
444
|
+
result.stdout || "",
|
|
445
|
+
"",
|
|
446
|
+
"## stderr",
|
|
447
|
+
result.stderr || "",
|
|
448
|
+
""
|
|
449
|
+
].join("\n"),
|
|
450
|
+
"utf8"
|
|
451
|
+
);
|
|
452
|
+
return {
|
|
453
|
+
id: spec.id,
|
|
454
|
+
command: commandText,
|
|
455
|
+
cwd: spec.cwd,
|
|
456
|
+
status: result.status === null ? 1 : result.status,
|
|
457
|
+
signal: result.signal,
|
|
458
|
+
stdout: result.stdout || "",
|
|
459
|
+
stderr: result.stderr || "",
|
|
460
|
+
logPath,
|
|
461
|
+
locator: `${logPath}:1`
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function packDryRunCommand() {
|
|
466
|
+
return [
|
|
467
|
+
node,
|
|
468
|
+
[
|
|
469
|
+
"-e",
|
|
470
|
+
[
|
|
471
|
+
"const {spawnSync}=require('child_process');",
|
|
472
|
+
"const r=spawnSync('npm',['pack','--dry-run','--json'],{encoding:'utf8',maxBuffer:1024*1024*20});",
|
|
473
|
+
"process.stdout.write(r.stdout||'');",
|
|
474
|
+
"process.stderr.write(r.stderr||'');",
|
|
475
|
+
"if(r.status!==0) process.exit(r.status);",
|
|
476
|
+
"const pack=JSON.parse(r.stdout)[0];",
|
|
477
|
+
"const files=(pack.files||[]).map(f=>f.path);",
|
|
478
|
+
"const leaked=files.filter(f=>f.startsWith('.cw/')||f.includes('/.cw/'));",
|
|
479
|
+
"if(leaked.length){console.error('npm pack includes .cw files: '+leaked.slice(0,5).join(', '));process.exit(1);}",
|
|
480
|
+
"console.log(JSON.stringify({checked:'npm-pack-dry-run',entryCount:files.length,cwCount:0,filename:pack.filename}));"
|
|
481
|
+
].join("")
|
|
482
|
+
]
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function evidenceForTask(taskId, commandResults, context) {
|
|
487
|
+
const commandEvidence = commandResults.map((result) => result.locator);
|
|
488
|
+
if (taskId !== "verdict:release") return commandEvidence;
|
|
489
|
+
return compactEvidence([
|
|
490
|
+
...context.commandResults.map((result) => result.locator),
|
|
491
|
+
context.statePath,
|
|
492
|
+
context.reportPath
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderWorkerResult({ task, manifest, commandResults, findings, evidence, dryRun, smoke }) {
|
|
497
|
+
const passed = commandResults.every((result) => result.status === 0);
|
|
498
|
+
const summary = passed
|
|
499
|
+
? `${task.id} completed with real dogfood evidence.`
|
|
500
|
+
: `${task.id} held because one or more dogfood evidence commands failed.`;
|
|
501
|
+
return [
|
|
502
|
+
`# Dogfood ${task.id}`,
|
|
503
|
+
"",
|
|
504
|
+
`Worker: ${manifest.id}`,
|
|
505
|
+
`Sandbox: ${manifest.sandboxProfileId}`,
|
|
506
|
+
`Mode: ${smoke ? "smoke" : "full"}`,
|
|
507
|
+
`Dry run: ${dryRun}`,
|
|
508
|
+
"",
|
|
509
|
+
"## Commands",
|
|
510
|
+
...commandResults.map((result) => `- ${result.status === 0 ? "PASS" : "FAIL"} ${result.command} (${result.locator})`),
|
|
511
|
+
"",
|
|
512
|
+
"```cw:result",
|
|
513
|
+
JSON.stringify(
|
|
514
|
+
{
|
|
515
|
+
summary,
|
|
516
|
+
findings,
|
|
517
|
+
evidence
|
|
518
|
+
},
|
|
519
|
+
null,
|
|
520
|
+
2
|
|
521
|
+
),
|
|
522
|
+
"```",
|
|
523
|
+
""
|
|
524
|
+
].join("\n");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function requiredEvidencePresent(context) {
|
|
528
|
+
const required = context.options.smoke
|
|
529
|
+
? ["git-status", "version-sync", "release-docs", "app-validate-release-cut", "canonical-apps", "golden-path"]
|
|
530
|
+
: [
|
|
531
|
+
"git-status",
|
|
532
|
+
"version-sync",
|
|
533
|
+
"release-docs",
|
|
534
|
+
"build",
|
|
535
|
+
"check",
|
|
536
|
+
"test",
|
|
537
|
+
"fixture-compat",
|
|
538
|
+
"canonical-apps",
|
|
539
|
+
"golden-path",
|
|
540
|
+
"release-check",
|
|
541
|
+
"cw-audit-summary",
|
|
542
|
+
"cw-audit-provenance"
|
|
543
|
+
];
|
|
544
|
+
const passed = new Set(context.commandResults.filter((result) => result.status === 0).map((result) => result.id));
|
|
545
|
+
return required.every((id) => passed.has(id));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function enforceReleaseActionGate(options, dryRun) {
|
|
549
|
+
const requestedAction = Boolean(options.tag || options.push || options.publish);
|
|
550
|
+
if (dryRun && requestedAction) {
|
|
551
|
+
throw new Error("--tag, --push, and --publish require --execute");
|
|
552
|
+
}
|
|
553
|
+
if (!dryRun && options.confirmReleaseActions !== TARGET_VERSION) {
|
|
554
|
+
throw new Error(`--execute requires --confirm-release-actions=${TARGET_VERSION}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function currentBranch() {
|
|
559
|
+
const result = spawnSync("git", ["branch", "--show-current"], { cwd: repoRoot, encoding: "utf8" });
|
|
560
|
+
return (result.stdout || "").trim() || "main";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function cwJson(args, cwd) {
|
|
564
|
+
const text = cwText(args, cwd);
|
|
565
|
+
try {
|
|
566
|
+
return JSON.parse(text);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw new Error(`cw JSON parse failed for ${args.join(" ")}\n${text}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function cwText(args, cwd) {
|
|
573
|
+
const result = spawnSync(node, [cli, ...args], {
|
|
574
|
+
cwd,
|
|
575
|
+
encoding: "utf8",
|
|
576
|
+
env: process.env,
|
|
577
|
+
maxBuffer: 1024 * 1024 * 20
|
|
578
|
+
});
|
|
579
|
+
if (result.status !== 0) {
|
|
580
|
+
throw new Error(`cw ${args.join(" ")} exited ${result.status}\n${result.stdout}\n${result.stderr}`);
|
|
581
|
+
}
|
|
582
|
+
return result.stdout;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function parseArgs(argv) {
|
|
586
|
+
const options = {
|
|
587
|
+
smoke: false,
|
|
588
|
+
json: false,
|
|
589
|
+
execute: false,
|
|
590
|
+
tag: false,
|
|
591
|
+
push: false,
|
|
592
|
+
publish: false,
|
|
593
|
+
confirmReleaseActions: ""
|
|
594
|
+
};
|
|
595
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
596
|
+
const token = argv[index];
|
|
597
|
+
if (token === "--smoke") options.smoke = true;
|
|
598
|
+
else if (token === "--json") options.json = true;
|
|
599
|
+
else if (token === "--execute") options.execute = true;
|
|
600
|
+
else if (token === "--tag") options.tag = true;
|
|
601
|
+
else if (token === "--push") options.push = true;
|
|
602
|
+
else if (token === "--publish") options.publish = true;
|
|
603
|
+
else if (token.startsWith("--confirm-release-actions=")) options.confirmReleaseActions = token.split("=")[1] || "";
|
|
604
|
+
else if (token === "--confirm-release-actions") options.confirmReleaseActions = argv[++index] || "";
|
|
605
|
+
else if (token === "--dry-run") options.execute = false;
|
|
606
|
+
else throw new Error(`Unknown dogfood-release option: ${token}`);
|
|
607
|
+
}
|
|
608
|
+
return options;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function compactEvidence(entries) {
|
|
612
|
+
return [...new Set(entries.filter(Boolean).map(String))];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function safeName(value) {
|
|
616
|
+
return String(value).replace(/[^a-zA-Z0-9_.-]+/g, "-");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function writeJson(file, value) {
|
|
620
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
621
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
main();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Append a per-release forward-reference section to every doc that version:sync
|
|
5
|
+
// requires to carry the current version. The repo documents each release by
|
|
6
|
+
// appending a "## <Title> (vX)" section to the sibling .7.md docs; doing that by
|
|
7
|
+
// hand across ~12 docs is the dominant mechanical toil left after bump-version
|
|
8
|
+
// and new-feature. This automates it.
|
|
9
|
+
//
|
|
10
|
+
// node scripts/forward-ref-docs.js "<Title>" "<one-line summary>"
|
|
11
|
+
// npm run forward-ref -- "Release Tooling" "what it does"
|
|
12
|
+
//
|
|
13
|
+
// APPEND-ONLY: it never rewrites existing (historical) version labels — it only
|
|
14
|
+
// adds a new trailing section for the current package.json version. Idempotent:
|
|
15
|
+
// re-running for the same version is a no-op.
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
const pluginRoot = path.resolve(__dirname, "..");
|
|
21
|
+
const docsDir = path.join(pluginRoot, "docs");
|
|
22
|
+
|
|
23
|
+
// SINGLE SOURCE OF TRUTH: derive the doc list from version-sync-check.js — every
|
|
24
|
+
// doc it asserts must carry the current VERSION receives the forward reference. A
|
|
25
|
+
// newly-added feature doc is auto-included the moment its
|
|
26
|
+
// `checkIncludes(..., VERSION)` assertion lands, so this list can never drift out
|
|
27
|
+
// of sync with the gate (the bug that silently dropped docs each release).
|
|
28
|
+
function versionCheckedDocs() {
|
|
29
|
+
const src = fs.readFileSync(path.join(__dirname, "version-sync-check.js"), "utf8");
|
|
30
|
+
return [...new Set([...src.matchAll(/docs\/([a-z0-9-]+\.7\.md)"\s*,\s*VERSION/g)].map((match) => match[1]))];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
const title = process.argv[2];
|
|
35
|
+
const summary = process.argv[3] || `${title}.`;
|
|
36
|
+
if (!title) {
|
|
37
|
+
process.stderr.write('usage: node scripts/forward-ref-docs.js "<Title>" "<summary>"\n');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const version = JSON.parse(fs.readFileSync(path.join(pluginRoot, "package.json"), "utf8")).version;
|
|
41
|
+
const heading = `## ${title} (v${version})`;
|
|
42
|
+
const section = `\n${heading}\n\n${summary}\n`;
|
|
43
|
+
const VERSION_DOCS = versionCheckedDocs();
|
|
44
|
+
|
|
45
|
+
const touched = [];
|
|
46
|
+
for (const rel of VERSION_DOCS) {
|
|
47
|
+
const abs = path.join(docsDir, rel);
|
|
48
|
+
if (!fs.existsSync(abs)) continue;
|
|
49
|
+
const text = fs.readFileSync(abs, "utf8");
|
|
50
|
+
// Skip if the doc already carries the current version — its OWN feature doc
|
|
51
|
+
// (intro "CW vX adds ...") or a prior run of this command. This mirrors
|
|
52
|
+
// version:sync's pass condition, so forward-ref does exactly the work the
|
|
53
|
+
// gate requires: no self-referential section on the new doc, fully idempotent.
|
|
54
|
+
if (text.includes(version)) continue;
|
|
55
|
+
fs.writeFileSync(abs, `${text.replace(/\s*$/, "")}\n${section}`);
|
|
56
|
+
touched.push(`docs/${rel}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// README carries the version via its lead line; append a short forward-ref so
|
|
60
|
+
// version:sync's `v<version>` check passes without rewriting the intro prose.
|
|
61
|
+
const readme = path.join(pluginRoot, "README.md");
|
|
62
|
+
const readmeText = fs.readFileSync(readme, "utf8");
|
|
63
|
+
if (!readmeText.includes(`v${version}`)) {
|
|
64
|
+
fs.writeFileSync(readme, `${readmeText.replace(/\s*$/, "")}\n\n${heading}\n\n${summary}\n`);
|
|
65
|
+
touched.push("README.md");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.stdout.write(`forward-ref "${title}" (v${version}) -> ${touched.length} docs\n`);
|
|
69
|
+
for (const rel of touched) process.stdout.write(` appended ${rel}\n`);
|
|
70
|
+
if (!touched.length) process.stdout.write(" (all docs already carry this section)\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main();
|