@chllming/wave-orchestration 0.8.5 → 0.8.6

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 (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +14 -9
  3. package/docs/README.md +3 -1
  4. package/docs/context7/bundles.json +19 -20
  5. package/docs/context7/planner-agent/README.md +4 -1
  6. package/docs/guides/author-and-run-waves.md +4 -1
  7. package/docs/guides/planner.md +3 -1
  8. package/docs/guides/signal-wrappers.md +165 -0
  9. package/docs/guides/terminal-surfaces.md +13 -0
  10. package/docs/plans/context7-wave-orchestrator.md +24 -7
  11. package/docs/plans/current-state.md +7 -3
  12. package/docs/plans/end-state-architecture.md +16 -4
  13. package/docs/plans/examples/wave-example-design-handoff.md +1 -1
  14. package/docs/plans/examples/wave-example-live-proof.md +1 -1
  15. package/docs/plans/migration.md +165 -72
  16. package/docs/plans/wave-orchestrator.md +3 -0
  17. package/docs/reference/cli-reference.md +11 -2
  18. package/docs/reference/npmjs-trusted-publishing.md +2 -2
  19. package/docs/reference/sample-waves.md +5 -5
  20. package/docs/reference/skills.md +9 -1
  21. package/docs/reference/wave-control.md +2 -0
  22. package/package.json +1 -1
  23. package/releases/manifest.json +19 -0
  24. package/scripts/context7-api-check.sh +57 -13
  25. package/scripts/wave-orchestrator/control-cli.mjs +19 -0
  26. package/scripts/wave-orchestrator/coordination.mjs +35 -0
  27. package/scripts/wave-orchestrator/install.mjs +2 -0
  28. package/scripts/wave-orchestrator/launcher-runtime.mjs +11 -0
  29. package/scripts/wave-orchestrator/launcher.mjs +20 -0
  30. package/scripts/wave-orchestrator/session-supervisor.mjs +113 -0
  31. package/scripts/wave-orchestrator/shared.mjs +1 -0
  32. package/scripts/wave-orchestrator/signals.mjs +681 -0
  33. package/scripts/wave-orchestrator/wave-control-schema.mjs +2 -0
  34. package/scripts/wave-status.sh +200 -0
  35. package/scripts/wave-watch.sh +200 -0
  36. package/skills/README.md +3 -0
  37. package/skills/signal-hygiene/SKILL.md +51 -0
  38. package/skills/signal-hygiene/skill.json +20 -0
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ lane="main"
5
+ wave=""
6
+ agent=""
7
+ run_id=""
8
+ dry_run="0"
9
+ json_output="0"
10
+
11
+ usage() {
12
+ cat <<'EOF'
13
+ Usage:
14
+ scripts/wave-status.sh [--lane <lane>] [--wave <n>] [--agent <id>] [--run <id>] [--dry-run] [--json]
15
+
16
+ Exit codes:
17
+ 0 completed
18
+ 10 waiting or running
19
+ 20 input required
20
+ 40 failed
21
+ EOF
22
+ }
23
+
24
+ while [ "$#" -gt 0 ]; do
25
+ case "$1" in
26
+ --lane)
27
+ lane="${2:-}"
28
+ shift 2
29
+ ;;
30
+ --wave)
31
+ wave="${2:-}"
32
+ shift 2
33
+ ;;
34
+ --agent)
35
+ agent="${2:-}"
36
+ shift 2
37
+ ;;
38
+ --run)
39
+ run_id="${2:-}"
40
+ shift 2
41
+ ;;
42
+ --dry-run)
43
+ dry_run="1"
44
+ shift
45
+ ;;
46
+ --json)
47
+ json_output="1"
48
+ shift
49
+ ;;
50
+ --help|-h)
51
+ usage
52
+ exit 0
53
+ ;;
54
+ *)
55
+ echo "Unknown argument: $1" >&2
56
+ usage >&2
57
+ exit 2
58
+ ;;
59
+ esac
60
+ done
61
+
62
+ run_wave_cli() {
63
+ if [ -n "${WAVE_WRAPPER_ENTRY:-}" ]; then
64
+ node "$WAVE_WRAPPER_ENTRY" "$@"
65
+ return
66
+ fi
67
+ if [ -f "scripts/wave.mjs" ]; then
68
+ node "scripts/wave.mjs" "$@"
69
+ return
70
+ fi
71
+ if [ -f "node_modules/@chllming/wave-orchestration/scripts/wave.mjs" ]; then
72
+ node "node_modules/@chllming/wave-orchestration/scripts/wave.mjs" "$@"
73
+ return
74
+ fi
75
+ if command -v pnpm >/dev/null 2>&1; then
76
+ pnpm exec wave "$@"
77
+ return
78
+ fi
79
+ echo "Unable to locate Wave CLI. Set WAVE_WRAPPER_ENTRY or install the package locally." >&2
80
+ exit 2
81
+ }
82
+
83
+ infer_wave() {
84
+ node - "$lane" "$run_id" "$dry_run" <<'NODE'
85
+ const fs = require("node:fs");
86
+ const path = require("node:path");
87
+
88
+ const lane = process.argv[2] || "main";
89
+ const runId = process.argv[3] || "";
90
+ const dryRun = process.argv[4] === "1";
91
+ const stateDir = runId
92
+ ? path.join(process.cwd(), ".tmp", `${lane}-wave-launcher`, "adhoc", runId, dryRun ? "dry-run" : "")
93
+ : path.join(process.cwd(), ".tmp", `${lane}-wave-launcher`, dryRun ? "dry-run" : "");
94
+ const runStatePath = path.join(stateDir, "run-state.json");
95
+ let payload = null;
96
+ try {
97
+ payload = JSON.parse(fs.readFileSync(runStatePath, "utf8"));
98
+ } catch {
99
+ payload = null;
100
+ }
101
+ const waves = Object.values(payload?.waves || {})
102
+ .filter((entry) => entry && typeof entry === "object")
103
+ .map((entry) => ({
104
+ wave: Number.parseInt(String(entry.wave ?? ""), 10),
105
+ state: String(entry.currentState || "").trim().toLowerCase(),
106
+ }))
107
+ .filter((entry) => Number.isFinite(entry.wave))
108
+ .sort((left, right) => left.wave - right.wave);
109
+ const active = waves.findLast(
110
+ (entry) => !["completed", "failed", "timed_out", "timed-out"].includes(entry.state),
111
+ );
112
+ if (active) {
113
+ process.stdout.write(String(active.wave));
114
+ process.exit(0);
115
+ }
116
+ const completed = Array.isArray(payload?.completedWaves)
117
+ ? payload.completedWaves
118
+ .map((value) => Number.parseInt(String(value), 10))
119
+ .filter((value) => Number.isFinite(value))
120
+ .sort((left, right) => left - right)
121
+ : [];
122
+ process.stdout.write(String(completed.at(-1) ?? 0));
123
+ NODE
124
+ }
125
+
126
+ if [ -z "$wave" ]; then
127
+ wave="$(infer_wave)"
128
+ fi
129
+
130
+ status_args=(control status --lane "$lane" --wave "$wave" --json)
131
+ if [ -n "$agent" ]; then
132
+ status_args+=(--agent "$agent")
133
+ fi
134
+ if [ -n "$run_id" ]; then
135
+ status_args+=(--run "$run_id")
136
+ fi
137
+ if [ "$dry_run" = "1" ]; then
138
+ status_args+=(--dry-run)
139
+ fi
140
+
141
+ payload="$(run_wave_cli "${status_args[@]}")"
142
+
143
+ if [ "$json_output" = "1" ]; then
144
+ printf '%s\n' "$payload"
145
+ exit 0
146
+ fi
147
+
148
+ PAYLOAD="$payload" node - "$lane" "$wave" "$agent" <<'NODE'
149
+ const fs = require("node:fs");
150
+
151
+ const lane = process.argv[2] || "main";
152
+ const wave = Number.parseInt(String(process.argv[3] || "0"), 10) || 0;
153
+ const agentId = String(process.argv[4] || "").trim();
154
+ const payload = JSON.parse(process.env.PAYLOAD || "{}");
155
+ const signals = payload?.signals || {};
156
+ const snapshot = agentId
157
+ ? (Array.isArray(signals.agents) ? signals.agents.find((entry) => entry.agentId === agentId) : null)
158
+ : signals.wave;
159
+ const effective = snapshot || {
160
+ signal: payload?.blockingEdge?.kind === "human-input" ? "feedback-requested" : "waiting",
161
+ lane,
162
+ wave,
163
+ phase: payload?.phase || "unknown",
164
+ status: payload?.blockingEdge ? "blocked" : "running",
165
+ blocking: payload?.blockingEdge || null,
166
+ attempt: payload?.activeAttempt?.attemptNumber || 0,
167
+ targetAgentIds: agentId ? [agentId] : [],
168
+ shouldWake: agentId ? true : null,
169
+ version: 0,
170
+ };
171
+ const targetKey = agentId ? "agent" : "agents";
172
+ const targetValue = agentId || (effective.targetAgentIds || []).join(",") || "none";
173
+ const blocking = effective?.blocking?.kind || "none";
174
+ const shouldWake =
175
+ typeof effective.shouldWake === "boolean" ? (effective.shouldWake ? "yes" : "no") : "n/a";
176
+ console.log(
177
+ [
178
+ `signal=${effective.signal || "waiting"}`,
179
+ `lane=${lane}`,
180
+ `wave=${wave}`,
181
+ `phase=${effective.phase || "unknown"}`,
182
+ `status=${effective.status || "running"}`,
183
+ `blocking=${blocking}`,
184
+ `attempt=${effective.attempt || 0}`,
185
+ `${targetKey}=${targetValue}`,
186
+ `version=${effective.version || 0}`,
187
+ `should_wake=${shouldWake}`,
188
+ ].join(" "),
189
+ );
190
+ if (String(effective.signal || "").trim().toLowerCase() === "completed") {
191
+ process.exit(0);
192
+ }
193
+ if (String(effective.signal || "").trim().toLowerCase() === "failed") {
194
+ process.exit(40);
195
+ }
196
+ if (String(effective.signal || "").trim().toLowerCase() === "feedback-requested") {
197
+ process.exit(20);
198
+ }
199
+ process.exit(10);
200
+ NODE
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ lane="main"
5
+ wave=""
6
+ agent=""
7
+ run_id=""
8
+ dry_run="0"
9
+ mode="follow"
10
+ refresh_ms="2000"
11
+
12
+ usage() {
13
+ cat <<'EOF'
14
+ Usage:
15
+ scripts/wave-watch.sh [--lane <lane>] [--wave <n>] [--agent <id>] [--run <id>] [--dry-run] [--refresh-ms <n>] [--follow|--until-change]
16
+
17
+ Exit codes:
18
+ 0 completed
19
+ 20 input required
20
+ 30 watched signal changed while the wave remained active
21
+ 40 failed
22
+ EOF
23
+ }
24
+
25
+ while [ "$#" -gt 0 ]; do
26
+ case "$1" in
27
+ --lane)
28
+ lane="${2:-}"
29
+ shift 2
30
+ ;;
31
+ --wave)
32
+ wave="${2:-}"
33
+ shift 2
34
+ ;;
35
+ --agent)
36
+ agent="${2:-}"
37
+ shift 2
38
+ ;;
39
+ --run)
40
+ run_id="${2:-}"
41
+ shift 2
42
+ ;;
43
+ --dry-run)
44
+ dry_run="1"
45
+ shift
46
+ ;;
47
+ --refresh-ms)
48
+ refresh_ms="${2:-}"
49
+ shift 2
50
+ ;;
51
+ --follow)
52
+ mode="follow"
53
+ shift
54
+ ;;
55
+ --until-change)
56
+ mode="until-change"
57
+ shift
58
+ ;;
59
+ --help|-h)
60
+ usage
61
+ exit 0
62
+ ;;
63
+ *)
64
+ echo "Unknown argument: $1" >&2
65
+ usage >&2
66
+ exit 2
67
+ ;;
68
+ esac
69
+ done
70
+
71
+ status_script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/wave-status.sh"
72
+ common_args=(--lane "$lane")
73
+ if [ -n "$wave" ]; then
74
+ common_args+=(--wave "$wave")
75
+ fi
76
+ if [ -n "$agent" ]; then
77
+ common_args+=(--agent "$agent")
78
+ fi
79
+ if [ -n "$run_id" ]; then
80
+ common_args+=(--run "$run_id")
81
+ fi
82
+ if [ "$dry_run" = "1" ]; then
83
+ common_args+=(--dry-run)
84
+ fi
85
+
86
+ extract_field() {
87
+ local payload="$1"
88
+ local field="$2"
89
+ PAYLOAD="$payload" node - "$agent" "$field" <<'NODE'
90
+ const agentId = String(process.argv[2] || "").trim();
91
+ const field = String(process.argv[3] || "").trim();
92
+ const payload = JSON.parse(process.env.PAYLOAD || "{}");
93
+ const signals = payload?.signals || {};
94
+ const snapshot = agentId
95
+ ? (Array.isArray(signals.agents) ? signals.agents.find((entry) => entry.agentId === agentId) : null)
96
+ : signals.wave;
97
+ const value = snapshot?.[field];
98
+ process.stdout.write(value === undefined || value === null ? "" : String(value));
99
+ NODE
100
+ }
101
+
102
+ print_line() {
103
+ local payload="$1"
104
+ PAYLOAD="$payload" node - "$lane" "${wave:-0}" "$agent" <<'NODE'
105
+ const lane = process.argv[2] || "main";
106
+ const wave = Number.parseInt(String(process.argv[3] || "0"), 10) || 0;
107
+ const agentId = String(process.argv[4] || "").trim();
108
+ const payload = JSON.parse(process.env.PAYLOAD || "{}");
109
+ const signals = payload?.signals || {};
110
+ const snapshot = agentId
111
+ ? (Array.isArray(signals.agents) ? signals.agents.find((entry) => entry.agentId === agentId) : null)
112
+ : signals.wave;
113
+ const effective = snapshot || {
114
+ signal: payload?.blockingEdge?.kind === "human-input" ? "feedback-requested" : "waiting",
115
+ phase: payload?.phase || "unknown",
116
+ status: payload?.blockingEdge ? "blocked" : "running",
117
+ blocking: payload?.blockingEdge || null,
118
+ attempt: payload?.activeAttempt?.attemptNumber || 0,
119
+ version: 0,
120
+ shouldWake: agentId ? true : null,
121
+ targetAgentIds: agentId ? [agentId] : [],
122
+ };
123
+ const targetKey = agentId ? "agent" : "agents";
124
+ const targetValue = agentId || (effective.targetAgentIds || []).join(",") || "none";
125
+ const shouldWake =
126
+ typeof effective.shouldWake === "boolean" ? (effective.shouldWake ? "yes" : "no") : "n/a";
127
+ console.log(
128
+ [
129
+ `signal=${effective.signal || "waiting"}`,
130
+ `lane=${lane}`,
131
+ `wave=${wave}`,
132
+ `phase=${effective.phase || "unknown"}`,
133
+ `status=${effective.status || "running"}`,
134
+ `blocking=${effective?.blocking?.kind || "none"}`,
135
+ `attempt=${effective.attempt || 0}`,
136
+ `${targetKey}=${targetValue}`,
137
+ `version=${effective.version || 0}`,
138
+ `should_wake=${shouldWake}`,
139
+ ].join(" "),
140
+ );
141
+ NODE
142
+ }
143
+
144
+ exit_code_for_payload() {
145
+ local payload="$1"
146
+ PAYLOAD="$payload" node - "$agent" <<'NODE'
147
+ const agentId = String(process.argv[2] || "").trim();
148
+ const payload = JSON.parse(process.env.PAYLOAD || "{}");
149
+ const signals = payload?.signals || {};
150
+ const snapshot = agentId
151
+ ? (Array.isArray(signals.agents) ? signals.agents.find((entry) => entry.agentId === agentId) : null)
152
+ : signals.wave;
153
+ const signal = String(snapshot?.signal || "").trim().toLowerCase();
154
+ if (signal === "completed") {
155
+ process.exit(0);
156
+ }
157
+ if (signal === "failed") {
158
+ process.exit(40);
159
+ }
160
+ if (signal === "feedback-requested") {
161
+ process.exit(20);
162
+ }
163
+ process.exit(10);
164
+ NODE
165
+ }
166
+
167
+ payload="$("$status_script" "${common_args[@]}" --json)"
168
+ version="$(extract_field "$payload" "version")"
169
+ print_line "$payload"
170
+
171
+ if exit_code_for_payload "$payload"; then
172
+ exit 0
173
+ else
174
+ status=$?
175
+ if [ "$status" -eq 20 ] || [ "$status" -eq 40 ]; then
176
+ exit "$status"
177
+ fi
178
+ fi
179
+
180
+ while true; do
181
+ sleep "$(awk "BEGIN { printf \"%.3f\", ${refresh_ms}/1000 }")"
182
+ payload="$("$status_script" "${common_args[@]}" --json)"
183
+ next_version="$(extract_field "$payload" "version")"
184
+ if [ "$next_version" = "$version" ]; then
185
+ continue
186
+ fi
187
+ version="$next_version"
188
+ print_line "$payload"
189
+ if exit_code_for_payload "$payload"; then
190
+ exit 0
191
+ else
192
+ status=$?
193
+ if [ "$status" -eq 20 ] || [ "$status" -eq 40 ]; then
194
+ exit "$status"
195
+ fi
196
+ fi
197
+ if [ "$mode" = "until-change" ]; then
198
+ exit 30
199
+ fi
200
+ done
package/skills/README.md CHANGED
@@ -180,6 +180,7 @@ Runtime:
180
180
  Design reference:
