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 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
- - Workflow files — those live in the crew home's release, not in the project.
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>`.
@@ -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.10.1",
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
+ }
@@ -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
- "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 }.",
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