muse-crew 0.9.0 → 0.10.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/docs/crew-state-boundaries.md +124 -0
- package/lib/test-worktree-backend.sh +42 -0
- package/lib/worktree-lifecycle.sh +12 -3
- package/package.json +2 -2
- package/workflows/crew-init.js +51 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Spec: Crew-Owned State Boundaries
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-17
|
|
4
|
+
**Status:** Approved by Eric — build and publish
|
|
5
|
+
**Context:** Gate 1 J1/J2 canaries exposed two instances of crew-owned state leaking into user git operations (0.9.1: `.worktrees/` broke cleanliness check; 0.9.2: `.merge-lock` committed via `git add -A`). The root problem: we can't enumerate all crew-owned files, so surgical exclusions are whack-a-mole.
|
|
6
|
+
|
|
7
|
+
## The Rule
|
|
8
|
+
|
|
9
|
+
Crew state lives in exactly three places. Nothing crew-owned sits in the repo unclassified.
|
|
10
|
+
|
|
11
|
+
| Location | Version-controlled? | Contents |
|
|
12
|
+
|----------|---------------------|----------|
|
|
13
|
+
| `$REPO/.orchestration/` | **Yes** | Custom workflows, identities, phases — the user's crew configuration |
|
|
14
|
+
| `$REPO/.orchestration/user/` | **No** (gitignored) | Personal settings, local config, secrets — user-specific, not shared |
|
|
15
|
+
| `$CREW_HOME/` | N/A (outside repo) | Worktrees, merge lock, registry, pins — transient operational state |
|
|
16
|
+
|
|
17
|
+
## Directory Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$REPO/
|
|
21
|
+
.orchestration/
|
|
22
|
+
workflows/ # custom workflow definitions (versioned)
|
|
23
|
+
identities/ # custom identity definitions (versioned)
|
|
24
|
+
phases/ # custom phase definitions (versioned)
|
|
25
|
+
user/ # personal settings (GITIGNORED)
|
|
26
|
+
config.json # example: user preferences
|
|
27
|
+
secrets/ # example: local secrets (never versioned)
|
|
28
|
+
.gitignore # includes .orchestration/user/
|
|
29
|
+
|
|
30
|
+
$CREW_HOME/
|
|
31
|
+
worktrees/ # task worktrees (moved from $REPO/.worktrees/)
|
|
32
|
+
.merge-lock # merge lock file (moved from $REPO/.worktrees/.merge-lock)
|
|
33
|
+
.registry/ # task registry (moved from $REPO/.worktrees/.registry/)
|
|
34
|
+
.pins/ # release pins
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Changes
|
|
38
|
+
|
|
39
|
+
### 1. Gitignore consent flow (THIS RELEASE)
|
|
40
|
+
|
|
41
|
+
**What:** The crew needs to add `.worktrees/` to the repo's `.gitignore`. This is the ONLY user file outside crew-owned directories that the crew modifies.
|
|
42
|
+
|
|
43
|
+
**When:** During the setup conversation (before `crew-init` is launched), as part of the "setup is a conversation" experience.
|
|
44
|
+
|
|
45
|
+
**UX (in the setup conversation):**
|
|
46
|
+
|
|
47
|
+
The agent (emojiman) says:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
The crew keeps its working files in .worktrees/ (task worktrees, merge lock).
|
|
51
|
+
These should never be committed to git.
|
|
52
|
+
|
|
53
|
+
I'll add one line to your .gitignore:
|
|
54
|
+
|
|
55
|
+
+ .worktrees/
|
|
56
|
+
|
|
57
|
+
OK to apply? (yes/no)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**If user says yes:** Launch `crew-init` with `gitignoreConsent: true`. The scaffold phase appends the entry.
|
|
61
|
+
|
|
62
|
+
**If user says no:**
|
|
63
|
+
```
|
|
64
|
+
Got it — I'll leave your .gitignore alone. Add this line yourself when you get a chance:
|
|
65
|
+
.worktrees/
|
|
66
|
+
The crew will still work (it excludes .worktrees/ from its own git operations),
|
|
67
|
+
but you'll see it as untracked until you do.
|
|
68
|
+
```
|
|
69
|
+
Launch `crew-init` with `gitignoreConsent: false`.
|
|
70
|
+
|
|
71
|
+
**`crew-init.js` inputs:**
|
|
72
|
+
- `gitignoreConsent` (boolean, default false): Whether the user approved `.gitignore` modification.
|
|
73
|
+
|
|
74
|
+
**Edge cases:**
|
|
75
|
+
- **No `.gitignore` exists:** Create it with the entry (if consent given).
|
|
76
|
+
- **Entry already present:** Skip (idempotent).
|
|
77
|
+
- **Non-interactive / not provided:** Default false (fail closed).
|
|
78
|
+
|
|
79
|
+
### 2. `.orchestration/user/` structure (FUTURE)
|
|
80
|
+
|
|
81
|
+
When `$REPO/.orchestration/` is implemented (project-specific custom workflows, identities, phases), it will follow this structure:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
$REPO/.orchestration/
|
|
85
|
+
workflows/ # versioned — custom workflow definitions
|
|
86
|
+
identities/ # versioned — custom identity definitions
|
|
87
|
+
phases/ # versioned — custom phase definitions
|
|
88
|
+
user/ # GITIGNORED — personal settings, local config, secrets
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The gitignore consent flow will be extended to include `.orchestration/user/` at that time. The standing rule (crew modifies only `.gitignore`, with consent) already covers this.
|
|
92
|
+
|
|
93
|
+
### 3. Worktrees relocation (deferred)
|
|
94
|
+
|
|
95
|
+
**Decision:** For this release, `.worktrees/` stays in `$REPO/.worktrees/` but is covered by the gitignore consent flow (add `.worktrees/` to `.gitignore` alongside `.orchestration/user/`).
|
|
96
|
+
|
|
97
|
+
**Rationale:** Relocating to `$CREW_HOME/worktrees/` is the cleaner long-term solution (eliminates the entire class of bug), but it's a larger change affecting `worktree-lifecycle.sh`, `merge-lock.sh`, and all workflow `WORKTREE_HINT` constants. The gitignore approach solves the immediate leak with minimal risk. Relocation is tracked as future work.
|
|
98
|
+
|
|
99
|
+
**Gitignore entries added (with consent):**
|
|
100
|
+
```
|
|
101
|
+
.orchestration/user/
|
|
102
|
+
.worktrees/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. Migration
|
|
106
|
+
|
|
107
|
+
Existing setups (e.g., Room #12) have `$REPO/.worktrees/` without gitignore coverage. On the next `crew-init` re-run (or manual upgrade), the consent flow will offer to add the entries. No automatic migration — the user must consent.
|
|
108
|
+
|
|
109
|
+
## Standing Rule
|
|
110
|
+
|
|
111
|
+
If the crew ever needs to modify another user file outside `.orchestration/`, it goes through the same flow: heads-up, diff, Enter/skip. The `.gitignore` is currently the only such file.
|
|
112
|
+
|
|
113
|
+
## Test Plan
|
|
114
|
+
|
|
115
|
+
1. `crew-init` on a repo without `.gitignore` → creates file, adds entries, user confirms
|
|
116
|
+
2. `crew-init` on a repo with existing `.gitignore` → appends entries, shows diff
|
|
117
|
+
3. `crew-init` with entries already present → no-op, no prompt
|
|
118
|
+
4. User types 'skip' → setup continues, warning logged
|
|
119
|
+
5. Non-interactive → no modification, instruction logged
|
|
120
|
+
6. Full suite passes
|
|
121
|
+
|
|
122
|
+
## Release
|
|
123
|
+
|
|
124
|
+
Version 0.9.2 (includes the `.merge-lock` exclusion fix from d3e5790).
|
|
@@ -76,6 +76,19 @@ else
|
|
|
76
76
|
fi
|
|
77
77
|
git -C "$T" checkout -q -- f.txt
|
|
78
78
|
|
|
79
|
+
# require_clean_main ignores the crew's own .worktrees/ dir (2026-09-17,
|
|
80
|
+
# Gate 1 J2 canary): a repo WITHOUT .gitignore covering .worktrees/ must
|
|
81
|
+
# still prepare and integrate. The scratch repo T has no .gitignore, and
|
|
82
|
+
# .worktrees/ exists (with .registry/) from the prepares above.
|
|
83
|
+
export CREW_LIB="$REPO_DIR/lib"
|
|
84
|
+
out=$(bash "$LIFECYCLE" prepare "backendtest3") || fail "prepare failed with only .worktrees/ untracked: $out"
|
|
85
|
+
echo "$out" | grep -q '^CREATED' || fail "prepare: expected CREATED with only .worktrees/ untracked, got: $out"
|
|
86
|
+
echo taskwork >> "$T/.worktrees/backendtest3/f.txt"
|
|
87
|
+
(git -C "$T/.worktrees/backendtest3" commit -qam taskwork) || fail "worktree commit failed"
|
|
88
|
+
out=$(bash "$LIFECYCLE" integrate "backendtest3" "merge: backendtest3") || fail "integrate failed with .worktrees/ untracked: $out"
|
|
89
|
+
echo "$out" | grep -q '^MERGED: ' || fail "integrate: no MERGED line: $out"
|
|
90
|
+
"$REPO_DIR/lib/merge-lock.sh" release "backendtest3" >/dev/null || fail "integrate-test: lock release failed"
|
|
91
|
+
|
|
79
92
|
|
|
80
93
|
# integrate reconciles a diverged remote under the merge lock (canary
|
|
81
94
|
# a6d8b0c8, 2026-09-11): remote main advanced outside the lock; without the
|
|
@@ -197,6 +210,35 @@ git -C "$T4/repo" branch -D "task/deadbeef" "task/deadbeef-1111" >/dev/null
|
|
|
197
210
|
out=$(bash "$LIFECYCLE" resolve-branch "$fullid") || fail "prefix-test: resolve-branch failed with no branch present"
|
|
198
211
|
[ "$out" = "task/$fullid" ] || fail "prefix-test: resolve-branch returned '$out', expected canonical fallback task/$fullid"
|
|
199
212
|
|
|
213
|
+
# post-deploy never commits the crew's .worktrees/ state into main
|
|
214
|
+
# (2026-09-17, Gate 1 J1 canary): cmd_post_deploy's `git add -A` swept up
|
|
215
|
+
# a leftover .worktrees/.merge-lock into main. The add must exclude the
|
|
216
|
+
# crew-owned .worktrees/ dir.
|
|
217
|
+
T5=/tmp/crew-git-postdeploy-test
|
|
218
|
+
rm -rf "$T5"
|
|
219
|
+
mkdir -p "$T5"
|
|
220
|
+
git init -q -b main "$T5"
|
|
221
|
+
git -C "$T5" config user.email test@test.t
|
|
222
|
+
git -C "$T5" config user.name test
|
|
223
|
+
echo base > "$T5/f.txt"
|
|
224
|
+
git -C "$T5" add -A
|
|
225
|
+
git -C "$T5" commit -qm base
|
|
226
|
+
export CREW_REPO="$T5"
|
|
227
|
+
export CREW_LIB="$REPO_DIR/lib"
|
|
228
|
+
out=$(bash "$LIFECYCLE" prepare "pd1") || fail "postdeploy-test: prepare failed: $out"
|
|
229
|
+
# Simulate the merge-lock left behind by a real integrate flow
|
|
230
|
+
printf 'task_id=pd1\nholder=test\n' > "$T5/.worktrees/.merge-lock"
|
|
231
|
+
# A genuine user change the post-deploy commit should pick up
|
|
232
|
+
echo userchange > "$T5/user.txt"
|
|
233
|
+
out=$(bash "$LIFECYCLE" post-deploy "pd1") || fail "postdeploy-test: post-deploy failed: $out"
|
|
234
|
+
# The user change must be committed...
|
|
235
|
+
git -C "$T5" log --name-only --format= -1 | grep -q '^user\.txt$' || fail "postdeploy-test: user change not committed"
|
|
236
|
+
# ...but the crew's merge-lock must NOT be in main's history
|
|
237
|
+
if git -C "$T5" log --name-only --format= | grep -q '^\.worktrees/'; then
|
|
238
|
+
fail "postdeploy-test: .worktrees/ content committed into main"
|
|
239
|
+
fi
|
|
240
|
+
rm -rf "$T5"
|
|
241
|
+
|
|
200
242
|
rm -rf "$T2" "$T3" "$T4"
|
|
201
243
|
export CREW_REPO="$T"
|
|
202
244
|
rm -rf "$T"
|
|
@@ -138,8 +138,14 @@ validate_task_id() {
|
|
|
138
138
|
require_clean_main() {
|
|
139
139
|
cd "$REPO"
|
|
140
140
|
git checkout main 2>/dev/null
|
|
141
|
+
# The crew's own worktree dir is crew state, not user changes: exclude it
|
|
142
|
+
# from the cleanliness check. (2026-09-17: 0.9.0 failed closed on every
|
|
143
|
+
# target repo whose .gitignore doesn't cover .worktrees/ — which is every
|
|
144
|
+
# repo except the crew's own. Gate 1 J2 canary.)
|
|
145
|
+
local wt_esc=${WORKTREE_DIR##*/}
|
|
146
|
+
wt_esc=${wt_esc//./\\.}
|
|
141
147
|
local dirty
|
|
142
|
-
dirty=$(git status --porcelain 2>/dev/null | wc -l)
|
|
148
|
+
dirty=$(git status --porcelain 2>/dev/null | grep -vE "^.{3}$wt_esc(/|$)" | wc -l)
|
|
143
149
|
if [ "$dirty" -gt 0 ]; then
|
|
144
150
|
echo "ERROR: main has $dirty uncommitted changes"
|
|
145
151
|
git status --short
|
|
@@ -408,8 +414,11 @@ cmd_post_deploy() {
|
|
|
408
414
|
cd "$REPO"
|
|
409
415
|
git checkout main 2>/dev/null
|
|
410
416
|
|
|
411
|
-
# Commit any changes the artifact builder left behind
|
|
412
|
-
|
|
417
|
+
# Commit any changes the artifact builder left behind.
|
|
418
|
+
# The crew's own .worktrees/ dir (merge lock, registry) is crew-owned
|
|
419
|
+
# state, never user content — exclude it so a stray lock file can never
|
|
420
|
+
# be committed into main (J1 canary: .merge-lock landed in main via add -A).
|
|
421
|
+
git add -A -- ':!.worktrees/'
|
|
413
422
|
if ! git diff --cached --quiet 2>/dev/null; then
|
|
414
423
|
git commit -m "rebuild: $task_id"
|
|
415
424
|
echo "COMMITTED: artifact builder changes"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muse-crew",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Opinionated orchestration for Muse
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"repository": {
|
package/workflows/crew-init.js
CHANGED
|
@@ -20,6 +20,13 @@ const dashboardName = inputs.dashboardName || "Muse Crew";
|
|
|
20
20
|
const crewName = inputs.crewName || null;
|
|
21
21
|
const cronIds = inputs.cronIds || {};
|
|
22
22
|
|
|
23
|
+
// ── Gitignore consent input ─────────────────────────────────────────
|
|
24
|
+
// Set by the setup conversation before launch. If true, the scaffold phase
|
|
25
|
+
// appends crew-owned paths (.orchestration/user/, .worktrees/) to the repo's
|
|
26
|
+
// .gitignore. Defaults to false (fail closed) — the user must explicitly
|
|
27
|
+
// approve modifying their .gitignore during the setup conversation.
|
|
28
|
+
const gitignoreConsent = inputs.gitignoreConsent === true;
|
|
29
|
+
|
|
23
30
|
// ── Automatic-update policy inputs ──────────────────────────────────
|
|
24
31
|
// All optional. autoUpdateCrew/autoUpdateDashboard default to true (the
|
|
25
32
|
// update watcher files upgrade tasks through the loop); updateChannel
|
|
@@ -330,6 +337,50 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
|
|
|
330
337
|
var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
|
|
331
338
|
log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
|
|
332
339
|
|
|
340
|
+
// ── Gitignore consent execution ─────────────────────────────────────
|
|
341
|
+
// If the user approved during the setup conversation (gitignoreConsent=true)
|
|
342
|
+
// and we have a repo (dashboard mode), append crew-owned paths to .gitignore.
|
|
343
|
+
// This is the ONLY user file outside .orchestration/ that the crew modifies.
|
|
344
|
+
if (gitignoreConsent && dashboardRepoPath) {
|
|
345
|
+
var gitignoreResult;
|
|
346
|
+
try {
|
|
347
|
+
gitignoreResult = await agent(
|
|
348
|
+
"Update the .gitignore file in the repository to exclude crew-owned paths.\n\n" +
|
|
349
|
+
"Repository: " + dashboardRepoPath + "\n\n" +
|
|
350
|
+
"Steps:\n" +
|
|
351
|
+
"1. Check if " + dashboardRepoPath + "/.gitignore exists.\n" +
|
|
352
|
+
"2. Ensure this entry is present (one per line):\n" +
|
|
353
|
+
" .worktrees/\n" +
|
|
354
|
+
"3. If the file doesn't exist, create it with that line.\n" +
|
|
355
|
+
"4. If it exists, append the entry if missing (do not duplicate).\n" +
|
|
356
|
+
"5. Report what was done.\n\n" +
|
|
357
|
+
"Return JSON with: { created: boolean (true if file was created), appended: array of entries that were added, skipped: array of entries already present }.",
|
|
358
|
+
{
|
|
359
|
+
key: "scaffold-gitignore",
|
|
360
|
+
label: "Update .gitignore for crew paths",
|
|
361
|
+
schema: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {
|
|
364
|
+
created: { type: "boolean" },
|
|
365
|
+
appended: { type: "array", items: { type: "string" } },
|
|
366
|
+
skipped: { type: "array", items: { type: "string" } }
|
|
367
|
+
},
|
|
368
|
+
required: ["created", "appended", "skipped"]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
log("Gitignore: " + (gitignoreResult.created ? "created" : "updated") +
|
|
373
|
+
" — appended [" + (gitignoreResult.appended || []).join(", ") + "]" +
|
|
374
|
+
", already present [" + (gitignoreResult.skipped || []).join(", ") + "]");
|
|
375
|
+
} catch (e) {
|
|
376
|
+
log("Gitignore update failed (non-fatal): " + String(e.message || e));
|
|
377
|
+
}
|
|
378
|
+
} else if (dashboardRepoPath) {
|
|
379
|
+
log("Gitignore: user declined — add '.worktrees/' to " + dashboardRepoPath + "/.gitignore manually.");
|
|
380
|
+
} else {
|
|
381
|
+
log("Gitignore: no repo (CLI-only mode) — skipping.");
|
|
382
|
+
}
|
|
383
|
+
|
|
333
384
|
// ── Phase 3: Project registration ─────────────────────────────────────
|
|
334
385
|
// Registers the first project in crew-state.db via the Crew API. In
|
|
335
386
|
// dashboard mode, the dashboard becomes the crew's first project (so the
|