moflo 4.12.3 → 4.12.4-rc.2

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 (98) hide show
  1. package/.claude/agents/core/coder.md +2 -3
  2. package/.claude/agents/core/researcher.md +3 -4
  3. package/.claude/agents/core/reviewer.md +6 -8
  4. package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
  5. package/.claude/helpers/gate-hook.mjs +67 -2
  6. package/.claude/helpers/gate.cjs +173 -12
  7. package/.claude/helpers/hook-handler.cjs +0 -0
  8. package/.claude/helpers/prompt-hook.mjs +0 -0
  9. package/.claude/skills/connector-builder/SKILL.md +5 -4
  10. package/.claude/skills/fl/phases.md +1 -1
  11. package/.claude/skills/fl/sdd.md +1 -1
  12. package/.claude/skills/spell-builder/SKILL.md +5 -4
  13. package/.claude/skills/spell-builder/architecture.md +4 -4
  14. package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
  15. package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
  16. package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
  17. package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
  18. package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
  19. package/.claude/skills/verify/SKILL.md +24 -3
  20. package/bin/gate-hook.mjs +67 -2
  21. package/bin/gate.cjs +173 -12
  22. package/bin/generate-code-map.mjs +70 -17
  23. package/bin/index-all.mjs +38 -0
  24. package/bin/index-guidance.mjs +8 -41
  25. package/bin/index-patterns.mjs +2 -2
  26. package/bin/index-tests.mjs +6 -4
  27. package/bin/lib/guidance-config.mjs +119 -0
  28. package/bin/lib/index-fingerprint.mjs +0 -0
  29. package/dist/src/cli/commands/agent.js +1 -89
  30. package/dist/src/cli/commands/analyze.js +1 -1
  31. package/dist/src/cli/commands/benchmark.js +11 -8
  32. package/dist/src/cli/commands/claims.js +2 -2
  33. package/dist/src/cli/commands/completions.js +1 -1
  34. package/dist/src/cli/commands/config.js +256 -96
  35. package/dist/src/cli/commands/deployment.js +6 -6
  36. package/dist/src/cli/commands/diagnose.js +1 -1
  37. package/dist/src/cli/commands/doctor-checks-config.js +18 -22
  38. package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
  39. package/dist/src/cli/commands/doctor-fixes.js +36 -11
  40. package/dist/src/cli/commands/doctor.js +1 -1
  41. package/dist/src/cli/commands/embeddings.js +64 -23
  42. package/dist/src/cli/commands/github.js +1 -1
  43. package/dist/src/cli/commands/guidance.js +10 -10
  44. package/dist/src/cli/commands/hive-mind.js +8 -70
  45. package/dist/src/cli/commands/hooks.js +32 -219
  46. package/dist/src/cli/commands/mcp.js +5 -2
  47. package/dist/src/cli/commands/memory.js +59 -46
  48. package/dist/src/cli/commands/neural.js +21 -21
  49. package/dist/src/cli/commands/performance.js +14 -7
  50. package/dist/src/cli/commands/plugins.js +2 -2
  51. package/dist/src/cli/commands/providers.js +2 -2
  52. package/dist/src/cli/commands/retire.js +1 -1
  53. package/dist/src/cli/commands/route.js +1 -1
  54. package/dist/src/cli/commands/security.js +4 -4
  55. package/dist/src/cli/commands/status.js +18 -13
  56. package/dist/src/cli/commands/task.js +8 -0
  57. package/dist/src/cli/config/cli-config-store.js +326 -0
  58. package/dist/src/cli/hooks/workers/index.js +15 -9
  59. package/dist/src/cli/index.js +43 -33
  60. package/dist/src/cli/init/claudemd-generator.js +3 -3
  61. package/dist/src/cli/init/helpers-generator.js +203 -20
  62. package/dist/src/cli/init/moflo-yaml-template.js +11 -3
  63. package/dist/src/cli/init/settings-generator.js +10 -2
  64. package/dist/src/cli/mcp-client.js +12 -2
  65. package/dist/src/cli/mcp-server.js +5 -23
  66. package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
  67. package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
  68. package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
  69. package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
  70. package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
  71. package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
  72. package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
  73. package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
  74. package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
  75. package/dist/src/cli/mcp-tools/security-tools.js +1 -1
  76. package/dist/src/cli/mcp-tools/session-tools.js +116 -0
  77. package/dist/src/cli/mcp-tools/synthetic.js +74 -0
  78. package/dist/src/cli/mcp-tools/system-tools.js +126 -78
  79. package/dist/src/cli/mcp-tools/task-tools.js +101 -1
  80. package/dist/src/cli/movector/coverage-router.js +48 -6
  81. package/dist/src/cli/movector/diff-classifier.js +8 -6
  82. package/dist/src/cli/movector/flash-attention.js +1 -1
  83. package/dist/src/cli/movector/vector-db.js +1 -1
  84. package/dist/src/cli/parser.js +81 -12
  85. package/dist/src/cli/services/daemon-lock.js +5 -2
  86. package/dist/src/cli/services/hook-block-hash.js +3 -1
  87. package/dist/src/cli/services/moflo-version.js +62 -0
  88. package/dist/src/cli/services/movector-training.js +1 -1
  89. package/dist/src/cli/services/spell-gate.js +6 -4
  90. package/dist/src/cli/services/worker-daemon.js +37 -7
  91. package/dist/src/cli/shared/utils/load-average.js +59 -0
  92. package/dist/src/cli/spells/commands/agent-command.js +55 -25
  93. package/dist/src/cli/suggest.js +1 -1
  94. package/dist/src/cli/swarm/swarm-persistence.js +112 -0
  95. package/dist/src/cli/swarm/unified-coordinator.js +118 -3
  96. package/dist/src/cli/version.js +1 -1
  97. package/package.json +5 -3
  98. package/dist/src/cli/mcp-tools/github-tools.js +0 -318
