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.
- package/.claude/guidance/shipped/moflo-user-facing-language.md +34 -0
- package/.claude/skills/connector-builder/SKILL.md +52 -0
- package/.claude/skills/publish/SKILL.md +164 -0
- package/.claude/skills/reset-epic/SKILL.md +2 -1
- package/.claude/skills/spell-builder/SKILL.md +66 -0
- package/package.json +2 -2
- package/src/modules/cli/dist/src/commands/epic.js +95 -9
- package/src/modules/cli/dist/src/epic/runner-adapter.js +44 -2
- package/src/modules/cli/dist/src/epic/spells/auto-merge.yaml +31 -8
- package/src/modules/cli/dist/src/epic/spells/single-branch.yaml +32 -9
- package/src/modules/cli/dist/src/init/moflo-init.js +13 -4
- package/src/modules/cli/dist/src/mcp-tools/spell-tools.js +44 -0
- package/src/modules/cli/dist/src/mcp-tools/tool-names.js +1 -0
- package/src/modules/cli/dist/src/version.js +1 -1
- package/src/modules/cli/package.json +2 -2
- package/src/modules/plugins/dist/index.js +0 -1
- package/src/modules/shared/dist/utils/platform.js +0 -2
- package/src/modules/spells/dist/commands/bash-command.js +7 -2
- package/src/modules/spells/dist/commands/github-command.js +36 -0
- package/src/modules/spells/dist/core/permission-disclosure.js +36 -43
- package/src/modules/spells/dist/core/permission-resolver.js +17 -9
- package/src/modules/spells/dist/core/preflight-checker.js +198 -0
- package/src/modules/spells/dist/core/runner.js +95 -8
- package/src/modules/spells/dist/factory/runner-bridge.js +2 -0
- package/src/modules/spells/dist/factory/runner-factory.js +2 -1
- 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. **
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
249
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|