stageflow 0.3.0 → 0.9.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 (164) hide show
  1. package/README.md +44 -14
  2. package/dist/agent/activity.d.ts +2 -0
  3. package/dist/agent/cursorProvider.d.ts +1 -10
  4. package/dist/agent/cursorProvider.js +18 -10
  5. package/dist/agent/fakeAgent.js +11 -7
  6. package/dist/agent/piAdapter.d.ts +2 -1
  7. package/dist/agent/piAdapter.js +28 -25
  8. package/dist/agent/port.d.ts +6 -1
  9. package/dist/agent/port.js +3 -0
  10. package/dist/cli/artifactCommand.d.ts +10 -0
  11. package/dist/cli/artifactCommand.js +120 -0
  12. package/dist/cli/envelopeCommand.d.ts +32 -0
  13. package/dist/cli/envelopeCommand.js +285 -0
  14. package/dist/cli/exportRunCommand.d.ts +10 -0
  15. package/dist/cli/exportRunCommand.js +150 -0
  16. package/dist/cli/handoffFormat.d.ts +22 -0
  17. package/dist/cli/handoffFormat.js +61 -0
  18. package/dist/cli/runCommand.d.ts +5 -1
  19. package/dist/cli/runCommand.js +36 -5
  20. package/dist/cli/runOutput.d.ts +4 -1
  21. package/dist/cli/runOutput.js +36 -8
  22. package/dist/cli/runsCommand.d.ts +22 -0
  23. package/dist/cli/runsCommand.js +591 -0
  24. package/dist/cli/skillsCommand.d.ts +16 -0
  25. package/dist/cli/skillsCommand.js +459 -0
  26. package/dist/cli/validateOutput.d.ts +2 -2
  27. package/dist/cli/validateOutput.js +2 -2
  28. package/dist/cli.js +110 -5
  29. package/dist/config/loadPipeline.js +5 -1
  30. package/dist/config/normalizePipelineStageEntry.d.ts +2 -0
  31. package/dist/config/normalizePipelineStageEntry.js +25 -1
  32. package/dist/config/pipelineStageKeys.js +1 -1
  33. package/dist/config/resolveForkEmitContext.d.ts +2 -1
  34. package/dist/config/resolveForkEmitContext.js +23 -1
  35. package/dist/config/resolvePipelineDag.js +36 -4
  36. package/dist/envelope/check.d.ts +2 -0
  37. package/dist/envelope/check.js +51 -0
  38. package/dist/envelope/cloneForks.d.ts +2 -0
  39. package/dist/envelope/cloneForks.js +62 -0
  40. package/dist/envelope/forkChoice.js +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.js +3 -1
  43. package/dist/mcp/catalogTools.d.ts +3 -0
  44. package/dist/mcp/catalogTools.js +187 -0
  45. package/dist/mcp/controlTools.d.ts +3 -0
  46. package/dist/mcp/controlTools.js +147 -0
  47. package/dist/mcp/deps.d.ts +11 -0
  48. package/dist/mcp/deps.js +1 -0
  49. package/dist/mcp/projectRun.d.ts +1 -40
  50. package/dist/mcp/projectRun.js +1 -64
  51. package/dist/mcp/resources.d.ts +5 -0
  52. package/dist/mcp/resources.js +45 -0
  53. package/dist/mcp/server.d.ts +16 -3
  54. package/dist/mcp/server.js +159 -3
  55. package/dist/mcp/toolResults.d.ts +7 -0
  56. package/dist/mcp/toolResults.js +6 -0
  57. package/dist/mcp/tools.d.ts +2 -7
  58. package/dist/mcp/tools.js +52 -103
  59. package/dist/mcp/waitRun.d.ts +46 -0
  60. package/dist/mcp/waitRun.js +146 -0
  61. package/dist/mcp/waitingGates.d.ts +12 -0
  62. package/dist/mcp/waitingGates.js +83 -0
  63. package/dist/package-meta.d.ts +1 -0
  64. package/dist/package-meta.js +1 -0
  65. package/dist/projection/projectRun.d.ts +44 -0
  66. package/dist/projection/projectRun.js +71 -0
  67. package/dist/prompt/priorEnvelope.d.ts +1 -1
  68. package/dist/prompt/priorEnvelope.js +4 -1
  69. package/dist/runstore/paths.js +2 -0
  70. package/dist/runstore/pipelineDagSnapshot.d.ts +9 -0
  71. package/dist/runstore/pipelineDagSnapshot.js +90 -1
  72. package/dist/runstore/port.d.ts +13 -2
  73. package/dist/runstore/port.js +4 -1
  74. package/dist/runstore/runProjection.d.ts +1 -1
  75. package/dist/runstore/runProjection.js +14 -3
  76. package/dist/runstore/sqlite/SqliteRunStore.d.ts +3 -2
  77. package/dist/runstore/sqlite/SqliteRunStore.js +35 -3
  78. package/dist/runstore/stageInstanceId.d.ts +4 -0
  79. package/dist/runstore/stageInstanceId.js +23 -0
  80. package/dist/runstore/syntheticStageSnapshot.d.ts +1 -1
  81. package/dist/runstore/syntheticStageSnapshot.js +2 -1
  82. package/dist/runstore/trackProjection.js +28 -12
  83. package/dist/runstore/workspaceLayout.d.ts +1 -0
  84. package/dist/runstore/workspaceLayout.js +1 -1
  85. package/dist/runtime/cloneSchedule.d.ts +18 -0
  86. package/dist/runtime/cloneSchedule.js +233 -0
  87. package/dist/runtime/envelopeRouting.d.ts +2 -1
  88. package/dist/runtime/envelopeRouting.js +80 -13
  89. package/dist/runtime/pipelineScheduler.d.ts +6 -2
  90. package/dist/runtime/pipelineScheduler.js +166 -30
  91. package/dist/runtime/resumeReconstruct.js +8 -3
  92. package/dist/runtime/runChangeBus.d.ts +18 -0
  93. package/dist/runtime/runChangeBus.js +83 -0
  94. package/dist/runtime/runManager.d.ts +7 -0
  95. package/dist/runtime/runManager.js +31 -2
  96. package/dist/runtime/runRetryCoordinator.d.ts +2 -0
  97. package/dist/runtime/runRetryCoordinator.js +4 -1
  98. package/dist/runtime/stageAttemptBootstrap.d.ts +1 -0
  99. package/dist/runtime/stageAttemptBootstrap.js +17 -9
  100. package/dist/runtime/stageRunner.d.ts +1 -0
  101. package/dist/runtime/stageRunner.js +17 -15
  102. package/dist/runtime/stageWorker.js +11 -4
  103. package/dist/server/bootstrap.d.ts +32 -0
  104. package/dist/server/bootstrap.js +56 -0
  105. package/dist/server/createHttpHost.d.ts +34 -0
  106. package/dist/server/createHttpHost.js +70 -0
  107. package/dist/server/http.d.ts +6 -13
  108. package/dist/server/http.js +295 -374
  109. package/dist/server/mcpHost.d.ts +22 -0
  110. package/dist/server/mcpHost.js +21 -0
  111. package/dist/server/operatorResults.d.ts +13 -0
  112. package/dist/server/operatorResults.js +22 -0
  113. package/dist/tools/emitStageEnvelope.d.ts +11 -2
  114. package/dist/tools/emitStageEnvelope.js +22 -6
  115. package/dist/types/envelope.d.ts +2 -0
  116. package/dist/types/forkChoice.d.ts +22 -0
  117. package/dist/types/pipeline.d.ts +8 -0
  118. package/dist/ui/assets/{index-DefBlEvN.css → index-C3N7MXAC.css} +1 -1
  119. package/dist/ui/assets/index-CSoGDA3A.js +118 -0
  120. package/dist/ui/index.html +2 -2
  121. package/package.json +2 -1
  122. package/skills/install-suite.sh +107 -0
  123. package/skills/stageflow/SKILL.md +26 -0
  124. package/skills/stageflow/references/control-surface.md +28 -0
  125. package/skills/stageflow/scripts/detect-host.mjs +54 -0
  126. package/skills/stageflow/scripts/detect-host.test.mjs +97 -0
  127. package/skills/stageflow-author/SKILL.md +58 -0
  128. package/skills/stageflow-author/assets/examples/branch-decision/hotfix.yaml +10 -0
  129. package/skills/stageflow-author/assets/examples/branch-decision/release-gate.pipeline.yaml +12 -0
  130. package/skills/stageflow-author/assets/examples/branch-decision/run-tests.yaml +13 -0
  131. package/skills/stageflow-author/assets/examples/branch-decision/ship.yaml +10 -0
  132. package/skills/stageflow-author/assets/examples/linear-review/draft.yaml +9 -0
  133. package/skills/stageflow-author/assets/examples/linear-review/publish.yaml +10 -0
  134. package/skills/stageflow-author/assets/examples/linear-review/review-loop.pipeline.yaml +10 -0
  135. package/skills/stageflow-author/assets/examples/linear-review/review.yaml +15 -0
  136. package/skills/stageflow-author/assets/examples/non-sdlc-digest/gather.yaml +9 -0
  137. package/skills/stageflow-author/assets/examples/non-sdlc-digest/research-digest.pipeline.yaml +10 -0
  138. package/skills/stageflow-author/assets/examples/non-sdlc-digest/send.yaml +10 -0
  139. package/skills/stageflow-author/assets/examples/non-sdlc-digest/summarize.yaml +9 -0
  140. package/skills/stageflow-author/references/catalog-mapping.md +131 -0
  141. package/skills/stageflow-author/references/catalog-write-conventions.md +29 -0
  142. package/skills/stageflow-author/references/stage-prompt-template.md +55 -0
  143. package/skills/stageflow-author/references/validate-and-report.md +38 -0
  144. package/skills/stageflow-delegate/SKILL.md +18 -0
  145. package/skills/stageflow-delegate/references/authoring-or-run.md +15 -0
  146. package/skills/stageflow-delegate/references/example-walkthrough.md +30 -0
  147. package/skills/stageflow-delegate/references/pattern-detection.md +44 -0
  148. package/skills/stageflow-run/SKILL.md +182 -0
  149. package/skills/stageflow-run/references/mcp-call.md +17 -0
  150. package/skills/stageflow-run/references/native-question-ui.md +78 -0
  151. package/skills/stageflow-run/references/task-and-pipeline-selection.md +52 -0
  152. package/skills/stageflow-run/scripts/mcp-call.mjs +299 -0
  153. package/skills/stageflow-run/scripts/mcp-call.test.mjs +271 -0
  154. package/skills/stageflow-session-capture/SKILL.md +94 -0
  155. package/skills/stageflow-session-capture/assets/example-pipeline/example.pipeline.yaml +7 -0
  156. package/skills/stageflow-session-capture/assets/example-pipeline/implement.yaml +8 -0
  157. package/skills/stageflow-session-capture/assets/example-pipeline/research.yaml +8 -0
  158. package/skills/stageflow-session-capture/references/catalog-authoring.md +40 -0
  159. package/skills/stageflow-session-capture/references/transcript-sources.md +25 -0
  160. package/skills/stageflow-session-capture/scripts/check-provider-gate.mjs +43 -0
  161. package/skills/stageflow-session-capture/scripts/locate-session-transcript.mjs +141 -0
  162. package/skills/stageflow-session-capture/scripts/resolve-catalog-id.mjs +119 -0
  163. package/skills/stageflow-setup/SKILL.md +110 -0
  164. package/dist/ui/assets/index-CFSzDZje.js +0 -118