@@ -236,10 +236,9 @@ mcp__moflo__performance_benchmark {
236
236
  iterations: 10
237
237
  }
238
238
 
239
- // Analyze bottlenecks
239
+ // Read this process's CPU / memory / heap usage
240
240
  mcp__moflo__performance_report {
241
- component: "api-endpoint",
242
- metrics: ["response-time", "memory-usage"]
241
+ format: "detailed"
243
242
  }
244
243
  ```
245
244
 
@@ -155,10 +155,9 @@ mcp__moflo__memory_search {
155
155
 
156
156
  ### Analysis Tools
157
157
  ```javascript
158
- // Analyze codebase
159
- mcp__moflo__github_repo_analyze {
160
- repo: "current",
161
- analysis_type: "code_quality"
158
+ // Understand what a change touches and how risky it is
159
+ mcp__moflo__analyze_diff {
160
+ ref: "main"
162
161
  }
163
162
 
164
163
  // Track research metrics
@@ -304,16 +304,14 @@ mcp__moflo__memory_retrieve {
304
304
 
305
305
  ### Code Analysis
306
306
  ```javascript
307
- // Analyze code quality
308
- mcp__moflo__github_repo_analyze {
309
- repo: "current",
310
- analysis_type: "code_quality"
307
+ // Assess the risk and shape of the change under review
308
+ mcp__moflo__analyze_diff {
309
+ ref: "main"
311
310
  }
312
311
 
313
- // Run security scan
314
- mcp__moflo__github_repo_analyze {
315
- repo: "current",
316
- analysis_type: "security"
312
+ // Scan the diff for prompt-injection and unsafe content
313
+ mcp__moflo__aidefence_scan {
314
+ content: "<the diff or file under review>"
317
315
  }
318
316
  ```
319
317
 
@@ -1,6 +1,6 @@
1
1
  # Spell Engine — Definition Format & Step Types
2
2
 
3
- **Purpose:** How to define a spell (YAML/JSON schema, arguments, steps, variable interpolation) and a reference for the nine built-in step command types. For execution mechanics (running, dry-run, error codes, pause/resume, layering, credentials), see `.claude/guidance/moflo-spell-runner.md`.
3
+ **Purpose:** How to define a spell (YAML/JSON schema, arguments, steps, variable interpolation) and a reference for the built-in step command types. For execution mechanics (running, dry-run, error codes, pause/resume, layering, credentials), see `.claude/guidance/moflo-spell-runner.md`.
4
4
 
5
5
  ---
6
6
 
@@ -96,16 +96,16 @@ steps:
96
96
  config:
97
97
  command: "curl -s {args.api_url}"
98
98
  - id: process
99
- type: agent
99
+ type: bash
100
100
  config:
101
- prompt: "Analyze this response: {fetch-url.stdout}"
101
+ command: 'claude -p "Analyze this response: {fetch-url.stdout}"'
102
102
  ```
103
103
 
104
104
  ---
105
105
 
106
106
  ## Step Command Types
107
107
 
108
- **Nine built-in step types are registered automatically.** Each implements `execute()`, `validate()`, `describeOutputs()`, and optional `rollback()`. To add new step types via JS/TS files, YAML composite steps, or `moflo-step-*` npm packages, see `.claude/guidance/moflo-spell-custom-steps.md`.
108
+ **Fifteen built-in step types are registered automatically** (one of them, `agent`, is registered but not executable — see below). Each implements `execute()`, `validate()`, `describeOutputs()`, and optional `rollback()`. To add new step types via JS/TS files, YAML composite steps, or `moflo-step-*` npm packages, see `.claude/guidance/moflo-spell-custom-steps.md`.
109
109
 
110
110
  ### bash — Run a Shell Command
111
111
 
@@ -122,18 +122,21 @@ steps:
122
122
 
123
123
  ---
124
124
 
125
- ### agent — Spawn a Claude Subagent
125
+ ### agent — NOT EXECUTABLE
126
+
127
+ **Do not use this step type.** It has never spawned a subagent — moflo has no agent spawner in the spell runner. It is still registered so existing spell YAML keeps parsing, but casting it now always fails with an explanatory error (#1334). Earlier versions returned `success: true` and a `result` string for work that never happened.
128
+
129
+ **To run a Claude subagent from a spell, use a `bash` step:**
126
130
 
127
131
  ```yaml
128
132
  - id: research
129
- type: agent
133
+ type: bash
130
134
  config:
131
- agentType: "researcher" # Required. researcher, coder, tester, etc.
132
- prompt: "Find all API endpoints in {args.directory}" # Required.
133
- background: false # Optional. Default false.
135
+ command: 'claude -p "Find all API endpoints in {args.directory}"'
136
+ timeout: 300000
134
137
  ```
135
138
 
136
- **Outputs:** `result` (string), `agentType` (string), `prompt` (string).
139
+ **Outputs:** none the step always fails. `agentType` and `prompt` are echoed in the failure for diagnosis only.
137
140
 
138
141
  ---
139
142
 
@@ -32,14 +32,79 @@ if (hookContext.tool_name) env.TOOL_NAME = hookContext.tool_name;
32
32
  if (typeof hookContext.session_id === 'string' && hookContext.session_id) {
33
33
  env.HOOK_SESSION_ID = hookContext.session_id;
34
34
  }
35
+ // #1332: structured tool inputs are forwarded as JSON, not dropped.
36
+ //
37
+ // This previously forwarded ONLY string values, so any object-valued input was
38
+ // invisible to gate.cjs. That blocked the verify-before-done gate from reading
39
+ // `/verify`'s per-criterion verdict, which #1328 stores in memory_store's
40
+ // `metadata` — an object. Parsing the verdict out of the prose `value` string
41
+ // instead would re-create exactly the free-text dependency #1328 removed.
42
+ //
43
+ // Cross-platform (Rule #1): Windows caps a single environment variable at
44
+ // ~32KB and the whole block at ~32K wide chars, and exceeding it fails the
45
+ // spawn rather than truncating. Newly-forwarded values are therefore skipped
46
+ // when oversized, not clipped — a truncated JSON blob would parse as malformed
47
+ // on the far side and read as a corrupt record rather than an absent one.
48
+ // `metadata` is capped at 64KB by memory_store, so a real verdict never nears
49
+ // this. STRING values keep their previous uncapped behaviour byte-for-byte:
50
+ // gate.cjs reads TOOL_INPUT_command, and dropping an oversized heredoc command
51
+ // would silently stop check-dangerous-command from firing on the exact inputs
52
+ // most worth checking.
53
+ var MAX_STRUCTURED_LEN = 16384;
35
54
  if (hookContext.tool_input && typeof hookContext.tool_input === 'object') {
36
55
  Object.keys(hookContext.tool_input).forEach(function(key) {
37
- if (typeof hookContext.tool_input[key] === 'string') {
38
- env['TOOL_INPUT_' + key] = hookContext.tool_input[key];
56
+ var raw = hookContext.tool_input[key];
57
+ if (typeof raw === 'string') {
58
+ env['TOOL_INPUT_' + key] = raw;
59
+ return;
39
60
  }
61
+ var val;
62
+ if (typeof raw === 'number' || typeof raw === 'boolean') {
63
+ val = String(raw);
64
+ } else if (raw && typeof raw === 'object') {
65
+ try { val = JSON.stringify(raw); } catch (e) { return; }
66
+ } else {
67
+ return; // null/undefined/function — nothing meaningful to forward
68
+ }
69
+ if (val.length > MAX_STRUCTURED_LEN) return;
70
+ env['TOOL_INPUT_' + key] = val;
40
71
  });
41
72
  }
42
73
 
74
+ // #1322: forward the parts of tool_response that actually exist, so a gate can
75
+ // observe an OUTCOME rather than only the intent it was handed.
76
+ //
77
+ // Claude Code's PostToolUse payload carries NO exit status — probed on v2.1.220,
78
+ // tool_response for a Bash call is {stdout, stderr, interrupted, isImage,
79
+ // noOutputExpected}. PostToolUse also does not fire at all when the command
80
+ // exits non-zero, so the only case a gate can still be fooled by is an exit code
81
+ // MASKED by a pipe or `|| true`, where the response looks clean. The runner's
82
+ // own output is the sole remaining signal; record-test-run reads it in gate.cjs.
83
+ //
84
+ // Tail, not head. Every test runner prints its pass/fail summary LAST, so
85
+ // clipping the front of a long log would discard the exact lines this exists to
86
+ // read. Bounds are deliberately tight — Windows caps the whole environment
87
+ // block at ~32K wide chars and fails the spawn rather than truncating, and
88
+ // TOOL_INPUT_command is already forwarded uncapped alongside these.
89
+ var MAX_RESPONSE_STDOUT = 4096;
90
+ var MAX_RESPONSE_STDERR = 2048;
91
+ function tailOf(value, max) {
92
+ return value.length > max ? value.slice(value.length - max) : value;
93
+ }
94
+ if (hookContext.tool_response && typeof hookContext.tool_response === 'object') {
95
+ var resp = hookContext.tool_response;
96
+ if (typeof resp.stdout === 'string' && resp.stdout) {
97
+ env.TOOL_RESPONSE_stdout = tailOf(resp.stdout, MAX_RESPONSE_STDOUT);
98
+ }
99
+ if (typeof resp.stderr === 'string' && resp.stderr) {
100
+ env.TOOL_RESPONSE_stderr = tailOf(resp.stderr, MAX_RESPONSE_STDERR);
101
+ }
102
+ // Boolean — the string-typed forwarding above would drop it silently.
103
+ if (typeof resp.interrupted === 'boolean') {
104
+ env.TOOL_RESPONSE_interrupted = String(resp.interrupted);
105
+ }
106
+ }
107
+
43
108
  // Run gate.cjs with the enriched environment
44
109
  var projectDir = (env.CLAUDE_PROJECT_DIR || process.cwd()).replace(/^\/([a-z])\//i, '$1:/');
45
110
  var gateScript = resolve(projectDir, '.claude/helpers/gate.cjs');
@@ -139,6 +139,14 @@ var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state
139
139
  // failures and "fixes" them with allow rules — which cannot override a hook
140
140
  // block at all (#1307 finding 5). Naming the gate makes the cause self-evident.
141
141
  var GATE_ORIGIN_NOTE = 'This is a moflo hook, not a Claude Code permission rule — allow-rules cannot override it.';
142
+
143
+ // #1348 — the pre-PR gates are order-dependent and each block message named only
144
+ // its own missing gate, so a caller could satisfy them one at a time forever:
145
+ // /flo-simplify may edit code (which resets tests and verify), and /verify must
146
+ // not, so simplify strictly precedes verify. Both blocking gates print this.
147
+ // /verify's Step 5 memory_store carries the verdict AND stamps learnings, which
148
+ // is why learnings has no separate step here.
149
+ var ORDER_HINT = 'Order that satisfies all of them: tests green -> /flo-simplify (re-run tests if it edits) -> /verify -> its memory_store verdict -> gh pr create\n';
142
150
  var GATE_DISABLE_NOTE = 'Disable per-gate via moflo.yaml: gates: memory_first: false';
143
151
 
144
152
  // #1294 Finding 3 — reads/scans of EPHEMERAL files under the OS temp dir
@@ -498,6 +506,53 @@ function applyPromptStateReset(state, promptText) {
498
506
  // and language-native test commands. The bare-runner arm is anchored so that
499
507
  // `npm install jest`, `grep -r vitest src/`, and similar don't false-positive.
500
508
  var TEST_RUNNER_RE = /(?:^|[^a-z])(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:test|t)(?:[:\s]|$)|\b(?:npx|pnpx)\s+(?:vitest|jest|mocha|ava|tap|jasmine|pytest)\b|(?:^|;|&&|\|\|)\s*(?:vitest|jest|pytest|mocha|jasmine|tap|ava)\s|\b(?:cargo|go|deno|dotnet|mvn)\s+test\b|\bgradle\w*\s+test\b/i;
509
+ // #1322 — failure markers in a test runner's own OUTPUT.
510
+ //
511
+ // This is deliberately not an exit-code check: Claude Code's PostToolUse payload
512
+ // carries no exit status, and PostToolUse does not fire at all when a command
513
+ // exits non-zero — so an unmasked red suite already leaves testsRun false, by
514
+ // accident of the hook lifecycle rather than by design. What DOES defeat the
515
+ // gate is a masked exit (`npm test | tail -20`, `npm test || true`,
516
+ // `npm test 2>&1 | grep -i fail`): the pipeline exits 0, PostToolUse fires with
517
+ // a clean-looking response, and a red suite credits the gate. Output is the only
518
+ // signal left, and it is genuinely weaker than a status — see the ticket.
519
+ //
520
+ // Every arm matches a SUMMARY shape a runner emits, never a bare "fail", which
521
+ // occurs constantly in ordinary passing test names ("returns null when the
522
+ // lookup failed"). The count arm excludes an explicit zero so jest's
523
+ // `0 failed, 12 passed` cannot self-block.
524
+ //
525
+ // The count arm's trailing lookahead is what keeps a GREEN run from blocking
526
+ // itself. Mocha's default spec reporter prints every passing test name, so
527
+ // `npm test | tail -20` on a green suite legitimately contains lines like
528
+ // `✓ handles 2 failed retries`. A real summary is followed by a delimiter or a
529
+ // line end (`3 failed | 40 passed`, `1 failed, 2 passed`, `1 failing`), never by
530
+ // more prose — so a lowercase word after the count means it is a sentence, not a
531
+ // tally. `tests`/`test` is exempted because `2 failed tests` is a real summary.
532
+ // Same-line whitespace only: at a line end there is nothing to disqualify.
533
+ var TEST_FAILURE_RE = new RegExp([
534
+ '\\b(?!0\\b)\\d+\\s+(?:tests?\\s+)?(?:failed|failing|failures?)\\b(?![^\\S\\n]+(?!tests?\\b)[a-z])', // vitest/jest/pytest/mocha counts
535
+ '^\\s*(?:FAIL|FAILED)\\b', // vitest + jest per-file, pytest FAILED
536
+ '^\\s*---\\s*FAIL:', // go test
537
+ '\\btest result:\\s*FAILED\\b', // cargo
538
+ '^npm ERR!', // npm wrapper around any of the above
539
+ ].join('|'), 'im');
540
+
541
+ /**
542
+ * #1322 — why a just-fired record-test-run must NOT be credited, or null.
543
+ *
544
+ * Absent output is not evidence of failure: a quiet green `npm test > /dev/null`
545
+ * and a silently-masked red one are indistinguishable, and treating the pair as
546
+ * failures would block every consumer who redirects test output. Absent means
547
+ * unknown, and unknown keeps the pre-#1322 behaviour.
548
+ */
549
+ function detectTestFailure() {
550
+ if (process.env.TOOL_RESPONSE_interrupted === 'true') return 'the run was interrupted';
551
+ var out = (process.env.TOOL_RESPONSE_stdout || '') + '\n' + (process.env.TOOL_RESPONSE_stderr || '');
552
+ if (!out.trim()) return null;
553
+ var hit = out.match(TEST_FAILURE_RE);
554
+ return hit ? 'output reports "' + hit[0].trim().slice(0, 40) + '"' : null;
555
+ }
501
556
  // Edits to these don't change runtime behaviour, so they don't invalidate prior test/simplify runs.
502
557
  // Lock files and .gitignore are tracked but inert; package.json/*.yaml ARE source — they reset.
503
558
  var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^|[\\\/])(CHANGELOG(?:\.md)?|\.env\.example|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb)$/i;
@@ -508,7 +563,13 @@ var EDIT_RESET_SKIP_BOTH_RE = /\.(md|markdown|txt|rst|adoc|lock|gitignore)$|(?:^
508
563
  // they shouldn't reset testsRun/simplifyRun the way a real source edit does.
509
564
  // Trailing terminator includes `.` so the single-file template form
510
565
  // `.github/PULL_REQUEST_TEMPLATE.md` matches alongside the directory form.
511
- var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\/])\.github[\\\/](?:workflows|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)(?:[\\\/.]|$)/i;
566
+ // #1348 `.moflo/` joins them. It is moflo's own gitignored state directory
567
+ // (daemon locks, memory db, SDD spec/plan artifacts), so nothing written there
568
+ // can appear in the branch diff, and a spec edit invalidating the test run is
569
+ // pure noise. Scoped to the reset only — deliberately NOT added to EXEMPT,
570
+ // which would also un-gate reads of `.moflo/specs/**`, and those are indexed
571
+ // guidance that memory-first should still route through a search.
572
+ var EDIT_RESET_SKIP_PATH_RE = /(?:^|[\\\/])\.github[\\\/](?:workflows|ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)(?:[\\\/.]|$)|(?:^|[\\\/])\.moflo[\\\/]/i;
512
573
  // Test files: invalidate the testing gate (tests are stale once test code changes)
513
574
  // but NOT the simplify gate — /simplify already reviewed the production code; touching
514
575
  // a test file or fixture doesn't expose new untested surface for code review (#908).
@@ -637,8 +698,11 @@ function getChangedFilesVsBase() {
637
698
 
638
699
  switch (command) {
639
700
  case 'check-before-agent': {
640
- // Advisory only agent spawning is never blocked.
641
- // Memory-first enforcement happens at the scan/read gate layer.
701
+ // Mostly advisory. The TaskCreate + memory reminders below go to stdout and
702
+ // never block their wording must not claim otherwise (#1326). The one
703
+ // exception is the #952 swarm/hive check at the bottom of this case, which
704
+ // writes to stderr and exits 2.
705
+ // Memory-first enforcement otherwise happens at the scan/read gate layer.
642
706
  // SubagentStart hook injects guidance directive into subagent context.
643
707
  //
644
708
  // #931 — TaskCreate REMINDER and the namespace hint moved here from
@@ -647,7 +711,7 @@ switch (command) {
647
711
  // consumer.
648
712
  var s = readState();
649
713
  if (config.task_create_first && !s.tasksCreated) {
650
- process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
714
+ process.stdout.write('REMINDER: Use TaskCreate before spawning agents.\n');
651
715
  }
652
716
  if (config.memory_first && s.memoryRequired && !s.memorySearched) {
653
717
  process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
@@ -809,8 +873,17 @@ switch (command) {
809
873
  case 'record-test-run': {
810
874
  var cmd = process.env.TOOL_INPUT_command || '';
811
875
  if (TEST_RUNNER_RE.test(cmd)) {
876
+ // #1322 — a red run is evidence AGAINST the gate, so it also clears a
877
+ // flag an earlier green run earned. Without the reset, `npm test` (green)
878
+ // followed by an edit and `npm test | tail -20` (red) would leave the
879
+ // gate satisfied by the stale first run — the edit resets testsRun, but
880
+ // the masked red run would immediately set it back.
881
+ var failure = detectTestFailure();
812
882
  var s = readState();
813
- if (!s.testsRun) {
883
+ if (failure) {
884
+ if (s.testsRun) { s.testsRun = false; writeState(s); }
885
+ process.stderr.write('gate: record-test-run not credited — ' + failure + '\n');
886
+ } else if (!s.testsRun) {
814
887
  s.testsRun = true;
815
888
  writeState(s);
816
889
  }
@@ -865,30 +938,92 @@ switch (command) {
865
938
  // crediting them would let the gate pass without an end-to-end verify.
866
939
  if (vName === 'verify') {
867
940
  var s = readState();
868
- if (!s.verifyRun) { s.verifyRun = true; writeState(s); }
941
+ // #1332: invoking /verify starts a verification; it does not conclude
942
+ // one. Clear any prior verdict so the run in progress cannot inherit the
943
+ // PASS from a previous issue and satisfy check-before-done on its own.
944
+ if (!s.verifyRun || s.verifyOutcome) {
945
+ s.verifyRun = true;
946
+ s.verifyOutcome = null;
947
+ writeState(s);
948
+ }
869
949
  } else if (vName) {
870
950
  process.stderr.write('gate: record-verify-run no-op — TOOL_INPUT_skill="' + vName + '" is not verify\n');
871
951
  }
872
952
  break;
873
953
  }
954
+ case 'record-verify-outcome': {
955
+ // #1332. Fires PostToolUse on mcp__moflo__memory_store. `record-verify-run`
956
+ // above proves a verification was ATTEMPTED; this proves how it ENDED.
957
+ //
958
+ // The verdict is read from the structured record #1328 made /verify write
959
+ // to memory_store's `metadata` — never parsed out of the prose `value`,
960
+ // which is precisely the free-text dependency #1328 removed. gate-hook.mjs
961
+ // forwards the object as JSON (see its MAX_STRUCTURED_LEN note).
962
+ var mKey = process.env.TOOL_INPUT_key || '';
963
+ if (mKey.indexOf('verify:') !== 0) break;
964
+ var rawMeta = process.env.TOOL_INPUT_metadata || '';
965
+ if (!rawMeta) {
966
+ process.stderr.write('gate: record-verify-outcome — "' + mKey + '" carries no metadata; verdict not recorded\n');
967
+ break;
968
+ }
969
+ var parsedMeta = null;
970
+ try { parsedMeta = JSON.parse(rawMeta); } catch (e) { parsedMeta = null; }
971
+ if (!parsedMeta || typeof parsedMeta !== 'object' || parsedMeta.type !== 'verify-record') break;
972
+ var overall = typeof parsedMeta.overall === 'string' ? parsedMeta.overall.toUpperCase() : '';
973
+ if (overall !== 'PASS' && overall !== 'FAIL' && overall !== 'UNVERIFIED') {
974
+ process.stderr.write('gate: record-verify-outcome — unrecognised overall="' + parsedMeta.overall + '"; treating as not-passing\n');
975
+ overall = 'UNVERIFIED';
976
+ }
977
+ var vs = readState();
978
+ // #1348 — refuse a verdict for a run that is no longer live. `verifyRun`
979
+ // false here means a code edit fired reset-edit-gates between the /verify
980
+ // invocation and this store, so the verdict describes pre-edit code.
981
+ // Recording it anyway produced the contradictory `verifyRun:false,
982
+ // verifyOutcome:'PASS'` state that check-before-done's four-way message
983
+ // chain has no branch for — it reports "a code edit invalidated the previous
984
+ // verification" while a PASS sits in state, which reads as a gate bug.
985
+ if (!vs.verifyRun) {
986
+ process.stderr.write('gate: record-verify-outcome — "' + mKey + '" arrived after a code edit invalidated the run; verdict not recorded (re-run /verify)\n');
987
+ break;
988
+ }
989
+ vs.verifyOutcome = overall;
990
+ writeState(vs);
991
+ break;
992
+ }
874
993
  case 'reset-edit-gates': {
875
994
  var fp = process.env.TOOL_INPUT_file_path || '';
876
995
  // Inert files (markdown, lockfiles, CHANGELOG, .env.example) AND inert paths
877
996
  // (.github/workflows/, .github/ISSUE_TEMPLATE/, .github/PULL_REQUEST_TEMPLATE/, #1176):
878
997
  // no gate reset — editing these doesn't expose new runtime surface.
879
998
  if (fp && (EDIT_RESET_SKIP_BOTH_RE.test(fp) || EDIT_RESET_SKIP_PATH_RE.test(fp))) break;
999
+ // #1348 — a scratchpad write under the OS temp dir is transient tool I/O and
1000
+ // can never reach the branch diff, but it used to reset tests + simplify +
1001
+ // verify like any source change: a /verify run that jotted a probe cleared
1002
+ // the /flo-simplify stamp, and the two gates invalidated each other with no
1003
+ // ordering that satisfied both. Reuses the predicate the memory-first gate
1004
+ // already trusts for these paths (#1294) rather than a second one.
1005
+ // Scope: tmp-only, NOT project-root containment — so a project rooted under
1006
+ // tmp has its own edits skipped too (pinned in the #1348 tests). Containment
1007
+ // is the more general rule, but it fails OPEN when the root can't be
1008
+ // resolved, and a gate that silently stops resetting is the worse failure.
1009
+ if (isEphemeralPath(fp)) break;
880
1010
  var s = readState();
881
1011
  // Test-only edits invalidate testsRun but preserve simplifyRun (#908).
882
1012
  var isTestOnly = fp && EDIT_RESET_SKIP_SIMPLIFY_ONLY_RE.test(fp);
883
1013
  var resetTests = s.testsRun;
884
1014
  // A code edit invalidates a prior verification (Story #1274) — same as tests,
885
1015
  // including test-only edits (the criteria being verified may have moved).
886
- var resetVerify = s.verifyRun;
1016
+ // #1332: also fires when a verdict lingers without the flag, so no path
1017
+ // can leave a recorded outcome behind after a source edit.
1018
+ var resetVerify = s.verifyRun || !!s.verifyOutcome;
887
1019
  var resetSimplify = s.simplifyRun && !isTestOnly;
888
1020
  if (!resetTests && !resetSimplify && !resetVerify) break;
889
1021
  var gates = [];
890
1022
  if (resetTests) { s.testsRun = false; gates.push('tests'); }
891
- if (resetVerify) { s.verifyRun = false; gates.push('verify'); }
1023
+ // #1332: drop the recorded verdict with the flag. Leaving a stale PASS
1024
+ // behind would let the next check-before-done pass on a verdict that
1025
+ // describes pre-edit code.
1026
+ if (resetVerify) { s.verifyRun = false; s.verifyOutcome = null; gates.push('verify'); }
892
1027
  if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
893
1028
  if (fp) {
894
1029
  s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
@@ -978,7 +1113,7 @@ switch (command) {
978
1113
  }
979
1114
  }
980
1115
  var missing = [];
981
- if (config.testing_gate && !s.testsRun) missing.push('tests have not run since the last code edit (run npm test, vitest, jest, pytest, or similar)');
1116
+ if (config.testing_gate && !s.testsRun) missing.push('tests have not run green since the last code edit (run npm test, vitest, jest, pytest, or similar — a run whose output reports failures does not count, #1322)');
982
1117
  if (config.simplify_gate && !s.simplifyRun) missing.push('/flo-simplify (or /distill) has not run since the last code edit');
983
1118
  if (config.learnings_gate && !s.learningsStored) missing.push('learnings have not been stored (call mcp__moflo__memory_store)');
984
1119
  if (missing.length === 0) break;
@@ -989,6 +1124,10 @@ switch (command) {
989
1124
  if (s.lastResetBy && s.lastResetBy.file) {
990
1125
  process.stderr.write('Last gate reset: ' + s.lastResetBy.file + ' (' + (s.lastResetBy.gates || []).join(', ') + ')\n');
991
1126
  }
1127
+ // #1348 — name the order, not just the missing gate. Satisfying one gate can
1128
+ // invalidate another (/flo-simplify edits code, which resets tests + verify),
1129
+ // so "what is missing" alone left callers rediscovering the sequence by trial.
1130
+ process.stderr.write(ORDER_HINT);
992
1131
  process.stderr.write('Disable per-gate via moflo.yaml:\n');
993
1132
  process.stderr.write(' gates:\n testing_gate: false\n simplify_gate: false\n learnings_gate: false\n');
994
1133
  process.exit(2);
@@ -1017,12 +1156,34 @@ switch (command) {
1017
1156
  }
1018
1157
  }
1019
1158
  var sd = readState();
1020
- if (sd.verifyRun) break;
1159
+ // #1332: gate on the OUTCOME, not on attendance. Before this, `verifyRun`
1160
+ // alone opened the gate, so a /verify returning FAIL satisfied it exactly
1161
+ // as a PASS did — a failing verdict is still a successful tool invocation.
1162
+ if (sd.verifyRun && sd.verifyOutcome === 'PASS') break;
1021
1163
  process.stderr.write('BLOCKED: gh pr create requires verification before done:\n');
1022
- process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\n');
1023
- if (sd.lastResetBy && sd.lastResetBy.file && (sd.lastResetBy.gates || []).indexOf('verify') >= 0) {
1164
+ // The four states need different remedies, so name which one applies
1165
+ // rather than emitting one message that fits none of them.
1166
+ var invalidated = sd.lastResetBy && sd.lastResetBy.file
1167
+ && (sd.lastResetBy.gates || []).indexOf('verify') >= 0;
1168
+ if (!sd.verifyRun && invalidated) {
1169
+ process.stderr.write(' - a code edit invalidated the previous verification — re-run /verify\n');
1024
1170
  process.stderr.write('Last gate reset: ' + sd.lastResetBy.file + ' (verify)\n');
1171
+ } else if (!sd.verifyRun) {
1172
+ process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\n');
1173
+ } else if (sd.verifyOutcome === 'FAIL' || sd.verifyOutcome === 'UNVERIFIED') {
1174
+ process.stderr.write(' - /verify ran and returned ' + sd.verifyOutcome + ' — fix the failing criteria, then re-run /verify\n');
1175
+ process.stderr.write(' (a FAIL is a real result, not a gate error; the PR is blocked because the change did not meet its acceptance criteria)\n');
1176
+ } else {
1177
+ // Ran, but no verdict reached the gate: /verify was invoked and never
1178
+ // recorded a structured outcome (interrupted, or it stored prose only).
1179
+ process.stderr.write(' - /verify ran but recorded no verdict — re-run it so it stores a structured result\n');
1180
+ process.stderr.write(' (Step 5 of the verify skill must pass metadata.overall to memory_store)\n');
1181
+ // #1348 — the trap this state sets: re-invoking /verify CLEARS any prior
1182
+ // verdict by design (#1332), so the obvious recovery lands right back here
1183
+ // unless Step 5 completes. Say so, rather than letting it be rediscovered.
1184
+ process.stderr.write(' Re-invoking /verify clears the prior verdict, so a re-run that skips Step 5 lands here again.\n');
1025
1185
  }
1186
+ process.stderr.write(ORDER_HINT);
1026
1187
  process.stderr.write('Disable via moflo.yaml:\n');
1027
1188
  process.stderr.write(' gates:\n verify_before_done: false\n');
1028
1189
  process.exit(2);
File without changes
File without changes
@@ -118,13 +118,14 @@ steps:
118
118
  command: "echo 'preparing...'"
119
119
 
120
120
  - name: use-<name>
121
- type: agent
121
+ type: <name> # the step command registered in Step 4
122
122
  config:
123
- prompt: |
124
- Use the <name> connector to <action-1>.
125
- Access via context.tools.execute('<name>', '<action-1>', { ... })
123
+ action: <action-1>
124
+ # ...action params
126
125
  ```
127
126
 
127
+ **Do not reach a connector from an `agent` step.** That step type has never been executable. A connector is reached from its own step command (Step 4 above), from a composite step's `tool` action, or from a custom step command.
128
+
128
129
  ---
129
130
 
130
131
  ## Building a Step Command
@@ -182,7 +182,7 @@ git commit -m "type(scope): description
182
182
 
183
183
  Closes #<issue-number>
184
184
 
185
- Co-Authored-By: moflo <noreply@motailz.com>"
185
+ Co-Authored-By: moflo <noreply@cielolimitada.com>"
186
186
  ```
187
187
 
188
188
  ### 5.1b Verify-before-done (default; skipped only with `--no-verify`)
@@ -52,7 +52,7 @@ Artifacts live at `<specs_dir>/<slug>/{spec,plan}.md` — default `.moflo/specs`
52
52
 
53
53
  Runs at step 8 of the full-mode flow, before the PR — **by default** and always under `--sdd`; `--no-verify` skips it for one run.
54
54
 
55
- **Delegate to the `/verify` skill** — `Skill({ skill: "verify" })`, passing the issue number or spec slug. It owns the mechanics (single source of truth — don't restate them here): locate the acceptance criteria (plan, else ticket) → reuse the Tests-phase run (no double verify) → map each criterion to evidence → run only uncovered checks → record its own outcome to memory (`learnings`, `verify:<slug-or-issue>`) → return a per-criterion PASS/FAIL. *Invoking* it is the point it trips `record-verify-run` and satisfies the `check-before-done` gate (describing verification in prose does not). A source edit after verifying invalidates it — re-run `/verify`. Full how-to: `.claude/skills/verify/SKILL.md`.
55
+ **Delegate to the `/verify` skill** — `Skill({ skill: "verify" })`, passing the issue number or spec slug. It owns the mechanics (single source of truth — don't restate them here): locate the acceptance criteria (plan, else ticket) → reuse the Tests-phase run (no double verify) → map each criterion to evidence → run only uncovered checks → record its own outcome to memory (`learnings`, `verify:<slug-or-issue>`) → return a per-criterion PASS/FAIL. **The verdict is the point, not the invocation** (#1332). Invoking `/verify` trips `record-verify-run`, but that alone no longer opens `check-before-done` — the gate requires the recorded verdict to be `PASS`. A run returning FAIL leaves `gh pr create` blocked, which is the intended behaviour: the change did not meet its acceptance criteria. The verdict reaches the gate from the structured record `/verify` Step 5 writes to `memory_store` under `metadata.overall` (#1328), so a run that stores prose only is treated as *no verdict* and also blocks. Describing verification in prose has never satisfied the gate. A source edit after verifying invalidates both the flag and the verdict — re-run `/verify`. Full how-to: `.claude/skills/verify/SKILL.md`.
56
56
 
57
57
  `/ward` and `/quicken` stay targeted audits, not the completion gate.
58
58
 
@@ -314,11 +314,12 @@ steps:
314
314
  output: audit-result
315
315
 
316
316
  - id: analyze-findings
317
- type: agent
317
+ type: bash
318
318
  config:
319
- prompt: |
320
- Analyze the npm audit results and filter for severity >= {args.severity}.
321
- Audit output: {scan-deps.result}
319
+ command: |
320
+ claude -p "Analyze these npm audit results and filter for severity >= {args.severity}.
321
+ Audit output: {scan-deps.stdout}"
322
+ timeout: 300000
322
323
  output: analysis
323
324
 
324
325
  - id: save-report
@@ -136,11 +136,11 @@ steps:
136
136
  output: inbox # ← stores output as "inbox"
137
137
 
138
138
  - id: process
139
- type: agent
139
+ type: bash
140
140
  config:
141
- prompt: |
142
- Emails: {inbox.emails} # ← references inbox output
143
- Total: {inbox.totalEmails}
141
+ command: |
142
+ claude -p "Emails: {inbox.emails} # ← references inbox output
143
+ Total: {inbox.totalEmails}"
144
144
  ```
145
145
 
146
146
  **Variable reference rules:**
@@ -1,31 +1,36 @@
1
1
  # `github-cli` — GitHub CLI Connector
2
2
 
3
- **Purpose:** Use this connector to execute GitHub CLI operations from agent steps or as the backing connector for the `github` step command. Choose this when you need programmatic access to GitHub issues, PRs, and repos.
3
+ **Purpose:** Use this connector as the backing connector for the `github` step command, or from a custom step command. Choose this when you need programmatic access to GitHub issues, PRs, and repos.
4
4
 
5
5
  ## Usage
6
6
 
7
+ From a spell, reach this connector through the `github` step, which delegates to it:
8
+
7
9
  ```yaml
8
- - id: list-open-bugs
9
- type: agent
10
+ - id: fetch-bug
11
+ type: github
10
12
  config:
11
- prompt: |
12
- Use the github-cli connector to find open bugs.
13
- Call context.tools.execute('github-cli', 'issue-list', {
14
- repo: 'my-org/backend-api',
15
- labels: ['bug'],
16
- state: 'open'
17
- })
13
+ action: issue-fetch
14
+ issue: 42
15
+ fields: ["number", "title", "labels", "state"]
18
16
  ```
19
17
 
18
+ > Earlier revisions of this file showed an `agent` step calling `context.tools.execute(...)` from a prompt. The `agent` step type has never been executable — use the `github` step, a composite step's `tool` action, or a custom step command.
19
+
20
20
  ## Actions
21
21
 
22
- | Action | Description |
23
- |--------|-------------|
24
- | `issue-create` | Create a new GitHub issue |
25
- | `issue-list` | List issues with optional filters |
26
- | `pr-create` | Create a pull request |
27
- | `pr-list` | List pull requests with optional filters |
28
- | `repo-view` | View repository metadata |
22
+ Taken from `VALID_ACTIONS` in `src/cli/spells/connectors/github-cli.ts`. `issue-create`, `issue-list`, `pr-list` and `repo-view` were listed here previously but have never existed.
23
+
24
+ | Action | Required params | Description |
25
+ |--------|-----------------|-------------|
26
+ | `issue-fetch` | `issue` | Fetch issue details as JSON (`fields` selects columns) |
27
+ | `issue-edit` | `issue` | Edit an existing issue |
28
+ | `pr-create` | `title` | Create a pull request |
29
+ | `pr-merge` | `pr` or `issue` | Merge a PR (`mergeMethod`: squash \| merge \| rebase) |
30
+ | `pr-find` | `head` or `search` | Find a PR by head branch or search query |
31
+ | `label` | (`issue` or `pr`) + `labels` | Add/remove labels |
32
+ | `comment` | (`issue` or `pr`) + `body` | Post a comment |
33
+ | `repo-info` | — | View repository metadata |
29
34
 
30
35
  ## Direct Usage
31
36