squadrant 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.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/index.js +9154 -0
- package/dist/index.js.map +1 -0
- package/dist/squadrantd.js +3928 -0
- package/dist/squadrantd.js.map +1 -0
- package/package.json +68 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/skills/add-pick-crew-rule/SKILL.md +88 -0
- package/plugin/skills/captain-ops/SKILL.md +390 -0
- package/plugin/skills/command-ops/SKILL.md +157 -0
- package/plugin/skills/config-doctor/SKILL.md +46 -0
- package/plugin/skills/daily-log/SKILL.md +44 -0
- package/plugin/skills/karpathy-principles/SKILL.md +82 -0
- package/plugin/skills/set-effort/SKILL.md +59 -0
- package/plugin/skills/side-session/SKILL.md +113 -0
- package/plugin/skills/squadrant-effort/SKILL.md +8 -0
- package/plugin/skills/squadrant-new-project/SKILL.md +67 -0
- package/plugin/skills/squadrant-register-project/SKILL.md +60 -0
- package/plugin/skills/where-i-am/SKILL.md +102 -0
- package/plugin/skills/wiki-ops/SKILL.md +96 -0
- package/plugin/skills/wim/SKILL.md +8 -0
- package/scripts/acceptance-interactive-codex.sh +56 -0
- package/scripts/capture-skill.sh +32 -0
- package/scripts/claude-iv-smoke.mjs +133 -0
- package/scripts/fix-skill.sh +39 -0
- package/scripts/gen-codex-types.sh +18 -0
- package/scripts/mailbox-injector-smoke.mjs +124 -0
- package/scripts/mark-learning-useful.sh +22 -0
- package/scripts/migrate-to-squadrant.sh +158 -0
- package/scripts/notify-relay-placement-smoke.mjs +59 -0
- package/scripts/read-handoff.sh +22 -0
- package/scripts/record-learning.sh +31 -0
- package/scripts/record-side-handoff.sh +32 -0
- package/scripts/smoke-push-notify.mjs +147 -0
- package/scripts/spawn-crew-pane.sh +17 -0
- package/scripts/spawn-workspace.sh +206 -0
- package/scripts/wiki-ingest.sh +121 -0
- package/scripts/wiki-log.sh +20 -0
- package/scripts/wiki-query.sh +36 -0
- package/scripts/write-handoff.sh +31 -0
- package/templates/captain.claude.md +44 -0
- package/templates/captain.generic.md +45 -0
- package/templates/command.claude.md +32 -0
- package/templates/crew.claude.md +64 -0
- package/templates/crew.generic.md +51 -0
- package/templates/crew.opencode.md +51 -0
- package/templates/learnings.claude.md +40 -0
- package/templates/side.debug.claude.md +78 -0
- package/templates/side.research.claude.md +63 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: where-i-am
|
|
3
|
+
description: Print a tight "where am I on this project?" orientation report — a "right now" punchline plus Done (and what it means) / In progress / Next / Watch. Use when context-switching into a long-running project, resuming after a compact, or whenever you ask "where was I / what's the status / /wim / /where-i-am".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Where I Am
|
|
7
|
+
|
|
8
|
+
Answer **"where am I on this project?"** in a short, scannable report instead of a wall of text. Built for switching between long-running projects without losing the thread.
|
|
9
|
+
|
|
10
|
+
Read-only. This skill **writes nothing** and **consumes nothing** — pure orientation.
|
|
11
|
+
|
|
12
|
+
## Output shape
|
|
13
|
+
|
|
14
|
+
Open with a one-line punchline, then the four sections in order:
|
|
15
|
+
|
|
16
|
+
```markdown
|
|
17
|
+
**Right now →** <what you're actively doing / waiting on this moment, and your immediate next move>
|
|
18
|
+
|
|
19
|
+
## ✅ Done — and what it means
|
|
20
|
+
- <completed work> → <why it matters / what it unblocks>
|
|
21
|
+
|
|
22
|
+
## ⏳ In progress
|
|
23
|
+
- <work actively moving> — <state / owner: crew fixing, waiting on CI, mid-refactor>
|
|
24
|
+
|
|
25
|
+
## ▶️ Next
|
|
26
|
+
- ⬜ <the next not-yet-started step(s)>
|
|
27
|
+
|
|
28
|
+
## ⚠️ Watch
|
|
29
|
+
- <blockers, fragile state, gotchas, "don't forget", contradictions between sources>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Status markers — use inline when listing the steps of a track: ✅ done · ⏳ in progress · ⬜ not started.
|
|
33
|
+
|
|
34
|
+
Rules for the report:
|
|
35
|
+
- **`Right now →` is the most important line** — the fast-orientation cue. Always include it; one line.
|
|
36
|
+
- **Bullets, not paragraphs.** A handful per section, most-relevant first.
|
|
37
|
+
- Each **Done** bullet pairs *what happened* with *why it matters* (`→`). A commit without significance is noise.
|
|
38
|
+
- **In progress vs Next:** ⏳ is work already moving (a crew is on it, a branch is open, you're mid-edit); ⬜ Next is not yet started. Don't conflate them — the whole point is knowing what's live vs queued.
|
|
39
|
+
- **Multi-track projects** (e.g. a feature track + a bugfix track running in parallel): group bullets under bold track labels (`**Reorg track:**`, `**Bug-fix track:**`) inside the sections.
|
|
40
|
+
- If a section is genuinely empty, write `- — nothing` rather than padding.
|
|
41
|
+
- Synthesize, don't dump. Never paste raw git log / observation lists — distill them.
|
|
42
|
+
|
|
43
|
+
## Source priority (current → durable)
|
|
44
|
+
|
|
45
|
+
Lead with the live session; use durable sources to fill gaps and cross-check.
|
|
46
|
+
|
|
47
|
+
1. **Current session context — primary.** What *this* conversation has done, decided, and left open. Freshest signal, especially mid-task.
|
|
48
|
+
2. **claude-mem recent observations** — the narrative across prior sessions (use the `mem-search` skill, or the recent-context already injected at session start).
|
|
49
|
+
3. **Handoff + latest daily-log** — explicit `nextSteps` / `blockedItems` / `decisions` and `Tomorrow` / `Blocked`.
|
|
50
|
+
4. **git** — ground truth of where the code actually sits.
|
|
51
|
+
|
|
52
|
+
When the session is thin (e.g. right after a compact / brand-new session), lean harder on 2–4. When sources **disagree** — git shows work the session doesn't mention, or claude-mem says a thing shipped that the branch contradicts — that contradiction is a **⚠️ Watch** item, not noise.
|
|
53
|
+
|
|
54
|
+
## How to build it
|
|
55
|
+
|
|
56
|
+
**1. Resolve the current project** (degrade gracefully if not a squadrant project):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
PROJECT_JSON=$(node -e '
|
|
60
|
+
const fs=require("fs"),os=require("os"),path=require("path");
|
|
61
|
+
const cfg=JSON.parse(fs.readFileSync(os.homedir()+"/.config/squadrant/config.json","utf8"));
|
|
62
|
+
const cwd=process.cwd();
|
|
63
|
+
let best=null;
|
|
64
|
+
for (const [name,p] of Object.entries(cfg.projects||{})) {
|
|
65
|
+
if (cwd===p.path || cwd.startsWith(p.path+"/")) {
|
|
66
|
+
if (!best || p.path.length>best.path.length) best={name,...p};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write(JSON.stringify(best||{}));
|
|
70
|
+
' 2>/dev/null)
|
|
71
|
+
echo "$PROJECT_JSON"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If empty `{}`: not inside a known squadrant project — build the report from **session context + git only**, and skip steps 2–3 below.
|
|
75
|
+
|
|
76
|
+
Otherwise note `name` and `spokeVault` for the next steps.
|
|
77
|
+
|
|
78
|
+
**2. Read the handoff WITHOUT consuming it** (note the `--keep` — never drop the `--keep`, or you destroy the next session's startup context):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
~/.config/squadrant/scripts/read-handoff.sh "<spokeVault>" --keep
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`{"exists": false}` means none — fine, skip it.
|
|
85
|
+
|
|
86
|
+
**3. Read the latest daily-log** (if any):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ls -t "<spokeVault>"/daily-logs/*.md 2>/dev/null | head -1
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Read that one file for `Completed` / `In Progress` / `Blocked` / `Tomorrow`.
|
|
93
|
+
|
|
94
|
+
**4. Read git ground-truth:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git -C "<project path or PWD>" status -sb && echo "---" && git -C "<project path or PWD>" log --oneline -8
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**5. claude-mem** — if recent observations weren't already injected this session, pull them with the `mem-search` skill scoped to the project name.
|
|
101
|
+
|
|
102
|
+
**6. Synthesize** all of the above into the `Right now →` line plus the four sections and print. Lead with the session, resolve disagreements into **⚠️ Watch**. Stop there — do not start acting on "Next" unless the user asks.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki-ops
|
|
3
|
+
description: Compile discovered knowledge into persistent, cross-referenced wiki pages in spoke vaults. Use after learning something notable, at task completion, and during session shutdown.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wiki Operations
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The wiki is your project's compiled knowledge base. Unlike learnings (individual observations, possibly ephemeral), wiki pages are **persistent, cross-referenced, and indexed**.
|
|
11
|
+
|
|
12
|
+
- **Learnings** = raw observations ("I found that X causes Y")
|
|
13
|
+
- **Wiki pages** = compiled knowledge ("How X works", "Architecture of Y", "Patterns for Z")
|
|
14
|
+
|
|
15
|
+
## When to Ingest
|
|
16
|
+
|
|
17
|
+
1. **After task completion** — if you discovered how a system works, document it
|
|
18
|
+
2. **After resolving a tricky bug** — document the root cause and fix pattern
|
|
19
|
+
3. **When a learning is marked useful 2+ times** — promote it to a wiki page
|
|
20
|
+
4. **During session shutdown** — review what you learned, compile if notable
|
|
21
|
+
5. **When you notice a gap** — if you searched the wiki and didn't find what you needed, create the page after you find the answer
|
|
22
|
+
|
|
23
|
+
## Creating/Updating a Wiki Page
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
~/.config/squadrant/scripts/wiki-ingest.sh "{spokeVaultPath}" "{slug}" "{title}" "{category}" "{body}" "{tags}" "{source}"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Parameters:**
|
|
30
|
+
- `slug`: URL-friendly name (e.g., `auth-flow`, `cairo-contract-patterns`)
|
|
31
|
+
- `title`: Human-readable title
|
|
32
|
+
- `category`: One of: `Architecture`, `Patterns`, `APIs`, `Configuration`, `Debugging`, `Conventions`, `Dependencies`, `Deployment`
|
|
33
|
+
- `body`: Full markdown content (can be multi-paragraph)
|
|
34
|
+
- `tags`: Comma-separated keywords
|
|
35
|
+
- `source`: How this knowledge was discovered (e.g., "crew debugging issue #42")
|
|
36
|
+
|
|
37
|
+
**Example:**
|
|
38
|
+
```bash
|
|
39
|
+
~/.config/squadrant/scripts/wiki-ingest.sh "/path/to/spoke" "starknet-account-deploy" "StarkNet Account Deployment" "Patterns" "Account deployment on StarkNet requires a two-step process:
|
|
40
|
+
|
|
41
|
+
1. Compute the address from the class hash and constructor args
|
|
42
|
+
2. Fund the computed address with ETH
|
|
43
|
+
3. Call deploy_account
|
|
44
|
+
|
|
45
|
+
Common pitfall: the salt must match between compute and deploy." "starknet,deployment,account" "crew debugging deploy failures"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Querying the Wiki
|
|
49
|
+
|
|
50
|
+
Before starting a new task, check if the wiki has relevant knowledge:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
~/.config/squadrant/scripts/wiki-query.sh "{spokeVaultPath}" "{keyword}"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a quick overview:
|
|
57
|
+
```bash
|
|
58
|
+
~/.config/squadrant/scripts/wiki-query.sh "{spokeVaultPath}" "{keyword}" --titles-only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
To browse the full index:
|
|
62
|
+
```bash
|
|
63
|
+
cat "{spokeVaultPath}/wiki/index.md"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Viewing Recent Changes
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
~/.config/squadrant/scripts/wiki-log.sh "{spokeVaultPath}" 10
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Promoting Learnings to Wiki
|
|
73
|
+
|
|
74
|
+
When reviewing learnings and you find one that's been useful multiple times:
|
|
75
|
+
|
|
76
|
+
1. Read the learning file
|
|
77
|
+
2. Expand the observation into a full wiki page with context, examples, and related links
|
|
78
|
+
3. Ingest via wiki-ingest.sh
|
|
79
|
+
4. The original learning remains (it's the "source" reference)
|
|
80
|
+
|
|
81
|
+
## Cross-Referencing
|
|
82
|
+
|
|
83
|
+
When writing wiki page body content, reference related pages using `[[slug]]` syntax:
|
|
84
|
+
```
|
|
85
|
+
See also [[auth-flow]] for the authentication architecture.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
After ingesting, check if existing pages should reference the new one.
|
|
89
|
+
|
|
90
|
+
## Quality Guidelines
|
|
91
|
+
|
|
92
|
+
- **Be specific**: "StarkNet account deployment requires 3 steps" > "Deployment is complex"
|
|
93
|
+
- **Include examples**: Code snippets, command sequences, config fragments
|
|
94
|
+
- **Note caveats**: Version-specific behavior, known limitations
|
|
95
|
+
- **Cite sources**: Which task/issue/exploration revealed this knowledge
|
|
96
|
+
- **Keep pages focused**: One concept per page, link to related pages
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wim
|
|
3
|
+
description: Shortcut for /where-i-am — print the tight "where am I on this project?" orientation report (Done / Next / Watch). Use when the user types /wim or asks where they are on the project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wim — shortcut for /where-i-am
|
|
7
|
+
|
|
8
|
+
Shorthand alias. **Invoke the `where-i-am` skill** (via the Skill tool) and follow it exactly. All logic lives there — this file intentionally holds none, so the two never drift.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Manual acceptance walk-through for spec §4.10 (squadrant interactive codex).
|
|
3
|
+
# Run from the repo root after `npm run build`. Drives the full end-to-end
|
|
4
|
+
# flow including a daemon-bounce reattach.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PROJECT="${PROJECT:-SMOKE}"
|
|
8
|
+
CWD="${CWD:-$(mktemp -d -t squadrant-iv-codex-XXXXXX)}"
|
|
9
|
+
|
|
10
|
+
echo "=== squadrant interactive-codex acceptance (spec §4.10) ==="
|
|
11
|
+
echo "Project: $PROJECT cwd: $CWD"
|
|
12
|
+
echo
|
|
13
|
+
|
|
14
|
+
echo "=== 1. fresh build + relink ==="
|
|
15
|
+
npm run build
|
|
16
|
+
echo " build OK"
|
|
17
|
+
echo
|
|
18
|
+
|
|
19
|
+
echo "=== 2. open a chat ==="
|
|
20
|
+
echo " squadrant crew chat --provider codex --project $PROJECT --cwd $CWD"
|
|
21
|
+
echo
|
|
22
|
+
echo "Manually, in the opened cmux workspace tab:"
|
|
23
|
+
echo " a. type: please write the string ACCEPT-OK to a file named demo.txt in the cwd"
|
|
24
|
+
echo " b. when the [approval] prompt appears, answer with: approve"
|
|
25
|
+
echo " c. wait for [done — type a follow-up or Ctrl-C], then type: thanks"
|
|
26
|
+
echo " d. observe a second [done] line"
|
|
27
|
+
echo " e. type: please run \"ls\" in this directory"
|
|
28
|
+
echo " f. press Ctrl-C mid-turn to verify SIGINT → turn/interrupt round-trips"
|
|
29
|
+
echo
|
|
30
|
+
echo "Verify in the host shell:"
|
|
31
|
+
echo " squadrant crew status $PROJECT <task-id> # state is 'awaiting-input' (NOT 'done') after each turn"
|
|
32
|
+
echo " cat $CWD/demo.txt # contains ACCEPT-OK"
|
|
33
|
+
echo
|
|
34
|
+
|
|
35
|
+
echo "=== 3. simulate daemon bounce in another shell ==="
|
|
36
|
+
echo " launchctl kickstart -kp gui/\$(id -u)/com.squadrant.daemon"
|
|
37
|
+
echo
|
|
38
|
+
echo "Then check the cmux tab: expect a '(attached)' line within a few seconds"
|
|
39
|
+
echo "and that the next 'say' continues on the SAME thread (codex remembers the"
|
|
40
|
+
echo "earlier file write and 'thanks' exchange). This proves spec §5 / closes the"
|
|
41
|
+
echo "interactive-codex slice of #86."
|
|
42
|
+
echo
|
|
43
|
+
|
|
44
|
+
echo "=== 4. gate promotion (HITL slice, spec §4.9) ==="
|
|
45
|
+
echo "Open a new chat with --provider codex; ask codex to write a file in a path"
|
|
46
|
+
echo "that needs approval, then IMMEDIATELY close the cmux tab without answering."
|
|
47
|
+
echo "Wait 6 seconds, then run:"
|
|
48
|
+
echo " squadrant crew status $PROJECT <task-id>"
|
|
49
|
+
echo "Expect 'gates' array containing one pending gate."
|
|
50
|
+
echo "Resolve from the Captain:"
|
|
51
|
+
echo " squadrant crew reply $PROJECT <task-id> approve --gate <gateId>"
|
|
52
|
+
echo "Re-attach to verify the turn completes:"
|
|
53
|
+
echo " squadrant crew attach <task-id>"
|
|
54
|
+
echo
|
|
55
|
+
|
|
56
|
+
echo "=== done — capture output to /tmp/iv-codex-acceptance.txt for the PR ==="
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Usage: capture-skill.sh <spoke-vault-path> <skill-name> <description> <body>
|
|
3
|
+
# Captures a successful task pattern as a reusable skill (CAPTURED evolution)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
VAULT="${1:?Usage: capture-skill.sh <vault-path> <skill-name> <description> <body>}"
|
|
6
|
+
SKILL_NAME="${2:?}"
|
|
7
|
+
DESCRIPTION="${3:?}"
|
|
8
|
+
BODY="${4:?}"
|
|
9
|
+
DATE=$(date +"%Y-%m-%d")
|
|
10
|
+
SKILL_DIR="${VAULT}/skills/${SKILL_NAME}"
|
|
11
|
+
SKILL_FILE="${SKILL_DIR}/SKILL.md"
|
|
12
|
+
|
|
13
|
+
mkdir -p "${SKILL_DIR}"
|
|
14
|
+
|
|
15
|
+
if [ -f "$SKILL_FILE" ]; then
|
|
16
|
+
echo "Skill '${SKILL_NAME}' already exists at ${SKILL_FILE} — use fix-skill.sh to update it"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
cat > "$SKILL_FILE" << EOF
|
|
21
|
+
---
|
|
22
|
+
name: ${SKILL_NAME}
|
|
23
|
+
description: ${DESCRIPTION}
|
|
24
|
+
captured: ${DATE}
|
|
25
|
+
origin: auto-captured
|
|
26
|
+
times_used: 0
|
|
27
|
+
times_successful: 0
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
${BODY}
|
|
31
|
+
EOF
|
|
32
|
+
echo "Captured skill: $SKILL_FILE"
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Daemon-level smoke for the claude interactive control-plane wiring.
|
|
3
|
+
//
|
|
4
|
+
// Runs against a PRIVATE temp-socket squadrantd started from the freshly-built
|
|
5
|
+
// dist code in this branch — does NOT touch the shared system daemon (which
|
|
6
|
+
// has other captains' tasks in flight; cannot bounce). Does NOT spawn a real
|
|
7
|
+
// Claude process either (the smoke runner is itself a squadrant crew — no
|
|
8
|
+
// nested crew spawns). Posts the same socket frames that runCrewSpawn would
|
|
9
|
+
// post for a real Claude crew and verifies state transitions end-to-end.
|
|
10
|
+
//
|
|
11
|
+
// Invocation: node scripts/claude-iv-smoke.mjs
|
|
12
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { createConnection } from "node:net";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { startSquadrantd } from "../dist/control/squadrantd.js";
|
|
18
|
+
|
|
19
|
+
const PROJECT = "smoke";
|
|
20
|
+
const tmp = mkdtempSync(join(tmpdir(), "claude-iv-smoke-"));
|
|
21
|
+
const SOCK = join(tmp, "c.sock");
|
|
22
|
+
const h = startSquadrantd({ stateRoot: join(tmp, "state"), sockPath: SOCK, sweepMs: 0 });
|
|
23
|
+
|
|
24
|
+
function rpc(req) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const conn = createConnection(SOCK);
|
|
27
|
+
let buf = "";
|
|
28
|
+
conn.setEncoding("utf-8");
|
|
29
|
+
conn.on("connect", () => conn.write(JSON.stringify(req) + "\n"));
|
|
30
|
+
conn.on("data", (c) => {
|
|
31
|
+
buf += c;
|
|
32
|
+
const i = buf.indexOf("\n");
|
|
33
|
+
if (i < 0) return;
|
|
34
|
+
const line = buf.slice(0, i);
|
|
35
|
+
conn.end();
|
|
36
|
+
try {
|
|
37
|
+
const m = JSON.parse(line);
|
|
38
|
+
if (m.ok) resolve(m.reply);
|
|
39
|
+
else reject(new Error(m.error));
|
|
40
|
+
} catch (e) { reject(e); }
|
|
41
|
+
});
|
|
42
|
+
conn.on("error", reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function step(label, fn) {
|
|
47
|
+
process.stdout.write(`\n--- ${label} ---\n`);
|
|
48
|
+
const r = await fn();
|
|
49
|
+
process.stdout.write(JSON.stringify(r, null, 2) + "\n");
|
|
50
|
+
return r;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const taskId = randomUUID();
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
|
|
57
|
+
// 1. Dispatch — daemon stores submitted, launchInteractive emits task.started.
|
|
58
|
+
const dispatched = await step("1. dispatch claude interactive", () =>
|
|
59
|
+
rpc({
|
|
60
|
+
kind: "dispatch",
|
|
61
|
+
record: {
|
|
62
|
+
id: taskId,
|
|
63
|
+
project: PROJECT,
|
|
64
|
+
provider: "claude",
|
|
65
|
+
mode: "interactive",
|
|
66
|
+
state: "submitted",
|
|
67
|
+
task: "smoke test — verify the daemon wires for claude interactive",
|
|
68
|
+
cwd: "/Users/q3labsadmin/me/claude-cockpit",
|
|
69
|
+
createdAt: now,
|
|
70
|
+
lastHeartbeat: now,
|
|
71
|
+
lastEvent: "dispatch",
|
|
72
|
+
heartbeatBudgetMs: 60000,
|
|
73
|
+
attempts: [{ attemptId: randomUUID(), startedAt: now, lastHeartbeatAt: now }],
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
if (dispatched.state !== "submitted")
|
|
78
|
+
throw new Error(`expected state=submitted at dispatch ack, got ${dispatched.state}`);
|
|
79
|
+
|
|
80
|
+
// Brief settle for the async task.started emit.
|
|
81
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
82
|
+
|
|
83
|
+
// 2. Status should show working now (task.started landed).
|
|
84
|
+
const afterStart = await step("2. status after launchInteractive emits task.started", () =>
|
|
85
|
+
rpc({ kind: "status", project: PROJECT, id: taskId }),
|
|
86
|
+
);
|
|
87
|
+
if (afterStart.state !== "working")
|
|
88
|
+
throw new Error(`expected state=working after task.started, got ${afterStart.state}`);
|
|
89
|
+
|
|
90
|
+
// 3. Simulate Stop hook → task.progress (liveness only — anti-#2576).
|
|
91
|
+
const afterProgress = await step("3. POST task.progress (simulating Stop hook from cmux tab)", () =>
|
|
92
|
+
rpc({
|
|
93
|
+
kind: "event",
|
|
94
|
+
project: PROJECT,
|
|
95
|
+
event: { type: "task.progress", id: taskId, note: "stop" },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
if (afterProgress.state !== "working")
|
|
99
|
+
throw new Error(`bare Stop hook MUST NOT advance state (anti-#2576). Got: ${afterProgress.state}`);
|
|
100
|
+
if (afterProgress.lastEvent !== "task.progress")
|
|
101
|
+
throw new Error(`expected lastEvent=task.progress, got ${afterProgress.lastEvent}`);
|
|
102
|
+
|
|
103
|
+
// 4. Simulate explicit `squadrant crew signal done` → task.done with resultRef.
|
|
104
|
+
const afterDone = await step("4. POST task.done (simulating 'squadrant crew signal done')", () =>
|
|
105
|
+
rpc({
|
|
106
|
+
kind: "event",
|
|
107
|
+
project: PROJECT,
|
|
108
|
+
event: { type: "task.done", id: taskId, resultRef: "/tmp/smoke-result.txt" },
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
if (afterDone.state !== "done")
|
|
112
|
+
throw new Error(`expected state=done after explicit signal, got ${afterDone.state}`);
|
|
113
|
+
|
|
114
|
+
// 5. Status read confirms terminal state without any pane scrape.
|
|
115
|
+
const final = await step("5. status read — captain learns 'done' WITHOUT scraping", () =>
|
|
116
|
+
rpc({ kind: "status", project: PROJECT, id: taskId }),
|
|
117
|
+
);
|
|
118
|
+
if (final.state !== "done")
|
|
119
|
+
throw new Error(`final read mismatch: ${final.state}`);
|
|
120
|
+
if (final.resultRef !== "/tmp/smoke-result.txt")
|
|
121
|
+
throw new Error(`final resultRef mismatch: ${final.resultRef}`);
|
|
122
|
+
|
|
123
|
+
process.stdout.write("\n✔ ALL ASSERTIONS PASSED — claude interactive control-plane wiring works end-to-end.\n");
|
|
124
|
+
process.stdout.write(` taskId: ${taskId}\n`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
main()
|
|
128
|
+
.then(() => { h.stop(); rmSync(tmp, { recursive: true, force: true }); process.exit(0); })
|
|
129
|
+
.catch((e) => {
|
|
130
|
+
process.stderr.write(`✘ SMOKE FAILED: ${e.message}\n`);
|
|
131
|
+
try { h.stop(); rmSync(tmp, { recursive: true, force: true }); } catch { /* noop */ }
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Usage: fix-skill.sh <spoke-vault-path> <skill-name> <new-body>
|
|
3
|
+
# Fixes a broken skill in-place (FIX evolution) — backs up the old version
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
VAULT="${1:?Usage: fix-skill.sh <vault-path> <skill-name> <new-body>}"
|
|
6
|
+
SKILL_NAME="${2:?}"
|
|
7
|
+
NEW_BODY="${3:?}"
|
|
8
|
+
DATE=$(date +"%Y-%m-%d")
|
|
9
|
+
SKILL_DIR="${VAULT}/skills/${SKILL_NAME}"
|
|
10
|
+
SKILL_FILE="${SKILL_DIR}/SKILL.md"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$SKILL_FILE" ]; then
|
|
13
|
+
echo "Skill '${SKILL_NAME}' not found at ${SKILL_FILE}"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Backup old version
|
|
18
|
+
BACKUP="${SKILL_DIR}/SKILL.${DATE}.bak.md"
|
|
19
|
+
cp "$SKILL_FILE" "$BACKUP"
|
|
20
|
+
|
|
21
|
+
# Extract existing frontmatter fields, bump version info
|
|
22
|
+
OLD_DESC=$(grep '^description:' "$SKILL_FILE" | sed 's/^description: //')
|
|
23
|
+
OLD_TIMES_USED=$(grep '^times_used:' "$SKILL_FILE" | sed 's/^times_used: //' || echo "0")
|
|
24
|
+
OLD_TIMES_SUCCESS=$(grep '^times_successful:' "$SKILL_FILE" | sed 's/^times_successful: //' || echo "0")
|
|
25
|
+
|
|
26
|
+
cat > "$SKILL_FILE" << EOF
|
|
27
|
+
---
|
|
28
|
+
name: ${SKILL_NAME}
|
|
29
|
+
description: ${OLD_DESC}
|
|
30
|
+
captured: ${DATE}
|
|
31
|
+
origin: fixed
|
|
32
|
+
times_used: ${OLD_TIMES_USED}
|
|
33
|
+
times_successful: ${OLD_TIMES_SUCCESS}
|
|
34
|
+
last_fixed: ${DATE}
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
${NEW_BODY}
|
|
38
|
+
EOF
|
|
39
|
+
echo "Fixed skill: $SKILL_FILE (backup: $BACKUP)"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regenerate the codex app-server protocol bindings.
|
|
3
|
+
# Requires codex-cli ≥0.130.0 on PATH.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
OUT="src/control/codex/protocol"
|
|
6
|
+
rm -rf "$OUT"
|
|
7
|
+
mkdir -p "$OUT"
|
|
8
|
+
codex app-server generate-ts --experimental --out "$OUT"
|
|
9
|
+
cat > "$OUT/README.md" <<'MD'
|
|
10
|
+
# codex app-server protocol bindings (auto-generated)
|
|
11
|
+
|
|
12
|
+
Generated by `npm run codex:gen-types` (`scripts/gen-codex-types.sh`).
|
|
13
|
+
**DO NOT EDIT BY HAND.** Re-run the generator to update.
|
|
14
|
+
|
|
15
|
+
Source: `codex app-server generate-ts --experimental --out <here>` against
|
|
16
|
+
`codex-cli ≥0.130.0`. See `docs/specs/2026-05-20-squadrant-interactive-codex-design.md` §3.3.
|
|
17
|
+
MD
|
|
18
|
+
echo "Generated $OUT"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/mailbox-injector-smoke.mjs
|
|
3
|
+
//
|
|
4
|
+
// E2E smoke for mailbox-injector refactor (replaces #109/#111 socket push).
|
|
5
|
+
// Spins up a real squadrantd on a temp socket + stateRoot, drives a few
|
|
6
|
+
// task.done events through the daemon, then verifies:
|
|
7
|
+
// 1. defaultNotify writes structured entries to <stateRoot>/inbox/<project>.log
|
|
8
|
+
// 2. readFromCursor yields each entry exactly once, in order
|
|
9
|
+
// 3. writeCursor persists, readCursor returns the saved seq
|
|
10
|
+
// 4. After a simulated "injector offline" window, resuming from the cursor
|
|
11
|
+
// reads only the entries added during the outage (no duplicates, no gaps)
|
|
12
|
+
//
|
|
13
|
+
// Uses startSquadrantd + sendRequest directly (same pattern as
|
|
14
|
+
// scripts/smoke-push-notify.mjs) rather than env vars — the daemon's CLI
|
|
15
|
+
// surface takes stateRoot/sockPath as arguments, not env vars.
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
|
+
|
|
22
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
23
|
+
const distEntry = pathToFileURL(join(__dirname, "..", "dist", "control", "squadrantd.js")).href;
|
|
24
|
+
const distProto = pathToFileURL(join(__dirname, "..", "dist", "control", "protocol.js")).href;
|
|
25
|
+
const distMailbox = pathToFileURL(join(__dirname, "..", "dist", "control", "mailbox.js")).href;
|
|
26
|
+
|
|
27
|
+
const { startSquadrantd } = await import(distEntry);
|
|
28
|
+
const { sendRequest } = await import(distProto);
|
|
29
|
+
const mailbox = await import(distMailbox);
|
|
30
|
+
|
|
31
|
+
const evidencePath = join(__dirname, "..", ".mailbox-injector-smoke.local");
|
|
32
|
+
const evidence = [];
|
|
33
|
+
const record = (m) => { evidence.push(m); process.stdout.write(m + "\n"); };
|
|
34
|
+
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), "mailbox-smoke-"));
|
|
36
|
+
const stateRoot = join(dir, "state");
|
|
37
|
+
const sock = join(dir, "c.sock");
|
|
38
|
+
|
|
39
|
+
const handle = startSquadrantd({ stateRoot, sockPath: sock, sweepMs: 0 });
|
|
40
|
+
|
|
41
|
+
const baseRec = (id, overrides = {}) => ({
|
|
42
|
+
id, project: "demo", provider: "claude", mode: "headless",
|
|
43
|
+
state: "submitted", task: "mailbox smoke", cwd: "/",
|
|
44
|
+
createdAt: 1, lastHeartbeat: 1, lastEvent: "",
|
|
45
|
+
heartbeatBudgetMs: 60000,
|
|
46
|
+
attempts: [{ attemptId: "a0", startedAt: 1, lastHeartbeatAt: 1 }],
|
|
47
|
+
...overrides,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let failures = 0;
|
|
51
|
+
const assert = (cond, label) => {
|
|
52
|
+
if (cond) { record(` ✓ ${label}`); }
|
|
53
|
+
else { record(` ✗ ${label}`); failures++; }
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
record(`# Mailbox + injector smoke — ${new Date().toISOString()}`);
|
|
58
|
+
record(`state: ${stateRoot}`);
|
|
59
|
+
record(`socket: ${sock}`);
|
|
60
|
+
|
|
61
|
+
record("\n[1] dispatch + task.done → mailbox file gets one entry");
|
|
62
|
+
await sendRequest(sock, { kind: "seed", record: baseRec("smoke-1abcdef0", { state: "working" }) });
|
|
63
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
64
|
+
event: { type: "task.done", id: "smoke-1abcdef0", resultRef: "/r1" } });
|
|
65
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
66
|
+
|
|
67
|
+
const logPath = join(stateRoot, "inbox", "demo.log");
|
|
68
|
+
assert(existsSync(logPath), `mailbox file created at ${logPath}`);
|
|
69
|
+
const lines1 = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
70
|
+
assert(lines1.length === 1, `exactly one entry (got ${lines1.length})`);
|
|
71
|
+
const entry1 = JSON.parse(lines1[0]);
|
|
72
|
+
assert(entry1.seq === 1, `seq=1 (got ${entry1.seq})`);
|
|
73
|
+
assert(entry1.kind === "task.done", `kind=task.done (got ${entry1.kind})`);
|
|
74
|
+
assert(entry1.taskId === "smoke-1abcdef0", `taskId matches`);
|
|
75
|
+
|
|
76
|
+
record("\n[2] simulate injector reading from cursor");
|
|
77
|
+
const items = [];
|
|
78
|
+
for await (const it of mailbox.readFromCursor({ stateRoot, project: "demo", fromSeq: 1 })) items.push(it);
|
|
79
|
+
assert(items.length === 1, `injector reads 1 entry from cursor (got ${items.length})`);
|
|
80
|
+
await mailbox.writeCursor({ stateRoot, project: "demo", subscriber: "captain", lastAckedSeq: 1 });
|
|
81
|
+
const cursor = await mailbox.readCursor({ stateRoot, project: "demo", subscriber: "captain" });
|
|
82
|
+
assert(cursor?.lastAckedSeq === 1, `cursor advanced to 1 (got ${cursor?.lastAckedSeq})`);
|
|
83
|
+
|
|
84
|
+
record("\n[3] simulate injector 'offline' — dispatch more events");
|
|
85
|
+
await sendRequest(sock, { kind: "seed", record: baseRec("smoke-2feedface", { state: "working" }) });
|
|
86
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
87
|
+
event: { type: "task.done", id: "smoke-2feedface", resultRef: "/r2" } });
|
|
88
|
+
await sendRequest(sock, { kind: "seed", record: baseRec("smoke-3deadbeef", { state: "working" }) });
|
|
89
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
90
|
+
event: { type: "task.blocked", id: "smoke-3deadbeef", question: "what now?" } });
|
|
91
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
92
|
+
|
|
93
|
+
record("\n[4] injector resumes — reads only new events, in order, no duplicates");
|
|
94
|
+
const items2 = [];
|
|
95
|
+
for await (const it of mailbox.readFromCursor({ stateRoot, project: "demo", fromSeq: 2 })) items2.push(it);
|
|
96
|
+
assert(items2.length === 2, `replay yields exactly 2 new entries (got ${items2.length})`);
|
|
97
|
+
assert(items2[0]?.seq === 2 && items2[1]?.seq === 3,
|
|
98
|
+
`seqs are 2,3 (got ${items2.map((x) => x.seq).join(",")})`);
|
|
99
|
+
assert(items2[0]?.kind === "task.done", `seq=2 is task.done`);
|
|
100
|
+
assert(items2[1]?.kind === "task.blocked", `seq=3 is task.blocked`);
|
|
101
|
+
|
|
102
|
+
record("\n[5] liveness events (task.started, task.progress) do NOT write to mailbox");
|
|
103
|
+
// firePush gates on ATTENTION_STATES = {done, blocked, failed, stalled}.
|
|
104
|
+
// task.started / task.progress drive working state → must be suppressed.
|
|
105
|
+
await sendRequest(sock, { kind: "seed", record: baseRec("smoke-4cafebabe", { state: "submitted" }) });
|
|
106
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
107
|
+
event: { type: "task.started", id: "smoke-4cafebabe" } });
|
|
108
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
109
|
+
event: { type: "task.progress", id: "smoke-4cafebabe" } });
|
|
110
|
+
await sendRequest(sock, { kind: "event", project: "demo",
|
|
111
|
+
event: { type: "task.progress", id: "smoke-4cafebabe" } });
|
|
112
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
113
|
+
|
|
114
|
+
const linesAfter = readFileSync(logPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
115
|
+
assert(linesAfter.length === 3,
|
|
116
|
+
`mailbox still has 3 entries after liveness traffic (got ${linesAfter.length}) — liveness suppressed`);
|
|
117
|
+
|
|
118
|
+
record(`\n${failures === 0 ? "✔ ALL SMOKE ASSERTIONS PASSED" : `✗ ${failures} FAILURES`}`);
|
|
119
|
+
} finally {
|
|
120
|
+
handle.stop();
|
|
121
|
+
writeFileSync(evidencePath, evidence.join("\n") + "\n");
|
|
122
|
+
record(`evidence: ${evidencePath}`);
|
|
123
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
124
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Usage: mark-learning-useful.sh <learning-file-path>
|
|
3
|
+
# Increments times_useful counter on a learning
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
FILE="${1:?Usage: mark-learning-useful.sh <learning-file-path>}"
|
|
6
|
+
|
|
7
|
+
if [ ! -f "$FILE" ]; then
|
|
8
|
+
echo "Learning file not found: $FILE"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Increment times_useful
|
|
13
|
+
CURRENT=$(grep '^times_useful:' "$FILE" | sed 's/^times_useful: //')
|
|
14
|
+
NEW=$((${CURRENT:-0} + 1))
|
|
15
|
+
sed -i '' "s/^times_useful: .*/times_useful: ${NEW}/" "$FILE"
|
|
16
|
+
|
|
17
|
+
# Increment times_loaded
|
|
18
|
+
LOADED=$(grep '^times_loaded:' "$FILE" | sed 's/^times_loaded: //')
|
|
19
|
+
NEW_LOADED=$((${LOADED:-0} + 1))
|
|
20
|
+
sed -i '' "s/^times_loaded: .*/times_loaded: ${NEW_LOADED}/" "$FILE"
|
|
21
|
+
|
|
22
|
+
echo "Marked useful (${NEW}x): $FILE"
|