moflo 4.12.3 → 4.12.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
  2. package/.claude/helpers/gate-hook.mjs +33 -2
  3. package/.claude/helpers/gate.cjs +63 -6
  4. package/.claude/skills/connector-builder/SKILL.md +5 -4
  5. package/.claude/skills/fl/phases.md +1 -1
  6. package/.claude/skills/fl/sdd.md +1 -1
  7. package/.claude/skills/spell-builder/SKILL.md +5 -4
  8. package/.claude/skills/spell-builder/architecture.md +4 -4
  9. package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
  10. package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
  11. package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
  12. package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
  13. package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
  14. package/.claude/skills/verify/SKILL.md +24 -3
  15. package/bin/gate-hook.mjs +33 -2
  16. package/bin/gate.cjs +63 -6
  17. package/bin/generate-code-map.mjs +70 -17
  18. package/bin/index-all.mjs +38 -0
  19. package/bin/index-guidance.mjs +8 -41
  20. package/bin/index-patterns.mjs +2 -2
  21. package/bin/index-tests.mjs +6 -4
  22. package/bin/lib/guidance-config.mjs +119 -0
  23. package/bin/lib/index-fingerprint.mjs +0 -0
  24. package/dist/src/cli/commands/analyze.js +1 -1
  25. package/dist/src/cli/commands/claims.js +2 -2
  26. package/dist/src/cli/commands/completions.js +1 -1
  27. package/dist/src/cli/commands/deployment.js +2 -2
  28. package/dist/src/cli/commands/diagnose.js +1 -1
  29. package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
  30. package/dist/src/cli/commands/doctor.js +1 -1
  31. package/dist/src/cli/commands/embeddings.js +2 -2
  32. package/dist/src/cli/commands/github.js +1 -1
  33. package/dist/src/cli/commands/neural.js +2 -2
  34. package/dist/src/cli/commands/performance.js +2 -2
  35. package/dist/src/cli/commands/plugins.js +2 -2
  36. package/dist/src/cli/commands/providers.js +2 -2
  37. package/dist/src/cli/commands/retire.js +1 -1
  38. package/dist/src/cli/commands/route.js +1 -1
  39. package/dist/src/cli/commands/security.js +2 -2
  40. package/dist/src/cli/index.js +2 -2
  41. package/dist/src/cli/init/helpers-generator.js +38 -5
  42. package/dist/src/cli/init/moflo-yaml-template.js +11 -3
  43. package/dist/src/cli/init/settings-generator.js +10 -2
  44. package/dist/src/cli/mcp-server.js +5 -23
  45. package/dist/src/cli/mcp-tools/github-tools.js +18 -5
  46. package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
  47. package/dist/src/cli/mcp-tools/memory-tools.js +25 -4
  48. package/dist/src/cli/mcp-tools/neural-tools.js +26 -5
  49. package/dist/src/cli/mcp-tools/performance-tools.js +22 -4
  50. package/dist/src/cli/mcp-tools/security-tools.js +1 -1
  51. package/dist/src/cli/mcp-tools/synthetic.js +74 -0
  52. package/dist/src/cli/mcp-tools/system-tools.js +17 -2
  53. package/dist/src/cli/movector/flash-attention.js +1 -1
  54. package/dist/src/cli/movector/vector-db.js +1 -1
  55. package/dist/src/cli/services/daemon-lock.js +5 -2
  56. package/dist/src/cli/services/hook-block-hash.js +3 -1
  57. package/dist/src/cli/services/moflo-version.js +62 -0
  58. package/dist/src/cli/services/movector-training.js +1 -1
  59. package/dist/src/cli/services/spell-gate.js +1 -1
  60. package/dist/src/cli/spells/commands/agent-command.js +55 -25
  61. package/dist/src/cli/suggest.js +1 -1
  62. package/dist/src/cli/swarm/swarm-persistence.js +112 -0
  63. package/dist/src/cli/swarm/unified-coordinator.js +118 -3
  64. package/dist/src/cli/version.js +1 -1
  65. package/package.json +5 -3
@@ -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,11 +32,42 @@ 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
 
