moflo 4.11.9 → 4.11.10-rc.10
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-core-guidance.md +1 -0
- package/.claude/guidance/shipped/moflo-epic-processing.md +1 -0
- package/.claude/guidance/shipped/moflo-sdd.md +97 -0
- package/.claude/guidance/shipped/moflo-session-continuity.md +71 -0
- package/.claude/helpers/gate.cjs +232 -16
- package/.claude/skills/commune/SKILL.md +6 -1
- package/.claude/skills/fl/SKILL.md +101 -8
- package/.claude/skills/fl/execution-modes.md +2 -0
- package/.claude/skills/fl/phases.md +49 -2
- package/.claude/skills/fl/sdd.md +71 -0
- package/.claude/skills/verify/SKILL.md +94 -0
- package/README.md +61 -13
- package/bin/gate.cjs +232 -16
- package/bin/index-guidance.mjs +55 -1
- package/bin/lib/yaml-upgrader.mjs +93 -1
- package/dist/src/cli/commands/doctor-checks-config.js +52 -0
- package/dist/src/cli/commands/doctor-checks-deep.js +3 -0
- package/dist/src/cli/commands/doctor-checks-sdd.js +191 -0
- package/dist/src/cli/commands/doctor-fixes.js +100 -0
- package/dist/src/cli/commands/doctor-registry.js +13 -1
- package/dist/src/cli/commands/index.js +2 -0
- package/dist/src/cli/commands/sdd.js +338 -0
- package/dist/src/cli/config/moflo-config.js +46 -0
- package/dist/src/cli/init/executor.js +1 -0
- package/dist/src/cli/init/helpers-generator.js +172 -16
- package/dist/src/cli/init/moflo-yaml-template.js +15 -0
- package/dist/src/cli/init/settings-generator.js +20 -3
- package/dist/src/cli/mcp-tools/agent-tools.js +11 -1
- package/dist/src/cli/mcp-tools/hooks-tools.js +1 -0
- package/dist/src/cli/movector/model-router.js +38 -0
- package/dist/src/cli/parser.js +14 -2
- package/dist/src/cli/sdd/artifacts.js +298 -0
- package/dist/src/cli/sdd/index.js +9 -0
- package/dist/src/cli/sdd/templates.js +59 -0
- package/dist/src/cli/services/claude-stats.js +103 -20
- package/dist/src/cli/services/claudemd-injection.js +27 -15
- package/dist/src/cli/services/daemon-dashboard.js +25 -7
- package/dist/src/cli/services/hook-block-hash.js +4 -2
- package/dist/src/cli/services/hook-wiring.js +10 -0
- package/dist/src/cli/services/spell-gate.js +41 -2
- package/dist/src/cli/spells/core/runner.js +2 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +4 -3
|
@@ -218,6 +218,7 @@ See `.claude/guidance/moflo-memory-strategy.md` for memory-specific troubleshoot
|
|
|
218
218
|
## See Also
|
|
219
219
|
|
|
220
220
|
- `.claude/guidance/moflo-skills-reference.md` — Slash-command skills catalog (`/flo`, `/commune`, `/meditate`, `/divine`, …) plus the automatic features (auto-meditate, session-continuity)
|
|
221
|
+
- `.claude/guidance/moflo-sdd.md` — Spec-Driven Development (`/flo -sd`/`-v`, `flo sdd`, verify-before-done gate, `sdd.default` / `gates.verify_before_done`)
|
|
221
222
|
- `.claude/guidance/moflo-cli-reference.md` — CLI commands, agents, hooks, hive-mind, RuVector
|
|
222
223
|
- `.claude/guidance/moflo-yaml-reference.md` — `moflo.yaml` schema, environment variables, `.mcp.json` setup
|
|
223
224
|
- `.claude/guidance/moflo-subagents.md` — Subagents memory-first protocol and store patterns
|
|
@@ -57,6 +57,7 @@ The strategy is resolved with this precedence (highest first):
|
|
|
57
57
|
|
|
58
58
|
## See Also
|
|
59
59
|
|
|
60
|
+
- `.claude/guidance/moflo-session-continuity.md` — How `epic-state` + git reconciliation resume works vs. native Claude Code checkpoints (`/rewind`, agent-tree)
|
|
60
61
|
- `src/cli/commands/epic.ts` — Epic command implementation
|
|
61
62
|
- `src/cli/config/moflo-config.ts` — `MofloConfig.epic` interface and defaults
|
|
62
63
|
- `.claude/skills/fl/SKILL.md` — `/flo` skill epic handling section
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# MoFlo Spec-Driven Development (SDD) & Verify-Before-Done
|
|
2
|
+
|
|
3
|
+
**Purpose:** How to run and reason about the `spec → plan → implement → verify` cycle in `/flo`, the `flo sdd` artifact CLI, and the verify-before-done gate. Read this before using `-sd`/`-v`/`--no-verify`, editing `.moflo/specs/`, or tuning `sdd.default` / `gates.verify_before_done`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to Use `-sd` vs `-v` vs Neither
|
|
8
|
+
|
|
9
|
+
Two independent modifiers on `/flo`, orthogonal to execution mode (`-n`/`-s`/`-h`) and `--worktree`. The **spec/plan ceremony (`-sd`) is opt-in**; **verify-before-done (`-v`) runs by default** (#1294) — separable from SDD by design.
|
|
10
|
+
|
|
11
|
+
| Situation | Flag | Effect |
|
|
12
|
+
|-----------|------|--------|
|
|
13
|
+
| Fuzzy or large unit of work; you want a reviewed spec/plan before code | `-sd` / `--sdd` | Full spec → plan → (review) → implement → verify. Implies `--verify`. Opt-in. |
|
|
14
|
+
| A default run already verifies before done | (none) | Verify-before-done runs automatically via the `/verify` skill. |
|
|
15
|
+
| Skip verification for one run | `--no-verify` | Drops the (default-on) verify step. |
|
|
16
|
+
|
|
17
|
+
`--sdd` **always implies** `--verify` — a spec/plan without an enforced verify step drifts. `--no-sdd` / `--no-verify` opt a single run out; `-v` only matters to force verify back on where a project set `verify_before_done: false`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## The Artifact Convention
|
|
22
|
+
|
|
23
|
+
Specs and plans persist as Markdown, one directory per unit of work, under the configured specs directory (default `.moflo/specs`):
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
<specs_dir>/<slug>/spec.md # the "what" + acceptance criteria
|
|
27
|
+
<specs_dir>/<slug>/plan.md # the "steps" + how each criterion is verified
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
They are indexed into memory on session start, so `mcp__moflo__memory_search` surfaces prior specs across sessions. **Always create and mutate them through the `flo sdd` CLI** — never hand-write the path in a skill step (cross-platform, Rule #1: the CLI builds every path with `path.join`).
|
|
31
|
+
|
|
32
|
+
**Where they live is configurable (`sdd.specs_dir`, #1294).** The default `.moflo/specs` is **gitignored** by `flo init` — specs stay local and do not bloat source control, but they also do **not** appear in PRs. To make specs reviewable, point `sdd.specs_dir` at a **tracked** path and commit them:
|
|
33
|
+
|
|
34
|
+
| `sdd.specs_dir` | Committed? | Use when |
|
|
35
|
+
|-----------------|------------|----------|
|
|
36
|
+
| `.moflo/specs` (default) | No (gitignored) | You want the SDD workflow but not spec artifacts in history |
|
|
37
|
+
| `docs/specs`, `.specs`, … (tracked) | Yes | You want specs reviewed in the PR alongside the code |
|
|
38
|
+
|
|
39
|
+
Set it once in `moflo.yaml`; the `flo sdd` CLI and the session-start indexer both honor it. If the path sits inside a `guidance.directories` entry, specs are indexed once (as guidance), not twice.
|
|
40
|
+
|
|
41
|
+
Each artifact carries a `status` of `draft` or `reviewed` in its frontmatter. The constitution layer (`CLAUDE.md` + `.claude/guidance/`) is referenced by every stage — never restate its invariants inside a spec.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Driving the Cycle with `flo sdd`
|
|
46
|
+
|
|
47
|
+
The `flo sdd` CLI is the single cross-platform entry point for every SDD artifact operation — scaffold, review, gate, and status. Drive the cycle through these commands; never hand-write a `.moflo/specs/...` path in a skill step.
|
|
48
|
+
|
|
49
|
+
| Command | Does |
|
|
50
|
+
|---------|------|
|
|
51
|
+
| `flo sdd spec "<title>"` | Scaffold `spec.md` (or show an existing one). `--from <file\|->` seeds the body. |
|
|
52
|
+
| `flo sdd review <slug>` | Mark the spec reviewed — unlocks the plan. |
|
|
53
|
+
| `flo sdd plan <slug>` | Scaffold `plan.md`; **requires the spec be reviewed**. |
|
|
54
|
+
| `flo sdd review <slug> plan` | Mark the plan reviewed — unlocks implementation. |
|
|
55
|
+
| `flo sdd check <slug> implement` | Review-checkpoint gate — exits non-zero until the plan is reviewed. |
|
|
56
|
+
| `flo sdd list` / `flo sdd status <slug>` | Enumerate specs / show one unit's spec+plan status. |
|
|
57
|
+
|
|
58
|
+
The two review checkpoints are the point: **a spec must be reviewed before its plan; a plan must be reviewed before implementation.** Pass `--force` only to deliberately skip a checkpoint.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Verify-Before-Done Gate
|
|
63
|
+
|
|
64
|
+
When enforced, `gh pr create` is blocked until the change has been verified end-to-end since the last code edit.
|
|
65
|
+
|
|
66
|
+
- **On by default (#1294).** Enforced for every `/flo` run; disable per-project with `gates.verify_before_done: false` or per-run with `--no-verify`. On upgrade, consumers with no `verify_before_done` key start enforcing; an explicit value is preserved. Docs-only diffs are exempt, so a pure-docs PR is never blocked.
|
|
67
|
+
- **Satisfy it by running the `/verify` skill** — `/flo` delegates to it. It exercises the change against the plan's (or ticket's) acceptance criteria and records its own outcome to memory (`namespace: learnings, key: verify:<slug>`). It reuses the Tests-phase run rather than repeating it (no double verify).
|
|
68
|
+
- **A source edit invalidates a prior verification** — re-run `/verify` after editing. `/ward` and `/quicken` are targeted audits, not the completion gate.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## SDD & Verify moflo.yaml Defaults
|
|
73
|
+
|
|
74
|
+
`sdd.default` is off (opt-in); `verify_before_done` is **on** (#1294). Override per run with the flags.
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
sdd:
|
|
78
|
+
default: false # true → every /flo run uses the SDD cycle unless --no-sdd
|
|
79
|
+
gates:
|
|
80
|
+
verify_before_done: true # on by default (#1294); false → skip /verify unless -v. Per-run: --no-verify
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Check wiring status with `/healer` (or `/eldar`) — the `SDD + Verify Wiring` check reports whether the gate cases and hooks are present and which toggles are on.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## From a Fuzzy Idea: `/commune` → Spec
|
|
88
|
+
|
|
89
|
+
When the unit of work is still fuzzy, start with `/commune`. It runs a short Socratic dialogue and synthesizes a spec, then can hand that spec straight into the SDD spine (`flo sdd spec "<title>" --from -`). Rename its **Success criteria** section to **`## Acceptance Criteria`** on the way in — the SDD validator requires it. `/commune` is the pre-execution counterpart to `/meditate`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## See Also
|
|
94
|
+
|
|
95
|
+
- `.claude/skills/fl/sdd.md` — the `/flo` companion that drives `-sd`/`-v` end to end
|
|
96
|
+
- `.claude/guidance/moflo-core-guidance.md` — CLI, hooks, gates, and config hub
|
|
97
|
+
- `.claude/guidance/moflo-yaml-reference.md` — full `moflo.yaml` field reference, including `sdd` and `gates`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Native Checkpoints vs. MoFlo Continuity — Who Owns Undo and Resume
|
|
2
|
+
|
|
3
|
+
**Purpose:** Decide which layer owns a given "undo" or "resume" — Claude Code's native checkpoints (`/rewind`, agent-tree checkpointing) or moflo's session-continuity + epic-state — so the two coexist by layering, never by competing. Read this before wiring any resume/rollback behavior into a moflo workflow.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Ownership Decision Table
|
|
8
|
+
|
|
9
|
+
The two systems solve different problems. Never route a job to the wrong layer or build a moflo mechanism that duplicates a native one.
|
|
10
|
+
|
|
11
|
+
| You want to… | Owner | Mechanism |
|
|
12
|
+
|--------------|-------|-----------|
|
|
13
|
+
| Undo code edits from earlier THIS session | Native | `/rewind` code checkpoints |
|
|
14
|
+
| Pause/resume a multi-hour agent tree WITHIN a session | Native | Agent-tree checkpointing (beta) |
|
|
15
|
+
| Pick up context ACROSS sessions ("where you left off") | MoFlo | Session-continuity digest |
|
|
16
|
+
| Resume an interrupted `flo epic` from real progress | MoFlo | `epic-state` reconciled against git + the GitHub checklist |
|
|
17
|
+
| Fail over to another model when a tier is unavailable | Native | `fallbackModel` — moflo's router emits the chain |
|
|
18
|
+
|
|
19
|
+
**Rule:** native checkpoints govern *in-session* rollback of code and agent trees; moflo governs *cross-session* coordination and the durable learning record. They layer; they do not overlap.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Native Checkpoints Are Runtime-Only — Never Couple MoFlo to Them
|
|
24
|
+
|
|
25
|
+
Native `/rewind`, code checkpoints, and agent-tree checkpoint state live entirely inside the Claude Code runtime. There is **no stable public file or API** for them, and moflo library code (the `flo` CLI, MCP server, hooks, launcher, daemon) is never handed a reference to that state.
|
|
26
|
+
|
|
27
|
+
**Never read or write native checkpoint state from moflo code.** Coupling to an undocumented runtime format violates the cross-platform rule and the consumer-blast-radius rule — it would break silently on any Claude Code update, in every consumer install. Treat native checkpoints as a black box the human drives; moflo reacts to committed results (git, GitHub), not to session snapshots.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## MoFlo Session-Continuity — The Cross-Session Digest
|
|
32
|
+
|
|
33
|
+
Session-continuity is moflo's narrative bridge between sessions, distinct from native checkpoints. At session end it captures a scrubbed digest — `{goal, decisions, gitState}` — to a rotated JSON store under `.moflo/`. At session start it scores digests by branch match, changed-file overlap, and recency, then injects the best one as a **"Where you left off"** lead.
|
|
34
|
+
|
|
35
|
+
**Treat the injected digest as a verifiable lead, not ground truth.** It reflects what was true when it was written — re-check the current branch, working tree, and files before acting on it. Configure via `session_continuity: {capture, inject, max_age_hours}` in `moflo.yaml`; both flags default on.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Epic Resume Is Checkpoint-Agnostic — Git Is the Source of Truth
|
|
40
|
+
|
|
41
|
+
`flo epic` records progress in the `epic-state` memory namespace, but that namespace is an **in-session cache, hard-deleted on every session start** — never rely on it as the durable resume store. The durable sources of truth are the **git epic branch + commits** and the **GitHub issue checklist** (`[ ]` vs `[x]`).
|
|
42
|
+
|
|
43
|
+
On resume, `epic.ts` reconciles memory against git: it recomputes the `epic/{n}-{slug}` branch and, if the branch is missing or has no commits ahead of the base, discards the memory record and starts fresh. This makes epic resume **independent of native checkpoints** — it works identically whether or not the user used `/rewind`, because truth is re-derived from committed state, not a session snapshot. Do not add native-checkpoint detection to epic resume; it would be unverifiable noise.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Swarm and Hive-Mind Are a Logical Layer Native Nesting Does Not Replace
|
|
48
|
+
|
|
49
|
+
MoFlo's `UnifiedSwarmCoordinator` is a **logical coordination layer** — an agent registry plus MessageBus routing, consensus voting, and workload-balanced task distribution. "Spawn" registers a tracked record; it never launches a process. Hive-mind is one level deep (queen → flat worker list; workers never spawn sub-workers).
|
|
50
|
+
|
|
51
|
+
Native 3-level nested subagents solve **process and context isolation for real, executing agents** — an orthogonal concern. Native nesting does not replace the registry, MessageBus, consensus, or task distribution.
|
|
52
|
+
|
|
53
|
+
**Never rewire swarm/hive-mind onto native nesting.** Reducing coordinator handlers to stubs to "map onto native primitives" is exactly the disconnection the protected-functionality rule forbids, for zero functional gain. The only seam between the two — the subagent bootstrap directive — is already carried in every spawn's metadata and response. Keep every handler wired to the coordinator.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Model Routing Emits Native `fallbackModel` Chains
|
|
58
|
+
|
|
59
|
+
MoFlo's model router already tracks per-tier circuit-breaker state. Its routing result carries a `fallbackModel` chain: an ordered, circuit-breaker-aware failover list that excludes the primary, drops `inherit` and zero-score tiers, and demotes any open-circuit tier to the tail.
|
|
60
|
+
|
|
61
|
+
The chain is **advisory** — surfaced to the orchestrator via `hooks_model-route` and `agent_spawn`, then applied when spawning a real subagent. MoFlo never auto-launches a model itself; the orchestrator (or the native `Task` spawn) consumes the chain and maps it onto Claude Code's native `fallbackModel`. A healthy lower tier is always tried before a failing one.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## See Also
|
|
66
|
+
|
|
67
|
+
- `.claude/guidance/moflo-epic-processing.md` — Epic orchestration whose `epic-state` + git reconciliation this doc frames against native checkpoints
|
|
68
|
+
- `.claude/guidance/moflo-claude-swarm-cohesion.md` — Swarm/hive coordination model the "logical layer" note protects
|
|
69
|
+
- `.claude/guidance/moflo-core-guidance.md` — CLI, hooks, swarm, memory, and config hub
|
|
70
|
+
- `src/cli/movector/model-router.ts` — Router that emits the `fallbackModel` chain
|
|
71
|
+
- `src/cli/commands/epic.ts` — Epic resume + git-branch reconciliation
|
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
var fs = require('fs');
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var cp = require('child_process');
|
|
6
|
+
var os = require('os');
|
|
6
7
|
|
|
7
8
|
var PROJECT_DIR = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
|
|
8
9
|
var STATE_FILE = path.join(PROJECT_DIR, '.claude', 'workflow-state.json');
|
|
9
10
|
|
|
10
|
-
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false };
|
|
11
|
+
var STATE_DEFAULTS = { tasksCreated: false, taskCount: 0, memorySearched: false, memorySearchedBy: {}, memoryRequired: true, learningsStored: false, testsRun: false, simplifyRun: false, simplifySnapshotSha: null, verifyRun: false, verifyOutcome: null, interactionCount: 0, sessionStart: null, lastBlockedAt: null, lastNamespaceHint: '', lastNamespaceHintEmittedBy: {}, flMode: null, swarmInitialized: false, hiveInitialized: false, sddMode: false, activeSddSlug: null };
|
|
11
12
|
|
|
12
13
|
// Per-actor memory-search tracking (#838). The legacy `memorySearched` boolean
|
|
13
14
|
// is session-wide, so once the parent searches memory, every spawned subagent
|
|
@@ -60,27 +61,86 @@ function writeState(s) {
|
|
|
60
61
|
|
|
61
62
|
// Load moflo.yaml gate config (defaults: all enabled)
|
|
62
63
|
function loadGateConfig() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
// verify_before_done is opt-OUT (default true), like every other gate: #1294
|
|
65
|
+
// ships a real /verify skill and has /flo delegate to it, so leaving it off by
|
|
66
|
+
// default would make the default /flo run silently skip the acceptance check.
|
|
67
|
+
// Disable per-project with `verify_before_done: false` or per-run `--no-verify`.
|
|
68
|
+
var defaults = { memory_first: true, task_create_first: true, context_tracking: true, testing_gate: true, simplify_gate: true, learnings_gate: true, swarm_invocation_gate: true, verify_before_done: true, sdd_gate: true };
|
|
69
|
+
var content = MOFLO_YAML;
|
|
70
|
+
if (content) {
|
|
71
|
+
if (/memory_first:\s*false/i.test(content)) defaults.memory_first = false;
|
|
72
|
+
if (/task_create_first:\s*false/i.test(content)) defaults.task_create_first = false;
|
|
73
|
+
if (/context_tracking:\s*false/i.test(content)) defaults.context_tracking = false;
|
|
74
|
+
if (/testing_gate:\s*false/i.test(content)) defaults.testing_gate = false;
|
|
75
|
+
if (/simplify_gate:\s*false/i.test(content)) defaults.simplify_gate = false;
|
|
76
|
+
if (/learnings_gate:\s*false/i.test(content)) defaults.learnings_gate = false;
|
|
77
|
+
if (/swarm_invocation_gate:\s*false/i.test(content)) defaults.swarm_invocation_gate = false;
|
|
78
|
+
// Opt-out: on by default; disable only when explicitly set false.
|
|
79
|
+
if (/verify_before_done:\s*false/i.test(content)) defaults.verify_before_done = false;
|
|
80
|
+
// sdd_gate is the check-before-implement backstop (#1297). Opt-out; the
|
|
81
|
+
// gate only fires when a run is actually armed for SDD (sddMode), so
|
|
82
|
+
// leaving it on costs non-SDD work nothing.
|
|
83
|
+
if (/sdd_gate:\s*false/i.test(content)) defaults.sdd_gate = false;
|
|
84
|
+
}
|
|
77
85
|
return defaults;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
// Parse the top-level `sdd:` block from moflo.yaml (#1297). Scoped to the block
|
|
89
|
+
// body so we never match a `default:` key from another section (epic, merge).
|
|
90
|
+
// Cross-platform: tolerates CRLF; no path separators hardcoded.
|
|
91
|
+
function loadSddConfig() {
|
|
92
|
+
var out = { default: false, specsDir: '.moflo/specs' };
|
|
93
|
+
var content = MOFLO_YAML;
|
|
94
|
+
if (!content) return out;
|
|
95
|
+
var block = content.match(/^sdd:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m);
|
|
96
|
+
if (!block) return out;
|
|
97
|
+
var body = block[1];
|
|
98
|
+
if (/^\s*default:\s*true\b/im.test(body)) out.default = true;
|
|
99
|
+
var sd = body.match(/^\s*specs_dir:\s*(.+?)\s*$/im);
|
|
100
|
+
if (sd) {
|
|
101
|
+
var v = sd[1].replace(/\s+#.*$/, '').replace(/^["']|["']$/g, '').trim();
|
|
102
|
+
if (v) out.specsDir = v;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read moflo.yaml exactly once per gate process (#1297 review): loadGateConfig
|
|
108
|
+
// and loadSddConfig both parse it, and the gate fires on every Write/Edit — a
|
|
109
|
+
// second read is wasted syscalls. Single readFileSync in try/catch (no existsSync
|
|
110
|
+
// double-stat); ENOENT/unreadable → '' → every parser falls back to defaults.
|
|
111
|
+
function readMofloYaml() {
|
|
112
|
+
try { return fs.readFileSync(path.join(PROJECT_DIR, 'moflo.yaml'), 'utf-8'); }
|
|
113
|
+
catch (e) { return ''; }
|
|
114
|
+
}
|
|
115
|
+
var MOFLO_YAML = readMofloYaml();
|
|
116
|
+
|
|
80
117
|
var config = loadGateConfig();
|
|
118
|
+
var sddConf = loadSddConfig();
|
|
81
119
|
var command = process.argv[2];
|
|
82
120
|
|
|
83
121
|
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
122
|
+
|
|
123
|
+
// #1294 Finding 3 — reads/scans of EPHEMERAL files under the OS temp dir
|
|
124
|
+
// (background-task output/transcripts, agent scratchpads) are transient tool
|
|
125
|
+
// I/O and never carry indexable project knowledge, so they must not trip the
|
|
126
|
+
// memory-first gate. Cross-platform (Rule #1): os.tmpdir() is correct on every
|
|
127
|
+
// OS; we normalize a leading `/private` on both sides so macOS's
|
|
128
|
+
// /var/folders (os.tmpdir) vs /private/var/folders (realpath) symlink pair
|
|
129
|
+
// still matches (CLAUDE.md #1145). Never hardcode `/tmp`.
|
|
130
|
+
function stripPrivate(p) { return p.indexOf('/private/') === 0 ? p.slice('/private'.length) : p; }
|
|
131
|
+
function isEphemeralPath(fp) {
|
|
132
|
+
if (!fp) return false;
|
|
133
|
+
var tmp;
|
|
134
|
+
try { tmp = path.resolve(os.tmpdir()); } catch (e) { return false; }
|
|
135
|
+
var t = stripPrivate(tmp);
|
|
136
|
+
function under(p) { var n = stripPrivate(p); return n === t || n.indexOf(t + path.sep) === 0; }
|
|
137
|
+
var resolved = path.resolve(fp);
|
|
138
|
+
if (!under(resolved)) return false;
|
|
139
|
+
// Under tmp by literal path — confirm it isn't a symlink staged in tmp that
|
|
140
|
+
// dereferences to a real project file (realpath BOTH sides, CLAUDE.md Rule #2).
|
|
141
|
+
// On ENOENT (a not-yet-created tmp file) keep the verdict — still ephemeral.
|
|
142
|
+
try { return under(fs.realpathSync(resolved)); } catch (e) { return true; }
|
|
143
|
+
}
|
|
84
144
|
// #1171 — DANGEROUS gained PowerShell additions to match the matcher widening
|
|
85
145
|
// that now routes the dedicated `PowerShell` tool through check-dangerous-command.
|
|
86
146
|
// POSIX entries still apply because PS will execute them when invoked. Substring
|
|
@@ -228,6 +288,59 @@ function detectFlMode(promptText) {
|
|
|
228
288
|
return null;
|
|
229
289
|
}
|
|
230
290
|
|
|
291
|
+
// #1297 — arm the SDD implement gate from the user prompt. Only /fl or /flo runs
|
|
292
|
+
// can arm it. Explicit -sd/--sdd wins; --no-sdd disarms; otherwise honor the
|
|
293
|
+
// sdd.default config. `-sd` is a distinct token from `-s` (swarm): the `d` sits
|
|
294
|
+
// on the word boundary so `-s\b` never matches `-sd`.
|
|
295
|
+
function detectSddMode(promptText) {
|
|
296
|
+
var p = promptText || '';
|
|
297
|
+
if (!/^\s*\/(?:fl|flo)\b/i.test(p)) return false;
|
|
298
|
+
if (/(?:^|\s)--no-sdd\b/.test(p)) return false;
|
|
299
|
+
if (/(?:^|\s)(?:-sd|--sdd)\b/.test(p)) return true;
|
|
300
|
+
return !!sddConf.default;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Resolve the absolute specs root the same way TS specsRoot does (#1294): split
|
|
304
|
+
// the /-written config value on either separator, reject absolute/`..`-escaping
|
|
305
|
+
// values, and fall back to the gitignored default. Rule #1: no separator hardcoded.
|
|
306
|
+
function sddSpecsRootAbs() {
|
|
307
|
+
var configured = (sddConf.specsDir || '.moflo/specs');
|
|
308
|
+
var segments = configured.split(/[\\/]+/).filter(Boolean);
|
|
309
|
+
var escapes = segments.length === 0
|
|
310
|
+
|| segments.indexOf('..') >= 0
|
|
311
|
+
|| /^([a-zA-Z]:|~)$/.test(segments[0])
|
|
312
|
+
|| configured.charAt(0) === '/'
|
|
313
|
+
|| configured.charAt(0) === '\\';
|
|
314
|
+
if (escapes) return path.join(PROJECT_DIR, '.moflo', 'specs');
|
|
315
|
+
return path.join.apply(path, [PROJECT_DIR].concat(segments));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Is the edited path inside the specs dir? Editing spec.md/plan.md themselves must
|
|
319
|
+
// never trip the implement gate. Compares resolved absolute paths (Rule #1: the
|
|
320
|
+
// edit path may be relative or absolute; normalize both before the prefix test).
|
|
321
|
+
function isInsideSpecsDir(filePath) {
|
|
322
|
+
try {
|
|
323
|
+
var root = sddSpecsRootAbs();
|
|
324
|
+
var abs = path.isAbsolute(filePath) ? filePath : path.resolve(PROJECT_DIR, filePath);
|
|
325
|
+
var rel = path.relative(root, abs);
|
|
326
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
327
|
+
} catch (e) { return false; }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Read plan.md frontmatter for the active slug and report whether it is reviewed.
|
|
331
|
+
// Pure fs + regex (no spawn) so it stays cheap on every Write/Edit. Matches the
|
|
332
|
+
// double-quoted scalar serializeArtifact emits (`status: "reviewed"`).
|
|
333
|
+
function isPlanReviewed(slug) {
|
|
334
|
+
try {
|
|
335
|
+
var planPath = path.join(sddSpecsRootAbs(), slug, 'plan.md');
|
|
336
|
+
if (!fs.existsSync(planPath)) return false;
|
|
337
|
+
var content = fs.readFileSync(planPath, 'utf-8').replace(/\r\n/g, '\n');
|
|
338
|
+
var fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
339
|
+
if (!fm) return false;
|
|
340
|
+
return /^\s*status:\s*["']?reviewed["']?\s*$/im.test(fm[1]);
|
|
341
|
+
} catch (e) { return false; }
|
|
342
|
+
}
|
|
343
|
+
|
|
231
344
|
function classifyNamespaceHint(promptText) {
|
|
232
345
|
var lower = (promptText || '').toLowerCase();
|
|
233
346
|
if (NS_TEST_RE.test(lower)) return 'Memory namespace hint: use "tests" for test inventory and coverage lookups.';
|
|
@@ -300,6 +413,10 @@ function applyPromptStateReset(state, promptText) {
|
|
|
300
413
|
state.flMode = detectFlMode(promptText);
|
|
301
414
|
state.swarmInitialized = false;
|
|
302
415
|
state.hiveInitialized = false;
|
|
416
|
+
// #1297 — arm/disarm the SDD implement gate per prompt. A fresh /flo run starts
|
|
417
|
+
// with no active slug; `flo sdd spec` stamps activeSddSlug during the run.
|
|
418
|
+
state.sddMode = detectSddMode(promptText);
|
|
419
|
+
state.activeSddSlug = null;
|
|
303
420
|
}
|
|
304
421
|
// Match npm/yarn/pnpm/bun test, npx vitest|jest|..., bare runners at command-start only,
|
|
305
422
|
// and language-native test commands. The bare-runner arm is anchored so that
|
|
@@ -525,6 +642,7 @@ switch (command) {
|
|
|
525
642
|
var s = readState();
|
|
526
643
|
if (!s.memoryRequired || isMemorySearchedFor(s)) break;
|
|
527
644
|
var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
|
|
645
|
+
if (isEphemeralPath(process.env.TOOL_INPUT_path)) break;
|
|
528
646
|
if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
|
|
529
647
|
process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
530
648
|
process.exit(2);
|
|
@@ -534,6 +652,9 @@ switch (command) {
|
|
|
534
652
|
var s = readState();
|
|
535
653
|
if (!s.memoryRequired || isMemorySearchedFor(s)) break;
|
|
536
654
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
655
|
+
// Ephemeral tmp/scratch reads are exempt even when they look like guidance
|
|
656
|
+
// (a temp copy is still transient tool I/O, not the indexed source).
|
|
657
|
+
if (isEphemeralPath(fp)) break;
|
|
537
658
|
var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
|
|
538
659
|
if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
|
|
539
660
|
process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
|
|
@@ -654,6 +775,25 @@ switch (command) {
|
|
|
654
775
|
}
|
|
655
776
|
break;
|
|
656
777
|
}
|
|
778
|
+
case 'record-verify-run': {
|
|
779
|
+
// Story #1274 (Epic #1269). Fires PostToolUse on ^Skill$ when the native
|
|
780
|
+
// /verify skill runs, satisfying the verify-before-done gate. Mirrors
|
|
781
|
+
// record-skill-run's fault-tolerant shape. The verification OUTCOME (what
|
|
782
|
+
// was checked, pass/fail) is written to memory by the verify flow itself
|
|
783
|
+
// via mcp__moflo__memory_store — same division of labour as testsRun vs the
|
|
784
|
+
// test output; this recorder only tracks that verification happened.
|
|
785
|
+
var vName = (process.env.TOOL_INPUT_skill || '');
|
|
786
|
+
// Only the native /verify skill satisfies verify-before-done. /ward and
|
|
787
|
+
// /quicken are targeted audits, NOT the completion gate (see fl/sdd.md) —
|
|
788
|
+
// crediting them would let the gate pass without an end-to-end verify.
|
|
789
|
+
if (vName === 'verify') {
|
|
790
|
+
var s = readState();
|
|
791
|
+
if (!s.verifyRun) { s.verifyRun = true; writeState(s); }
|
|
792
|
+
} else if (vName) {
|
|
793
|
+
process.stderr.write('gate: record-verify-run no-op — TOOL_INPUT_skill="' + vName + '" is not verify\n');
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
657
797
|
case 'reset-edit-gates': {
|
|
658
798
|
var fp = process.env.TOOL_INPUT_file_path || '';
|
|
659
799
|
// Inert files (markdown, lockfiles, CHANGELOG, .env.example) AND inert paths
|
|
@@ -664,10 +804,14 @@ switch (command) {
|
|
|
664
804
|
// Test-only edits invalidate testsRun but preserve simplifyRun (#908).
|
|
665
805
|
var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
|
|
666
806
|
var resetTests = s.testsRun;
|
|
807
|
+
// A code edit invalidates a prior verification (Story #1274) — same as tests,
|
|
808
|
+
// including test-only edits (the criteria being verified may have moved).
|
|
809
|
+
var resetVerify = s.verifyRun;
|
|
667
810
|
var resetSimplify = s.simplifyRun && !isTestOnly;
|
|
668
|
-
if (!resetTests && !resetSimplify) break;
|
|
811
|
+
if (!resetTests && !resetSimplify && !resetVerify) break;
|
|
669
812
|
var gates = [];
|
|
670
813
|
if (resetTests) { s.testsRun = false; gates.push('tests'); }
|
|
814
|
+
if (resetVerify) { s.verifyRun = false; gates.push('verify'); }
|
|
671
815
|
if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
|
|
672
816
|
if (fp) {
|
|
673
817
|
s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
|
|
@@ -675,6 +819,44 @@ switch (command) {
|
|
|
675
819
|
writeState(s);
|
|
676
820
|
break;
|
|
677
821
|
}
|
|
822
|
+
case 'check-before-implement': {
|
|
823
|
+
// #1297 — the SDD front-half backstop. When a run is armed for SDD
|
|
824
|
+
// (sddMode, set from -sd/--sdd or sdd.default on a /flo run), block source
|
|
825
|
+
// Write/Edit until a spec exists and its plan is reviewed. Mirrors the
|
|
826
|
+
// memory_first gate shape. Disarmed runs (the default for non-SDD work)
|
|
827
|
+
// pass instantly. Opt out per-project with `gates: sdd_gate: false`.
|
|
828
|
+
if (!config.sdd_gate) break;
|
|
829
|
+
var si = readState();
|
|
830
|
+
if (!si.sddMode) break; // not an SDD run — no enforcement
|
|
831
|
+
var fpi = process.env.TOOL_INPUT_file_path || '';
|
|
832
|
+
if (!fpi) break;
|
|
833
|
+
// Only gate real source edits. Exempt the same inert files/paths the other
|
|
834
|
+
// gates skip, plus the spec/plan artifacts themselves.
|
|
835
|
+
if (EXEMPT.some(function (e) { return fpi.indexOf(e) >= 0; })) break;
|
|
836
|
+
if (!SOURCE_FILE_RE.test(fpi)) break;
|
|
837
|
+
if (EDIT_RESET_SKIP_PATH_RE.test(fpi)) break;
|
|
838
|
+
if (isInsideSpecsDir(fpi)) break;
|
|
839
|
+
if (!si.activeSddSlug) {
|
|
840
|
+
process.stderr.write(
|
|
841
|
+
'BLOCKED: SDD mode is on — author a spec before editing source.\n' +
|
|
842
|
+
'Run: flo sdd spec "<title>" (then review it, and plan)\n' +
|
|
843
|
+
'This run is spec-gated (-sd / sdd.default). One-off skip: re-run with --no-sdd.\n' +
|
|
844
|
+
'Disable per-project via moflo.yaml: gates: sdd_gate: false\n'
|
|
845
|
+
);
|
|
846
|
+
process.exit(2);
|
|
847
|
+
}
|
|
848
|
+
if (!isPlanReviewed(si.activeSddSlug)) {
|
|
849
|
+
process.stderr.write(
|
|
850
|
+
'BLOCKED: SDD — the plan for "' + si.activeSddSlug + '" is not reviewed yet.\n' +
|
|
851
|
+
'Author + review the plan first:\n' +
|
|
852
|
+
' flo sdd plan ' + si.activeSddSlug + '\n' +
|
|
853
|
+
' flo sdd review ' + si.activeSddSlug + ' plan\n' +
|
|
854
|
+
'One-off skip: re-run with --no-sdd. Disable via moflo.yaml: gates: sdd_gate: false\n'
|
|
855
|
+
);
|
|
856
|
+
process.exit(2);
|
|
857
|
+
}
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
678
860
|
case 'check-before-pr': {
|
|
679
861
|
// Anchored to command-start (or chained via && / || / ;) so heredoc bodies
|
|
680
862
|
// and quoted strings that contain the literal "gh pr create" don't trip
|
|
@@ -734,6 +916,40 @@ switch (command) {
|
|
|
734
916
|
process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
|
|
735
917
|
process.exit(2);
|
|
736
918
|
}
|
|
919
|
+
case 'check-before-done': {
|
|
920
|
+
// Story #1274 (Epic #1269) + #1294. Verify-before-done: block `gh pr create`
|
|
921
|
+
// until the change has been verified end-to-end (the /verify skill) against
|
|
922
|
+
// the plan's acceptance criteria. ON by default (#1294) — /flo delegates to
|
|
923
|
+
// /verify, so a default run does the acceptance check; disable per-project
|
|
924
|
+
// with `gates: verify_before_done: false` or per-run `--no-verify`. Same
|
|
925
|
+
// trigger + no-source exemption as check-before-pr, so they compose on one
|
|
926
|
+
// command (docs-only diffs are exempt, so this never blocks a docs PR).
|
|
927
|
+
if (!config.verify_before_done) break;
|
|
928
|
+
var cmd = process.env.TOOL_INPUT_command || '';
|
|
929
|
+
if (!/(?:^|&&\s*|\|\|\s*|;\s*)\s*(?:[A-Z_][A-Z0-9_]*=\S+\s+)*gh\s+pr\s+create\b/.test(cmd)) break;
|
|
930
|
+
// No-source-files exemption — a docs-only / path-inert diff needs no verify.
|
|
931
|
+
var changedD = getChangedFilesVsBase();
|
|
932
|
+
if (changedD && changedD.length > 0) {
|
|
933
|
+
var hasSourceD = changedD.some(function(f) {
|
|
934
|
+
return SOURCE_FILE_RE.test(f) && !EDIT_RESET_SKIP_PATH_RE.test(f);
|
|
935
|
+
});
|
|
936
|
+
if (!hasSourceD) {
|
|
937
|
+
var reasonD = changedD.every(function(f) { return DOCS_ONLY_RE.test(f); }) ? 'Docs-only' : 'No source files in branch diff';
|
|
938
|
+
process.stdout.write(reasonD + ' (' + changedD.length + ' file' + (changedD.length === 1 ? '' : 's') + ') — skipping verify-before-done gate.\n');
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
var sd = readState();
|
|
943
|
+
if (sd.verifyRun) break;
|
|
944
|
+
process.stderr.write('BLOCKED: gh pr create requires verification before done:\n');
|
|
945
|
+
process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\n');
|
|
946
|
+
if (sd.lastResetBy && sd.lastResetBy.file && (sd.lastResetBy.gates || []).indexOf('verify') >= 0) {
|
|
947
|
+
process.stderr.write('Last gate reset: ' + sd.lastResetBy.file + ' (verify)\n');
|
|
948
|
+
}
|
|
949
|
+
process.stderr.write('Disable via moflo.yaml:\n');
|
|
950
|
+
process.stderr.write(' gates:\n verify_before_done: false\n');
|
|
951
|
+
process.exit(2);
|
|
952
|
+
}
|
|
737
953
|
case 'check-dangerous-command': {
|
|
738
954
|
// #1171 follow-up — strip quoted string bodies and heredoc bodies before
|
|
739
955
|
// substring-matching DANGEROUS. Without this, `git commit -m "...remove-item
|
|
@@ -119,11 +119,16 @@ The whole point is to feed moflo's existing strengths, not start a parallel trac
|
|
|
119
119
|
| Destination | When | How |
|
|
120
120
|
|-------------|------|-----|
|
|
121
121
|
| **`/flo` ticket** (Recommended) | The spec is a unit of work to build | Map the spec to Description / Acceptance Criteria / Suggested Test Cases, then run `/fl -t <title>` to create the GitHub issue (or `/fl <issue#>` to implement immediately). The spec's Success criteria become Acceptance Criteria; Scope+Approach become the Description. |
|
|
122
|
+
| **SDD spec artifact** | The user wants the spec to drive an `/flo --sdd` run (the spec→plan→implement→verify cycle) | Pipe the synthesized markdown into the SDD spine: `flo sdd spec "<title>" --from -` (writes `.moflo/specs/<slug>/spec.md`, git-tracked + memory-indexed). Rename the spec's **Success criteria** section to **`## Acceptance Criteria`** first — the SDD validator requires it. Then `flo sdd review <slug>` when signed off, and hand to `/fl --sdd <issue#>` (or `/fl -sd <issue#>`). This is the pre-execution counterpart to `/meditate`. |
|
|
122
123
|
| **Spell** | The spec describes a repeatable, automatable pipeline | Hand the spec to `/spell-builder` as the design input. |
|
|
123
124
|
| **Memory** | The spec is a decision/insight to retain, not build now | `mcp__moflo__memory_store { namespace: "learnings", key: "spec:<topic>", value: <spec> }` (use `patterns` for a reusable approach). |
|
|
124
125
|
| **Just the file** | The user wants the artifact only | `Write` the markdown to a path the user names, or to a repo-relative `docs/specs/<kebab-title>.md`. Never hardcode an absolute or OS-specific path (e.g. `/tmp`); build the path from the project root. |
|
|
125
126
|
|
|
126
|
-
Offer to do more than one (e.g. save to memory **and** open a ticket). Default to the `/flo` ticket path when the user is unsure.
|
|
127
|
+
Offer to do more than one (e.g. save to memory **and** open a ticket). Default to the `/flo` ticket path when the user is unsure. For the **SDD spec** path, always create the artifact through the `flo sdd` CLI — never hand-write the `.moflo/specs/...` path (cross-platform, Rule #1).
|
|
128
|
+
|
|
129
|
+
## See Also — SDD
|
|
130
|
+
|
|
131
|
+
`.claude/skills/fl/sdd.md` — how `/flo --sdd` consumes the spec artifact this skill produces and drives it through plan → implement → verify.
|
|
127
132
|
|
|
128
133
|
## Guardrails
|
|
129
134
|
|