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.
Files changed (193) hide show
  1. package/.claude-plugin/plugin.json +20 -0
  2. package/.codex-plugin/mcp.json +10 -0
  3. package/.codex-plugin/plugin.json +38 -0
  4. package/.mcp.json +10 -0
  5. package/LICENSE +24 -0
  6. package/README.md +638 -0
  7. package/apps/architecture-review/app.json +51 -0
  8. package/apps/architecture-review/workflow.js +116 -0
  9. package/apps/end-to-end-golden-path/app.json +30 -0
  10. package/apps/end-to-end-golden-path/workflow.js +33 -0
  11. package/apps/pr-review-fix-ci/app.json +59 -0
  12. package/apps/pr-review-fix-ci/workflow.js +90 -0
  13. package/apps/release-cut/app.json +54 -0
  14. package/apps/release-cut/workflow.js +82 -0
  15. package/apps/research-synthesis/app.json +50 -0
  16. package/apps/research-synthesis/workflow.js +76 -0
  17. package/apps/workflow-app-framework-demo/app.json +29 -0
  18. package/apps/workflow-app-framework-demo/workflow.js +44 -0
  19. package/dist/agent-config.js +223 -0
  20. package/dist/candidate-scoring.js +715 -0
  21. package/dist/capability-core.js +630 -0
  22. package/dist/capability-dispatcher.js +86 -0
  23. package/dist/capability-registry.js +523 -0
  24. package/dist/cli.js +1276 -0
  25. package/dist/collaboration.js +727 -0
  26. package/dist/commit.js +570 -0
  27. package/dist/contract-migration.js +234 -0
  28. package/dist/coordinator.js +1163 -0
  29. package/dist/daemon.js +44 -0
  30. package/dist/dispatch.js +201 -0
  31. package/dist/drive.js +503 -0
  32. package/dist/error-feedback.js +415 -0
  33. package/dist/evidence-grounding.js +179 -0
  34. package/dist/evidence-reasoning.js +733 -0
  35. package/dist/execution-backend.js +1279 -0
  36. package/dist/harness.js +61 -0
  37. package/dist/mcp-server.js +1615 -0
  38. package/dist/multi-agent-eval.js +857 -0
  39. package/dist/multi-agent-host.js +764 -0
  40. package/dist/multi-agent-operator-ux.js +537 -0
  41. package/dist/multi-agent-trust.js +366 -0
  42. package/dist/multi-agent.js +1173 -0
  43. package/dist/node-snapshot.js +270 -0
  44. package/dist/observability.js +922 -0
  45. package/dist/operator-ux.js +971 -0
  46. package/dist/orchestrator/audit-operations.js +182 -0
  47. package/dist/orchestrator/candidate-operations.js +117 -0
  48. package/dist/orchestrator/cli-options.js +288 -0
  49. package/dist/orchestrator/collaboration-operations.js +86 -0
  50. package/dist/orchestrator/feedback-operations.js +81 -0
  51. package/dist/orchestrator/host-operations.js +78 -0
  52. package/dist/orchestrator/lifecycle-operations.js +462 -0
  53. package/dist/orchestrator/migration-operations.js +44 -0
  54. package/dist/orchestrator/multi-agent-operations.js +362 -0
  55. package/dist/orchestrator/report.js +369 -0
  56. package/dist/orchestrator/topology-operations.js +84 -0
  57. package/dist/orchestrator.js +874 -0
  58. package/dist/pipeline-contract.js +92 -0
  59. package/dist/pipeline-runner.js +285 -0
  60. package/dist/reclamation.js +882 -0
  61. package/dist/result-normalize.js +194 -0
  62. package/dist/run-export.js +64 -0
  63. package/dist/run-registry.js +1347 -0
  64. package/dist/run-state-schema.js +67 -0
  65. package/dist/sandbox-profile.js +471 -0
  66. package/dist/scheduler.js +266 -0
  67. package/dist/scheduling.js +184 -0
  68. package/dist/schema-validate.js +98 -0
  69. package/dist/state-explosion.js +1213 -0
  70. package/dist/state-migrations.js +463 -0
  71. package/dist/state-node.js +301 -0
  72. package/dist/state.js +308 -0
  73. package/dist/telemetry-attestation.js +156 -0
  74. package/dist/telemetry-ledger.js +145 -0
  75. package/dist/topology.js +527 -0
  76. package/dist/triggers.js +159 -0
  77. package/dist/trust-audit.js +475 -0
  78. package/dist/types/blackboard.js +2 -0
  79. package/dist/types/boundary.js +29 -0
  80. package/dist/types/candidate.js +2 -0
  81. package/dist/types/collaboration.js +2 -0
  82. package/dist/types/core.js +2 -0
  83. package/dist/types/drive.js +10 -0
  84. package/dist/types/error-feedback.js +2 -0
  85. package/dist/types/evidence-reasoning.js +2 -0
  86. package/dist/types/execution-backend.js +2 -0
  87. package/dist/types/multi-agent.js +2 -0
  88. package/dist/types/observability.js +2 -0
  89. package/dist/types/pipeline.js +2 -0
  90. package/dist/types/reclamation.js +8 -0
  91. package/dist/types/result.js +2 -0
  92. package/dist/types/run-registry.js +2 -0
  93. package/dist/types/run.js +2 -0
  94. package/dist/types/sandbox.js +2 -0
  95. package/dist/types/schedule.js +2 -0
  96. package/dist/types/state-node.js +2 -0
  97. package/dist/types/topology.js +2 -0
  98. package/dist/types/trust.js +2 -0
  99. package/dist/types/workbench.js +2 -0
  100. package/dist/types/worker.js +2 -0
  101. package/dist/types/workflow-app.js +2 -0
  102. package/dist/types.js +43 -0
  103. package/dist/verifier-registry.js +46 -0
  104. package/dist/verifier.js +78 -0
  105. package/dist/version.js +8 -0
  106. package/dist/workbench-host.js +172 -0
  107. package/dist/workbench.js +190 -0
  108. package/dist/worker-isolation.js +1028 -0
  109. package/dist/workflow-api.js +98 -0
  110. package/dist/workflow-app-framework.js +626 -0
  111. package/docs/agent-delegation-drive.7.md +190 -0
  112. package/docs/agent-framework.md +176 -0
  113. package/docs/candidate-scoring.7.md +106 -0
  114. package/docs/canonical-workflow-apps.7.md +137 -0
  115. package/docs/capability-topology-registry.7.md +168 -0
  116. package/docs/cli-mcp-parity.7.md +373 -0
  117. package/docs/contract-migration-tooling.7.md +123 -0
  118. package/docs/control-plane-scheduling.7.md +110 -0
  119. package/docs/coordinator-blackboard.7.md +183 -0
  120. package/docs/dogfood/architecture-review-cool-workflow.md +16 -0
  121. package/docs/dogfood-one-real-repo.7.md +168 -0
  122. package/docs/durable-state-and-locking.7.md +107 -0
  123. package/docs/end-to-end-golden-path.7.md +117 -0
  124. package/docs/error-feedback.7.md +153 -0
  125. package/docs/evidence-adoption-reasoning-chain.7.md +270 -0
  126. package/docs/execution-backends.7.md +300 -0
  127. package/docs/getting-started.md +99 -0
  128. package/docs/index.md +41 -0
  129. package/docs/mcp-app-surface.7.md +235 -0
  130. package/docs/multi-agent-cli-mcp-surface.7.md +265 -0
  131. package/docs/multi-agent-eval-replay-harness.7.md +302 -0
  132. package/docs/multi-agent-operator-ux.7.md +314 -0
  133. package/docs/multi-agent-runtime-core.7.md +231 -0
  134. package/docs/multi-agent-topologies.7.md +103 -0
  135. package/docs/multi-agent-trust-policy-audit.7.md +154 -0
  136. package/docs/node-snapshot-diff-replay.7.md +135 -0
  137. package/docs/observability-cost-accounting.7.md +194 -0
  138. package/docs/operator-ux.7.md +180 -0
  139. package/docs/pipeline-runner.7.md +136 -0
  140. package/docs/project-index.md +261 -0
  141. package/docs/real-execution-backends.7.md +142 -0
  142. package/docs/release-and-migration.7.md +280 -0
  143. package/docs/release-tooling.7.md +159 -0
  144. package/docs/routines.md +48 -0
  145. package/docs/run-registry-control-plane.7.md +312 -0
  146. package/docs/run-retention-reclamation.7.md +191 -0
  147. package/docs/sandbox-profiles.7.md +137 -0
  148. package/docs/scheduled-tasks.md +80 -0
  149. package/docs/security-trust-hardening.7.md +117 -0
  150. package/docs/state-explosion-management.7.md +264 -0
  151. package/docs/state-node.7.md +96 -0
  152. package/docs/team-collaboration.7.md +207 -0
  153. package/docs/unix-principles.md +192 -0
  154. package/docs/verifier-gated-commit.7.md +140 -0
  155. package/docs/web-desktop-workbench.7.md +215 -0
  156. package/docs/worker-isolation.7.md +167 -0
  157. package/docs/workflow-app-framework.7.md +274 -0
  158. package/manifest/README.md +43 -0
  159. package/manifest/plugin.manifest.json +316 -0
  160. package/manifest/pricing.policy.json +14 -0
  161. package/package.json +79 -0
  162. package/scripts/agents/claude-p-agent.js +104 -0
  163. package/scripts/agents/claude-p-agent.sh +9 -0
  164. package/scripts/agents/cw-attest-keygen.js +55 -0
  165. package/scripts/agents/cw-attest-wrap.js +143 -0
  166. package/scripts/block-unapproved-tag.sh +39 -0
  167. package/scripts/bump-version.js +249 -0
  168. package/scripts/canonical-apps.js +171 -0
  169. package/scripts/cw.js +4 -0
  170. package/scripts/dist-drift-check.js +79 -0
  171. package/scripts/dogfood-architecture-review.js +237 -0
  172. package/scripts/dogfood-release.js +624 -0
  173. package/scripts/forward-ref-docs.js +73 -0
  174. package/scripts/gen-manifests.js +232 -0
  175. package/scripts/golden-path.js +300 -0
  176. package/scripts/mcp-server.js +4 -0
  177. package/scripts/new-feature.js +121 -0
  178. package/scripts/parity-check.js +213 -0
  179. package/scripts/release-check.js +118 -0
  180. package/scripts/release-flow.js +272 -0
  181. package/scripts/release-gate.sh +85 -0
  182. package/scripts/sync-project-index.js +387 -0
  183. package/scripts/validate-run-state-schema.js +126 -0
  184. package/scripts/verify-container-selfref.js +64 -0
  185. package/scripts/version-sync-check.js +237 -0
  186. package/skills/cool-workflow/SKILL.md +162 -0
  187. package/skills/cool-workflow/references/commands.md +282 -0
  188. package/tsconfig.json +16 -0
  189. package/ui/workbench/app.css +76 -0
  190. package/ui/workbench/app.js +159 -0
  191. package/ui/workbench/index.html +32 -0
  192. package/workflows/architecture-review.workflow.js +84 -0
  193. package/workflows/research-synthesis.workflow.js +47 -0
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # release-gate.sh — deterministic release checks for cool-workflow.
3
+ # Pass = writes .cw-release/gate-<HEAD-sha>.ok
4
+ # This script encodes everything that does NOT need LLM judgment.
5
+ set -euo pipefail
6
+
7
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
8
+ cd "$REPO_ROOT"
9
+ SHA="$(git rev-parse HEAD)"
10
+ # Resolve the PREVIOUS release tag. When this script runs from CI on a tag push
11
+ # (.github/workflows/release-gate.yml), HEAD already carries the tag being
12
+ # released, so a plain `git describe` returns *that* tag and the diff range
13
+ # collapses to empty — making substance/evidence/cadence false-fail every real
14
+ # release. Exclude any tag that points at HEAD so we always compare against the
15
+ # prior release (the parent commit's nearest tag).
16
+ HEAD_TAGS="$(git tag --points-at HEAD 2>/dev/null || echo "")"
17
+ PREV_TAG="$(git describe --tags --abbrev=0 2>/dev/null || echo "")"
18
+ if [[ -n "$HEAD_TAGS" ]] && printf '%s\n' "$HEAD_TAGS" | grep -qxF "$PREV_TAG"; then
19
+ PREV_TAG="$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")"
20
+ fi
21
+ MARKER_DIR="$REPO_ROOT/.cw-release"
22
+ mkdir -p "$MARKER_DIR"
23
+ FAIL=0
24
+
25
+ say() { printf '%s\n' "$*"; }
26
+ fail() { say "GATE FAIL: $*"; FAIL=1; }
27
+
28
+ # --- 1. Build & tests (run, don't trust pasted output) -----------------
29
+ say "[1/6] build"
30
+ npm run --prefix plugins/cool-workflow build >/dev/null 2>&1 || fail "build failed"
31
+
32
+ say "[2/6] tests"
33
+ npm test --prefix plugins/cool-workflow >/dev/null 2>&1 || fail "tests failed"
34
+
35
+ if [[ -n "$PREV_TAG" ]]; then
36
+ RANGE="$PREV_TAG..HEAD"
37
+
38
+ # --- 2. Substance: changes must exist outside src/types/ and dist/ ---
39
+ # The spec (AGENTS.md / reviewer-agent.md Gate 1) is "at least one changed
40
+ # file outside src/types/ and dist/" — ANY such file (src, scripts, docs,
41
+ # workflows, tests). Count every changed path that is not under those two
42
+ # generated/declaration-only trees; declared-but-unread spec accretion is the
43
+ # reviewer agent's deeper judgment call, not this deterministic floor.
44
+ say "[3/6] substance (diff outside src/types/ and dist/)"
45
+ SUBSTANCE=$(git diff --name-only "$RANGE" \
46
+ | grep -cvE '^plugins/cool-workflow/(src/types/|dist/)' || true)
47
+ [[ "$SUBSTANCE" -gt 0 ]] || fail "only types/dist changed since $PREV_TAG (spec accretion)"
48
+
49
+ # --- 3. Test evidence: test files must have changed ------------------
50
+ say "[4/6] test evidence"
51
+ TESTS_CHANGED=$(git diff --name-only "$RANGE" | grep -cE '\.(test|spec)\.|/tests?/' || true)
52
+ [[ "$TESTS_CHANGED" -gt 0 ]] || fail "zero test changes since $PREV_TAG"
53
+
54
+ # --- 4. Cadence: >=4 cycles logged OR >=24h since previous tag -------
55
+ say "[5/6] cadence"
56
+ CYCLES=0
57
+ if [[ -f ITERATION_LOG.md && -n "$PREV_TAG" ]]; then
58
+ CYCLES=$(git diff "$RANGE" -- ITERATION_LOG.md | grep -c '^+.*|' || true)
59
+ fi
60
+ PREV_TS=$(git log -1 --format=%ct "$PREV_TAG")
61
+ NOW_TS=$(date +%s)
62
+ HOURS=$(( (NOW_TS - PREV_TS) / 3600 ))
63
+ if [[ "$CYCLES" -lt 4 && "$HOURS" -lt 24 ]]; then
64
+ fail "cadence: only $CYCLES cycles logged and ${HOURS}h since $PREV_TAG (need >=4 cycles or >=24h)"
65
+ fi
66
+ else
67
+ say "[3-5/6] no previous tag; substance/evidence/cadence checks skipped"
68
+ fi
69
+
70
+ # --- 5. Branch naming: forbid version-number branches -------------------
71
+ say "[6/6] branch naming"
72
+ BRANCH="$(git rev-parse --abbrev-ref HEAD)"
73
+ if [[ "$BRANCH" =~ ^feat/(batch-)?v?[0-9]+ ]]; then
74
+ fail "branch '$BRANCH' is version-number-driven; name the capability instead"
75
+ fi
76
+
77
+ # --- Verdict ------------------------------------------------------------
78
+ if [[ "$FAIL" -ne 0 ]]; then
79
+ rm -f "$MARKER_DIR/gate-$SHA.ok"
80
+ say "RELEASE GATE: REJECTED ($SHA)"
81
+ exit 1
82
+ fi
83
+
84
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$MARKER_DIR/gate-$SHA.ok"
85
+ say "RELEASE GATE: PASSED ($SHA) — next step: release-reviewer agent must record APPROVED"
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ const pluginRoot = path.resolve(__dirname, "..");
9
+ const repoRoot = path.resolve(pluginRoot, "..", "..");
10
+ const packageJson = readJson(path.join(pluginRoot, "package.json"));
11
+ const repoRemote = git(["config", "--get", "remote.origin.url"]).trim();
12
+ const repoUrl = normalizeGitRemote(repoRemote) || "https://github.com/coo1white/cool-workflow";
13
+ const wikiDir = process.env.CW_GITHUB_WIKI_DIR || path.resolve(repoRoot, "..", "cool-workflow.wiki");
14
+ const obsidianVault = process.env.CW_OBSIDIAN_VAULT || detectObsidianVault();
15
+ const obsidianDir = obsidianVault ? path.join(obsidianVault, "Cool Workflow") : "";
16
+ const generatedDate = new Date().toISOString().slice(0, 10);
17
+
18
+ // --check : verify the committed docs/project-index.md still matches a fresh
19
+ // scan of the source tree, write nothing, exit non-zero on drift.
20
+ // --repo-only : write ONLY the committed repo doc; skip the optional personal
21
+ // sync targets (Obsidian vault, GitHub wiki working tree).
22
+ // CW_PROJECT_INDEX_PATH : override the doc path that --check compares against
23
+ // (used by the smoke to point at throwaway fixtures).
24
+ const CHECK = process.argv.includes("--check");
25
+ const REPO_ONLY = process.argv.includes("--repo-only");
26
+ const indexPathOverride = process.env.CW_PROJECT_INDEX_PATH || "";
27
+
28
+ const moduleCatalog = {
29
+ "Core runtime": [
30
+ ["orchestrator.ts", "Plans runs, loads workflows, records results, writes reports, and exposes runner commands."],
31
+ ["state.ts", "Persists run checkpoints, JSON state, run paths, and state migration entrypoints."],
32
+ ["state-node.ts", "Defines explicit state nodes, pipeline transitions, evidence checks, and node persistence."],
33
+ ["pipeline-contract.ts", "Builds the default pipeline contract used by run state."],
34
+ ["pipeline-runner.ts", "Finds runnable stages and advances/fails pipeline nodes with retry-aware errors."],
35
+ ["types.ts", "Owns the shared workflow, run, app, evidence, worker, candidate, audit, and topology types."]
36
+ ],
37
+ "Verification and state gates": [
38
+ ["verifier.ts", "Validates result envelopes, findings, evidence, and run gate completion."],
39
+ ["commit.ts", "Creates verifier-gated commits and explicit manual checkpoints."],
40
+ ["candidate-scoring.ts", "Registers, scores, ranks, selects, rejects, and summarizes candidate outputs."],
41
+ ["error-feedback.ts", "Turns failures into persisted feedback records and correction tasks."],
42
+ ["trust-audit.ts", "Records provenance, sandbox decisions, host attestations, and acceptance rationale."]
43
+ ],
44
+ "Workers and policy": [
45
+ ["dispatch.ts", "Selects runnable tasks and writes dispatch manifests."],
46
+ ["worker-isolation.ts", "Allocates worker scopes, writes manifests, records worker outputs, and validates boundaries."],
47
+ ["sandbox-profile.ts", "Resolves named sandbox policy contracts and validates read/write/command/network boundaries."],
48
+ ["harness.ts", "Renders task files for dispatched work."]
49
+ ],
50
+ "Multi-agent layer": [
51
+ ["multi-agent.ts", "Persists multi-agent runs, roles, groups, memberships, fanouts, and fanins."],
52
+ ["coordinator.ts", "Owns blackboard topics, messages, context, artifacts, snapshots, and coordinator decisions."],
53
+ ["topology.ts", "Defines and applies official map-reduce, debate, and judge-panel topologies."],
54
+ ["multi-agent-host.ts", "Provides the preferred host loop for run, status, step, blackboard, score, and select."]
55
+ ],
56
+ "User and host surfaces": [
57
+ ["cli.ts", "Routes human CLI commands to runtime, app, topology, multi-agent, and operator flows."],
58
+ ["mcp-server.ts", "Exposes JSON-RPC/MCP tool parity for agent hosts."],
59
+ ["operator-ux.ts", "Formats status, reports, graph, worker, candidate, feedback, commit, and trust summaries."],
60
+ ["workflow-app-framework.ts", "Validates app manifests and loads app entrypoints."],
61
+ ["workflow-api.ts", "Provides the fluent workflow, phase, task, artifact, and input API."],
62
+ ["daemon.ts", "Runs scheduled tasks through the desktop scheduler daemon."],
63
+ ["scheduler.ts", "Creates, stores, computes, and runs schedules."],
64
+ ["triggers.ts", "Bridges routine triggers to explicit workflow events."],
65
+ ["version.ts", "Defines current package and state schema versions."]
66
+ ]
67
+ };
68
+
69
+ function main() {
70
+ const apps = listApps();
71
+ const docs = listMarkdown(path.join(pluginRoot, "docs"));
72
+ const sourceFiles = listFiles(path.join(pluginRoot, "src"), ".ts");
73
+ const smokeTests = listFiles(path.join(pluginRoot, "test"), ".js").filter((file) => file.endsWith("-smoke.js"));
74
+ const context = { apps, docs, sourceFiles, smokeTests };
75
+
76
+ // Fail closed if the hand-maintained moduleCatalog references a src module that
77
+ // no longer exists. Without this the generator emits a dead `../src/<file>` link
78
+ // and --check would bless it: the catalog region renders identically from the
79
+ // (stale) constant on both sides, so it is invisible to the diff. This closes
80
+ // the STRUCTURAL half of the catalog blind spot. Responsibility-PROSE staleness
81
+ // (a cataloged file whose contents changed) is not auto-derivable from source
82
+ // and remains out of scope by design — see the PR notes.
83
+ const catalogedFiles = Object.values(moduleCatalog).flat().map(([file]) => file);
84
+ const missingCataloged = catalogedFiles.filter((file) => !sourceFiles.includes(file));
85
+ if (missingCataloged.length > 0) {
86
+ process.stderr.write(
87
+ `project-index FAILED: moduleCatalog references src module(s) that no longer exist: ${missingCataloged.join(", ")}.\n` +
88
+ `A rename/delete must be reflected in moduleCatalog (scripts/sync-project-index.js).\n`
89
+ );
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+
94
+ const repoDoc = indexPathOverride
95
+ ? path.resolve(indexPathOverride)
96
+ : path.join(pluginRoot, "docs", "project-index.md");
97
+ const rendered = renderIndex("repo", context);
98
+
99
+ // Gate mode: compare the committed doc against a fresh render and exit
100
+ // non-zero on drift. Writes nothing and never touches the personal targets.
101
+ // Deliberately reads the WORKING TREE (not git HEAD like version-sync-check.js):
102
+ // this gate is meant to catch "you changed source but forgot to regenerate the
103
+ // index" BEFORE you commit. CI checks out a clean HEAD, so working tree == HEAD
104
+ // there; there is no concurrent-mutation race here (unlike the release-cut flow).
105
+ if (CHECK) {
106
+ checkInSync(repoDoc, rendered);
107
+ return;
108
+ }
109
+
110
+ const outputs = [];
111
+ writeFile(repoDoc, rendered);
112
+ outputs.push(repoDoc);
113
+
114
+ if (!REPO_ONLY && obsidianDir) {
115
+ fs.mkdirSync(obsidianDir, { recursive: true });
116
+ const obsidianDoc = path.join(obsidianDir, "CW Project Index.md");
117
+ writeFile(obsidianDoc, renderIndex("obsidian", context));
118
+ ensureLine(
119
+ path.join(obsidianDir, "Cool Workflow - MOC.md"),
120
+ "- [[CW Project Index]]",
121
+ "## 核心笔记"
122
+ );
123
+ outputs.push(obsidianDoc);
124
+ }
125
+
126
+ if (!REPO_ONLY && fs.existsSync(wikiDir)) {
127
+ const wikiDoc = path.join(wikiDir, "Project-Index.md");
128
+ writeFile(wikiDoc, renderIndex("wiki", context));
129
+ ensureLine(path.join(wikiDir, "_Sidebar.md"), "- [[Project Index]]", "# Cool Workflow");
130
+ outputs.push(wikiDoc);
131
+ }
132
+
133
+ process.stdout.write(`${JSON.stringify({
134
+ ok: true,
135
+ generatedDate,
136
+ package: packageJson.name,
137
+ version: packageJson.version,
138
+ sourceModules: sourceFiles.length,
139
+ workflowApps: apps.length,
140
+ docs: docs.length,
141
+ smokeTests: smokeTests.length,
142
+ outputs: outputs.map((file) => path.relative(repoRoot, file))
143
+ }, null, 2)}\n`);
144
+ }
145
+
146
+ function listApps() {
147
+ const appsDir = path.join(pluginRoot, "apps");
148
+ return fs.readdirSync(appsDir, { withFileTypes: true })
149
+ .filter((entry) => entry.isDirectory())
150
+ .map((entry) => {
151
+ const appDir = path.join(appsDir, entry.name);
152
+ const manifest = readJson(path.join(appDir, "app.json"));
153
+ return {
154
+ id: manifest.id,
155
+ title: manifest.title,
156
+ summary: manifest.summary,
157
+ version: manifest.version,
158
+ inputs: (manifest.inputs || []).map((input) => input.name),
159
+ sandboxProfiles: manifest.sandboxProfiles || [],
160
+ canonical: Boolean(manifest.metadata && manifest.metadata.canonical),
161
+ example: Boolean(manifest.metadata && manifest.metadata.example),
162
+ manifestPath: path.relative(pluginRoot, path.join(appDir, "app.json")),
163
+ workflowPath: path.relative(pluginRoot, path.join(appDir, manifest.workflow && manifest.workflow.entrypoint || "workflow.js"))
164
+ };
165
+ })
166
+ .sort((a, b) => a.id.localeCompare(b.id));
167
+ }
168
+
169
+ function listMarkdown(dir) {
170
+ return fs.readdirSync(dir)
171
+ .filter((file) => file.endsWith(".md"))
172
+ .sort()
173
+ .map((file) => {
174
+ const absolute = path.join(dir, file);
175
+ const text = fs.readFileSync(absolute, "utf8");
176
+ const title = (text.match(/^#\s+(.+)$/m) || [null, file])[1];
177
+ return { file, title };
178
+ });
179
+ }
180
+
181
+ function listFiles(dir, extension) {
182
+ return fs.readdirSync(dir)
183
+ .filter((file) => file.endsWith(extension))
184
+ .sort();
185
+ }
186
+
187
+ function renderIndex(target, context) {
188
+ const link = linkFactory(target);
189
+ const lines = [];
190
+ lines.push("# Cool Workflow Project Index");
191
+ lines.push("");
192
+ lines.push(`Generated from the current repository code on ${generatedDate} by \`npm run sync:project-index\`.`);
193
+ lines.push("");
194
+ lines.push("## Snapshot");
195
+ lines.push("");
196
+ lines.push(`- Package: \`${packageJson.name}\``);
197
+ lines.push(`- Version: \`${packageJson.version}\``);
198
+ lines.push(`- Source modules: \`${context.sourceFiles.length}\``);
199
+ lines.push(`- Workflow apps: \`${context.apps.length}\``);
200
+ lines.push(`- Docs: \`${context.docs.length}\``);
201
+ lines.push(`- Smoke tests: \`${context.smokeTests.length}\``);
202
+ lines.push(`- Repository: ${repoUrl}`);
203
+ lines.push("");
204
+ lines.push("## Architecture");
205
+ lines.push("");
206
+ lines.push("```text");
207
+ lines.push("workflow app -> runner -> dispatch -> isolated workers");
208
+ lines.push(" -> results -> feedback/candidates -> verifier gate");
209
+ lines.push(" -> commit/checkpoint -> report/trust audit");
210
+ lines.push("");
211
+ lines.push("multi-agent host -> topology -> blackboard/coordinator");
212
+ lines.push(" -> fanout/fanin -> candidate score/select");
213
+ lines.push("```");
214
+ lines.push("");
215
+ lines.push("## Source Map");
216
+ lines.push("");
217
+ for (const [area, modules] of Object.entries(moduleCatalog)) {
218
+ lines.push(`### ${area}`);
219
+ lines.push("");
220
+ lines.push("| Module | Responsibility |");
221
+ lines.push("| --- | --- |");
222
+ for (const [file, responsibility] of modules) {
223
+ lines.push(`| ${link(`src/${file}`, file)} | ${responsibility} |`);
224
+ }
225
+ lines.push("");
226
+ }
227
+ const cataloged = new Set(Object.values(moduleCatalog).flat().map(([file]) => file));
228
+ const uncataloged = context.sourceFiles.filter((file) => !cataloged.has(file));
229
+ if (uncataloged.length > 0) {
230
+ lines.push("### Other Source Modules");
231
+ lines.push("");
232
+ for (const file of uncataloged) lines.push(`- ${link(`src/${file}`, file)}`);
233
+ lines.push("");
234
+ }
235
+ lines.push("## Workflow Apps");
236
+ lines.push("");
237
+ lines.push("| App | Type | Inputs | Sandbox | Source |");
238
+ lines.push("| --- | --- | --- | --- | --- |");
239
+ for (const app of context.apps) {
240
+ const type = app.canonical ? "canonical" : app.example ? "example" : "userland";
241
+ lines.push(`| \`${app.id}\` - ${app.summary} | ${type} | ${inlineList(app.inputs)} | ${inlineList(app.sandboxProfiles)} | ${link(app.manifestPath, "manifest")} / ${link(app.workflowPath, "workflow")} |`);
242
+ }
243
+ lines.push("");
244
+ lines.push("## Documentation Map");
245
+ lines.push("");
246
+ for (const doc of context.docs) {
247
+ lines.push(`- ${link(`docs/${doc.file}`, doc.title)}`);
248
+ }
249
+ lines.push("");
250
+ lines.push("## Test Surface");
251
+ lines.push("");
252
+ lines.push("Smoke tests mirror the public contracts. The high-signal suites are:");
253
+ lines.push("");
254
+ for (const file of context.smokeTests) lines.push(`- ${link(`test/${file}`, file)}`);
255
+ lines.push("");
256
+ lines.push("## Sync Targets");
257
+ lines.push("");
258
+ // Keep absolute / personal local paths OUT of the committed doc: describe the
259
+ // optional sync targets generically. The actual destinations are resolved at
260
+ // run time from CW_OBSIDIAN_VAULT / CW_GITHUB_WIKI_DIR (see top of this file).
261
+ lines.push(`- Repository docs: ${link("docs/project-index.md", "docs/project-index.md")}`);
262
+ lines.push("- Obsidian vault (optional): set `CW_OBSIDIAN_VAULT` to your local vault path.");
263
+ lines.push("- GitHub Wiki: the `cool-workflow.wiki` working tree (override with `CW_GITHUB_WIKI_DIR`).");
264
+ lines.push("");
265
+ lines.push("## Maintenance");
266
+ lines.push("");
267
+ lines.push("Run this after changing source modules, workflow app manifests, public docs, or smoke test coverage:");
268
+ lines.push("");
269
+ lines.push("```bash");
270
+ lines.push("cd plugins/cool-workflow");
271
+ lines.push("npm run sync:project-index");
272
+ lines.push("```");
273
+ lines.push("");
274
+ lines.push("Then review the Obsidian page and GitHub Wiki working tree before publishing wiki changes.");
275
+ lines.push("");
276
+ return lines.join("\n");
277
+ }
278
+
279
+ function linkFactory(target) {
280
+ if (target === "repo") {
281
+ return (relativePath, label) => `[${label}](${relativeLinkFromDocs(relativePath)})`;
282
+ }
283
+ if (target === "wiki") {
284
+ return (relativePath, label) => `[${label}](${repoUrl}/blob/main/plugins/cool-workflow/${relativePath})`;
285
+ }
286
+ return (relativePath, label) => `[${label}](${path.join(pluginRoot, relativePath)})`;
287
+ }
288
+
289
+ function relativeLinkFromDocs(relativePath) {
290
+ if (relativePath.startsWith("docs/")) return relativePath.slice("docs/".length);
291
+ return `../${relativePath}`;
292
+ }
293
+
294
+ function inlineList(values) {
295
+ if (!values || values.length === 0) return "-";
296
+ return values.map((value) => `\`${value}\``).join(", ");
297
+ }
298
+
299
+ function ensureLine(file, line, afterHeading) {
300
+ if (!fs.existsSync(file)) return;
301
+ const text = fs.readFileSync(file, "utf8");
302
+ if (text.includes(line)) return;
303
+ const lines = text.split(/\r?\n/);
304
+ const index = lines.findIndex((entry) => entry.trim() === afterHeading);
305
+ if (index === -1) {
306
+ lines.push("", line);
307
+ } else {
308
+ lines.splice(index + 1, 0, line);
309
+ }
310
+ writeFile(file, lines.join("\n").replace(/\n{3,}/g, "\n\n"));
311
+ }
312
+
313
+ function detectObsidianVault() {
314
+ const candidate = path.join(process.env.HOME || "", "Documents", "Nick");
315
+ return fs.existsSync(path.join(candidate, ".obsidian")) ? candidate : "";
316
+ }
317
+
318
+ function readJson(file) {
319
+ return JSON.parse(fs.readFileSync(file, "utf8"));
320
+ }
321
+
322
+ function writeFile(file, text) {
323
+ fs.mkdirSync(path.dirname(file), { recursive: true });
324
+ fs.writeFileSync(file, text, "utf8");
325
+ }
326
+
327
+ function git(args) {
328
+ const result = spawnSync("git", args, { cwd: repoRoot, encoding: "utf8" });
329
+ return result.status === 0 ? result.stdout : "";
330
+ }
331
+
332
+ function normalizeGitRemote(remote) {
333
+ if (!remote) return "";
334
+ if (remote.endsWith(".git")) remote = remote.slice(0, -4);
335
+ const sshMatch = remote.match(/^git@github\.com:(.+)$/);
336
+ if (sshMatch) return `https://github.com/${sshMatch[1]}`;
337
+ return remote;
338
+ }
339
+
340
+ function normalizeForCompare(text) {
341
+ // Strip ONLY the values derived from the runtime ENVIRONMENT (not the source
342
+ // tree), so the gate compares purely source-derived content — it can neither
343
+ // false-RED across machines/forks nor false-GREEN by over-normalizing. Mirrors
344
+ // the parity payload-identity rule: normalize now/env-derived fields, nothing
345
+ // else. Exactly two such fields exist in the rendered index:
346
+ // 1. the generated date ("Generated on <date>") — changes every day;
347
+ // 2. the Repository URL — derived from `git remote get-url origin`, so it
348
+ // differs on every fork/mirror clone of an otherwise in-sync tree.
349
+ // The Snapshot COUNTS are source-derived and MUST NOT be normalized — a wrong
350
+ // count is exactly the drift this gate exists to catch.
351
+ return text
352
+ .replace(/\r\n/g, "\n")
353
+ .replace(
354
+ /(Generated from the current repository code on )\d{4}-\d{2}-\d{2}( by)/,
355
+ "$1<DATE>$2"
356
+ )
357
+ .replace(/^(- Repository: ).*$/m, "$1<REPO>");
358
+ }
359
+
360
+ function checkInSync(repoDoc, rendered) {
361
+ const rel = path.relative(repoRoot, repoDoc);
362
+ if (!fs.existsSync(repoDoc)) {
363
+ process.stderr.write(`project-index check FAILED: ${rel} does not exist.\nRun: (cd plugins/cool-workflow && npm run sync:project-index)\n`);
364
+ process.exitCode = 1;
365
+ return;
366
+ }
367
+ const committed = normalizeForCompare(fs.readFileSync(repoDoc, "utf8"));
368
+ const fresh = normalizeForCompare(rendered);
369
+ if (committed === fresh) {
370
+ process.stdout.write(`${JSON.stringify({ ok: true, check: true, version: packageJson.version, doc: rel }, null, 2)}\n`);
371
+ return;
372
+ }
373
+ const a = committed.split("\n");
374
+ const b = fresh.split("\n");
375
+ let i = 0;
376
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
377
+ process.stderr.write(
378
+ `project-index check FAILED: ${rel} is stale (does not match a fresh source scan).\n` +
379
+ `First difference at line ${i + 1}:\n` +
380
+ ` committed: ${JSON.stringify(a[i] ?? "<missing>")}\n` +
381
+ ` expected: ${JSON.stringify(b[i] ?? "<missing>")}\n` +
382
+ `Regenerate with: (cd plugins/cool-workflow && npm run sync:project-index)\n`
383
+ );
384
+ process.exitCode = 1;
385
+ }
386
+
387
+ main();
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ // validate-run-state-schema.js — fail-closed build gate for ABI consistency.
3
+ //
4
+ // BSD discipline (mechanism, not policy):
5
+ // - Extracts REQUIRED_TOP_LEVEL_KEYS from the compiled run-state-schema module.
6
+ // - Parses the TypeScript source of WorkflowRun to extract interface keys.
7
+ // - Cross-checks: every required (non-optional) key in the type MUST appear
8
+ // in the schema. Every optional key in the schema MUST match the type.
9
+ // - FAIL CLOSED on any mismatch — this gates the release.
10
+ //
11
+ // From v0.1.53: prevents silent three-point drift between WorkflowRun,
12
+ // normalizeRunState, and validateMigratedRunState.
13
+
14
+ "use strict";
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+
19
+ const PLUGIN_ROOT = path.resolve(__dirname, "..");
20
+ const TYPES_FILE = path.join(PLUGIN_ROOT, "src", "types", "run.ts");
21
+ const SCHEMA_MODULE = path.join(PLUGIN_ROOT, "dist", "run-state-schema.js");
22
+
23
+ function main() {
24
+ const errors = [];
25
+
26
+ // ---- 1. Extract WorkflowRun keys from the TypeScript interface source ----
27
+ const typeSource = fs.readFileSync(TYPES_FILE, "utf8");
28
+ const runKeys = extractInterfaceKeys(typeSource, "WorkflowRun");
29
+ if (runKeys.size === 0) {
30
+ console.error("validate-run-state-schema: FATAL — could not extract WorkflowRun keys from types/run.ts");
31
+ process.exit(1);
32
+ }
33
+
34
+ // ---- 2. Load the schema module (compiled dist) --------------------------
35
+ let schema;
36
+ try {
37
+ schema = require(SCHEMA_MODULE);
38
+ } catch (e) {
39
+ console.error(`validate-run-state-schema: FATAL — cannot load ${SCHEMA_MODULE}. Run 'npm run build' first.`);
40
+ process.exit(1);
41
+ }
42
+
43
+ const requiredKeys = new Set(schema.REQUIRED_TOP_LEVEL_KEYS || []);
44
+ const optionalKeys = new Set(schema.OPTIONAL_TOP_LEVEL_KEYS || []);
45
+
46
+ // ---- 3. Cross-check: every type key must be either required or optional ----
47
+ for (const key of runKeys) {
48
+ if (requiredKeys.has(key)) continue;
49
+ if (optionalKeys.has(key)) continue;
50
+ errors.push(
51
+ `Drift: WorkflowRun key "${key}" is NOT in REQUIRED_TOP_LEVEL_KEYS or OPTIONAL_TOP_LEVEL_KEYS in run-state-schema.ts. ` +
52
+ `Add it to the appropriate list.`
53
+ );
54
+ }
55
+
56
+ // ---- 4. Reverse check: every declared key must exist in the type ---------
57
+ for (const key of requiredKeys) {
58
+ if (!runKeys.has(key)) {
59
+ errors.push(`Drift: REQUIRED_TOP_LEVEL_KEYS contains "${key}" but it is not in WorkflowRun.`);
60
+ }
61
+ }
62
+ for (const key of optionalKeys) {
63
+ if (!runKeys.has(key)) {
64
+ errors.push(`Drift: OPTIONAL_TOP_LEVEL_KEYS contains "${key}" but it is not in WorkflowRun.`);
65
+ }
66
+ }
67
+
68
+ // ---- Report ------------------------------------------------------------
69
+ if (errors.length) {
70
+ console.error("validate-run-state-schema: FAIL CLOSED — schema drift detected.");
71
+ for (const e of errors) console.error(` ${e}`);
72
+ console.error(
73
+ `\nFix: update REQUIRED_TOP_LEVEL_KEYS and/or OPTIONAL_TOP_LEVEL_KEYS in ` +
74
+ `src/run-state-schema.ts, then update normalizeRunState() and validateMigratedRunState() ` +
75
+ `in src/state-migrations.ts to handle any new required fields.`
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ const total = requiredKeys.size + optionalKeys.size;
81
+ console.error(
82
+ `validate-run-state-schema: ok — ${runKeys.size} WorkflowRun keys accounted for ` +
83
+ `(${requiredKeys.size} required + ${optionalKeys.size} optional = ${total})`
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Extract top-level keys from a TypeScript interface definition.
89
+ * Uses brace-depth tracking to skip keys inside nested object types.
90
+ */
91
+ function extractInterfaceKeys(source, interfaceName) {
92
+ const startIdx = source.indexOf(`export interface ${interfaceName} {`);
93
+ if (startIdx < 0) return new Set();
94
+
95
+ const braceStart = source.indexOf("{", startIdx);
96
+ let depth = 0;
97
+ let endIdx = -1;
98
+ for (let i = braceStart; i < source.length; i++) {
99
+ if (source[i] === "{") depth++;
100
+ if (source[i] === "}") depth--;
101
+ if (depth === 0) { endIdx = i; break; }
102
+ }
103
+ if (endIdx < 0) return new Set();
104
+
105
+ const body = source.slice(braceStart + 1, endIdx);
106
+ const keys = new Set();
107
+ let braceDepth = 0;
108
+
109
+ for (const line of body.split("\n")) {
110
+ const m = line.match(/^\s*(\w+)\??\s*:/);
111
+ if (m) {
112
+ // Key is top-level only when current brace depth is 0
113
+ if (braceDepth === 0) keys.add(m[1]);
114
+ }
115
+ // Update brace depth from this line's braces
116
+ for (const ch of line) {
117
+ if (ch === "{") braceDepth++;
118
+ if (ch === "}") braceDepth--;
119
+ }
120
+ if (braceDepth < 0) braceDepth = 0;
121
+ }
122
+
123
+ return keys;
124
+ }
125
+
126
+ main();
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // verify-container-selfref — the v0.1.34 self-referential acceptance check.
5
+ //
6
+ // Runs CW's own `node dist/cli.js list` through the `node` backend AND the
7
+ // `container` backend, and asserts result + evidence are byte-IDENTICAL after
8
+ // provenance is excluded (backendId/handle/attestation differ by design). This is
9
+ // the bar that proves a delegated run is backend-agnostic.
10
+ //
11
+ // Requires a RUNNING container daemon and a pullable node image. Not part of
12
+ // release:check (CI has no daemon); run it manually before tagging v0.1.34.
13
+ //
14
+ // node scripts/verify-container-selfref.js
15
+ // CW_CONTAINER_IMAGE=node:22-slim node scripts/verify-container-selfref.js
16
+ //
17
+ // Exit: 0 PASS · 1 FAIL (evidence differs) · 2 node backend did not complete ·
18
+ // 3 SKIP (container refused — daemon down / image not pullable).
19
+
20
+ const path = require("node:path");
21
+
22
+ const pluginRoot = path.resolve(__dirname, "..");
23
+ const { runBackend } = require(path.join(pluginRoot, "dist/execution-backend.js"));
24
+ const { showBundledSandboxProfile, sandboxContextForValidation } = require(path.join(pluginRoot, "dist/sandbox-profile.js"));
25
+
26
+ const image = (process.env.CW_CONTAINER_IMAGE || "node:lts-slim").trim();
27
+ const cli = path.join(pluginRoot, "dist", "cli.js");
28
+ const ctx = sandboxContextForValidation(pluginRoot);
29
+ const policy = showBundledSandboxProfile("default", ctx);
30
+
31
+ const common = { schemaVersion: 1, command: "node", args: [cli, "list"], cwd: pluginRoot, sandboxPolicy: policy, label: "cw-self-verify" };
32
+ const nodeEnv = runBackend({ ...common, backendId: "node" });
33
+ const containerEnv = runBackend({ ...common, backendId: "container", delegation: { image } });
34
+
35
+ const line = (label, e) => ` ${label.padEnd(9)} status=${e.status} backend=${e.provenance.backendId} evidence=${JSON.stringify(e.evidence)}`;
36
+ process.stdout.write(`verify-container-selfref (image: ${image})\n${line("node", nodeEnv)}\n${line("container", containerEnv)}\n`);
37
+
38
+ if (nodeEnv.status !== "completed") {
39
+ process.stderr.write(`\n✗ node backend did not complete (${nodeEnv.status}) — cannot compare.\n`);
40
+ process.exit(2);
41
+ }
42
+ if (containerEnv.status === "refused") {
43
+ process.stderr.write(
44
+ `\n⚠ SKIP — container refused (${containerEnv.evidence[0]}).\n` +
45
+ ` ${containerEnv.result.summary}\n` +
46
+ ` Start the container daemon and ensure the image is pullable (e.g. \`docker pull ${image}\`), then re-run.\n`
47
+ );
48
+ process.exit(3);
49
+ }
50
+
51
+ const sameResult = JSON.stringify(nodeEnv.result) === JSON.stringify(containerEnv.result);
52
+ const sameEvidence = JSON.stringify(nodeEnv.evidence) === JSON.stringify(containerEnv.evidence);
53
+ if (containerEnv.status === "completed" && sameResult && sameEvidence) {
54
+ process.stdout.write("\n✓ PASS — container result + evidence are byte-identical to node (provenance differs, as designed).\n");
55
+ process.exit(0);
56
+ }
57
+
58
+ process.stderr.write(
59
+ `\n✗ FAIL — container output differs from node (status=${containerEnv.status}):\n` +
60
+ ` node evidence: ${JSON.stringify(nodeEnv.evidence)}\n` +
61
+ ` container evidence: ${JSON.stringify(containerEnv.evidence)}\n` +
62
+ ` (a node-version difference in the image can change stdout; pin CW_CONTAINER_IMAGE to a matching node.)\n`
63
+ );
64
+ process.exit(1);