@amsterdamdatalabs/enact-extensions 0.1.34 → 0.1.37
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.
|
@@ -36,11 +36,13 @@ cmux tree # process/Surface tree (optional)
|
|
|
36
36
|
# Read a follower
|
|
37
37
|
cmux read-screen --surface surface:N [--lines 40] [--scrollback]
|
|
38
38
|
|
|
39
|
-
# Drive a follower (
|
|
39
|
+
# Drive a follower (send → 2×Enter → ctrl+b → verify EMPTY box)
|
|
40
40
|
cmux send --surface surface:N "text\n"
|
|
41
|
-
cmux send-key --surface surface:N enter #
|
|
41
|
+
cmux send-key --surface surface:N enter # Enter #1
|
|
42
|
+
cmux send-key --surface surface:N enter # Enter #2 — one Enter often does NOT land
|
|
43
|
+
cmux send-key --surface surface:N ctrl+b # flush a QUEUED msg — no-op while only thinking
|
|
44
|
+
cmux read-screen --surface surface:N --lines 6 # confirm the ❯ box is EMPTY (it landed)
|
|
42
45
|
cmux send-key --surface surface:N ctrl+c # interrupt a runaway
|
|
43
|
-
cmux send-key --surface surface:N ctrl+b # flush a QUEUED msg — only if a command is running
|
|
44
46
|
|
|
45
47
|
# Spawn a follower (root it correctly, THEN launch claude)
|
|
46
48
|
cmux new-split right --focus false # -> surface:N
|
|
@@ -56,8 +58,9 @@ cmux close-surface --surface surface:N
|
|
|
56
58
|
|
|
57
59
|
Every follower briefing must tell the follower to use the **cmux-send** skill. Each message: prefix
|
|
58
60
|
`[LEADER] to [FOLLOWER-<name>] `, end with `Respond back once you are done with using prefix
|
|
59
|
-
"[FOLLOWER-<name>] to [LEADER] "`. After every `send`, `
|
|
60
|
-
|
|
61
|
+
"[FOLLOWER-<name>] to [LEADER] "`. After every `send`, fire **two** `send-key … enter` (one Enter
|
|
62
|
+
often does not land) + one `ctrl+b`, then `read-screen` and confirm the `❯` box is **empty** — an
|
|
63
|
+
empty inbox is the only proof it submitted.
|
|
61
64
|
|
|
62
65
|
## Leader loop
|
|
63
66
|
|
|
@@ -65,6 +68,7 @@ parked in the `❯` box, `send-key … enter` (NOT `ctrl+b`).
|
|
|
65
68
|
2. **Spawn & brief** — split → `cd <repo>` (verify pwd) → `claude` → self-contained briefing (role,
|
|
66
69
|
scope, exit criteria, "use cmux-send to reply, report in one line").
|
|
67
70
|
3. **Poll on a ~15-min cadence** — never go idle while followers run. Re-discover topology, `read-screen`
|
|
68
|
-
each, classify (working / idle-done / parked / blocked / ctx≥80%), and act: parked → `send-key enter
|
|
69
|
-
ctx≥80% → tell it to `/compact`; blocked → answer;
|
|
71
|
+
each, classify (working / idle-done / parked / blocked / ctx≥80%), and act: parked → `send-key enter`
|
|
72
|
+
×2 + `ctrl+b`, then verify the `❯` box is empty; ctx≥80% → tell it to `/compact`; blocked → answer;
|
|
73
|
+
idle-done → assign next or `close-surface`.
|
|
70
74
|
4. **Verify & gate** — confirm real output before treating a slice as green; drive merges from the leader.
|
|
@@ -7,19 +7,27 @@ description: "How to correctly message a cmux surface — the send→verify→en
|
|
|
7
7
|
|
|
8
8
|
Two parts are easy to get wrong: **submitting** the message and **wrapping** it.
|
|
9
9
|
|
|
10
|
-
## 1. Submit protocol (
|
|
10
|
+
## 1. Submit protocol (send → 2×Enter → ctrl+b → verify empty box)
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
cmux send --surface surface:N "<one-line message>\n" # \n = Enter
|
|
14
|
-
cmux
|
|
15
|
-
cmux send-key --surface surface:N enter #
|
|
13
|
+
cmux send --surface surface:N "<one-line message>\n" # \n = Enter (often not enough alone)
|
|
14
|
+
cmux send-key --surface surface:N enter # Enter #1
|
|
15
|
+
cmux send-key --surface surface:N enter # Enter #2 — one Enter frequently does NOT land
|
|
16
|
+
cmux send-key --surface surface:N ctrl+b # flush if a bash command is running
|
|
17
|
+
cmux read-screen --surface surface:N --lines 6 # VERIFY: the ❯ box is EMPTY (message left)
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
- **
|
|
19
|
-
thinking — the text parks in the `❯` box.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`ctrl+b`
|
|
20
|
+
- **Send TWO Enter keys, not one.** A single `send-key … enter` (or the trailing `\n`) frequently
|
|
21
|
+
fails to submit while the follower is booting or thinking — the text parks in the `❯` box. Two
|
|
22
|
+
Enters is the reliable default; a leftover empty Enter is harmless.
|
|
23
|
+
- **Then one `ctrl+b`.** It flushes a *queued* message only when the follower has a **running bash
|
|
24
|
+
command** to background; while it is purely thinking `ctrl+b` is a harmless no-op. Sending it
|
|
25
|
+
unconditionally costs nothing and frees the queue the moment a command is running.
|
|
26
|
+
- **Always confirm an EMPTY inbox.** `read-screen` after submitting and check the `❯` box is clear
|
|
27
|
+
(no parked text, no "Press up to edit queued messages"). An empty box is the only proof it landed —
|
|
28
|
+
do not assume submission, verify it.
|
|
29
|
+
- **To submit a parked message, `send-key … enter` (×2), NOT `ctrl+b` alone.** `ctrl+b` does nothing
|
|
30
|
+
while the follower is only thinking.
|
|
23
31
|
- **One line only.** Every literal `\n` submits, so a multi-line prompt fires early and fragments.
|
|
24
32
|
- `read-screen` takes `--surface`, not `--pane`.
|
|
25
33
|
|
|
@@ -35,6 +43,8 @@ with `cmux list-pane-surfaces [--pane pane:N]`.
|
|
|
35
43
|
|
|
36
44
|
```bash
|
|
37
45
|
cmux send --surface surface:3 "[LEADER] to [FOLLOWER-enact-wiki] <instruction on one line>. Respond back once you are done with using prefix \"[FOLLOWER-enact-wiki] to [LEADER] \"\n"
|
|
38
|
-
cmux
|
|
39
|
-
|
|
46
|
+
cmux send-key --surface surface:3 enter # ×2 — one Enter often does not land
|
|
47
|
+
cmux send-key --surface surface:3 enter
|
|
48
|
+
cmux send-key --surface surface:3 ctrl+b # flush if a command is running (no-op while thinking)
|
|
49
|
+
cmux read-screen --surface surface:3 --lines 6 # confirm the ❯ box is EMPTY
|
|
40
50
|
```
|
package/package.json
CHANGED
|
@@ -938,6 +938,8 @@ if (command === "serve") {
|
|
|
938
938
|
// Reads a UserPromptSubmit hook payload from stdin, extracts the prompt,
|
|
939
939
|
// builds the skill catalog from the bundled extensions dir, routes the
|
|
940
940
|
// prompt, and writes the hook response to stdout.
|
|
941
|
+
// Set ENACT_EXTENSIONS_UPS_ROUTER=0 to disable this hook locally without
|
|
942
|
+
// removing the installed plugin.
|
|
941
943
|
//
|
|
942
944
|
// Output contract (Claude Code hooks):
|
|
943
945
|
// Match: { continue: true, hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: <string> } }
|
|
@@ -973,6 +975,11 @@ if (command === "hook") {
|
|
|
973
975
|
process.exit(0);
|
|
974
976
|
}
|
|
975
977
|
|
|
978
|
+
if (process.env.ENACT_EXTENSIONS_UPS_ROUTER === "0") {
|
|
979
|
+
process.stdout.write(JSON.stringify({ continue: true }) + "\n");
|
|
980
|
+
process.exit(0);
|
|
981
|
+
}
|
|
982
|
+
|
|
976
983
|
// Read stdin to get the hook payload JSON.
|
|
977
984
|
let payload;
|
|
978
985
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ups-router.mjs — pure, testable module for the UserPromptSubmit keyword-router.
|
|
3
3
|
*
|
|
4
|
-
* Discovers plugin bundles and their skills, extracts trigger
|
|
5
|
-
* SKILL.md frontmatter, and routes a user prompt to matching skills.
|
|
4
|
+
* Discovers plugin bundles and their skills, extracts explicit trigger phrases
|
|
5
|
+
* from SKILL.md frontmatter, and routes a user prompt to matching skills.
|
|
6
6
|
*
|
|
7
7
|
* Two exports:
|
|
8
8
|
* buildSkillCatalog(extensionsDir) → CatalogEntry[]
|
|
@@ -147,49 +147,11 @@ function extractExplicitTriggers(description) {
|
|
|
147
147
|
return quoted;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
/**
|
|
151
|
-
* Derive salient keywords from a skill's description text.
|
|
152
|
-
*
|
|
153
|
-
* Conservative: only extracts tokens that are plausibly skill identifiers:
|
|
154
|
-
* - Hyphenated compound tokens (e.g. "contract-runner", "test-driven")
|
|
155
|
-
* - $skill-name mentions (e.g. "$loop", "$tdd")
|
|
156
|
-
* - Quoted short phrases other than the "Triggers on" section
|
|
157
|
-
* - Unquoted tokens that are longer (>=6 chars) and not stop-words
|
|
158
|
-
*
|
|
159
|
-
* @param {string} description
|
|
160
|
-
* @returns {string[]}
|
|
161
|
-
*/
|
|
162
|
-
function descriptionKeywords(description) {
|
|
163
|
-
const tokens = new Set();
|
|
164
|
-
|
|
165
|
-
// $skill mentions: "$foo", "$foo-bar"
|
|
166
|
-
const dollarRe = /\$([a-z][a-z0-9-]+)/gi;
|
|
167
|
-
let m;
|
|
168
|
-
while ((m = dollarRe.exec(description)) !== null) {
|
|
169
|
-
tokens.add(m[1].toLowerCase());
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Hyphenated compound tokens: at least two segments, each 2+ chars
|
|
173
|
-
const hyphenRe = /\b([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+)\b/gi;
|
|
174
|
-
while ((m = hyphenRe.exec(description)) !== null) {
|
|
175
|
-
const tok = m[1].toLowerCase();
|
|
176
|
-
if (!isStopWord(tok)) tokens.add(tok);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Longer bare words (>=6 chars) that don't look like common English prose.
|
|
180
|
-
const longWordRe = /\b([a-z]{6,})\b/gi;
|
|
181
|
-
while ((m = longWordRe.exec(description)) !== null) {
|
|
182
|
-
const tok = m[1].toLowerCase();
|
|
183
|
-
if (!isStopWord(tok)) tokens.add(tok);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return [...tokens];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
150
|
/**
|
|
190
151
|
* Derive the full keyword set for a single skill.
|
|
191
|
-
*
|
|
192
|
-
*
|
|
152
|
+
* Only explicit `Triggers on "..."` phrases and the skill name are used.
|
|
153
|
+
* Do not mine arbitrary words from descriptions; that makes everyday prompts
|
|
154
|
+
* like "change this contract" trigger unrelated skill hints.
|
|
193
155
|
*
|
|
194
156
|
* @param {string} skillName
|
|
195
157
|
* @param {string|null} description
|
|
@@ -208,8 +170,6 @@ function deriveKeywords(skillName, description) {
|
|
|
208
170
|
if (description) {
|
|
209
171
|
// Explicit triggers have highest priority.
|
|
210
172
|
for (const t of extractExplicitTriggers(description)) push(t);
|
|
211
|
-
// Salient terms from the description.
|
|
212
|
-
for (const t of descriptionKeywords(description)) push(t);
|
|
213
173
|
}
|
|
214
174
|
|
|
215
175
|
return [...seen];
|