moflo 4.8.56-rc.9 → 4.8.57

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.
Files changed (26) hide show
  1. package/.claude/guidance/shipped/moflo-user-facing-language.md +34 -0
  2. package/.claude/skills/connector-builder/SKILL.md +52 -0
  3. package/.claude/skills/publish/SKILL.md +164 -0
  4. package/.claude/skills/reset-epic/SKILL.md +2 -1
  5. package/.claude/skills/spell-builder/SKILL.md +66 -0
  6. package/package.json +2 -2
  7. package/src/modules/cli/dist/src/commands/epic.js +95 -9
  8. package/src/modules/cli/dist/src/epic/runner-adapter.js +44 -2
  9. package/src/modules/cli/dist/src/epic/spells/auto-merge.yaml +31 -8
  10. package/src/modules/cli/dist/src/epic/spells/single-branch.yaml +32 -9
  11. package/src/modules/cli/dist/src/init/moflo-init.js +13 -4
  12. package/src/modules/cli/dist/src/mcp-tools/spell-tools.js +44 -0
  13. package/src/modules/cli/dist/src/mcp-tools/tool-names.js +1 -0
  14. package/src/modules/cli/dist/src/version.js +1 -1
  15. package/src/modules/cli/package.json +2 -2
  16. package/src/modules/plugins/dist/index.js +0 -1
  17. package/src/modules/shared/dist/utils/platform.js +0 -2
  18. package/src/modules/spells/dist/commands/bash-command.js +7 -2
  19. package/src/modules/spells/dist/commands/github-command.js +36 -0
  20. package/src/modules/spells/dist/core/permission-disclosure.js +36 -43
  21. package/src/modules/spells/dist/core/permission-resolver.js +17 -9
  22. package/src/modules/spells/dist/core/preflight-checker.js +198 -0
  23. package/src/modules/spells/dist/core/runner.js +95 -8
  24. package/src/modules/spells/dist/factory/runner-bridge.js +2 -0
  25. package/src/modules/spells/dist/factory/runner-factory.js +2 -1
  26. package/src/modules/spells/dist/schema/validator.js +56 -0
@@ -0,0 +1,34 @@
1
+ # User-Facing Language Guidelines
2
+
3
+ **Purpose:** Ensure all text shown to end users is approachable and non-alarming. MoFlo is used by developers and non-technical users alike — the language we use in output, prompts, reports, and error messages must be clear to someone who has never written code.
4
+
5
+ ---
6
+
7
+ ## Principles
8
+
9
+ 1. **Avoid technical jargon in user-visible output.** Terms like "destructive", "mutation", "elevated privileges", or "sandbox escape" are meaningful to engineers but alarming or confusing to non-technical users. Prefer plain risk-level language: "No risk", "Low risk", "Moderate risk", "Higher risk".
10
+
11
+ 2. **Explain what happens, not the mechanism.** Instead of "shell command execution", say "Runs commands on your machine". Instead of "fs:write capability", say "Creates, overwrites, or deletes files".
12
+
13
+ 3. **Reserve technical terms for internal code.** Type names, variable names, config keys, log prefixes, and developer documentation can use precise technical language. The boundary is: if the user sees it, simplify it.
14
+
15
+ 4. **Err on the side of calm.** When something requires attention, state the facts without dramatizing. "This step modifies files" is better than "WARNING: DESTRUCTIVE OPERATION DETECTED".
16
+
17
+ ---
18
+
19
+ ## Where This Applies
20
+
21
+ - Permission disclosure / risk analysis reports
22
+ - Error messages shown to users (not debug logs)
23
+ - CLI output and status messages
24
+ - MCP tool descriptions and response messages
25
+ - Spell dry-run and acceptance gate output
26
+ - Any `console.log` that a user will see during normal operation
27
+
28
+ ## Where This Does NOT Apply
29
+
30
+ - Internal type definitions and variable names
31
+ - Developer-facing config keys (e.g., `allowDestructive` in spell YAML)
32
+ - Debug/trace logs gated behind verbose flags
33
+ - Code comments and docstrings
34
+ - Test descriptions
@@ -414,6 +414,46 @@ export const <type>Command: StepCommand<<Type>StepConfig> = {
414
414
  ];
415
415
  },
416
416
 
