muse-crew 0.10.1 → 0.11.1

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,184 @@
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.
144
+
145
+ ## 8. Setup commits its scaffold (0.11.1)
146
+
147
+ **Defect (room #13, 2026-09-17):** 0.11.0's setup wrote `.orchestration/`
148
+ (and the consented `.gitignore` update) but never committed them. The
149
+ first task's Integrate then refused to merge onto the dirty main — twice,
150
+ deterministically. An uncommitted scaffold deterministically breaks the
151
+ first task after setup.
152
+
153
+ **Contract:** setup leaves the repo's main checkout clean. After the
154
+ scaffold + `.gitignore` steps, `lib/commit-scaffold.js` commits exactly
155
+ the crew's paths (`.orchestration/`, plus `.gitignore` when the setup step
156
+ changed it under consent). The commit message names what was staged,
157
+ computed from the index.
158
+
159
+ **Rules:**
160
+ - Only the crew's paths are ever staged. The user's unrelated changes
161
+ (tracked modifications, untracked files) are never swept in.
162
+ - Idempotent: nothing new staged → no commit, `{ committed: false,
163
+ reason: "nothing-to-commit" }`. Never an empty commit.
164
+ - Missing `.orchestration/` → throw (the scaffold step did not run).
165
+ - Commit failures (e.g. no git identity) throw loudly with git's stderr.
166
+ No identity is ever invented — the commit belongs to the user; the
167
+ caller configures identity and re-runs.
168
+ - With `--consent`, `.gitignore` must be clean before setup touches it:
169
+ a pre-existing dirty `.gitignore` fails setup loudly (refuse — never
170
+ sweep the user's edits into the setup commit). `.orchestration/` is
171
+ crew-owned: pre-existing uncommitted content there is committed, not
172
+ refused (this also repairs 0.11.0 rooms).
173
+ - `lib/setup-project-repo.js` (the composer) runs all three steps and
174
+ exits non-zero on any failure. crew-init's dashboard mode calls the
175
+ composer (one agent call, replacing the old two-call shape); a failed
176
+ commit is logged loudly and surfaced as `repoSetupWarning` in the init
177
+ summary, since the first task will fail Integrate until the scaffold
178
+ is committed.
179
+
180
+ **Tests:** `tests/commit-scaffold.test.js` (behavioral: clean tree after
181
+ setup, accurate message, idempotency, non-repo no-op, missing scaffold
182
+ throws, missing identity throws, composer refusal on dirty `.gitignore`,
183
+ declined consent leaves `.gitignore` untouched; structural: crew-init
184
+ calls the composer).
package/lib/AGENTS.md CHANGED
@@ -27,3 +27,8 @@ 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, setup-commit 2026-09-18): runs the `.orchestration/` scaffold plus the `.gitignore` update (only with explicit `--consent`), then COMMITS both via `commit-scaffold.js` so the repo's main checkout is clean when setup finishes (room #13: an uncommitted scaffold deterministically broke the first task's Integrate). Refuses loudly when `.gitignore` has pre-existing uncommitted changes (never sweeps the user's edits into the setup commit); `.orchestration/` is crew-owned so pre-existing content there is committed, not refused. 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]`; prints `{ orchestration, gitignore, commit }`, exits non-zero on any failure.
33
+ - `commit-scaffold.js` — deterministic setup-scaffold commit (2026-09-18): stages only `.orchestration/` (+ `.gitignore` with `--gitignore`) and commits with a message naming exactly what was staged. Idempotent (`nothing-to-commit`, never an empty commit); not-a-git-repo is a clean no-op; missing `.orchestration/` and commit failures (e.g. no git identity) throw loudly — the commit belongs to the user, so no identity is ever invented here. Exports `isPathDirty` (porcelain check for one path) for the composer's pre-mutation guard. CLI: `--repo <path> [--gitignore]`.
34
+ - `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,121 @@
1
+ #!/usr/bin/env node
2
+ // lib/commit-scaffold.js — commit the crew's setup scaffold files.
3
+ //
4
+ // After project setup writes $REPO/.orchestration/ (and, with the user's
5
+ // consent, updates .gitignore), this module commits exactly those paths so
6
+ // the repo's main checkout is clean when setup finishes.
7
+ //
8
+ // Why this exists (room #13, 2026-09-17): 0.11.0's setup wrote the scaffold
9
+ // but never committed it. The first task's Integrate then refused to merge
10
+ // onto the dirty main — twice in a row, deterministically. An uncommitted
11
+ // scaffold deterministically breaks the first task after setup. Setup must
12
+ // leave main clean; this is the mechanical half of that contract.
13
+ //
14
+ // Contract:
15
+ // - Only ever stages `.orchestration/` and (with --gitignore) `.gitignore`.
16
+ // The user's unrelated changes are never swept into the setup commit.
17
+ // - Idempotent: nothing new staged → { committed: false, reason:
18
+ // "nothing-to-commit" }; never creates an empty commit.
19
+ // - Not a git repo → { committed: false, reason: "not-a-git-repo" }.
20
+ // There is no main to keep clean, so there is nothing to do.
21
+ // - Missing `.orchestration/` → throws. The scaffold step did not run;
22
+ // fail loudly, never silently skip.
23
+ // - Commit failures (e.g. no git identity configured) throw with git's
24
+ // stderr. The caller configures identity and re-runs; never invent an
25
+ // identity here — the commit belongs to the user, not the crew.
26
+ // - The commit message names exactly what was staged (computed from the
27
+ // index, not from flags).
28
+ //
29
+ // Usage:
30
+ // node lib/commit-scaffold.js --repo <path> [--gitignore]
31
+ // --gitignore: the setup step modified .gitignore (consent was given);
32
+ // include it in the commit.
33
+ // Prints JSON: { committed, sha, reason }
34
+
35
+ const fs = require("fs");
36
+ const path = require("path");
37
+ const { execFileSync } = require("child_process");
38
+
39
+ function git(repoPath, args, env) {
40
+ try {
41
+ // No .trim(): callers that parse fixed-width output (porcelain) need
42
+ // the raw lines; scalar callers trim themselves.
43
+ return execFileSync("git", args, {
44
+ cwd: repoPath,
45
+ encoding: "utf8",
46
+ env: env || process.env,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ } catch (e) {
50
+ const stderr = e.stderr ? String(e.stderr).trim() : "";
51
+ throw new Error("git " + args.join(" ") + " failed: " + (stderr || e.message));
52
+ }
53
+ }
54
+
55
+ // Porcelain status lines touching exactly relPath (e.g. ".gitignore").
56
+ // Used by the composer as a pre-mutation guard; exported for tests.
57
+ function isPathDirty(repoPath, relPath) {
58
+ const out = git(repoPath, ["status", "--porcelain", "--", relPath]);
59
+ return out.split("\n").some(function (line) {
60
+ if (line.length < 4) return false;
61
+ const p = line.slice(3).replace(/^"|"$/g, "");
62
+ return p === relPath;
63
+ });
64
+ }
65
+
66
+ function commitScaffold(repoPath, opts) {
67
+ opts = opts || {};
68
+ const includeGitignore = opts.includeGitignore === true;
69
+
70
+ if (!fs.existsSync(path.join(repoPath, ".git"))) {
71
+ return { committed: false, sha: null, reason: "not-a-git-repo" };
72
+ }
73
+ if (!fs.existsSync(path.join(repoPath, ".orchestration"))) {
74
+ throw new Error(
75
+ "no .orchestration/ scaffold found in " + repoPath + " — run the scaffold step before committing"
76
+ );
77
+ }
78
+
79
+ const paths = [".orchestration"];
80
+ if (includeGitignore) paths.push(".gitignore");
81
+ git(repoPath, ["add", "--"].concat(paths));
82
+
83
+ const staged = git(repoPath, ["diff", "--cached", "--name-only"]).trim();
84
+ if (!staged) {
85
+ return { committed: false, sha: null, reason: "nothing-to-commit" };
86
+ }
87
+
88
+ const stagedFiles = staged.split("\n");
89
+ const subject = stagedFiles.indexOf(".gitignore") !== -1
90
+ ? "Muse Crew setup: commit .orchestration scaffold and crew .gitignore entries"
91
+ : "Muse Crew setup: commit .orchestration scaffold";
92
+ const body =
93
+ "Setup leaves the repo's main checkout clean: these are the crew-owned\n" +
94
+ "files written during project setup. Integrate refuses to merge onto a\n" +
95
+ "dirty main, so they are committed here instead of left uncommitted.";
96
+ git(repoPath, ["commit", "-m", subject, "-m", body, "--quiet"]);
97
+ const sha = git(repoPath, ["rev-parse", "HEAD"]).trim();
98
+
99
+ return { committed: true, sha: sha, reason: null };
100
+ }
101
+
102
+ module.exports = { commitScaffold, isPathDirty };
103
+
104
+ if (require.main === module) {
105
+ const argv = process.argv.slice(2);
106
+ const get = (flag) => {
107
+ const i = argv.indexOf(flag);
108
+ return i >= 0 ? argv[i + 1] : null;
109
+ };
110
+ const repoPath = get("--repo");
111
+ if (!repoPath) {
112
+ console.error("Usage: node lib/commit-scaffold.js --repo <path> [--gitignore]");
113
+ process.exit(2);
114
+ }
115
+ try {
116
+ console.log(JSON.stringify(commitScaffold(repoPath, { includeGitignore: argv.includes("--gitignore") })));
117
+ } catch (e) {
118
+ console.error("commit-scaffold failed: " + (e.message || e));
119
+ process.exit(1);
120
+ }
121
+ }
@@ -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,96 @@
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
+ // 3. Commit the scaffold (and the .gitignore update when consented) so
8
+ // the repo's main checkout is clean when setup finishes.
9
+ //
10
+ // Step 3 is the 2026-09-18 fix for the room #13 defect: 0.11.0 wrote the
11
+ // scaffold but never committed it, and the first task's Integrate then
12
+ // refused to merge onto the dirty main — twice, deterministically. Setup
13
+ // must leave main clean; the commit is mechanical, not agent judgment.
14
+ //
15
+ // This is the CLI path equivalent of what crew-init's dashboard mode does.
16
+ // The dashboard calls the same script. No new API endpoint — composition.
17
+ //
18
+ // Guards:
19
+ // - With --consent, .gitignore must be clean before we touch it. The
20
+ // commit only ever stages .orchestration/ (+ .gitignore when we changed
21
+ // it), but a pre-existing dirty .gitignore would be swept into our
22
+ // commit by `git add` — so refuse loudly instead. Commit or stash it,
23
+ // then re-run.
24
+ // - .orchestration/ is crew-owned: pre-existing uncommitted content there
25
+ // (e.g. left dirty by 0.11.0) is committed, not refused.
26
+ //
27
+ // Usage:
28
+ // node lib/setup-project-repo.js --repo <path> --crew-repo <path> [--consent]
29
+ // --consent: user approved the .gitignore change (from the setup conversation)
30
+ // Prints JSON: { orchestration: {...}, gitignore: {...} | { declined: true }, commit: {...} }
31
+ // Exits non-zero on any failure (fail closed, loud).
32
+
33
+ const fs = require("fs");
34
+ const path = require("path");
35
+ const { scaffoldRepoOrchestration } = require("./repo-orchestration");
36
+ const { ensureGitignoreEntries, describeGitignoreChange } = require("./gitignore");
37
+ const { commitScaffold, isPathDirty } = require("./commit-scaffold");
38
+
39
+ function main() {
40
+ const argv = process.argv.slice(2);
41
+ const get = (flag) => {
42
+ const i = argv.indexOf(flag);
43
+ return i >= 0 ? argv[i + 1] : null;
44
+ };
45
+ const repoPath = get("--repo");
46
+ const crewRepoPath = get("--crew-repo");
47
+ const consent = argv.includes("--consent");
48
+
49
+ if (!repoPath || !crewRepoPath) {
50
+ console.error("Usage: node lib/setup-project-repo.js --repo <path> --crew-repo <path> [--consent]");
51
+ process.exit(2);
52
+ }
53
+
54
+ try {
55
+ // Guard: never sweep the user's own .gitignore edits into our commit.
56
+ if (consent && fs.existsSync(path.join(repoPath, ".git")) && isPathDirty(repoPath, ".gitignore")) {
57
+ throw new Error(
58
+ "setup refused: .gitignore has uncommitted changes not made by setup — " +
59
+ "commit or stash them, then re-run setup"
60
+ );
61
+ }
62
+
63
+ const orchestration = scaffoldRepoOrchestration(repoPath, crewRepoPath);
64
+
65
+ let gitignore;
66
+ if (consent) {
67
+ gitignore = ensureGitignoreEntries(repoPath);
68
+ } else {
69
+ // Declined (or non-interactive default): report what WOULD change.
70
+ const preview = describeGitignoreChange(repoPath);
71
+ gitignore = { declined: true, wouldChange: preview };
72
+ }
73
+
74
+ let commit;
75
+ try {
76
+ commit = commitScaffold(repoPath, { includeGitignore: consent });
77
+ } catch (commitErr) {
78
+ throw new Error(
79
+ "commit failed: " +
80
+ commitErr.message +
81
+ " — the repo has uncommitted crew files; configure git identity and run: " +
82
+ "node lib/commit-scaffold.js --repo <path>" +
83
+ (consent ? " --gitignore" : "")
84
+ );
85
+ }
86
+
87
+ console.log(JSON.stringify({ orchestration, gitignore, commit }, null, 2));
88
+ } catch (e) {
89
+ console.error("setup-project-repo failed: " + (e.message || e));
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ if (require.main === module) main();
95
+
96
+ 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.1",
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
+ }
@@ -337,50 +337,6 @@ var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0
337
337
  var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
338
338
  log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
339
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
-
384
340
  // ── Phase 3: Project registration ─────────────────────────────────────
385
341
  // Registers the first project in crew-state.db via the Crew API. In
386
342
  // dashboard mode, the dashboard becomes the crew's first project (so the
@@ -458,6 +414,84 @@ try {
458
414
  log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
459
415
  } // end dashboard-mode project registration
460
416
 
417
+ // ── Project repo setup (2026-09-18) ─────────────────────────────────
418
+ // One deterministic composer — lib/setup-project-repo.js — scaffolds
419
+ // $REPO/.orchestration/, applies the consented .gitignore update, and COMMITS
420
+ // both so the repo's main checkout is clean when setup finishes.
421
+ // Room #13 (2026-09-17): the old two-call shape wrote the scaffold but never
422
+ // committed it, and the first task's Integrate then refused to merge onto the
423
+ // dirty main — twice, deterministically. This replaces both calls; the agent
424
+ // still runs one fixed command and all logic stays in the tested lib modules.
425
+ // Best-effort as before (a failed setup never blocks init), but a failed
426
+ // COMMIT is logged loudly: the repo is dirty and the first task will fail
427
+ // Integrate until the scaffold is committed.
428
+ var setupRepoWarning = null;
429
+ var setupRepoResult = null;
430
+ if (dashboardRepoPath) {
431
+ try {
432
+ setupRepoResult = await agent(
433
+ "Run the deterministic project-repo setup composer. Do not decide anything — just run the command and return its JSON output.\n\n" +
434
+ "Command:\n" +
435
+ "node " + crewHome + "/current/lib/setup-project-repo.js --repo \"" + gateFacts.dashboardRepoExpanded + "\" --crew-repo \"" + crewRepoPath + "\"" + (gitignoreConsent ? " --consent" : "") + "\n\n" +
436
+ "The script prints one JSON object: { orchestration, gitignore, commit }.\n" +
437
+ "orchestration carries orchDir, dirsCreated, and per-area created/skipped arrays;\n" +
438
+ "gitignore carries created/appended/skipped (or declined:true with a wouldChange preview);\n" +
439
+ "commit carries { committed, sha, reason }.\n" +
440
+ "Return that JSON verbatim.",
441
+ {
442
+ key: "scaffold-repo-setup",
443
+ label: "Set up project repo (scaffold + gitignore + commit)",
444
+ schema: {
445
+ type: "object",
446
+ properties: {
447
+ orchestration: {
448
+ type: "object",
449
+ properties: {
450
+ orchDir: { type: "string" },
451
+ dirsCreated: { type: "array", items: { type: "string" } }
452
+ },
453
+ required: ["orchDir", "dirsCreated"]
454
+ },
455
+ gitignore: { type: "object" },
456
+ commit: {
457
+ type: "object",
458
+ properties: {
459
+ committed: { type: "boolean" },
460
+ sha: { type: ["string", "null"] },
461
+ reason: { type: ["string", "null"] }
462
+ },
463
+ required: ["committed"]
464
+ }
465
+ },
466
+ required: ["orchestration", "gitignore", "commit"]
467
+ }
468
+ }
469
+ );
470
+ var orchDirs = (setupRepoResult.orchestration.dirsCreated || []).join(", ");
471
+ var commitMsg = setupRepoResult.commit.committed
472
+ ? "committed " + setupRepoResult.commit.sha
473
+ : "not committed (" + setupRepoResult.commit.reason + ")";
474
+ log("Repo setup: scaffolded " + setupRepoResult.orchestration.orchDir +
475
+ " (dirs: [" + orchDirs + "]); gitignore " +
476
+ (gitignoreConsent ? "update consented" : "declined by user — add '.worktrees/' and '.orchestration/user/' manually") +
477
+ "; scaffold " + commitMsg);
478
+ if (!setupRepoResult.commit.committed && setupRepoResult.commit.reason !== "nothing-to-commit") {
479
+ setupRepoWarning = "Project repo setup left the scaffold uncommitted (" +
480
+ setupRepoResult.commit.reason + ") — the first task's Integrate will refuse to merge until it is committed.";
481
+ log("WARNING: " + setupRepoWarning);
482
+ }
483
+ } catch (e) {
484
+ var setupErr = String(e.message || e);
485
+ setupRepoWarning = "Project repo setup failed: " + setupErr + " — continuing without the repo scaffold.";
486
+ if (/commit failed/i.test(setupErr)) {
487
+ setupRepoWarning += " The repo has uncommitted crew files; the first task's Integrate will refuse to merge until they are committed.";
488
+ }
489
+ log(setupRepoWarning + " (non-fatal)");
490
+ }
491
+ } else {
492
+ log("Repo setup: no repo (CLI-only mode) — skipping.");
493
+ }
494
+
461
495
  // ── Scheduler identity (2026-09-16) ──────────────────────────────────
462
496
  // The crew's scheduler identity is dashboard-independent and chosen once.
463
497
  // First init uses the crew-home basename; a re-run keeps the instanceId
@@ -716,6 +750,7 @@ return {
716
750
  releaseHash: releaseResult.hash,
717
751
  scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
718
752
  project: projectResult.action,
753
+ repoSetupWarning: setupRepoWarning,
719
754
  crons: cronsResult.summary.crons,
720
755
  registry: cronsResult.summary.registry,
721
756
  autoUpdate: {