181
181
 
182
182
  - `tui-design`
183
+ - `signal-hygiene`
183
184
 
184
185
  Provider:
185
186
 
@@ -205,6 +206,8 @@ Provider skills are configured by deploy kind, but the shipped manifests further
205
206
 
206
207
  For terminal or operator-surface design work, keep `role-design` as the packet contract and add `tui-design` explicitly in the wave's `### Skills`.
207
208
 
209
+ For long-running watcher agents, add `signal-hygiene` explicitly in the wave's `### Skills`. Do not attach it to normal one-shot implementation agents. For the wrapper and ack-loop contract, read [../docs/guides/signal-wrappers.md](../docs/guides/signal-wrappers.md).
210
+
208
211
  ## Further Reading
209
212
 
210
213
  - [Skills Reference](../docs/reference/skills.md)
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: signal-hygiene
3
+ description: Use for long-running resident or waiting agents that must stay idle until the orchestrator writes a new signal version, then acknowledge that change before acting.
4
+ ---
5
+
6
+ # Signal Hygiene
7
+
8
+ Use this skill only when the agent is intentionally long-running.
9
+
10
+ This is not a generic polling skill for normal one-shot implementation work.
11
+
12
+ ## Core loop
13
+
14
+ - Treat the signal state file as the orchestrator-controlled wakeup surface.
15
+ - Treat the signal ack file as your durable confirmation that you observed a specific signal version.
16
+ - Stay idle while the signal version is unchanged.
17
+ - Act once when the signal version increases and the new signal is actionable.
18
+
19
+ ## Required behavior
20
+
21
+ - Read the signal state path provided in the prompt before deciding whether to keep waiting or resume work.
22
+ - If the signal file is missing, assume the orchestrator has not published a new signal yet and keep waiting.
23
+ - Compare the signal file's `version` to the version already recorded in the signal ack file.
24
+ - When the signal version increases, write the ack file immediately before you act on the change.
25
+ - Write the ack file as JSON with exactly these keys:
26
+ - `agentId`
27
+ - `version`
28
+ - `signal`
29
+ - `observedAt`
30
+ - After acknowledging the new version, re-read the inbox, shared summary, message board, and any explicitly referenced artifacts before taking action.
31
+ - If the signal kind is `completed` or `failed`, stop the waiting loop and finish cleanly.
32
+
33
+ ## Do not do this
34
+
35
+ - Do not busy-loop or emit repeated status chatter while the signal version is unchanged.
36
+ - Do not keep re-processing the same signal version.
37
+ - Do not invent your own wakeup surface when the orchestrator already provided signal and ack paths.
38
+ - Do not stay resident forever once the signal clearly becomes terminal.
39
+
40
+ ## Actionability rule
41
+
42
+ Treat these signal kinds as actionable by default:
43
+
44
+ - `feedback-requested`
45
+ - `feedback-answered`
46
+ - `coordination-action`
47
+ - `resume-ready`
48
+ - `completed`
49
+ - `failed`
50
+
51
+ Treat `waiting` and `stable` as non-actionable until the version changes again.
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "signal-hygiene",
3
+ "title": "Long-Running Signal Hygiene",
4
+ "description": "Use for long-running resident or waiting agents that must stay idle until the orchestrator writes a new signal version, then acknowledge that change before acting.",
5
+ "activation": {
6
+ "when": "Attach explicitly when an agent is expected to stay resident, wait for human feedback or coordination changes, and react only when the orchestrator updates the signal state file.",
7
+ "roles": [],
8
+ "runtimes": [],
9
+ "deployKinds": []
10
+ },
11
+ "termination": "Stop when the signal reaches a terminal state or the agent is no longer expected to stay resident and wait for follow-up work.",
12
+ "permissions": {
13
+ "network": [],
14
+ "shell": [],
15
+ "mcpServers": []
16
+ },
17
+ "trust": {
18
+ "tier": "repo-owned"
19
+ }
20
+ }