@@ -13,8 +13,8 @@
13
13
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap" />
14
14
  <link rel="icon" type="image/svg+xml" href="/stageflow-icon.svg" />
15
15
  <title>Stageflow</title>
16
- <script type="module" crossorigin src="/assets/index-CFSzDZje.js"></script>
17
- <link rel="stylesheet" crossorigin href="/assets/index-DefBlEvN.css">
16
+ <script type="module" crossorigin src="/assets/index-CSoGDA3A.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index-C3N7MXAC.css">
18
18
  </head>
19
19
  <body>
20
20
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stageflow",
3
- "version": "0.3.0",
3
+ "version": "0.9.0",
4
4
  "description": "Stageflow — CLI pipeline runtime for configurable stages on Pi",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "skills",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ SOURCE_DIR="$SCRIPT_DIR"
6
+ DEST_ROOT="$(pwd)"
7
+
8
+ SKILLS=(
9
+ stageflow
10
+ stageflow-setup
11
+ stageflow-session-capture
12
+ stageflow-author
13
+ stageflow-run
14
+ stageflow-delegate
15
+ )
16
+
17
+ TARGETS=(
18
+ .cursor/skills
19
+ .claude/skills
20
+ .agents/skills
21
+ )
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ Usage: install-suite.sh [--source-dir PATH] [--dest-cwd PATH]
26
+
27
+ Copy the Stageflow harness skills suite into a project's
28
+ .cursor/skills/, .claude/skills/, and .agents/skills/.
29
+
30
+ Options:
31
+ --source-dir PATH Canonical skills tree (default: this script's directory)
32
+ --dest-cwd PATH Project root to install into (default: current directory)
33
+ EOF
34
+ }
35
+
36
+ err() {
37
+ echo "install-suite: $*" >&2
38
+ }
39
+
40
+ parse_args() {
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --source-dir)
44
+ SOURCE_DIR="${2:?--source-dir requires a path}"
45
+ shift 2
46
+ ;;
47
+ --dest-cwd)
48
+ DEST_ROOT="${2:?--dest-cwd requires a path}"
49
+ shift 2
50
+ ;;
51
+ -h|--help)
52
+ usage
53
+ exit 0
54
+ ;;
55
+ *)
56
+ err "unknown argument: $1"
57
+ usage >&2
58
+ exit 1
59
+ ;;
60
+ esac
61
+ done
62
+ }
63
+
64
+ resolve_paths() {
65
+ if [ ! -d "$SOURCE_DIR" ]; then
66
+ err "source directory not found: $SOURCE_DIR"
67
+ exit 1
68
+ fi
69
+ SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)"
70
+
71
+ if [ ! -d "$DEST_ROOT" ]; then
72
+ err "destination directory not found: $DEST_ROOT"
73
+ exit 1
74
+ fi
75
+ DEST_ROOT="$(cd "$DEST_ROOT" && pwd)"
76
+
77
+ local expected="${SOURCE_DIR}/stageflow/SKILL.md"
78
+ if [ ! -f "$expected" ]; then
79
+ err "source directory missing stageflow/SKILL.md: ${expected}"
80
+ exit 1
81
+ fi
82
+ }
83
+
84
+ copy_suite() {
85
+ local target skill
86
+ for target in "${TARGETS[@]}"; do
87
+ mkdir -p "${DEST_ROOT}/${target}"
88
+ for skill in "${SKILLS[@]}"; do
89
+ rm -rf "${DEST_ROOT}/${target}/${skill}"
90
+ cp -R "${SOURCE_DIR}/${skill}" "${DEST_ROOT}/${target}/${skill}"
91
+ done
92
+ done
93
+ }
94
+
95
+ main() {
96
+ parse_args "$@"
97
+ resolve_paths
98
+ copy_suite
99
+
100
+ echo "Installed Stageflow skills suite"
101
+ echo "Source: ${SOURCE_DIR}"
102
+ for target in "${TARGETS[@]}"; do
103
+ echo "${DEST_ROOT}/${target}"
104
+ done
105
+ }
106
+
107
+ main "$@"
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: stageflow
3
+ description: >-
4
+ Routes Stageflow work to one job: setup (install, catalog, or provider),
5
+ session-capture (reuse this chat or a past session), author (a loop the human
6
+ can explain), run (start, watch, or answer a pipeline), or delegate (a
7
+ repeating pattern). Use when the user mentions Stageflow, sf, a pipeline,
8
+ HITL, or a repeating Stageflow workflow.
9
+ compatibility: Requires the Stageflow CLI (sf). An MCP host (sf ui or sf mcp) is optional.
10
+ ---
11
+
12
+ # Stageflow
13
+
14
+ Read [references/control-surface.md](references/control-surface.md) before talking to a run. Probe with [scripts/detect-host.mjs](scripts/detect-host.mjs). Do not restate that policy here.
15
+
16
+ Read the matching job `SKILL.md` and follow it. Do not invent job behavior in this router.
17
+
18
+ | Reach this job when | Read |
19
+ |---|---|
20
+ | Install is missing, there is no catalog, or a provider is not logged in | [../stageflow-setup/SKILL.md](../stageflow-setup/SKILL.md) |
21
+ | The request should reuse a past session or this chat | [../stageflow-session-capture/SKILL.md](../stageflow-session-capture/SKILL.md) |
22
+ | The request describes a loop the human can explain | [../stageflow-author/SKILL.md](../stageflow-author/SKILL.md) |
23
+ | The request needs to start, watch, or answer a run | [../stageflow-run/SKILL.md](../stageflow-run/SKILL.md) |
24
+ | The request repeats a known pattern, or could become one | [../stageflow-delegate/SKILL.md](../stageflow-delegate/SKILL.md) |
25
+
26
+ If none of those fit, name the five jobs in one line and ask which.
@@ -0,0 +1,28 @@
1
+ # Control surface
2
+
3
+ Prefer MCP when a Stageflow host is up. Otherwise use the CLI. This file is the only copy of that rule; job skills cite it instead of restating it.
4
+
5
+ ## Probe
6
+
7
+ Run [../scripts/detect-host.mjs](../scripts/detect-host.mjs). Do not write a second probe.
8
+
9
+ ```bash
10
+ node ../scripts/detect-host.mjs
11
+ node ../scripts/detect-host.mjs --base-url http://127.0.0.1:3847
12
+ ```
13
+
14
+ The script `GET`s `{baseUrl}/api/health` (default `http://127.0.0.1:3847`, 1500 ms timeout). **up** means HTTP 200 and parseable JSON. Non-200, non-JSON, or timeout is **down**. This probe is Stageflow host up/down only — it does not detect a coding-agent question UI. Gate presentation lives in [`../../stageflow-run/references/native-question-ui.md`](../../stageflow-run/references/native-question-ui.md).
15
+
16
+ Stdout is one line: `up <baseUrl>` or `down <baseUrl>`. Exit `0` when up, `1` when down, `2` on usage error.
17
+
18
+ Start a host with `sf ui` or `sf mcp` when the user wants MCP. Do not auto-start one.
19
+
20
+ ## When the host is up
21
+
22
+ Use MCP tools over the Streamable HTTP endpoint at `{baseUrl}/mcp`. Tool names and payloads live in [docs/mcp.md](../../../docs/mcp.md). Typical talking-job tools: `list_pipelines`, `list_tasks`, `start_run`, `get_run`, `wait_run`, `list_waiting`, `answer_gate`, `get_envelope`, `read_artifact`, `validate`, `get_health`.
23
+
24
+ ## When the host is down
25
+
26
+ Use the `sf` CLI. Command names and flags live in [docs/cli-reference.md](../../../docs/cli-reference.md). Typical talking-job commands: `sf run`, `sf runs waiting`, `sf runs answer`, `sf runs wait`, `sf validate`, `sf envelope get`, `sf artifact read`, `sf providers`.
27
+
28
+ Probe before each mutating `sf runs` verb. If the probe is **up** or the command refuses because a host is up, continue that gate via MCP — do not start a second mutating writer, and do not start `sf mcp` as a disposable bridge.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ const DEFAULT_BASE_URL = "http://127.0.0.1:3847";
4
+ const TIMEOUT_MS = 1500;
5
+
6
+ function usage() {
7
+ return "Usage: detect-host.mjs [--base-url URL]";
8
+ }
9
+
10
+ function parseArgs(argv) {
11
+ let baseUrl = DEFAULT_BASE_URL;
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = argv[i];
14
+ if (arg === "-h" || arg === "--help") {
15
+ console.log(usage());
16
+ process.exit(0);
17
+ }
18
+ if (arg === "--base-url") {
19
+ const value = argv[i + 1];
20
+ if (!value) {
21
+ console.error("detect-host: --base-url requires a URL");
22
+ console.error(usage());
23
+ process.exit(2);
24
+ }
25
+ baseUrl = value.replace(/\/$/, "");
26
+ i += 1;
27
+ continue;
28
+ }
29
+ console.error(`detect-host: unknown argument: ${arg}`);
30
+ console.error(usage());
31
+ process.exit(2);
32
+ }
33
+ return { baseUrl };
34
+ }
35
+
36
+ async function probe(baseUrl) {
37
+ const ac = new AbortController();
38
+ const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
39
+ try {
40
+ const res = await fetch(`${baseUrl}/api/health`, { signal: ac.signal });
41
+ if (res.status !== 200) return "down";
42
+ JSON.parse(await res.text());
43
+ return "up";
44
+ } catch {
45
+ return "down";
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+
51
+ const { baseUrl } = parseArgs(process.argv.slice(2));
52
+ const status = await probe(baseUrl);
53
+ console.log(`${status} ${baseUrl}`);
54
+ process.exit(status === "up" ? 0 : 1);
@@ -0,0 +1,97 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn } from "node:child_process";
3
+ import { createServer } from "node:http";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { test } from "node:test";
7
+
8
+ const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "detect-host.mjs");
9
+
10
+ function run(baseUrl) {
11
+ return new Promise((resolve) => {
12
+ const child = spawn(process.execPath, [script, "--base-url", baseUrl], {
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ });
15
+ let stdout = "";
16
+ let stderr = "";
17
+ child.stdout.setEncoding("utf8");
18
+ child.stderr.setEncoding("utf8");
19
+ child.stdout.on("data", (chunk) => {
20
+ stdout += chunk;
21
+ });
22
+ child.stderr.on("data", (chunk) => {
23
+ stderr += chunk;
24
+ });
25
+ child.on("close", (status) => {
26
+ resolve({ status, stdout, stderr });
27
+ });
28
+ });
29
+ }
30
+
31
+ function listen(handler) {
32
+ return new Promise((resolve) => {
33
+ const server = createServer(handler);
34
+ server.listen(0, "127.0.0.1", () => {
35
+ const addr = server.address();
36
+ resolve({ server, baseUrl: `http://127.0.0.1:${addr.port}` });
37
+ });
38
+ });
39
+ }
40
+
41
+ test("classifies reachable 200 JSON /api/health as up", async () => {
42
+ const { server, baseUrl } = await listen((req, res) => {
43
+ if (req.url === "/api/health") {
44
+ res.writeHead(200, { "content-type": "application/json" });
45
+ res.end(JSON.stringify({ ok: true }));
46
+ return;
47
+ }
48
+ res.writeHead(404);
49
+ res.end();
50
+ });
51
+ try {
52
+ const result = await run(baseUrl);
53
+ assert.equal(result.status, 0);
54
+ assert.equal(result.stdout.trim(), `up ${baseUrl}`);
55
+ } finally {
56
+ server.close();
57
+ }
58
+ });
59
+
60
+ test("classifies non-200 /api/health as down", async () => {
61
+ const { server, baseUrl } = await listen((_req, res) => {
62
+ res.writeHead(503, { "content-type": "application/json" });
63
+ res.end(JSON.stringify({ ok: false }));
64
+ });
65
+ try {
66
+ const result = await run(baseUrl);
67
+ assert.equal(result.status, 1);
68
+ assert.equal(result.stdout.trim(), `down ${baseUrl}`);
69
+ } finally {
70
+ server.close();
71
+ }
72
+ });
73
+
74
+ test("classifies 200 non-JSON /api/health as down", async () => {
75
+ const { server, baseUrl } = await listen((_req, res) => {
76
+ res.writeHead(200, { "content-type": "text/plain" });
77
+ res.end("ok");
78
+ });
79
+ try {
80
+ const result = await run(baseUrl);
81
+ assert.equal(result.status, 1);
82
+ assert.equal(result.stdout.trim(), `down ${baseUrl}`);
83
+ } finally {
84
+ server.close();
85
+ }
86
+ });
87
+
88
+ test("classifies a hung /api/health as down", async () => {
89
+ const { server, baseUrl } = await listen(() => {});
90
+ try {
91
+ const result = await run(baseUrl);
92
+ assert.equal(result.status, 1);
93
+ assert.equal(result.stdout.trim(), `down ${baseUrl}`);
94
+ } finally {
95
+ server.close();
96
+ }
97
+ });
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: stageflow-author
3
+ description: >-
4
+ Turns a described structure or repeatable loop into a reusable Stageflow
5
+ pipeline and one stage YAML per step in the project catalog. Use when a
6
+ human explains steps, sequencing, a decision point, or a review they want
7
+ to reuse.
8
+ compatibility: Requires the sf CLI on PATH
9
+ disable-model-invocation: true
10
+ ---
11
+
12
+ # Stageflow author
13
+
14
+ Turn a structure a human can explain into one pipeline YAML file and one stage YAML file per step. `stageflow-run` owns starting a run and any throwaway task.
15
+
16
+ Talking jobs cite [`../stageflow/references/control-surface.md`](../stageflow/references/control-surface.md). Probe with [`../stageflow/scripts/detect-host.mjs`](../stageflow/scripts/detect-host.mjs) only when choosing MCP `validate` — see [`references/validate-and-report.md`](references/validate-and-report.md). Do not write a second probe.
17
+
18
+ Shape reference: [`assets/examples/linear-review/`](assets/examples/linear-review/), [`assets/examples/branch-decision/`](assets/examples/branch-decision/), [`assets/examples/non-sdlc-digest/`](assets/examples/non-sdlc-digest/).
19
+
20
+ ## Provider gate
21
+
22
+ Run before any catalog write:
23
+
24
+ ```
25
+ sf providers status
26
+ ```
27
+
28
+ A provider counts when a row's second column is `configured`. **Done when** at least one row is `configured`. If none are, or the command fails, stop. Write nothing. Name `stageflow-setup`. Do not run `sf providers login`.
29
+
30
+ ## Elicit
31
+
32
+ Ask until you can name every step, how they sequence, any decision point, and any review or sign-off. Confirm that mapped summary with the human before writing.
33
+
34
+ **Done when** the human agrees the step list, sequencing, decisions, and review points.
35
+
36
+ ## Map
37
+
38
+ Read [`references/catalog-mapping.md`](references/catalog-mapping.md). Apply those rules to the confirmed structure. Read [`references/stage-prompt-template.md`](references/stage-prompt-template.md) for each stage's `system_prompt`, `model`, and `gate_kinds`.
39
+
40
+ **Done when** you have a pipeline id, one stage id per step, `needs` / `fork` wiring, and a prompt plan per stage.
41
+
42
+ ## Locate
43
+
44
+ Read [`references/catalog-write-conventions.md`](references/catalog-write-conventions.md). Resolve the write directory and check every candidate pipeline and stage id against files already there.
45
+
46
+ **Done when** every id is free, or the human has given a different id.
47
+
48
+ ## Write
49
+
50
+ Write one external stage YAML per step and one pipeline YAML that wires them with `uses: ./<id>.yaml`. Use the native Write tool. Do not call `createPipeline` or `createStage`. Do not write a `*.task.yaml`.
51
+
52
+ **Done when** the pipeline file and every stage file exist on disk.
53
+
54
+ ## Validate and report
55
+
56
+ Read [`references/validate-and-report.md`](references/validate-and-report.md). Follow it to the end.
57
+
58
+ **Done when** that file's success report is printed.
@@ -0,0 +1,10 @@
1
+ id: hotfix
2
+ system_prompt: |
3
+ Take the failing results from the prior envelope and repair the work they
4
+ describe.
5
+
6
+ Write the repair notes via write_stage_artifact (for example hotfix.md).
7
+
8
+ When finished, call emit_stage_envelope exactly once with status, summary,
9
+ artifacts, and a payload describing what you repaired.
10
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,12 @@
1
+ id: release-gate
2
+ stages:
3
+ - id: run-tests
4
+ uses: ./run-tests.yaml
5
+ fork:
6
+ select: one
7
+ - id: hotfix
8
+ uses: ./hotfix.yaml
9
+ needs: run-tests
10
+ - id: ship
11
+ uses: ./ship.yaml
12
+ needs: run-tests
@@ -0,0 +1,13 @@
1
+ id: run-tests
2
+ system_prompt: |
3
+ Assess whether the current work is ready to ship.
4
+
5
+ Write the results via write_stage_artifact (for example results.md).
6
+
7
+ When finished, call emit_stage_envelope exactly once with status, summary,
8
+ artifacts, and a payload the next stage can use.
9
+
10
+ On a success emit, include fork_choice naming immediate successor id(s) that
11
+ should run. Name only ids listed as this stage's children. select: one →
12
+ exactly one id: hotfix when the results fail, ship when they pass.
13
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,10 @@
1
+ id: ship
2
+ system_prompt: |
3
+ Take the passing results from the prior envelope and produce the shippable
4
+ package.
5
+
6
+ Write the package notes via write_stage_artifact (for example ship.md).
7
+
8
+ When finished, call emit_stage_envelope exactly once with status, summary,
9
+ artifacts, and a payload describing what you shipped.
10
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,9 @@
1
+ id: draft
2
+ system_prompt: |
3
+ Produce a concise draft of the document described in the task.
4
+
5
+ Write the draft via write_stage_artifact (for example draft.md).
6
+
7
+ When finished, call emit_stage_envelope exactly once with status, summary,
8
+ artifacts, and a payload the next stage can use.
9
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,10 @@
1
+ id: publish
2
+ system_prompt: |
3
+ Take the accepted draft and review from prior envelopes and produce the
4
+ version that goes out.
5
+
6
+ Write that version via write_stage_artifact (for example published.md).
7
+
8
+ When finished, call emit_stage_envelope exactly once with status, summary,
9
+ artifacts, and a payload describing what you produced.
10
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,10 @@
1
+ id: review-loop
2
+ stages:
3
+ - id: draft
4
+ uses: ./draft.yaml
5
+ - id: review
6
+ uses: ./review.yaml
7
+ needs: draft
8
+ - id: publish
9
+ uses: ./publish.yaml
10
+ needs: review
@@ -0,0 +1,15 @@
1
+ id: review
2
+ gate_kinds:
3
+ - artifact_backed
4
+ system_prompt: |
5
+ Review the draft from the prior stage and get operator acceptance before
6
+ completing this stage.
7
+
8
+ 1. Write a concise review via write_stage_artifact (for example review.md).
9
+ 2. Call ask_operator with kind artifact_backed referencing that artifact path
10
+ so the operator can review it.
11
+ 3. On reject or change text: revise the review artifact and call ask_operator
12
+ again with artifact_backed in this same stage. Do not complete yet.
13
+ 4. Call emit_stage_envelope with an advancing success status only after the
14
+ operator accepts. Never emit before accept.
15
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,9 @@
1
+ id: gather
2
+ system_prompt: |
3
+ Collect the sources named in the incoming goal for this week's digest.
4
+
5
+ Write the collected notes via write_stage_artifact (for example sources.md).
6
+
7
+ When finished, call emit_stage_envelope exactly once with status, summary,
8
+ artifacts, and a payload the next stage can use.
9
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,10 @@
1
+ id: research-digest
2
+ stages:
3
+ - id: gather
4
+ uses: ./gather.yaml
5
+ - id: summarize
6
+ uses: ./summarize.yaml
7
+ needs: gather
8
+ - id: send
9
+ uses: ./send.yaml
10
+ needs: summarize
@@ -0,0 +1,10 @@
1
+ id: send
2
+ system_prompt: |
3
+ Address the digest from the prior envelope to the named recipients and
4
+ record what went out.
5
+
6
+ Write that record via write_stage_artifact (for example sent.md).
7
+
8
+ When finished, call emit_stage_envelope exactly once with status, summary,
9
+ artifacts, and a payload describing who received the digest.
10
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,9 @@
1
+ id: summarize
2
+ system_prompt: |
3
+ Turn the gathered notes from the prior envelope into a short weekly digest.
4
+
5
+ Write the digest via write_stage_artifact (for example digest.md).
6
+
7
+ When finished, call emit_stage_envelope exactly once with status, summary,
8
+ artifacts, and a payload the next stage can use.
9
+ model: anthropic/claude-sonnet-4-5
@@ -0,0 +1,131 @@
1
+ # Catalog mapping
2
+
3
+ Turn the confirmed step list into one pipeline id and one stage id per step. Ids follow `STAGE_ID_PATTERN`: start with a letter, then lowercase letters, digits, and single hyphens (`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`).
4
+
5
+ Derive each id from the human's name for that step or loop:
6
+
7
+ 1. Lowercase.
8
+ 2. Replace each run of non-alphanumeric characters with `-`.
9
+ 3. Collapse repeated hyphens; trim leading and trailing hyphens.
10
+ 4. If the first character is a digit, prefix `x`.
11
+ 5. If the result is empty, use `x`.
12
+ 6. Cap at 64 characters; trim a trailing hyphen after the cut.
13
+ 7. Keep the result only when it matches the pattern.
14
+
15
+ Collision handling lives in [`catalog-write-conventions.md`](catalog-write-conventions.md).
16
+
17
+ ## Sequencing
18
+
19
+ `needs` is a single parent stage id. Fan-in (one stage needing two parents) is out of catalog shape.
20
+
21
+ | Human says | Pipeline shape |
22
+ |---|---|
23
+ | Steps in order | Each later stage `needs` the previous id. No `fork` field. |
24
+ | Steps happen together | Sibling stages share one `needs` (the same parent). No `fork` field. |
25
+ | Exactly one branch runs | Deciding stage gets `fork: { select: one }`. Each branch `needs` the decider. |
26
+ | Either, both, or a subset may run | Deciding stage gets `fork: { select: subset }`. Each branch `needs` the decider. |
27
+
28
+ Default `select` is `one` unless the human says more than one branch can run.
29
+
30
+ Map linear chains, sibling fan-out, and single-level `fork`. A runtime clone count is not knowable from a static description — stay on those three shapes. Leave `clonable` and `clone_forks` unset.
31
+
32
+ A review, approval, or sign-off step is a gated stage: put `gate_kinds` on that stage file and follow [`stage-prompt-template.md`](stage-prompt-template.md).
33
+
34
+ ## Worked examples
35
+
36
+ ### Review loop (linear)
37
+
38
+ Three sequential steps; the middle one is a sign-off.
39
+
40
+ ```yaml
41
+ id: review-loop
42
+ stages:
43
+ - id: draft
44
+ uses: ./draft.yaml
45
+ - id: review
46
+ uses: ./review.yaml
47
+ needs: draft
48
+ - id: publish
49
+ uses: ./publish.yaml
50
+ needs: review
51
+ ```
52
+
53
+ `review` carries `gate_kinds: [artifact_backed]`. Full set: [`../assets/examples/linear-review/`](../assets/examples/linear-review/).
54
+
55
+ ### Release gate (fork, select one)
56
+
57
+ One deciding step, then exactly one successor.
58
+
59
+ ```yaml
60
+ id: release-gate
61
+ stages:
62
+ - id: run-tests
63
+ uses: ./run-tests.yaml
64
+ fork:
65
+ select: one
66
+ - id: hotfix
67
+ uses: ./hotfix.yaml
68
+ needs: run-tests
69
+ - id: ship
70
+ uses: ./ship.yaml
71
+ needs: run-tests
72
+ ```
73
+
74
+ Full set: [`../assets/examples/branch-decision/`](../assets/examples/branch-decision/).
75
+
76
+ ### Research digest (linear, non-software)
77
+
78
+ Weekly gather → summarize → send. Same `needs` chain as the review loop; ids and prompts stay in that domain.
79
+
80
+ ```yaml
81
+ id: research-digest
82
+ stages:
83
+ - id: gather
84
+ uses: ./gather.yaml
85
+ - id: summarize
86
+ uses: ./summarize.yaml
87
+ needs: gather
88
+ - id: send
89
+ uses: ./send.yaml
90
+ needs: summarize
91
+ ```
92
+
93
+ Full set: [`../assets/examples/non-sdlc-digest/`](../assets/examples/non-sdlc-digest/).
94
+
95
+ ### Sibling fan-out
96
+
97
+ "After intake, collect quotes and collect notes at the same time":
98
+
99
+ ```yaml
100
+ stages:
101
+ - id: intake
102
+ uses: ./intake.yaml
103
+ - id: collect-quotes
104
+ uses: ./collect-quotes.yaml
105
+ needs: intake
106
+ - id: collect-notes
107
+ uses: ./collect-notes.yaml
108
+ needs: intake
109
+ ```
110
+
111
+ No `fork` field. Both siblings run.
112
+
113
+ ### Fork, select subset
114
+
115
+ "Email, post, or both could go out":
116
+
117
+ ```yaml
118
+ stages:
119
+ - id: choose-channels
120
+ uses: ./choose-channels.yaml
121
+ fork:
122
+ select: subset
123
+ - id: email
124
+ uses: ./email.yaml
125
+ needs: choose-channels
126
+ - id: post
127
+ uses: ./post.yaml
128
+ needs: choose-channels
129
+ ```
130
+
131
+ The success emit names one or more of those successor ids in `fork_choice`.