@@ -865,12 +865,47 @@ switch (command) {
865
865
  // crediting them would let the gate pass without an end-to-end verify.
866
866
  if (vName === 'verify') {
867
867
  var s = readState();
868
- if (!s.verifyRun) { s.verifyRun = true; writeState(s); }
868
+ // #1332: invoking /verify starts a verification; it does not conclude
869
+ // one. Clear any prior verdict so the run in progress cannot inherit the
870
+ // PASS from a previous issue and satisfy check-before-done on its own.
871
+ if (!s.verifyRun || s.verifyOutcome) {
872
+ s.verifyRun = true;
873
+ s.verifyOutcome = null;
874
+ writeState(s);
875
+ }
869
876
  } else if (vName) {
870
877
  process.stderr.write('gate: record-verify-run no-op — TOOL_INPUT_skill="' + vName + '" is not verify\n');
871
878
  }
872
879
  break;
873
880
  }
881
+ case 'record-verify-outcome': {
882
+ // #1332. Fires PostToolUse on mcp__moflo__memory_store. `record-verify-run`
883
+ // above proves a verification was ATTEMPTED; this proves how it ENDED.
884
+ //
885
+ // The verdict is read from the structured record #1328 made /verify write
886
+ // to memory_store's `metadata` — never parsed out of the prose `value`,
887
+ // which is precisely the free-text dependency #1328 removed. gate-hook.mjs
888
+ // forwards the object as JSON (see its MAX_STRUCTURED_LEN note).
889
+ var mKey = process.env.TOOL_INPUT_key || '';
890
+ if (mKey.indexOf('verify:') !== 0) break;
891
+ var rawMeta = process.env.TOOL_INPUT_metadata || '';
892
+ if (!rawMeta) {
893
+ process.stderr.write('gate: record-verify-outcome — "' + mKey + '" carries no metadata; verdict not recorded\n');
894
+ break;
895
+ }
896
+ var parsedMeta = null;
897
+ try { parsedMeta = JSON.parse(rawMeta); } catch (e) { parsedMeta = null; }
898
+ if (!parsedMeta || typeof parsedMeta !== 'object' || parsedMeta.type !== 'verify-record') break;
899
+ var overall = typeof parsedMeta.overall === 'string' ? parsedMeta.overall.toUpperCase() : '';
900
+ if (overall !== 'PASS' && overall !== 'FAIL' && overall !== 'UNVERIFIED') {
901
+ process.stderr.write('gate: record-verify-outcome — unrecognised overall="' + parsedMeta.overall + '"; treating as not-passing\n');
902
+ overall = 'UNVERIFIED';
903
+ }
904
+ var vs = readState();
905
+ vs.verifyOutcome = overall;
906
+ writeState(vs);
907
+ break;
908
+ }
874
909
  case 'reset-edit-gates': {
875
910
  var fp = process.env.TOOL_INPUT_file_path || '';
876
911
  // Inert files (markdown, lockfiles, CHANGELOG, .env.example) AND inert paths
@@ -883,12 +918,17 @@ switch (command) {
883
918
  var resetTests = s.testsRun;
884
919
  // A code edit invalidates a prior verification (Story #1274) — same as tests,
885
920
  // including test-only edits (the criteria being verified may have moved).
886
- var resetVerify = s.verifyRun;
921
+ // #1332: also fires when a verdict lingers without the flag, so no path
922
+ // can leave a recorded outcome behind after a source edit.
923
+ var resetVerify = s.verifyRun || !!s.verifyOutcome;
887
924
  var resetSimplify = s.simplifyRun && !isTestOnly;
888
925
  if (!resetTests && !resetSimplify && !resetVerify) break;
889
926
  var gates = [];
890
927
  if (resetTests) { s.testsRun = false; gates.push('tests'); }
891
- if (resetVerify) { s.verifyRun = false; gates.push('verify'); }
928
+ // #1332: drop the recorded verdict with the flag. Leaving a stale PASS
929
+ // behind would let the next check-before-done pass on a verdict that
930
+ // describes pre-edit code.
931
+ if (resetVerify) { s.verifyRun = false; s.verifyOutcome = null; gates.push('verify'); }
892
932
  if (resetSimplify) { s.simplifyRun = false; gates.push('simplify'); }
893
933
  if (fp) {
894
934
  s.lastResetBy = { file: fp, at: new Date().toISOString(), gates: gates };
@@ -1017,11 +1057,28 @@ switch (command) {
1017
1057
  }
1018
1058
  }
1019
1059
  var sd = readState();
1020
- if (sd.verifyRun) break;
1060
+ // #1332: gate on the OUTCOME, not on attendance. Before this, `verifyRun`
1061
+ // alone opened the gate, so a /verify returning FAIL satisfied it exactly
1062
+ // as a PASS did — a failing verdict is still a successful tool invocation.
1063
+ if (sd.verifyRun && sd.verifyOutcome === 'PASS') break;
1021
1064
  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) {
1065
+ // The four states need different remedies, so name which one applies
1066
+ // rather than emitting one message that fits none of them.
1067
+ var invalidated = sd.lastResetBy && sd.lastResetBy.file
1068
+ && (sd.lastResetBy.gates || []).indexOf('verify') >= 0;
1069
+ if (!sd.verifyRun && invalidated) {
1070
+ process.stderr.write(' - a code edit invalidated the previous verification — re-run /verify\n');
1024
1071
  process.stderr.write('Last gate reset: ' + sd.lastResetBy.file + ' (verify)\n');
1072
+ } else if (!sd.verifyRun) {
1073
+ process.stderr.write(' - the change has not been verified since the last code edit (run /verify)\n');
1074
+ } else if (sd.verifyOutcome === 'FAIL' || sd.verifyOutcome === 'UNVERIFIED') {
1075
+ process.stderr.write(' - /verify ran and returned ' + sd.verifyOutcome + ' — fix the failing criteria, then re-run /verify\n');
1076
+ 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');
1077
+ } else {
1078
+ // Ran, but no verdict reached the gate: /verify was invoked and never
1079
+ // recorded a structured outcome (interrupted, or it stored prose only).
1080
+ process.stderr.write(' - /verify ran but recorded no verdict — re-run it so it stores a structured result\n');
1081
+ process.stderr.write(' (Step 5 of the verify skill must pass metadata.overall to memory_store)\n');
1025
1082
  }
1026
1083
  process.stderr.write('Disable via moflo.yaml:\n');
1027
1084
  process.stderr.write(' gates:\n verify_before_done: false\n');
@@ -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
 
@@ -1,22 +1,29 @@
1
1
  # `http` — HTTP Requests
2
2
 
3
- **Purpose:** Use this connector to make HTTP requests to any URL from within agent steps. Choose this when you need to call REST APIs, fetch data, or post payloads during spell execution.
3
+ **Purpose:** Use this connector to make HTTP requests to any URL. Choose this when you need to call REST APIs, fetch data, or post payloads during spell execution.
4
4
 
5
5
  ## Usage
6
6
 
7
+ Unlike `playwright`/`github-cli`/`local-outlook`, this connector has **no dedicated step type**. Reach it from a composite (YAML) step's `tool` action:
8
+
7
9
  ```yaml
8
- - id: fetch-status
9
- type: agent
10
- config:
11
- prompt: |
12
- Use the http connector to check the deployment status.
13
- Call context.tools.execute('http', 'request', {
14
- method: 'GET',
15
- url: 'https://api.myapp.com/deploy/status',
16
- headers: { 'Authorization': 'Bearer {credentials.API_TOKEN}' }
17
- })
10
+ name: fetch-status
11
+ inputs:
12
+ token: { type: string }
13
+ actions:
14
+ - tool: http
15
+ action: request
16
+ params:
17
+ method: GET
18
+ url: "https://api.myapp.com/deploy/status"
19
+ headers:
20
+ Authorization: "Bearer ${inputs.token}"
18
21
  ```
19
22
 
23
+ …or from a custom step command via the Direct Usage API below. See `.claude/guidance/moflo-spell-custom-steps.md`.
24
+
25
+ > Earlier revisions of this file showed an `agent` step calling `context.tools.execute(...)` from a prompt. The `agent` step type has never been executable.
26
+
20
27
  ## Actions
21
28
 
22
29
  | Action | Parameters | Description |
@@ -4,18 +4,19 @@
4
4
 
5
5
  ## Usage
6
6
 
7
+ From a spell, reach this connector through the `outlook` step, which delegates to it:
8
+
7
9
  ```yaml
8
10
  - id: check-for-invoices
9
- type: agent
11
+ type: outlook
10
12
  config:
11
- prompt: |
12
- Use the local-outlook connector to search for recent invoices.
13
- Call context.tools.execute('local-outlook', 'search', {
14
- query: 'invoice from:billing@vendor.com',
15
- limit: 5
16
- })
13
+ action: search
14
+ query: "invoice from:billing@vendor.com"
15
+ limit: 5
17
16
  ```
18
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 `outlook` step, a composite step's `tool` action, or a custom step command.
19
+
19
20
  ## Actions
20
21
 
21
22
  | Action | Parameters | Description |
@@ -1,21 +1,23 @@
1
1
  # `playwright` — Browser Automation Connector
2
2
 
3
- **Purpose:** Use this connector for low-level browser automation via Playwright. Choose this when you need direct browser control from agent steps, or when building browser-based connectors like `local-outlook`.
3
+ **Purpose:** Use this connector for low-level browser automation via Playwright. Choose this when you need direct browser control from a step command, or when building browser-based connectors like `local-outlook`.
4
4
 
5
5
  ## Usage
6
6
 
7
+ From a spell, reach this connector through the `browser` step, which delegates to it:
8
+
7
9
  ```yaml
8
10
  - id: take-screenshot
9
- type: agent
11
+ type: browser
10
12
  config:
11
- prompt: |
12
- Use the playwright connector to screenshot the homepage.
13
- Call context.tools.execute('playwright', 'navigate', {
14
- url: 'https://myapp.com'
15
- })
16
- Then call context.tools.execute('playwright', 'screenshot', {})
13
+ actions:
14
+ - action: navigate
15
+ url: "https://myapp.com"
16
+ - action: screenshot
17
17
  ```
18
18
 
19
+ > 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 `browser` step, a composite step's `tool` action, or a custom step command.
20
+
19
21
  ## Actions
20
22
 
21
23
  | Action | Parameters | Description |
@@ -1,36 +1,40 @@
1
- # `agent` — Spawn a Claude Subagent
1
+ # `agent` — NOT EXECUTABLE
2
2
 
3
- **Purpose:** Use this step to delegate a task to a Claude subagent and capture its response. Choose this over `bash` when the task requires reasoning, code generation, or research rather than a deterministic command.
3
+ **Purpose:** Document that this step type does not work, so it is not offered when authoring a spell. It is registered but has never spawned a subagent moflo has no agent spawner in the spell runner. Casting it always fails.
4
4
 
5
- ## Usage
5
+ **Do not add `agent` steps to a spell.** Previously the step returned `success: true` with `result: "Agent task prepared: <type>"`, so spells containing one completed green and downstream steps consumed that string as though it were agent output. It now fails with an explanatory error instead.
6
+
7
+ The type stays registered on purpose: removing it would turn a silently-useless step into a hard parse error for any existing spell YAML that contains one.
8
+
9
+ ## Use this instead
10
+
11
+ Run the Claude CLI from a `bash` step:
6
12
 
7
13
  ```yaml
8
14
  - id: research
9
- type: agent
15
+ type: bash
10
16
  config:
11
- agentType: researcher
12
- prompt: "Find all REST API endpoints in src/ and list their HTTP methods and paths"
13
- model: claude-sonnet-5
14
- background: false
17
+ command: 'claude -p "Find all REST API endpoints in src/ and list their HTTP methods and paths"'
18
+ timeout: 300000
15
19
  ```
16
20
 
17
- ## Config
21
+ This is what moflo's own shipped spells do — see `src/cli/spells/definitions/epic-auto-merge.yaml`.
22
+
23
+ **Note on cost:** a `bash` step invoking `claude -p` has no spend ceiling, which matters most for daemon-scheduled spells. Set a `timeout` and prefer `failOnError: true`.
24
+
25
+ ## If you are reading an older spell
26
+
27
+ | Old `agent` config | Replacement |
28
+ |---|---|
29
+ | `prompt` | the prompt text inside `claude -p "..."` |
30
+ | `agentType` | no equivalent — describe the role in the prompt |
31
+ | `background` | no equivalent — the bash step waits |
18
32
 
19
- | Field | Required | Default | Description |
20
- |-------|----------|---------|-------------|
21
- | `prompt` | Yes | — | Task prompt for the agent |
22
- | `agentType` | No | `coder` | Agent specialization: `researcher`, `coder`, `tester`, `reviewer` |
23
- | `model` | No | system default | Model override (e.g. `claude-sonnet-5`) |
24
- | `systemPrompt` | No | — | Custom system prompt replacing the default |
25
- | `background` | No | `false` | Run without waiting for the result |
33
+ `model` and `systemPrompt` were documented here previously but were never implemented — the step's `configSchema` only ever accepted `prompt`, `agentType`, and `background`.
26
34
 
27
35
  ## Outputs
28
36
 
29
- | Field | Type | Description |
30
- |-------|------|-------------|
31
- | `result` | string | Agent response text |
32
- | `agentType` | string | Agent type that was used |
33
- | `prompt` | string | Prompt that was sent |
37
+ None. The step always fails. `agentType` and `prompt` are echoed in the failure data for diagnosis only; there is no `result`.
34
38
 
35
39
  ## Source
36
40
 
@@ -14,7 +14,9 @@ Prove the current change **actually does what it was supposed to** before it shi
14
14
 
15
15
  ## What satisfies the gate
16
16
 
17
- Invoking this skill (name `verify`) trips the `record-verify-run` hook, which flips the `verifyRun` state the `check-before-done` gate reads. **Only `/verify` satisfies it** `/ward` and `/quicken` are targeted audits, not an end-to-end verification. A source edit *after* verifying invalidates the result (the edit gate resets `verifyRun`), so run `/verify` as the last step before the PR.
17
+ Invoking this skill (name `verify`) trips the `record-verify-run` hook, which flips the `verifyRun` state. **That alone does not open the gate** (#1332): `check-before-done` also requires the recorded verdict to be `PASS`, which reaches it from the `metadata.overall` your Step 5 store writes. So a run returning FAIL leaves `gh pr create` blocked as it should, since the change did not meet its criteria — and a run that stores prose without `metadata` counts as *no verdict* and blocks too.
18
+
19
+ **Only `/verify` satisfies it** — `/ward` and `/quicken` are targeted audits, not an end-to-end verification. A source edit *after* verifying invalidates both the flag and the verdict, so run `/verify` as the last step before the PR.
18
20
 
19
21
  ## Step 0 — Memory first
20
22
 
@@ -71,17 +73,36 @@ Overall verdict is PASS iff no row is FAIL or UNVERIFIED.
71
73
 
72
74
  ## Step 5 — Record the outcome to memory
73
75
 
74
- Always store the result (feeds routing/learning and the SDD trail):
76
+ Always store the result (feeds routing/learning and the SDD trail). Store it **twice over**: the prose summary in `value`, and the Step 4 table itself — unflattened — in `metadata`.
75
77
 
76
78
  ```
77
79
  mcp__moflo__memory_store {
78
80
  namespace: "learnings",
79
81
  key: "verify:<slug-or-issue>",
80
82
  value: "<overall PASS/FAIL> — per-criterion: <criterion → evidence → verdict>; commit <sha>",
81
- tags: ["verify", "sdd"]
83
+ tags: ["verify", "sdd"],
84
+ metadata: {
85
+ type: "verify-record",
86
+ issue: "<slug-or-issue>",
87
+ commit: "<sha>",
88
+ overall: "PASS" | "FAIL" | "UNVERIFIED",
89
+ verifiedAt: "<ISO-8601>",
90
+ criteria: [
91
+ { id: 1, statement: "<criterion>", verdict: "PASS" | "FAIL" | "UNVERIFIED",
92
+ evidence: "<test name / observed output>", freshlyExecuted: true }
93
+ ]
94
+ }
82
95
  }
83
96
  ```
84
97
 
98
+ **Why both.** `value` is what gets embedded for semantic search, so it stays prose — a JSON blob there would degrade every future `learnings` search. `metadata` is stored verbatim and not embedded, so the structure survives without that cost. `memory_retrieve` returns the parsed `metadata` object for non-chunk entries, which is what makes the record readable rather than write-only.
99
+
100
+ **`freshlyExecuted` is required on every criterion, not optional.** Step 3 deliberately permits *citing* an earlier green run instead of re-executing. That is a sound cost optimisation, but it means evidence may be a citation rather than a fresh result — set `freshlyExecuted: false` when you cited. Without that flag a later reader silently inherits stale evidence and cannot tell a re-verified criterion from a re-cited one.
101
+
102
+ **Never record an exit code.** Claude Code's `tool_response` for Bash carries `stdout`/`stderr` but **no exit status**, and PostToolUse does not fire at all when a command exits non-zero (#1322). `evidence` is therefore descriptive by necessity; a field named `exitCode` would be agent-narrated fiction, which is the problem this record exists to remove.
103
+
104
+ `overall` MUST agree with the Step 4 rule — PASS iff no criterion is FAIL or UNVERIFIED. Record a FAIL as a FAIL; the store is the audit trail, and a verdict that only ever reads PASS is worth nothing.
105
+
85
106
  ## Step 6 — Report
86
107
 
87
108
  One concise summary: overall verdict, the per-criterion table, and — on FAIL/UNVERIFIED — exactly which criteria are unproven and what evidence is missing. Do **not** open the PR or edit code from here; hand the verdict back to the caller.
package/bin/gate-hook.mjs CHANGED
@@ -32,11 +32,42 @@ 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