417
+ // Optional: runtime preflight checks — run BEFORE any step executes.
418
+ // Use for validating runtime state (issue open, service reachable, etc).
419
+ // CRITICAL: the `reason` string IS the message the end user sees.
420
+ // Write it in plain English. State the problem AND the fix. No tool
421
+ // jargon, no exit codes, no internal identifiers.
422
+ //
423
+ // Default severity is 'fatal' (abort on failure). Set severity: 'warning'
424
+ // + resolutions when the user can safely choose how to proceed; in
425
+ // interactive runs they'll be prompted, in non-interactive runs warnings
426
+ // behave like fatals.
427
+ //
428
+ // preflight: [
429
+ // {
430
+ // name: '<service> reachable',
431
+ // severity: 'fatal',
432
+ // check: async (config, ctx) => {
433
+ // const ok = await ping(config.endpoint);
434
+ // if (ok) return { passed: true };
435
+ // return {
436
+ // passed: false,
437
+ // reason: `Can't reach ${config.endpoint}. Check your network connection or the service URL in your spell config.`,
438
+ // };
439
+ // },
440
+ // },
441
+ // {
442
+ // name: 'local cache fresh',
443
+ // severity: 'warning',
444
+ // resolutions: [
445
+ // { label: 'Refresh the cache now', command: '<type>-cli cache refresh' },
446
+ // { label: 'Continue with stale cache' },
447
+ // ],
448
+ // check: async (config) => {
449
+ // const stale = await isCacheStale(config.endpoint);
450
+ // return stale
451
+ // ? { passed: false, reason: 'Your local cache is more than 24 hours old and may produce outdated results.' }
452
+ // : { passed: true };
453
+ // },
454
+ // },
455
+ // ],
456
+
417
457
  // Optional: rollback on failure
418
458
  // async rollback(config, context) { /* undo side effects */ },
419
459
  };
