muse-crew 0.10.1 → 0.11.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/guide.md +27 -2
- package/docs/state-boundary-spec.md +143 -0
- package/lib/AGENTS.md +4 -0
- package/lib/gitignore.js +147 -0
- package/lib/repo-orchestration.js +126 -0
- package/lib/sample-project.js +84 -0
- package/lib/setup-project-repo.js +50 -0
- package/package.json +2 -2
- package/workflows/crew-init.js +42 -12
package/docs/guide.md
CHANGED
|
@@ -170,9 +170,34 @@ The project must have:
|
|
|
170
170
|
- For the `artifact` publish target: an existing web artifact at the specified `deploy_slug`.
|
|
171
171
|
|
|
172
172
|
The project does **not** need:
|
|
173
|
-
- A `.orchestration/` folder (that belongs to the crew home, not the project).
|
|
174
173
|
- Any special file structure — the crew works with whatever the repo contains.
|
|
175
|
-
|
|
174
|
+
|
|
175
|
+
### The repo-local `.orchestration/` scaffold
|
|
176
|
+
|
|
177
|
+
After registration, each project gets a repository-local `.orchestration/` directory alongside the crew-home one:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
<repo>/.orchestration/
|
|
181
|
+
workflows/ # project-specific workflow docs (version-controlled)
|
|
182
|
+
identities/ # project-specific identity files (version-controlled)
|
|
183
|
+
phases/ # project-specific phase docs (version-controlled)
|
|
184
|
+
user/ # local-only config, ignored by Git
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The scaffold seeds `workflows/`, `identities/`, and `phases/` from the platform defaults with no-clobber semantics — existing project files are never overwritten, so customizations survive re-runs. `user/` is for local-only config (preferences, scratch notes) and is added to `.gitignore` by the consent step below.
|
|
188
|
+
|
|
189
|
+
State boundaries:
|
|
190
|
+
- Version-controlled config (what the crew *should be*): `workflows/`, `identities/`, `phases/` — committed with the repo.
|
|
191
|
+
- Local-only config (who is asking): `user/` — never committed.
|
|
192
|
+
- Working state (what the crew *is doing*): `<repo>/.worktrees/` — ephemeral, ignored by Git.
|
|
193
|
+
|
|
194
|
+
### The `.gitignore` conversation
|
|
195
|
+
|
|
196
|
+
The crew asks before touching anything in your repo. When you register a project, setup shows you the exact `.gitignore` lines it wants to add — `.worktrees/` and `.orchestration/user/` — and asks for explicit consent. Say yes and the lines are appended (a missing `.gitignore` is created; existing content is never modified); say no and setup continues without them. Declining never blocks the crew — it only means worktree dirs and local config won't be ignored automatically.
|
|
197
|
+
|
|
198
|
+
### The sample project
|
|
199
|
+
|
|
200
|
+
If you don't have a project in mind, the crew can set up a sample: a tiny flashcard study tool that ships inside the npm package. It's extracted into a directory you choose, initialized as a fresh Git repo with an initial commit (no remote needed), and registered like any other project. Explicitly declining the sample leaves you with zero projects — a supported state; the crew idles until you register one.
|
|
176
201
|
|
|
177
202
|
### Worktrees
|
|
178
203
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# State-Boundary Spec: Repository-local `.orchestration/`
|
|
2
|
+
|
|
3
|
+
**Status:** Approved by Eric 2026-09-17 ("Yes spec it and build it. Publish it.")
|
|
4
|
+
**Scope:** 8 gaps from the 0.10.x gitignore work
|
|
5
|
+
|
|
6
|
+
## 1. Repository-local `.orchestration/`
|
|
7
|
+
|
|
8
|
+
When a project is registered with a repo path (dashboard mode init, or
|
|
9
|
+
`create-project` via CLI/dashboard), scaffold `$REPO/.orchestration/`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
$REPO/.orchestration/
|
|
13
|
+
workflows/ # version-controlled — project workflow docs, seeded from platform defaults
|
|
14
|
+
identities/ # version-controlled — project crew identities, seeded from platform defaults
|
|
15
|
+
phases/ # version-controlled — project phase definitions, seeded from platform defaults
|
|
16
|
+
user/ # IGNORED — personal/local configuration, never committed
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This is distinct from `$CREW_HOME/.orchestration/` (the crew instance's own
|
|
20
|
+
config, already scaffolded by crew-init Phase 2).
|
|
21
|
+
|
|
22
|
+
**Seeding:** Copy from the crew repo's canonical sources with `--no-clobber`
|
|
23
|
+
semantics (never overwrite existing project customizations):
|
|
24
|
+
- `workflows/` ← `$CREW_REPO/seed/workflows/*.md`
|
|
25
|
+
- `identities/` ← `$CREW_REPO/identities/*.md` (and portraits)
|
|
26
|
+
- `phases/` ← `$CREW_REPO/seed/phases/*.md` (if exists; else skip)
|
|
27
|
+
|
|
28
|
+
**Idempotency:** Re-running scaffold on an existing `$REPO/.orchestration/`
|
|
29
|
+
must not modify existing files.
|
|
30
|
+
|
|
31
|
+
## 2. `.orchestration/user/`
|
|
32
|
+
|
|
33
|
+
- Created by the scaffold (empty, with a `.gitkeep` or README explaining
|
|
34
|
+
it's for local config).
|
|
35
|
+
- MUST be listed in the repo's `.gitignore` as `.orchestration/user/`.
|
|
36
|
+
- The crew never commits this directory. User preferences/configuration
|
|
37
|
+
live here, never in version control.
|
|
38
|
+
|
|
39
|
+
## 3. Deterministic `.gitignore` management
|
|
40
|
+
|
|
41
|
+
Replace the current agent-driven `.gitignore` mutation (crew-init.js lines
|
|
42
|
+
344-379, which prompts an agent to edit the file) with deterministic JS.
|
|
43
|
+
|
|
44
|
+
**Function:** `ensureGitignoreEntries(repoPath, entries)` in a new
|
|
45
|
+
`lib/gitignore.js` (pure, testable, no agent calls).
|
|
46
|
+
|
|
47
|
+
**Contract:**
|
|
48
|
+
- `entries`: array of gitignore patterns, e.g. `[".worktrees/", ".orchestration/user/"]`
|
|
49
|
+
- If `$REPO/.gitignore` does not exist: create it containing the entries,
|
|
50
|
+
one per line, trailing newline. Return `{ created: true, appended: entries, skipped: [] }`.
|
|
51
|
+
- If it exists: read lines, append each missing entry (exact line match;
|
|
52
|
+
no duplicates). Preserve existing content byte-for-byte (including
|
|
53
|
+
trailing newline handling). Return `{ created: false, appended: [...], skipped: [...] }`.
|
|
54
|
+
- Idempotent: second call with same entries returns `appended: []`.
|
|
55
|
+
- Never removes or reorders existing lines.
|
|
56
|
+
|
|
57
|
+
**Crew-owned entries** (the complete set):
|
|
58
|
+
- `.worktrees/`
|
|
59
|
+
- `.orchestration/user/`
|
|
60
|
+
|
|
61
|
+
## 4. Consent flow
|
|
62
|
+
|
|
63
|
+
The `.gitignore` modification requires explicit user approval. This is the
|
|
64
|
+
ONLY user-owned file outside `.orchestration/` that the crew touches.
|
|
65
|
+
|
|
66
|
+
**Setup conversation (before crew-init launches):**
|
|
67
|
+
1. Explain WHY: "The crew needs to ignore its working directories
|
|
68
|
+
(`.worktrees/` for isolated task workspaces, `.orchestration/user/`
|
|
69
|
+
for your local config) so they don't pollute your git status."
|
|
70
|
+
2. Show the EXACT diff: what lines will be added to which file.
|
|
71
|
+
- New file: show full proposed content.
|
|
72
|
+
- Existing file: show the lines to be appended.
|
|
73
|
+
3. Offer two choices:
|
|
74
|
+
- Approve (Enter / "yes") → pass `gitignoreConsent: true`
|
|
75
|
+
- "I'll handle it" (decline) → pass `gitignoreConsent: false`;
|
|
76
|
+
setup continues; the user is told exactly what to add manually.
|
|
77
|
+
|
|
78
|
+
**Rules:**
|
|
79
|
+
- Declining MUST NOT block setup.
|
|
80
|
+
- Non-interactive (no TTY / `--non-interactive`): default to `false`
|
|
81
|
+
(fail closed — never modify a user's file without consent).
|
|
82
|
+
- The consent decision is recorded in the init summary output.
|
|
83
|
+
|
|
84
|
+
**Dashboard path:** The dashboard onboarding flow must present the same
|
|
85
|
+
consent UI before launching crew-init.
|
|
86
|
+
|
|
87
|
+
**CLI path:** The CLI setup conversation (or `crew-init` wrapper) must
|
|
88
|
+
present the same consent prompt. `create-project` on an existing repo
|
|
89
|
+
must also offer the `.orchestration/` scaffold + gitignore consent.
|
|
90
|
+
|
|
91
|
+
## 5. Sample-project setup
|
|
92
|
+
|
|
93
|
+
Optional; never forced.
|
|
94
|
+
|
|
95
|
+
**Flow:**
|
|
96
|
+
1. Setup asks: "Do you have a git project in mind? [yes/no]"
|
|
97
|
+
2. If no → offer: "Want me to set up the sample flashcards project so
|
|
98
|
+
you can try the crew? [yes/no]"
|
|
99
|
+
3. If yes to sample:
|
|
100
|
+
a. Extract `seed/sample-project.tar.gz` from the installed package
|
|
101
|
+
to the chosen location (default: `~/workspace/<name>/`).
|
|
102
|
+
b. `git init`, initial commit.
|
|
103
|
+
c. Register via the existing `create-project` action (no new endpoint).
|
|
104
|
+
4. If no to sample: setup completes with zero projects. This is a
|
|
105
|
+
supported state. The user is told how to add a project later
|
|
106
|
+
(`create-project` via CLI or dashboard).
|
|
107
|
+
|
|
108
|
+
**Rules:**
|
|
109
|
+
- No sample-specific API endpoint. Compose existing actions.
|
|
110
|
+
- No remote required. Workflows must not treat a missing remote as a defect.
|
|
111
|
+
- The sample's `.gitignore` (ships in the tarball) is preserved; the
|
|
112
|
+
crew appends its entries per the consent flow.
|
|
113
|
+
|
|
114
|
+
## 6. Behavioral tests
|
|
115
|
+
|
|
116
|
+
New `tests/gitignore.test.js` and `tests/repo-orchestration.test.js`:
|
|
117
|
+
|
|
118
|
+
**gitignore:**
|
|
119
|
+
- Missing file → created with entries
|
|
120
|
+
- Existing file, missing entries → appended, no duplicates
|
|
121
|
+
- Existing file, entries present → no-op (idempotent)
|
|
122
|
+
- Existing file with entries in different order → no-op
|
|
123
|
+
- Entries that are substrings of existing lines are still added
|
|
124
|
+
(exact line match, not substring)
|
|
125
|
+
- Trailing newline preserved/added correctly
|
|
126
|
+
|
|
127
|
+
**consent:**
|
|
128
|
+
- Decline → setup completes, `.gitignore` untouched
|
|
129
|
+
- Non-interactive → defaults to decline, `.gitignore` untouched
|
|
130
|
+
- Accept → entries added
|
|
131
|
+
|
|
132
|
+
**repo-orchestration:**
|
|
133
|
+
- Scaffold creates all four subdirs
|
|
134
|
+
- `user/` is created
|
|
135
|
+
- Re-run does not overwrite customizations
|
|
136
|
+
- Seed sources are copied correctly
|
|
137
|
+
|
|
138
|
+
## 7. What this does NOT do
|
|
139
|
+
|
|
140
|
+
- Does not move `.worktrees/` (still repo-local; relocation deferred,
|
|
141
|
+
no approval to change).
|
|
142
|
+
- Does not add new API endpoints.
|
|
143
|
+
- Does not change Room #12 or any existing crew's behavior.
|
package/lib/AGENTS.md
CHANGED
|
@@ -27,3 +27,7 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
27
27
|
- `build-readback-request.js` — builds the LLM-inspector read-back `verbatim_request` from the merge commit's diff (2026-09-14): carries the merged diff as the expected change and asks for an independent read of the artifact's actual source. Retained as the manual fallback; the deterministic `readback-disk.js` is the primary sensor.
|
|
28
28
|
- `verify-publish.js` — mechanical publish verification judge (2026-09-14/16): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every added line PRESENT / every removed line ABSENT, HEAD==commit supersession check) and only then stamps provenance. Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp. Collision exemption (2026-09-15): a removed line occurring in the old tree strictly more times than the diff removes is exempt from ABSENT (zero discriminating power); computed from `git show <base>:<path>`, never the working tree.
|
|
29
29
|
- `update-watch.js` — deterministic automatic update watcher (2026-09-16, zero deps): `node update-watch.js --crew-home <path>` (missing arg → usage, exit 2; every other path exits 0). Watches the public npm registry (`npm view muse-crew version` pinned to `https://registry.npmjs.org/`) vs `crew-release.sh current` and files a `workflow: "upgrade"` task with `source: npm@<version>` when policy (`auto_update_crew`, `update_channel`) and channel gating allow; watches `git ls-remote origin HEAD` on the first `deploy_type=artifact` project vs `$CREW_HOME/.update-watch.json` and files a `workflow: "chore"` task carrying the mechanical dashboard-upgrade journey. Reads the `.crew-version` compatibility anchor at the new ref via `git fetch` + `git show <sha>:.crew-version` (never the working tree) and orders dashboard-led: a declared newer crew files the crew upgrade task FIRST and the dashboard task notes it follows the crew upgrade (declaration bypasses `update_channel`, not the `auto_update_crew=false` opt-out); a declared older crew skips the dashboard leg entirely as a human decision; a missing/invalid/unfetchable anchor fails open to the dashboard leg as today. Idempotency via the same state file (records at file time); check failures log to `$CREW_HOME/update-watch.log` and are never thrown. Safety: only files tasks — never deploys, never touches the artifact/config/scheduler. Run by the daily `crew-update-watch` cron through the `current` symlink (latest release); deliberately NOT in the lib-pinning `PIN_BASENAMES`.
|
|
30
|
+
- `gitignore.js` — deterministic .gitignore management for crew-owned paths (2026-09-17): the crew touches exactly one user-owned file outside `.orchestration/` — the repo's `.gitignore`. `ensureGitignoreEntries(repoPath, entries)` creates the file when missing, appends missing entries (exact line match, no duplicates), preserves existing content byte-for-byte, and is idempotent. `describeGitignoreChange(repoPath, entries)` renders the exact diff for the setup consent conversation. Crew-owned entries: `.worktrees/`, `.orchestration/user/`. CLI: `--repo <path> [--dry-run]`.
|
|
31
|
+
- `repo-orchestration.js` — repository-local `.orchestration/` scaffold (2026-09-17): `scaffoldRepoOrchestration(repoPath, crewRepoPath)` creates `$REPO/.orchestration/{workflows,identities,phases,user}/`, seeds workflows/identities/phases from the crew repo's platform defaults with no-clobber semantics (existing project customizations never overwritten), and writes a README in `user/` explaining it's for local config. Idempotent. CLI: `--repo <path> --crew-repo <path>`.
|
|
32
|
+
- `setup-project-repo.js` — repo-local setup composer for a registered project (2026-09-17): runs the `.orchestration/` scaffold plus the `.gitignore` update (only with explicit `--consent`). The CLI-path equivalent of crew-init's dashboard-mode repo setup; the dashboard calls the same script. No new API endpoint — composition of existing deterministic modules. CLI: `--repo <path> --crew-repo <path> [--consent]`.
|
|
33
|
+
- `sample-project.js` — optional sample-project setup (2026-09-17): extracts `seed/sample-project.tar.gz` (stripping the `sample-project/` prefix), `git init`, initial commit. No remote required. Registration happens via the existing `create-project` action — no sample-specific endpoint. CLI: `--tarball <path> --dest <dir> --name <project-name>`.
|
package/lib/gitignore.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/gitignore.js — deterministic .gitignore management for crew-owned paths.
|
|
3
|
+
//
|
|
4
|
+
// The crew touches exactly one user-owned file outside .orchestration/:
|
|
5
|
+
// the repo's .gitignore. This module does it deterministically — no agent
|
|
6
|
+
// judgment, no prose parsing. Pure functions, fully testable.
|
|
7
|
+
//
|
|
8
|
+
// Crew-owned entries:
|
|
9
|
+
// .worktrees/ — isolated task workspaces
|
|
10
|
+
// .orchestration/user/ — personal/local configuration, never committed
|
|
11
|
+
//
|
|
12
|
+
// Contract of ensureGitignoreEntries(repoPath, entries):
|
|
13
|
+
// - Missing .gitignore → create it with entries, one per line.
|
|
14
|
+
// - Existing .gitignore → append each missing entry (exact line match).
|
|
15
|
+
// - Idempotent: second call with same entries is a no-op.
|
|
16
|
+
// - Never removes, reorders, or reformats existing content.
|
|
17
|
+
// - Returns { created, appended[], skipped[] }.
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
|
|
22
|
+
const CREW_GITIGNORE_ENTRIES = [".worktrees/", ".orchestration/user/"];
|
|
23
|
+
|
|
24
|
+
function gitignorePath(repoPath) {
|
|
25
|
+
return path.join(repoPath, ".gitignore");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Normalize a line for comparison: trim trailing whitespace and \r.
|
|
29
|
+
// Leading whitespace is significant in gitignore (it isn't, actually —
|
|
30
|
+
// gitignore strips leading/trailing whitespace unless escaped — but we
|
|
31
|
+
// treat lines literally to avoid merging distinct user intent).
|
|
32
|
+
function normLine(line) {
|
|
33
|
+
return line.replace(/\r$/, "").replace(/[ \t]+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureGitignoreEntries(repoPath, entries) {
|
|
37
|
+
const file = gitignorePath(repoPath);
|
|
38
|
+
const wanted = entries || CREW_GITIGNORE_ENTRIES;
|
|
39
|
+
|
|
40
|
+
let existing = null;
|
|
41
|
+
try {
|
|
42
|
+
existing = fs.readFileSync(file, "utf8");
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e.code !== "ENOENT") throw e;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Missing file → create.
|
|
48
|
+
if (existing === null) {
|
|
49
|
+
const content = wanted.join("\n") + "\n";
|
|
50
|
+
fs.writeFileSync(file, content, "utf8");
|
|
51
|
+
return { created: true, appended: wanted.slice(), skipped: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Existing file → append missing entries only.
|
|
55
|
+
const lines = existing.split("\n").map(normLine);
|
|
56
|
+
// Drop the phantom last element from a trailing newline so it doesn't
|
|
57
|
+
// count as content, but remember whether the file ended with one.
|
|
58
|
+
const endsWithNewline = existing.endsWith("\n");
|
|
59
|
+
const contentLines = endsWithNewline ? lines.slice(0, -1) : lines;
|
|
60
|
+
const present = new Set(contentLines);
|
|
61
|
+
|
|
62
|
+
const appended = [];
|
|
63
|
+
const skipped = [];
|
|
64
|
+
for (const entry of wanted) {
|
|
65
|
+
if (present.has(entry)) {
|
|
66
|
+
skipped.push(entry);
|
|
67
|
+
} else {
|
|
68
|
+
appended.push(entry);
|
|
69
|
+
present.add(entry); // dedupe within `wanted` itself
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (appended.length === 0) {
|
|
74
|
+
return { created: false, appended: [], skipped };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Preserve existing bytes; ensure exactly one newline before appending.
|
|
78
|
+
let out = existing;
|
|
79
|
+
if (!out.endsWith("\n")) out += "\n";
|
|
80
|
+
out += appended.join("\n") + "\n";
|
|
81
|
+
fs.writeFileSync(file, out, "utf8");
|
|
82
|
+
return { created: false, appended, skipped };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Render the exact diff the setup conversation shows the user for consent.
|
|
86
|
+
// Returns a human-readable description of what would change.
|
|
87
|
+
function describeGitignoreChange(repoPath, entries) {
|
|
88
|
+
const file = gitignorePath(repoPath);
|
|
89
|
+
const wanted = entries || CREW_GITIGNORE_ENTRIES;
|
|
90
|
+
|
|
91
|
+
let existing = null;
|
|
92
|
+
try {
|
|
93
|
+
existing = fs.readFileSync(file, "utf8");
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e.code !== "ENOENT") throw e;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (existing === null) {
|
|
99
|
+
return {
|
|
100
|
+
file,
|
|
101
|
+
mode: "create",
|
|
102
|
+
diff: wanted.map((e) => "+ " + e).join("\n"),
|
|
103
|
+
lines: wanted.slice(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = existing.split("\n").map(normLine);
|
|
108
|
+
const endsWithNewline = existing.endsWith("\n");
|
|
109
|
+
const contentLines = endsWithNewline ? lines.slice(0, -1) : lines;
|
|
110
|
+
const present = new Set(contentLines);
|
|
111
|
+
const missing = wanted.filter((e) => !present.has(e));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
file,
|
|
115
|
+
mode: missing.length === 0 ? "noop" : "append",
|
|
116
|
+
diff: missing.map((e) => "+ " + e).join("\n"),
|
|
117
|
+
lines: missing,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
CREW_GITIGNORE_ENTRIES,
|
|
123
|
+
ensureGitignoreEntries,
|
|
124
|
+
describeGitignoreChange,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// CLI: node lib/gitignore.js --repo <path> [--dry-run]
|
|
128
|
+
// Used by crew-init and by tests. --dry-run prints the diff without writing.
|
|
129
|
+
if (require.main === module) {
|
|
130
|
+
const argv = process.argv.slice(2);
|
|
131
|
+
const repoIdx = argv.indexOf("--repo");
|
|
132
|
+
const repoPath = repoIdx >= 0 ? argv[repoIdx + 1] : null;
|
|
133
|
+
const dryRun = argv.includes("--dry-run");
|
|
134
|
+
if (!repoPath) {
|
|
135
|
+
console.error("Usage: node lib/gitignore.js --repo <path> [--dry-run]");
|
|
136
|
+
process.exit(2);
|
|
137
|
+
}
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
const d = describeGitignoreChange(repoPath);
|
|
140
|
+
console.log("File: " + d.file);
|
|
141
|
+
console.log("Mode: " + d.mode);
|
|
142
|
+
if (d.diff) console.log(d.diff);
|
|
143
|
+
} else {
|
|
144
|
+
const r = ensureGitignoreEntries(repoPath);
|
|
145
|
+
console.log(JSON.stringify(r));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/repo-orchestration.js — scaffold repository-local .orchestration/ state.
|
|
3
|
+
//
|
|
4
|
+
// When a project is registered with a repo path, the crew scaffolds
|
|
5
|
+
// $REPO/.orchestration/ with version-controlled project configuration
|
|
6
|
+
// and an ignored user/ directory for personal/local config:
|
|
7
|
+
//
|
|
8
|
+
// $REPO/.orchestration/
|
|
9
|
+
// workflows/ # version-controlled — seeded from $CREW_REPO/seed/workflows/
|
|
10
|
+
// identities/ # version-controlled — seeded from $CREW_REPO/identities/
|
|
11
|
+
// phases/ # version-controlled — seeded from $CREW_REPO/seed/phases/ (if exists)
|
|
12
|
+
// user/ # IGNORED — local config, must be in .gitignore
|
|
13
|
+
//
|
|
14
|
+
// Seeding uses no-clobber semantics: existing project customizations are
|
|
15
|
+
// never overwritten. Idempotent: re-running is a no-op.
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
function ensureDir(dirPath) {
|
|
21
|
+
if (!fs.existsSync(dirPath)) {
|
|
22
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Copy files from srcDir to destDir, skipping existing files (no-clobber).
|
|
29
|
+
// Returns { created: [relative paths], skipped: [relative paths] }.
|
|
30
|
+
function seedDir(srcDir, destDir, patterns) {
|
|
31
|
+
const created = [];
|
|
32
|
+
const skipped = [];
|
|
33
|
+
if (!fs.existsSync(srcDir)) return { created, skipped };
|
|
34
|
+
|
|
35
|
+
ensureDir(destDir);
|
|
36
|
+
const files = fs.readdirSync(srcDir);
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
if (patterns && !patterns.some((p) => file.endsWith(p))) continue;
|
|
39
|
+
const src = path.join(srcDir, file);
|
|
40
|
+
const dest = path.join(destDir, file);
|
|
41
|
+
if (fs.statSync(src).isDirectory()) continue;
|
|
42
|
+
if (fs.existsSync(dest)) {
|
|
43
|
+
skipped.push(file);
|
|
44
|
+
} else {
|
|
45
|
+
fs.copyFileSync(src, dest);
|
|
46
|
+
created.push(file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { created, skipped };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scaffoldRepoOrchestration(repoPath, crewRepoPath) {
|
|
53
|
+
const orchDir = path.join(repoPath, ".orchestration");
|
|
54
|
+
const result = {
|
|
55
|
+
orchDir,
|
|
56
|
+
dirsCreated: [],
|
|
57
|
+
workflows: { created: [], skipped: [] },
|
|
58
|
+
identities: { created: [], skipped: [] },
|
|
59
|
+
phases: { created: [], skipped: [] },
|
|
60
|
+
userDir: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Create the four subdirectories.
|
|
64
|
+
for (const sub of ["workflows", "identities", "phases", "user"]) {
|
|
65
|
+
if (ensureDir(path.join(orchDir, sub))) {
|
|
66
|
+
result.dirsCreated.push(sub);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
result.userDir = true;
|
|
70
|
+
|
|
71
|
+
// Seed workflows from platform defaults (markdown docs, not JS).
|
|
72
|
+
result.workflows = seedDir(
|
|
73
|
+
path.join(crewRepoPath, "seed", "workflows"),
|
|
74
|
+
path.join(orchDir, "workflows"),
|
|
75
|
+
[".md"]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Seed identities (markdown + portraits).
|
|
79
|
+
result.identities = seedDir(
|
|
80
|
+
path.join(crewRepoPath, "identities"),
|
|
81
|
+
path.join(orchDir, "identities"),
|
|
82
|
+
[".md", ".png"]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Seed phases (if the crew repo has them).
|
|
86
|
+
result.phases = seedDir(
|
|
87
|
+
path.join(crewRepoPath, "seed", "phases"),
|
|
88
|
+
path.join(orchDir, "phases"),
|
|
89
|
+
[".md"]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Ensure user/ has a README explaining it's for local config.
|
|
93
|
+
const userReadme = path.join(orchDir, "user", "README.md");
|
|
94
|
+
if (!fs.existsSync(userReadme)) {
|
|
95
|
+
fs.writeFileSync(
|
|
96
|
+
userReadme,
|
|
97
|
+
"# .orchestration/user/\n\n" +
|
|
98
|
+
"Personal and local configuration for this project. " +
|
|
99
|
+
"This directory is ignored by git (see .gitignore) and never committed.\n\n" +
|
|
100
|
+
"Put your local overrides, preferences, and machine-specific settings here.\n",
|
|
101
|
+
"utf8"
|
|
102
|
+
);
|
|
103
|
+
result.userDir = "created-with-readme";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
scaffoldRepoOrchestration,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// CLI: node lib/repo-orchestration.js --repo <path> --crew-repo <path>
|
|
114
|
+
if (require.main === module) {
|
|
115
|
+
const argv = process.argv.slice(2);
|
|
116
|
+
const repoIdx = argv.indexOf("--repo");
|
|
117
|
+
const crewRepoIdx = argv.indexOf("--crew-repo");
|
|
118
|
+
const repoPath = repoIdx >= 0 ? argv[repoIdx + 1] : null;
|
|
119
|
+
const crewRepoPath = crewRepoIdx >= 0 ? argv[crewRepoIdx + 1] : null;
|
|
120
|
+
if (!repoPath || !crewRepoPath) {
|
|
121
|
+
console.error("Usage: node lib/repo-orchestration.js --repo <path> --crew-repo <path>");
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
const r = scaffoldRepoOrchestration(repoPath, crewRepoPath);
|
|
125
|
+
console.log(JSON.stringify(r, null, 2));
|
|
126
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/sample-project.js — optional sample-project setup.
|
|
3
|
+
//
|
|
4
|
+
// Extracts the bundled flashcards sample (seed/sample-project.tar.gz),
|
|
5
|
+
// git-inits it, and creates the initial commit. Registration happens via
|
|
6
|
+
// the existing create-project action (no sample-specific endpoint).
|
|
7
|
+
//
|
|
8
|
+
// The sample is OPTIONAL — the caller (setup conversation) asks the user
|
|
9
|
+
// first. This script is the mechanical half: extract + git init + commit.
|
|
10
|
+
// Zero projects after an explicit decline is a supported state.
|
|
11
|
+
//
|
|
12
|
+
// Usage: node lib/sample-project.js --tarball <path> --dest <dir> --name <project-name>
|
|
13
|
+
// --tarball: path to sample-project.tar.gz (from the installed package)
|
|
14
|
+
// --dest: parent directory for the new project (project goes to <dest>/<name>)
|
|
15
|
+
// --name: directory/project name (e.g. "flashcards")
|
|
16
|
+
// Prints JSON: { path, committed, commitSha }
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const { execFileSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
function run(cmd, args, cwd) {
|
|
23
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setupSampleProject(tarballPath, destParent, name) {
|
|
27
|
+
if (!fs.existsSync(tarballPath)) {
|
|
28
|
+
throw new Error("sample tarball not found: " + tarballPath);
|
|
29
|
+
}
|
|
30
|
+
const destDir = path.join(destParent, name);
|
|
31
|
+
if (fs.existsSync(destDir)) {
|
|
32
|
+
throw new Error("destination already exists: " + destDir + " — refusing to overwrite");
|
|
33
|
+
}
|
|
34
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Extract. The tarball wraps files in a sample-project/ prefix —
|
|
37
|
+
// strip it so the project lands directly in destDir.
|
|
38
|
+
run("tar", ["-xzf", tarballPath, "-C", destDir, "--strip-components=1"]);
|
|
39
|
+
|
|
40
|
+
// Git init + initial commit. No remote required — workflows must not
|
|
41
|
+
// treat a missing remote as a defect.
|
|
42
|
+
run("git", ["init"], destDir);
|
|
43
|
+
run("git", ["add", "-A"], destDir);
|
|
44
|
+
// Use deterministic identity for the seed commit; the user's identity
|
|
45
|
+
// takes over for their own commits.
|
|
46
|
+
const env = Object.assign({}, process.env, {
|
|
47
|
+
GIT_AUTHOR_NAME: "Muse Crew",
|
|
48
|
+
GIT_AUTHOR_EMAIL: "crew@localhost",
|
|
49
|
+
GIT_COMMITTER_NAME: "Muse Crew",
|
|
50
|
+
GIT_COMMITTER_EMAIL: "crew@localhost",
|
|
51
|
+
});
|
|
52
|
+
execFileSync("git", ["commit", "-m", "Initial commit: flashcards sample project", "--quiet"], {
|
|
53
|
+
cwd: destDir,
|
|
54
|
+
env,
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
const sha = run("git", ["rev-parse", "HEAD"], destDir);
|
|
59
|
+
|
|
60
|
+
return { path: destDir, committed: true, commitSha: sha };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { setupSampleProject };
|
|
64
|
+
|
|
65
|
+
if (require.main === module) {
|
|
66
|
+
const argv = process.argv.slice(2);
|
|
67
|
+
const get = (flag) => {
|
|
68
|
+
const i = argv.indexOf(flag);
|
|
69
|
+
return i >= 0 ? argv[i + 1] : null;
|
|
70
|
+
};
|
|
71
|
+
const tarball = get("--tarball");
|
|
72
|
+
const dest = get("--dest");
|
|
73
|
+
const name = get("--name");
|
|
74
|
+
if (!tarball || !dest || !name) {
|
|
75
|
+
console.error("Usage: node lib/sample-project.js --tarball <path> --dest <dir> --name <project-name>");
|
|
76
|
+
process.exit(2);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
console.log(JSON.stringify(setupSampleProject(tarball, dest, name)));
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error("sample-project setup failed: " + (e.message || e));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lib/setup-project-repo.js — repo-local setup for a registered project.
|
|
3
|
+
//
|
|
4
|
+
// Composes the deterministic building blocks after create-project:
|
|
5
|
+
// 1. Scaffold $REPO/.orchestration/ (workflows/identities/phases versioned, user/ ignored)
|
|
6
|
+
// 2. Optionally update .gitignore (only with explicit --consent)
|
|
7
|
+
//
|
|
8
|
+
// This is the CLI path equivalent of what crew-init's dashboard mode does.
|
|
9
|
+
// The dashboard calls the same script. No new API endpoint — composition.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node lib/setup-project-repo.js --repo <path> --crew-repo <path> [--consent]
|
|
13
|
+
// --consent: user approved the .gitignore change (from the setup conversation)
|
|
14
|
+
// Prints JSON: { orchestration: {...}, gitignore: {...} | { declined: true } }
|
|
15
|
+
|
|
16
|
+
const { scaffoldRepoOrchestration } = require("./repo-orchestration");
|
|
17
|
+
const { ensureGitignoreEntries, describeGitignoreChange } = require("./gitignore");
|
|
18
|
+
|
|
19
|
+
function main() {
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const get = (flag) => {
|
|
22
|
+
const i = argv.indexOf(flag);
|
|
23
|
+
return i >= 0 ? argv[i + 1] : null;
|
|
24
|
+
};
|
|
25
|
+
const repoPath = get("--repo");
|
|
26
|
+
const crewRepoPath = get("--crew-repo");
|
|
27
|
+
const consent = argv.includes("--consent");
|
|
28
|
+
|
|
29
|
+
if (!repoPath || !crewRepoPath) {
|
|
30
|
+
console.error("Usage: node lib/setup-project-repo.js --repo <path> --crew-repo <path> [--consent]");
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const orchestration = scaffoldRepoOrchestration(repoPath, crewRepoPath);
|
|
35
|
+
|
|
36
|
+
let gitignore;
|
|
37
|
+
if (consent) {
|
|
38
|
+
gitignore = ensureGitignoreEntries(repoPath);
|
|
39
|
+
} else {
|
|
40
|
+
// Declined (or non-interactive default): report what WOULD change.
|
|
41
|
+
const preview = describeGitignoreChange(repoPath);
|
|
42
|
+
gitignore = { declined: true, wouldChange: preview };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(JSON.stringify({ orchestration, gitignore }, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (require.main === module) main();
|
|
49
|
+
|
|
50
|
+
module.exports = { main };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muse-crew",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Opinionated orchestration for Muse \u2014 workflows, identities, and tooling for autonomous software development.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"playwright-core": "1.63.0"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
package/workflows/crew-init.js
CHANGED
|
@@ -339,22 +339,19 @@ log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped"
|
|
|
339
339
|
|
|
340
340
|
// ── Gitignore consent execution ─────────────────────────────────────
|
|
341
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
|
|
342
|
+
// and we have a repo (dashboard mode), append crew-owned paths to .gitignore
|
|
343
|
+
// via the deterministic lib/gitignore.js (no agent judgment — the agent
|
|
344
|
+
// just runs the fixed command; all merge logic is in the tested JS module).
|
|
343
345
|
// This is the ONLY user file outside .orchestration/ that the crew modifies.
|
|
344
346
|
if (gitignoreConsent && dashboardRepoPath) {
|
|
345
347
|
var gitignoreResult;
|
|
346
348
|
try {
|
|
347
349
|
gitignoreResult = await agent(
|
|
348
|
-
"
|
|
349
|
-
"
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
"
|
|
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 }.",
|
|
350
|
+
"Run the deterministic gitignore updater. Do not decide anything — just run the command and return its JSON output.\n\n" +
|
|
351
|
+
"Command:\n" +
|
|
352
|
+
"node " + crewHome + "/current/lib/gitignore.js --repo \"" + gateFacts.dashboardRepoExpanded + "\"\n\n" +
|
|
353
|
+
"The script prints one JSON line: { created, appended[], skipped[] }.\n" +
|
|
354
|
+
"Return that JSON verbatim.",
|
|
358
355
|
{
|
|
359
356
|
key: "scaffold-gitignore",
|
|
360
357
|
label: "Update .gitignore for crew paths",
|
|
@@ -376,7 +373,7 @@ if (gitignoreConsent && dashboardRepoPath) {
|
|
|
376
373
|
log("Gitignore update failed (non-fatal): " + String(e.message || e));
|
|
377
374
|
}
|
|
378
375
|
} else if (dashboardRepoPath) {
|
|
379
|
-
log("Gitignore: user declined — add '.worktrees/' to " + dashboardRepoPath + "/.gitignore manually.");
|
|
376
|
+
log("Gitignore: user declined — add '.worktrees/' and '.orchestration/user/' to " + dashboardRepoPath + "/.gitignore manually.");
|
|
380
377
|
} else {
|
|
381
378
|
log("Gitignore: no repo (CLI-only mode) — skipping.");
|
|
382
379
|
}
|
|
@@ -458,6 +455,39 @@ try {
|
|
|
458
455
|
log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
|
|
459
456
|
} // end dashboard-mode project registration
|
|
460
457
|
|
|
458
|
+
// ── Repo-local .orchestration/ scaffold ─────────────────────────────
|
|
459
|
+
// Scaffold $REPO/.orchestration/ in the dashboard repo (version-controlled
|
|
460
|
+
// workflows/identities/phases + ignored user/). Deterministic — the agent
|
|
461
|
+
// runs the fixed command; all logic is in the tested lib module.
|
|
462
|
+
if (dashboardRepoPath) {
|
|
463
|
+
var repoOrchResult;
|
|
464
|
+
try {
|
|
465
|
+
repoOrchResult = await agent(
|
|
466
|
+
"Scaffold the repository-local .orchestration/ directory. Do not decide anything — just run the command and return its JSON output.\n\n" +
|
|
467
|
+
"Command:\n" +
|
|
468
|
+
"node " + crewHome + "/current/lib/repo-orchestration.js --repo \"" + gateFacts.dashboardRepoExpanded + "\" --crew-repo \"" + crewRepoPath + "\"\n\n" +
|
|
469
|
+
"The script prints JSON with dirsCreated, workflows, identities, phases.\n" +
|
|
470
|
+
"Return that JSON verbatim.",
|
|
471
|
+
{
|
|
472
|
+
key: "scaffold-repo-orch",
|
|
473
|
+
label: "Scaffold repo-local .orchestration",
|
|
474
|
+
schema: {
|
|
475
|
+
type: "object",
|
|
476
|
+
properties: {
|
|
477
|
+
orchDir: { type: "string" },
|
|
478
|
+
dirsCreated: { type: "array", items: { type: "string" } }
|
|
479
|
+
},
|
|
480
|
+
required: ["orchDir", "dirsCreated"]
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
log("Repo-orchestration: scaffolded " + repoOrchResult.orchDir +
|
|
485
|
+
" (dirs: [" + (repoOrchResult.dirsCreated || []).join(", ") + "])");
|
|
486
|
+
} catch (e) {
|
|
487
|
+
log("Repo-orchestration scaffold failed (non-fatal): " + String(e.message || e));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
461
491
|
// ── Scheduler identity (2026-09-16) ──────────────────────────────────
|
|
462
492
|
// The crew's scheduler identity is dashboard-independent and chosen once.
|
|
463
493
|
// First init uses the crew-home basename; a re-run keeps the instanceId
|