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.
- package/.claude/agents/core/coder.md +2 -3
- package/.claude/agents/core/researcher.md +3 -4
- package/.claude/agents/core/reviewer.md +6 -8
- package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
- package/.claude/helpers/gate-hook.mjs +67 -2
- package/.claude/helpers/gate.cjs +173 -12
- package/.claude/helpers/hook-handler.cjs +0 -0
- package/.claude/helpers/prompt-hook.mjs +0 -0
- package/.claude/skills/connector-builder/SKILL.md +5 -4
- package/.claude/skills/fl/phases.md +1 -1
- package/.claude/skills/fl/sdd.md +1 -1
- package/.claude/skills/spell-builder/SKILL.md +5 -4
- package/.claude/skills/spell-builder/architecture.md +4 -4
- package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
- package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
- package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
- package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
- package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
- package/.claude/skills/verify/SKILL.md +24 -3
- package/bin/gate-hook.mjs +67 -2
- package/bin/gate.cjs +173 -12
- package/bin/generate-code-map.mjs +70 -17
- package/bin/index-all.mjs +38 -0
- package/bin/index-guidance.mjs +8 -41
- package/bin/index-patterns.mjs +2 -2
- package/bin/index-tests.mjs +6 -4
- package/bin/lib/guidance-config.mjs +119 -0
- package/bin/lib/index-fingerprint.mjs +0 -0
- package/dist/src/cli/commands/agent.js +1 -89
- package/dist/src/cli/commands/analyze.js +1 -1
- package/dist/src/cli/commands/benchmark.js +11 -8
- package/dist/src/cli/commands/claims.js +2 -2
- package/dist/src/cli/commands/completions.js +1 -1
- package/dist/src/cli/commands/config.js +256 -96
- package/dist/src/cli/commands/deployment.js +6 -6
- package/dist/src/cli/commands/diagnose.js +1 -1
- package/dist/src/cli/commands/doctor-checks-config.js +18 -22
- package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
- package/dist/src/cli/commands/doctor-fixes.js +36 -11
- package/dist/src/cli/commands/doctor.js +1 -1
- package/dist/src/cli/commands/embeddings.js +64 -23
- package/dist/src/cli/commands/github.js +1 -1
- package/dist/src/cli/commands/guidance.js +10 -10
- package/dist/src/cli/commands/hive-mind.js +8 -70
- package/dist/src/cli/commands/hooks.js +32 -219
- package/dist/src/cli/commands/mcp.js +5 -2
- package/dist/src/cli/commands/memory.js +59 -46
- package/dist/src/cli/commands/neural.js +21 -21
- package/dist/src/cli/commands/performance.js +14 -7
- package/dist/src/cli/commands/plugins.js +2 -2
- package/dist/src/cli/commands/providers.js +2 -2
- package/dist/src/cli/commands/retire.js +1 -1
- package/dist/src/cli/commands/route.js +1 -1
- package/dist/src/cli/commands/security.js +4 -4
- package/dist/src/cli/commands/status.js +18 -13
- package/dist/src/cli/commands/task.js +8 -0
- package/dist/src/cli/config/cli-config-store.js +326 -0
- package/dist/src/cli/hooks/workers/index.js +15 -9
- package/dist/src/cli/index.js +43 -33
- package/dist/src/cli/init/claudemd-generator.js +3 -3
- package/dist/src/cli/init/helpers-generator.js +203 -20
- package/dist/src/cli/init/moflo-yaml-template.js +11 -3
- package/dist/src/cli/init/settings-generator.js +10 -2
- package/dist/src/cli/mcp-client.js +12 -2
- package/dist/src/cli/mcp-server.js +5 -23
- package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
- package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
- package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
- package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
- package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
- package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
- package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
- package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
- package/dist/src/cli/mcp-tools/security-tools.js +1 -1
- package/dist/src/cli/mcp-tools/session-tools.js +116 -0
- package/dist/src/cli/mcp-tools/synthetic.js +74 -0
- package/dist/src/cli/mcp-tools/system-tools.js +126 -78
- package/dist/src/cli/mcp-tools/task-tools.js +101 -1
- package/dist/src/cli/movector/coverage-router.js +48 -6
- package/dist/src/cli/movector/diff-classifier.js +8 -6
- package/dist/src/cli/movector/flash-attention.js +1 -1
- package/dist/src/cli/movector/vector-db.js +1 -1
- package/dist/src/cli/parser.js +81 -12
- package/dist/src/cli/services/daemon-lock.js +5 -2
- package/dist/src/cli/services/hook-block-hash.js +3 -1
- package/dist/src/cli/services/moflo-version.js +62 -0
- package/dist/src/cli/services/movector-training.js +1 -1
- package/dist/src/cli/services/spell-gate.js +6 -4
- package/dist/src/cli/services/worker-daemon.js +37 -7
- package/dist/src/cli/shared/utils/load-average.js +59 -0
- package/dist/src/cli/spells/commands/agent-command.js +55 -25
- package/dist/src/cli/suggest.js +1 -1
- package/dist/src/cli/swarm/swarm-persistence.js +112 -0
- package/dist/src/cli/swarm/unified-coordinator.js +118 -3
- package/dist/src/cli/version.js +1 -1
- package/package.json +5 -3
- package/dist/src/cli/mcp-tools/github-tools.js +0 -318
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
# `http` — HTTP Requests
|
|
2
2
|
|
|
3
|
-
**Purpose:** Use this connector to make HTTP requests to any URL
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
11
|
+
type: outlook
|
|
10
12
|
config:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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:
|
|
11
|
+
type: browser
|
|
10
12
|
config:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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` —
|
|
1
|
+
# `agent` — NOT EXECUTABLE
|
|
2
2
|
|
|
3
|
-
**Purpose:**
|
|
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
|
-
|
|
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:
|
|
15
|
+
type: bash
|
|
10
16
|
config:
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
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,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
|
-
|
|
38
|
-
|
|
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');
|
package/bin/gate.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
641
|
-
//
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
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);
|