@@ -421,6 +461,18 @@ export const <type>Command: StepCommand<<Type>StepConfig> = {
421
461
 
422
462
  Alternatively, use the `createStepCommand()` factory from `src/modules/spells/src/commands/create-step-command.ts` for compile-time type safety.
423
463
 
464
+ #### Preflight `reason` strings — write for humans
465
+
466
+ When your step declares `preflight` checks, the `reason` string returned on failure is shown verbatim to end users as the error message. Treat it as user-facing copy:
467
+
468
+ - Plain English, no command names, exit codes, or internal identifiers.
469
+ - State BOTH the problem and the fix.
470
+ - Assume a non-technical reader.
471
+
472
+ Good: `"You're not signed in to GitHub. Run: gh auth login"`
473
+ Bad: `"gh auth status exited with code 1"` — leaks implementation detail
474
+ Bad: `"auth check failed"` — tells the user nothing actionable
475
+
424
476
  ### Step 3: Generate Step Command Test
425
477
 
426
478
  Create at `tests/packages/spells/commands/<type>-command.test.ts`:
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: publish
3
+ description: Bump version, build, test, publish to npm, and install locally
4
+ arguments: "[patch|minor|major] [-rc]"
5
+ ---
6
+
7
+ # /publish - Version Bump, Build, Test & Publish
8
+
9
+ Automated release pipeline for moflo. Bumps version, commits, builds, tests, runs doctor, publishes to npm, and installs the new version locally.
10
+
11
+ **Arguments:** $ARGUMENTS
12
+
13
+ ## Usage
14
+
15
+ ```
16
+ /publish # patch bump (4.8.56 → 4.8.57)
17
+ /publish minor # minor bump (4.8.56 → 4.9.0)
18
+ /publish major # major bump (4.8.56 → 5.0.0)
19
+ /publish -rc # patch RC bump (4.8.56 → 4.8.57-rc.1, or 4.8.57-rc.1 → 4.8.57-rc.2)
20
+ /publish minor -rc # minor RC bump (4.8.56 → 4.9.0-rc.1)
21
+ /publish patch # explicit patch bump
22
+ ```
23
+
24
+ ## Step-by-Step Procedure
25
+
26
+ Follow docs/BUILD.md exactly. Every step must succeed before proceeding to the next.
27
+
28
+ ### Step 1: Parse Arguments
29
+
30
+ - Default bump type is `patch` if not specified
31
+ - If `-rc` flag is present, produce a release candidate version
32
+ - Determine the new version string:
33
+ - **Without `-rc`:** Use `npm version <patch|minor|major> --no-git-tag-version`
34
+ - **With `-rc`:** Calculate manually:
35
+ - If current version is already an RC of the same bump level (e.g., `4.8.57-rc.3`), increment the RC number (→ `4.8.57-rc.4`)
36
+ - Otherwise, bump the base version and append `-rc.1` (e.g., `4.8.56` → `4.8.57-rc.1`)
37
+ - Write with: `npm version <new-version> --no-git-tag-version`
38
+
39
+ ### Step 2: Rebuild After Version Bump
40
+
41
+ ```bash
42
+ npm run build
43
+ ```
44
+
45
+ This syncs the version to `src/modules/cli/src/version.ts` and `src/modules/cli/package.json` via the `version` lifecycle script, then compiles all TypeScript.
46
+
47
+ **Must exit 0.** If it fails, stop and fix the build error.
48
+
49
+ ### Step 3: Run Tests
50
+
51
+ ```bash
52
+ npm test
53
+ ```
54
+
55
+ **Must have 0 test file failures.** If any test files fail, retest them individually to distinguish real failures from flaky ones (per broken window theory). Fix all real failures before proceeding.
56
+
57
+ ### Step 4: Run Doctor
58
+
59
+ ```bash
60
+ npx moflo doctor --fix
61
+ ```
62
+
63
+ All checks must pass (warnings are acceptable on Windows for sandbox tier). If doctor finds fixable issues, it will auto-fix them. If manual fixes are needed, stop and address them.
64
+
65
+ ### Step 5: Commit & Push
66
+
67
+ Commit the version bump files:
68
+
69
+ ```bash
70
+ git add package.json package-lock.json src/modules/cli/package.json src/modules/cli/src/version.ts
71
+ git commit -m "chore: bump version to <new-version>"
72
+ git push origin main
73
+ ```
74
+
75
+ Only commit version-related files. Do not stage unrelated changes.
76
+
77
+ ### Step 6: Verify npm Authentication
78
+
79
+ Before publishing, verify npm auth is valid:
80
+
81
+ ```bash
82
+ npm whoami
83
+ ```
84
+
85
+ If this returns a 401 or "not logged in" error, the auth token in `~/.npmrc` is missing or expired. Ask the user to provide a valid npm publish token.
86
+
87
+ **How to create a token** (provide these instructions to the user if needed):
88
+ 1. Go to https://www.npmjs.com/settings/~/tokens
89
+ 2. Click "Generate New Token" → select "Granular Access Token"
90
+ 3. Set permissions: "Read and write" for the `moflo` package
91
+ 4. Copy the token (starts with `npm_...`)
92
+
93
+ Once the user provides the token, write it to `~/.npmrc`:
94
+
95
+ ```bash
96
+ echo "//registry.npmjs.org/:_authToken=<token>" > ~/.npmrc
97
+ ```
98
+
99
+ Then verify with `npm whoami` before proceeding.
100
+
101
+ ### Step 7: Publish to npm
102
+
103
+ ```bash
104
+ npm publish --tag <tag>
105
+ ```
106
+
107
+ - For stable releases: `npm publish` (publishes to `latest` tag by default)
108
+ - For RC releases: `npm publish --tag rc` (publishes to `rc` tag so it doesn't become `latest`)
109
+
110
+ **OTP handling:**
111
+ - If npm returns an OTP/one-time-password error, ask the user for their authenticator code
112
+ - Then retry with: `npm publish --otp=<code> --tag <tag>`
113
+ - Tip: Granular access tokens created on npmjs.com do NOT require OTP — prefer those over legacy tokens
114
+
115
+ ### Step 8: Verify Publication
116
+
117
+ ```bash
118
+ npm view moflo version
119
+ npm view moflo dist-tags
120
+ ```
121
+
122
+ Confirm the published version matches what we just built.
123
+
124
+ ### Step 9: Install Locally
125
+
126
+ ```bash
127
+ npm install moflo@<new-version> --save-dev
128
+ ```
129
+
130
+ This updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
131
+
132
+ ### Step 10: Final Commit
133
+
134
+ If `package.json` or `package-lock.json` changed from the install:
135
+
136
+ ```bash
137
+ git add package.json package-lock.json
138
+ git commit -m "chore: install moflo@<new-version>"
139
+ git push origin main
140
+ ```
141
+
142
+ ## Output
143
+
144
+ Print a summary at the end:
145
+
146
+ ```
147
+ Publish Summary
148
+ ───────────────
149
+ Version: <old> → <new>
150
+ Tag: latest | rc
151
+ Build: passed
152
+ Tests: <N> files passed, <N> tests passed
153
+ Doctor: <N> passed, <N> warnings
154
+ Published: moflo@<new-version>
155
+ Installed: moflo@<new-version> (devDependency)
156
+ ```
157
+
158
+ ## Important
159
+
160
+ - All commands run from the project root — never cd into subdirectories
161
+ - Never use `--force` on npm publish
162
+ - Never install moflo globally — it is always a local devDependency
163
+ - If any step fails, stop and fix the issue before proceeding — do not skip steps
164
+ - The `prepublishOnly` script in package.json runs `check-version-sync.mjs && npm run build` automatically, so npm publish will fail-fast if versions are out of sync
@@ -26,7 +26,8 @@ Given an epic issue number, perform ALL of the following:
26
26
  4. **Delete local branches**: Delete any local branches matching the same patterns (skip the current branch)
27
27
  5. **Reopen issues**: Reopen all story issues and the epic itself if any were closed
28
28
  6. **Uncheck task lists**: Edit the epic body to uncheck all `- [x]` checkboxes back to `- [ ]`
29
- 7. **Verify clean state**: Confirm all stories are OPEN, no related PRs are open, no related branches exist
29
+ 7. **Clear epic-state memory**: Delete all entries in the `epic-state` memory namespace for this epic and its stories (keys: `epic-<N>`, `story-<N>` for each story). Use `mcp__moflo__memory_list` with namespace `epic-state` to find entries, then `mcp__moflo__memory_delete` for each.
30
+ 8. **Verify clean state**: Confirm all stories are OPEN, no related PRs are open, no related branches exist, and epic-state memory is empty
30
31
 
31
32
  ## Output
32
33
 
@@ -136,6 +136,65 @@ Permissions for step "analyze-logs":
136
136
  - `{credentials.NAME}` — references a credential (resolved at runtime)
137
137
  - `{stepId.outputKey}` — references output from a previous step
138
138
 
139
+ #### REQUIRED: Preflight checks with human-readable hints
140
+
141
+ When a step depends on runtime state the user controls (clean git tree, logged-in CLI, reachable host, etc.), declare a `preflight:` block so the spell fails fast with a helpful message BEFORE any side effects occur.
142
+
143
+ **Every preflight MUST include a `hint:` field.** The hint is what the end user will see when the check fails. Without it they get raw shell output (`command "git diff --quiet" exited with 1, expected 0`), which looks like a bug in the spell engine.
144
+
145
+ Good hints:
146
+ - Speak in plain English (no command names, exit codes, or tool jargon).
147
+ - State the problem AND the fix in one or two sentences.
148
+ - Assume a non-technical reader.
149
+
150
+ ```yaml
151
+ steps:
152
+ - id: create-branch
153
+ type: bash
154
+ preflight:
155
+ - name: "working tree clean (tracked changes)"
156
+ command: "git diff --quiet"
157
+ hint: "You have uncommitted changes to tracked files. Commit them or stash them (git stash) before running this spell."
158
+ - name: "gh cli authenticated"
159
+ command: "gh auth status"
160
+ hint: "The GitHub CLI isn't signed in. Run: gh auth login"
161
+ config:
162
+ command: "git checkout -b feature/new"
163
+ ```
164
+
165
+ Bad hint (don't do this):
166
+ ```yaml
167
+ hint: "git diff --quiet failed with exit code 1" # leaks command name + exit code
168
+ hint: "Precondition violated" # tells user nothing actionable
169
+ ```
170
+
171
+ Preflights with no `hint` still work but produce unfriendly default output — flag this to the user as a quality issue before saving the spell.
172
+
173
+ ##### Fatal vs warning severity
174
+
175
+ By default every preflight is `severity: fatal` — if it fails, the spell aborts. Some preflights are better expressed as `severity: warning`: the user gets to choose how to handle the problem, and the spell continues if they pick a resolution.
176
+
177
+ Use `warning` ONLY when:
178
+ - The underlying problem has a safe, one-step fix the user might reasonably want to apply.
179
+ - Proceeding is viable either way — the step itself is robust to the condition.
180
+
181
+ Warning preflights MUST declare `resolutions:` — a list of options the user can pick from. Each resolution has a `label` and an optional `command` to run before continuing. If `command` is omitted, picking the resolution just proceeds (useful for "I'll handle it myself").
182
+
183
+ ```yaml
184
+ preflight:
185
+ - name: "working tree clean (tracked changes)"
186
+ command: "git diff --quiet"
187
+ severity: "warning"
188
+ hint: "You have uncommitted changes. If you want them carried onto the new branch, pick 'Stash and carry over'."
189
+ resolutions:
190
+ - label: "Stash changes and carry them onto the new branch"
191
+ command: "git stash push --include-untracked --message 'pre-spell autostash'"
192
+ - label: "Commit changes to the current branch first, then continue"
193
+ command: "git commit -am 'wip: pre-spell snapshot'"
194
+ ```
195
+
196
+ In non-interactive contexts (CI, daemons, scheduled spells) warnings automatically behave like fatals, because there is no one to prompt. Don't use `warning` as a way to silently ignore a problem — if ignoring it is always safe, the check shouldn't be there.
197
+
139
198
  ### Step 4: Generate the Spell YAML
140
199
 
141
200
  Assemble the definition into YAML format following this structure:
@@ -407,6 +466,13 @@ arguments:
407
466
  steps:
408
467
  - id: scan-deps
409
468
  type: bash
469
+ preflight:
470
+ - name: "npm available"
471
+ command: "npm --version"
472
+ hint: "npm isn't installed or isn't on your PATH. Install Node.js from https://nodejs.org and try again."
473
+ - name: "target directory exists"
474
+ command: "test -d \"{args.target}\""
475
+ hint: "The directory you passed as --target doesn't exist. Check the path and try again."
410
476
  config:
411
477
  command: "npm audit --json"
412
478
  cwd: "{args.target}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.8.56-rc.9",
3
+ "version": "4.8.57",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -111,7 +111,7 @@
111
111
  "@types/js-yaml": "^4.0.9",
112
112
  "@types/node": "^20.19.37",
113
113
  "eslint": "^8.0.0",
114
- "moflo": "^4.8.56-rc.8",
114
+ "moflo": "^4.8.56",
115
115
  "tsx": "^4.21.0",
116
116
  "typescript": "^5.9.3",
117
117
  "vitest": "^4.0.0"
@@ -12,10 +12,12 @@
12
12
  * flo epic reset <epic-number> Clear epic memory state
13
13
  */
14
14
  import { readFileSync } from 'node:fs';
15
+ import { execSync } from 'node:child_process';
15
16
  import { join, dirname } from 'node:path';
16
17
  import { fileURLToPath } from 'node:url';
17
18
  import { isEpicIssue, fetchEpicIssue, extractStories, enrichStoryNames, resolveExecutionOrder, } from '../epic/index.js';
18
- import { runEpicSpell } from '../epic/runner-adapter.js';
19
+ import { runEpicSpell, } from '../epic/runner-adapter.js';
20
+ import { select } from '../prompt.js';
19
21
  // ============================================================================
20
22
  // Spell Template Loader
21
23
  // ============================================================================
@@ -80,8 +82,34 @@ async function runEpic(source, strategy, dryRun) {
80
82
  catch {
81
83
  // No prior state — fresh run
82
84
  }
85
+ // 3b. Reconcile memory with branch state — if the branch doesn't exist
86
+ // or has no commits ahead of main, memory is stale (e.g. after a reset)
83
87
  if (completedStories.size > 0) {
84
- console.log(`[epic] Resuming: ${completedStories.size} stories already completed`);
88
+ const slug = issue.title
89
+ .toLowerCase()
90
+ .replace(/[^a-z0-9]+/g, '-')
91
+ .replace(/^-|-$/g, '')
92
+ .substring(0, 40);
93
+ const epicBranch = `epic/${issueNumber}-${slug}`;
94
+ let branchHasCommits = false;
95
+ try {
96
+ // Check if the epic branch exists (local or remote) and has commits ahead of main
97
+ const revCheck = execSync(`git rev-parse --verify refs/heads/${epicBranch} 2>/dev/null || git rev-parse --verify refs/remotes/origin/${epicBranch} 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
98
+ if (revCheck) {
99
+ const aheadCount = execSync(`git rev-list --count main..${revCheck}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
100
+ branchHasCommits = parseInt(aheadCount, 10) > 0;
101
+ }
102
+ }
103
+ catch {
104
+ // Branch doesn't exist — memory is stale
105
+ }
106
+ if (branchHasCommits) {
107
+ console.log(`[epic] Resuming: ${completedStories.size} stories already completed (branch verified)`);
108
+ }
109
+ else {
110
+ console.log(`[epic] Memory shows ${completedStories.size} completed stories, but epic branch is missing or empty — starting fresh`);
111
+ completedStories = new Set();
112
+ }
85
113
  }
86
114
  // 4. Build slug for branch name
87
115
  const slug = issue.title
@@ -156,6 +184,7 @@ async function runEpic(source, strategy, dryRun) {
156
184
  console.log(`[epic] └─ ${stepResult.error}`);
157
185
  }
158
186
  },
187
+ onPreflightWarnings: isInteractive() ? resolvePreflightWarningsInteractively : undefined,
159
188
  });
160
189
  if (result.success) {
161
190
  console.log(`\n[epic] Epic #${issueNumber} completed successfully`);
@@ -168,8 +197,16 @@ async function runEpic(source, strategy, dryRun) {
168
197
  return { success: true, message: 'Epic completed', data: result };
169
198
  }
170
199
  else {
200
+ const firstErr = result.errors[0];
201
+ const errCode = firstErr?.code;
202
+ // Preflight failures are user environment problems, not epic bugs.
203
+ // Show only the friendly prerequisite message, nothing else.
204
+ if (errCode === 'PREFLIGHT_FAILED' && typeof firstErr?.message === 'string') {
205
+ console.log(`\n${firstErr.message}`);
206
+ console.log('\nThe epic was not started. Fix the item(s) above and try again.');
207
+ return { success: false, message: firstErr.message, data: result };
208
+ }
171
209
  console.log(`\n[epic] Epic #${issueNumber} failed`);
172
- // Print step-level results for visibility
173
210
  if (result.steps && result.steps.length > 0) {
174
211
  for (const step of result.steps) {
175
212
  const icon = step.status === 'succeeded' ? '✓' : step.status === 'skipped' ? '○' : '✗';
@@ -180,7 +217,6 @@ async function runEpic(source, strategy, dryRun) {
180
217
  }
181
218
  }
182
219
  }
183
- // Print spell-level errors with full detail
184
220
  for (const err of result.errors) {
185
221
  const prefix = err.stepId
186
222
  ? ` [${err.stepId}]`
@@ -193,8 +229,6 @@ async function runEpic(source, strategy, dryRun) {
193
229
  }
194
230
  }
195
231
  }
196
- // Build actionable error message from first failure
197
- const firstErr = result.errors[0];
198
232
  const rawMsg = firstErr?.message ?? 'Unknown error';
199
233
  const summary = buildFailureSummary(rawMsg, {
200
234
  stepId: firstErr?.stepId,
@@ -244,16 +278,68 @@ async function resetEpic(epicNumber) {
244
278
  return { success: false, message: 'Usage: flo epic reset <epic-number>' };
245
279
  }
246
280
  console.log(`[epic] Clearing state for epic #${epicNumber}...`);
281
+ // 1. Find all story keys for this epic from memory
282
+ let storyKeys = [];
247
283
  try {
248
- await runEpicSpell(`name: epic-reset\nsteps:\n - id: clear-state\n type: memory\n config:\n action: write\n namespace: epic-state\n key: "epic-${epicNumber}"\n value: null`, { args: {} });
249
- console.log(`[epic] State cleared for epic #${epicNumber}`);
284
+ const searchResult = await runEpicSpell(`name: epic-reset-search\nsteps:\n - id: find-stories\n type: memory\n config:\n action: search\n namespace: epic-state\n query: "epic ${epicNumber} story completed"`, { args: {} });
285
+ if (searchResult.success && searchResult.outputs['find-stories']) {
286
+ const data = searchResult.outputs['find-stories'];
287
+ if (data.results) {
288
+ storyKeys = data.results.map(r => r.key).filter(k => k.startsWith('story-'));
289
+ }
290
+ }
250
291
  }
251
292
  catch {
252
- console.log(`[epic] Could not clear epic state.`);
293
+ // Couldn't search — just clear the epic key below
253
294
  }
295
+ // 2. Clear epic key + all story keys
296
+ const keysToDelete = [`epic-${epicNumber}`, ...storyKeys];
297
+ let cleared = 0;
298
+ for (const key of keysToDelete) {
299
+ try {
300
+ await runEpicSpell(`name: epic-reset-delete\nsteps:\n - id: delete-key\n type: memory\n config:\n action: write\n namespace: epic-state\n key: "${key}"\n value: null`, { args: {} });
301
+ cleared++;
302
+ }
303
+ catch {
304
+ // Key may not exist — safe to ignore
305
+ }
306
+ }
307
+ console.log(`[epic] Cleared ${cleared} memory entries for epic #${epicNumber}`);
254
308
  return { success: true };
255
309
  }
256
310
  // ============================================================================
311
+ // Preflight warning interactive resolver
312
+ // ============================================================================
313
+ function isInteractive() {
314
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY) && process.env.CI !== 'true';
315
+ }
316
+ async function resolvePreflightWarningsInteractively(warnings) {
317
+ console.log('\nBefore this spell starts, some things need your attention:\n');
318
+ const decisions = [];
319
+ for (let i = 0; i < warnings.length; i++) {
320
+ const w = warnings[i];
321
+ const choices = [
322
+ ...w.resolutions.map((r, idx) => ({
323
+ label: r.label,
324
+ value: { action: 'resolve', resolutionIndex: idx },
325
+ })),
326
+ { label: "Continue anyway (I'll handle it)", value: { action: 'continue' } },
327
+ { label: 'Abort the spell', value: { action: 'abort' } },
328
+ ];
329
+ const decision = await select({
330
+ message: `${i + 1}. ${w.reason}`,
331
+ options: choices,
332
+ });
333
+ decisions.push(decision);
334
+ if (decision.action === 'abort') {
335
+ while (decisions.length < warnings.length)
336
+ decisions.push({ action: 'abort' });
337
+ break;
338
+ }
339
+ }
340
+ return decisions;
341
+ }
342
+ // ============================================================================
257
343
  // Error remediation hints
258
344
  // ============================================================================
259
345
  const REMEDIATION_PATTERNS = [
@@ -7,10 +7,24 @@
7
7
  * Story #197: Thin adapter for running spell YAML from epic command.
8
8
  * Story #229: Uses shared engine loader instead of inline dynamic import.
9
9
  */
10
- import { loadSpellEngine } from '../services/engine-loader.js';
10
+ import * as readline from 'node:readline';
11
+ import { loadSpellEngine, } from '../services/engine-loader.js';
11
12
  import { createDashboardMemoryAccessor } from '../services/daemon-dashboard.js';
12
13
  /** Cached memory accessor — created once per process. */
13
14
  let memoryAccessor = null;
15
+ /** Prompt the user to accept or decline spell permissions. */
16
+ async function promptAcceptPermissions() {
17
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
18
+ try {
19
+ const answer = await new Promise(resolve => {
20
+ rl.question('\n[epic] Accept these permissions? (y/N) ', resolve);
21
+ });
22
+ return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
23
+ }
24
+ finally {
25
+ rl.close();
26
+ }
27
+ }
14
28
  /**
15
29
  * Run a spell YAML string via the spell engine.
16
30
  *
@@ -31,6 +45,34 @@ export async function runEpicSpell(yamlContent, options = {}) {
31
45
  console.warn('[epic] ⚠ Spell executions will NOT appear in the dashboard');
32
46
  }
33
47
  }
34
- return engine.runSpellFromContent(yamlContent, undefined, { ...options, ...(memoryAccessor ? { memory: memoryAccessor } : {}) });
48
+ const runOpts = { ...options, projectRoot: process.cwd(), ...(memoryAccessor ? { memory: memoryAccessor } : {}) };
49
+ const result = await engine.runSpellFromContent(yamlContent, undefined, runOpts);
50
+ // Auto-accept permissions on first run: the spell runner already printed
51
+ // the full risk analysis to the console. The user initiated the epic
52
+ // command, so we accept on their behalf and retry immediately.
53
+ const hasAcceptanceError = !result.success &&
54
+ result.errors.some(e => e.code === 'ACCEPTANCE_REQUIRED');
55
+ if (hasAcceptanceError) {
56
+ const accepted = await promptAcceptPermissions();
57
+ if (!accepted) {
58
+ return result;
59
+ }
60
+ // Use the already-loaded engine module (dynamic import) for spells internals.
61
+ // Static cross-package imports break when installed as a dependency because
62
+ // the relative paths from dist/ don't match the source layout.
63
+ const spells = engine;
64
+ const { parseSpell, StepCommandRegistry, builtinCommands, analyzeSpellPermissions, recordAcceptance } = spells;
65
+ const projectRoot = process.cwd();
66
+ const parsed = parseSpell(yamlContent);
67
+ const stepRegistry = new StepCommandRegistry();
68
+ for (const cmd of builtinCommands) {
69
+ stepRegistry.register(cmd, 'built-in');
70
+ }
71
+ const report = analyzeSpellPermissions(parsed.definition, stepRegistry);
72
+ await recordAcceptance(projectRoot, parsed.definition.name, report.permissionHash);
73
+ console.log(`[epic] Permissions accepted for "${parsed.definition.name}" — retrying...\n`);
74
+ return engine.runSpellFromContent(yamlContent, undefined, runOpts);
75
+ }
76
+ return result;
35
77
  }
36
78
  //# sourceMappingURL=runner-adapter.js.map
@@ -33,14 +33,6 @@ arguments:
33
33
  mofloLevel: hooks
34
34
 
35
35
  steps:
36
- # Step 0: Pre-flight — fail fast if worktree is dirty or has unmerged files
37
- - id: preflight-check
38
- type: bash
39
- config:
40
- command: "if [ -n \"$(git diff --name-only --diff-filter=U 2>/dev/null)\" ]; then echo 'ERROR: Unmerged files detected. Resolve conflicts before running epic.' >&2; git diff --name-only --diff-filter=U 2>/dev/null >&2; exit 1; fi && if [ -n \"$(git -c core.autocrlf=false -c core.safecrlf=false status --porcelain 2>/dev/null)\" ]; then echo 'ERROR: Working tree is dirty. Commit, stash, or discard changes before running epic.' >&2; git -c core.autocrlf=false -c core.safecrlf=false status --short 2>/dev/null >&2; exit 1; fi"
41
- timeout: 120000
42
- failOnError: true
43
-
44
36
  # Step 1: Initialize epic state in memory (enables resume)
45
37
  - id: init-state
46
38
  type: memory
@@ -62,8 +54,39 @@ steps:
62
54
  output: story_results
63
55
  steps:
64
56
  # 1: Checkout and pull latest base branch
57
+ # Preflights run once BEFORE any step (not per iteration) — fail fast
58
+ # on dirty tree, missing auth, or unreachable remote.
65
59
  - id: checkout-base
66
60
  type: bash
61
+ preflight:
62
+ - name: "no unmerged files"
63
+ command: "git diff --name-only --diff-filter=U"
64
+ expectExitCode: 0
65
+ hint: "You have unresolved merge conflicts. Resolve them and commit before running this spell."
66
+ - name: "working tree clean (tracked changes)"
67
+ command: "git diff --quiet"
68
+ severity: "warning"
69
+ hint: "You have uncommitted changes to tracked files. If you want them carried onto the epic branch, pick 'Stash and carry over'."
70
+ resolutions:
71
+ - label: "Stash changes and carry them onto the epic branch"
72
+ command: "git stash push --include-untracked --message 'moflo-epic-autostash'"
73
+ - label: "Commit changes to the current branch first, then continue"
74
+ command: "git commit -am 'wip: pre-epic snapshot'"
75
+ - name: "working tree clean (staged changes)"
76
+ command: "git diff --cached --quiet"
77
+ severity: "warning"
78
+ hint: "You have staged changes that aren't committed. If you want them carried onto the epic branch, pick 'Stash and carry over'."
79
+ resolutions:
80
+ - label: "Stash staged changes and carry them onto the epic branch"
81
+ command: "git stash push --include-untracked --message 'moflo-epic-autostash'"
82
+ - label: "Commit staged changes to the current branch first, then continue"
83
+ command: "git commit -m 'wip: pre-epic snapshot'"
84
+ - name: "gh cli authenticated"
85
+ command: "gh auth status"
86
+ hint: "The GitHub CLI isn't signed in. Run: gh auth login"
87
+ - name: "origin remote configured"
88
+ command: "git remote get-url origin"
89
+ hint: "This repo has no 'origin' remote. Set one with: git remote add origin <url>"
67
90
  config:
68
91
  command: "git stash --include-untracked -q 2>/dev/null; git checkout {args.base_branch} && git pull origin {args.base_branch}; git stash pop -q 2>/dev/null || true"
69
92
  failOnError: true